FASTER relay
continuous-integration/drone/push Build is passing Details

This commit is contained in:
Kieran 2024-01-12 16:22:38 +00:00
parent a0e59757b5
commit a22122e824
Signed by: Kieran
GPG Key ID: DE71CEB3925BE941
17 changed files with 726 additions and 33 deletions

View File

@ -0,0 +1,24 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<IsPackable>false</IsPackable>
<IsTestProject>true</IsTestProject>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.6.0"/>
<PackageReference Include="NUnit" Version="3.13.3"/>
<PackageReference Include="NUnit3TestAdapter" Version="4.2.1"/>
<PackageReference Include="NUnit.Analyzers" Version="3.6.1"/>
<PackageReference Include="coverlet.collector" Version="6.0.0"/>
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\NostrRelay\NostrRelay.csproj" />
</ItemGroup>
</Project>

View File

@ -0,0 +1 @@
global using NUnit.Framework;

View File

@ -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<NostrEvent>(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]));
}
}
}
}

1
FASTERlay/.gitignore vendored Normal file
View File

@ -0,0 +1 @@
data/

23
FASTERlay/Dockerfile Normal file
View File

@ -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"]

5
FASTERlay/Extensions.cs Normal file
View File

@ -0,0 +1,5 @@
namespace FASTERlay;
public static class Extensions
{
}

118
FASTERlay/FASTERRelay.cs Normal file
View File

@ -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<byte[], byte[], byte[], byte[], Empty, SimpleFunctions<byte[], byte[]>> _session;
private readonly ClientSession<string, byte[], byte[], byte[], Empty, TagSetFunctions> _tagsSession;
public FasterRelay(NostrStore store)
{
_session = store.MainStore.For(new SimpleFunctions<byte[], byte[]>()).NewSession<SimpleFunctions<byte[], byte[]>>();
_tagsSession = store.TagStore.For(new TagSetFunctions()).NewSession<TagSetFunctions>();
}
public ValueTask<bool> AcceptConnection(NostrClientContext context)
{
return ValueTask.FromResult(true);
}
public async IAsyncEnumerable<NostrEvent> 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<HandleEventResponse> 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<NostrEvent?> 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<string, byte[]>
{
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;
})
{
}
}
}

View File

@ -0,0 +1,24 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<DockerDefaultTargetOS>Linux</DockerDefaultTargetOS>
</PropertyGroup>
<ItemGroup>
<Content Include="..\.dockerignore">
<Link>.dockerignore</Link>
</Content>
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\NostrRelay\NostrRelay.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.FASTER.Core" Version="2.6.1" />
</ItemGroup>
</Project>

58
FASTERlay/NostrStore.cs Normal file
View File

@ -0,0 +1,58 @@
using FASTER.core;
namespace FASTERlay;
public class NostrStore : IDisposable
{
private readonly ILogger<NostrStore> _logger;
private readonly CancellationTokenSource _cts = new();
private readonly FasterKVSettings<byte[], byte[]> _storeSettings;
private readonly FasterKV<byte[], byte[]> _store;
private readonly FasterKVSettings<string, byte[]> _tagStoreSettings;
private readonly FasterKV<string, byte[]> _tagStore;
private readonly Task _checkpointer;
public NostrStore(string dir, ILogger<NostrStore> logger)
{
_logger = logger;
_storeSettings = new FasterKVSettings<byte[], byte[]>(Path.Combine(dir, "main"), false, _logger);
_store = new(_storeSettings);
var version = _store.GetLatestCheckpointVersion();
if (version != -1)
{
_store.Recover();
}
_tagStoreSettings = new FasterKVSettings<string, byte[]>(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<byte[], byte[]> MainStore => _store;
public FasterKV<string, byte[]> TagStore => _tagStore;
public void Dispose()
{
_cts.Cancel();
_checkpointer.ConfigureAwait(false).GetAwaiter().GetResult();
_store.Dispose();
_storeSettings.Dispose();
_tagStore.Dispose();
_tagStoreSettings.Dispose();
}
}

77
FASTERlay/Program.cs Normal file
View File

@ -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<NostrStore>(svc =>
{
var logger = svc.GetRequiredService<ILogger<NostrStore>>();
return new NostrStore("./data", logger);
});
builder.Services.AddTransient<FasterRelay>();
var app = builder.Build();
app.UseWebSockets();
app.MapGet("/", async ctx =>
{
if (ctx.WebSockets.IsWebSocketRequest)
{
var logger = app.Services.GetRequiredService<ILogger<NostrRelay<FasterRelay>>>();
using var handler = app.Services.GetRequiredService<FasterRelay>();
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<FasterRelay>(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();

View File

@ -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"
}
}
}
}

View File

@ -0,0 +1,10 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning",
"NostrRelay": "Debug",
"FASTER": "Debug"
}
}
}

View File

@ -0,0 +1,9 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"AllowedHosts": "*"
}

118
NostrRelay/NostrBuf.cs Normal file
View File

@ -0,0 +1,118 @@
using System.Text;
using Nostr.Client.Messages;
namespace NostrRelay;
/// <summary>
/// Simple fixed length nostr event storage
/// </summary>
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<byte> 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<NostrEventTag>();
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;
}
}

View File

@ -27,7 +27,7 @@ public class NostrRelay<THandler> where THandler : INostrRelay
private async Task WriteResponse<T>(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<THandler> where THandler : INostrRelay
if (!await _handler.AcceptConnection(_ctx)) return;
var offset = 0;
var mem = MemoryPool<byte>.Shared.Rent(1024 * 1024);
var mem = MemoryPool<byte>.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<THandler> 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<NostrRequest>(str, NostrSerializer.Settings);
if (req != default)
if (str.StartsWith("[\"REQ\""))
{
await foreach (var ev in _handler.HandleRequest(_ctx, req))
var req = JsonConvert.DeserializeObject<NostrInboundRequest>(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<NostrEventRequest>(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<NostrEventRequest>(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; }
}

103
NostrRelay/RelayDocument.cs Normal file
View File

@ -0,0 +1,103 @@
using Newtonsoft.Json;
namespace NostrRelay;
public class Fees
{
[JsonProperty("subscription")]
public List<Subscription> Subscription { get; set; }
}
public class Limitation
{
[JsonProperty("payment_required")]
public bool? PaymentRequired { get; set; } = false;
[JsonProperty("max_message_length")]
public int? MaxMessageLength { get; set; }
/// <summary>
/// Maximum number of filter values in each subscription. Must be one or higher.
/// </summary>
[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<int>? SupportedNips { get; set; }
[JsonProperty("version")]
public string? Version { get; set; }
/// <summary>
/// A URL pointing to an image to be used as an icon for the relay
/// </summary>
[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<string>? RelayCountries { get; init; }
/// <summary>
/// Ordered list of IETF language tags indicating the major languages spoken on the relay.
/// </summary>
[JsonProperty("language_tags")]
public List<string>? LanguageTags { get; init; }
[JsonProperty("tags")]
public List<string>? Tags { get; init; }
/// <summary>
/// Link to a human-readable page which specifies the community policies for the relay
/// </summary>
[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; }
}

View File

@ -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