2024-01-14 21:44:12 +00:00

187 lines
4.9 KiB
C#

using System.Text;
using System.Text.RegularExpressions;
using Newtonsoft.Json;
using Nostr.Client.Json;
using Nostr.Client.Messages;
using Nostr.Client.Utils;
public enum LNURLErrorCode
{
ServiceUnavailable = 1,
InvalidLNURL = 2,
}
public class LNURLError : Exception
{
public LNURLErrorCode Code { get; private set; }
public LNURLError(LNURLErrorCode code, string message) : base(message)
{
Code = code;
}
}
public class Lnurl
{
private readonly HttpClient _client;
public Lnurl(HttpClient client)
{
_client = client;
_client.Timeout = TimeSpan.FromSeconds(60);
}
public static Uri? ParseLnUrl(string lnurl)
{
var emailRegex =
new Regex(
"^(([^<>()\\[\\]\\\\.,;:\\s@\"]+(\\.[^<>()\\[\\]\\\\.,;:\\s@\"]+)*)|(\".+\"))@((\\[[0-9]{1,3}\\.[0-9]{1,3}\\.[0-9]{1,3}\\.[0-9]{1,3}])|(([a-zA-Z\\-0-9]+\\.)+[a-zA-Z]{2,}))$");
lnurl = lnurl.ToLower().Trim();
if (lnurl.StartsWith("lnurlp:"))
{
return new Uri(lnurl.Replace("lnurlp://", "https://"));
}
if (lnurl.StartsWith("lnurl"))
{
Bech32.Decode(lnurl, out var hrp, out var decoded);
var decodedString = Encoding.UTF8.GetString(decoded!);
if (!decodedString.StartsWith("http"))
{
return default;
}
return new Uri(decodedString);
}
if (emailRegex.IsMatch(lnurl))
{
var parts = lnurl.Split('@');
return new Uri($"https://{parts[1]}/.well-known/lnurlp/{parts[0]}");
}
if (lnurl.StartsWith("https:"))
{
return new Uri(lnurl);
}
return default;
}
public async Task<LNURLService?> LoadAsync(string lnurl)
{
var url = ParseLnUrl(lnurl);
var response = await _client.GetAsync(url);
if (response.IsSuccessStatusCode)
{
var json = await response.Content.ReadAsStringAsync();
return JsonConvert.DeserializeObject<LNURLService>(json)!;
}
return default;
}
public async Task<LNURLInvoice> GetInvoiceAsync(LNURLService service, int amount, string? comment = null, NostrEvent? zap = null)
{
if (service == null || string.IsNullOrWhiteSpace(service.Callback))
{
throw new LNURLError(LNURLErrorCode.InvalidLNURL, "No callback url");
}
var query = new Dictionary<string, string>
{
["amount"] = (amount * 1000).ToString()
};
if (!string.IsNullOrWhiteSpace(comment) && service.CommentAllowed.HasValue)
{
query["comment"] = comment;
}
if (!string.IsNullOrWhiteSpace(service.NostrPubkey) && zap != null)
{
query["nostr"] = JsonConvert.SerializeObject(zap, NostrSerializer.Settings);
}
var builder = new UriBuilder(service.Callback)
{
Query = string.Join('&', query.Select(a => $"{a.Key}={Uri.EscapeDataString(a.Value)}"))
};
try
{
var response = await _client.GetAsync(builder.Uri);
if (response.IsSuccessStatusCode)
{
var json = await response.Content.ReadAsStringAsync();
var data = JsonConvert.DeserializeObject<LNURLInvoice>(json);
if (data?.Status == "ERROR")
{
throw new Exception(data.Reason);
}
return data!;
}
throw new LNURLError(LNURLErrorCode.ServiceUnavailable, $"Failed to fetch invoice ({response.ReasonPhrase})");
}
catch (Exception e)
{
throw new LNURLError(LNURLErrorCode.ServiceUnavailable, $"Failed to load callback: {e.Message}");
}
}
}
public class LNURLService
{
[JsonProperty("tag")]
public string? Tag { get; init; }
[JsonProperty("nostrPubkey")]
public string? NostrPubkey { get; init; }
[JsonProperty("minSendable")]
public int? MinSendable { get; init; }
[JsonProperty("maxSnedable")]
public int? MaxSendable { get; init; }
[JsonProperty("metadata")]
public string Metadata { get; init; } = null!;
[JsonProperty("callback")]
public string Callback { get; init; } = null!;
[JsonProperty("commentAllowed")]
public int? CommentAllowed { get; init; }
}
public class LNURLStatus
{
[JsonProperty("status")]
public string Status { get; set; } = null!;
[JsonProperty("reason")]
public string? Reason { get; set; }
}
public class LNURLInvoice : LNURLStatus
{
[JsonProperty("pr")]
public string Pr { get; set; } = null!;
[JsonProperty("successAction")]
public LNURLSuccessAction? SuccessAction { get; set; }
}
public class LNURLSuccessAction
{
[JsonProperty("description")]
public string? Description { get; set; }
[JsonProperty("url")]
public string? Url { get; set; }
}