diff --git a/BasicTests/BasicTests.csproj b/BasicTests/BasicTests.csproj
new file mode 100644
index 0000000..01ff9db
--- /dev/null
+++ b/BasicTests/BasicTests.csproj
@@ -0,0 +1,24 @@
+
+
+
+ net8.0
+ enable
+ enable
+
+ false
+ true
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/BasicTests/GlobalUsings.cs b/BasicTests/GlobalUsings.cs
new file mode 100644
index 0000000..3244567
--- /dev/null
+++ b/BasicTests/GlobalUsings.cs
@@ -0,0 +1 @@
+global using NUnit.Framework;
diff --git a/BasicTests/NostrBufTests.cs b/BasicTests/NostrBufTests.cs
new file mode 100644
index 0000000..d19cff5
--- /dev/null
+++ b/BasicTests/NostrBufTests.cs
@@ -0,0 +1,44 @@
+using Newtonsoft.Json;
+using Nostr.Client.Json;
+using Nostr.Client.Messages;
+using NostrRelay;
+
+namespace BasicTests;
+
+public class Tests
+{
+ private NostrEvent _testEvent = null!;
+
+ [SetUp]
+ public void Setup()
+ {
+ var json =
+ "{\"id\":\"0552df8c6617c8919f1872ce8a0b2834f3f28100e4bf07be8b4aa262e9d59321\",\"pubkey\":\"27154fb873badf69c3ea83a0da6e65d6a150d2bf8f7320fc3314248d74645c64\",\"created_at\":1705062495,\"kind\":1,\"tags\":[[\"r\",\"https://www.coindesk.com/business/2024/01/12/bitcoin-miner-outflows-hit-six-year-highs-ahead-of-halving-sparking-mixed-signals/\"]],\"content\":\"‚Miner outflow has hit a multi-year high as tens of thousands of bitcoin (BTC), worth over $1 billion, have been sent to exchanges.‘\\n\\nhttps://www.coindesk.com/business/2024/01/12/bitcoin-miner-outflows-hit-six-year-highs-ahead-of-halving-sparking-mixed-signals/\",\"sig\":\"ba2523600bea47637bcced3b75ec446253dca184783a98ec807bf0a4fc90031152c225e7a39392bb3af23ceafc64aa67d9c10874e8ec6d2c1af268235a554d93\"}";
+
+ _testEvent = JsonConvert.DeserializeObject(json, NostrSerializer.Settings)!;
+ }
+
+ [Test]
+ public void TestNostrBuf()
+ {
+ var data = NostrBuf.Encode(_testEvent);
+ Assert.That(data.Length, Is.EqualTo(NostrBuf.CalculateSize(_testEvent)));
+
+ var ev = NostrBuf.Decode(data);
+ Assert.That(ev.Id, Is.EqualTo(_testEvent.Id));
+ Assert.That(ev.Pubkey, Is.EqualTo(_testEvent.Pubkey));
+ Assert.That(ev.Sig, Is.EqualTo(_testEvent.Sig));
+ Assert.That(ev.CreatedAt, Is.EqualTo(_testEvent.CreatedAt));
+ Assert.That(ev.Kind, Is.EqualTo(_testEvent.Kind));
+ Assert.That(ev.Tags!.Count, Is.EqualTo(_testEvent.Tags!.Count));
+ for (var tx = 0; tx < ev.Tags.Count; tx++)
+ {
+ Assert.That(ev.Tags[tx].TagIdentifier, Is.EqualTo(_testEvent.Tags[tx].TagIdentifier));
+ Assert.That(ev.Tags[tx].AdditionalData.Length, Is.EqualTo(_testEvent.Tags[tx].AdditionalData.Length));
+ for (var ty = 0; ty < ev.Tags[tx].AdditionalData.Length; ty++)
+ {
+ Assert.That(ev.Tags[tx].AdditionalData[ty], Is.EqualTo(_testEvent.Tags[tx].AdditionalData[ty]));
+ }
+ }
+ }
+}
diff --git a/FASTERlay/.gitignore b/FASTERlay/.gitignore
new file mode 100644
index 0000000..adbb97d
--- /dev/null
+++ b/FASTERlay/.gitignore
@@ -0,0 +1 @@
+data/
\ No newline at end of file
diff --git a/FASTERlay/Dockerfile b/FASTERlay/Dockerfile
new file mode 100644
index 0000000..4281adb
--- /dev/null
+++ b/FASTERlay/Dockerfile
@@ -0,0 +1,23 @@
+FROM mcr.microsoft.com/dotnet/aspnet:8.0 AS base
+USER $APP_UID
+WORKDIR /app
+EXPOSE 8080
+EXPOSE 8081
+
+FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build
+ARG BUILD_CONFIGURATION=Release
+WORKDIR /src
+COPY ["FASTERlay/FASTERlay.csproj", "FASTERlay/"]
+RUN dotnet restore "FASTERlay/FASTERlay.csproj"
+COPY . .
+WORKDIR "/src/FASTERlay"
+RUN dotnet build "FASTERlay.csproj" -c $BUILD_CONFIGURATION -o /app/build
+
+FROM build AS publish
+ARG BUILD_CONFIGURATION=Release
+RUN dotnet publish "FASTERlay.csproj" -c $BUILD_CONFIGURATION -o /app/publish /p:UseAppHost=false
+
+FROM base AS final
+WORKDIR /app
+COPY --from=publish /app/publish .
+ENTRYPOINT ["dotnet", "FASTERlay.dll"]
diff --git a/FASTERlay/Extensions.cs b/FASTERlay/Extensions.cs
new file mode 100644
index 0000000..b2ea89c
--- /dev/null
+++ b/FASTERlay/Extensions.cs
@@ -0,0 +1,5 @@
+namespace FASTERlay;
+
+public static class Extensions
+{
+}
diff --git a/FASTERlay/FASTERRelay.cs b/FASTERlay/FASTERRelay.cs
new file mode 100644
index 0000000..5ed5e4e
--- /dev/null
+++ b/FASTERlay/FASTERRelay.cs
@@ -0,0 +1,118 @@
+using FASTER.core;
+using Nostr.Client.Messages;
+using Nostr.Client.Requests;
+using NostrRelay;
+
+namespace FASTERlay;
+
+public class FasterRelay : INostrRelay, IDisposable
+{
+ private readonly ClientSession> _session;
+ private readonly ClientSession _tagsSession;
+
+ public FasterRelay(NostrStore store)
+ {
+ _session = store.MainStore.For(new SimpleFunctions()).NewSession>();
+ _tagsSession = store.TagStore.For(new TagSetFunctions()).NewSession();
+ }
+
+ public ValueTask AcceptConnection(NostrClientContext context)
+ {
+ return ValueTask.FromResult(true);
+ }
+
+ public async IAsyncEnumerable HandleRequest(NostrClientContext context, NostrRequest req)
+ {
+ var iter = _session.Iterate();
+ var filter = req.NostrFilter;
+ while (iter.GetNext(out _, out _, out var bytes))
+ {
+ var ev = NostrBuf.Decode(bytes);
+ if (ev != default)
+ {
+ if (filter.Since.HasValue && ev.CreatedAt <= filter.Since)
+ {
+ continue;
+ }
+
+ if (filter.Until.HasValue && ev.CreatedAt > filter.Until)
+ {
+ continue;
+ }
+
+ if (filter.Ids != default && !filter.Ids.Contains(ev.Id!))
+ {
+ continue;
+ }
+
+ if (filter.Authors != default && !filter.Authors.Contains(ev.Pubkey!))
+ {
+ continue;
+ }
+
+ if (filter.Kinds != default && !filter.Kinds.Contains(ev.Kind))
+ {
+ continue;
+ }
+
+ yield return ev;
+ }
+ }
+ }
+
+ public async ValueTask HandleEvent(NostrClientContext context, NostrEvent ev)
+ {
+ await SaveEvent(ev);
+ return new(true, null);
+ }
+
+ public void Dispose()
+ {
+ _session.Dispose();
+ _tagsSession.Dispose();
+ }
+
+ private async Task SaveEvent(NostrEvent ev)
+ {
+ var idBytes = Convert.FromHexString(ev.Id!);
+ var eventBytes = NostrBuf.Encode(ev);
+
+ await _session.UpsertAsync(ref idBytes, ref eventBytes);
+
+ var indexTags = ev.Tags!.Where(a => a.TagIdentifier.Length == 1);
+ foreach (var tag in indexTags)
+ {
+ var kIndex = $"{tag.TagIdentifier}.{tag.AdditionalData[0]}";
+ await _tagsSession.RMWAsync(ref kIndex, ref idBytes);
+ }
+
+ await _session.CompletePendingAsync();
+ await _tagsSession.CompletePendingAsync();
+ }
+
+ private async Task GetEvent(string id)
+ {
+ var idBytes = Convert.FromHexString(id);
+ var read = await _session.ReadAsync(ref idBytes);
+ if (read.Status.Found)
+ {
+ var data = read.Complete().output;
+ return NostrBuf.Decode(data);
+ }
+
+ return default;
+ }
+
+ private class TagSetFunctions : SimpleFunctions
+ {
+ public TagSetFunctions() : base((l, r) =>
+ {
+ var output = new byte[l.Length + r.Length];
+ Buffer.BlockCopy(l, 0, output, 0, l.Length);
+ Buffer.BlockCopy(r, 0, output, l.Length, r.Length);
+ return output;
+ })
+ {
+ }
+ }
+}
diff --git a/FASTERlay/FASTERlay.csproj b/FASTERlay/FASTERlay.csproj
new file mode 100644
index 0000000..5547258
--- /dev/null
+++ b/FASTERlay/FASTERlay.csproj
@@ -0,0 +1,24 @@
+
+
+
+ net8.0
+ enable
+ enable
+ Linux
+
+
+
+
+ .dockerignore
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/FASTERlay/NostrStore.cs b/FASTERlay/NostrStore.cs
new file mode 100644
index 0000000..8ee9429
--- /dev/null
+++ b/FASTERlay/NostrStore.cs
@@ -0,0 +1,58 @@
+using FASTER.core;
+
+namespace FASTERlay;
+
+public class NostrStore : IDisposable
+{
+ private readonly ILogger _logger;
+ private readonly CancellationTokenSource _cts = new();
+ private readonly FasterKVSettings _storeSettings;
+ private readonly FasterKV _store;
+ private readonly FasterKVSettings _tagStoreSettings;
+ private readonly FasterKV _tagStore;
+
+ 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();
+ }
+
+ _tagStoreSettings = new FasterKVSettings(Path.Combine(dir, "tags"), false, _logger);
+ _tagStore = new(_tagStoreSettings);
+ var keyVersion = _tagStore.GetLatestCheckpointVersion();
+ if (keyVersion != -1)
+ {
+ _tagStore.Recover();
+ }
+
+ _checkpointer = Task.Factory.StartNew(async () =>
+ {
+ while (!_cts.IsCancellationRequested)
+ {
+ await Task.Delay(10_000);
+ await _store.TakeHybridLogCheckpointAsync(CheckpointType.Snapshot, true);
+ await _tagStore.TakeHybridLogCheckpointAsync(CheckpointType.Snapshot, true);
+ }
+ });
+ }
+ public FasterKV MainStore => _store;
+ public FasterKV TagStore => _tagStore;
+
+ public void Dispose()
+ {
+ _cts.Cancel();
+ _checkpointer.ConfigureAwait(false).GetAwaiter().GetResult();
+
+ _store.Dispose();
+ _storeSettings.Dispose();
+ _tagStore.Dispose();
+ _tagStoreSettings.Dispose();
+ }
+}
diff --git a/FASTERlay/Program.cs b/FASTERlay/Program.cs
new file mode 100644
index 0000000..177f63f
--- /dev/null
+++ b/FASTERlay/Program.cs
@@ -0,0 +1,77 @@
+using System.Net.WebSockets;
+using FASTERlay;
+using Newtonsoft.Json;
+using Nostr.Client.Json;
+using NostrRelay;
+
+var builder = WebApplication.CreateBuilder(args);
+
+builder.Services.AddSingleton(svc =>
+{
+ var logger = svc.GetRequiredService>();
+ return new NostrStore("./data", logger);
+});
+
+builder.Services.AddTransient();
+
+var app = builder.Build();
+
+
+app.UseWebSockets();
+app.MapGet("/", async ctx =>
+{
+ if (ctx.WebSockets.IsWebSocketRequest)
+ {
+ var logger = app.Services.GetRequiredService>>();
+ using var handler = app.Services.GetRequiredService();
+ try
+ {
+ var ws = await ctx.WebSockets.AcceptWebSocketAsync();
+ var wsCtx = new NostrClientContext(ws, ctx.Connection.RemoteIpAddress!,
+ ctx.Request.Headers.UserAgent.FirstOrDefault() ?? string.Empty);
+
+ var nostrRelay = new NostrRelay(handler, wsCtx, ctx.RequestAborted, logger);
+
+ await nostrRelay.Read();
+
+ await ws.CloseAsync(WebSocketCloseStatus.NormalClosure, null, ctx.RequestAborted);
+ }
+ catch (Exception ex)
+ {
+ logger.LogDebug(ex.Message);
+ }
+ }
+ else
+ {
+ var isRelayDocRequest = ctx.Request.Headers.Accept.FirstOrDefault()
+ ?.Equals("application/nostr+json", StringComparison.InvariantCultureIgnoreCase) ?? false;
+
+ if (isRelayDocRequest)
+ {
+ var doc = new RelayDocument
+ {
+ Name = "",
+ Description = "C# FASTER relay",
+ Pubkey = "63fe6318dc58583cfe16810f86dd09e18bfd76aabc24a0081ce2856f330504ed",
+ SupportedNips = [1],
+ Software = "NostrServices",
+ Icon = "https://void.cat/d/CtXsF5EvqNLG3K85TgezCt.webp",
+ Limitation = new()
+ {
+ RestrictedWrites = false,
+ AuthRequired = false,
+ PaymentRequired = false
+ }
+ };
+
+ ctx.Response.ContentType = "application/json";
+ await ctx.Response.WriteAsync(JsonConvert.SerializeObject(doc, NostrSerializer.Settings));
+ }
+ else
+ {
+ await ctx.Response.WriteAsync("Hey");
+ }
+ }
+});
+
+app.Run();
diff --git a/FASTERlay/Properties/launchSettings.json b/FASTERlay/Properties/launchSettings.json
new file mode 100644
index 0000000..e4a92cd
--- /dev/null
+++ b/FASTERlay/Properties/launchSettings.json
@@ -0,0 +1,14 @@
+{
+ "$schema": "http://json.schemastore.org/launchsettings.json",
+ "profiles": {
+ "http": {
+ "commandName": "Project",
+ "dotnetRunMessages": true,
+ "launchBrowser": false,
+ "applicationUrl": "http://localhost:5042",
+ "environmentVariables": {
+ "ASPNETCORE_ENVIRONMENT": "Development"
+ }
+ }
+ }
+}
diff --git a/FASTERlay/appsettings.Development.json b/FASTERlay/appsettings.Development.json
new file mode 100644
index 0000000..afc0632
--- /dev/null
+++ b/FASTERlay/appsettings.Development.json
@@ -0,0 +1,10 @@
+{
+ "Logging": {
+ "LogLevel": {
+ "Default": "Information",
+ "Microsoft.AspNetCore": "Warning",
+ "NostrRelay": "Debug",
+ "FASTER": "Debug"
+ }
+ }
+}
diff --git a/FASTERlay/appsettings.json b/FASTERlay/appsettings.json
new file mode 100644
index 0000000..10f68b8
--- /dev/null
+++ b/FASTERlay/appsettings.json
@@ -0,0 +1,9 @@
+{
+ "Logging": {
+ "LogLevel": {
+ "Default": "Information",
+ "Microsoft.AspNetCore": "Warning"
+ }
+ },
+ "AllowedHosts": "*"
+}
diff --git a/NostrRelay/NostrBuf.cs b/NostrRelay/NostrBuf.cs
new file mode 100644
index 0000000..d8aea26
--- /dev/null
+++ b/NostrRelay/NostrBuf.cs
@@ -0,0 +1,118 @@
+using System.Text;
+using Nostr.Client.Messages;
+
+namespace NostrRelay;
+
+///
+/// Simple fixed length nostr event storage
+///
+public static class NostrBuf
+{
+ public static byte[] Encode(NostrEvent ev)
+ {
+ var buf = new byte[CalculateSize(ev)];
+ var span = buf.AsSpan();
+ span[0] = 0x01; // version 1
+ Convert.FromHexString(ev.Id!).CopyTo(span[1..33]);
+ Convert.FromHexString(ev.Pubkey!).CopyTo(span[33..65]);
+ Convert.FromHexString(ev.Sig!).CopyTo(span[65..129]);
+ BitConverter.GetBytes((ulong)ToUnixTime(ev.CreatedAt!.Value)).CopyTo(span[129..137]);
+ BitConverter.GetBytes((uint)ev.Kind).CopyTo(span[137..141]);
+ var contentBytes = Encoding.UTF8.GetBytes(ev.Content!);
+ BitConverter.GetBytes((uint)contentBytes.Length).CopyTo(span[141..145]);
+ var pos = 145;
+ contentBytes.CopyTo(span[pos..(pos += contentBytes.Length)]);
+ BitConverter.GetBytes((ushort)ev.Tags!.Count).CopyTo(span[pos..(pos += 2)]);
+
+ foreach (var tag in ev.Tags)
+ {
+ var tagKey = Encoding.UTF8.GetBytes(tag.TagIdentifier);
+ span[pos++] = (byte)tagKey.Length;
+ tagKey.CopyTo(span[pos..(pos += tagKey.Length)]);
+
+ span[pos++] = (byte)tag.AdditionalData.Length;
+ foreach (var tagAdd in tag.AdditionalData)
+ {
+ var tagAddBytes = Encoding.UTF8.GetBytes(tagAdd);
+ BitConverter.GetBytes((ushort)tagAddBytes.Length).CopyTo(span[pos..(pos += 2)]);
+ tagAddBytes.CopyTo(span[pos..(pos += tagAddBytes.Length)]);
+ }
+ }
+
+ return buf;
+ }
+
+ public static NostrEvent? Decode(Span data)
+ {
+ var version = data[0];
+ if (version != 0x01) throw new Exception("Version not supported");
+
+ var id = Convert.ToHexString(data[1..33]).ToLower();
+ var pubkey = Convert.ToHexString(data[33..65]).ToLower();
+ var sig = Convert.ToHexString(data[65..129]).ToLower();
+ var createdAt = BitConverter.ToUInt64(data[129..137]);
+ var kind = BitConverter.ToUInt32(data[137..141]);
+ var contentLen = BitConverter.ToUInt32(data[141..145]);
+ var pos = 145;
+ var content = Encoding.UTF8.GetString(data[pos..(pos += (int)contentLen)]);
+ var nTags = BitConverter.ToUInt16(data[pos..(pos += 2)]);
+
+ var tags = new List();
+ for (var x = 0; x < nTags; x++)
+ {
+ var keyLen = data[pos++];
+ var keyString = Encoding.UTF8.GetString(data[pos..(pos += keyLen)]);
+
+ var nElements = data[pos++];
+ var elms = new string[nElements];
+ for (var y = 0; y < nElements; y++)
+ {
+ var elmLen = BitConverter.ToUInt16(data[pos..(pos += 2)]);
+ var elmString = Encoding.UTF8.GetString(data[pos..(pos += elmLen)]);
+ elms[y] = elmString;
+ }
+
+ tags.Add(new(keyString, elms));
+ }
+
+ return new NostrEvent()
+ {
+ Id = id,
+ Pubkey = pubkey,
+ Sig = sig,
+ CreatedAt = DateTimeOffset.FromUnixTimeSeconds((long)createdAt).UtcDateTime,
+ Kind = (NostrKind)kind,
+ Content = content,
+ Tags = new NostrEventTags(tags)
+ };
+ }
+
+ public static long CalculateSize(NostrEvent ev)
+ {
+ const long fixedPart =
+ 1 // version byte
+ + 32 // id
+ + 32 // pubkey
+ + 64 // sig
+ + 8 // created_at (uint64)
+ + 4 // kind (uint32)
+ + 4 // len(content) (uint32)
+ + 2; // len(tags) (uint16)
+
+ var variableLength =
+ Encoding.UTF8.GetByteCount(ev.Content!)
+ + ev.Tags!.Sum(a =>
+ 1 + // 1 byte for length of first tag element (key)
+ Encoding.UTF8.GetByteCount(a.TagIdentifier) + // length of the tag key
+ a.AdditionalData.Length + // 1 byte - number of elements
+ a.AdditionalData.Sum(b => Encoding.UTF8.GetByteCount(b)) + // length of the content of the items in the tag
+ (a.AdditionalData.Length * 2)); // 2 bytes per tag element for length prefix
+
+ return fixedPart + variableLength;
+ }
+
+ private static long ToUnixTime(DateTime dt)
+ {
+ return dt.ToUniversalTime().Ticks / 10000000L - 62135596800L;
+ }
+}
diff --git a/NostrRelay/Relay.cs b/NostrRelay/Relay.cs
index 0ee02b0..9b7e242 100644
--- a/NostrRelay/Relay.cs
+++ b/NostrRelay/Relay.cs
@@ -27,7 +27,7 @@ public class NostrRelay where THandler : INostrRelay
private async Task WriteResponse(T obj)
{
var rspJson = JsonConvert.SerializeObject(obj, NostrSerializer.Settings);
- _logger.LogDebug("Sending {msg}", rspJson);
+ _logger.LogTrace("Sending {msg}", rspJson);
await _ctx.WebSocket.SendAsync(Encoding.UTF8.GetBytes(rspJson), WebSocketMessageType.Text, true, _cts.Token);
}
@@ -36,10 +36,11 @@ public class NostrRelay where THandler : INostrRelay
if (!await _handler.AcceptConnection(_ctx)) return;
var offset = 0;
- var mem = MemoryPool.Shared.Rent(1024 * 1024);
+ var mem = MemoryPool.Shared.Rent(1024 * 1024 * 32);
while (!_cts.IsCancellationRequested)
{
var msg = await _ctx.WebSocket.ReceiveAsync(mem.Memory[offset..], _cts.Token);
+
if (msg.MessageType is WebSocketMessageType.Text)
{
var buff = mem.Memory[..(offset + msg.Count)];
@@ -47,49 +48,100 @@ public class NostrRelay where THandler : INostrRelay
if (!msg.EndOfMessage) continue;
var str = Encoding.UTF8.GetString(buff.Span);
- _logger.LogDebug("Got msg {msg}", str);
- if (str.StartsWith("[\"REQ\""))
+ _logger.LogTrace("Got msg {msg}", str);
+ try
{
- var req = JsonConvert.DeserializeObject(str, NostrSerializer.Settings);
- if (req != default)
+ if (str.StartsWith("[\"REQ\""))
{
- await foreach (var ev in _handler.HandleRequest(_ctx, req))
+ var req = JsonConvert.DeserializeObject(str, NostrSerializer.Settings);
+ if (req != default)
{
- await WriteResponse(new NostrEventResponse()
+ await foreach (var ev in _handler.HandleRequest(_ctx, req))
{
- MessageType = "EVENT",
- Subscription = req.Subscription,
- Event = ev
- });
+ await WriteResponse(new NostrEventResponse()
+ {
+ MessageType = "EVENT",
+ Subscription = req.Subscription,
+ Event = ev
+ });
+ }
+
+ var rsp = new NostrEoseResponse
+ {
+ MessageType = "EOSE",
+ Subscription = req.Subscription
+ };
+
+ await WriteResponse(rsp);
}
-
- var rsp = new NostrEoseResponse
+ }
+ else if (str.StartsWith("[\"EVENT\""))
+ {
+ var req = JsonConvert.DeserializeObject(str, NostrSerializer.Settings);
+ if (req != default)
{
- MessageType = "EOSE",
- Subscription = req.Subscription
- };
+ if (!req.Event.IsSignatureValid())
+ {
+ var rsp = new NostrOkResponse()
+ {
+ MessageType = "OK",
+ EventId = req.Event.Id,
+ Accepted = false,
+ Message = "invalid: sig check failed"
+ };
- await WriteResponse(rsp);
+ await WriteResponse(rsp);
+ }
+ else
+ {
+ var result = await _handler.HandleEvent(_ctx, req.Event);
+ var rsp = new NostrOkResponse()
+ {
+ MessageType = "OK",
+ EventId = req.Event.Id,
+ Accepted = result.Ok,
+ Message = result.Message ?? ""
+ };
+
+ await WriteResponse(rsp);
+ }
+ }
}
}
- else if (str.StartsWith("[\"EVENT\""))
+ catch (Exception ex)
{
- var req = JsonConvert.DeserializeObject(str, NostrSerializer.Settings);
- if (req != default)
- {
- var result = await _handler.HandleEvent(_ctx, req.Event);
- var rsp = new NostrOkResponse()
- {
- MessageType = "OK",
- EventId = req.Event.Id,
- Accepted = result.Ok,
- Message = result.Message ?? ""
- };
-
- await WriteResponse(rsp);
- }
+ _logger.LogWarning("Failed to process msg {error}", ex.Message);
+ offset = 0;
}
}
}
}
}
+
+[JsonConverter(typeof(ArrayConverter))]
+public class NostrInboundRequest
+{
+ public NostrInboundRequest()
+ {
+ }
+
+ public NostrInboundRequest(string subscription, NostrFilter nostrFilter)
+ {
+ this.Subscription = subscription;
+ this.NostrFilter = nostrFilter;
+ }
+
+ public static implicit operator NostrRequest(NostrInboundRequest req)
+ {
+ return new(req.Subscription, req.NostrFilter);
+ }
+
+ [ArrayProperty(0)]
+ public string Type { get; init; } = "REQ";
+
+ [ArrayProperty(1)]
+ public string Subscription { get; init; }
+
+ [ArrayProperty(2)]
+ public NostrFilter NostrFilter { get; init; }
+}
diff --git a/NostrRelay/RelayDocument.cs b/NostrRelay/RelayDocument.cs
new file mode 100644
index 0000000..e4f38ed
--- /dev/null
+++ b/NostrRelay/RelayDocument.cs
@@ -0,0 +1,103 @@
+using Newtonsoft.Json;
+
+namespace NostrRelay;
+
+public class Fees
+{
+ [JsonProperty("subscription")]
+ public List Subscription { get; set; }
+}
+
+public class Limitation
+{
+ [JsonProperty("payment_required")]
+ public bool? PaymentRequired { get; set; } = false;
+
+ [JsonProperty("max_message_length")]
+ public int? MaxMessageLength { get; set; }
+
+ ///
+ /// Maximum number of filter values in each subscription. Must be one or higher.
+ ///
+ [JsonProperty("max_filters")]
+ public int? MaxFilters { get; init; } = 1;
+
+ [JsonProperty("max_event_tags")]
+ public int? MaxEventTags { get; set; }
+
+ [JsonProperty("max_subscriptions")]
+ public int? MaxSubscriptions { get; set; }
+
+ [JsonProperty("auth_required")]
+ public bool? AuthRequired { get; set; }
+
+ [JsonProperty("restricted_writes")]
+ public bool? RestrictedWrites { get; set; }
+}
+
+public class RelayDocument
+{
+ [JsonProperty("description")]
+ public string? Description { get; set; }
+
+ [JsonProperty("name")]
+ public string? Name { get; set; }
+
+ [JsonProperty("pubkey")]
+ public string? Pubkey { get; set; }
+
+ [JsonProperty("software")]
+ public string? Software { get; set; }
+
+
+ [JsonProperty("supported_nips")]
+ public List? SupportedNips { get; set; }
+
+ [JsonProperty("version")]
+ public string? Version { get; set; }
+
+ ///
+ /// A URL pointing to an image to be used as an icon for the relay
+ ///
+ [JsonProperty("icon")]
+ public string? Icon { get; init; }
+
+ [JsonProperty("limitation")]
+ public Limitation? Limitation { get; set; }
+
+ [JsonProperty("payments_url")]
+ public string? PaymentsUrl { get; set; }
+
+ [JsonProperty("fees")]
+ public Fees? Fees { get; set; }
+
+ [JsonProperty("relay_countries")]
+ public List? RelayCountries { get; init; }
+
+ ///
+ /// Ordered list of IETF language tags indicating the major languages spoken on the relay.
+ ///
+ [JsonProperty("language_tags")]
+ public List? LanguageTags { get; init; }
+
+ [JsonProperty("tags")]
+ public List? Tags { get; init; }
+
+ ///
+ /// Link to a human-readable page which specifies the community policies for the relay
+ ///
+ [JsonProperty("posting_policy")]
+ public string? PostingPolicy { get; set; }
+}
+
+public class Subscription
+{
+ [JsonProperty("amount")]
+ public long? Amount { get; set; }
+
+ [JsonProperty("unit")]
+ public string Unit { get; set; } = "msats";
+
+ [JsonProperty("period")]
+ public int? Period { get; set; }
+}
diff --git a/NostrServices.sln b/NostrServices.sln
index 48d1596..be0a416 100644
--- a/NostrServices.sln
+++ b/NostrServices.sln
@@ -11,6 +11,10 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NostrRelay", "NostrRelay\NostrRelay.csproj", "{FBCF209E-9C58-45EB-BC59-99569B28D811}"
EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FASTERlay", "FASTERlay\FASTERlay.csproj", "{A9BD2D9D-9BAA-463C-8FC1-027D86A17E91}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BasicTests", "BasicTests\BasicTests.csproj", "{1F9E8BD2-38B6-4FA4-9FAA-7376759C2CBB}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -25,5 +29,13 @@ Global
{FBCF209E-9C58-45EB-BC59-99569B28D811}.Debug|Any CPU.Build.0 = Debug|Any CPU
{FBCF209E-9C58-45EB-BC59-99569B28D811}.Release|Any CPU.ActiveCfg = Release|Any CPU
{FBCF209E-9C58-45EB-BC59-99569B28D811}.Release|Any CPU.Build.0 = Release|Any CPU
+ {A9BD2D9D-9BAA-463C-8FC1-027D86A17E91}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {A9BD2D9D-9BAA-463C-8FC1-027D86A17E91}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {A9BD2D9D-9BAA-463C-8FC1-027D86A17E91}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {A9BD2D9D-9BAA-463C-8FC1-027D86A17E91}.Release|Any CPU.Build.0 = Release|Any CPU
+ {1F9E8BD2-38B6-4FA4-9FAA-7376759C2CBB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {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
EndGlobalSection
EndGlobal