187 lines
4.9 KiB
C#
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; }
|
|
}
|