Skip to content

Commit 640441e

Browse files
committed
support nut18
1 parent 77decea commit 640441e

29 files changed

+558
-208
lines changed

.github/workflows/ci.yml

+23
Original file line numberDiff line numberDiff line change
@@ -39,4 +39,27 @@ jobs:
3939
INCLUDE_SYMBOLS: false
4040
VERSION_REGEX: ^\s*<PackageVersion>(.*)<\/PackageVersion>\s*$
4141
TAG_COMMIT: true
42+
TAG_FORMAT: DotNut/v*
43+
- name: Publish NuGet for nostr
44+
if: ${{ github.ref == 'refs/heads/master' }} # Publish only when the push is on master
45+
uses: Rebel028/[email protected]
46+
with:
47+
PROJECT_FILE_PATH: DotNut.Nostr/DotNut.Nostr.csproj
48+
NUGET_KEY: ${{secrets.NUGET_KEY}}
49+
PACKAGE_NAME: DotNut.Nostr
50+
INCLUDE_SYMBOLS: false
51+
VERSION_REGEX: ^\s*<PackageVersion>(.*)<\/PackageVersion>\s*$
52+
TAG_COMMIT: true
53+
TAG_FORMAT: DotNut.Nostr/v*
54+
- name: Publish Github Package Registry for nostr
55+
if: ${{ github.ref == 'refs/heads/master' }} # Publish only when the push is on master
56+
uses: Rebel028/[email protected]
57+
with:
58+
PROJECT_FILE_PATH: DotNut.Nostr/DotNut.Nostr.csproj
59+
NUGET_SOURCE: "https://nuget.pkg.github.com/Kukks"
60+
NUGET_KEY: ${{secrets.GH_TOKEN}}
61+
PACKAGE_NAME: DotNut.Nostr
62+
INCLUDE_SYMBOLS: false
63+
VERSION_REGEX: ^\s*<PackageVersion>(.*)<\/PackageVersion>\s*$
64+
TAG_COMMIT: true
4265
TAG_FORMAT: DotNut/v*

DotNut.Nostr/DotNut.Nostr.csproj

+27
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
<Project Sdk="Microsoft.NET.Sdk">
2+
3+
<PropertyGroup>
4+
<TargetFramework>net8.0</TargetFramework>
5+
<ImplicitUsings>enable</ImplicitUsings>
6+
<Nullable>enable</Nullable>
7+
<GeneratePackageOnBuild>true</GeneratePackageOnBuild>
8+
<Title>DotNut.Nostr</Title>
9+
<Authors>Kukks</Authors>
10+
<Description>Support Cashu payment requests through Nostr </Description>
11+
<Copyright>MIT</Copyright>
12+
<PackageProjectUrl>https://github.com/Kukks/DotNut</PackageProjectUrl>
13+
<PackageVersion>1.0.0</PackageVersion>
14+
<RepositoryUrl>https://github.com/Kukks/DotNut</RepositoryUrl>
15+
<RepositoryType>git</RepositoryType>
16+
<PackageTags>bitcoin cashu ecash secp256k1 nostr</PackageTags>
17+
<PackageLicenseUrl>https://github.com/Kukks/DotNut/blob/master/LICENSE</PackageLicenseUrl>
18+
19+
</PropertyGroup>
20+
21+
<ItemGroup>
22+
23+
<PackageReference Include="NNostr.Client" Version="0.0.53" />
24+
<ProjectReference Include="..\DotNut\DotNut.csproj" />
25+
</ItemGroup>
26+
27+
</Project>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
using System.Security.Cryptography;
2+
using System.Text.Json;
3+
using NBitcoin.Secp256k1;
4+
using NNostr.Client;
5+
using NNostr.Client.Protocols;
6+
7+
namespace DotNut.Nostr;
8+
9+
public class NostrNip17PaymentRequestInterfaceHandler : PaymentRequestInterfaceHandler
10+
{
11+
public static void Register()
12+
{
13+
PaymentRequestTransportInitiator.Handlers.Add(new NostrNip17PaymentRequestInterfaceHandler());
14+
}
15+
16+
public bool CanHandle(PaymentRequest request)
17+
{
18+
return request.Transports.Any(t => t.Type == "nostr" && t.Tags.Any( t => t.Key == "n" && t.Value == "17"));
19+
}
20+
21+
public async Task SendPayment(PaymentRequest request, PaymentRequestPayload payload,
22+
CancellationToken cancellationToken = default)
23+
{
24+
var nprofileStr = request.Transports.First(t => t.Type == "nostr" && t.Tags.Any( t => t.Key == "n" && t.Value == "17")).Target;
25+
var nprofile = (NIP19.NosteProfileNote) NIP19.FromNIP19Note(nprofileStr);
26+
using var client = new CompositeNostrClient(nprofile.Relays.Select(r => new Uri(r)).ToArray());
27+
await client.Connect(cancellationToken);
28+
var ephemeralKey = ECPrivKey.Create(RandomNumberGenerator.GetBytes(32));
29+
var msg = new NostrEvent()
30+
{
31+
Kind = 14,
32+
Content = JsonSerializer.Serialize(payload),
33+
CreatedAt = DateTimeOffset.Now,
34+
PublicKey = ephemeralKey.CreateXOnlyPubKey().ToHex(),
35+
Tags = new(),
36+
};
37+
msg.Id = msg.ComputeId();
38+
39+
var giftWrap = await NIP17.Create(msg, ephemeralKey,ECXOnlyPubKey.Create(Convert.FromHexString(nprofile.PubKey)), null);
40+
await client.SendEventsAndWaitUntilReceived(new []{giftWrap}, cancellationToken);
41+
42+
}
43+
}

DotNut.Tests/UnitTest1.cs

+19
Original file line numberDiff line numberDiff line change
@@ -367,6 +367,25 @@ public void Nut14Tests_HTLCSecret()
367367
var rebuiltNut10 = new Nut10Secret(HTLCProofSecret.Key, rebuiltHtlcSecret);
368368
Assert.Equal(JsonSerializer.Serialize(nut10Secret), JsonSerializer.Serialize(rebuiltNut10));
369369
}
370+
371+
[Fact]
372+
public void Nut18Tests()
373+
{
374+
var creqA =
375+
"creqApWF0gaNhdGVub3N0cmFheKlucHJvZmlsZTFxeTI4d3VtbjhnaGo3dW45ZDNzaGp0bnl2OWtoMnVld2Q5aHN6OW1od2RlbjV0ZTB3ZmprY2N0ZTljdXJ4dmVuOWVlaHFjdHJ2NWhzenJ0aHdkZW41dGUwZGVoaHh0bnZkYWtxcWd5ZGFxeTdjdXJrNDM5eWtwdGt5c3Y3dWRoZGh1NjhzdWNtMjk1YWtxZWZkZWhrZjBkNDk1Y3d1bmw1YWeBgmFuYjE3YWloYjdhOTAxNzZhYQphdWNzYXRhbYF4Imh0dHBzOi8vbm9mZWVzLnRlc3RudXQuY2FzaHUuc3BhY2U=";
376+
var pr = PaymentRequest.Parse(creqA);
377+
Assert.Equal("https://nofees.testnut.cashu.space", Assert.Single( pr.Mints));
378+
Assert.Equal(10, pr.Amount);
379+
Assert.Equal("b7a90176", pr.PaymentId);
380+
Assert.Equal("sat", pr.Unit);
381+
var t = Assert.Single(pr.Transports);
382+
Assert.Equal("nostr", t.Type);
383+
Assert.Equal("nprofile1qy28wumn8ghj7un9d3shjtnyv9kh2uewd9hsz9mhwden5te0wfjkccte9curxven9eehqctrv5hszrthwden5te0dehhxtnvdakqqgydaqy7curk439ykptkysv7udhdhu68sucm295akqefdehkf0d495cwunl5", t.Target);
384+
Assert.Equal("n",Assert.Single(t.Tags).Key );
385+
Assert.Equal("17",Assert.Single(t.Tags).Value );
386+
Assert.Equal(creqA, pr.ToString());
387+
388+
}
370389

371390
internal readonly struct TestCase(in string path, in string keyHex, in string ccHex)
372391
{

DotNut.sln

+6
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DotNut", "DotNut\DotNut.csp
44
EndProject
55
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DotNut.Tests", "DotNut.Tests\DotNut.Tests.csproj", "{0AAAA85C-9FDC-4FD5-9CC2-ED8385B40106}"
66
EndProject
7+
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DotNut.Nostr", "DotNut.Nostr\DotNut.Nostr.csproj", "{EDBCFA42-B7A0-4443-9359-8A44BDA17935}"
8+
EndProject
79
Global
810
GlobalSection(SolutionConfigurationPlatforms) = preSolution
911
Debug|Any CPU = Debug|Any CPU
@@ -18,5 +20,9 @@ Global
1820
{0AAAA85C-9FDC-4FD5-9CC2-ED8385B40106}.Debug|Any CPU.Build.0 = Debug|Any CPU
1921
{0AAAA85C-9FDC-4FD5-9CC2-ED8385B40106}.Release|Any CPU.ActiveCfg = Release|Any CPU
2022
{0AAAA85C-9FDC-4FD5-9CC2-ED8385B40106}.Release|Any CPU.Build.0 = Release|Any CPU
23+
{EDBCFA42-B7A0-4443-9359-8A44BDA17935}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
24+
{EDBCFA42-B7A0-4443-9359-8A44BDA17935}.Debug|Any CPU.Build.0 = Debug|Any CPU
25+
{EDBCFA42-B7A0-4443-9359-8A44BDA17935}.Release|Any CPU.ActiveCfg = Release|Any CPU
26+
{EDBCFA42-B7A0-4443-9359-8A44BDA17935}.Release|Any CPU.Build.0 = Release|Any CPU
2127
EndGlobalSection
2228
EndGlobal

DotNut.sln.DotSettings.user

+14-5
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,21 @@
11
<wpf:ResourceDictionary xml:space="preserve" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:s="clr-namespace:System;assembly=mscorlib" xmlns:ss="urn:shemas-jetbrains-com:settings-storage-xaml" xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
2+
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003ACBORObject_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003FUsers_003Fevilk_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E2_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003F1178f88c7f664f88be66f2163788c5392f600_003F8a_003F2c7374c2_003FCBORObject_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
3+
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003ACBORReader_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003FUsers_003Fevilk_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E2_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003F1178f88c7f664f88be66f2163788c5392f600_003Fd6_003F6991e711_003FCBORReader_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
4+
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AEqualityAsserts_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003FUsers_003Fevilk_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E2_003Fresharper_002Dhost_003FSourcesCache_003Fec11489f13fea2ba3e5dfbdea086f41dac024556d53f318c75a75a2cbd73_003FEqualityAsserts_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
5+
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AICBORConverter_00601_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003FUsers_003Fevilk_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E2_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003F1178f88c7f664f88be66f2163788c5392f600_003Fd2_003F856b4d3b_003FICBORConverter_00601_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
6+
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AIGrouping_00602_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003FUsers_003Fevilk_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E2_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003F93ad1732e6bb4cc0a5b6f0de2317699584908_003F3b_003F8a9e12bf_003FIGrouping_00602_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
7+
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AJsonConverterOfT_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003FUsers_003Fevilk_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E2_003Fresharper_002Dhost_003FSourcesCache_003Fc247cf3d53f062b5bf983a13ed187d7444d3f29fd6b7ae1a3cdc8cd0f3d213_003FJsonConverterOfT_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
8+
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003ANostrClient_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003FUsers_003Fevilk_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E2_003Fresharper_002Dhost_003FSourcesCache_003Fd0b54a6fb9f089bd45a21661e5f515f5109c46c869c360abb2ca21edbf267d60_003FNostrClient_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
9+
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AObject_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003FUsers_003Fevilk_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E2_003Fresharper_002Dhost_003FSourcesCache_003F9a6b1457cbcf17db31a383ba49ef9bcc786cf3ef77146d997eee499b27a46d_003FObject_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
10+
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AStringAsserts_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003FUsers_003Fevilk_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E2_003Fresharper_002Dhost_003FSourcesCache_003F999ae9cc4ab7b7cfbc5080803e994426e97fd9d87c5b1f44544a799bc114_003FStringAsserts_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
11+
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AThrowHelper_002ESerialization_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003FUsers_003Fevilk_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E2_003Fresharper_002Dhost_003FSourcesCache_003F60afdfcef9945be6977fa4b76b3d84506e2a2067afb63480aacf49417cfa_003FThrowHelper_002ESerialization_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
12+
<s:String x:Key="/Default/Environment/Highlighting/HighlightingSourceSnapshotLocation/@EntryValue">C:\Users\evilk\AppData\Local\JetBrains\Rider2024.2\resharper-host\temp\Rider\vAny\CoverageData\_DotNut.1481064820\Snapshot\snapshot.utdcvr</s:String>
13+
214
<s:String x:Key="/Default/Environment/UnitTesting/UnitTestSessionStore/Sessions/=7c67fc42_002Dfa49_002D41c5_002Db4e4_002D876bfc07dd62/@EntryIndexedValue">&lt;SessionState ContinuousTestingMode="0" Name="Test1" xmlns="urn:schemas-jetbrains-com:jetbrains-ut-session"&gt;&#xD;
315
&lt;TestAncestor&gt;&#xD;
4-
&lt;TestId&gt;xUnit::0AAAA85C-9FDC-4FD5-9CC2-ED8385B40106::net8.0::DotNuts.Tests.UnitTest1&lt;/TestId&gt;&#xD;
16+
&lt;TestId&gt;xUnit::0AAAA85C-9FDC-4FD5-9CC2-ED8385B40106::net8.0::DotNut.Tests.UnitTest1.Nut00Tests_TokenSerialization&lt;/TestId&gt;&#xD;
517
&lt;/TestAncestor&gt;&#xD;
618
&lt;/SessionState&gt;</s:String>
719
<s:String x:Key="/Default/Environment/UnitTesting/UnitTestSessionStore/Sessions/=b0637c43_002D4622_002D4768_002D94e5_002D70df8e44a8fe/@EntryIndexedValue">&lt;SessionState ContinuousTestingMode="0" IsActive="True" Name="Nut11_Signatures" xmlns="urn:schemas-jetbrains-com:jetbrains-ut-session"&gt;&#xD;
8-
&lt;And&gt;&#xD;
9-
&lt;Namespace&gt;DotNuts.Tests&lt;/Namespace&gt;&#xD;
10-
&lt;Project Location="C:\Git\DotNuts\DotNuts.Tests" Presentation="&amp;lt;DotNuts.Tests&amp;gt;" /&gt;&#xD;
11-
&lt;/And&gt;&#xD;
20+
&lt;Project Location="C:\Git\DotNuts\DotNut.Tests" Presentation="&amp;lt;DotNut.Tests&amp;gt;" /&gt;&#xD;
1221
&lt;/SessionState&gt;</s:String></wpf:ResourceDictionary>

DotNut/ApiModels/ContactInfo.cs

+9
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
using System.Text.Json.Serialization;
2+
3+
namespace DotNut.ApiModels;
4+
5+
public class ContactInfo
6+
{
7+
[JsonPropertyName("method")] public string Method { get; set; }
8+
[JsonPropertyName("info")] public string Info { get; set; }
9+
}

DotNut/ApiModels/GetInfoResponse.cs

-6
Original file line numberDiff line numberDiff line change
@@ -47,10 +47,4 @@ public class GetInfoResponse
4747
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)]
4848
[JsonPropertyName("nuts")]
4949
public Dictionary<string, JsonDocument>? Nuts { get; set; }
50-
}
51-
52-
public class ContactInfo
53-
{
54-
[JsonPropertyName("method")] public string Method { get; set; }
55-
[JsonPropertyName("info")] public string Info { get; set; }
5650
}

DotNut/DotNut.csproj

+1-1
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919
</PropertyGroup>
2020

2121
<ItemGroup>
22-
<PackageReference Include="NBitcoin.Secp256k1" Version="3.1.5" />
22+
<PackageReference Include="NBitcoin.Secp256k1" Version="3.1.6" />
2323
<PackageReference Include="NBip32Fast" Version="1.0.5" />
2424
<PackageReference Include="PeterO.Cbor" Version="4.5.5" />
2525
</ItemGroup>

DotNut/Encoding/CashuTokenHelper.cs

+1-129
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,4 @@
1-
using System.Text;
2-
using System.Text.Json;
3-
using NBitcoin.Secp256k1;
4-
using PeterO.Cbor;
5-
6-
namespace DotNut;
1+
namespace DotNut;
72

83
public static class CashuTokenHelper
94
{
@@ -64,127 +59,4 @@ public static CashuToken Decode(string token, out string? version)
6459
token = token.Substring(1);
6560
return encoder.Decode(token);
6661
}
67-
}
68-
69-
public interface ICashuTokenEncoder
70-
{
71-
string Encode(CashuToken token);
72-
CashuToken Decode(string token);
73-
}
74-
75-
public class CashuTokenV3Encoder : ICashuTokenEncoder
76-
{
77-
public string Encode(CashuToken token)
78-
{
79-
var json = JsonSerializer.Serialize(token);
80-
return Base64UrlSafe.Encode(Encoding.UTF8.GetBytes(json));
81-
}
82-
83-
public CashuToken Decode(string token)
84-
{
85-
var json = Encoding.UTF8.GetString(Base64UrlSafe.Decode(token));
86-
return JsonSerializer.Deserialize<CashuToken>(json)!;
87-
}
88-
}
89-
90-
public class CashuTokenV4Encoder : ICashuTokenEncoder, ICBORToFromConverter<CashuToken>
91-
{
92-
public string Encode(CashuToken token)
93-
{
94-
var obj = ToCBORObject(token);
95-
return Base64UrlSafe.Encode(obj.EncodeToBytes());
96-
}
97-
98-
public CashuToken Decode(string token)
99-
{
100-
var obj = CBORObject.DecodeFromBytes(Base64UrlSafe.Decode(token));
101-
return FromCBORObject(obj);
102-
}
103-
104-
public CBORObject ToCBORObject(CashuToken token)
105-
{
106-
//ensure that all token mints are the same
107-
var mints = token.Tokens.Select(token1 => token1.Mint).ToArray();
108-
if (mints.Distinct().Count() != 1)
109-
throw new FormatException("All proofs must have the same mint in v4 tokens");
110-
var proofSets = CBORObject.NewArray();
111-
foreach (var proofSet in token.Tokens.SelectMany(token1 => token1.Proofs).GroupBy(proof => proof.Id))
112-
{
113-
var proofSetItem = CBORObject.NewOrderedMap();
114-
proofSetItem.Add("i", Convert.FromHexString(proofSet.Key.ToString()));
115-
var proofSetItemArray = CBORObject.NewArray();
116-
foreach (var proof in proofSet)
117-
{
118-
var proofItem = CBORObject.NewOrderedMap()
119-
.Add("a", proof.Amount)
120-
.Add("s", Encoding.UTF8.GetString(proof.Secret.GetBytes()))
121-
.Add("c", proof.C.Key.ToBytes());
122-
if (proof.DLEQ is not null)
123-
{
124-
proofItem.Add("d", CBORObject
125-
.NewOrderedMap()
126-
.Add("e", proof.DLEQ.E.Key.ToBytes())
127-
.Add("s", proof.DLEQ.S.Key.ToBytes())
128-
.Add("r", proof.DLEQ.R.Key.ToBytes()));
129-
}
130-
131-
if (proof.Witness is not null)
132-
{
133-
proofItem.Add("w", proof.Witness);
134-
}
135-
136-
proofSetItemArray.Add(proofItem);
137-
}
138-
139-
proofSetItem.Add("p", proofSetItemArray);
140-
proofSets.Add(proofSetItem);
141-
}
142-
143-
var cbor = CBORObject.NewOrderedMap();
144-
145-
146-
if (token.Memo is not null)
147-
cbor.Add("d", token.Memo);
148-
cbor.Add("t", proofSets)
149-
.Add("m", mints.First())
150-
.Add("u", token.Unit!);
151-
return cbor;
152-
}
153-
154-
public CashuToken FromCBORObject(CBORObject obj)
155-
{
156-
return new CashuToken
157-
{
158-
Unit = obj["u"].AsString(),
159-
Memo = obj.GetOrDefault("d", null)?.AsString(),
160-
Tokens =
161-
[
162-
new CashuToken.Token()
163-
{
164-
Mint = obj["m"].AsString(),
165-
Proofs = obj["t"].Values.SelectMany(proofSet =>
166-
{
167-
var id = new KeysetId(Convert.ToHexString(proofSet["i"].GetByteString()).ToLowerInvariant());
168-
169-
return proofSet["p"].Values.Select(proof => new Proof()
170-
{
171-
Amount = proof["a"].AsInt32(),
172-
Secret = JsonSerializer.Deserialize<ISecret>(proof["s"].ToJSONString())!,
173-
C = ECPubKey.Create(proof["c"].GetByteString()),
174-
Witness = proof.GetOrDefault("w", null)?.AsString(),
175-
DLEQ = proof.GetOrDefault("d", null) is { } cborDLEQ
176-
? new DLEQProof
177-
{
178-
E = ECPrivKey.Create(cborDLEQ["e"].GetByteString()),
179-
S = ECPrivKey.Create(cborDLEQ["s"].GetByteString()),
180-
R = ECPrivKey.Create(cborDLEQ["r"].GetByteString())
181-
}
182-
: null,
183-
Id = id
184-
});
185-
}).ToList()
186-
}
187-
]
188-
};
189-
}
19062
}
+19
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
using System.Text;
2+
using System.Text.Json;
3+
4+
namespace DotNut;
5+
6+
public class CashuTokenV3Encoder : ICashuTokenEncoder
7+
{
8+
public string Encode(CashuToken token)
9+
{
10+
var json = JsonSerializer.Serialize(token);
11+
return Base64UrlSafe.Encode(Encoding.UTF8.GetBytes(json));
12+
}
13+
14+
public CashuToken Decode(string token)
15+
{
16+
var json = Encoding.UTF8.GetString(Base64UrlSafe.Decode(token));
17+
return JsonSerializer.Deserialize<CashuToken>(json)!;
18+
}
19+
}

0 commit comments

Comments
 (0)