From d1cb6193166434b1960792a9fd93d3664caa1252 Mon Sep 17 00:00:00 2001 From: Kieran Date: Sat, 13 Jan 2024 20:07:27 +0000 Subject: [PATCH] feat: zap2boost --- .gitignore | 4 +- .idea/.idea.NostrServices/.idea/.gitignore | 13 ++ .../NostrServices.Client.csproj | 13 ++ NostrServices.Client/NostrServicesClient.cs | 35 ++++ NostrServices.sln | 12 ++ .../Controllers/OpenGraphController.cs | 36 +--- PayForReactions/AlbyApi.cs | 45 +++++ PayForReactions/Config.cs | 17 ++ PayForReactions/Dockerfile | 21 ++ PayForReactions/LnUrl.cs | 187 ++++++++++++++++++ PayForReactions/NostrStore.cs | 44 +++++ PayForReactions/PayForReactions.csproj | 25 +++ PayForReactions/Program.cs | 38 ++++ .../Properties/launchSettings.json | 12 ++ PayForReactions/ZapperRelay.cs | 155 +++++++++++++++ PayForReactions/appsettings.Development.json | 8 + 16 files changed, 630 insertions(+), 35 deletions(-) create mode 100644 .idea/.idea.NostrServices/.idea/.gitignore create mode 100644 NostrServices.Client/NostrServices.Client.csproj create mode 100644 NostrServices.Client/NostrServicesClient.cs create mode 100644 PayForReactions/AlbyApi.cs create mode 100644 PayForReactions/Config.cs create mode 100644 PayForReactions/Dockerfile create mode 100644 PayForReactions/LnUrl.cs create mode 100644 PayForReactions/NostrStore.cs create mode 100644 PayForReactions/PayForReactions.csproj create mode 100644 PayForReactions/Program.cs create mode 100644 PayForReactions/Properties/launchSettings.json create mode 100644 PayForReactions/ZapperRelay.cs create mode 100644 PayForReactions/appsettings.Development.json diff --git a/.gitignore b/.gitignore index add57be..1d87522 100644 --- a/.gitignore +++ b/.gitignore @@ -2,4 +2,6 @@ bin/ obj/ /packages/ riderModule.iml -/_ReSharper.Caches/ \ No newline at end of file +/_ReSharper.Caches/ +appsettings.json +data/ \ No newline at end of file diff --git a/.idea/.idea.NostrServices/.idea/.gitignore b/.idea/.idea.NostrServices/.idea/.gitignore new file mode 100644 index 0000000..c2586fb --- /dev/null +++ b/.idea/.idea.NostrServices/.idea/.gitignore @@ -0,0 +1,13 @@ +# Default ignored files +/shelf/ +/workspace.xml +# Rider ignored files +/projectSettingsUpdater.xml +/.idea.NostrServices.iml +/contentModel.xml +/modules.xml +# Editor-based HTTP Client requests +/httpRequests/ +# Datasource local storage ignored files +/dataSources/ +/dataSources.local.xml diff --git a/NostrServices.Client/NostrServices.Client.csproj b/NostrServices.Client/NostrServices.Client.csproj new file mode 100644 index 0000000..a9c778a --- /dev/null +++ b/NostrServices.Client/NostrServices.Client.csproj @@ -0,0 +1,13 @@ + + + + net8.0 + enable + enable + + + + + + + diff --git a/NostrServices.Client/NostrServicesClient.cs b/NostrServices.Client/NostrServicesClient.cs new file mode 100644 index 0000000..4c2fbb5 --- /dev/null +++ b/NostrServices.Client/NostrServicesClient.cs @@ -0,0 +1,35 @@ +using Newtonsoft.Json; + +namespace NostrServices.Client; + +public class NostrServicesClient +{ + private readonly HttpClient _client; + + public NostrServicesClient(HttpClient client) + { + _client = client; + _client.BaseAddress = new Uri("https://nostr.api.v0l.io"); + } + + public async Task Profile(string id) + { + var rsp = await _client.GetAsync($"/api/v1/export/profile/{id}"); + if (rsp.IsSuccessStatusCode) + { + var json = await rsp.Content.ReadAsStringAsync(); + return JsonConvert.DeserializeObject(json); + } + + return default; + } + + public class NostrProfile + { + [JsonProperty("name")] + public string? Name { get; init; } + + [JsonProperty("lud16")] + public string? LightningAddress { get; init; } + } +} diff --git a/NostrServices.sln b/NostrServices.sln index be0a416..4ffb49f 100644 --- a/NostrServices.sln +++ b/NostrServices.sln @@ -15,6 +15,10 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FASTERlay", "FASTERlay\FAST EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BasicTests", "BasicTests\BasicTests.csproj", "{1F9E8BD2-38B6-4FA4-9FAA-7376759C2CBB}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PayForReactions", "PayForReactions\PayForReactions.csproj", "{18CFBD86-576C-4185-82E4-A04866E2ED22}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NostrServices.Client", "NostrServices.Client\NostrServices.Client.csproj", "{17CA7036-95BC-4851-BAB1-419996EA2761}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -37,5 +41,13 @@ Global {1F9E8BD2-38B6-4FA4-9FAA-7376759C2CBB}.Debug|Any CPU.Build.0 = Debug|Any CPU {1F9E8BD2-38B6-4FA4-9FAA-7376759C2CBB}.Release|Any CPU.ActiveCfg = Release|Any CPU {1F9E8BD2-38B6-4FA4-9FAA-7376759C2CBB}.Release|Any CPU.Build.0 = Release|Any CPU + {18CFBD86-576C-4185-82E4-A04866E2ED22}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {18CFBD86-576C-4185-82E4-A04866E2ED22}.Debug|Any CPU.Build.0 = Debug|Any CPU + {18CFBD86-576C-4185-82E4-A04866E2ED22}.Release|Any CPU.ActiveCfg = Release|Any CPU + {18CFBD86-576C-4185-82E4-A04866E2ED22}.Release|Any CPU.Build.0 = Release|Any CPU + {17CA7036-95BC-4851-BAB1-419996EA2761}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {17CA7036-95BC-4851-BAB1-419996EA2761}.Debug|Any CPU.Build.0 = Debug|Any CPU + {17CA7036-95BC-4851-BAB1-419996EA2761}.Release|Any CPU.ActiveCfg = Release|Any CPU + {17CA7036-95BC-4851-BAB1-419996EA2761}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection EndGlobal diff --git a/NostrServices/Controllers/OpenGraphController.cs b/NostrServices/Controllers/OpenGraphController.cs index 60dc6e2..c38ffd2 100644 --- a/NostrServices/Controllers/OpenGraphController.cs +++ b/NostrServices/Controllers/OpenGraphController.cs @@ -59,9 +59,8 @@ public class OpenGraphController : Controller } } - if ((NostrIdentifierParser.TryParse(id, out var nid) && nid != default) || - (nid = NostrBareIdentifier.Parse(id)) != default || - (nid = await TryParseNip05(id, cts)) != default) + var nid = await Extensions.TryParseIdentifier(id); + if (nid != default) { try { @@ -221,37 +220,6 @@ public class OpenGraphController : Controller } record HeadElement(string Element, List> Attributes); - - private async Task TryParseNip05(string id, CancellationToken cts) - { - try - { - if (id.Contains("@")) - { - var idSplit = id.Split("@"); - var url = new Uri($"https://{idSplit[1]}/.well-known/nostr.json?name={Uri.EscapeDataString(idSplit[0])}"); - var json = await _httpClient.GetStringAsync(url, cts); - var parsed = JsonConvert.DeserializeObject(json); - var match = parsed?.Names?.FirstOrDefault(a => a.Key.Equals(idSplit[0], StringComparison.CurrentCultureIgnoreCase)); - if (match.HasValue && !string.IsNullOrEmpty(match.Value.Value)) - { - return new NostrProfileIdentifier(match.Value.Value.ToLower(), null); - } - } - } - catch (Exception ex) - { - _logger.LogWarning("Failed to parse nostr address {id} {msg}", id, ex.Message); - } - - return default; - } - - class NostrJson - { - [JsonProperty("names")] - public Dictionary? Names { get; init; } = new(); - } } [ProtoContract] diff --git a/PayForReactions/AlbyApi.cs b/PayForReactions/AlbyApi.cs new file mode 100644 index 0000000..44c2685 --- /dev/null +++ b/PayForReactions/AlbyApi.cs @@ -0,0 +1,45 @@ +using System.Net.Http.Headers; +using Newtonsoft.Json; + +namespace PayForReactions; + +public class AlbyApi +{ + private readonly HttpClient _client; + + public AlbyApi(HttpClient client, Config config) + { + _client = client; + _client.BaseAddress = new("https://api.getalby.com"); + _client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue(config.Alby.Type, config.Alby.Token); + } + + public async Task GetBalance() + { + var json = await _client.GetStringAsync("/balance"); + var obj = JsonConvert.DeserializeObject(json); + return (long?)obj?.Balance ?? 0L; + } + + public async Task PayInvoice(string pr) + { + var rsp = await _client.PostAsync("/payments/bolt11", new StringContent(JsonConvert.SerializeObject(new + { + invoice = pr + }), new MediaTypeHeaderValue("application/json"))); + + return rsp.IsSuccessStatusCode; + } + + class BalanceResponse + { + [JsonProperty("balance")] + public decimal Balance { get; init; } + + [JsonProperty("currency")] + public string Currency { get; init; } = "BTC"; + + [JsonProperty("unit")] + public string Unit { get; init; } = "sat"; + } +} diff --git a/PayForReactions/Config.cs b/PayForReactions/Config.cs new file mode 100644 index 0000000..41e858e --- /dev/null +++ b/PayForReactions/Config.cs @@ -0,0 +1,17 @@ +namespace PayForReactions; + +public class Config +{ + public OAuthToken Alby { get; init; } = null!; + + public string PrivateKey { get; init; } = null!; + + public string Target { get; init; } = null!; +} + +public class OAuthToken +{ + public string Token { get; init; } = null!; + public string Type { get; init; } = "Bearer"; + public string RefreshToken { get; init; } = null!; +} diff --git a/PayForReactions/Dockerfile b/PayForReactions/Dockerfile new file mode 100644 index 0000000..82555ad --- /dev/null +++ b/PayForReactions/Dockerfile @@ -0,0 +1,21 @@ +FROM mcr.microsoft.com/dotnet/runtime:8.0 AS base +USER $APP_UID +WORKDIR /app + +FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build +ARG BUILD_CONFIGURATION=Release +WORKDIR /src +COPY ["PayForReactions/PayForReactions.csproj", "PayForReactions/"] +RUN dotnet restore "PayForReactions/PayForReactions.csproj" +COPY . . +WORKDIR "/src/PayForReactions" +RUN dotnet build "PayForReactions.csproj" -c $BUILD_CONFIGURATION -o /app/build + +FROM build AS publish +ARG BUILD_CONFIGURATION=Release +RUN dotnet publish "PayForReactions.csproj" -c $BUILD_CONFIGURATION -o /app/publish /p:UseAppHost=false + +FROM base AS final +WORKDIR /app +COPY --from=publish /app/publish . +ENTRYPOINT ["dotnet", "PayForReactions.dll"] diff --git a/PayForReactions/LnUrl.cs b/PayForReactions/LnUrl.cs new file mode 100644 index 0000000..427cfd3 --- /dev/null +++ b/PayForReactions/LnUrl.cs @@ -0,0 +1,187 @@ +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; + } + + 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 LoadAsync(string lnurl) + { + var url = ParseLnUrl(lnurl); + using var httpClient = new HttpClient(); + var response = await httpClient.GetAsync(url); + if (response.IsSuccessStatusCode) + { + var json = await response.Content.ReadAsStringAsync(); + return JsonConvert.DeserializeObject(json)!; + } + + return default; + } + + public async Task 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 + { + ["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 + { + using var httpClient = new HttpClient(); + var response = await httpClient.GetAsync(builder.Uri); + if (response.IsSuccessStatusCode) + { + var json = await response.Content.ReadAsStringAsync(); + var data = JsonConvert.DeserializeObject(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"); + } + } +} + +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; } +} diff --git a/PayForReactions/NostrStore.cs b/PayForReactions/NostrStore.cs new file mode 100644 index 0000000..06b22b2 --- /dev/null +++ b/PayForReactions/NostrStore.cs @@ -0,0 +1,44 @@ +using FASTER.core; + +namespace PayForReactions; + +public class NostrStore : IDisposable +{ + private readonly ILogger _logger; + private readonly CancellationTokenSource _cts = new(); + private readonly FasterKVSettings _storeSettings; + private readonly FasterKV _store; + + private readonly Task _checkPointer; + + public NostrStore(string dir, ILogger logger) + { + _logger = logger; + _storeSettings = new FasterKVSettings(Path.Combine(dir, "main"), false, _logger); + _store = new(_storeSettings); + var version = _store.GetLatestCheckpointVersion(); + if (version != -1) + { + _store.Recover(); + } + + _checkPointer = Task.Factory.StartNew(async () => + { + while (!_cts.IsCancellationRequested) + { + await Task.Delay(10_000); + await _store.TakeHybridLogCheckpointAsync(CheckpointType.Snapshot, true); + } + }); + } + public FasterKV MainStore => _store; + + public void Dispose() + { + _cts.Cancel(); + _checkPointer.ConfigureAwait(false).GetAwaiter().GetResult(); + + _store.Dispose(); + _storeSettings.Dispose(); + } +} diff --git a/PayForReactions/PayForReactions.csproj b/PayForReactions/PayForReactions.csproj new file mode 100644 index 0000000..b4d0344 --- /dev/null +++ b/PayForReactions/PayForReactions.csproj @@ -0,0 +1,25 @@ + + + + net8.0 + enable + enable + Linux + + + + + + + + + + .dockerignore + + + + + + + + diff --git a/PayForReactions/Program.cs b/PayForReactions/Program.cs new file mode 100644 index 0000000..bbede7c --- /dev/null +++ b/PayForReactions/Program.cs @@ -0,0 +1,38 @@ +using NostrRelay; +using NostrServices.Client; +using PayForReactions; + +var builder = WebApplication.CreateBuilder(args); + +var config = builder.Configuration.GetSection("Config").Get()!; +builder.Services.AddSingleton(config); + +builder.Services.AddHttpClient(); +builder.Services.AddTransient(); +builder.Services.AddTransient(); +builder.Services.AddTransient(); +builder.Services.AddTransient(); +builder.Services.AddSingleton(svc => +{ + var logger = svc.GetRequiredService>(); + return new NostrStore("./data", logger); +}); + +var host = builder.Build(); + +host.UseWebSockets(); +host.MapRelay("/", new RelayDocument() +{ + Name = "wss://reactions.v0l.io", + Description = "Zapping server for Kieran's paid reactions", + SupportedNips = [1], + Software = "PayForReactions (https://git.v0l.io/Kieran/NostrServices)", + Limitation = new() + { + RestrictedWrites = true, + AuthRequired = false, + PaymentRequired = false + } +}); + +host.Run(); diff --git a/PayForReactions/Properties/launchSettings.json b/PayForReactions/Properties/launchSettings.json new file mode 100644 index 0000000..b7a7ac4 --- /dev/null +++ b/PayForReactions/Properties/launchSettings.json @@ -0,0 +1,12 @@ +{ + "$schema": "http://json.schemastore.org/launchsettings.json", + "profiles": { + "PayForReactions": { + "commandName": "Project", + "dotnetRunMessages": true, + "environmentVariables": { + "DOTNET_ENVIRONMENT": "Development" + } + } + } +} diff --git a/PayForReactions/ZapperRelay.cs b/PayForReactions/ZapperRelay.cs new file mode 100644 index 0000000..97ec084 --- /dev/null +++ b/PayForReactions/ZapperRelay.cs @@ -0,0 +1,155 @@ +using FASTER.core; +using Nostr.Client.Keys; +using Nostr.Client.Messages; +using Nostr.Client.Requests; +using NostrRelay; +using NostrServices.Client; +using EventSession = + FASTER.core.ClientSession>; + +namespace PayForReactions; + +public class ZapperRelay : INostrRelay, IDisposable +{ + private readonly Config _config; + private readonly Lnurl _lnurl; + private readonly AlbyApi _albyApi; + private readonly NostrServicesClient _nostrServices; + private readonly EventSession _session; + + public ZapperRelay(Lnurl lnurl, AlbyApi albyApi, NostrServicesClient nostrServices, Config config, NostrStore store) + { + _lnurl = lnurl; + _albyApi = albyApi; + _nostrServices = nostrServices; + _config = config; + _session = store.MainStore.For(new SimpleFunctions()).NewSession>(); + } + + public ValueTask AcceptConnection(NostrClientContext context) + { + return ValueTask.FromResult(true); + } + + public async IAsyncEnumerable HandleRequest(NostrClientContext context, NostrRequest req) + { + yield break; + } + + public async ValueTask HandleEvent(NostrClientContext context, NostrEvent ev) + { + var target = NostrPublicKey.FromBech32(_config.Target).Hex; + if (ev.Pubkey == target) + { + var id = Convert.FromHexString(ev.Id!); + var obj = NostrBuf.Encode(ev); + (await _session.UpsertAsync(ref id, ref obj)).Complete(); + return new(true, ""); + } + + var amount = MapKindToAmount(ev.Kind); + if (amount != default) + { + var eTag = ev.Tags?.FirstOrDefault(a => a.TagIdentifier == "e"); + var pTag = ev.Tags?.FirstOrDefault(a => a.TagIdentifier == "p" && a.AdditionalData[0] == target); + if (pTag == default || eTag == default) + { + return new(false, $"blocked: must be a reaction to {target}"); + } + + var idBytes = Convert.FromHexString(eTag.AdditionalData[0]); + var refEventResult = (await _session.ReadAsync(ref idBytes)).Complete(); + if (refEventResult.status.NotFound) + { + return new(false, "blocked: parent event not found"); + } + + var seenId = Convert.FromHexString(ev.Id!); + var seenIdResult = (await _session.ReadAsync(ref seenId)).Complete(); + if (seenIdResult.status.Found) + { + return new(false, "blocked: already zapped this one, how dare you!"); + } + + var refEvent = NostrBuf.Decode(refEventResult.output); + if (refEvent == default) + { + return new(false, "blocked: parent event not found"); + } + + if (refEvent.Pubkey != target) + { + return new(false, $"blocked: parent event not posted by {target}"); + } + + var sender = ev.Pubkey!; + var senderProfile = await _nostrServices.Profile(NostrPublicKey.FromHex(sender).Bech32); + if (senderProfile == default) + { + return new(false, "blocked: couldn't find your profile anon!"); + } + + var parsedTarget = Lnurl.ParseLnUrl(senderProfile.LightningAddress ?? ""); + if (parsedTarget == default) + { + return new(false, + $"blocked: so sad... couldn't send a zap because you don't have a lightning address in your profile {senderProfile.Name}!"); + } + + var svc = await _lnurl.LoadAsync(parsedTarget.ToString()); + if (svc == default) + { + return new(false, "blocked: wallet is down, no zap for you!"); + } + + var key = NostrPrivateKey.FromBech32(_config.PrivateKey); + var myPubkey = key.DerivePublicKey().Hex; + var zap = new NostrEvent + { + Kind = NostrKind.ZapRequest, + Content = "Thanks for your interaction!", + CreatedAt = DateTime.UtcNow, + Pubkey = myPubkey, + Tags = new NostrEventTags( + new NostrEventTag("e", ev.Id!), + new NostrEventTag("p", sender), + new NostrEventTag("amount", (amount * 1000).ToString()!) + ) + }; + + var zapSigned = zap.Sign(key); + var invoice = await _lnurl.GetInvoiceAsync(svc, 5, "Thanks for your interaction!", zapSigned); + if (string.IsNullOrEmpty(invoice.Pr)) + { + return new(false, $"blocked: failed to get invoice from {parsedTarget}"); + } + + if (!await _albyApi.PayInvoice(invoice.Pr)) + { + return new(false, "blocked: failed to pay invoice!"); + } + + var seenEvent = NostrBuf.Encode(ev); + (await _session.UpsertAsync(ref seenId, ref seenEvent)).Complete(); + + return new(true, "Zapped!"); + } + + return new(false, "blocked: kind not accepted, no zap for you!"); + } + + public void Dispose() + { + } + + private int? MapKindToAmount(NostrKind k) + { + switch (k) + { + case NostrKind.Reaction: return 5; + case NostrKind.GenericRepost: return 10; + } + + return default; + } +} diff --git a/PayForReactions/appsettings.Development.json b/PayForReactions/appsettings.Development.json new file mode 100644 index 0000000..b2dcdb6 --- /dev/null +++ b/PayForReactions/appsettings.Development.json @@ -0,0 +1,8 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.Hosting.Lifetime": "Information" + } + } +}