From c8bb6b4bb5d9d59dcef324a6f3d57e10e34d92e6 Mon Sep 17 00:00:00 2001 From: d4rp4t Date: Sat, 20 Sep 2025 23:41:41 +0200 Subject: [PATCH 01/70] MintInfo --- DotNut/Abstractions/MintInfo.cs | 293 ++++++++++++++++++ DotNut/ApiModels/{ => Info}/ContactInfo.cs | 0 .../ApiModels/{ => Info}/GetInfoResponse.cs | 2 +- DotNut/ApiModels/Info/MPPInfo.cs | 18 ++ DotNut/ApiModels/Info/SwapInfo.cs | 37 +++ DotNut/ApiModels/Info/WebSocketSupport.cs | 13 + 6 files changed, 362 insertions(+), 1 deletion(-) create mode 100644 DotNut/Abstractions/MintInfo.cs rename DotNut/ApiModels/{ => Info}/ContactInfo.cs (100%) rename DotNut/ApiModels/{ => Info}/GetInfoResponse.cs (96%) create mode 100644 DotNut/ApiModels/Info/MPPInfo.cs create mode 100644 DotNut/ApiModels/Info/SwapInfo.cs create mode 100644 DotNut/ApiModels/Info/WebSocketSupport.cs diff --git a/DotNut/Abstractions/MintInfo.cs b/DotNut/Abstractions/MintInfo.cs new file mode 100644 index 0000000..d4b0cec --- /dev/null +++ b/DotNut/Abstractions/MintInfo.cs @@ -0,0 +1,293 @@ +using System.Data.Common; +using System.Text.Json; +using System.Text.Json.Serialization; +using DotNut.ApiModels; +using DotNut.ApiModels.Info; + +namespace DotNut.Abstractions; + +public class MintInfo +{ + private readonly GetInfoResponse _mintInfo; + private readonly ProtectedEndpoints? _protectedEndpoints; + + public MintInfo(GetInfoResponse info) + { + _mintInfo = info; + + if (info.Nuts?.TryGetValue(22, out var nut22Json) == true) + { + try + { + var nut22 = JsonSerializer.Deserialize(nut22Json.RootElement.GetRawText()); + if (nut22?.ProtectedEndpoints != null) + { + _protectedEndpoints = new ProtectedEndpoints + { + Cache = new Dictionary(), + ApiReturn = nut22.ProtectedEndpoints.Select(o => new ProtectedEndpoint + { + Method = o.Method, + Regex = new System.Text.RegularExpressions.Regex(o.Path) + }).ToArray() + }; + } + } + catch (JsonException) + { + // Ignore parsing errors for NUT-22 + } + } + } + + /// + /// Checks support for NUTs 4 and 5 (mint/melt operations) + /// + public SwapInfo IsSupportedMintMelt(int nutNumber) + { + if (nutNumber != 4 && nutNumber != 5) + throw new ArgumentException("Only NUT 4 and 5 are supported by this method", nameof(nutNumber)); + + return CheckMintMelt(nutNumber); + } + + /// + /// Checks support for generic NUTs (7, 8, 9, 10, 11, 12, 14, 20) + /// + public GenericNut IsSupportedGeneric(int nutNumber) + { + var supportedNuts = new[] { 7, 8, 9, 10, 11, 12, 14, 20 }; + if (!supportedNuts.Contains(nutNumber)) + throw new ArgumentException($"NUT {nutNumber} is not supported by this method", nameof(nutNumber)); + + return CheckGenericNut(nutNumber); + } + + /// + /// Checks support for NUT 17 (WebSocket) + /// + public WebSocketSupportResult IsSupportedWebSocket() + { + return CheckNut17(); + } + + /// + /// Checks support for NUT 15 (MPP) + /// + public MppSupport IsSupportedMpp() + { + return CheckNut15(); + } + + /// + /// Determines if an endpoint requires blind authentication token based on NUT-22 + /// + public bool RequiresBlindAuthToken(string path) + { + if (_protectedEndpoints == null) + return false; + + if (_protectedEndpoints.Cache.TryGetValue(path, out var cachedValue)) + return cachedValue; + + var isProtectedEndpoint = _protectedEndpoints.ApiReturn + .Any(e => e.Regex.IsMatch(path)); + + _protectedEndpoints.Cache[path] = isProtectedEndpoint; + return isProtectedEndpoint; + } + + private GenericNut CheckGenericNut(int nutNumber) + { + if (_mintInfo.Nuts?.TryGetValue(nutNumber, out var nutJson) == true) + { + try + { + var nut = JsonSerializer.Deserialize(nutJson.RootElement.GetRawText()); + return new GenericNut { Supported = nut?.Supported == true }; + } + catch (JsonException) + { + return new GenericNut { Supported = false }; + } + } + return new GenericNut { Supported = false }; + } + + private SwapInfo CheckMintMelt(int nutNumber) + { + if (_mintInfo.Nuts?.TryGetValue(nutNumber, out var nutJson) == true) + { + try + { + var nut = JsonSerializer.Deserialize(nutJson.RootElement.GetRawText()); + if (nut?.Methods != null && nut.Methods.Length > 0 && nut.Disabled != true) + { + return new SwapInfo + { + Disabled = false, + Methods = nut.Methods + }; + } + return new SwapInfo + { + Disabled = true, + Methods = nut?.Methods ?? [] + }; + } + catch (JsonException) + { + return new SwapInfo + { + Disabled = true, + Methods = [] + }; + } + } + return new SwapInfo + { + Disabled = true, + Methods = [] + }; + } + + private WebSocketSupportResult CheckNut17() + { + if (_mintInfo.Nuts?.TryGetValue(17, out var nutJson) == true) + { + try + { + var nut = JsonSerializer.Deserialize(nutJson.RootElement.GetRawText()); + if (nut?.Supported != null && nut.Supported.Length > 0) + { + return new WebSocketSupportResult + { + Supported = true, + Params = nut.Supported + }; + } + } + catch (JsonException) + { + // Ignore parsing errors + } + } + return new WebSocketSupportResult { Supported = false }; + } + + private MppSupport CheckNut15() + { + if (_mintInfo.Nuts?.TryGetValue(15, out var nutJson) == true) + { + try + { + var nut = JsonSerializer.Deserialize(nutJson.RootElement.GetRawText()); + if (nut?.Methods != null && nut.Methods.Length > 0) + { + return new MppSupport + { + Supported = true, + Methods = nut.Methods + }; + } + } + catch (JsonException) + { + // Ignore parsing errors + } + } + return new MppSupport() { Supported = false }; + } + + public bool SupportsBolt12Description + { + get + { + if (_mintInfo.Nuts?.TryGetValue(4, out var nut4Json) == true) + { + try + { + var nut4 = JsonSerializer.Deserialize(nut4Json.RootElement.GetRawText()); + return nut4?.Methods?.Any(method => + method.Method == "bolt12" && method.Options?.Description == true) == true; + } + catch (JsonException) + { + return false; + } + } + return false; + } + } + + + public string? Contact => _mintInfo.Contact?.FirstOrDefault()?.ToString(); + public string? Description => _mintInfo.Description; + public string? DescriptionLong => _mintInfo.DescriptionLong; + public string? Name => _mintInfo.Name; + public string? Pubkey => _mintInfo.Pubkey; + public Dictionary? Nuts => _mintInfo.Nuts; + public string? Version => _mintInfo.Version; + public string? Motd => _mintInfo.Motd; +} + +// Supporting classes for different NUT types +public class GenericNut +{ + [JsonPropertyName("supported")] + public bool Supported { get; set; } +} + +public class MintMeltNut +{ + [JsonPropertyName("methods")] + public SwapInfo.SwapMethod[]? Methods { get; set; } + + [JsonPropertyName("disabled")] + public bool? Disabled { get; set; } +} + +public class WebSocketNut +{ + [JsonPropertyName("supported")] + public WebSocketSupport[]? Supported { get; set; } +} + +public class Nut22 +{ + [JsonPropertyName("protected_endpoints")] + public ProtectedEndpointSpec[]? ProtectedEndpoints { get; set; } +} + +public class ProtectedEndpointSpec +{ + [JsonPropertyName("method")] + public string Method { get; set; } = string.Empty; + + [JsonPropertyName("path")] + public string Path { get; set; } = string.Empty; +} + +// Internal classes for protected endpoints caching +internal class ProtectedEndpoints +{ + public Dictionary Cache { get; set; } = new(); + public ProtectedEndpoint[] ApiReturn { get; set; } = Array.Empty(); +} + +internal class ProtectedEndpoint +{ + public string Method { get; set; } = string.Empty; + public System.Text.RegularExpressions.Regex Regex { get; set; } = null!; +} + +public class WebSocketSupportResult +{ + public bool Supported { get; set; } + public WebSocketSupport[]? Params { get; set; } +} + +public class MppSupport : MmpInfo +{ + public bool Supported { get; set; } +} diff --git a/DotNut/ApiModels/ContactInfo.cs b/DotNut/ApiModels/Info/ContactInfo.cs similarity index 100% rename from DotNut/ApiModels/ContactInfo.cs rename to DotNut/ApiModels/Info/ContactInfo.cs diff --git a/DotNut/ApiModels/GetInfoResponse.cs b/DotNut/ApiModels/Info/GetInfoResponse.cs similarity index 96% rename from DotNut/ApiModels/GetInfoResponse.cs rename to DotNut/ApiModels/Info/GetInfoResponse.cs index 4e7b4ea..0ded0bd 100644 --- a/DotNut/ApiModels/GetInfoResponse.cs +++ b/DotNut/ApiModels/Info/GetInfoResponse.cs @@ -53,5 +53,5 @@ public class GetInfoResponse [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] [JsonPropertyName("nuts")] - public Dictionary? Nuts { get; set; } + public Dictionary? Nuts { get; set; } } \ No newline at end of file diff --git a/DotNut/ApiModels/Info/MPPInfo.cs b/DotNut/ApiModels/Info/MPPInfo.cs new file mode 100644 index 0000000..d1dce29 --- /dev/null +++ b/DotNut/ApiModels/Info/MPPInfo.cs @@ -0,0 +1,18 @@ +using System.Text.Json.Serialization; + +namespace DotNut.ApiModels.Info; + +public class MmpInfo +{ + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] + [JsonPropertyName("methods")] + public MPPMethod[]? Methods {get; set;} + public class MPPMethod + { + [JsonPropertyName("method")] + public string Method {get; set;} + + [JsonPropertyName("unit")] + public string Unit { get; set; } + } +} diff --git a/DotNut/ApiModels/Info/SwapInfo.cs b/DotNut/ApiModels/Info/SwapInfo.cs new file mode 100644 index 0000000..d0b3a8b --- /dev/null +++ b/DotNut/ApiModels/Info/SwapInfo.cs @@ -0,0 +1,37 @@ +using System.Text.Json.Serialization; + +namespace DotNut.ApiModels; + +public class SwapInfo +{ + [JsonPropertyName("methods")] + public SwapMethod[] Methods { get; set; } + + [JsonPropertyName("disabled")] + public bool Disabled { get; set; } + + public class SwapMethod + { + [JsonPropertyName("method")] + public string Method {get; set;} + + [JsonPropertyName("unit")] + public string Unit {get; set;} + + [JsonPropertyName("min_amount")] + public ulong MinAmount { get; set; } + + [JsonPropertyName("max_amount")] + public ulong MaxAmount { get; set; } + + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] + [JsonPropertyName("options")] + public SwapOptions? Options {get; set;} + + public class SwapOptions + { + [JsonPropertyName("description")] + public bool? Description; + } + } +} diff --git a/DotNut/ApiModels/Info/WebSocketSupport.cs b/DotNut/ApiModels/Info/WebSocketSupport.cs new file mode 100644 index 0000000..be12e35 --- /dev/null +++ b/DotNut/ApiModels/Info/WebSocketSupport.cs @@ -0,0 +1,13 @@ +using System.Text.Json.Serialization; + +namespace DotNut.ApiModels; + +public class WebSocketSupport +{ + [JsonPropertyName("method")] + public string Method { get; set; } + [JsonPropertyName("unit")] + public string Unit {get; set;} + [JsonPropertyName("commands")] + public string[] Commands { get; set; } +} \ No newline at end of file From 6cb88120cd773e5fe1bb20917f2309c82cff5374 Mon Sep 17 00:00:00 2001 From: d4rp4t Date: Mon, 22 Sep 2025 19:18:42 +0200 Subject: [PATCH 02/70] cleanup --- DotNut/{ => Abstractions}/ProofSelector.cs | 9 ++------- DotNut/Api/CashuHttpClient.cs | 2 +- DotNut/{ => NUT00}/BlindSignature.cs | 0 DotNut/{ => NUT00}/BlindedMessage.cs | 0 DotNut/{ => NUT00}/Cashu.cs | 0 DotNut/{ => NUT00}/CashuToken.cs | 0 DotNut/{ => NUT00}/ISecret.cs | 0 DotNut/{ => NUT00}/Proof.cs | 0 DotNut/{ => NUT00}/StringSecret.cs | 0 DotNut/{ => NUT01}/Keyset.cs | 0 DotNut/{ => NUT02}/FeeHelper.cs | 0 DotNut/{ => NUT02}/KeysetId.cs | 0 DotNut/{ => NUT04}/MintMethodSetting.cs | 0 DotNut/{ => NUT05}/MeltMethodSetting.cs | 0 DotNut/{ => NUT10}/Nut10ProofSecret.cs | 0 DotNut/{ => NUT10}/Nut10Secret.cs | 0 DotNut/{ => NUT11}/P2PKProofSecret.cs | 0 DotNut/{ => NUT11}/P2PKWitness.cs | 0 DotNut/{P2PKBuilder.cs => NUT11/P2PkBuilder.cs} | 0 DotNut/{ => NUT12}/DLEQ.cs | 0 DotNut/{ => NUT12}/DLEQProof.cs | 0 DotNut/{ => NUT14}/HTLCBuilder.cs | 0 DotNut/{ => NUT14}/HTLCProofSecret.cs | 0 DotNut/{ => NUT14}/HTLCWitness.cs | 0 DotNut/{ => NUT15}/MultipathPaymentSetting.cs | 0 DotNut/{ => NUT18}/HttpPaymentRequestInterfaceHandler.cs | 0 DotNut/{ => NUT18}/PaymentRequest.cs | 0 DotNut/{ => NUT18}/PaymentRequestEncoder.cs | 0 DotNut/{ => NUT18}/PaymentRequestInterfaceHandler.cs | 0 DotNut/{ => NUT18}/PaymentRequestPayload.cs | 0 DotNut/{ => NUT18}/PaymentRequestTransport.cs | 0 DotNut/{ => NUT18}/PaymentRequestTransportInitiator.cs | 0 DotNut/NUT18/PaymentRequestTransportTag.cs | 7 +++++++ DotNut/PrivKey.cs | 5 +++++ 34 files changed, 15 insertions(+), 8 deletions(-) rename DotNut/{ => Abstractions}/ProofSelector.cs (98%) rename DotNut/{ => NUT00}/BlindSignature.cs (100%) rename DotNut/{ => NUT00}/BlindedMessage.cs (100%) rename DotNut/{ => NUT00}/Cashu.cs (100%) rename DotNut/{ => NUT00}/CashuToken.cs (100%) rename DotNut/{ => NUT00}/ISecret.cs (100%) rename DotNut/{ => NUT00}/Proof.cs (100%) rename DotNut/{ => NUT00}/StringSecret.cs (100%) rename DotNut/{ => NUT01}/Keyset.cs (100%) rename DotNut/{ => NUT02}/FeeHelper.cs (100%) rename DotNut/{ => NUT02}/KeysetId.cs (100%) rename DotNut/{ => NUT04}/MintMethodSetting.cs (100%) rename DotNut/{ => NUT05}/MeltMethodSetting.cs (100%) rename DotNut/{ => NUT10}/Nut10ProofSecret.cs (100%) rename DotNut/{ => NUT10}/Nut10Secret.cs (100%) rename DotNut/{ => NUT11}/P2PKProofSecret.cs (100%) rename DotNut/{ => NUT11}/P2PKWitness.cs (100%) rename DotNut/{P2PKBuilder.cs => NUT11/P2PkBuilder.cs} (100%) rename DotNut/{ => NUT12}/DLEQ.cs (100%) rename DotNut/{ => NUT12}/DLEQProof.cs (100%) rename DotNut/{ => NUT14}/HTLCBuilder.cs (100%) rename DotNut/{ => NUT14}/HTLCProofSecret.cs (100%) rename DotNut/{ => NUT14}/HTLCWitness.cs (100%) rename DotNut/{ => NUT15}/MultipathPaymentSetting.cs (100%) rename DotNut/{ => NUT18}/HttpPaymentRequestInterfaceHandler.cs (100%) rename DotNut/{ => NUT18}/PaymentRequest.cs (100%) rename DotNut/{ => NUT18}/PaymentRequestEncoder.cs (100%) rename DotNut/{ => NUT18}/PaymentRequestInterfaceHandler.cs (100%) rename DotNut/{ => NUT18}/PaymentRequestPayload.cs (100%) rename DotNut/{ => NUT18}/PaymentRequestTransport.cs (100%) rename DotNut/{ => NUT18}/PaymentRequestTransportInitiator.cs (100%) create mode 100644 DotNut/NUT18/PaymentRequestTransportTag.cs diff --git a/DotNut/ProofSelector.cs b/DotNut/Abstractions/ProofSelector.cs similarity index 98% rename from DotNut/ProofSelector.cs rename to DotNut/Abstractions/ProofSelector.cs index 22f7caf..356ad91 100644 --- a/DotNut/ProofSelector.cs +++ b/DotNut/Abstractions/ProofSelector.cs @@ -1,16 +1,11 @@ using System.Diagnostics; +using DotNut.Abstractions; namespace DotNut; -public class SendResponse -{ - public List Keep { get; set; } = new(); - public List Send { get; set; } = new(); -} - // Borrowed from cashu-ts // see https://github.com/cashubtc/cashu-ts/pull/314 -public class ProofSelector +public class ProofSelector : IProofSelector { private class ProofWithFee { diff --git a/DotNut/Api/CashuHttpClient.cs b/DotNut/Api/CashuHttpClient.cs index cd156bb..a62fc6b 100644 --- a/DotNut/Api/CashuHttpClient.cs +++ b/DotNut/Api/CashuHttpClient.cs @@ -9,7 +9,7 @@ namespace DotNut.Api; public class CashuHttpClient : ICashuApi { private readonly HttpClient _httpClient; - + public CashuHttpClient(HttpClient httpClient) { _httpClient = httpClient; diff --git a/DotNut/BlindSignature.cs b/DotNut/NUT00/BlindSignature.cs similarity index 100% rename from DotNut/BlindSignature.cs rename to DotNut/NUT00/BlindSignature.cs diff --git a/DotNut/BlindedMessage.cs b/DotNut/NUT00/BlindedMessage.cs similarity index 100% rename from DotNut/BlindedMessage.cs rename to DotNut/NUT00/BlindedMessage.cs diff --git a/DotNut/Cashu.cs b/DotNut/NUT00/Cashu.cs similarity index 100% rename from DotNut/Cashu.cs rename to DotNut/NUT00/Cashu.cs diff --git a/DotNut/CashuToken.cs b/DotNut/NUT00/CashuToken.cs similarity index 100% rename from DotNut/CashuToken.cs rename to DotNut/NUT00/CashuToken.cs diff --git a/DotNut/ISecret.cs b/DotNut/NUT00/ISecret.cs similarity index 100% rename from DotNut/ISecret.cs rename to DotNut/NUT00/ISecret.cs diff --git a/DotNut/Proof.cs b/DotNut/NUT00/Proof.cs similarity index 100% rename from DotNut/Proof.cs rename to DotNut/NUT00/Proof.cs diff --git a/DotNut/StringSecret.cs b/DotNut/NUT00/StringSecret.cs similarity index 100% rename from DotNut/StringSecret.cs rename to DotNut/NUT00/StringSecret.cs diff --git a/DotNut/Keyset.cs b/DotNut/NUT01/Keyset.cs similarity index 100% rename from DotNut/Keyset.cs rename to DotNut/NUT01/Keyset.cs diff --git a/DotNut/FeeHelper.cs b/DotNut/NUT02/FeeHelper.cs similarity index 100% rename from DotNut/FeeHelper.cs rename to DotNut/NUT02/FeeHelper.cs diff --git a/DotNut/KeysetId.cs b/DotNut/NUT02/KeysetId.cs similarity index 100% rename from DotNut/KeysetId.cs rename to DotNut/NUT02/KeysetId.cs diff --git a/DotNut/MintMethodSetting.cs b/DotNut/NUT04/MintMethodSetting.cs similarity index 100% rename from DotNut/MintMethodSetting.cs rename to DotNut/NUT04/MintMethodSetting.cs diff --git a/DotNut/MeltMethodSetting.cs b/DotNut/NUT05/MeltMethodSetting.cs similarity index 100% rename from DotNut/MeltMethodSetting.cs rename to DotNut/NUT05/MeltMethodSetting.cs diff --git a/DotNut/Nut10ProofSecret.cs b/DotNut/NUT10/Nut10ProofSecret.cs similarity index 100% rename from DotNut/Nut10ProofSecret.cs rename to DotNut/NUT10/Nut10ProofSecret.cs diff --git a/DotNut/Nut10Secret.cs b/DotNut/NUT10/Nut10Secret.cs similarity index 100% rename from DotNut/Nut10Secret.cs rename to DotNut/NUT10/Nut10Secret.cs diff --git a/DotNut/P2PKProofSecret.cs b/DotNut/NUT11/P2PKProofSecret.cs similarity index 100% rename from DotNut/P2PKProofSecret.cs rename to DotNut/NUT11/P2PKProofSecret.cs diff --git a/DotNut/P2PKWitness.cs b/DotNut/NUT11/P2PKWitness.cs similarity index 100% rename from DotNut/P2PKWitness.cs rename to DotNut/NUT11/P2PKWitness.cs diff --git a/DotNut/P2PKBuilder.cs b/DotNut/NUT11/P2PkBuilder.cs similarity index 100% rename from DotNut/P2PKBuilder.cs rename to DotNut/NUT11/P2PkBuilder.cs diff --git a/DotNut/DLEQ.cs b/DotNut/NUT12/DLEQ.cs similarity index 100% rename from DotNut/DLEQ.cs rename to DotNut/NUT12/DLEQ.cs diff --git a/DotNut/DLEQProof.cs b/DotNut/NUT12/DLEQProof.cs similarity index 100% rename from DotNut/DLEQProof.cs rename to DotNut/NUT12/DLEQProof.cs diff --git a/DotNut/HTLCBuilder.cs b/DotNut/NUT14/HTLCBuilder.cs similarity index 100% rename from DotNut/HTLCBuilder.cs rename to DotNut/NUT14/HTLCBuilder.cs diff --git a/DotNut/HTLCProofSecret.cs b/DotNut/NUT14/HTLCProofSecret.cs similarity index 100% rename from DotNut/HTLCProofSecret.cs rename to DotNut/NUT14/HTLCProofSecret.cs diff --git a/DotNut/HTLCWitness.cs b/DotNut/NUT14/HTLCWitness.cs similarity index 100% rename from DotNut/HTLCWitness.cs rename to DotNut/NUT14/HTLCWitness.cs diff --git a/DotNut/MultipathPaymentSetting.cs b/DotNut/NUT15/MultipathPaymentSetting.cs similarity index 100% rename from DotNut/MultipathPaymentSetting.cs rename to DotNut/NUT15/MultipathPaymentSetting.cs diff --git a/DotNut/HttpPaymentRequestInterfaceHandler.cs b/DotNut/NUT18/HttpPaymentRequestInterfaceHandler.cs similarity index 100% rename from DotNut/HttpPaymentRequestInterfaceHandler.cs rename to DotNut/NUT18/HttpPaymentRequestInterfaceHandler.cs diff --git a/DotNut/PaymentRequest.cs b/DotNut/NUT18/PaymentRequest.cs similarity index 100% rename from DotNut/PaymentRequest.cs rename to DotNut/NUT18/PaymentRequest.cs diff --git a/DotNut/PaymentRequestEncoder.cs b/DotNut/NUT18/PaymentRequestEncoder.cs similarity index 100% rename from DotNut/PaymentRequestEncoder.cs rename to DotNut/NUT18/PaymentRequestEncoder.cs diff --git a/DotNut/PaymentRequestInterfaceHandler.cs b/DotNut/NUT18/PaymentRequestInterfaceHandler.cs similarity index 100% rename from DotNut/PaymentRequestInterfaceHandler.cs rename to DotNut/NUT18/PaymentRequestInterfaceHandler.cs diff --git a/DotNut/PaymentRequestPayload.cs b/DotNut/NUT18/PaymentRequestPayload.cs similarity index 100% rename from DotNut/PaymentRequestPayload.cs rename to DotNut/NUT18/PaymentRequestPayload.cs diff --git a/DotNut/PaymentRequestTransport.cs b/DotNut/NUT18/PaymentRequestTransport.cs similarity index 100% rename from DotNut/PaymentRequestTransport.cs rename to DotNut/NUT18/PaymentRequestTransport.cs diff --git a/DotNut/PaymentRequestTransportInitiator.cs b/DotNut/NUT18/PaymentRequestTransportInitiator.cs similarity index 100% rename from DotNut/PaymentRequestTransportInitiator.cs rename to DotNut/NUT18/PaymentRequestTransportInitiator.cs diff --git a/DotNut/NUT18/PaymentRequestTransportTag.cs b/DotNut/NUT18/PaymentRequestTransportTag.cs new file mode 100644 index 0000000..d3ed4ad --- /dev/null +++ b/DotNut/NUT18/PaymentRequestTransportTag.cs @@ -0,0 +1,7 @@ +namespace DotNut; + +public class PaymentRequestTransportTag +{ + public string Key { get; set; } + public string Value { get; set; } +} \ No newline at end of file diff --git a/DotNut/PrivKey.cs b/DotNut/PrivKey.cs index 0b6861e..033d0b2 100644 --- a/DotNut/PrivKey.cs +++ b/DotNut/PrivKey.cs @@ -14,6 +14,11 @@ public PrivKey(string hex) Key = hex.ToPrivKey(); } + public PrivKey(byte[] bytes) + { + Key = Convert.ToHexString(bytes).ToPrivKey(); + } + private PrivKey(ECPrivKey ecPrivKey) { Key = ecPrivKey; From 2eb6ebbd6873ef83e88d45bfa2b98e3ae8a068f0 Mon Sep 17 00:00:00 2001 From: d4rp4t Date: Mon, 22 Sep 2025 20:30:17 +0200 Subject: [PATCH 03/70] Counter and CashuUtils --- DotNut/Abstractions/CashuUtils.cs | 210 ++++++++++++++++++++++++++++++ DotNut/Abstractions/Counter.cs | 25 ++++ DotNut/NUT01/Keyset.cs | 11 +- 3 files changed, 242 insertions(+), 4 deletions(-) create mode 100644 DotNut/Abstractions/CashuUtils.cs create mode 100644 DotNut/Abstractions/Counter.cs diff --git a/DotNut/Abstractions/CashuUtils.cs b/DotNut/Abstractions/CashuUtils.cs new file mode 100644 index 0000000..e34f290 --- /dev/null +++ b/DotNut/Abstractions/CashuUtils.cs @@ -0,0 +1,210 @@ +using System.Security.Cryptography; +using DotNut.NUT13; +using NBitcoin.Secp256k1; + +namespace DotNut.Abstractions; + +public class CashuUtils +{ + /// + /// Function mapping payment amount to keyset supported amounts in order to create swap payload. Always tries to fit the biggest proof. + /// + /// Amount that has to be covered. + /// Mints keyset> + /// List of ulong proof amounts for given keyset + public static List SplitToProofsAmounts(ulong paymentAmount, Keyset keyset) + { + var outputAmounts = new List(); + var possibleValues = keyset.Keys.OrderByDescending(x => x).ToList(); + foreach (var value in possibleValues) + { + while (paymentAmount >= value) + { + outputAmounts.Add(value); + paymentAmount -= value; + } + + if (paymentAmount == 0) + { + break; + } + } + + return outputAmounts; + } + + /// + /// Creates blank outputs (see nut-08) + /// + /// Amount that blank outputs have to cover + /// Active keyset id which will sign outputs + /// Keys for given KeysetId + /// Blank Outputs + public static OutputData CreateBlankOutputs(ulong amount, KeysetId keysetId, Keyset keys, DotNut.NBitcoin.BIP39.Mnemonic? mnemonic = null, int? counter = null) + { + if (amount == 0) + { + throw new ArgumentException("Cannot create blank outputs zero amount."); + } + + var count = CalculateNumberOfBlankOutputs(amount); + + // Amount is set for 1, they're blank. Mint will automatically set their amount and sign each by pk corresponding to value + var amounts = Enumerable.Repeat((ulong)1, count).ToList(); + return CreateOutputs(amounts, keysetId, keys, mnemonic, counter); + } + + /// + /// Calculates amount of blank outputs needed by mint to return overpaid fees + /// + /// Amount of tokens that has to be covered by mint. + /// Integer amount of blank outputs needed + /// If amount is 0 - idk why someone would do that + private static int CalculateNumberOfBlankOutputs(ulong amountToCover) + { + if (amountToCover == 0) + { + return 0; + } + + return Math.Max( + Convert.ToInt32( + Math.Ceiling( + Math.Log2(amountToCover) + ) + ), 1); + } + + /// + /// Creates outputs for swap/melt fee return. Outputs should have valid amounts. + /// + /// Amounts for each output (e.g. [1,2,4,8] + /// ID of keyset we want to receive the proofs + /// Keyset for given ID + /// + /// + public static OutputData CreateOutputs( + List amounts, + KeysetId keysetId, + Keyset keys, + NBitcoin.BIP39.Mnemonic? mnemonic = null, + int? counter = null) + { + if (amounts.Any(a => !keys.Keys.Contains(a))) + throw new ArgumentException("Invalid amounts"); + + var blindedMessages = new List(amounts.Count); + var secrets = new List(amounts.Count); + var blindingFactors = new List(amounts.Count); + + Func secretFactory; + Func blindingFactorFactory; + + if (mnemonic is not null && counter is { } c) + { + secretFactory = () => mnemonic.DeriveSecret(keysetId, c); + blindingFactorFactory = () => new PrivKey( + Convert.ToHexString(mnemonic.DeriveBlindingFactor(keysetId, c)) + ); + } + else + { + secretFactory = () => + { + var bytes = RandomNumberGenerator.GetBytes(32); + return new StringSecret(Convert.ToHexString(bytes)); + }; + + blindingFactorFactory = () => + { + var bytes = RandomNumberGenerator.GetBytes(32); + return new PrivKey(Convert.ToHexString(bytes)); + }; + } + + foreach (var amount in amounts) + { + var secret = secretFactory(); + secrets.Add(secret); + + var r = blindingFactorFactory(); + blindingFactors.Add(r); + + var B_ = DotNut.Cashu.ComputeB_(secret.ToCurve(), r); + blindedMessages.Add(new BlindedMessage() { Amount = amount, B_ = B_, Id = keysetId }); + } + + return new OutputData() + { + BlindingFactors = blindingFactors.ToArray(), + BlindedMessages = blindedMessages.ToArray(), + Secrets = secrets.ToArray() + }; + } + + /// + /// Method creating proofs, from provided promises (blinded signatures) + /// + /// Blinded Signature + /// Blinding factor + /// Yeah, secret + /// Key, corresponding to proof amount + /// Valid proof + public static Proof ConstructProofFromPromise( + BlindSignature promise, + PrivKey r, + DotNut.ISecret secret, + PubKey amountPubkey) + { + + //unblind signature + var C = DotNut.Cashu.ComputeC(promise.C_, r, amountPubkey); + + if (promise.DLEQ is not null) + { + promise.DLEQ = new DLEQProof + { + E = promise.DLEQ.E, + S = promise.DLEQ.S, + R = r + }; + } + + return new Proof + { + Id = promise.Id, + Amount = promise.Amount, + Secret = secret, + C = C, + DLEQ = promise.DLEQ, + }; + } + + public static List ConstructProofsFromPromises( + List promises, + OutputData outputs, + Keyset keys + ) + { + List proofs = new List(); + for (int i = promises.Count() - 1; i >= 0; i--) + { + if (!keys.TryGetValue(promises[i].Amount, out PubKey key)) + { + throw new ArgumentException($"Provided keyset doesn't contain PubKey for amount {promises[i].Amount}" ); + } + var proof = ConstructProofFromPromise( + promises[i], + outputs.BlindingFactors[i], + outputs.Secrets[i], + key + ); + proofs.Add(proof); + } + return proofs; + } + + + + +} \ No newline at end of file diff --git a/DotNut/Abstractions/Counter.cs b/DotNut/Abstractions/Counter.cs new file mode 100644 index 0000000..21a5af0 --- /dev/null +++ b/DotNut/Abstractions/Counter.cs @@ -0,0 +1,25 @@ +using DotNut; + +public class Counter : Dictionary +{ + public Counter(IDictionary dictionary) : base(dictionary) { } + + public int GetCounterForId(KeysetId keysetId) + { + if (TryGetValue(keysetId, out var counter)) + return counter; + + return this[keysetId] = 0; + } + + public int IncrementCounter(KeysetId keysetId, int bumpBy = 1) + { + var current = GetCounterForId(keysetId); + var next = current + bumpBy; + this[keysetId] = next; + return next; + } + + public void SetCounter(KeysetId keysetId, int counter) => this[keysetId] = counter; + public Counter Clone() => new(this); +} \ No newline at end of file diff --git a/DotNut/NUT01/Keyset.cs b/DotNut/NUT01/Keyset.cs index b831233..aceb73a 100644 --- a/DotNut/NUT01/Keyset.cs +++ b/DotNut/NUT01/Keyset.cs @@ -1,4 +1,6 @@ -using System.Text; +using System.Net.Mime; +using System.Text; +using System.Text.Encodings.Web; using System.Text.Json.Serialization; using DotNut.JsonConverters; using SHA256 = System.Security.Cryptography.SHA256; @@ -51,8 +53,9 @@ public KeysetId GetKeysetId(byte version = 0x00, string? unit = null, ulong? inp // 3 - add the lowercase UTF8-encoded unit string prefixed with "|unit:" to the byte array (e.g. "|unit:sat") if (String.IsNullOrWhiteSpace(unit)) - { - throw new ArgumentNullException( nameof(unit), $"Unit parameter is required with version: {version}"); + { + throw new ArgumentNullException(nameof(unit), + $"Unit parameter is required with version: {version}"); } var unitBytes = Encoding.UTF8.GetBytes($"|unit:{unit.Trim().ToLowerInvariant()}"); @@ -84,7 +87,7 @@ public KeysetId GetKeysetId(byte version = 0x00, string? unit = null, ulong? inp default: throw new ArgumentException($"Unsupported keyset version: {version}"); } - + } public bool VerifyKeysetId(KeysetId keysetId, string? unit = null, ulong? inputFeePpk = null, string? finalExpiration = null) From fc919e0db2b5bf99f703fc42d07f9bb80a9a90e5 Mon Sep 17 00:00:00 2001 From: d4rp4t Date: Wed, 24 Sep 2025 00:41:54 +0200 Subject: [PATCH 04/70] wip --- .github/workflows/ci.yml | 34 +- DotNut/Abstractions/CashuWallet.cs | 674 ++++++++++++++++++ DotNut/Abstractions/Counter.cs | 2 + .../Abstractions/Interfaces/ICashuWallet.cs | 79 ++ .../Abstractions/Interfaces/IProofSelector.cs | 6 + DotNut/Abstractions/OutputData.cs | 8 + DotNut/Abstractions/Quotes/MintQuoteBolt11.cs | 15 + DotNut/Abstractions/Quotes/MintQuoteBolt12.cs | 6 + DotNut/Abstractions/SendResponse.cs | 7 + DotNut/Abstractions/WalletResults.cs | 87 +++ DotNut/Abstractions/WebsocketService.cs | 5 + .../bolt12/PostMeltQuoteBolt12Response.cs | 5 + DotNut/NUT00/Cashu.cs | 108 ++- 13 files changed, 1000 insertions(+), 36 deletions(-) create mode 100644 DotNut/Abstractions/CashuWallet.cs create mode 100644 DotNut/Abstractions/Interfaces/ICashuWallet.cs create mode 100644 DotNut/Abstractions/Interfaces/IProofSelector.cs create mode 100644 DotNut/Abstractions/OutputData.cs create mode 100644 DotNut/Abstractions/Quotes/MintQuoteBolt11.cs create mode 100644 DotNut/Abstractions/Quotes/MintQuoteBolt12.cs create mode 100644 DotNut/Abstractions/SendResponse.cs create mode 100644 DotNut/Abstractions/WalletResults.cs create mode 100644 DotNut/Abstractions/WebsocketService.cs diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index fd8dfba..a714eb9 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -2,6 +2,37 @@ on: push jobs: + tests: + runs-on: ubuntu-latest + steps: + # Checkout the code + - uses: actions/checkout@v5 + + # Install .NET Core SDK + - name: Setup .NET Core + uses: actions/setup-dotnet@v4 + with: + dotnet-version: 8.0.x + + - name: Pull and start mint + run: | + docker run -d -p 3338:3338 \ + --name cdk-mint \ + -e CDK_MINTD_DATABASE=sqlite \ + -e CDK_MINTD_LN_BACKEND=fakewallet \ + -e CDK_MINTD_INPUT_FEE_PPK=100 \ + -e CDK_MINTD_LISTEN_HOST=0.0.0.0 \ + -e CDK_MINTD_LISTEN_PORT=3338 \ + -e CDK_MINTD_FAKE_WALLET_MIN_DELAY=0 \ + -e CDK_MINTD_MNEMONIC='abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about' \ + cashubtc/mintd:latest-amd64 \ + + - name: Wait for mint to be ready + run: | + timeout 60s bash -c 'until curl -f localhost:3338/v1/info; do sleep 2; done' + + - name: Test + run: dotnet test build: runs-on: ubuntu-latest steps: @@ -13,9 +44,6 @@ jobs: uses: actions/setup-dotnet@v1 with: dotnet-version: 8.0.x - - - name: Test - run: dotnet test - name: Publish NuGet if: ${{ github.ref == 'refs/heads/master' }} # Publish only when the push is on master diff --git a/DotNut/Abstractions/CashuWallet.cs b/DotNut/Abstractions/CashuWallet.cs new file mode 100644 index 0000000..79a7e9a --- /dev/null +++ b/DotNut/Abstractions/CashuWallet.cs @@ -0,0 +1,674 @@ +using DotNut.Api; +using DotNut.ApiModels; +using DotNut.NBitcoin.BIP39; + +namespace DotNut.Abstractions; + +// It should: +// Allow user to use local keys from db +// Fetch keyets on start, if user didn't provide them. Fetch additional Keys. (if necessary) +// How can user save these? +// ARE FLUENT BUILDER \ + +/// +/// Main Cashu Wallet class implementing fluent builder pattern +/// +public class CashuWallet : ICashuWalletBuilder +{ + private MintInfo? _info; + private IProofSelector? _selector; + private ICashuApi? _mintApi; + private List? _keysets; + private List? _keys; + private Dictionary? _keysetFees => _keysets?.ToDictionary(k=>k.Id, k=>k.InputFee??0); + private string _unit = "sat"; + + private Mnemonic? _mnemonic; + private Counter? _counter; + + //flags + private bool _shouldSyncKeyset = true; + private bool _shouldBumpCounter = true; + private bool _allowInvalidKeysetIds = false; + + public static ICashuWalletBuilder Create() => new CashuWallet(); + + /// + /// Mandatory. Sets a mint in a wallet object + /// + /// Mint API object. + public ICashuWalletBuilder WithMint(ICashuApi mintApi) + { + _mintApi = mintApi; + return this; + } + + /// + /// Mandatory. Sets a mint in a wallet object (with default CashuHttpClient) + /// + /// Mint URL string. + public ICashuWalletBuilder WithMint(string mintUrl) + { + var httpClient = new HttpClient{ BaseAddress = new Uri(mintUrl)}; + _mintApi = new CashuHttpClient(httpClient); + return this; + } + + /// + /// Optional. Import Mint Info to CashuWallet. Otherwise, it will be fetched from /v1/info endpoint. + /// + /// MintInfo object + public ICashuWalletBuilder WithInfo(MintInfo info) + { + this._info = info; + return this; + } + + /// + /// Optional. Import Mint Info to CashuWallet. Otherwise, it will be fetched from /v1/info endpoint. + /// + /// GetInfoResponse payload returned from mints API + public ICashuWalletBuilder WithInfo(GetInfoResponse info) => this.WithInfo(new MintInfo(info)); + + /// + /// Optional. Import Keysets into CashuWallet class. Otherwise, they will be fetched from /v1/keysets endpoint. + /// + /// List of Keysets + public ICashuWalletBuilder WithKeysets(IEnumerable keysets) + { + this._keysets = keysets.ToList(); + return this; + } + + /// + /// Optional. Import Keysets into CashuWallet class. Otherwise, they will be fetched from /v1/keysets endpoint. + /// + /// GetKeysetsResponse payload returned from mints API + public ICashuWalletBuilder WithKeysets(GetKeysetsResponse keysets) => this.WithKeysets(keysets.Keysets.ToList()); + + /// + /// Optional. Import Keys into CashuWallet class. Otherwise, they will be fetched from /v1/keys endpoint. + /// + /// List of mints Keys + public ICashuWalletBuilder WithKeys(IEnumerable keys) + { + this._keys = keys.ToList(); + return this; + } + + /// + /// Optional. Import Keys into CashuWallet class. Otherwise, they will be fetched from /v1/keys endpoint. + /// + /// GetKeysResponse payload returned from mints API + public ICashuWalletBuilder WithKeys(GetKeysResponse keys) => this.WithKeys(keys.Keysets.ToList()); + + /// + /// Optional. Flag suggesting if CashuWallet should sync provided Keys and Keysets with actual mints state. + /// Very useful if wallet stores keys in storage. + /// + /// boolean, true by default + public ICashuWalletBuilder WithKeysetSync(bool syncKeyset = true) + { + this._shouldSyncKeyset = syncKeyset; + return this; + } + + /// + /// Optional. Base unit of wallet instance. If not set defaults to "SAT". + /// + /// + public ICashuWalletBuilder WithUnit(string unit) + { + this._unit = unit; + return this; + } + + /// + /// Optional. Proof selecting algorithm. If not set, defaults to RGLI proof selector. + /// + /// + public ICashuWalletBuilder WithSelector(IProofSelector selector) + { + _selector = selector; + return this; + } + + /// + /// Optional. BIP39 seed for secret and blinding factors derivation. All proofs generated by CashuWallet will be recoverable. + /// + /// Mnemonic object + public ICashuWalletBuilder WithMnemonic(Mnemonic mnemonic) + { + _mnemonic = mnemonic; + return this; + } + + /// + /// Optional. BIP39 seed for secret and blinding factors derivation. All proofs generated by CashuWallet will be recoverable. + /// + /// Bip39 seed string separated by spaces. + public ICashuWalletBuilder WithMnemonic(string mnemonic) + { + _mnemonic = new Mnemonic(mnemonic); + return this; + } + + /// + /// Optional and mandatory if Mnemonic provided. Counter for each Keyset Id for derivation purposes. + /// + /// Counter object + public ICashuWalletBuilder WithCounter(Counter counter) + { + this._counter = counter; + return this; + } + + /// + /// Optional and mandatory if Mnemonic provided. Counter for each Keyset Id for derivation purposes. + /// + /// Counter dictionary + /// + public ICashuWalletBuilder WithCounter(IDictionary counter) + { + this._counter = new Counter(counter); + return this; + } + + /// + /// Optional and if not set, always true. Controls automatic counter incrementation for secret generation. + /// + /// If true, counter increments automatically. If false, requires manual management. + /// + /// WARNING: Disabling auto-increment is potentially dangerous. Manual counter management is required + /// to prevent secret reuse, which will cause mint rejection and operation failures. + /// + public ICashuWalletBuilder ShouldBumpCounter(bool shouldBumpCounter = true) + { + this._shouldBumpCounter = shouldBumpCounter; + return this; + } + + /// + /// Create swap transaction builder. + /// + /// Swap transaction builder + public async Task Swap() + { + return new CashuWalletSwapBuilder(this); + } + + public async Task CreateMeltQuote() + { + return new CashuWalletMeltQuoteBuilder(this); + } + + public async Task CreateMintQuote() + { + return new CashuWalletMintQuoteBuilder(this); + } + + public async Task Restore() + { + return new CashuWalletRestoreBuilder(this); + } + + /// + /// Wrapper for GetKeysets api endpoint. Formats Keysets to list. + /// + /// List of Keysets + /// May be thrown if mint is not set. + private async Task> _fetchKeysets(CancellationToken cts = default) + { + if (!_ensureApiConnected()) + { + throw new ArgumentNullException(nameof(this._mintApi), "Can't fetch mint info without mintApi"); + } + var keysetsRaw = await _mintApi!.GetKeysets(cts); + return keysetsRaw.Keysets.ToList(); + } + + /// + /// Wrapper for GetKeys api endpoint. Validates returned KeysetIds and formats Keys to list. + /// + /// List of Keys (lists :)) + /// May be thrown if mint is not set. + /// May be thrown if mint returns invalid keysetId for at least one Keyset + private async Task> _fetchKeys(CancellationToken cts = default) + { + if (!_ensureApiConnected()) + { + throw new ArgumentNullException(nameof(_mintApi), "Can't fetch mint info without mintApi"); + } + var keysRaw = await _mintApi!.GetKeys(cts); + foreach (var keysetItemResponse in keysRaw.Keysets) + { + var isKeysetIdValid = keysetItemResponse.Keys.VerifyKeysetId(keysetItemResponse.Id, keysetItemResponse.Unit, keysetItemResponse.FinalExpiry); + if (!isKeysetIdValid) + { + throw new ArgumentException($"Mint provided invalid keysetId. Provided: {keysetItemResponse.Id}, derived: {keysetItemResponse.Keys.GetKeysetId()} "); + } + } + return keysRaw.Keysets.ToList(); + } + + /// + /// Wrapper for GetKeys api endpoint. Validates KeysetId and fetches keys for single KeysetId Formats Keys to list. + /// + /// KeysetId we want fetch keys for. + /// Keys + /// May be thrown if mint returns invalid keysetId for at least one Keyset + /// May be thrown if mint is not set. + private async Task _fetchKeys(KeysetId id, CancellationToken cts = default) + { + if (!_ensureApiConnected()) + { + throw new ArgumentNullException(nameof(_mintApi), "Can't fetch mint info without mintApi"); + } + var keysRaw = await _mintApi!.GetKeys(id, cts); + foreach (var keysetItemResponse in keysRaw.Keysets) + { + var isKeysetIdValid = keysetItemResponse.Keys.VerifyKeysetId(keysetItemResponse.Id, keysetItemResponse.Unit, keysetItemResponse.FinalExpiry); + if (!isKeysetIdValid) + { + throw new ArgumentException($"Mint provided invalid keysetId. Provided: {keysetItemResponse.Id}, derived: {keysetItemResponse.Keys.GetKeysetId()} "); + } + } + return keysRaw.Keysets.Single(); + } + + /// + /// Wrapper for GetInfo api endpoint. Translates Payload to MintInfo. + /// + /// May be thrown if mint is not set. + private async Task _fetchMintInfo(CancellationToken cts = default) + { + if (!_ensureApiConnected()) + { + throw new ArgumentNullException(nameof(this._mintApi), "Can't fetch mint info without mintApi"); + } + + var infoRaw = await _mintApi!.GetInfo(cts); + return new MintInfo(infoRaw); + } + + /// + /// Fetches mint info if not present in CashuWallet. + /// + /// + private async Task _lazyFetchMintInfo(CancellationToken cts = default) + { + if (this._info != null) return this._info; + if (_ensureApiConnected()) + { + throw new ArgumentNullException(nameof(this._mintApi), "Can't fetch mint info without mintApi"); + } + return await this._fetchMintInfo(cts); + } + + /// + /// Local Keys sync. + /// + /// + /// + private async Task _maybeSyncKeys(CancellationToken cts = default) + { + if (!_shouldSyncKeyset) + { + return; + } + if (!_ensureApiConnected()) + { + throw new ArgumentNullException(nameof(this._mintApi), "Can't sync mint keys without mintApi"); + } + + this._keysets = await _fetchKeysets(cts); + if (_keys == null) + { + this._keys = await _fetchKeys(cts); // we're fetching all keys here, so no need for additional check. + return; + } + + var knownIds = _keys.Select(key => key.Id).ToHashSet(); + var unknownKeysets = _keysets.Where(k => !knownIds.Contains(k.Id)).ToList(); + + if (unknownKeysets.Count > 2) // just make a single request. May override stored keys. + { + this._keys = await _fetchKeys(cts); + return; + } + + foreach (var unknownKeyset in unknownKeysets) + { + var keyset = await this._fetchKeys(unknownKeyset.Id, cts); + this._keys.Add(keyset); + } + } + public async Task? GetActiveKeysetId(CancellationToken cts = default) + { + return _keysets? + .OrderBy(k => k.InputFee) + .FirstOrDefault(k => k.Active == true && k.Unit == this._unit, null) + ?.Id; + } + public async Task> GetKeys(bool forceRefresh = false, CancellationToken cts = default) + { + if (forceRefresh) + { + this._keys = await _fetchKeys(cts); + } + return this._keys ?? []; + } + public async Task> GetKeysets(bool forceRefresh = false, CancellationToken cts = default) + { + if (forceRefresh) + { + this._keysets = await _fetchKeysets(cts); + } + + return _keysets ?? []; + } + public async Task GetInfo(bool forceReferesh = false, CancellationToken cts = default) + { + if (forceReferesh) + { + return await _fetchMintInfo(cts); + } + return await _lazyFetchMintInfo(cts); + } + public async Task CreateOutputs(List amounts, KeysetId id, CancellationToken cts = default) + { + if (this._keys == null) + { + throw new ArgumentNullException(nameof(this._keys), "No Keys found. Make sure to fetch them!"); + } + var keyset = this._keys.Single(k => k.Id == id); + if (this._mnemonic == null) + { + return CashuUtils.CreateOutputs(amounts, id, keyset.Keys); + } + + if (this._counter == null) + { + throw new ArgumentNullException(nameof(Counter), "Can't derive outputs without keyset counter"); + } + + var counterValue = this._counter.GetCounterForId(id); + if (_shouldBumpCounter) + { + this._counter.IncrementCounter(id, amounts.Count); + } + return CashuUtils.CreateOutputs(amounts, id, keyset.Keys, this._mnemonic, counterValue); + } + + public IProofSelector? GetSelector() => _selector; + public ICashuApi? GetMintApi() => _mintApi; + public Mnemonic? GetMnemonic() => _mnemonic; + public string GetUnit() => _unit; + public Counter? GetCounter() => _counter; + private bool _ensureApiConnected() => _mintApi != null; +} + + + +/// +/// Receive operation builder implementation +/// +internal class CashuWalletSwapBuilder : ICashuWalletSwapBuilder +{ + private readonly CashuWallet _wallet; + + // input + private readonly string? _tokenString; + private readonly CashuToken? _token; + private List? _proofsToSwap; + + private OutputData? _outputs; + private List? _amounts; + private KeysetId? _keysetId; + + private bool _verifySignatures = true; + + public CashuWalletSwapBuilder(CashuWallet wallet, string tokenString) + { + _wallet = wallet; + _tokenString = tokenString; + } + + public CashuWalletSwapBuilder(CashuWallet wallet, CashuToken token) + { + _wallet = wallet; + _token = token; + } + + public CashuWalletSwapBuilder(CashuWallet wallet) + { + _wallet = wallet; + } + + public ICashuWalletSwapBuilder WithSignatureVerification(bool verify = true) + { + _verifySignatures = verify; + return this; + } + + public ICashuWalletSwapBuilder WithOutputs(OutputData outputs) + { + _outputs = outputs; + return this; + } + + public ICashuWalletSwapBuilder WithAmounts(IEnumerable amounts) + { + _amounts = amounts.ToList(); + return this; + } + + public ICashuWalletSwapBuilder ForKeyset(KeysetId keysetId) + { + _keysetId = keysetId; + return this; + } + + private async Task> _getSwapProofs() + { + _proofsToSwap ??= new(); + if (_tokenString != null) + { + var token = CashuTokenHelper.Decode(this._tokenString, out var v); + if (v == "A") // todo ensure + { + //if token is v1, ensure everything is from the same mint + var mints = token.Tokens.Select(t => t.Mint).ToList(); + if (mints.Count > 1) + { + throw new ArgumentException("Only swap from single mint is allowed"); + } + + } + this._proofsToSwap.AddRange(token.Tokens.SelectMany(t=>t.Proofs)); + } + + if (_token == null) return _proofsToSwap; + + //if token is v1, ensure everything is from the same mint + var tokenMints = _token.Tokens.Select(t => t.Mint).ToList(); + if (tokenMints.Count > 1) + { + throw new ArgumentException("Only swap from single mint is allowed"); + } + this._proofsToSwap.AddRange(_token.Tokens.SelectMany(t=>t.Proofs)); + + return _proofsToSwap; + } + + + public async Task> ProcessAsync(CancellationToken cts = default) + { + if (_wallet.GetMintApi() == null) + throw new InvalidOperationException("Mint API must be configured"); + + await _getSwapProofs(); + if (_proofsToSwap == null || _proofsToSwap.Count == 0) + { + throw new ArgumentException("Nothing to swap!"); + } + + // if there's no keysetId specified - let's choose it. + if (_keysetId == null) + { + _keysetId = await _wallet.GetActiveKeysetId(cts) ?? + throw new InvalidOperationException("Could not fetch Keyset ID"); + } + var keys = await _wallet.GetKeys(false, cts); + var keysForCurrentId = keys.Single(k=>k.Id == _keysetId); + + if (_verifySignatures) + { + foreach (var proof in _proofsToSwap!) + { + var keyset = keys.Single(k => k.Id == proof.Id); + if (keyset.Keys.TryGetValue(proof.Amount, out var key)) + { + throw new InvalidOperationException($"Can't find key for amount {proof.Amount} in keyset ${keyset.Id}"); + } + var isValid = proof.Verify(key); + if (!isValid) + throw new InvalidOperationException($"Invalid proof signature for amount {proof.Amount}"); + } + } + + ulong total = _proofsToSwap!.Aggregate(0, (acc, p) => acc + p.Amount); + // Swap received proofs to our keyset + var amounts = _amounts ?? CashuUtils.SplitToProofsAmounts(total, keysForCurrentId.Keys); + + this._outputs ??= await this._wallet.CreateOutputs(amounts, _keysetId, cts); + + var request = new PostSwapRequest() + { + Inputs = this._proofsToSwap.ToArray(), + Outputs = this._outputs.BlindedMessages, + }; + + + var swapResponse = await _wallet.GetMintApi()!.Swap(request, cts); + + var swappedProofs = + CashuUtils.ConstructProofsFromPromises(swapResponse.Signatures.ToList(), this._outputs, keysForCurrentId.Keys); + + return swappedProofs; + } +} + + +internal class CashuWalletMeltQuoteBuilder : ICashuWalletMeltQuoteBuilder +{ + private readonly CashuWallet _wallet; + private List? _proofs; + private string? _invoice; + private OutputData? _blankOutputs; + private ulong? _amount; + public CashuWalletMeltQuoteBuilder(CashuWallet wallet) + { + _wallet = wallet; + } + + public ICashuWalletMeltQuoteBuilder WithQuote(string quoteId) + { + throw new NotImplementedException(); + } + + public ICashuWalletMeltQuoteBuilder WithInvoice(string invoice) + { + this._invoice = invoice; + return this; + } + + public ICashuWalletMeltQuoteBuilder WithMethod(string method = "bolt11") + { + throw new NotImplementedException(); + } + + public Task ProcessAsync(CancellationToken cancellationToken = default) + { + throw new NotImplementedException(); + } + + public ICashuWalletMeltQuoteBuilder WithProofs(IEnumerable proofs) + { + this._proofs = proofs.ToList(); + return this; + } + + public ICashuWalletMeltQuoteBuilder WithBlankOutputs(OutputData blankOutputs) + { + this._blankOutputs = blankOutputs; + return this; + } + + public ICashuWalletMeltQuoteBuilder WithAmount(ulong amount) + { + this._amount = amount; + return this; + } + + public async Task ProcessAsyncBolt11(CancellationToken cancellationToken = default) + { + var activeKeysetId = _wallet.GetActiveKeysetId()?? + throw new InvalidOperationException("Could not fetch active Keyset ID"); + var activeKeyset = _wallet.GetKeysets().SingleOrDefault(k => k.Id == activeKeysetId, null)?? + throw new InvalidOperationException($"Could not fetch keyset for KeysetId: {activeKeysetId}"); + var mnemonic = _wallet.GetMnemonic(); + var counter = _wallet.GetCounter(); + + if (_blankOutputs == null) + { + if (_amount != null) + { + _blankOutputs = CashuUtils.CreateBlankOutputs(_amount, activeKeysetId, activeKeyset.Keys, mnemonic, counter?.GetCounterForId(activeKeysetId)); + } + // processing without blank outputs + } + var req = new PostMeltQuoteBolt11Request + { + Request = this._invoice, + Unit = _wallet.GetUnit() + }; + var mintResponse = await _wallet.GetMintApi().CreateMeltQuote("bolt11", req, cancellationToken); + + return new MeltQuoteBolt11(mintResponse); + } +} +internal class CashuWalletMintQuoteBuilder(CashuWallet wallet) : ICashuWalletMintBuilder +{ + private readonly CashuWallet _wallet = wallet; + + public ICashuWalletMintBuilder WithQuote(string quoteId) => this; + public ICashuWalletMintBuilder WithAmount(ulong amount) => this; + public ICashuWalletMintBuilder WithOutputs(IEnumerable outputs) => this; + public ICashuWalletMintBuilder WithMethod(string method = "bolt11") => this; + + public Task ProcessAsync(CancellationToken cancellationToken = default) + { + throw new NotImplementedException(); + } +} +internal class CashuWalletRestoreBuilder : ICashuWalletRestoreBuilder +{ + private readonly CashuWallet _wallet; + private List _specifiedKeysets; + public CashuWalletRestoreBuilder(CashuWallet wallet) => _wallet = wallet; + + public ICashuWalletRestoreBuilder ForKeysets(IEnumerable keysetIds) => this; + + public Task ProcessAsync(CancellationToken cancellationToken = default) + { + var mnemonic = _wallet.GetMnemonic()?? + throw new ArgumentNullException("Can't restore wallet without Mnemonic"); + if (_specifiedKeysets == null) + { + _specifiedKeysets = _wallet.GetKeysets(); + } + var counter = new Counter(); + + } +} diff --git a/DotNut/Abstractions/Counter.cs b/DotNut/Abstractions/Counter.cs index 21a5af0..53e1a43 100644 --- a/DotNut/Abstractions/Counter.cs +++ b/DotNut/Abstractions/Counter.cs @@ -4,6 +4,8 @@ public class Counter : Dictionary { public Counter(IDictionary dictionary) : base(dictionary) { } + public Counter() {} + public int GetCounterForId(KeysetId keysetId) { if (TryGetValue(keysetId, out var counter)) diff --git a/DotNut/Abstractions/Interfaces/ICashuWallet.cs b/DotNut/Abstractions/Interfaces/ICashuWallet.cs new file mode 100644 index 0000000..fe61acb --- /dev/null +++ b/DotNut/Abstractions/Interfaces/ICashuWallet.cs @@ -0,0 +1,79 @@ +using DotNut.Api; +using DotNut.ApiModels; +using DotNut.NBitcoin.BIP39; + +namespace DotNut.Abstractions; + +/// +/// Fluent builder interface for Cashu Wallet operations +/// +public interface ICashuWalletBuilder +{ + ICashuWalletBuilder WithInfo(MintInfo info); + ICashuWalletBuilder WithInfo(GetInfoResponse info); + ICashuWalletBuilder WithKeysets(IEnumerable keysets); + ICashuWalletBuilder WithKeysets(GetKeysetsResponse keysets); + ICashuWalletBuilder WithKeys(IEnumerable keys); + ICashuWalletBuilder WithUnit(string unit = "sat"); + ICashuWalletBuilder WithSelector(IProofSelector selector); + ICashuWalletBuilder WithMint(ICashuApi mintApi); + ICashuWalletBuilder WithMint(string mintUrl); + ICashuWalletBuilder WithMnemonic(Mnemonic mnemonic); + ICashuWalletBuilder WithMnemonic(string mnemonic); + ICashuWalletBuilder WithCounter(Counter counter); + Task GetInfo(bool forceReferesh = false, CancellationToken cts = default); + + // Swap operations + Task Swap(); + + // Melt operations (pay invoices) + Task CreateMeltQuote(); + + // Mint operations (receive from invoice) + Task CreateMintQuote(); + + // Restore operations + Task Restore(); +} + +/// +/// Swap operation builder +/// +public interface ICashuWalletSwapBuilder +{ + ICashuWalletSwapBuilder ForKeyset(KeysetId targetKeysetId); + ICashuWalletSwapBuilder WithOutputs(IEnumerable outputs); + ICashuWalletSwapBuilder GenerateOutputsForAmount(ulong amount); + Task> ProcessAsync(CancellationToken cancellationToken = default); +} + +/// +/// Melt operation builder (pay invoices) +/// +public interface ICashuWalletMeltQuoteBuilder +{ + ICashuWalletMeltQuoteBuilder WithQuote(string quoteId); + ICashuWalletMeltQuoteBuilder WithInvoice(string bolt11Invoice); + ICashuWalletMeltQuoteBuilder WithMethod(string method = "bolt11"); + Task ProcessAsync(CancellationToken cancellationToken = default); +} + +/// +/// Mint operation builder (receive from invoice) +/// +public interface ICashuWalletMintBuilder +{ + ICashuWalletMintBuilder WithAmount(ulong amount); + ICashuWalletMintBuilder WithOutputs(IEnumerable outputs); + ICashuWalletMintBuilder WithMethod(string method = "bolt11"); + Task ProcessAsync(CancellationToken cancellationToken = default); +} + +/// +/// Restore operation builder +/// +public interface ICashuWalletRestoreBuilder +{ + ICashuWalletRestoreBuilder ForKeysetIds(IEnumerable keysetIds); + Task ProcessAsync(CancellationToken cancellationToken = default); +} diff --git a/DotNut/Abstractions/Interfaces/IProofSelector.cs b/DotNut/Abstractions/Interfaces/IProofSelector.cs new file mode 100644 index 0000000..c89f707 --- /dev/null +++ b/DotNut/Abstractions/Interfaces/IProofSelector.cs @@ -0,0 +1,6 @@ +namespace DotNut.Abstractions; + +public interface IProofSelector +{ + public SendResponse SelectProofsToSend(List proofs, ulong amountToSend, bool includeFees = false); +} \ No newline at end of file diff --git a/DotNut/Abstractions/OutputData.cs b/DotNut/Abstractions/OutputData.cs new file mode 100644 index 0000000..f359313 --- /dev/null +++ b/DotNut/Abstractions/OutputData.cs @@ -0,0 +1,8 @@ +namespace DotNut; + +public class OutputData +{ + public BlindedMessage[] BlindedMessages { get; set; } + public ISecret[] Secrets { get; set; } + public PrivKey[] BlindingFactors { get; set; } +} \ No newline at end of file diff --git a/DotNut/Abstractions/Quotes/MintQuoteBolt11.cs b/DotNut/Abstractions/Quotes/MintQuoteBolt11.cs new file mode 100644 index 0000000..7fd0633 --- /dev/null +++ b/DotNut/Abstractions/Quotes/MintQuoteBolt11.cs @@ -0,0 +1,15 @@ +using DotNut.ApiModels; + +namespace DotNut.Abstractions; + +public class MintQuoteBolt11 +{ + private readonly string method = "bolt11"; + private ulong Amount; + + public MintQuoteBolt11(PostMintQuoteBolt11Response response) + { + this.Amount = response.Amount; + + } +} \ No newline at end of file diff --git a/DotNut/Abstractions/Quotes/MintQuoteBolt12.cs b/DotNut/Abstractions/Quotes/MintQuoteBolt12.cs new file mode 100644 index 0000000..f075690 --- /dev/null +++ b/DotNut/Abstractions/Quotes/MintQuoteBolt12.cs @@ -0,0 +1,6 @@ +namespace DotNut.Abstractions; + +public class MintQuoteBolt12 +{ + +} \ No newline at end of file diff --git a/DotNut/Abstractions/SendResponse.cs b/DotNut/Abstractions/SendResponse.cs new file mode 100644 index 0000000..78d4e65 --- /dev/null +++ b/DotNut/Abstractions/SendResponse.cs @@ -0,0 +1,7 @@ +namespace DotNut.Abstractions; + +public class SendResponse +{ + public List Keep { get; set; } = new(); + public List Send { get; set; } = new(); +} \ No newline at end of file diff --git a/DotNut/Abstractions/WalletResults.cs b/DotNut/Abstractions/WalletResults.cs new file mode 100644 index 0000000..88c4a5f --- /dev/null +++ b/DotNut/Abstractions/WalletResults.cs @@ -0,0 +1,87 @@ +namespace DotNut; + +/// +/// Result of a send operation +/// +public class SendResult +{ + public CashuToken Token { get; set; } = null!; + public string TokenString { get; set; } = string.Empty; + public ulong AmountSent { get; set; } + public List RemainingProofs { get; set; } = new(); + public ulong FeesPaid { get; set; } +} + +/// +/// Result of a receive operation +/// +public class ReceiveResult +{ + public List ReceivedProofs { get; set; } = new(); + public ulong AmountReceived { get; set; } + public CashuToken Token { get; set; } = null!; + public bool SignatureVerified { get; set; } +} + +/// +/// Result of a swap operation +/// +public class SwapResult +{ + public List SwappedProofs { get; set; } = new(); + public ulong TotalAmount { get; set; } + public KeysetId TargetKeysetId { get; set; } = new(""); + public ulong FeesPaid { get; set; } +} + +/// +/// Result of a melt operation (paying invoice) +/// +public class MeltResult +{ + public bool Paid { get; set; } + public string? PaymentPreimage { get; set; } + public List ChangeProofs { get; set; } = new(); + public ulong AmountPaid { get; set; } + public ulong FeesPaid { get; set; } + public string QuoteId { get; set; } = string.Empty; +} + +/// +/// Result of a mint operation (receiving from invoice) +/// +public class MintResult +{ + public List MintedProofs { get; set; } = new(); + public ulong AmountMinted { get; set; } + public string QuoteId { get; set; } = string.Empty; + public bool QuotePaid { get; set; } +} + +/// +/// Result of checking proof states +/// +public class StateResult +{ + public Dictionary States { get; set; } = new(); +} + +/// +/// Proof state information +/// +public class ProofState +{ + public bool Spent { get; set; } + public bool Pending { get; set; } + public string? Witness { get; set; } +} + +/// +/// Result of a restore operation +/// +public class RestoreResult +{ + public List RestoredProofs { get; set; } = new(); + public Dictionary States { get; set; } = new(); + public ulong TotalAmountRestored { get; set; } +} diff --git a/DotNut/Abstractions/WebsocketService.cs b/DotNut/Abstractions/WebsocketService.cs new file mode 100644 index 0000000..ad904e9 --- /dev/null +++ b/DotNut/Abstractions/WebsocketService.cs @@ -0,0 +1,5 @@ +namespace DotNut.Abstractions; + +public class WebsocketService +{ +} \ No newline at end of file diff --git a/DotNut/ApiModels/Melt/bolt12/PostMeltQuoteBolt12Response.cs b/DotNut/ApiModels/Melt/bolt12/PostMeltQuoteBolt12Response.cs index e84ff34..905f6d5 100644 --- a/DotNut/ApiModels/Melt/bolt12/PostMeltQuoteBolt12Response.cs +++ b/DotNut/ApiModels/Melt/bolt12/PostMeltQuoteBolt12Response.cs @@ -9,6 +9,8 @@ public class PostMeltQuoteBolt12Response [JsonPropertyName("request")] public string Request { get; set; } [JsonPropertyName("amount")] public ulong Amount { get; set; } + + [JsonPropertyName("unit")] public string Unit { get; set; } [JsonPropertyName("fee_reserve")] public ulong FeeReserve { get; set; } @@ -18,5 +20,8 @@ public class PostMeltQuoteBolt12Response [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] [JsonPropertyName("payment_preimage")] public string PaymentPreimage { get; set; } + + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("change")] public BlindSignature[] Change { get; set; } } \ No newline at end of file diff --git a/DotNut/NUT00/Cashu.cs b/DotNut/NUT00/Cashu.cs index b6800ca..c7a00e9 100644 --- a/DotNut/NUT00/Cashu.cs +++ b/DotNut/NUT00/Cashu.cs @@ -18,12 +18,13 @@ public static ECPubKey MessageToCurve(string message) var hash = Encoding.UTF8.GetBytes(message); return HashToCurve(hash); } - + public static ECPubKey HexToCurve(string hex) { var bytes = Convert.FromHexString(hex); return HashToCurve(bytes); } + public static ECPubKey HashToCurve(byte[] x) { var msgHash = SHA256.HashData(Concat(DOMAIN_SEPARATOR, x)); @@ -40,47 +41,51 @@ public static ECPubKey HashToCurve(byte[] x) } } } - - public static GE ToGE(this Scalar scalar) - { - // Multiply the scalar by the generator point to get the group element - GEJ gej = Context.Instance.EcMultGenContext.MultGen(scalar); - return gej.ToGroupElement(); - } - - public static ECPubKey ToPubkey(this Scalar scalar) - { - return new ECPubKey(scalar.ToGE(), Context.Instance); - } - - public static ECPrivKey ToPrivateKey(this Scalar scalar) - { - return ECPrivKey.TryCreate(scalar, out var key) ? key : throw new InvalidOperationException(); - } - - public static ECPubKey ToPubkey(this GEJ gej) - { - return new ECPubKey(gej.ToGroupElement(), Context.Instance); - } - - public static ECPubKey ToPubkey(this GE ge) - { - return new ECPubKey(ge, Context.Instance); - } - + + /// + /// Blinding + /// + /// hash_to_curve of the secret + /// Blinding factor + /// Blinded Y (Blinded message) public static ECPubKey ComputeB_(ECPubKey Y, ECPrivKey r) { //B_ = Y + rG return Y.Q.ToGroupElementJacobian().Add(r.CreatePubKey().Q).ToPubkey(); } + /// + /// Signing blinded message + /// + /// B_ blinded message + /// private key of mint (one for each amount) + /// Blind signature (on B_) public static ECPubKey ComputeC_(ECPubKey B_, ECPrivKey k) { //C_ = kB_ return (B_.Q * k.sec).ToPubkey(); } - + /// + /// Unblinding + /// + /// Blind signature + /// Blinding factor + /// Amount Pubkey + /// Unblinded Signature + public static ECPubKey ComputeC(ECPubKey C_, ECPrivKey r, ECPubKey A) + { + //C_ - rA = C + return C_.Q.ToGroupElementJacobian().Add((A.Q * r.sec).ToGroupElement().Negate()).ToPubkey(); + } + + /// + /// Creates DLEQ Proof. + /// + /// Blinded message + /// Privkey for given amount + /// Blinding factor + /// Tuple (e, s) representing the DLEQ proof public static (ECPrivKey e, ECPrivKey s) ComputeProof(ECPubKey B_, ECPrivKey a, ECPrivKey p) { //C_ - rK = kY + krG - krG = kY = C @@ -94,12 +99,26 @@ public static (ECPrivKey e, ECPrivKey s) ComputeProof(ECPubKey B_, ECPrivKey a, return (e.ToPrivateKey(), s); } + /// + /// Computes the challenge scalar 'e' for the DLEQ proof. + /// + /// Commitment point r*G + /// + /// + /// + /// The challenge scalar e derived as a SHA256 hash over the concatenation of the uncompressed points. public static Scalar ComputeE(ECPubKey R1, ECPubKey R2, ECPubKey K, ECPubKey C_) { byte[] eBytes = Encoding.UTF8.GetBytes(string.Concat(new[] {R1, R2, K, C_}.Select(pk => pk.ToHex(false)))); return new Scalar(SHA256.HashData(eBytes)); } + /// + /// Verify DLEQ proof of Cashu proof. + /// + /// Cashu Proof + /// + /// public static bool Verify(this Proof proof, ECPubKey A) { return VerifyProof(proof.Secret.ToCurve(),proof.DLEQ.R, proof.C, proof.DLEQ.E, proof.DLEQ.S, A); @@ -124,11 +143,33 @@ public static bool VerifyProof(ECPubKey Y, ECPrivKey r, ECPubKey C, ECPrivKey e, var B_ = Y.Q.ToGroupElementJacobian().Add(r.CreatePubKey().Q).ToPubkey(); return VerifyProof(B_, C_, e, s, A); } + - public static ECPubKey ComputeC(ECPubKey C_, ECPrivKey r, ECPubKey A) + public static GE ToGE(this Scalar scalar) { - //C_ - rA = C - return C_.Q.ToGroupElementJacobian().Add((A.Q * r.sec).ToGroupElement().Negate()).ToPubkey(); + // Multiply the scalar by the generator point to get the group element + GEJ gej = Context.Instance.EcMultGenContext.MultGen(scalar); + return gej.ToGroupElement(); + } + + public static ECPubKey ToPubkey(this Scalar scalar) + { + return new ECPubKey(scalar.ToGE(), Context.Instance); + } + + public static ECPrivKey ToPrivateKey(this Scalar scalar) + { + return ECPrivKey.TryCreate(scalar, out var key) ? key : throw new InvalidOperationException(); + } + + public static ECPubKey ToPubkey(this GEJ gej) + { + return new ECPubKey(gej.ToGroupElement(), Context.Instance); + } + + public static ECPubKey ToPubkey(this GE ge) + { + return new ECPubKey(ge, Context.Instance); } public static byte[] ComputeZx(ECPrivKey e, ECPubKey P) @@ -181,6 +222,7 @@ public static byte[] ToBytes(this ECPrivKey key) return output.ToArray(); } + public static byte[] ToUncompressedBytes(this ECPubKey key) { Span output = stackalloc byte[65]; From 7c5c55e61cec3bbf1343c1d6b4447bba2c4f78c7 Mon Sep 17 00:00:00 2001 From: d4rp4t Date: Fri, 26 Sep 2025 13:24:34 +0200 Subject: [PATCH 05/70] ws wip --- .../Interfaces/IWebsocketService.cs | 24 ++ DotNut/Abstractions/WebsocketService.cs | 293 +++++++++++++++++- .../Websockets/NotificationParser.cs | 43 +++ .../Websockets/NotificationPayloads.cs | 56 ++++ .../Websockets/WebsocketEntities.cs | 48 +++ .../Abstractions/Websockets/WebsocketEnums.cs | 26 ++ .../Websockets/WebsocketEvents.cs | 15 + .../Websockets/WebsocketModels.cs | 95 ++++++ .../Websockets/WebsocketServiceExtensions.cs | 97 ++++++ 9 files changed, 695 insertions(+), 2 deletions(-) create mode 100644 DotNut/Abstractions/Interfaces/IWebsocketService.cs create mode 100644 DotNut/Abstractions/Websockets/NotificationParser.cs create mode 100644 DotNut/Abstractions/Websockets/NotificationPayloads.cs create mode 100644 DotNut/Abstractions/Websockets/WebsocketEntities.cs create mode 100644 DotNut/Abstractions/Websockets/WebsocketEnums.cs create mode 100644 DotNut/Abstractions/Websockets/WebsocketEvents.cs create mode 100644 DotNut/Abstractions/Websockets/WebsocketModels.cs create mode 100644 DotNut/Abstractions/Websockets/WebsocketServiceExtensions.cs diff --git a/DotNut/Abstractions/Interfaces/IWebsocketService.cs b/DotNut/Abstractions/Interfaces/IWebsocketService.cs new file mode 100644 index 0000000..d1d06bc --- /dev/null +++ b/DotNut/Abstractions/Interfaces/IWebsocketService.cs @@ -0,0 +1,24 @@ +using System.Net.WebSockets; + +namespace DotNut.Abstractions.Websockets; + +public interface IWebsocketService : IDisposable +{ + event EventHandler? NotificationReceived; + + event EventHandler? ConnectionStateChanged; + + Task ConnectAsync(string mintUrl, CancellationToken cancellationToken = default); + + Task SubscribeAsync(string connectionId, SubscriptionKind kind, string[] filters, CancellationToken cancellationToken = default); + + Task UnsubscribeAsync(string connectionId, string subId, CancellationToken cancellationToken = default); + + Task DisconnectAsync(string connectionId, CancellationToken cancellationToken = default); + + WebSocketState GetConnectionState(string connectionId); + + IEnumerable GetSubscriptions(string connectionId); + + IEnumerable GetConnections(); +} diff --git a/DotNut/Abstractions/WebsocketService.cs b/DotNut/Abstractions/WebsocketService.cs index ad904e9..031d370 100644 --- a/DotNut/Abstractions/WebsocketService.cs +++ b/DotNut/Abstractions/WebsocketService.cs @@ -1,5 +1,294 @@ +using System.Collections.Concurrent; +using System.Net.WebSockets; +using System.Text; +using System.Text.Json; +using DotNut.Abstractions.Websockets; + namespace DotNut.Abstractions; -public class WebsocketService +public class WebsocketService : IWebsocketService { -} \ No newline at end of file + private readonly ConcurrentDictionary _connections = new(); + private readonly ConcurrentDictionary _subscriptions = new(); + private readonly object _lockObject = new(); + private int _nextRequestId = 0; + + public event EventHandler? NotificationReceived; + public event EventHandler? ConnectionStateChanged; + public async Task ConnectAsync(string mintUrl, CancellationToken cancellationToken = default) + { + var connectionId = Guid.NewGuid().ToString(); + var wsUrl = GetWebSocketUrl(mintUrl); + + var clientWebSocket = new ClientWebSocket(); + await clientWebSocket.ConnectAsync(new Uri(wsUrl), cancellationToken); + + var connection = new WebSocketConnection + { + Id = connectionId, + MintUrl = mintUrl, + WebSocket = clientWebSocket, + State = WebSocketState.Open + }; + + _connections[connectionId] = connection; + + _ = Task.Run(async () => await ListenForMessages(connection, cancellationToken), cancellationToken); + + OnConnectionStateChanged(connectionId, WebSocketState.Open); + + return connectionId; + } + public async Task SubscribeAsync(string connectionId, SubscriptionKind kind, string[] filters, CancellationToken cancellationToken = default) + { + if (!_connections.TryGetValue(connectionId, out var connection)) + throw new InvalidOperationException($"Connection {connectionId} not found"); + + if (connection.State != WebSocketState.Open) + throw new InvalidOperationException($"Connection {connectionId} is not open"); + + var subId = Guid.NewGuid().ToString(); + var requestId = GetNextRequestId(); + + var request = new WsRequest + { + JsonRpc = "2.0", + Method = WsRequestMethod.subscribe, + Params = new WsRequestParams + { + Kind = kind, + SubId = subId, + Filters = filters + }, + Id = requestId + }; + + var subscription = new Subscription + { + Id = subId, + ConnectionId = connectionId, + Kind = kind, + Filters = filters, + CreatedAt = DateTime.UtcNow + }; + + _subscriptions[subId] = subscription; + + await SendMessageAsync(connection, request, cancellationToken); + + return subId; + } + public async Task UnsubscribeAsync(string connectionId, string subId, CancellationToken cancellationToken = default) + { + if (!_connections.TryGetValue(connectionId, out var connection)) + throw new InvalidOperationException($"Connection {connectionId} not found"); + + if (connection.State != WebSocketState.Open) + throw new InvalidOperationException($"Connection {connectionId} is not open"); + + var requestId = GetNextRequestId(); + + var request = new WsRequest + { + JsonRpc = "2.0", + Method = WsRequestMethod.unsubscribe, + Params = new WsRequestParams + { + SubId = subId + }, + Id = requestId + }; + + await SendMessageAsync(connection, request, cancellationToken); + + _subscriptions.TryRemove(subId, out _); + } + + public async Task DisconnectAsync(string connectionId, CancellationToken cancellationToken = default) + { + if (!_connections.TryGetValue(connectionId, out var connection)) + return; + + try + { + if (connection.State == WebSocketState.Open) + { + await connection.WebSocket.CloseAsync(WebSocketCloseStatus.NormalClosure, "Client disconnecting", cancellationToken); + } + } + catch (Exception) + { + // Ignore close exceptions + } + finally + { + connection.WebSocket.Dispose(); + _connections.TryRemove(connectionId, out _); + + var subscriptionsToRemove = _subscriptions + .Where(s => s.Value.ConnectionId == connectionId) + .Select(s => s.Key) + .ToList(); + + foreach (var subId in subscriptionsToRemove) + { + _subscriptions.TryRemove(subId, out _); + } + + OnConnectionStateChanged(connectionId, WebSocketState.Closed); + } + } + + public async ValueTask DisposeAsync() + { + var connectionIds = _connections.Keys.ToList(); + foreach (var connectionId in connectionIds) + { + await DisconnectAsync(connectionId); + } + } + + // Use only if necessary. pls use DisposeAsync + public void Dispose() + { + var connectionIds = _connections.Keys.ToList(); + foreach (var connectionId in connectionIds) + { + DisconnectAsync(connectionId).Wait(TimeSpan.FromSeconds(5)); + } + } + + public WebSocketState GetConnectionState(string connectionId) + { + return _connections.TryGetValue(connectionId, out var connection) + ? connection.State + : WebSocketState.None; + } + + public IEnumerable GetSubscriptions(string connectionId) + { + return _subscriptions.Values.Where(s => s.ConnectionId == connectionId); + } + + public IEnumerable GetConnections() + { + return _connections.Values; + } + + private async Task ListenForMessages(WebSocketConnection connection, CancellationToken cancellationToken) + { + var buffer = new byte[4096]; + + try + { + while (connection.State == WebSocketState.Open && !cancellationToken.IsCancellationRequested) + { + var result = await connection.WebSocket.ReceiveAsync(new ArraySegment(buffer), cancellationToken); + + if (result.MessageType == WebSocketMessageType.Close) + { + connection.State = WebSocketState.Closed; + OnConnectionStateChanged(connection.Id, WebSocketState.Closed); + break; + } + + if (result.MessageType == WebSocketMessageType.Text) + { + var message = Encoding.UTF8.GetString(buffer, 0, result.Count); + await ProcessMessage(connection, message); + } + } + } + catch (Exception ex) + { + connection.State = WebSocketState.Aborted; + OnConnectionStateChanged(connection.Id, WebSocketState.Aborted); + // Log exception + } + } + + private async Task ProcessMessage(WebSocketConnection connection, string message) + { + try + { + var jsonElement = JsonSerializer.Deserialize(message); + + if (jsonElement.TryGetProperty("method", out var methodProp) && + methodProp.GetString() == "subscribe") + { + var notification = JsonSerializer.Deserialize(message); + if (notification != null) + { + OnNotificationReceived(connection.Id, notification); + } + } + else if (jsonElement.TryGetProperty("result", out _)) + { + var response = JsonSerializer.Deserialize(message); + } + else if (jsonElement.TryGetProperty("error", out _)) + { + var error = JsonSerializer.Deserialize(message); + } + } + catch (Exception ex) + { + } + } + + private async Task SendMessageAsync(WebSocketConnection connection, T message, CancellationToken cancellationToken) + { + var json = JsonSerializer.Serialize(message); + var bytes = Encoding.UTF8.GetBytes(json); + + await connection.WebSocket.SendAsync( + new ArraySegment(bytes), + WebSocketMessageType.Text, + true, + cancellationToken); + } + + private string GetWebSocketUrl(string mintUrl) + { + var uri = new Uri(mintUrl.TrimEnd('/')); + var scheme = uri.Scheme == "https" ? "wss" : "ws"; + return $"{scheme}://{uri.Host}:{uri.Port}/v1/ws"; + } + + private int GetNextRequestId() + { + lock (_lockObject) + { + return ++_nextRequestId; + } + } + + private void OnNotificationReceived(string connectionId, WsNotification notification) + { + NotificationReceived?.Invoke(this, new NotificationEventArgs + { + ConnectionId = connectionId, + Notification = notification + }); + } + private void OnConnectionStateChanged(string connectionId, WebSocketState state) + { + ConnectionStateChanged?.Invoke(this, new ConnectionStateChangedEventArgs + { + ConnectionId = connectionId, + State = state + }); + } + public bool IsConnected(string mintUrl) + => _connections.Any(x => _normalizeMintUrl(x.Value.MintUrl) == _normalizeMintUrl(mintUrl)); + private string _normalizeMintUrl(string mintUrl) + { + if (Uri.TryCreate(mintUrl.TrimEnd('/'), UriKind.Absolute, out var uri)) + { + var host = uri.Host.ToLowerInvariant(); + var builder = new UriBuilder(uri) { Host = host }; + return builder.Uri.ToString().TrimEnd('/'); + } + return mintUrl.TrimEnd('/').ToLowerInvariant(); + } + } diff --git a/DotNut/Abstractions/Websockets/NotificationParser.cs b/DotNut/Abstractions/Websockets/NotificationParser.cs new file mode 100644 index 0000000..129c1e4 --- /dev/null +++ b/DotNut/Abstractions/Websockets/NotificationParser.cs @@ -0,0 +1,43 @@ +using System.Text.Json; + +namespace DotNut.Abstractions.Websockets; + +public static class NotificationParser +{ + public static object? ParsePayload(WsNotification notification, SubscriptionKind subscriptionKind) + { + if (notification.Params.Payload == null) + return null; + + var jsonElement = (JsonElement)notification.Params.Payload; + + return subscriptionKind switch + { + SubscriptionKind.bolt11_mint_quote => jsonElement.Deserialize(), + SubscriptionKind.bolt11_melt_quote => jsonElement.Deserialize(), + SubscriptionKind.proof_state => jsonElement.Deserialize(), + _ => notification.Params.Payload + }; + } + + public static T? ParsePayload(WsNotification notification) where T : class + { + if (notification.Params.Payload == null) + return null; + + var jsonElement = (JsonElement)notification.Params.Payload; + return jsonElement.Deserialize(); + } + + public static bool IsPayloadOfType(WsNotification notification) where T : class + { + try + { + return ParsePayload(notification) != null; + } + catch + { + return false; + } + } +} diff --git a/DotNut/Abstractions/Websockets/NotificationPayloads.cs b/DotNut/Abstractions/Websockets/NotificationPayloads.cs new file mode 100644 index 0000000..912b10b --- /dev/null +++ b/DotNut/Abstractions/Websockets/NotificationPayloads.cs @@ -0,0 +1,56 @@ +using System.Text.Json.Serialization; + +namespace DotNut.Abstractions.Websockets; + +public class MintQuoteNotificationPayload +{ + [JsonPropertyName("quote")] + public string Quote { get; set; } = string.Empty; + + [JsonPropertyName("request")] + public string Request { get; set; } = string.Empty; + + [JsonPropertyName("paid")] + public bool Paid { get; set; } + + [JsonPropertyName("expiry")] + public long? Expiry { get; set; } +} + +public class MeltQuoteNotificationPayload +{ + [JsonPropertyName("quote")] + public string Quote { get; set; } = string.Empty; + + [JsonPropertyName("amount")] + public ulong Amount { get; set; } + + [JsonPropertyName("fee_reserve")] + public ulong FeeReserve { get; set; } + + [JsonPropertyName("paid")] + public bool Paid { get; set; } + + [JsonPropertyName("expiry")] + public long? Expiry { get; set; } + + [JsonPropertyName("payment_preimage")] + public string? PaymentPreimage { get; set; } + + [JsonPropertyName("change")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public object[]? Change { get; set; } +} + +public class ProofStateNotificationPayload +{ + [JsonPropertyName("Y")] + public string Y { get; set; } = string.Empty; + + [JsonPropertyName("state")] + public ProofState State { get; set; } + + [JsonPropertyName("witness")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public string? Witness { get; set; } +} diff --git a/DotNut/Abstractions/Websockets/WebsocketEntities.cs b/DotNut/Abstractions/Websockets/WebsocketEntities.cs new file mode 100644 index 0000000..0569a23 --- /dev/null +++ b/DotNut/Abstractions/Websockets/WebsocketEntities.cs @@ -0,0 +1,48 @@ +using System.Net.WebSockets; + +namespace DotNut.Abstractions.Websockets; + +public class WebSocketConnection +{ + public string Id { get; set; } = string.Empty; + public string MintUrl { get; set; } = string.Empty; + public ClientWebSocket WebSocket { get; set; } = new(); + public WebSocketState State { get; set; } + public DateTime ConnectedAt { get; set; } = DateTime.UtcNow; + + public bool Equals(WebSocketConnection? other) + { + if (other is null) return false; + if (ReferenceEquals(this, other)) return true; + return string.Equals(MintUrl, other.MintUrl, StringComparison.OrdinalIgnoreCase); + } + + public override bool Equals(object? obj) + { + return obj is WebSocketConnection other && Equals(other); + } + + public override int GetHashCode() + { + return MintUrl?.GetHashCode(StringComparison.OrdinalIgnoreCase) ?? 0; + } + + public static bool operator ==(WebSocketConnection? left, WebSocketConnection? right) + { + return Equals(left, right); + } + + public static bool operator !=(WebSocketConnection? left, WebSocketConnection? right) + { + return !Equals(left, right); + } +} + +public class Subscription +{ + public string Id { get; set; } = string.Empty; + public string ConnectionId { get; set; } = string.Empty; + public SubscriptionKind Kind { get; set; } + public string[] Filters { get; set; } = Array.Empty(); + public DateTime CreatedAt { get; set; } = DateTime.UtcNow; +} diff --git a/DotNut/Abstractions/Websockets/WebsocketEnums.cs b/DotNut/Abstractions/Websockets/WebsocketEnums.cs new file mode 100644 index 0000000..26bf0d8 --- /dev/null +++ b/DotNut/Abstractions/Websockets/WebsocketEnums.cs @@ -0,0 +1,26 @@ +using System.Text.Json.Serialization; + +namespace DotNut.Abstractions.Websockets; + +[JsonConverter(typeof(JsonStringEnumConverter))] +public enum SubscriptionKind +{ + bolt11_melt_quote, + bolt11_mint_quote, + proof_state +} + +[JsonConverter(typeof(JsonStringEnumConverter))] +public enum WsRequestMethod +{ + subscribe, + unsubscribe +} + +[JsonConverter(typeof(JsonStringEnumConverter))] +public enum ProofState +{ + UNSPENT, + PENDING, + SPENT +} diff --git a/DotNut/Abstractions/Websockets/WebsocketEvents.cs b/DotNut/Abstractions/Websockets/WebsocketEvents.cs new file mode 100644 index 0000000..62b8480 --- /dev/null +++ b/DotNut/Abstractions/Websockets/WebsocketEvents.cs @@ -0,0 +1,15 @@ +using System.Net.WebSockets; + +namespace DotNut.Abstractions.Websockets; + +public class NotificationEventArgs : EventArgs +{ + public string ConnectionId { get; set; } = string.Empty; + public WsNotification Notification { get; set; } = new(); +} + +public class ConnectionStateChangedEventArgs : EventArgs +{ + public string ConnectionId { get; set; } = string.Empty; + public WebSocketState State { get; set; } +} diff --git a/DotNut/Abstractions/Websockets/WebsocketModels.cs b/DotNut/Abstractions/Websockets/WebsocketModels.cs new file mode 100644 index 0000000..32ab739 --- /dev/null +++ b/DotNut/Abstractions/Websockets/WebsocketModels.cs @@ -0,0 +1,95 @@ +using System.Text.Json.Serialization; + +namespace DotNut.Abstractions.Websockets; + +public class WsRequest +{ + [JsonPropertyName("jsonrpc")] + public string JsonRpc { get; set; } = "2.0"; + + [JsonPropertyName("method")] + public WsRequestMethod Method { get; set; } + + [JsonPropertyName("params")] + public WsRequestParams Params { get; set; } = new(); + + [JsonPropertyName("id")] + public int Id { get; set; } +} + +public class WsRequestParams +{ + [JsonPropertyName("kind")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public SubscriptionKind? Kind { get; set; } + + [JsonPropertyName("subId")] + public string SubId { get; set; } = string.Empty; + + [JsonPropertyName("filters")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public string[]? Filters { get; set; } +} + +public class WsResponse +{ + [JsonPropertyName("jsonrpc")] + public string JsonRpc { get; set; } = "2.0"; + + [JsonPropertyName("result")] + public WsResult Result { get; set; } = new(); + + [JsonPropertyName("id")] + public int Id { get; set; } +} + +public class WsResult +{ + [JsonPropertyName("status")] + public string Status { get; set; } = string.Empty; + + [JsonPropertyName("subId")] + public string SubId { get; set; } = string.Empty; +} + +public class WsError +{ + [JsonPropertyName("jsonrpc")] + public string JsonRpc { get; set; } = "2.0"; + + [JsonPropertyName("error")] + public WsErrorDetails Error { get; set; } = new(); + + [JsonPropertyName("id")] + public int Id { get; set; } +} + +public class WsErrorDetails +{ + [JsonPropertyName("code")] + public int Code { get; set; } + + [JsonPropertyName("message")] + public string Message { get; set; } = string.Empty; +} + +public class WsNotification +{ + [JsonPropertyName("jsonrpc")] + public string JsonRpc { get; set; } = "2.0"; + + [JsonPropertyName("method")] + public string Method { get; set; } = "subscribe"; + + [JsonPropertyName("params")] + public WsNotificationParams Params { get; set; } = new(); +} + +public class WsNotificationParams +{ + [JsonPropertyName("subId")] + public string SubId { get; set; } = string.Empty; + + [JsonPropertyName("payload")] + public object? Payload { get; set; } +} diff --git a/DotNut/Abstractions/Websockets/WebsocketServiceExtensions.cs b/DotNut/Abstractions/Websockets/WebsocketServiceExtensions.cs new file mode 100644 index 0000000..168b9e0 --- /dev/null +++ b/DotNut/Abstractions/Websockets/WebsocketServiceExtensions.cs @@ -0,0 +1,97 @@ +namespace DotNut.Abstractions.Websockets; + +public static class WebsocketServiceExtensions +{ + public static async Task SubscribeToMintQuoteAsync( + this IWebsocketService service, + string connectionId, + string[] quoteIds, + CancellationToken cancellationToken = default) + { + return await service.SubscribeAsync(connectionId, SubscriptionKind.bolt11_mint_quote, quoteIds, cancellationToken); + } + + public static async Task SubscribeToMeltQuoteAsync( + this IWebsocketService service, + string connectionId, + string[] quoteIds, + CancellationToken cancellationToken = default) + { + return await service.SubscribeAsync(connectionId, SubscriptionKind.bolt11_melt_quote, quoteIds, cancellationToken); + } + + public static async Task SubscribeToProofStateAsync( + this IWebsocketService service, + string connectionId, + string[] proofYs, + CancellationToken cancellationToken = default) + { + return await service.SubscribeAsync(connectionId, SubscriptionKind.proof_state, proofYs, cancellationToken); + } + + public static async Task SubscribeToSingleProofStateAsync( + this IWebsocketService service, + string connectionId, + string proofY, + CancellationToken cancellationToken = default) + { + return await service.SubscribeToProofStateAsync(connectionId, new[] { proofY }, cancellationToken); + } + + public static async Task SubscribeToSingleMintQuoteAsync( + this IWebsocketService service, + string connectionId, + string quoteId, + CancellationToken cancellationToken = default) + { + return await service.SubscribeToMintQuoteAsync(connectionId, new[] { quoteId }, cancellationToken); + } + + public static async Task SubscribeToSingleMeltQuoteAsync( + this IWebsocketService service, + string connectionId, + string quoteId, + CancellationToken cancellationToken = default) + { + return await service.SubscribeToMeltQuoteAsync(connectionId, new[] { quoteId }, cancellationToken); + } + + public static bool IsConnectionActive(this IWebsocketService service, string connectionId) + { + var state = service.GetConnectionState(connectionId); + return state == System.Net.WebSockets.WebSocketState.Open; + } + + public static IEnumerable GetSubscriptionsByKind( + this IWebsocketService service, + string connectionId, + SubscriptionKind kind) + { + return service.GetSubscriptions(connectionId).Where(s => s.Kind == kind); + } + + public static async Task UnsubscribeAllAsync( + this IWebsocketService service, + string connectionId, + CancellationToken cancellationToken = default) + { + var subscriptions = service.GetSubscriptions(connectionId).ToList(); + foreach (var subscription in subscriptions) + { + await service.UnsubscribeAsync(connectionId, subscription.Id, cancellationToken); + } + } + + public static async Task UnsubscribeByKindAsync( + this IWebsocketService service, + string connectionId, + SubscriptionKind kind, + CancellationToken cancellationToken = default) + { + var subscriptions = service.GetSubscriptionsByKind(connectionId, kind).ToList(); + foreach (var subscription in subscriptions) + { + await service.UnsubscribeAsync(connectionId, subscription.Id, cancellationToken); + } + } +} From bbfc1ccf41b9a877487aec3332c38524ca530fd2 Mon Sep 17 00:00:00 2001 From: d4rp4t Date: Sun, 28 Sep 2025 20:33:33 +0200 Subject: [PATCH 06/70] interfaces everywhere --- DotNut.Tests/Integration.cs | 48 +++++++ DotNut/Abstractions/Counter.cs | 24 ++-- DotNut/Abstractions/Interfaces/ICounter.cs | 8 ++ .../Abstractions/Interfaces/IMeltHandler.cs | 14 ++ .../Abstractions/Interfaces/IMintHandler.cs | 10 ++ .../{ICashuWallet.cs => IWallet.cs} | 18 ++- DotNut/Abstractions/MintHandler.cs | 85 ++++++++++++ DotNut/Abstractions/Quotes/MintQuoteBolt11.cs | 15 --- DotNut/Abstractions/Quotes/MintQuoteBolt12.cs | 6 - .../{CashuWallet.cs => Wallet.cs} | 121 ++++++++++-------- 10 files changed, 256 insertions(+), 93 deletions(-) create mode 100644 DotNut.Tests/Integration.cs create mode 100644 DotNut/Abstractions/Interfaces/ICounter.cs create mode 100644 DotNut/Abstractions/Interfaces/IMeltHandler.cs create mode 100644 DotNut/Abstractions/Interfaces/IMintHandler.cs rename DotNut/Abstractions/Interfaces/{ICashuWallet.cs => IWallet.cs} (84%) create mode 100644 DotNut/Abstractions/MintHandler.cs delete mode 100644 DotNut/Abstractions/Quotes/MintQuoteBolt11.cs delete mode 100644 DotNut/Abstractions/Quotes/MintQuoteBolt12.cs rename DotNut/Abstractions/{CashuWallet.cs => Wallet.cs} (89%) diff --git a/DotNut.Tests/Integration.cs b/DotNut.Tests/Integration.cs new file mode 100644 index 0000000..4f779e9 --- /dev/null +++ b/DotNut.Tests/Integration.cs @@ -0,0 +1,48 @@ +using DotNut.Abstractions; +using DotNut.Api; + +namespace DotNut.Tests; + +public class Integration +{ + const string MintUrl = "http://localhost:3338"; + + [Fact] + public void CreatesWalletSuccesfully() + { + var wallet = Wallet.Create(); + Assert.NotNull(wallet); + } + [Fact] + public async Task FetchesInfoSuccessfully() + { + var wallet = Wallet.Create().WithMint(MintUrl); + var info = await wallet.GetInfo(); + Assert.NotNull(info); + } + + [Fact] + public async Task ThrowsWhenMintNotFound() + { + var wallet = Wallet.Create(); + await Assert.ThrowsAsync(async () => await wallet.GetInfo()); + await Assert.ThrowsAsync(async () => wallet.Restore()); + await Assert.ThrowsAsync(async () => wallet.Swap()); + await Assert.ThrowsAsync(async () => wallet.CreateMeltQuote()); + await Assert.ThrowsAsync(async () => wallet.CreateMintQuote()); + } + + [Fact] + public async Task MeltsSucessfully() + { + var wallet = Wallet.Create().WithMint(MintUrl); + + var quote = wallet + .CreateMintQuote() + .WithMethod("bolt11") + .WithUnit() + + + } + +} \ No newline at end of file diff --git a/DotNut/Abstractions/Counter.cs b/DotNut/Abstractions/Counter.cs index 53e1a43..4c61755 100644 --- a/DotNut/Abstractions/Counter.cs +++ b/DotNut/Abstractions/Counter.cs @@ -1,27 +1,25 @@ using DotNut; +using DotNut.Abstractions.Interfaces; -public class Counter : Dictionary +public class Counter : ICounter { - public Counter(IDictionary dictionary) : base(dictionary) { } - - public Counter() {} - - public int GetCounterForId(KeysetId keysetId) + private Dictionary _counter; + public Counter(IDictionary dictionary){ } + public async Task GetCounterForId(KeysetId keysetId) { - if (TryGetValue(keysetId, out var counter)) + if (_counter.TryGetValue(keysetId, out var counter)) return counter; - return this[keysetId] = 0; + return _counter[keysetId] = 0; } - public int IncrementCounter(KeysetId keysetId, int bumpBy = 1) + public async Task IncrementCounter(KeysetId keysetId, int bumpBy = 1) { - var current = GetCounterForId(keysetId); + var current = await GetCounterForId(keysetId); var next = current + bumpBy; - this[keysetId] = next; + _counter[keysetId] = next; return next; } - public void SetCounter(KeysetId keysetId, int counter) => this[keysetId] = counter; - public Counter Clone() => new(this); + public async Task SetCounter(KeysetId keysetId, int counter) => _counter[keysetId] = counter; } \ No newline at end of file diff --git a/DotNut/Abstractions/Interfaces/ICounter.cs b/DotNut/Abstractions/Interfaces/ICounter.cs new file mode 100644 index 0000000..9c09d16 --- /dev/null +++ b/DotNut/Abstractions/Interfaces/ICounter.cs @@ -0,0 +1,8 @@ +namespace DotNut.Abstractions.Interfaces; + +public interface ICounter +{ + public Task GetCounterForId(KeysetId keysetId); + public Task IncrementCounter(KeysetId keysetId, int bumpBy = 1); + public Task SetCounter(KeysetId keysetId, int counter); +} \ No newline at end of file diff --git a/DotNut/Abstractions/Interfaces/IMeltHandler.cs b/DotNut/Abstractions/Interfaces/IMeltHandler.cs new file mode 100644 index 0000000..32037ef --- /dev/null +++ b/DotNut/Abstractions/Interfaces/IMeltHandler.cs @@ -0,0 +1,14 @@ +using DotNut.Abstractions.Websockets; +using DotNut.ApiModels; +using DotNut.ApiModels.Melt.bolt12; + +namespace DotNut.Abstractions.Interfaces; + +public interface IMeltHandler{} + +public interface IMeltHandler: IMeltHandler +{ + Task Melt(TRequest request); + + Task Subscribe(TRequest request); +} \ No newline at end of file diff --git a/DotNut/Abstractions/Interfaces/IMintHandler.cs b/DotNut/Abstractions/Interfaces/IMintHandler.cs new file mode 100644 index 0000000..1cc30c2 --- /dev/null +++ b/DotNut/Abstractions/Interfaces/IMintHandler.cs @@ -0,0 +1,10 @@ +using DotNut.Abstractions.Websockets; + +namespace DotNut.Abstractions; + +public interface IMintHandler {} +public interface IMintHandler: IMintHandler +{ + Task Mint(CancellationToken cts = default); + Task Subscribe(); +} \ No newline at end of file diff --git a/DotNut/Abstractions/Interfaces/ICashuWallet.cs b/DotNut/Abstractions/Interfaces/IWallet.cs similarity index 84% rename from DotNut/Abstractions/Interfaces/ICashuWallet.cs rename to DotNut/Abstractions/Interfaces/IWallet.cs index fe61acb..99a27f9 100644 --- a/DotNut/Abstractions/Interfaces/ICashuWallet.cs +++ b/DotNut/Abstractions/Interfaces/IWallet.cs @@ -14,7 +14,6 @@ public interface ICashuWalletBuilder ICashuWalletBuilder WithKeysets(IEnumerable keysets); ICashuWalletBuilder WithKeysets(GetKeysetsResponse keysets); ICashuWalletBuilder WithKeys(IEnumerable keys); - ICashuWalletBuilder WithUnit(string unit = "sat"); ICashuWalletBuilder WithSelector(IProofSelector selector); ICashuWalletBuilder WithMint(ICashuApi mintApi); ICashuWalletBuilder WithMint(string mintUrl); @@ -22,18 +21,22 @@ public interface ICashuWalletBuilder ICashuWalletBuilder WithMnemonic(string mnemonic); ICashuWalletBuilder WithCounter(Counter counter); Task GetInfo(bool forceReferesh = false, CancellationToken cts = default); + Task CreateOutputs(List amounts, KeysetId id, CancellationToken cts = default); + + ICashuApi? GetMintApi(); + // Swap operations - Task Swap(); + ICashuWalletSwapBuilder Swap(); // Melt operations (pay invoices) - Task CreateMeltQuote(); + ICashuWalletMeltQuoteBuilder CreateMeltQuote(); // Mint operations (receive from invoice) - Task CreateMintQuote(); + ICashuWalletMintBuilder CreateMintQuote(); // Restore operations - Task Restore(); + ICashuWalletRestoreBuilder Restore(); } /// @@ -41,9 +44,9 @@ public interface ICashuWalletBuilder /// public interface ICashuWalletSwapBuilder { + ICashuWalletBuilder WithUnit(string unit); ICashuWalletSwapBuilder ForKeyset(KeysetId targetKeysetId); ICashuWalletSwapBuilder WithOutputs(IEnumerable outputs); - ICashuWalletSwapBuilder GenerateOutputsForAmount(ulong amount); Task> ProcessAsync(CancellationToken cancellationToken = default); } @@ -52,7 +55,7 @@ public interface ICashuWalletSwapBuilder /// public interface ICashuWalletMeltQuoteBuilder { - ICashuWalletMeltQuoteBuilder WithQuote(string quoteId); + ICashuWalletBuilder WithUnit(string unit); ICashuWalletMeltQuoteBuilder WithInvoice(string bolt11Invoice); ICashuWalletMeltQuoteBuilder WithMethod(string method = "bolt11"); Task ProcessAsync(CancellationToken cancellationToken = default); @@ -63,6 +66,7 @@ public interface ICashuWalletMeltQuoteBuilder /// public interface ICashuWalletMintBuilder { + ICashuWalletBuilder WithUnit(string unit); ICashuWalletMintBuilder WithAmount(ulong amount); ICashuWalletMintBuilder WithOutputs(IEnumerable outputs); ICashuWalletMintBuilder WithMethod(string method = "bolt11"); diff --git a/DotNut/Abstractions/MintHandler.cs b/DotNut/Abstractions/MintHandler.cs new file mode 100644 index 0000000..1b6d04e --- /dev/null +++ b/DotNut/Abstractions/MintHandler.cs @@ -0,0 +1,85 @@ +using System.Runtime.CompilerServices; +using DotNut.Abstractions.Websockets; +using DotNut.Api; +using DotNut.ApiModels; +using DotNut.ApiModels.Mint.bolt12; + +namespace DotNut.Abstractions.Quotes; + +// todo +// at this point we should already have everything that we need for minting the tokens. also, we assume the invoice is paid or it will be paid soon + +public class MintHandlerBolt11: IMintHandler +{ + private readonly ICashuWalletBuilder _wallet; + private readonly PostMintQuoteBolt11Response _quote; + private readonly GetKeysResponse.KeysetItemResponse _keyset; + private KeysetId _keysetId => _keyset.Id; // at this point keysetid MUST be validated so it's safe to asssume its correct + + private List? _amounts; + private OutputData? _outputs; + + private string SubscriptionId; + private WebsocketService _websocketService; + + public MintHandlerBolt11( + ICashuWalletBuilder wallet, + PostMintQuoteBolt11Response postMintQuoteBolt11Response, + GetKeysResponse.KeysetItemResponse? verifiedKeyset + ) + { + this._quote = postMintQuoteBolt11Response; + this._keyset = verifiedKeyset; + } + + public MintHandlerBolt11(PostMintQuoteBolt11Response postMintQuoteBolt11Response, GetKeysResponse.KeysetItemResponse? verifiedKeyset, List? amounts) + { + this._quote = postMintQuoteBolt11Response; + this._keyset = verifiedKeyset; + this._amounts = amounts; + } + + public async Task Mint(CancellationToken cts = default) + { + + if (_quote.Amount == null) + { + //todo amountless flow + return new PostMintResponse(); + } + + if (_amounts == null) + { + var amounts = CashuUtils.SplitToProofsAmounts(_quote.Amount.Value, _keyset.Keys); + } + + if (this._outputs == null) + { + this._outputs = await _wallet.CreateOutputs(_amounts!, _keysetId, cts); + } + + var req = new PostMintRequest + { + Outputs = this._outputs.BlindedMessages, + Quote = _quote.Quote + }; + return await this._processMint(req, cts); + } + + private async Task _processMint(PostMintRequest req, CancellationToken cts = default) + { + var client = this._wallet.GetMintApi(); + if (client is null) + { + throw new ArgumentNullException(nameof(CashuHttpClient), "Mint api can't be null!"); + } + + return await client.Mint("bolt11", req, cts); + } + + public Task Subscribe() + { + throw new NotImplementedException(); + // await this._websocketService.SubscribeToSingleMeltQuoteAsync(); + } +} \ No newline at end of file diff --git a/DotNut/Abstractions/Quotes/MintQuoteBolt11.cs b/DotNut/Abstractions/Quotes/MintQuoteBolt11.cs deleted file mode 100644 index 7fd0633..0000000 --- a/DotNut/Abstractions/Quotes/MintQuoteBolt11.cs +++ /dev/null @@ -1,15 +0,0 @@ -using DotNut.ApiModels; - -namespace DotNut.Abstractions; - -public class MintQuoteBolt11 -{ - private readonly string method = "bolt11"; - private ulong Amount; - - public MintQuoteBolt11(PostMintQuoteBolt11Response response) - { - this.Amount = response.Amount; - - } -} \ No newline at end of file diff --git a/DotNut/Abstractions/Quotes/MintQuoteBolt12.cs b/DotNut/Abstractions/Quotes/MintQuoteBolt12.cs deleted file mode 100644 index f075690..0000000 --- a/DotNut/Abstractions/Quotes/MintQuoteBolt12.cs +++ /dev/null @@ -1,6 +0,0 @@ -namespace DotNut.Abstractions; - -public class MintQuoteBolt12 -{ - -} \ No newline at end of file diff --git a/DotNut/Abstractions/CashuWallet.cs b/DotNut/Abstractions/Wallet.cs similarity index 89% rename from DotNut/Abstractions/CashuWallet.cs rename to DotNut/Abstractions/Wallet.cs index 79a7e9a..6d430e3 100644 --- a/DotNut/Abstractions/CashuWallet.cs +++ b/DotNut/Abstractions/Wallet.cs @@ -1,3 +1,4 @@ +using DotNut.Abstractions.Interfaces; using DotNut.Api; using DotNut.ApiModels; using DotNut.NBitcoin.BIP39; @@ -13,7 +14,7 @@ namespace DotNut.Abstractions; /// /// Main Cashu Wallet class implementing fluent builder pattern /// -public class CashuWallet : ICashuWalletBuilder +public class Wallet : ICashuWalletBuilder { private MintInfo? _info; private IProofSelector? _selector; @@ -24,14 +25,14 @@ public class CashuWallet : ICashuWalletBuilder private string _unit = "sat"; private Mnemonic? _mnemonic; - private Counter? _counter; + private ICounter? _counter; //flags private bool _shouldSyncKeyset = true; private bool _shouldBumpCounter = true; private bool _allowInvalidKeysetIds = false; - public static ICashuWalletBuilder Create() => new CashuWallet(); + public static ICashuWalletBuilder Create() => new Wallet(); /// /// Mandatory. Sets a mint in a wallet object @@ -112,16 +113,6 @@ public ICashuWalletBuilder WithKeysetSync(bool syncKeyset = true) this._shouldSyncKeyset = syncKeyset; return this; } - - /// - /// Optional. Base unit of wallet instance. If not set defaults to "SAT". - /// - /// - public ICashuWalletBuilder WithUnit(string unit) - { - this._unit = unit; - return this; - } /// /// Optional. Proof selecting algorithm. If not set, defaults to RGLI proof selector. @@ -157,7 +148,7 @@ public ICashuWalletBuilder WithMnemonic(string mnemonic) /// Optional and mandatory if Mnemonic provided. Counter for each Keyset Id for derivation purposes. /// /// Counter object - public ICashuWalletBuilder WithCounter(Counter counter) + public ICashuWalletBuilder WithCounter(ICounter counter) { this._counter = counter; return this; @@ -192,22 +183,22 @@ public ICashuWalletBuilder ShouldBumpCounter(bool shouldBumpCounter = true) /// Create swap transaction builder. /// /// Swap transaction builder - public async Task Swap() + public ICashuWalletSwapBuilder Swap() { return new CashuWalletSwapBuilder(this); } - public async Task CreateMeltQuote() + public CreateMintQuote() + public ICashuWalletMintBuilder CreateMintQuote() { return new CashuWalletMintQuoteBuilder(this); } - public async Task Restore() + public Task Restore() { return new CashuWalletRestoreBuilder(this); } @@ -389,22 +380,52 @@ public async Task CreateOutputs(List amounts, KeysetId id, Ca if (this._counter == null) { - throw new ArgumentNullException(nameof(Counter), "Can't derive outputs without keyset counter"); + throw new ArgumentNullException(nameof(ICounter), "Can't derive outputs without keyset counter"); } - var counterValue = this._counter.GetCounterForId(id); + var counterValue = await this._counter.GetCounterForId(id); if (_shouldBumpCounter) { - this._counter.IncrementCounter(id, amounts.Count); + await this._counter.IncrementCounter(id, amounts.Count); } return CashuUtils.CreateOutputs(amounts, id, keyset.Keys, this._mnemonic, counterValue); } + internal async Task _swap(PostSwapRequest request, CancellationToken cts = default) + { + if (!_ensureApiConnected()) + { + throw new ArgumentNullException(nameof(this._mintApi), "Can't swap without mintApi"); + } + + return await this._mintApi!.Swap(request, cts); + } + internal async Task Mint(string quote, OutputData outputs, string method, CancellationToken cts = default) + { + if (method != "bolt11" && method != "bolt12") + { + throw new ArgumentException("Only bolt11, and bolt12 methods are supported"); + } + + if (!_ensureApiConnected()) + { + throw new ArgumentNullException(nameof(this._mintApi), "Can't mint without mintApi"); + } + + + var req = new PostMintRequest() + { + Quote = quote, + Outputs = outputs.BlindedMessages + }; + this._mintApi!.Mint(method, req, cts); + } + public IProofSelector? GetSelector() => _selector; public ICashuApi? GetMintApi() => _mintApi; public Mnemonic? GetMnemonic() => _mnemonic; public string GetUnit() => _unit; - public Counter? GetCounter() => _counter; + public ICounter? GetCounter() => _counter; private bool _ensureApiConnected() => _mintApi != null; } @@ -415,7 +436,7 @@ public async Task CreateOutputs(List amounts, KeysetId id, Ca /// internal class CashuWalletSwapBuilder : ICashuWalletSwapBuilder { - private readonly CashuWallet _wallet; + private readonly Wallet _wallet; // input private readonly string? _tokenString; @@ -425,50 +446,54 @@ internal class CashuWalletSwapBuilder : ICashuWalletSwapBuilder private OutputData? _outputs; private List? _amounts; private KeysetId? _keysetId; - + private string? _unit; private bool _verifySignatures = true; - public CashuWalletSwapBuilder(CashuWallet wallet, string tokenString) + public CashuWalletSwapBuilder(Wallet wallet, string tokenString) { _wallet = wallet; _tokenString = tokenString; } - - public CashuWalletSwapBuilder(CashuWallet wallet, CashuToken token) + public CashuWalletSwapBuilder(Wallet wallet, CashuToken token) { _wallet = wallet; _token = token; } - - public CashuWalletSwapBuilder(CashuWallet wallet) + public CashuWalletSwapBuilder(Wallet wallet) { _wallet = wallet; } - + + /// + /// Optional. Base unit of wallet instance. If not set defaults to "SAT". + /// + /// + public ICashuWalletSwapBuilder WithUnit(string unit) + { + this._unit = unit; + return this; + } + public ICashuWalletSwapBuilder WithSignatureVerification(bool verify = true) { _verifySignatures = verify; return this; } - public ICashuWalletSwapBuilder WithOutputs(OutputData outputs) { _outputs = outputs; return this; } - public ICashuWalletSwapBuilder WithAmounts(IEnumerable amounts) { _amounts = amounts.ToList(); return this; } - public ICashuWalletSwapBuilder ForKeyset(KeysetId keysetId) { _keysetId = keysetId; return this; } - private async Task> _getSwapProofs() { _proofsToSwap ??= new(); @@ -500,8 +525,6 @@ private async Task> _getSwapProofs() return _proofsToSwap; } - - public async Task> ProcessAsync(CancellationToken cts = default) { if (_wallet.GetMintApi() == null) @@ -562,21 +585,16 @@ public async Task> ProcessAsync(CancellationToken cts = default) internal class CashuWalletMeltQuoteBuilder : ICashuWalletMeltQuoteBuilder { - private readonly CashuWallet _wallet; + private readonly Wallet _wallet; private List? _proofs; private string? _invoice; private OutputData? _blankOutputs; private ulong? _amount; - public CashuWalletMeltQuoteBuilder(CashuWallet wallet) + public CashuWalletMeltQuoteBuilder(Wallet wallet) { _wallet = wallet; } - - public ICashuWalletMeltQuoteBuilder WithQuote(string quoteId) - { - throw new NotImplementedException(); - } - + public ICashuWalletMeltQuoteBuilder WithInvoice(string invoice) { this._invoice = invoice; @@ -638,25 +656,24 @@ public async Task ProcessAsyncBolt11(CancellationToken cancella return new MeltQuoteBolt11(mintResponse); } } -internal class CashuWalletMintQuoteBuilder(CashuWallet wallet) : ICashuWalletMintBuilder +internal class CashuWalletMintQuoteBuilder(Wallet wallet) : ICashuWalletMintBuilder { - private readonly CashuWallet _wallet = wallet; - - public ICashuWalletMintBuilder WithQuote(string quoteId) => this; + private readonly Wallet _wallet = wallet; public ICashuWalletMintBuilder WithAmount(ulong amount) => this; public ICashuWalletMintBuilder WithOutputs(IEnumerable outputs) => this; public ICashuWalletMintBuilder WithMethod(string method = "bolt11") => this; - public Task ProcessAsync(CancellationToken cancellationToken = default) + public async Task ProcessAsync(CancellationToken cts = default) { - throw new NotImplementedException(); + var info = await this._wallet.GetInfo(false, cts); + info.IsSupportedMintMelt() } } internal class CashuWalletRestoreBuilder : ICashuWalletRestoreBuilder { - private readonly CashuWallet _wallet; + private readonly Wallet _wallet; private List _specifiedKeysets; - public CashuWalletRestoreBuilder(CashuWallet wallet) => _wallet = wallet; + public CashuWalletRestoreBuilder(Wallet wallet) => _wallet = wallet; public ICashuWalletRestoreBuilder ForKeysets(IEnumerable keysetIds) => this; From d81df3d918c1bbf50e76a1c543ec3506a527c3ea Mon Sep 17 00:00:00 2001 From: d4rp4t Date: Wed, 1 Oct 2025 12:53:42 +0200 Subject: [PATCH 07/70] wip + add restore --- DotNut/Abstractions/Interfaces/ICounter.cs | 6 +- DotNut/Abstractions/Interfaces/IWallet.cs | 14 +- .../{MintHandler.cs => MintHandlerBolt11.cs} | 0 DotNut/Abstractions/MintHandlerBolt12.cs | 30 ++ DotNut/Abstractions/Wallet.cs | 365 +++++++++++++++--- DotNut/Abstractions/WalletResults.cs | 32 +- DotNut/NUT13/Nut13.cs | 42 +- 7 files changed, 402 insertions(+), 87 deletions(-) rename DotNut/Abstractions/{MintHandler.cs => MintHandlerBolt11.cs} (100%) create mode 100644 DotNut/Abstractions/MintHandlerBolt12.cs diff --git a/DotNut/Abstractions/Interfaces/ICounter.cs b/DotNut/Abstractions/Interfaces/ICounter.cs index 9c09d16..0f1f5c7 100644 --- a/DotNut/Abstractions/Interfaces/ICounter.cs +++ b/DotNut/Abstractions/Interfaces/ICounter.cs @@ -2,7 +2,7 @@ namespace DotNut.Abstractions.Interfaces; public interface ICounter { - public Task GetCounterForId(KeysetId keysetId); - public Task IncrementCounter(KeysetId keysetId, int bumpBy = 1); - public Task SetCounter(KeysetId keysetId, int counter); + public Task GetCounterForId(KeysetId keysetId, CancellationToken cts = default); + public Task IncrementCounter(KeysetId keysetId, int bumpBy = 1, CancellationToken cts = default); + public Task SetCounter(KeysetId keysetId, int counter, CancellationToken cts = default); } \ No newline at end of file diff --git a/DotNut/Abstractions/Interfaces/IWallet.cs b/DotNut/Abstractions/Interfaces/IWallet.cs index 99a27f9..899df13 100644 --- a/DotNut/Abstractions/Interfaces/IWallet.cs +++ b/DotNut/Abstractions/Interfaces/IWallet.cs @@ -1,3 +1,4 @@ +using DotNut.Abstractions.Interfaces; using DotNut.Api; using DotNut.ApiModels; using DotNut.NBitcoin.BIP39; @@ -19,10 +20,12 @@ public interface ICashuWalletBuilder ICashuWalletBuilder WithMint(string mintUrl); ICashuWalletBuilder WithMnemonic(Mnemonic mnemonic); ICashuWalletBuilder WithMnemonic(string mnemonic); - ICashuWalletBuilder WithCounter(Counter counter); + ICashuWalletBuilder WithCounter(ICounter counter); Task GetInfo(bool forceReferesh = false, CancellationToken cts = default); Task CreateOutputs(List amounts, KeysetId id, CancellationToken cts = default); + Task?> GetActiveKeysetIdsWithUnits(); + ICashuApi? GetMintApi(); @@ -44,7 +47,7 @@ public interface ICashuWalletBuilder /// public interface ICashuWalletSwapBuilder { - ICashuWalletBuilder WithUnit(string unit); + ICashuWalletSwapBuilder WithUnit(string unit); ICashuWalletSwapBuilder ForKeyset(KeysetId targetKeysetId); ICashuWalletSwapBuilder WithOutputs(IEnumerable outputs); Task> ProcessAsync(CancellationToken cancellationToken = default); @@ -55,7 +58,7 @@ public interface ICashuWalletSwapBuilder /// public interface ICashuWalletMeltQuoteBuilder { - ICashuWalletBuilder WithUnit(string unit); + ICashuWalletMeltQuoteBuilder WithUnit(string unit); ICashuWalletMeltQuoteBuilder WithInvoice(string bolt11Invoice); ICashuWalletMeltQuoteBuilder WithMethod(string method = "bolt11"); Task ProcessAsync(CancellationToken cancellationToken = default); @@ -66,11 +69,12 @@ public interface ICashuWalletMeltQuoteBuilder /// public interface ICashuWalletMintBuilder { - ICashuWalletBuilder WithUnit(string unit); + ICashuWalletMintBuilder WithUnit(string unit); ICashuWalletMintBuilder WithAmount(ulong amount); ICashuWalletMintBuilder WithOutputs(IEnumerable outputs); ICashuWalletMintBuilder WithMethod(string method = "bolt11"); - Task ProcessAsync(CancellationToken cancellationToken = default); + // Task ProcessAsync(CancellationToken cancellationToken = default); + Task ProcessAsync(CancellationToken cancellationToken = default); } /// diff --git a/DotNut/Abstractions/MintHandler.cs b/DotNut/Abstractions/MintHandlerBolt11.cs similarity index 100% rename from DotNut/Abstractions/MintHandler.cs rename to DotNut/Abstractions/MintHandlerBolt11.cs diff --git a/DotNut/Abstractions/MintHandlerBolt12.cs b/DotNut/Abstractions/MintHandlerBolt12.cs new file mode 100644 index 0000000..f43443f --- /dev/null +++ b/DotNut/Abstractions/MintHandlerBolt12.cs @@ -0,0 +1,30 @@ +using DotNut.Abstractions.Websockets; +using DotNut.ApiModels; +using DotNut.ApiModels.Mint.bolt12; + +namespace DotNut.Abstractions; + +public class MintHandlerBolt12: IMintHandler +{ + + private readonly ICashuWalletBuilder _wallet; + private PostMintQuoteBolt12Response _quote; + private GetKeysResponse.KeysetItemResponse _keyset; + + public MintHandlerBolt12(Wallet wallet, PostMintQuoteBolt12Response quote, GetKeysResponse.KeysetItemResponse keyset) + { + this._wallet = wallet; + this._quote = quote; + this._keyset = keyset; + } + + public Task Mint(CancellationToken cts = default) + { + throw new NotImplementedException(); + } + + public Task Subscribe() + { + throw new NotImplementedException(); + } +} \ No newline at end of file diff --git a/DotNut/Abstractions/Wallet.cs b/DotNut/Abstractions/Wallet.cs index 6d430e3..790227d 100644 --- a/DotNut/Abstractions/Wallet.cs +++ b/DotNut/Abstractions/Wallet.cs @@ -1,13 +1,19 @@ +using System.Diagnostics; using DotNut.Abstractions.Interfaces; +using DotNut.Abstractions.Quotes; using DotNut.Api; using DotNut.ApiModels; +using DotNut.ApiModels.Mint.bolt12; using DotNut.NBitcoin.BIP39; +using DotNut.NUT13; namespace DotNut.Abstractions; // It should: // Allow user to use local keys from db + // Fetch keyets on start, if user didn't provide them. Fetch additional Keys. (if necessary) + // How can user save these? // ARE FLUENT BUILDER \ @@ -22,7 +28,6 @@ public class Wallet : ICashuWalletBuilder private List? _keysets; private List? _keys; private Dictionary? _keysetFees => _keysets?.ToDictionary(k=>k.Id, k=>k.InputFee??0); - private string _unit = "sat"; private Mnemonic? _mnemonic; private ICounter? _counter; @@ -144,6 +149,7 @@ public ICashuWalletBuilder WithMnemonic(string mnemonic) return this; } + /// /// Optional and mandatory if Mnemonic provided. Counter for each Keyset Id for derivation purposes. /// @@ -188,7 +194,7 @@ public ICashuWalletSwapBuilder Swap() return new CashuWalletSwapBuilder(this); } - public Restore() + public ICashuWalletRestoreBuilder Restore() { return new CashuWalletRestoreBuilder(this); } @@ -255,16 +261,15 @@ public Task Restore() { throw new ArgumentNullException(nameof(_mintApi), "Can't fetch mint info without mintApi"); } - var keysRaw = await _mintApi!.GetKeys(id, cts); - foreach (var keysetItemResponse in keysRaw.Keysets) + var keysRaw = (await _mintApi!.GetKeys(id, cts)).Keysets.Single(); + + var isKeysetIdValid = keysRaw.Keys.VerifyKeysetId(keysRaw.Id, keysRaw.Unit, keysRaw.FinalExpiry); + if (!isKeysetIdValid) { - var isKeysetIdValid = keysetItemResponse.Keys.VerifyKeysetId(keysetItemResponse.Id, keysetItemResponse.Unit, keysetItemResponse.FinalExpiry); - if (!isKeysetIdValid) - { - throw new ArgumentException($"Mint provided invalid keysetId. Provided: {keysetItemResponse.Id}, derived: {keysetItemResponse.Keys.GetKeysetId()} "); - } + throw new ArgumentException($"Mint provided invalid keysetId. Provided: {keysRaw.Id}, derived: {keysRaw.Keys.GetKeysetId()} "); } - return keysRaw.Keysets.Single(); + + return keysRaw; } /// @@ -301,7 +306,7 @@ private async Task _lazyFetchMintInfo(CancellationToken cts = default) /// /// /// - private async Task _maybeSyncKeys(CancellationToken cts = default) + internal async Task _maybeSyncKeys(CancellationToken cts = default) { if (!_shouldSyncKeyset) { @@ -334,13 +339,23 @@ private async Task _maybeSyncKeys(CancellationToken cts = default) this._keys.Add(keyset); } } - public async Task? GetActiveKeysetId(CancellationToken cts = default) + public async Task GetActiveKeysetId(string unit, CancellationToken cts = default) { return _keysets? .OrderBy(k => k.InputFee) - .FirstOrDefault(k => k.Active == true && k.Unit == this._unit, null) + .FirstOrDefault(k => k.Active == true && k.Unit == unit, null) ?.Id; } + + public async Task?> GetActiveKeysetIdsWithUnits() + { + return _keysets? + .GroupBy(k => k.Unit) + .ToDictionary( + g => g.Key, + g => g.OrderBy(k => k.InputFee).First().Id + ); + } public async Task> GetKeys(bool forceRefresh = false, CancellationToken cts = default) { if (forceRefresh) @@ -349,6 +364,19 @@ private async Task _maybeSyncKeys(CancellationToken cts = default) } return this._keys ?? []; } + + public async Task GetKeys(KeysetId id, bool forceRefresh = false, CancellationToken cts = default) + { + if (forceRefresh) + { + return await _fetchKeys(id, cts); + } + if (this._keys == null) + { + throw new ArgumentNullException(nameof(this._keys), "Wallet doesn't contain keys for this keyset!"); + } + return this._keys.Single(k => k.Id == id); + } public async Task> GetKeysets(bool forceRefresh = false, CancellationToken cts = default) { if (forceRefresh) @@ -400,41 +428,182 @@ internal async Task _swap(PostSwapRequest request, Cancellatio return await this._mintApi!.Swap(request, cts); } - internal async Task Mint(string quote, OutputData outputs, string method, CancellationToken cts = default) + + public IProofSelector? GetSelector() => _selector; + public ICashuApi? GetMintApi() => _mintApi; + public Mnemonic? GetMnemonic() => _mnemonic; + public ICounter? GetCounter() => _counter; + private bool _ensureApiConnected() => _mintApi != null; +} + +class CashuWalletMintQuoteBuilder : ICashuWalletMintBuilder +{ + private readonly Wallet _wallet; + private ulong? _amount; + private string _unit = "sat"; + private string? _description; + private OutputData? _outputs; + private string? _method = "bolt11"; + + //for bolt12 + private string? _pubkey; + + private KeysetId? _keysetId; + private GetKeysResponse.KeysetItemResponse keyset; + public CashuWalletMeltQuoteBuilder(Wallet wallet) + { + this._wallet = wallet; + } + + /// + /// Mandatory. + /// User has to provide Mint method + /// + /// Either MintMeltMethod.Bolt11 or MintMeltMethod.Bolt12 + /// + public ICashuWalletMintBuilder WithMethod(string method) + { + this._method = method; + return this; + } + + /// + /// Mandatory. + /// + /// Amount of token in currently choosen unit to be melted + public ICashuWalletMintBuilder WithAmount(ulong amount) + { + this._amount = amount; + return this; + } + + /// + /// Optional. + /// Sets unit of tokens being minted. Sat by default. + /// + /// Unit of minted proofs + public ICashuWalletMintBuilder WithUnit(string unit) + { + this._unit = unit; + return this; + } + + /// + /// Optional. Necessary for bolt12 + /// Sets pubkey for bolt12 offer + /// + /// + /// + public ICashuWalletMintBuilder WithPubkey(string pubkey) + { + this._pubkey = pubkey; + return this; + } + + /// + /// Optional. + /// Allows user to set keysetId manually. Otherwise, builder will choose active one manually, with the lowest fees. + /// + /// + public ICashuWalletMintBuilder WithKeyset(KeysetId keysetId) + { + this._keysetId = keysetId; + return this; + } + + + /// + /// Optional. + /// User may provide outputs for mint to sign. Blinding factors and secrets won't be revealed to mint. + /// If not provided, wallet will try to derive them from seed and counter, or create random ones if mnemonic is not avaible. + /// + /// OutputData instance. Enumerables of BlindingFactors, BlindedMessages and Secrets, in right order. + public ICashuWalletMintBuilder WithOutputs(OutputData outputs) + { + this._outputs = outputs; + return this; + } + + /// + /// Optional. + /// User may provide description for melt quote invoice. + /// + /// + /// + public ICashuWalletMintBuilder WithDescription(string description) { - if (method != "bolt11" && method != "bolt12") + this._description = description; + return this; + } + + + public async Task ProcessAsync(CancellationToken cts = default) + { + //todo implement info + + await this._wallet._maybeSyncKeys(cts); + if (_amount == null) { - throw new ArgumentException("Only bolt11, and bolt12 methods are supported"); + throw new ArgumentNullException(nameof(_amount), "can't create melt quote without amount!"); } - - if (!_ensureApiConnected()) + + var api = this._wallet.GetMintApi(); + if (api is null) { - throw new ArgumentNullException(nameof(this._mintApi), "Can't mint without mintApi"); + throw new ArgumentNullException(nameof(ICashuApi), "Can't request mint quote without mint API"); } - - var req = new PostMintRequest() + if (this._keysetId == null) { - Quote = quote, - Outputs = outputs.BlindedMessages - }; - this._mintApi!.Mint(method, req, cts); + this._keysetId = await this._wallet.GetActiveKeysetId(this._unit, cts) ?? + throw new ArgumentException($"Can't fetch active keyset ID for unit: {_unit}"); + } + + switch (_method) + { + case "bolt11": + { + var reqBolt11 = new PostMintQuoteBolt11Request() + { + Amount = this._amount.Value, + Unit = this._unit, + Description = this._description, + }; + var quoteBolt11 = + await api.CreateMintQuote("bolt11", reqBolt11, + cts); + return new MintHandlerBolt11(this._wallet, quoteBolt11, this.keyset); + } + case "bolt12": + { + if (this._pubkey == null) + { + throw new ArgumentNullException(nameof(_pubkey), "Can't request bolt12 mint quote without pubkey!"); + } + + var req = new PostMintQuoteBolt12Request() + { + Amount = this._amount.Value, + Unit = this._unit, + Pubkey = this._pubkey, + Description = this._description, + }; + var mintQuote = + await api.CreateMintQuote("bolt12", req, + cts); + return new MintHandlerBolt12(this._wallet, mintQuote, this.keyset); + } + default: + throw new ArgumentException($"Unknown mint method: {_method}"); + } } - public IProofSelector? GetSelector() => _selector; - public ICashuApi? GetMintApi() => _mintApi; - public Mnemonic? GetMnemonic() => _mnemonic; - public string GetUnit() => _unit; - public ICounter? GetCounter() => _counter; - private bool _ensureApiConnected() => _mintApi != null; } - - /// /// Receive operation builder implementation /// -internal class CashuWalletSwapBuilder : ICashuWalletSwapBuilder +class CashuWalletSwapBuilder : ICashuWalletSwapBuilder { private readonly Wallet _wallet; @@ -539,7 +708,7 @@ public async Task> ProcessAsync(CancellationToken cts = default) // if there's no keysetId specified - let's choose it. if (_keysetId == null) { - _keysetId = await _wallet.GetActiveKeysetId(cts) ?? + _keysetId = await _wallet.GetActiveKeysetId(this._unit, cts) ?? throw new InvalidOperationException("Could not fetch Keyset ID"); } var keys = await _wallet.GetKeys(false, cts); @@ -582,14 +751,15 @@ public async Task> ProcessAsync(CancellationToken cts = default) } } - -internal class CashuWalletMeltQuoteBuilder : ICashuWalletMeltQuoteBuilder +class CashuWalletMeltQuoteBuilder : ICashuWalletMeltQuoteBuilder { private readonly Wallet _wallet; private List? _proofs; private string? _invoice; private OutputData? _blankOutputs; private ulong? _amount; + private string? _method; + public CashuWalletMeltQuoteBuilder(Wallet wallet) { _wallet = wallet; @@ -656,36 +826,119 @@ public async Task ProcessAsyncBolt11(CancellationToken cancella return new MeltQuoteBolt11(mintResponse); } } -internal class CashuWalletMintQuoteBuilder(Wallet wallet) : ICashuWalletMintBuilder + +class CashuWalletRestoreBuilder : ICashuWalletRestoreBuilder { - private readonly Wallet _wallet = wallet; - public ICashuWalletMintBuilder WithAmount(ulong amount) => this; - public ICashuWalletMintBuilder WithOutputs(IEnumerable outputs) => this; - public ICashuWalletMintBuilder WithMethod(string method = "bolt11") => this; + private readonly Wallet _wallet; + private List? _specifiedKeysets; + + private bool _shouldSwap = true; - public async Task ProcessAsync(CancellationToken cts = default) + public CashuWalletRestoreBuilder(Wallet wallet) { - var info = await this._wallet.GetInfo(false, cts); - info.IsSupportedMintMelt() + this._wallet = wallet; + } + + public ICashuWalletRestoreBuilder ForKeysetIds(IEnumerable keysetIds) + { + this._specifiedKeysets = keysetIds.ToList(); + return this; } -} -internal class CashuWalletRestoreBuilder : ICashuWalletRestoreBuilder -{ - private readonly Wallet _wallet; - private List _specifiedKeysets; - public CashuWalletRestoreBuilder(Wallet wallet) => _wallet = wallet; - public ICashuWalletRestoreBuilder ForKeysets(IEnumerable keysetIds) => this; + public ICashuWalletRestoreBuilder WithSwap(bool shouldSwap = true) + { + this._shouldSwap = shouldSwap; + } - public Task ProcessAsync(CancellationToken cancellationToken = default) + public async Task> ProcessAsync(CancellationToken cts = default) { var mnemonic = _wallet.GetMnemonic()?? throw new ArgumentNullException("Can't restore wallet without Mnemonic"); if (_specifiedKeysets == null) { - _specifiedKeysets = _wallet.GetKeysets(); + _specifiedKeysets = (await _wallet.GetKeysets()).Select(k=>k.Id).ToList(); + } + var api = _wallet.GetMintApi(); + if (api == null) + { + throw new ArgumentNullException(nameof(api), "Can't restore wallet without MintApi"); + } + + var counter = _wallet.GetCounter(); + if (counter == null) + { + _wallet.WithCounter(new Counter(new Dictionary())); + } + + List recoveredProofs = new List(); + foreach (var keysetId in _specifiedKeysets) + { + bool isKeysetRestored = false; + int batchNumber = 0; + int emptyBatchesRemaining = 3; + + var keyset = await _wallet.GetKeys(keysetId, false, cts); + + while (!isKeysetRestored && emptyBatchesRemaining > 0) + { + var outputs = await _createBatch(mnemonic, keysetId, batchNumber, cts); + await counter!.IncrementCounter(keysetId, batchNumber * 100); + var req = new PostRestoreRequest + { + Outputs = outputs.BlindedMessages + }; + var res = await api.Restore(req, cts); + + if (!res.Signatures.Any()) + { + emptyBatchesRemaining--; + } + + var proofs = CashuUtils.ConstructProofsFromPromises(res.Signatures.ToList(), outputs, keyset.Keys); + recoveredProofs.AddRange(proofs); + } + + } + + if (!this._shouldSwap || !recoveredProofs.Any()) + { + return recoveredProofs; } - var counter = new Counter(); + var freshProofs = new List(); + var activeUnits = await this._wallet.GetActiveKeysetIdsWithUnits(); + if (activeUnits != null && !activeUnits.Any()) + { + throw new InvalidOperationException("Could not restore wallet without active keysets"); + } + + foreach (var unitKeyset in activeUnits) + { + var correspondingKeys = await _wallet.GetKeys(unitKeyset.Value, false, cts); + var totalAmount = recoveredProofs.Select(p=>p.Amount).Aggregate((a,c) => a + c); + var amounts = CashuUtils.SplitToProofsAmounts(totalAmount, correspondingKeys.Keys); + var ctr = await counter!.GetCounterForId(unitKeyset.Value, cts); + var newOutputs = CashuUtils.CreateOutputs(amounts, unitKeyset.Value, correspondingKeys.Keys, mnemonic, ctr); + await counter.IncrementCounter(unitKeyset.Value, newOutputs.BlindedMessages.Length, cts); + + var swapRequest = new PostSwapRequest + { + Inputs = recoveredProofs.ToArray(), + Outputs = newOutputs.BlindedMessages, + }; + + var swapResult = await _wallet._swap(swapRequest, cts); + + var constructedProofs = CashuUtils.ConstructProofsFromPromises(swapResult.Signatures.ToList(), newOutputs, correspondingKeys.Keys); + + freshProofs.AddRange(constructedProofs); + } + return freshProofs; + } + + private async Task _createBatch(Mnemonic mnemonic, KeysetId keysetId, int batchNubmber, CancellationToken cts) + { + var amounts = Enumerable.Repeat((ulong)1, 100).ToList(); + return mnemonic.DeriveOutputs(amounts, keysetId, batchNubmber*100); } } diff --git a/DotNut/Abstractions/WalletResults.cs b/DotNut/Abstractions/WalletResults.cs index 88c4a5f..2741927 100644 --- a/DotNut/Abstractions/WalletResults.cs +++ b/DotNut/Abstractions/WalletResults.cs @@ -47,16 +47,16 @@ public class MeltResult public string QuoteId { get; set; } = string.Empty; } -/// -/// Result of a mint operation (receiving from invoice) -/// -public class MintResult -{ - public List MintedProofs { get; set; } = new(); - public ulong AmountMinted { get; set; } - public string QuoteId { get; set; } = string.Empty; - public bool QuotePaid { get; set; } -} +// /// +// /// Result of a mint operation (receiving from invoice) +// /// +// public class MintResult +// { +// public List MintedProofs { get; set; } = new(); +// public ulong AmountMinted { get; set; } +// public string QuoteId { get; set; } = string.Empty; +// public bool QuotePaid { get; set; } +// } /// /// Result of checking proof states @@ -66,7 +66,7 @@ public class StateResult public Dictionary States { get; set; } = new(); } -/// +/// /// Proof state information /// public class ProofState @@ -75,13 +75,3 @@ public class ProofState public bool Pending { get; set; } public string? Witness { get; set; } } - -/// -/// Result of a restore operation -/// -public class RestoreResult -{ - public List RestoredProofs { get; set; } = new(); - public Dictionary States { get; set; } = new(); - public ulong TotalAmountRestored { get; set; } -} diff --git a/DotNut/NUT13/Nut13.cs b/DotNut/NUT13/Nut13.cs index c9f3a49..5fb23b5 100644 --- a/DotNut/NUT13/Nut13.cs +++ b/DotNut/NUT13/Nut13.cs @@ -13,8 +13,46 @@ public static byte[] DeriveBlindingFactor(this Mnemonic mnemonic, KeysetId keyse public static StringSecret DeriveSecret(this Mnemonic mnemonic, KeysetId keysetId, uint counter) => DeriveSecret(mnemonic.DeriveSeed(), keysetId, counter); - - public static byte[] DeriveBlindingFactor(this byte[] seed, KeysetId keysetId, uint counter) + + public static OutputData DeriveOutputs(this Mnemonic mnemonic, IEnumerable amounts, KeysetId keysetId, + int counter) + { + var blindedMessages = new List(); + var secrets = new List(); + var blindingFactors = new List(); + + var amountList = amounts.ToList(); + + for (int i = 0; i < amountList.Count; i++) + { + var secret = DeriveSecret(mnemonic, keysetId, counter + i); + var r = new PrivKey( + DeriveBlindingFactor(mnemonic, keysetId, counter + i) + ); + + var Y = secret.ToCurve(); + var B_ = Cashu.ComputeB_(Y, r); + + + blindedMessages.Add( + new BlindedMessage() + { + Amount = amountList.ElementAt(i), + Id = keysetId, + B_ = B_ + }); + secrets.Add(secret); + blindingFactors.Add(r); + } + + return new OutputData() + { + BlindedMessages = blindedMessages.ToArray(), + BlindingFactors = blindingFactors.ToArray(), + Secrets = secrets.ToArray() + }; + } + public static byte[] DeriveBlindingFactor(this byte[] seed, KeysetId keysetId, int counter) { switch (keysetId.GetVersion()) { From a98e9a9c2975e9134c5101dcdda75836297c52dc Mon Sep 17 00:00:00 2001 From: d4rp4t Date: Mon, 6 Oct 2025 00:22:17 +0200 Subject: [PATCH 08/70] wip --- DotNut.Tests/Integration.cs | 197 +++- DotNut.Tests/UnitTests2.cs | 33 + DotNut.sln.DotSettings.user | 23 +- DotNut/Abstractions/CashuUtils.cs | 70 +- DotNut/Abstractions/Counter.cs | 25 - .../Handlers/MeltHandlerBolt11.cs | 49 + .../Handlers/MeltHandlerBolt12.cs | 23 + .../Handlers/MintHandlerBolt11.cs | 82 ++ .../Handlers/MintHandlerBolt12.cs | 60 ++ DotNut/Abstractions/InMemoryCounter.cs | 37 + DotNut/Abstractions/InMemoryProofManager.cs | 30 + .../Abstractions/Interfaces/IMeltHandler.cs | 9 +- .../Interfaces/IMeltQuoteBuilder.cs | 16 + .../Abstractions/Interfaces/IMintHandler.cs | 7 +- .../Interfaces/IMintQuoteBuilder.cs | 18 + .../Abstractions/Interfaces/IProofManager.cs | 9 + .../Abstractions/Interfaces/IProofSelector.cs | 2 +- .../Interfaces/IRestoreBuilder.cs | 11 + .../Interfaces/IStatefulWalletBuilder.cs | 13 + .../Abstractions/Interfaces/ISwapBuilder.cs | 13 + DotNut/Abstractions/Interfaces/IWallet.cs | 87 -- .../Abstractions/Interfaces/IWalletBuilder.cs | 64 ++ DotNut/Abstractions/MeltQuoteBuilder.cs | 104 ++ DotNut/Abstractions/MintHandlerBolt11.cs | 85 -- DotNut/Abstractions/MintHandlerBolt12.cs | 30 - .../Abstractions/MintMeltDisabledException.cs | 3 + DotNut/Abstractions/MintQuoteBuilder.cs | 191 ++++ DotNut/Abstractions/ProofSelector.cs | 2 +- DotNut/Abstractions/RestoreBuilder.cs | 128 +++ DotNut/Abstractions/StatefulWallet.cs | 35 + DotNut/Abstractions/SwapBuilder.cs | 275 ++++++ DotNut/Abstractions/Wallet.cs | 898 ++++++------------ DotNut/Abstractions/WalletResults.cs | 77 -- DotNut/NUT02/FeeHelper.cs | 5 + 34 files changed, 1724 insertions(+), 987 deletions(-) create mode 100644 DotNut.Tests/UnitTests2.cs delete mode 100644 DotNut/Abstractions/Counter.cs create mode 100644 DotNut/Abstractions/Handlers/MeltHandlerBolt11.cs create mode 100644 DotNut/Abstractions/Handlers/MeltHandlerBolt12.cs create mode 100644 DotNut/Abstractions/Handlers/MintHandlerBolt11.cs create mode 100644 DotNut/Abstractions/Handlers/MintHandlerBolt12.cs create mode 100644 DotNut/Abstractions/InMemoryCounter.cs create mode 100644 DotNut/Abstractions/InMemoryProofManager.cs create mode 100644 DotNut/Abstractions/Interfaces/IMeltQuoteBuilder.cs create mode 100644 DotNut/Abstractions/Interfaces/IMintQuoteBuilder.cs create mode 100644 DotNut/Abstractions/Interfaces/IProofManager.cs create mode 100644 DotNut/Abstractions/Interfaces/IRestoreBuilder.cs create mode 100644 DotNut/Abstractions/Interfaces/IStatefulWalletBuilder.cs create mode 100644 DotNut/Abstractions/Interfaces/ISwapBuilder.cs delete mode 100644 DotNut/Abstractions/Interfaces/IWallet.cs create mode 100644 DotNut/Abstractions/Interfaces/IWalletBuilder.cs create mode 100644 DotNut/Abstractions/MeltQuoteBuilder.cs delete mode 100644 DotNut/Abstractions/MintHandlerBolt11.cs delete mode 100644 DotNut/Abstractions/MintHandlerBolt12.cs create mode 100644 DotNut/Abstractions/MintMeltDisabledException.cs create mode 100644 DotNut/Abstractions/MintQuoteBuilder.cs create mode 100644 DotNut/Abstractions/RestoreBuilder.cs create mode 100644 DotNut/Abstractions/StatefulWallet.cs create mode 100644 DotNut/Abstractions/SwapBuilder.cs delete mode 100644 DotNut/Abstractions/WalletResults.cs diff --git a/DotNut.Tests/Integration.cs b/DotNut.Tests/Integration.cs index 4f779e9..4852bbb 100644 --- a/DotNut.Tests/Integration.cs +++ b/DotNut.Tests/Integration.cs @@ -1,11 +1,22 @@ using DotNut.Abstractions; +using DotNut.Abstractions.Interfaces; using DotNut.Api; +using Xunit.Sdk; namespace DotNut.Tests; public class Integration { - const string MintUrl = "http://localhost:3338"; + private static string MintUrl = "http://localhost:3338"; + + private static string seed = + "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about"; + + private static readonly Dictionary valuesInvoices = new Dictionary() + { + {1000, "lnbc10u1p5w6vggsp5gn5xhswgn5299w6elu2z0vzjxhf9hwd6pwjcgfwphaxunyu0dx6spp5a60trrhce2u6tzqjwjczem8rpdesgzkawcqg2xqaesz2kd50z4uqhp5uwcvgs5clswpfxhm7nyfjmaeysn6us0yvjdexn9yjkv3k7zjhp2sxq9z0rgqcqpnrzjqw22kc09xj0dm65ew5h5r003vtn72eyzchdgjag66l0yhwdudfmuzrwvesqq8qgqqgqqqqqqqqqqzhsq2q9qxpqysgq955gcfr95wwz0ehtnk3xraatkyhj88z44ku7yqutnwnt3gkh82jxehvdff7n2js2p54jgpvg6dmwmq8t9d8x05j63mqjrsr4cwd4lpcpnc39ru"} + }; + private static ICounter counter = new InMemoryCounter(); [Fact] public void CreatesWalletSuccesfully() @@ -13,6 +24,18 @@ public void CreatesWalletSuccesfully() var wallet = Wallet.Create(); Assert.NotNull(wallet); } + + [Fact] + public async Task ThrowsWhenMintNotFound() + { + var wallet = Wallet.Create(); + await Assert.ThrowsAsync(async () => await wallet.GetInfo()); + await Assert.ThrowsAsync(async () => wallet.Restore()); + await Assert.ThrowsAsync(async () => wallet.Swap()); + await Assert.ThrowsAsync(async () => wallet.CreateMeltQuote()); + await Assert.ThrowsAsync(async () => wallet.CreateMintQuote()); + } + [Fact] public async Task FetchesInfoSuccessfully() { @@ -20,29 +43,169 @@ public async Task FetchesInfoSuccessfully() var info = await wallet.GetInfo(); Assert.NotNull(info); } - + [Fact] - public async Task ThrowsWhenMintNotFound() + public async Task MintsSuccessfully() { - var wallet = Wallet.Create(); - await Assert.ThrowsAsync(async () => await wallet.GetInfo()); - await Assert.ThrowsAsync(async () => wallet.Restore()); - await Assert.ThrowsAsync(async () => wallet.Swap()); - await Assert.ThrowsAsync(async () => wallet.CreateMeltQuote()); - await Assert.ThrowsAsync(async () => wallet.CreateMintQuote()); + var wallet = Wallet.Create().WithMint(MintUrl); + + var mintQuote = await wallet + .CreateMintQuote() + .WithUnit("sat") + .WithAmount(1337) + .ProcessAsyncBolt11(); + + Assert.NotNull(mintQuote); + + var paymentRequest = (await mintQuote.GetQuote()).Request; + Assert.Contains("lnbc1337", paymentRequest); + + //We're using fakewallet, so after 3 secs it will get paid automatically + await Task.Delay(3000); + + var mintResponse = await mintQuote.Mint(); + Assert.NotNull(mintResponse); + Assert.Equal(1337UL, CashuUtils.SumProofs(mintResponse)); } [Fact] - public async Task MeltsSucessfully() + public async Task MintsDeterministicSuccessfully() { - var wallet = Wallet.Create().WithMint(MintUrl); + var wallet = Wallet + .Create() + .WithMint(MintUrl) + .WithMnemonic(seed) + .WithCounter(counter); - var quote = wallet + var mintQuote = await wallet .CreateMintQuote() - .WithMethod("bolt11") - .WithUnit() - - + .WithUnit("sat") + .WithAmount(1337) + .ProcessAsyncBolt11(); + + Assert.NotNull(mintQuote); + + var paymentRequest = (await mintQuote.GetQuote()).Request; + Assert.Contains("lnbc1337", paymentRequest); + + await Task.Delay(3000); + var mintedProofs = await mintQuote.Mint(); + + var keysetId = mintedProofs.First().Id; + var currentCounter = await counter.GetCounterForId(keysetId); + // counter is bumped after every use, so its already one more + Assert.Equal(currentCounter, mintedProofs.Count); + } + + [Fact] + public async Task RestoresSuccessfully() + { + var wallet = Wallet + .Create() + .WithMint(MintUrl) + .WithMnemonic(seed); + var restoredProofs = await wallet + .Restore() + .WithSwap(false) + .ProcessAsync(); + var keyset = (await wallet.GetKeys()).First().Keys; + var expectedAmount = CashuUtils.SplitToProofsAmounts(1337UL, keyset).Count; + Assert.Equal(expectedAmount, restoredProofs.Count()); } -} \ No newline at end of file + [Fact] + public async Task SwapsSuccessfully() + { + var wallet = Wallet + .Create() + .WithMint(MintUrl); + + // 1. mint some proofs (deterministic, because why not) + var mintQuote = await wallet + .CreateMintQuote() + .WithAmount(64) + .WithUnit("sat") + .ProcessAsyncBolt11(); + + await Task.Delay(3000); + var mintedProofs = await mintQuote.Mint(); + Assert.NotEmpty(mintedProofs); + + //2. Swap them + var newProofs = await wallet + .Swap() + .FromInputs(mintedProofs) + .ProcessAsync(); + + Assert.NotEmpty(newProofs); + } + + public async Task SwapsDeterministicSuccessfully() + { + var wallet = Wallet + .Create() + .WithMint(MintUrl) + .WithMnemonic(seed) + .WithCounter(counter); + + // 1. mint some proofs (deterministic, because why not) + var mintQuote = await wallet + .CreateMintQuote() + .WithAmount(64) + .WithUnit("sat") + .ProcessAsyncBolt11(); + + await Task.Delay(3000); + var mintedProofs = await mintQuote.Mint(); + Assert.NotEmpty(mintedProofs); + + //2. Swap them + var newProofs = await wallet + .Swap() + .FromInputs(mintedProofs) + .ProcessAsync(); + + Assert.NotEmpty(newProofs); + } + + [Fact] + public async Task MeltsSuccessfully() + { + // mint proofs + var wallet = Wallet + .Create() + .WithMint(MintUrl) + .WithMnemonic(seed) + .WithCounter(counter); + + var mintQuote = await wallet + .CreateMintQuote() + .WithUnit("sat") + .WithAmount(1337) + .ProcessAsyncBolt11(); + await Task.Delay(3000); + var mintedProofs = await mintQuote.Mint(); + Assert.NotEmpty(mintedProofs); + + var Ids = mintedProofs.Select(proof => proof.Id).Count(); + + Console.WriteLine($"amounts {Ids}"); + // create melt quote + var meltQuote = await wallet + .CreateMeltQuote() + .WithInvoice(valuesInvoices[1000]) + .WithUnit("sat") + .ProcessAsyncBolt11(); + + // select proofs to send + var q = await meltQuote.GetQuote(); + var selectedProofs = await wallet.SelectProofsToSend(mintedProofs, q.Amount + (ulong)q.FeeReserve, true); + + //melt proofs + var change = await meltQuote.Melt(selectedProofs.Send); + + Assert.NotEmpty(change); + } +} + + diff --git a/DotNut.Tests/UnitTests2.cs b/DotNut.Tests/UnitTests2.cs new file mode 100644 index 0000000..3018e5d --- /dev/null +++ b/DotNut.Tests/UnitTests2.cs @@ -0,0 +1,33 @@ +using DotNut.Abstractions; + +namespace DotNut.Tests; +/// +/// Tests of higher-level abstractions +/// +public class UnitTests2 +{ + [Fact] + public async Task InMemoryCounter() + { + var ctr = new InMemoryCounter(); + Assert.NotNull(ctr); + var testId1 = new KeysetId("00qwertyuiopasdf"); + var ctrNum = await ctr.GetCounterForId(testId1); + Assert.Equal(0, ctrNum); + + await ctr.IncrementCounter(testId1); + Assert.Equal(0, ctrNum); + ctrNum = await ctr.GetCounterForId(testId1); + Assert.Equal(1, ctrNum); + + await ctr.IncrementCounter(testId1, 5); + ctrNum = await ctr.GetCounterForId(testId1); + Assert.Equal(6, ctrNum); + + await ctr.SetCounter(testId1, 1337); + ctrNum = await ctr.GetCounterForId(testId1); + Assert.Equal(1337, ctrNum); + } + + +} \ No newline at end of file diff --git a/DotNut.sln.DotSettings.user b/DotNut.sln.DotSettings.user index 47f8615..42384bd 100644 --- a/DotNut.sln.DotSettings.user +++ b/DotNut.sln.DotSettings.user @@ -2,20 +2,29 @@ ForceIncluded ForceIncluded ForceIncluded + ForceIncluded ForceIncluded ForceIncluded ForceIncluded + ForceIncluded ForceIncluded ForceIncluded ForceIncluded ForceIncluded - C:\Users\evilk\AppData\Local\JetBrains\Rider2024.2\resharper-host\temp\Rider\vAny\CoverageData\_DotNut.1481064820\Snapshot\snapshot.utdcvr - <SessionState ContinuousTestingMode="0" Name="Test1" xmlns="urn:schemas-jetbrains-com:jetbrains-ut-session"> - <TestAncestor> - <TestId>xUnit::0AAAA85C-9FDC-4FD5-9CC2-ED8385B40106::net8.0::DotNut.Tests.UnitTest1.Nut00Tests_TokenSerialization</TestId> - </TestAncestor> + + <SessionState ContinuousTestingMode="0" IsActive="True" Name="Test1" xmlns="urn:schemas-jetbrains-com:jetbrains-ut-session"> + <TestAncestor> + <TestId>xUnit::0AAAA85C-9FDC-4FD5-9CC2-ED8385B40106::net8.0::DotNut.Tests.UnitTest1</TestId> + <TestId>xUnit::0AAAA85C-9FDC-4FD5-9CC2-ED8385B40106::net8.0::DotNut.Tests.Integration</TestId> + </TestAncestor> </SessionState> - <SessionState ContinuousTestingMode="0" IsActive="True" Name="Nut11_Signatures" xmlns="urn:schemas-jetbrains-com:jetbrains-ut-session"> - <Project Location="C:\Git\DotNuts\DotNut.Tests" Presentation="&lt;DotNut.Tests&gt;" /> + <SessionState ContinuousTestingMode="0" Name="Nut11_Signatures" xmlns="urn:schemas-jetbrains-com:jetbrains-ut-session"> + <TestAncestor> + <TestId>xUnit::0AAAA85C-9FDC-4FD5-9CC2-ED8385B40106::net8.0::DotNut.Tests.UnitTest1</TestId> + <TestId>xUnit::0AAAA85C-9FDC-4FD5-9CC2-ED8385B40106::net8.0::DotNut.Tests.Integration.ThrowsWhenMintNotFound</TestId> + <TestId>xUnit::0AAAA85C-9FDC-4FD5-9CC2-ED8385B40106::net8.0::DotNut.Tests.Integration.FetchesInfoSuccessfully</TestId> + <TestId>xUnit::0AAAA85C-9FDC-4FD5-9CC2-ED8385B40106::net8.0::DotNut.Tests.Integration.MintsSuccessfully</TestId> + <TestId>xUnit::0AAAA85C-9FDC-4FD5-9CC2-ED8385B40106::net8.0::DotNut.Tests.Integration.MintsDeterministicSuccessfully</TestId> + </TestAncestor> </SessionState> \ No newline at end of file diff --git a/DotNut/Abstractions/CashuUtils.cs b/DotNut/Abstractions/CashuUtils.cs index e34f290..7c57009 100644 --- a/DotNut/Abstractions/CashuUtils.cs +++ b/DotNut/Abstractions/CashuUtils.cs @@ -1,10 +1,11 @@ +using System.Runtime.InteropServices.ComTypes; using System.Security.Cryptography; using DotNut.NUT13; using NBitcoin.Secp256k1; namespace DotNut.Abstractions; -public class CashuUtils +public static class CashuUtils { /// /// Function mapping payment amount to keyset supported amounts in order to create swap payload. Always tries to fit the biggest proof. @@ -59,8 +60,7 @@ public static OutputData CreateBlankOutputs(ulong amount, KeysetId keysetId, Key /// /// Amount of tokens that has to be covered by mint. /// Integer amount of blank outputs needed - /// If amount is 0 - idk why someone would do that - private static int CalculateNumberOfBlankOutputs(ulong amountToCover) + public static int CalculateNumberOfBlankOutputs(ulong amountToCover) { if (amountToCover == 0) { @@ -75,6 +75,7 @@ private static int CalculateNumberOfBlankOutputs(ulong amountToCover) ), 1); } + /// /// Creates outputs for swap/melt fee return. Outputs should have valid amounts. /// @@ -97,41 +98,34 @@ public static OutputData CreateOutputs( var secrets = new List(amounts.Count); var blindingFactors = new List(amounts.Count); - Func secretFactory; - Func blindingFactorFactory; if (mnemonic is not null && counter is { } c) { - secretFactory = () => mnemonic.DeriveSecret(keysetId, c); - blindingFactorFactory = () => new PrivKey( - Convert.ToHexString(mnemonic.DeriveBlindingFactor(keysetId, c)) - ); - } - else - { - secretFactory = () => + for (var i = 0; i < amounts.Count; i++) { - var bytes = RandomNumberGenerator.GetBytes(32); - return new StringSecret(Convert.ToHexString(bytes)); - }; + var secret = mnemonic.DeriveSecret(keysetId, c + i); + secrets.Add(secret); - blindingFactorFactory = () => - { - var bytes = RandomNumberGenerator.GetBytes(32); - return new PrivKey(Convert.ToHexString(bytes)); - }; - } + var r = new PrivKey(mnemonic.DeriveBlindingFactor(keysetId, c + i)); + blindingFactors.Add(r); - foreach (var amount in amounts) + var B_ = Cashu.ComputeB_(secret.ToCurve(), r); + blindedMessages.Add(new BlindedMessage {Amount = amounts[i], B_ = B_, Id = keysetId }); + } + } + else { - var secret = secretFactory(); - secrets.Add(secret); + foreach (var amount in amounts) + { + var secret = RandomSecret(); + secrets.Add(secret); - var r = blindingFactorFactory(); - blindingFactors.Add(r); + var r = RandomPrivkey(); + blindingFactors.Add(r); - var B_ = DotNut.Cashu.ComputeB_(secret.ToCurve(), r); - blindedMessages.Add(new BlindedMessage() { Amount = amount, B_ = B_, Id = keysetId }); + var B_ = DotNut.Cashu.ComputeB_(secret.ToCurve(), r); + blindedMessages.Add(new BlindedMessage() { Amount = amount, B_ = B_, Id = keysetId }); + } } return new OutputData() @@ -203,8 +197,22 @@ Keyset keys } return proofs; } - - + public static ulong SumProofs(List proofs) + { + return proofs.Aggregate(0UL, (current, proof) => current + proof.Amount); + } + + public static ISecret RandomSecret() + { + var bytes = RandomNumberGenerator.GetBytes(32); + return new StringSecret(Convert.ToHexString(bytes)); + } + + public static PrivKey RandomPrivkey() + { + var bytes = RandomNumberGenerator.GetBytes(32); + return new PrivKey(Convert.ToHexString(bytes)); + } } \ No newline at end of file diff --git a/DotNut/Abstractions/Counter.cs b/DotNut/Abstractions/Counter.cs deleted file mode 100644 index 4c61755..0000000 --- a/DotNut/Abstractions/Counter.cs +++ /dev/null @@ -1,25 +0,0 @@ -using DotNut; -using DotNut.Abstractions.Interfaces; - -public class Counter : ICounter -{ - private Dictionary _counter; - public Counter(IDictionary dictionary){ } - public async Task GetCounterForId(KeysetId keysetId) - { - if (_counter.TryGetValue(keysetId, out var counter)) - return counter; - - return _counter[keysetId] = 0; - } - - public async Task IncrementCounter(KeysetId keysetId, int bumpBy = 1) - { - var current = await GetCounterForId(keysetId); - var next = current + bumpBy; - _counter[keysetId] = next; - return next; - } - - public async Task SetCounter(KeysetId keysetId, int counter) => _counter[keysetId] = counter; -} \ No newline at end of file diff --git a/DotNut/Abstractions/Handlers/MeltHandlerBolt11.cs b/DotNut/Abstractions/Handlers/MeltHandlerBolt11.cs new file mode 100644 index 0000000..244f438 --- /dev/null +++ b/DotNut/Abstractions/Handlers/MeltHandlerBolt11.cs @@ -0,0 +1,49 @@ +using DotNut.Abstractions.Interfaces; +using DotNut.Abstractions.Websockets; +using DotNut.ApiModels; + +namespace DotNut.Abstractions.Handlers; + +public class MeltHandlerBolt11 : IMeltHandler> +{ + private IWalletBuilder _wallet; + private PostMeltQuoteBolt11Response _quote; + private OutputData _blankOutputs; + + public MeltHandlerBolt11(IWalletBuilder wallet, PostMeltQuoteBolt11Response quote) + { + _wallet = wallet; + _quote = quote; + } + public MeltHandlerBolt11(IWalletBuilder wallet, PostMeltQuoteBolt11Response quote, OutputData blankOutputs) + { + _wallet = wallet; + _quote = quote; + this._blankOutputs = blankOutputs; + } + public async Task GetQuote(CancellationToken cts = default) => this._quote; + public async Task> Melt(List inputs, CancellationToken cts = default) + { + var client = await _wallet.GetMintApi(); + var req = new PostMeltRequest + { + Quote = _quote.Quote, + Inputs = inputs.ToArray(), + Outputs = _blankOutputs.BlindedMessages + }; + + var res = await client.Melt("bolt11", req, cts); + if (res.Change == null) + { + return []; + } + + var keyset = await _wallet.GetKeys(res.Change.First().Id, false, cts); + return CashuUtils.ConstructProofsFromPromises(res.Change.ToList(), _blankOutputs, keyset.Keys); + } + + public Task Subscribe(CancellationToken cts = default) + { + throw new NotImplementedException(); + } +} \ No newline at end of file diff --git a/DotNut/Abstractions/Handlers/MeltHandlerBolt12.cs b/DotNut/Abstractions/Handlers/MeltHandlerBolt12.cs new file mode 100644 index 0000000..743bfff --- /dev/null +++ b/DotNut/Abstractions/Handlers/MeltHandlerBolt12.cs @@ -0,0 +1,23 @@ +using DotNut.Abstractions.Interfaces; +using DotNut.Abstractions.Websockets; +using DotNut.ApiModels.Melt.bolt12; + +namespace DotNut.Abstractions.Handlers; + +public class MeltHandlerBolt12: IMeltHandler> +{ + public Task GetQuote(CancellationToken cts = default) + { + throw new NotImplementedException(); + } + + public Task> Melt(List inputs, CancellationToken cts = default) + { + throw new NotImplementedException(); + } + + public Task Subscribe(CancellationToken cts = default) + { + throw new NotImplementedException(); + } +} \ No newline at end of file diff --git a/DotNut/Abstractions/Handlers/MintHandlerBolt11.cs b/DotNut/Abstractions/Handlers/MintHandlerBolt11.cs new file mode 100644 index 0000000..8334ae1 --- /dev/null +++ b/DotNut/Abstractions/Handlers/MintHandlerBolt11.cs @@ -0,0 +1,82 @@ +using System.Runtime.CompilerServices; +using DotNut.Abstractions.Websockets; +using DotNut.Api; +using DotNut.ApiModels; +using DotNut.ApiModels.Mint.bolt12; + +namespace DotNut.Abstractions.Quotes; + +public class MintHandlerBolt11: IMintHandler> +{ + private readonly PostMintQuoteBolt11Response _quote; + private readonly IWalletBuilder _wallet; + private readonly GetKeysResponse.KeysetItemResponse _keyset; + + private ulong? _amount; + + private List? _amounts; + private OutputData? _outputs; + + private string? SubscriptionId; + private WebsocketService? _websocketService; + + public MintHandlerBolt11( + IWalletBuilder wallet, + PostMintQuoteBolt11Response postMintQuoteBolt11Response, + GetKeysResponse.KeysetItemResponse? verifiedKeyset + ) + { + this._wallet = wallet; + this._quote = postMintQuoteBolt11Response; + this._keyset = verifiedKeyset; + } + + public MintHandlerBolt11( + IWalletBuilder wallet, + PostMintQuoteBolt11Response postMintQuoteBolt11Response, + GetKeysResponse.KeysetItemResponse? verifiedKeyset, + ulong amount + ) + { + this._quote = postMintQuoteBolt11Response; + this._keyset = verifiedKeyset; + this._amount = amount; + } + + public MintHandlerBolt11(PostMintQuoteBolt11Response postMintQuoteBolt11Response, GetKeysResponse.KeysetItemResponse? verifiedKeyset, List? amounts, ulong? amount) + { + this._quote = postMintQuoteBolt11Response; + this._keyset = verifiedKeyset; + this._amounts = amounts; + this._amount = amount; + } + + + public async Task GetQuote(CancellationToken cts = default) => _quote; + + public async Task> Mint(CancellationToken cts = default) + { + var client = await this._wallet.GetMintApi(); + + _amount ??= _quote.Amount ?? throw new ArgumentNullException(nameof(_quote.Amount), "Can't determine amount of quote!"); + + _amounts??= CashuUtils.SplitToProofsAmounts(_amount.Value, _keyset.Keys); + + this._outputs ??= await _wallet.CreateOutputs(_amounts!, _keyset.Id, cts); + + var req = new PostMintRequest + { + Outputs = this._outputs.BlindedMessages, + Quote = _quote.Quote + }; + + var promises= await client.Mint("bolt11", req, cts); + return CashuUtils.ConstructProofsFromPromises(promises.Signatures.ToList(), _outputs, _keyset.Keys); + } + + public Task Subscribe(CancellationToken cts = default) + { + throw new NotImplementedException(); + } + +} \ No newline at end of file diff --git a/DotNut/Abstractions/Handlers/MintHandlerBolt12.cs b/DotNut/Abstractions/Handlers/MintHandlerBolt12.cs new file mode 100644 index 0000000..9fc70e5 --- /dev/null +++ b/DotNut/Abstractions/Handlers/MintHandlerBolt12.cs @@ -0,0 +1,60 @@ +using DotNut.Abstractions.Websockets; +using DotNut.Api; +using DotNut.ApiModels; +using DotNut.ApiModels.Melt.bolt12; +using DotNut.ApiModels.Mint.bolt12; + +namespace DotNut.Abstractions; + +public class MintHandlerBolt12: IMintHandler> +{ + + private readonly IWalletBuilder _wallet; + private PostMintQuoteBolt12Response _quote; + private GetKeysResponse.KeysetItemResponse _keyset; + + private OutputData? _outputs; + private ulong? _amount; + private List? _amounts; + + public MintHandlerBolt12(Wallet wallet, PostMintQuoteBolt12Response quote, GetKeysResponse.KeysetItemResponse keyset) + { + this._wallet = wallet; + this._quote = quote; + this._keyset = keyset; + } + + public async Task GetQuote(CancellationToken cts = default) => this._quote; + public async Task> Mint(CancellationToken cts = default) + { + var client = await this._wallet.GetMintApi(); + + _amount ??= _quote.Amount ?? throw new ArgumentNullException(nameof(_quote.Amount), "Can't determine amount of quote!"); + + _amounts??= CashuUtils.SplitToProofsAmounts(_amount.Value, _keyset.Keys); + + this._outputs ??= await _wallet.CreateOutputs(_amounts!, _keyset.Id, cts); + + var req = new PostMintRequest + { + Outputs = this._outputs.BlindedMessages, + Quote = _quote.Quote + }; + + var promises= await client.Mint("bolt11", req, cts); + return CashuUtils.ConstructProofsFromPromises(promises.Signatures.ToList(), _outputs, _keyset.Keys); + } + + private async Task _processMint(PostMintRequest req, CancellationToken cts = default) + { + var client = await this._wallet.GetMintApi(); + + return await client.Mint("bolt12", req, cts); + } + + public Task Subscribe(CancellationToken cts = default) + { + throw new NotImplementedException(); + } + +} \ No newline at end of file diff --git a/DotNut/Abstractions/InMemoryCounter.cs b/DotNut/Abstractions/InMemoryCounter.cs new file mode 100644 index 0000000..74dbdef --- /dev/null +++ b/DotNut/Abstractions/InMemoryCounter.cs @@ -0,0 +1,37 @@ +using DotNut.Abstractions.Interfaces; + +namespace DotNut.Abstractions; + +public class InMemoryCounter : ICounter +{ + private IDictionary _counter; + + public InMemoryCounter(IDictionary counter) + { + this._counter = counter; + } + + public InMemoryCounter() + { + this._counter = new Dictionary(); + } + + public async Task GetCounterForId(KeysetId keysetId, CancellationToken cts = default) + { + if (_counter.TryGetValue(keysetId, out var counter)) + return counter; + + return _counter[keysetId] = 0; + } + + public async Task IncrementCounter(KeysetId keysetId, int bumpBy = 1, CancellationToken cts = default) + { + var current = await GetCounterForId(keysetId, cts); + var next = current + bumpBy; + _counter[keysetId] = next; + return next; + } + + public async Task SetCounter(KeysetId keysetId, int counter, CancellationToken cts = default) => _counter[keysetId] = counter; + +} \ No newline at end of file diff --git a/DotNut/Abstractions/InMemoryProofManager.cs b/DotNut/Abstractions/InMemoryProofManager.cs new file mode 100644 index 0000000..9f5c0f6 --- /dev/null +++ b/DotNut/Abstractions/InMemoryProofManager.cs @@ -0,0 +1,30 @@ +namespace DotNut.Abstractions.Interfaces; + +public class InMemoryProofManager: IProofManager +{ + private Dictionary> _proofsDictionary = new(); + + public async Task AddProofAsync(Proof proof, CancellationToken cts = default) + { + if (_proofsDictionary.TryGetValue(proof.Id, out var proofs)) + { + proofs.Add(proof); + return; + } + _proofsDictionary.Add(proof.Id, new List { proof }); + } + + public async Task> GetProofsForKeysetId(KeysetId ids, CancellationToken cts = default) + { + return _proofsDictionary.TryGetValue(ids, out var proofs) ? proofs : new List(); + } + + public Task> GetProofsForMint(List ids, CancellationToken cts = default) + { + throw new NotImplementedException(); + } + public Task MarkProofAsSpent(Proof proof, CancellationToken cts = default) + { + throw new NotImplementedException(); + } +} \ No newline at end of file diff --git a/DotNut/Abstractions/Interfaces/IMeltHandler.cs b/DotNut/Abstractions/Interfaces/IMeltHandler.cs index 32037ef..33a6693 100644 --- a/DotNut/Abstractions/Interfaces/IMeltHandler.cs +++ b/DotNut/Abstractions/Interfaces/IMeltHandler.cs @@ -4,11 +4,12 @@ namespace DotNut.Abstractions.Interfaces; -public interface IMeltHandler{} +public interface IMeltHandler; -public interface IMeltHandler: IMeltHandler +public interface IMeltHandler: IMeltHandler { - Task Melt(TRequest request); + Task GetQuote(CancellationToken cts = default); + Task Melt(List inputs, CancellationToken cts = default); - Task Subscribe(TRequest request); + Task Subscribe(CancellationToken cts = default); } \ No newline at end of file diff --git a/DotNut/Abstractions/Interfaces/IMeltQuoteBuilder.cs b/DotNut/Abstractions/Interfaces/IMeltQuoteBuilder.cs new file mode 100644 index 0000000..1f7c470 --- /dev/null +++ b/DotNut/Abstractions/Interfaces/IMeltQuoteBuilder.cs @@ -0,0 +1,16 @@ +using DotNut.ApiModels; +using DotNut.ApiModels.Melt.bolt12; + +namespace DotNut.Abstractions.Interfaces; + +/// +/// Melt operation builder (pay invoices) +/// +public interface IMeltQuoteBuilder +{ + IMeltQuoteBuilder WithUnit(string unit); + IMeltQuoteBuilder WithInvoice(string bolt11Invoice); + Task>> ProcessAsyncBolt11(CancellationToken cancellationToken = default); + Task>> ProcessAsyncBolt12(CancellationToken cancellationToken = default); + +} \ No newline at end of file diff --git a/DotNut/Abstractions/Interfaces/IMintHandler.cs b/DotNut/Abstractions/Interfaces/IMintHandler.cs index 1cc30c2..d94b097 100644 --- a/DotNut/Abstractions/Interfaces/IMintHandler.cs +++ b/DotNut/Abstractions/Interfaces/IMintHandler.cs @@ -2,9 +2,10 @@ namespace DotNut.Abstractions; -public interface IMintHandler {} -public interface IMintHandler: IMintHandler +public interface IMintHandler; +public interface IMintHandler: IMintHandler { + Task GetQuote(CancellationToken cts = default); Task Mint(CancellationToken cts = default); - Task Subscribe(); + Task Subscribe(CancellationToken cts = default); } \ No newline at end of file diff --git a/DotNut/Abstractions/Interfaces/IMintQuoteBuilder.cs b/DotNut/Abstractions/Interfaces/IMintQuoteBuilder.cs new file mode 100644 index 0000000..c0195a8 --- /dev/null +++ b/DotNut/Abstractions/Interfaces/IMintQuoteBuilder.cs @@ -0,0 +1,18 @@ +using DotNut.ApiModels; +using DotNut.ApiModels.Mint.bolt12; + +namespace DotNut.Abstractions.Interfaces; + +/// +/// Mint operation builder (receive from invoice) +/// +public interface IMintQuoteBuilder +{ + IMintQuoteBuilder WithUnit(string unit); + IMintQuoteBuilder WithAmount(ulong amount); + IMintQuoteBuilder WithOutputs(OutputData outputs); + // Task ProcessAsync(CancellationToken cancellationToken = default); + Task>> ProcessAsyncBolt11(CancellationToken cancellationToken = default); + Task>> ProcessAsyncBolt12(CancellationToken cancellationToken = default); + +} \ No newline at end of file diff --git a/DotNut/Abstractions/Interfaces/IProofManager.cs b/DotNut/Abstractions/Interfaces/IProofManager.cs new file mode 100644 index 0000000..b4efae1 --- /dev/null +++ b/DotNut/Abstractions/Interfaces/IProofManager.cs @@ -0,0 +1,9 @@ +namespace DotNut.Abstractions.Interfaces; + +public interface IProofManager +{ + Task AddProofAsync(Proof proof, CancellationToken cts = default); + Task> GetProofsForKeysetId(KeysetId ids, CancellationToken cts = default); + Task> GetProofsForMint(List ids, CancellationToken cts = default); // should still query proofs based on keysetid + Task MarkProofAsSpent(Proof proof, CancellationToken cts = default); +} \ No newline at end of file diff --git a/DotNut/Abstractions/Interfaces/IProofSelector.cs b/DotNut/Abstractions/Interfaces/IProofSelector.cs index c89f707..398b95c 100644 --- a/DotNut/Abstractions/Interfaces/IProofSelector.cs +++ b/DotNut/Abstractions/Interfaces/IProofSelector.cs @@ -2,5 +2,5 @@ namespace DotNut.Abstractions; public interface IProofSelector { - public SendResponse SelectProofsToSend(List proofs, ulong amountToSend, bool includeFees = false); + public Task SelectProofsToSend(List proofs, ulong amountToSend, bool includeFees = false, CancellationToken cts = default); } \ No newline at end of file diff --git a/DotNut/Abstractions/Interfaces/IRestoreBuilder.cs b/DotNut/Abstractions/Interfaces/IRestoreBuilder.cs new file mode 100644 index 0000000..a896d93 --- /dev/null +++ b/DotNut/Abstractions/Interfaces/IRestoreBuilder.cs @@ -0,0 +1,11 @@ +namespace DotNut.Abstractions.Interfaces; + +/// +/// Restore operation builder +/// +public interface IRestoreBuilder +{ + RestoreBuilder ForKeysetIds(IEnumerable keysetIds); + IRestoreBuilder WithSwap(bool shouldSwap = true); + Task> ProcessAsync(CancellationToken cancellationToken = default); +} diff --git a/DotNut/Abstractions/Interfaces/IStatefulWalletBuilder.cs b/DotNut/Abstractions/Interfaces/IStatefulWalletBuilder.cs new file mode 100644 index 0000000..135321a --- /dev/null +++ b/DotNut/Abstractions/Interfaces/IStatefulWalletBuilder.cs @@ -0,0 +1,13 @@ +namespace DotNut.Abstractions.Interfaces; + +/// +/// Abstraction on WalletBuilder, with Proof Manager. Stateful wallet library, abstracting all operations like melting/minting. +/// +/// +public interface IStatefulWalletBuilder +{ + Task ReceiveLightning(); + Task SendLightning(); + Task ReceiveProofs(); + Task SendProofs(); +} \ No newline at end of file diff --git a/DotNut/Abstractions/Interfaces/ISwapBuilder.cs b/DotNut/Abstractions/Interfaces/ISwapBuilder.cs new file mode 100644 index 0000000..4e98f88 --- /dev/null +++ b/DotNut/Abstractions/Interfaces/ISwapBuilder.cs @@ -0,0 +1,13 @@ +namespace DotNut.Abstractions.Interfaces; + +/// +/// Swap operation builder +/// +public interface ISwapBuilder +{ + ISwapBuilder WithUnit(string unit); + ISwapBuilder ForKeyset(KeysetId targetKeysetId); + ISwapBuilder FromInputs(IEnumerable inputs); + ISwapBuilder ForOutputs(OutputData outputs); + Task> ProcessAsync(CancellationToken cancellationToken = default); +} diff --git a/DotNut/Abstractions/Interfaces/IWallet.cs b/DotNut/Abstractions/Interfaces/IWallet.cs deleted file mode 100644 index 899df13..0000000 --- a/DotNut/Abstractions/Interfaces/IWallet.cs +++ /dev/null @@ -1,87 +0,0 @@ -using DotNut.Abstractions.Interfaces; -using DotNut.Api; -using DotNut.ApiModels; -using DotNut.NBitcoin.BIP39; - -namespace DotNut.Abstractions; - -/// -/// Fluent builder interface for Cashu Wallet operations -/// -public interface ICashuWalletBuilder -{ - ICashuWalletBuilder WithInfo(MintInfo info); - ICashuWalletBuilder WithInfo(GetInfoResponse info); - ICashuWalletBuilder WithKeysets(IEnumerable keysets); - ICashuWalletBuilder WithKeysets(GetKeysetsResponse keysets); - ICashuWalletBuilder WithKeys(IEnumerable keys); - ICashuWalletBuilder WithSelector(IProofSelector selector); - ICashuWalletBuilder WithMint(ICashuApi mintApi); - ICashuWalletBuilder WithMint(string mintUrl); - ICashuWalletBuilder WithMnemonic(Mnemonic mnemonic); - ICashuWalletBuilder WithMnemonic(string mnemonic); - ICashuWalletBuilder WithCounter(ICounter counter); - Task GetInfo(bool forceReferesh = false, CancellationToken cts = default); - Task CreateOutputs(List amounts, KeysetId id, CancellationToken cts = default); - - Task?> GetActiveKeysetIdsWithUnits(); - - ICashuApi? GetMintApi(); - - - // Swap operations - ICashuWalletSwapBuilder Swap(); - - // Melt operations (pay invoices) - ICashuWalletMeltQuoteBuilder CreateMeltQuote(); - - // Mint operations (receive from invoice) - ICashuWalletMintBuilder CreateMintQuote(); - - // Restore operations - ICashuWalletRestoreBuilder Restore(); -} - -/// -/// Swap operation builder -/// -public interface ICashuWalletSwapBuilder -{ - ICashuWalletSwapBuilder WithUnit(string unit); - ICashuWalletSwapBuilder ForKeyset(KeysetId targetKeysetId); - ICashuWalletSwapBuilder WithOutputs(IEnumerable outputs); - Task> ProcessAsync(CancellationToken cancellationToken = default); -} - -/// -/// Melt operation builder (pay invoices) -/// -public interface ICashuWalletMeltQuoteBuilder -{ - ICashuWalletMeltQuoteBuilder WithUnit(string unit); - ICashuWalletMeltQuoteBuilder WithInvoice(string bolt11Invoice); - ICashuWalletMeltQuoteBuilder WithMethod(string method = "bolt11"); - Task ProcessAsync(CancellationToken cancellationToken = default); -} - -/// -/// Mint operation builder (receive from invoice) -/// -public interface ICashuWalletMintBuilder -{ - ICashuWalletMintBuilder WithUnit(string unit); - ICashuWalletMintBuilder WithAmount(ulong amount); - ICashuWalletMintBuilder WithOutputs(IEnumerable outputs); - ICashuWalletMintBuilder WithMethod(string method = "bolt11"); - // Task ProcessAsync(CancellationToken cancellationToken = default); - Task ProcessAsync(CancellationToken cancellationToken = default); -} - -/// -/// Restore operation builder -/// -public interface ICashuWalletRestoreBuilder -{ - ICashuWalletRestoreBuilder ForKeysetIds(IEnumerable keysetIds); - Task ProcessAsync(CancellationToken cancellationToken = default); -} diff --git a/DotNut/Abstractions/Interfaces/IWalletBuilder.cs b/DotNut/Abstractions/Interfaces/IWalletBuilder.cs new file mode 100644 index 0000000..1330d8c --- /dev/null +++ b/DotNut/Abstractions/Interfaces/IWalletBuilder.cs @@ -0,0 +1,64 @@ +using DotNut.Abstractions.Interfaces; +using DotNut.Api; +using DotNut.ApiModels; +using DotNut.ApiModels.Mint.bolt12; +using DotNut.NBitcoin.BIP39; + +namespace DotNut.Abstractions; + +/// +/// Fluent builder interface for Cashu Wallet operations +/// +public interface IWalletBuilder +{ + IWalletBuilder WithMint(ICashuApi mintApi); + IWalletBuilder WithMint(string mintUrl); + IWalletBuilder WithInfo(MintInfo info); + IWalletBuilder WithInfo(GetInfoResponse info); + IWalletBuilder WithKeysets(IEnumerable keysets); + IWalletBuilder WithKeysets(GetKeysetsResponse keysets); + IWalletBuilder WithKeys(IEnumerable keys); + IWalletBuilder WithKeys(GetKeysResponse keys); + IWalletBuilder WithKeysetSync(bool syncKeyset = true); + IWalletBuilder WithSelector(IProofSelector selector); + IWalletBuilder WithMnemonic(Mnemonic mnemonic); + IWalletBuilder WithMnemonic(string mnemonic); + IWalletBuilder WithCounter(ICounter counter); + IWalletBuilder WithCounter(IDictionary counter); + IWalletBuilder ShouldBumpCounter(bool shouldBumpCounter = true); + + + Task GetInfo(bool forceReferesh = false, CancellationToken cts = default); + Task CreateOutputs(List amounts, KeysetId id, CancellationToken cts = default); + + Task?> GetActiveKeysetIdsWithUnits(CancellationToken cts = default); + + Task GetMintApi(CancellationToken cts = default); + + Task GetActiveKeysetId(string unit, CancellationToken cts = default); + Task> GetKeys(bool forceRefresh = false, CancellationToken cts = default); + + Task GetKeys(KeysetId id, bool forceRefresh = false, + CancellationToken cts = default); + + Task> GetKeysets(bool forceRefresh = false, + CancellationToken cts = default); + + Task CreateOutputs(List amounts, string unit, CancellationToken cts = default); + + Task SelectProofsToSend(List proofs, ulong amount, bool includeFees, + CancellationToken cts = default); + + // Swap operations + ISwapBuilder Swap(); + + // Melt operations (pay invoices) + IMeltQuoteBuilder CreateMeltQuote(); + + // Mint operations (receive from invoice) + IMintQuoteBuilder CreateMintQuote(); + + IRestoreBuilder Restore(); + +} + diff --git a/DotNut/Abstractions/MeltQuoteBuilder.cs b/DotNut/Abstractions/MeltQuoteBuilder.cs new file mode 100644 index 0000000..0af5aed --- /dev/null +++ b/DotNut/Abstractions/MeltQuoteBuilder.cs @@ -0,0 +1,104 @@ +using DotNut.Abstractions.Handlers; +using DotNut.Abstractions.Interfaces; +using DotNut.ApiModels; +using DotNut.ApiModels.Melt.bolt12; + +namespace DotNut.Abstractions; + +class MeltQuoteBuilder : IMeltQuoteBuilder +{ + private readonly Wallet _wallet; + private List? _proofs; + private string? _invoice; + private OutputData? _blankOutputs; + private ulong? _amount; + private string _unit = "sat"; + + public MeltQuoteBuilder(Wallet wallet) + { + _wallet = wallet; + } + + /// + /// Mandatory. + /// Invoice must be provided in order to create (Lightning) MeltQuote. + /// + /// + /// + public IMeltQuoteBuilder WithInvoice(string invoice) + { + this._invoice = invoice; + return this; + } + + /// + /// Optional. + /// If not set, defaults to satoshi. If token has other unit, must be set. + /// + /// + /// + public IMeltQuoteBuilder WithUnit(string unit) + { + this._unit = unit; + return this; + } + + /// + /// Optional. Allows user to specify blank outputs. If not set, these will be generated automatically. + /// + /// + /// + public IMeltQuoteBuilder WithBlankOutputs(OutputData blankOutputs) + { + this._blankOutputs = blankOutputs; + return this; + } + + /// + /// Mandatory. + /// User needs to specify the amount to be received. It MUST correspond to invoice amount. + /// + /// + /// + public IMeltQuoteBuilder WithAmount(ulong amount) + { + this._amount = amount; + return this; + } + + public async Task>> ProcessAsyncBolt11(CancellationToken cts = default) + { + var mintApi = await _wallet.GetMintApi(); + await _wallet._maybeSyncKeys(cts); + // ArgumentNullException.ThrowIfNull(this._amount); + ArgumentNullException.ThrowIfNull(this._invoice); + + + var req = new PostMeltQuoteBolt11Request + { + Request = this._invoice, + Unit = this._unit, + }; + + var quote = + await mintApi.CreateMeltQuote("bolt11", req, cts); + + + if (_blankOutputs == null) + { + var outputsAmount = CashuUtils.CalculateNumberOfBlankOutputs((ulong)quote.FeeReserve); + var amounts = Enumerable.Repeat(1UL, outputsAmount).ToList(); + this._blankOutputs = await this._wallet.CreateOutputs(amounts, this._unit, cts); + } + + return new MeltHandlerBolt11(_wallet, quote, _blankOutputs); + } + + + public async Task>> ProcessAsyncBolt12( + CancellationToken cts = default) + { + throw new NotImplementedException(); + } +} + diff --git a/DotNut/Abstractions/MintHandlerBolt11.cs b/DotNut/Abstractions/MintHandlerBolt11.cs deleted file mode 100644 index 1b6d04e..0000000 --- a/DotNut/Abstractions/MintHandlerBolt11.cs +++ /dev/null @@ -1,85 +0,0 @@ -using System.Runtime.CompilerServices; -using DotNut.Abstractions.Websockets; -using DotNut.Api; -using DotNut.ApiModels; -using DotNut.ApiModels.Mint.bolt12; - -namespace DotNut.Abstractions.Quotes; - -// todo -// at this point we should already have everything that we need for minting the tokens. also, we assume the invoice is paid or it will be paid soon - -public class MintHandlerBolt11: IMintHandler -{ - private readonly ICashuWalletBuilder _wallet; - private readonly PostMintQuoteBolt11Response _quote; - private readonly GetKeysResponse.KeysetItemResponse _keyset; - private KeysetId _keysetId => _keyset.Id; // at this point keysetid MUST be validated so it's safe to asssume its correct - - private List? _amounts; - private OutputData? _outputs; - - private string SubscriptionId; - private WebsocketService _websocketService; - - public MintHandlerBolt11( - ICashuWalletBuilder wallet, - PostMintQuoteBolt11Response postMintQuoteBolt11Response, - GetKeysResponse.KeysetItemResponse? verifiedKeyset - ) - { - this._quote = postMintQuoteBolt11Response; - this._keyset = verifiedKeyset; - } - - public MintHandlerBolt11(PostMintQuoteBolt11Response postMintQuoteBolt11Response, GetKeysResponse.KeysetItemResponse? verifiedKeyset, List? amounts) - { - this._quote = postMintQuoteBolt11Response; - this._keyset = verifiedKeyset; - this._amounts = amounts; - } - - public async Task Mint(CancellationToken cts = default) - { - - if (_quote.Amount == null) - { - //todo amountless flow - return new PostMintResponse(); - } - - if (_amounts == null) - { - var amounts = CashuUtils.SplitToProofsAmounts(_quote.Amount.Value, _keyset.Keys); - } - - if (this._outputs == null) - { - this._outputs = await _wallet.CreateOutputs(_amounts!, _keysetId, cts); - } - - var req = new PostMintRequest - { - Outputs = this._outputs.BlindedMessages, - Quote = _quote.Quote - }; - return await this._processMint(req, cts); - } - - private async Task _processMint(PostMintRequest req, CancellationToken cts = default) - { - var client = this._wallet.GetMintApi(); - if (client is null) - { - throw new ArgumentNullException(nameof(CashuHttpClient), "Mint api can't be null!"); - } - - return await client.Mint("bolt11", req, cts); - } - - public Task Subscribe() - { - throw new NotImplementedException(); - // await this._websocketService.SubscribeToSingleMeltQuoteAsync(); - } -} \ No newline at end of file diff --git a/DotNut/Abstractions/MintHandlerBolt12.cs b/DotNut/Abstractions/MintHandlerBolt12.cs deleted file mode 100644 index f43443f..0000000 --- a/DotNut/Abstractions/MintHandlerBolt12.cs +++ /dev/null @@ -1,30 +0,0 @@ -using DotNut.Abstractions.Websockets; -using DotNut.ApiModels; -using DotNut.ApiModels.Mint.bolt12; - -namespace DotNut.Abstractions; - -public class MintHandlerBolt12: IMintHandler -{ - - private readonly ICashuWalletBuilder _wallet; - private PostMintQuoteBolt12Response _quote; - private GetKeysResponse.KeysetItemResponse _keyset; - - public MintHandlerBolt12(Wallet wallet, PostMintQuoteBolt12Response quote, GetKeysResponse.KeysetItemResponse keyset) - { - this._wallet = wallet; - this._quote = quote; - this._keyset = keyset; - } - - public Task Mint(CancellationToken cts = default) - { - throw new NotImplementedException(); - } - - public Task Subscribe() - { - throw new NotImplementedException(); - } -} \ No newline at end of file diff --git a/DotNut/Abstractions/MintMeltDisabledException.cs b/DotNut/Abstractions/MintMeltDisabledException.cs new file mode 100644 index 0000000..4ccbbec --- /dev/null +++ b/DotNut/Abstractions/MintMeltDisabledException.cs @@ -0,0 +1,3 @@ +namespace DotNut.Abstractions; + +public class MintMeltDisabledException(string message) : Exception(message); \ No newline at end of file diff --git a/DotNut/Abstractions/MintQuoteBuilder.cs b/DotNut/Abstractions/MintQuoteBuilder.cs new file mode 100644 index 0000000..c046fcb --- /dev/null +++ b/DotNut/Abstractions/MintQuoteBuilder.cs @@ -0,0 +1,191 @@ +using DotNut.Abstractions.Interfaces; +using DotNut.Abstractions.Quotes; +using DotNut.Api; +using DotNut.ApiModels; +using DotNut.ApiModels.Mint.bolt12; + +namespace DotNut.Abstractions; + +class MintQuoteBuilder : IMintQuoteBuilder +{ + private readonly Wallet _wallet; + private ulong? _amount; + private string _unit = "sat"; + private string? _description; + private OutputData? _outputs; + private string? _method = "bolt11"; + + //for bolt12 + private string? _pubkey; + + private KeysetId? _keysetId; + private GetKeysResponse.KeysetItemResponse? _keyset; + + public MintQuoteBuilder(Wallet wallet) + { + this._wallet = wallet; + } + + /// + /// Mandatory. + /// User has to provide Mint method + /// + /// Either MintMeltMethod.Bolt11 or MintMeltMethod.Bolt12 + /// + public IMintQuoteBuilder WithMethod(string method) + { + this._method = method; + return this; + } + + /// + /// Mandatory. + /// + /// Amount of token in currently choosen unit to be melted + public IMintQuoteBuilder WithAmount(ulong amount) + { + this._amount = amount; + return this; + } + + /// + /// Optional. + /// Sets unit of tokens being minted. Sat by default. + /// + /// Unit of minted proofs + public IMintQuoteBuilder WithUnit(string unit) + { + this._unit = unit; + return this; + } + + /// + /// Optional. Necessary for bolt12 + /// Sets pubkey for bolt12 offer + /// + /// + /// + public IMintQuoteBuilder WithPubkey(string pubkey) + { + this._pubkey = pubkey; + return this; + } + + /// + /// Optional. + /// Allows user to set keysetId manually. Otherwise, builder will choose active one manually, with the lowest fees. + /// + /// + public IMintQuoteBuilder WithKeyset(KeysetId keysetId) + { + this._keysetId = keysetId; + return this; + } + + + /// + /// Optional. + /// User may provide outputs for mint to sign. Blinding factors and secrets won't be revealed to mint. + /// If not provided, wallet will try to derive them from seed and counter, or create random ones if mnemonic is not avaible. + /// + /// OutputData instance. Enumerables of BlindingFactors, BlindedMessages and Secrets, in right order. + public IMintQuoteBuilder WithOutputs(OutputData outputs) + { + this._outputs = outputs; + return this; + } + + /// + /// Optional. + /// User may provide description for melt quote invoice. + /// + /// + /// + public IMintQuoteBuilder WithDescription(string description) + { + this._description = description; + return this; + } + + public IMintQuoteBuilder WithP2PK() + { + throw new NotImplementedException(); + } + + public IMintQuoteBuilder WithHTLC() + { + throw new NotImplementedException(); + } + + public async Task>> ProcessAsyncBolt11( + CancellationToken cts = default) + { + //todo implement info + + await this._wallet._maybeSyncKeys(cts); + if (_amount == null) + { + throw new ArgumentNullException(nameof(_amount), "can't create melt quote without amount!"); + } + + var api = this._wallet.GetMintApi(); + if (api is null) + { + throw new ArgumentNullException(nameof(ICashuApi), "Can't request mint quote without mint API"); + } + + if (this._keysetId == null) + { + this._keysetId = await this._wallet.GetActiveKeysetId(this._unit, cts) ?? + throw new ArgumentException($"Can't get active keyset ID for unit: {_unit}"); + } + + if (this._keyset == null) + { + this._keyset = await this._wallet.GetKeys(this._keysetId, false, cts) ?? + throw new ArgumentException($"Cant get keys for keysetId: {_keysetId}"); + } + + var reqBolt11 = new PostMintQuoteBolt11Request() + { + Amount = this._amount.Value, + Unit = this._unit, + Description = this._description, + }; + var quoteBolt11 = + await (await this._wallet.GetMintApi()) + .CreateMintQuote("bolt11", reqBolt11, + cts); + return new MintHandlerBolt11(this._wallet, quoteBolt11, this._keyset); + } + + public async Task>> ProcessAsyncBolt12( + CancellationToken cts = default) + { + await this._wallet._maybeSyncKeys(cts); + if (this._pubkey == null) + { + throw new ArgumentNullException(nameof(_pubkey), "Can't request bolt12 mint quote without pubkey!"); + } + + if (this._keyset == null) + { + this._keyset = await this._wallet.GetKeys(this._keysetId, false, cts) ?? + throw new ArgumentException($"Cant fetch keys for keysetId: {_keysetId}"); + } + + var req = new PostMintQuoteBolt12Request() + { + Amount = this._amount.Value, + Unit = this._unit, + Pubkey = this._pubkey, + Description = this._description, + }; + var mintQuote = + await (await _wallet.GetMintApi()) + .CreateMintQuote("bolt12", req, + cts); + return new MintHandlerBolt12(this._wallet, mintQuote, this._keyset); + + } +} \ No newline at end of file diff --git a/DotNut/Abstractions/ProofSelector.cs b/DotNut/Abstractions/ProofSelector.cs index 356ad91..9b1b4d9 100644 --- a/DotNut/Abstractions/ProofSelector.cs +++ b/DotNut/Abstractions/ProofSelector.cs @@ -55,7 +55,7 @@ private ulong GetProofFeePPK(Proof proof) return _keysetFees.TryGetValue(proof.Id, out var fee) ? fee : 0; } - public SendResponse SelectProofsToSend(List proofs, ulong amountToSend, bool includeFees = false) + public async Task SelectProofsToSend(List proofs, ulong amountToSend, bool includeFees = false, CancellationToken cts = default) { // Init vars const int MAX_TRIALS = 60; // 40-80 is optimal (per RGLI paper) diff --git a/DotNut/Abstractions/RestoreBuilder.cs b/DotNut/Abstractions/RestoreBuilder.cs new file mode 100644 index 0000000..a45bdae --- /dev/null +++ b/DotNut/Abstractions/RestoreBuilder.cs @@ -0,0 +1,128 @@ +using DotNut.Abstractions.Interfaces; +using DotNut.ApiModels; +using DotNut.NBitcoin.BIP39; +using DotNut.NUT13; + +namespace DotNut.Abstractions; + +public class RestoreBuilder : IRestoreBuilder +{ + private readonly Wallet _wallet; + private List? _specifiedKeysets; + + private bool _shouldSwap = true; + + public RestoreBuilder(Wallet wallet) + { + this._wallet = wallet; + } + + public RestoreBuilder ForKeysetIds(IEnumerable keysetIds) + { + this._specifiedKeysets = keysetIds.ToList(); + return this; + } + + public IRestoreBuilder WithSwap(bool shouldSwap = true) + { + this._shouldSwap = shouldSwap; + return this; + } + + public async Task> ProcessAsync(CancellationToken cts = default) + { + var api = await _wallet.GetMintApi(); + await _wallet._maybeSyncKeys(cts); + + var mnemonic = _wallet.GetMnemonic()?? + throw new ArgumentNullException("Can't restore wallet without Mnemonic"); + + _specifiedKeysets ??= + (await _wallet.GetKeysets(cts: cts)).Select(k => k.Id).ToList(); + + if (_specifiedKeysets == null || _specifiedKeysets.Count == 0) + { + throw new ArgumentNullException(nameof(_specifiedKeysets)); + } + + var counter = _wallet.GetCounter(); + if (counter == null) + { + _wallet.WithCounter(new InMemoryCounter()); + counter = _wallet.GetCounter(); + } + + List recoveredProofs = new List(); + foreach (var keysetId in _specifiedKeysets) + { + int batchNumber = 0; + int emptyBatchesRemaining = 3; + + var keyset = await _wallet.GetKeys(keysetId, false, cts); + + while (emptyBatchesRemaining > 0) + { + var outputs = await _createBatch(mnemonic, keysetId, batchNumber, cts); + await counter!.IncrementCounter(keysetId, batchNumber * 100); + var req = new PostRestoreRequest + { + Outputs = outputs.BlindedMessages + }; + var res = await api.Restore(req, cts); + + if (!res.Signatures.Any()) + { + emptyBatchesRemaining--; + } + + var proofs = CashuUtils.ConstructProofsFromPromises(res.Signatures.ToList(), outputs, keyset.Keys); + recoveredProofs.AddRange(proofs); + batchNumber++; + } + + } + + if (!this._shouldSwap || !recoveredProofs.Any()) + { + return recoveredProofs; + } + + var freshProofs = new List(); + var activeUnits = await this._wallet.GetActiveKeysetIdsWithUnits(); + + if (activeUnits == null || !activeUnits.Any()) + { + throw new InvalidOperationException("Could not restore wallet without active keysets"); + } + + foreach (var unitKeyset in activeUnits) + { + var correspondingKeys = await _wallet.GetKeys(unitKeyset.Value, false, cts); + var totalAmount = recoveredProofs.Select(p=>p.Amount).Aggregate((a,c) => a + c); + var amounts = CashuUtils.SplitToProofsAmounts(totalAmount, correspondingKeys.Keys); + var ctr = await counter!.GetCounterForId(unitKeyset.Value, cts); + var newOutputs = CashuUtils.CreateOutputs(amounts, unitKeyset.Value, correspondingKeys.Keys, mnemonic, ctr); + await counter.IncrementCounter(unitKeyset.Value, newOutputs.BlindedMessages.Length, cts); + + var swapRequest = new PostSwapRequest + { + Inputs = recoveredProofs.ToArray(), + Outputs = newOutputs.BlindedMessages, + }; + + var swapResult = await api.Swap(swapRequest, cts); + var constructedProofs = CashuUtils.ConstructProofsFromPromises(swapResult.Signatures.ToList(), newOutputs, correspondingKeys.Keys); + + freshProofs.AddRange(constructedProofs); + } + return freshProofs; + } + + private async Task _createBatch(Mnemonic mnemonic, KeysetId keysetId, int batchNubmber, CancellationToken cts) + { + var amounts = Enumerable.Repeat((ulong)1, 100).ToList(); + Console.WriteLine(batchNubmber); + Console.WriteLine($"Where does batch start: {batchNubmber*100}"); + return mnemonic.DeriveOutputs(amounts, keysetId, batchNubmber*100); + } +} \ No newline at end of file diff --git a/DotNut/Abstractions/StatefulWallet.cs b/DotNut/Abstractions/StatefulWallet.cs new file mode 100644 index 0000000..62949df --- /dev/null +++ b/DotNut/Abstractions/StatefulWallet.cs @@ -0,0 +1,35 @@ +using DotNut.Abstractions.Interfaces; + +namespace DotNut.Abstractions; + +public class StatefulWallet: IStatefulWalletBuilder +{ + private IWalletBuilder _wallet; + private IProofManager _proofManager; + + public StatefulWallet(IWalletBuilder wallet, IProofManager proofManager) + { + this._wallet = wallet; + this._proofManager = proofManager; + } + + public Task ReceiveLightning() + { + throw new NotImplementedException(); + } + + public Task SendLightning() + { + throw new NotImplementedException(); + } + + public Task ReceiveProofs() + { + throw new NotImplementedException(); + } + + public Task SendProofs() + { + throw new NotImplementedException(); + } +} \ No newline at end of file diff --git a/DotNut/Abstractions/SwapBuilder.cs b/DotNut/Abstractions/SwapBuilder.cs new file mode 100644 index 0000000..898e3f5 --- /dev/null +++ b/DotNut/Abstractions/SwapBuilder.cs @@ -0,0 +1,275 @@ +using DotNut.Abstractions.Interfaces; +using DotNut.ApiModels; + +namespace DotNut.Abstractions; +/// +/// Receive operation builder implementation +/// +class SwapBuilder : ISwapBuilder +{ + private readonly Wallet _wallet; + + // input + private readonly string? _tokenString; + private readonly CashuToken? _token; + private List? _proofsToSwap; + + private OutputData? _outputs; + private List? _amounts; + private KeysetId? _keysetId; + private string _unit = "sat"; + private bool _verifySignatures = true; + + private bool _includeFees = true; + + + public SwapBuilder(Wallet wallet, string tokenString) + { + _wallet = wallet; + _tokenString = tokenString; + } + public SwapBuilder(Wallet wallet, CashuToken token) + { + _wallet = wallet; + _token = token; + } + public SwapBuilder(Wallet wallet) + { + _wallet = wallet; + } + + /// + /// Optional. Base unit of wallet instance. If not set defaults to "SAT". + /// + /// + public ISwapBuilder WithUnit(string unit) + { + this._unit = unit; + return this; + } + + /// + /// Provide inputs for a swap. + /// + /// + /// + public ISwapBuilder FromInputs(IEnumerable proofs) + { + this._proofsToSwap = proofs.ToList(); + return this; + } + + public ISwapBuilder ForOutputs(OutputData outputs) + { + this._outputs = outputs; + return this; + } + + /// + /// Optional. + /// True by default, allows user to turn off signature verification (not advised) + /// + /// + /// + public ISwapBuilder WithSignatureVerification(bool verify = true) + { + _verifySignatures = verify; + return this; + } + + /// + /// Optional. + /// Provide outputs for a swap. + /// + /// + /// + public ISwapBuilder WithOutputs(OutputData outputs) + { + _outputs = outputs; + return this; + } + + /// + /// Optional. + /// Allows user to turn off fee calculation. By default, it will calculate and generate smaller set of outputs. + /// + /// + /// + public ISwapBuilder WithFeeCalculation(bool includeFees = true) + { + this._includeFees = includeFees; + return this; + } + + /// + /// Optional. Allows user to choose amounts he wants to get. + /// If sum of amounts smaller than input size, all proofs will be swapped, but rest of proofs will get + /// standard outputs amounts (biggest proof size possible) + /// + /// + /// + public ISwapBuilder WithAmounts(IEnumerable amounts) + { + _amounts = amounts.ToList(); + return this; + } + + /// + /// Optional. Allows user to choose destination keysetId + /// + /// + /// + public ISwapBuilder ForKeyset(KeysetId keysetId) + { + _keysetId = keysetId; + return this; + } + + // when proofs were p2pk + public ISwapBuilder FromP2PK() + { + throw new NotImplementedException(); + } + + // to make p2pk proofs + public ISwapBuilder ToP2PK() + { + throw new NotImplementedException(); + } + + public ISwapBuilder FromHTLC() + { + throw new NotImplementedException(); + } + + public ISwapBuilder ToHTLC() + { + throw new NotImplementedException(); + } + + public async Task> ProcessAsync(CancellationToken cts = default) + { + var mintApi = await _wallet.GetMintApi(cts); + + var swapInputs = await _getSwapProofs(cts); + if (swapInputs == null || swapInputs.Count == 0) + { + throw new ArgumentException("Nothing to swap!"); + } + + // if there's no keysetId specified - let's choose it. + if (_keysetId == null) + { + _keysetId = await _wallet.GetActiveKeysetId(this._unit, cts) ?? + throw new InvalidOperationException("Could not fetch Keyset ID"); + } + var keys = await _wallet.GetKeys(false, cts); + var keysForCurrentId = keys.Single(k=>k.Id == _keysetId); + + if (_verifySignatures) + { + foreach (var proof in swapInputs!) + { + var keyset = keys.Single(k => k.Id == proof.Id); + if (!keyset.Keys.TryGetValue(proof.Amount, out var key)) + { + throw new InvalidOperationException($"Can't find key for amount {proof.Amount} in keyset {keyset.Id}"); + } + var isValid = proof.Verify(key); + if (!isValid) + throw new InvalidOperationException($"Invalid proof signature for amount {proof.Amount}"); + } + } + + var fee = 0UL; + if (_includeFees) + { + var keysetsFees = (await _wallet.GetKeysets(false, cts)).ToDictionary(k=>k.Id, k=>k.InputFee??0); + fee = swapInputs.ComputeFee(keysetsFees); + } + + + var total = CashuUtils.SumProofs(swapInputs); + // Swap received proofs to our keyset + var amounts = await _getAmounts(total, fee, keysForCurrentId.Keys); + + if (amounts.Sum() > total - fee) + { + throw new ArgumentException($"Invalid output amounts! Total output amount requested: ${amounts.Sum()}, total input amount: {total}, fee: ${fee}"); + } + + this._outputs ??= await this._wallet.CreateOutputs(amounts, _keysetId, cts); + + var request = new PostSwapRequest() + { + Inputs = swapInputs.ToArray(), + Outputs = this._outputs.BlindedMessages, + }; + + var swapResponse = await mintApi.Swap(request, cts); + + var swappedProofs = + CashuUtils.ConstructProofsFromPromises(swapResponse.Signatures.ToList(), this._outputs, keysForCurrentId.Keys); + + return swappedProofs; + } + + private async Task> _getSwapProofs(CancellationToken cts = default) + { + _proofsToSwap ??= new(); + if (_tokenString != null) + { + var token = CashuTokenHelper.Decode(this._tokenString, out var v); + if (v == "A") // todo ensure + { + //if token is v1, ensure everything is from the same mint + var mints = token.Tokens.Select(t => t.Mint).ToList(); + if (mints.Count > 1) + { + throw new ArgumentException("Only swap from single mint is allowed"); + } + + } + this._proofsToSwap.AddRange(token.Tokens.SelectMany(t=>t.Proofs)); + } + + if (_token == null) + { + return _proofsToSwap; + } + + //if token is v1, ensure everything is from the same mint + var tokenMints = _token.Tokens.Select(t => t.Mint).ToList(); + if (tokenMints.Count > 1) + { + throw new ArgumentException("Only swap from single mint is allowed"); + } + this._proofsToSwap.AddRange(_token.Tokens.SelectMany(t=>t.Proofs)); + + return _proofsToSwap; + } + + private async Task> _getAmounts(ulong total, ulong fee, Keyset keys) + { + if (_amounts != null) + { + var sum = _amounts.Sum(); + var underpay = total - fee - sum; + + if (underpay == 0) + { + return _amounts; + } + if (underpay > 0) + { + this._amounts.AddRange(CashuUtils.SplitToProofsAmounts(underpay, keys)); + return this._amounts; + } + throw new ArgumentException($"Invalid amounts requested. Sum of amounts: {sum}, total input: {total}, fee:{fee}."); + } + + this._amounts = CashuUtils.SplitToProofsAmounts(total - fee, keys); + return this._amounts; + } + +} \ No newline at end of file diff --git a/DotNut/Abstractions/Wallet.cs b/DotNut/Abstractions/Wallet.cs index 790227d..ba97762 100644 --- a/DotNut/Abstractions/Wallet.cs +++ b/DotNut/Abstractions/Wallet.cs @@ -1,26 +1,21 @@ using System.Diagnostics; +using System.Runtime.CompilerServices; using DotNut.Abstractions.Interfaces; using DotNut.Abstractions.Quotes; using DotNut.Api; using DotNut.ApiModels; +using DotNut.ApiModels.Melt.bolt12; using DotNut.ApiModels.Mint.bolt12; using DotNut.NBitcoin.BIP39; using DotNut.NUT13; +using NBitcoin.Secp256k1; namespace DotNut.Abstractions; -// It should: -// Allow user to use local keys from db - -// Fetch keyets on start, if user didn't provide them. Fetch additional Keys. (if necessary) - -// How can user save these? -// ARE FLUENT BUILDER \ - /// /// Main Cashu Wallet class implementing fluent builder pattern /// -public class Wallet : ICashuWalletBuilder +public class Wallet : IWalletBuilder { private MintInfo? _info; private IProofSelector? _selector; @@ -28,22 +23,27 @@ public class Wallet : ICashuWalletBuilder private List? _keysets; private List? _keys; private Dictionary? _keysetFees => _keysets?.ToDictionary(k=>k.Id, k=>k.InputFee??0); - private Mnemonic? _mnemonic; private ICounter? _counter; //flags private bool _shouldSyncKeyset = true; + private DateTime? _lastSync = DateTime.MinValue; + private TimeSpan? _syncThresold; // if null sync only once + private bool _shouldBumpCounter = true; private bool _allowInvalidKeysetIds = false; - public static ICashuWalletBuilder Create() => new Wallet(); + /* + * Fluent Builder Methods + */ + public static IWalletBuilder Create() => new Wallet(); /// /// Mandatory. Sets a mint in a wallet object /// /// Mint API object. - public ICashuWalletBuilder WithMint(ICashuApi mintApi) + public IWalletBuilder WithMint(ICashuApi mintApi) { _mintApi = mintApi; return this; @@ -53,7 +53,7 @@ public ICashuWalletBuilder WithMint(ICashuApi mintApi) /// Mandatory. Sets a mint in a wallet object (with default CashuHttpClient) /// /// Mint URL string. - public ICashuWalletBuilder WithMint(string mintUrl) + public IWalletBuilder WithMint(string mintUrl) { var httpClient = new HttpClient{ BaseAddress = new Uri(mintUrl)}; _mintApi = new CashuHttpClient(httpClient); @@ -64,7 +64,7 @@ public ICashuWalletBuilder WithMint(string mintUrl) /// Optional. Import Mint Info to CashuWallet. Otherwise, it will be fetched from /v1/info endpoint. /// /// MintInfo object - public ICashuWalletBuilder WithInfo(MintInfo info) + public IWalletBuilder WithInfo(MintInfo info) { this._info = info; return this; @@ -74,13 +74,13 @@ public ICashuWalletBuilder WithInfo(MintInfo info) /// Optional. Import Mint Info to CashuWallet. Otherwise, it will be fetched from /v1/info endpoint. /// /// GetInfoResponse payload returned from mints API - public ICashuWalletBuilder WithInfo(GetInfoResponse info) => this.WithInfo(new MintInfo(info)); + public IWalletBuilder WithInfo(GetInfoResponse info) => this.WithInfo(new MintInfo(info)); /// /// Optional. Import Keysets into CashuWallet class. Otherwise, they will be fetched from /v1/keysets endpoint. /// /// List of Keysets - public ICashuWalletBuilder WithKeysets(IEnumerable keysets) + public IWalletBuilder WithKeysets(IEnumerable keysets) { this._keysets = keysets.ToList(); return this; @@ -90,13 +90,13 @@ public ICashuWalletBuilder WithKeysets(IEnumerable /// GetKeysetsResponse payload returned from mints API - public ICashuWalletBuilder WithKeysets(GetKeysetsResponse keysets) => this.WithKeysets(keysets.Keysets.ToList()); + public IWalletBuilder WithKeysets(GetKeysetsResponse keysets) => this.WithKeysets(keysets.Keysets.ToList()); /// /// Optional. Import Keys into CashuWallet class. Otherwise, they will be fetched from /v1/keys endpoint. /// - /// List of mints Keys - public ICashuWalletBuilder WithKeys(IEnumerable keys) + /// List of mints Keys + public IWalletBuilder WithKeys(IEnumerable keys) { this._keys = keys.ToList(); return this; @@ -106,16 +106,30 @@ public ICashuWalletBuilder WithKeys(IEnumerable /// GetKeysResponse payload returned from mints API - public ICashuWalletBuilder WithKeys(GetKeysResponse keys) => this.WithKeys(keys.Keysets.ToList()); + public IWalletBuilder WithKeys(GetKeysResponse keys) => this.WithKeys(keys.Keysets.ToList()); /// /// Optional. Flag suggesting if CashuWallet should sync provided Keys and Keysets with actual mints state. /// Very useful if wallet stores keys in storage. /// /// boolean, true by default - public ICashuWalletBuilder WithKeysetSync(bool syncKeyset = true) + public IWalletBuilder WithKeysetSync(bool syncKeyset = true) + { + this._shouldSyncKeyset = syncKeyset; + return this; + } + /// + /// Optional. Flag suggesting if CashuWallet should sync provided Keys and Keysets with actual mints state. + /// Has an additional field limiting how often keysets can be refetched. If not set, keysets will be synced only single time, + /// with first operation requiring keysets. (I'd go for like, 60 minutes) + /// + /// + /// + /// + public IWalletBuilder WithKeysetSync(bool syncKeyset, TimeSpan syncThreesold) { this._shouldSyncKeyset = syncKeyset; + this._syncThresold = syncThreesold; return this; } @@ -123,7 +137,7 @@ public ICashuWalletBuilder WithKeysetSync(bool syncKeyset = true) /// Optional. Proof selecting algorithm. If not set, defaults to RGLI proof selector. /// /// - public ICashuWalletBuilder WithSelector(IProofSelector selector) + public IWalletBuilder WithSelector(IProofSelector selector) { _selector = selector; return this; @@ -133,7 +147,7 @@ public ICashuWalletBuilder WithSelector(IProofSelector selector) /// Optional. BIP39 seed for secret and blinding factors derivation. All proofs generated by CashuWallet will be recoverable. /// /// Mnemonic object - public ICashuWalletBuilder WithMnemonic(Mnemonic mnemonic) + public IWalletBuilder WithMnemonic(Mnemonic mnemonic) { _mnemonic = mnemonic; return this; @@ -143,18 +157,17 @@ public ICashuWalletBuilder WithMnemonic(Mnemonic mnemonic) /// Optional. BIP39 seed for secret and blinding factors derivation. All proofs generated by CashuWallet will be recoverable. /// /// Bip39 seed string separated by spaces. - public ICashuWalletBuilder WithMnemonic(string mnemonic) + public IWalletBuilder WithMnemonic(string mnemonic) { _mnemonic = new Mnemonic(mnemonic); return this; } - /// /// Optional and mandatory if Mnemonic provided. Counter for each Keyset Id for derivation purposes. /// /// Counter object - public ICashuWalletBuilder WithCounter(ICounter counter) + public IWalletBuilder WithCounter(ICounter counter) { this._counter = counter; return this; @@ -165,9 +178,9 @@ public ICashuWalletBuilder WithCounter(ICounter counter) /// /// Counter dictionary /// - public ICashuWalletBuilder WithCounter(IDictionary counter) + public IWalletBuilder WithCounter(IDictionary counter) { - this._counter = new Counter(counter); + this._counter = new InMemoryCounter(counter); return this; } @@ -179,176 +192,111 @@ public ICashuWalletBuilder WithCounter(IDictionary counter) /// WARNING: Disabling auto-increment is potentially dangerous. Manual counter management is required /// to prevent secret reuse, which will cause mint rejection and operation failures. /// - public ICashuWalletBuilder ShouldBumpCounter(bool shouldBumpCounter = true) + public IWalletBuilder ShouldBumpCounter(bool shouldBumpCounter = true) { this._shouldBumpCounter = shouldBumpCounter; return this; } - /// - /// Create swap transaction builder. + /// Optional. + /// Allows user to build stateful wallet, by providing a proof manager - a class allowing wallet to fetch, save and use proofs from desired kind of storage. + /// (See InMemoryProofManager.cs) /// - /// Swap transaction builder - public ICashuWalletSwapBuilder Swap() - { - return new CashuWalletSwapBuilder(this); - } - - public ICashuWalletMeltQuoteBuilder CreateMeltQuote() - { - return new CashuWalletMeltQuoteBuilder(this); - } - - public ICashuWalletMintBuilder CreateMintQuote() + /// + /// + public IStatefulWalletBuilder WithProofManager(IProofManager proofManager) { - return new CashuWalletMintQuoteBuilder(this); + return new StatefulWallet(this, proofManager); } - public ICashuWalletRestoreBuilder Restore() - { - return new CashuWalletRestoreBuilder(this); - } + /* + * Main api methods + */ /// - /// Wrapper for GetKeysets api endpoint. Formats Keysets to list. + /// Create Mint Quote builder /// - /// List of Keysets - /// May be thrown if mint is not set. - private async Task> _fetchKeysets(CancellationToken cts = default) + /// + public IMintQuoteBuilder CreateMintQuote() { - if (!_ensureApiConnected()) - { - throw new ArgumentNullException(nameof(this._mintApi), "Can't fetch mint info without mintApi"); - } - var keysetsRaw = await _mintApi!.GetKeysets(cts); - return keysetsRaw.Keysets.ToList(); + _ensureApiConnected(); + return new MintQuoteBuilder(this); } /// - /// Wrapper for GetKeys api endpoint. Validates returned KeysetIds and formats Keys to list. + /// Create swap transaction builder. /// - /// List of Keys (lists :)) - /// May be thrown if mint is not set. - /// May be thrown if mint returns invalid keysetId for at least one Keyset - private async Task> _fetchKeys(CancellationToken cts = default) + /// Swap transaction builder + public ISwapBuilder Swap() { - if (!_ensureApiConnected()) - { - throw new ArgumentNullException(nameof(_mintApi), "Can't fetch mint info without mintApi"); - } - var keysRaw = await _mintApi!.GetKeys(cts); - foreach (var keysetItemResponse in keysRaw.Keysets) - { - var isKeysetIdValid = keysetItemResponse.Keys.VerifyKeysetId(keysetItemResponse.Id, keysetItemResponse.Unit, keysetItemResponse.FinalExpiry); - if (!isKeysetIdValid) - { - throw new ArgumentException($"Mint provided invalid keysetId. Provided: {keysetItemResponse.Id}, derived: {keysetItemResponse.Keys.GetKeysetId()} "); - } - } - return keysRaw.Keysets.ToList(); + _ensureApiConnected(); + return new SwapBuilder(this); } - - /// - /// Wrapper for GetKeys api endpoint. Validates KeysetId and fetches keys for single KeysetId Formats Keys to list. - /// - /// KeysetId we want fetch keys for. - /// Keys - /// May be thrown if mint returns invalid keysetId for at least one Keyset - /// May be thrown if mint is not set. - private async Task _fetchKeys(KeysetId id, CancellationToken cts = default) - { - if (!_ensureApiConnected()) - { - throw new ArgumentNullException(nameof(_mintApi), "Can't fetch mint info without mintApi"); - } - var keysRaw = (await _mintApi!.GetKeys(id, cts)).Keysets.Single(); - - var isKeysetIdValid = keysRaw.Keys.VerifyKeysetId(keysRaw.Id, keysRaw.Unit, keysRaw.FinalExpiry); - if (!isKeysetIdValid) - { - throw new ArgumentException($"Mint provided invalid keysetId. Provided: {keysRaw.Id}, derived: {keysRaw.Keys.GetKeysetId()} "); - } - return keysRaw; + public IMeltQuoteBuilder CreateMeltQuote() + { + _ensureApiConnected(); + return new MeltQuoteBuilder(this); } - /// - /// Wrapper for GetInfo api endpoint. Translates Payload to MintInfo. - /// - /// May be thrown if mint is not set. - private async Task _fetchMintInfo(CancellationToken cts = default) - { - if (!_ensureApiConnected()) - { - throw new ArgumentNullException(nameof(this._mintApi), "Can't fetch mint info without mintApi"); - } - var infoRaw = await _mintApi!.GetInfo(cts); - return new MintInfo(infoRaw); + public async Task CheckState(IEnumerable proofs) + { + return await CheckState(proofs.Select(p => p.Secret.ToCurve())); } - /// - /// Fetches mint info if not present in CashuWallet. - /// - /// - private async Task _lazyFetchMintInfo(CancellationToken cts = default) + public async Task CheckState(IEnumerable Ys) { - if (this._info != null) return this._info; - if (_ensureApiConnected()) + _ensureApiConnected(); + var req = new PostCheckStateRequest() { - throw new ArgumentNullException(nameof(this._mintApi), "Can't fetch mint info without mintApi"); - } - return await this._fetchMintInfo(cts); + Ys = Ys.Select(y=>y.ToString()).ToArray(), + }; + return await _mintApi!.CheckState(req); + } + + public IRestoreBuilder Restore() + { + _ensureApiConnected(); + return new RestoreBuilder(this); } + + + /* + * Public Mint utils + */ + /// - /// Local Keys sync. + /// Set Last sync date to DateTime.MinValue - keysets will be synced before next operation /// - /// - /// - internal async Task _maybeSyncKeys(CancellationToken cts = default) + public void InvalidateCache() { - if (!_shouldSyncKeyset) - { - return; - } - if (!_ensureApiConnected()) - { - throw new ArgumentNullException(nameof(this._mintApi), "Can't sync mint keys without mintApi"); - } - - this._keysets = await _fetchKeysets(cts); - if (_keys == null) - { - this._keys = await _fetchKeys(cts); // we're fetching all keys here, so no need for additional check. - return; - } - - var knownIds = _keys.Select(key => key.Id).ToHashSet(); - var unknownKeysets = _keysets.Where(k => !knownIds.Contains(k.Id)).ToList(); - - if (unknownKeysets.Count > 2) // just make a single request. May override stored keys. - { - this._keys = await _fetchKeys(cts); - return; - } - - foreach (var unknownKeyset in unknownKeysets) - { - var keyset = await this._fetchKeys(unknownKeyset.Id, cts); - this._keys.Add(keyset); - } + _lastSync = DateTime.MinValue; } + + /// + /// Get active keyset id for chosen unit. + /// + /// keyset unit, e.g. sat + /// + /// Active keysetId public async Task GetActiveKeysetId(string unit, CancellationToken cts = default) { + await _maybeSyncKeys(cts); return _keysets? .OrderBy(k => k.InputFee) - .FirstOrDefault(k => k.Active == true && k.Unit == unit, null) + .FirstOrDefault(k => k is { Active: true } && k.Unit == unit, null) ?.Id; } - public async Task?> GetActiveKeysetIdsWithUnits() + /// + /// Get active keyset ids for each unit + /// + /// Dictionary of (unit, KeysetId) + public async Task?> GetActiveKeysetIdsWithUnits(CancellationToken cts = default) { + await _maybeSyncKeys(cts); return _keysets? .GroupBy(k => k.Unit) .ToDictionary( @@ -356,15 +304,32 @@ internal async Task _maybeSyncKeys(CancellationToken cts = default) g => g.OrderBy(k => k.InputFee).First().Id ); } + + /// + /// Get keys of current mint stored in wallet. + /// + /// Refetch flag + /// + /// Mints keys public async Task> GetKeys(bool forceRefresh = false, CancellationToken cts = default) { if (forceRefresh) { - this._keys = await _fetchKeys(cts); + this._keys = await _fetchKeys(cts); + return this._keys ?? []; } + await _maybeSyncKeys(cts); return this._keys ?? []; } + /// + /// Get Keys for given KeysetID + /// + /// KeysetId + /// Refetch flag + /// + /// Keys for given keyset + /// If wallet doesn't contain keysets for given keysetId public async Task GetKeys(KeysetId id, bool forceRefresh = false, CancellationToken cts = default) { if (forceRefresh) @@ -377,15 +342,30 @@ internal async Task _maybeSyncKeys(CancellationToken cts = default) } return this._keys.Single(k => k.Id == id); } + + /// + /// Get Keysets stored in wallet + /// + /// Refetch flag + /// + /// List of Keysets public async Task> GetKeysets(bool forceRefresh = false, CancellationToken cts = default) { if (forceRefresh) { this._keysets = await _fetchKeysets(cts); + return _keysets ?? []; } - + await _maybeSyncKeys(cts); return _keysets ?? []; } + + /// + /// Get Mints info, supported methods etc. + /// + /// Refetch flag + /// + /// MintInfo object public async Task GetInfo(bool forceReferesh = false, CancellationToken cts = default) { if (forceReferesh) @@ -394,8 +374,19 @@ public async Task GetInfo(bool forceReferesh = false, CancellationToke } return await _lazyFetchMintInfo(cts); } + + /// + /// Create Outputs (BlindedMessags, Blinding Factors, Secrets), for given keysetId. + /// Deterministic if Mnemonic and Counter set up. + /// + /// List of amounts in Outputs. + /// Keyset ID + /// + /// Outputs + /// If keys not set. If Mnemonic set, but no Counter. public async Task CreateOutputs(List amounts, KeysetId id, CancellationToken cts = default) { + await _maybeSyncKeys(cts); if (this._keys == null) { throw new ArgumentNullException(nameof(this._keys), "No Keys found. Make sure to fetch them!"); @@ -411,534 +402,203 @@ public async Task CreateOutputs(List amounts, KeysetId id, Ca throw new ArgumentNullException(nameof(ICounter), "Can't derive outputs without keyset counter"); } - var counterValue = await this._counter.GetCounterForId(id); + var counterValue = await this._counter.GetCounterForId(id, cts); if (_shouldBumpCounter) { - await this._counter.IncrementCounter(id, amounts.Count); + await this._counter.IncrementCounter(id, amounts.Count, cts); } return CashuUtils.CreateOutputs(amounts, id, keyset.Keys, this._mnemonic, counterValue); } - - internal async Task _swap(PostSwapRequest request, CancellationToken cts = default) - { - if (!_ensureApiConnected()) - { - throw new ArgumentNullException(nameof(this._mintApi), "Can't swap without mintApi"); - } - - return await this._mintApi!.Swap(request, cts); - } - - public IProofSelector? GetSelector() => _selector; - public ICashuApi? GetMintApi() => _mintApi; - public Mnemonic? GetMnemonic() => _mnemonic; - public ICounter? GetCounter() => _counter; - private bool _ensureApiConnected() => _mintApi != null; -} - -class CashuWalletMintQuoteBuilder : ICashuWalletMintBuilder -{ - private readonly Wallet _wallet; - private ulong? _amount; - private string _unit = "sat"; - private string? _description; - private OutputData? _outputs; - private string? _method = "bolt11"; - - //for bolt12 - private string? _pubkey; - - private KeysetId? _keysetId; - private GetKeysResponse.KeysetItemResponse keyset; - public CashuWalletMeltQuoteBuilder(Wallet wallet) - { - this._wallet = wallet; - } - + /// - /// Mandatory. - /// User has to provide Mint method - /// - /// Either MintMeltMethod.Bolt11 or MintMeltMethod.Bolt12 - /// - public ICashuWalletMintBuilder WithMethod(string method) - { - this._method = method; - return this; - } - - /// - /// Mandatory. - /// - /// Amount of token in currently choosen unit to be melted - public ICashuWalletMintBuilder WithAmount(ulong amount) - { - this._amount = amount; - return this; - } - - /// - /// Optional. - /// Sets unit of tokens being minted. Sat by default. + /// Create Outputs for active KeysetId for given unit. /// - /// Unit of minted proofs - public ICashuWalletMintBuilder WithUnit(string unit) + /// List of amounts. + /// + /// + /// Outputs + /// If no keysetID stored in wallet. + public async Task CreateOutputs(List amounts, string unit, CancellationToken cts = default) { - this._unit = unit; - return this; + var keysetId = await this.GetActiveKeysetId(unit, cts); + if (keysetId == null) + { + throw new ArgumentNullException(nameof(keysetId)); + } + return await this.CreateOutputs(amounts, keysetId, cts); } - /// - /// Optional. Necessary for bolt12 - /// Sets pubkey for bolt12 offer - /// - /// - /// - public ICashuWalletMintBuilder WithPubkey(string pubkey) - { - this._pubkey = pubkey; - return this; - } - - /// - /// Optional. - /// Allows user to set keysetId manually. Otherwise, builder will choose active one manually, with the lowest fees. - /// - /// - public ICashuWalletMintBuilder WithKeyset(KeysetId keysetId) + public async Task SelectProofsToSend(List proofs, ulong amount, bool includeFees, CancellationToken cts = default) { - this._keysetId = keysetId; - return this; - } - + if (this._selector == null) + { + await _maybeSyncKeys(cts); + ArgumentNullException.ThrowIfNull(this._keysetFees); + this._selector = new ProofSelector(this._keysetFees); + } - /// - /// Optional. - /// User may provide outputs for mint to sign. Blinding factors and secrets won't be revealed to mint. - /// If not provided, wallet will try to derive them from seed and counter, or create random ones if mnemonic is not avaible. - /// - /// OutputData instance. Enumerables of BlindingFactors, BlindedMessages and Secrets, in right order. - public ICashuWalletMintBuilder WithOutputs(OutputData outputs) - { - this._outputs = outputs; - return this; + return await _selector.SelectProofsToSend(proofs, amount, includeFees, cts); } - /// - /// Optional. - /// User may provide description for melt quote invoice. - /// - /// - /// - public ICashuWalletMintBuilder WithDescription(string description) + public async Task GetMintApi(CancellationToken cts = default) { - this._description = description; - return this; + _ensureApiConnected(); + return _mintApi; } - - - public async Task ProcessAsync(CancellationToken cts = default) + public async Task? GetSelector(CancellationToken cts = default) { - //todo implement info - - await this._wallet._maybeSyncKeys(cts); - if (_amount == null) + if (this._selector == null) { - throw new ArgumentNullException(nameof(_amount), "can't create melt quote without amount!"); + await _maybeSyncKeys(cts); + ArgumentNullException.ThrowIfNull(this._keysetFees); + this._selector = new ProofSelector(this._keysetFees); } + return this._selector; + } + public Mnemonic? GetMnemonic() => _mnemonic; + public ICounter? GetCounter() => _counter; - var api = this._wallet.GetMintApi(); - if (api is null) + internal void _ensureApiConnected(string? msg = null) + { + if (_mintApi != null) { - throw new ArgumentNullException(nameof(ICashuApi), "Can't request mint quote without mint API"); + return; } - if (this._keysetId == null) + if (msg is not null) { - this._keysetId = await this._wallet.GetActiveKeysetId(this._unit, cts) ?? - throw new ArgumentException($"Can't fetch active keyset ID for unit: {_unit}"); + throw new ArgumentNullException(nameof(this._mintApi), msg); } - - switch (_method) - { - case "bolt11": - { - var reqBolt11 = new PostMintQuoteBolt11Request() - { - Amount = this._amount.Value, - Unit = this._unit, - Description = this._description, - }; - var quoteBolt11 = - await api.CreateMintQuote("bolt11", reqBolt11, - cts); - return new MintHandlerBolt11(this._wallet, quoteBolt11, this.keyset); - } - case "bolt12": - { - if (this._pubkey == null) - { - throw new ArgumentNullException(nameof(_pubkey), "Can't request bolt12 mint quote without pubkey!"); - } - var req = new PostMintQuoteBolt12Request() - { - Amount = this._amount.Value, - Unit = this._unit, - Pubkey = this._pubkey, - Description = this._description, - }; - var mintQuote = - await api.CreateMintQuote("bolt12", req, - cts); - return new MintHandlerBolt12(this._wallet, mintQuote, this.keyset); - } - default: - throw new ArgumentException($"Unknown mint method: {_method}"); - } + throw new ArgumentNullException(nameof(this._mintApi)); } -} - -/// -/// Receive operation builder implementation -/// -class CashuWalletSwapBuilder : ICashuWalletSwapBuilder -{ - private readonly Wallet _wallet; - - // input - private readonly string? _tokenString; - private readonly CashuToken? _token; - private List? _proofsToSwap; - - private OutputData? _outputs; - private List? _amounts; - private KeysetId? _keysetId; - private string? _unit; - private bool _verifySignatures = true; - - public CashuWalletSwapBuilder(Wallet wallet, string tokenString) - { - _wallet = wallet; - _tokenString = tokenString; - } - public CashuWalletSwapBuilder(Wallet wallet, CashuToken token) - { - _wallet = wallet; - _token = token; - } - public CashuWalletSwapBuilder(Wallet wallet) - { - _wallet = wallet; - } + + + /* + * Private helpers + */ /// - /// Optional. Base unit of wallet instance. If not set defaults to "SAT". + /// Wrapper for GetKeysets api endpoint. Formats Keysets to list. /// - /// - public ICashuWalletSwapBuilder WithUnit(string unit) + /// List of Keysets + /// May be thrown if mint is not set. + private async Task> _fetchKeysets(CancellationToken cts = default) { - this._unit = unit; - return this; + _ensureApiConnected("Can't fetch keysets without mint api!"); + var keysetsRaw = await _mintApi!.GetKeysets(cts); + return keysetsRaw.Keysets.ToList(); } - public ICashuWalletSwapBuilder WithSignatureVerification(bool verify = true) - { - _verifySignatures = verify; - return this; - } - public ICashuWalletSwapBuilder WithOutputs(OutputData outputs) - { - _outputs = outputs; - return this; - } - public ICashuWalletSwapBuilder WithAmounts(IEnumerable amounts) - { - _amounts = amounts.ToList(); - return this; - } - public ICashuWalletSwapBuilder ForKeyset(KeysetId keysetId) - { - _keysetId = keysetId; - return this; - } - private async Task> _getSwapProofs() + /// + /// Wrapper for GetKeys api endpoint. Validates returned KeysetIds and formats Keys to list. + /// + /// List of Keys (lists :)) + /// May be thrown if mint is not set. + /// May be thrown if mint returns invalid keysetId for at least one Keyset + private async Task> _fetchKeys(CancellationToken cts = default) { - _proofsToSwap ??= new(); - if (_tokenString != null) + _ensureApiConnected("Can't fetch keys without mint api!"); + var keysRaw = await _mintApi!.GetKeys(cts); + foreach (var keysetItemResponse in keysRaw.Keysets) { - var token = CashuTokenHelper.Decode(this._tokenString, out var v); - if (v == "A") // todo ensure + var isKeysetIdValid = keysetItemResponse.Keys.VerifyKeysetId(keysetItemResponse.Id, keysetItemResponse.Unit, keysetItemResponse.FinalExpiry); + if (!isKeysetIdValid) { - //if token is v1, ensure everything is from the same mint - var mints = token.Tokens.Select(t => t.Mint).ToList(); - if (mints.Count > 1) - { - throw new ArgumentException("Only swap from single mint is allowed"); - } - + throw new ArgumentException($"Mint provided invalid keysetId. Provided: {keysetItemResponse.Id}, derived: {keysetItemResponse.Keys.GetKeysetId()} "); } - this._proofsToSwap.AddRange(token.Tokens.SelectMany(t=>t.Proofs)); - } - - if (_token == null) return _proofsToSwap; - - //if token is v1, ensure everything is from the same mint - var tokenMints = _token.Tokens.Select(t => t.Mint).ToList(); - if (tokenMints.Count > 1) - { - throw new ArgumentException("Only swap from single mint is allowed"); } - this._proofsToSwap.AddRange(_token.Tokens.SelectMany(t=>t.Proofs)); - - return _proofsToSwap; + return keysRaw.Keysets.ToList(); } - public async Task> ProcessAsync(CancellationToken cts = default) + + /// + /// Wrapper for GetKeys api endpoint. Validates KeysetId and fetches keys for single KeysetId Formats Keys to list. + /// + /// KeysetId we want fetch keys for. + /// Keys + /// May be thrown if mint returns invalid keysetId for at least one Keyset + /// May be thrown if mint is not set. + private async Task _fetchKeys(KeysetId id, CancellationToken cts = default) { - if (_wallet.GetMintApi() == null) - throw new InvalidOperationException("Mint API must be configured"); - - await _getSwapProofs(); - if (_proofsToSwap == null || _proofsToSwap.Count == 0) - { - throw new ArgumentException("Nothing to swap!"); - } - - // if there's no keysetId specified - let's choose it. - if (_keysetId == null) - { - _keysetId = await _wallet.GetActiveKeysetId(this._unit, cts) ?? - throw new InvalidOperationException("Could not fetch Keyset ID"); - } - var keys = await _wallet.GetKeys(false, cts); - var keysForCurrentId = keys.Single(k=>k.Id == _keysetId); + _ensureApiConnected("Can't fetch keys without mint api!"); + var keysRaw = (await _mintApi!.GetKeys(id, cts)).Keysets.Single(); - if (_verifySignatures) + var isKeysetIdValid = keysRaw.Keys.VerifyKeysetId(keysRaw.Id, keysRaw.Unit, keysRaw.FinalExpiry); + if (!isKeysetIdValid) { - foreach (var proof in _proofsToSwap!) - { - var keyset = keys.Single(k => k.Id == proof.Id); - if (keyset.Keys.TryGetValue(proof.Amount, out var key)) - { - throw new InvalidOperationException($"Can't find key for amount {proof.Amount} in keyset ${keyset.Id}"); - } - var isValid = proof.Verify(key); - if (!isValid) - throw new InvalidOperationException($"Invalid proof signature for amount {proof.Amount}"); - } + throw new ArgumentException($"Mint provided invalid keysetId. Provided: {keysRaw.Id}, derived: {keysRaw.Keys.GetKeysetId()} "); } - - ulong total = _proofsToSwap!.Aggregate(0, (acc, p) => acc + p.Amount); - // Swap received proofs to our keyset - var amounts = _amounts ?? CashuUtils.SplitToProofsAmounts(total, keysForCurrentId.Keys); - - this._outputs ??= await this._wallet.CreateOutputs(amounts, _keysetId, cts); - - var request = new PostSwapRequest() - { - Inputs = this._proofsToSwap.ToArray(), - Outputs = this._outputs.BlindedMessages, - }; - - - var swapResponse = await _wallet.GetMintApi()!.Swap(request, cts); - var swappedProofs = - CashuUtils.ConstructProofsFromPromises(swapResponse.Signatures.ToList(), this._outputs, keysForCurrentId.Keys); - - return swappedProofs; - } -} - -class CashuWalletMeltQuoteBuilder : ICashuWalletMeltQuoteBuilder -{ - private readonly Wallet _wallet; - private List? _proofs; - private string? _invoice; - private OutputData? _blankOutputs; - private ulong? _amount; - private string? _method; - - public CashuWalletMeltQuoteBuilder(Wallet wallet) - { - _wallet = wallet; + return keysRaw; } - public ICashuWalletMeltQuoteBuilder WithInvoice(string invoice) - { - this._invoice = invoice; - return this; - } - - public ICashuWalletMeltQuoteBuilder WithMethod(string method = "bolt11") - { - throw new NotImplementedException(); - } - - public Task ProcessAsync(CancellationToken cancellationToken = default) - { - throw new NotImplementedException(); - } - - public ICashuWalletMeltQuoteBuilder WithProofs(IEnumerable proofs) - { - this._proofs = proofs.ToList(); - return this; - } - - public ICashuWalletMeltQuoteBuilder WithBlankOutputs(OutputData blankOutputs) - { - this._blankOutputs = blankOutputs; - return this; - } - - public ICashuWalletMeltQuoteBuilder WithAmount(ulong amount) + /// + /// Wrapper for GetInfo api endpoint. Translates Payload to MintInfo. + /// + /// May be thrown if mint is not set. + private async Task _fetchMintInfo(CancellationToken cts = default) { - this._amount = amount; - return this; - } - - public async Task ProcessAsyncBolt11(CancellationToken cancellationToken = default) - { - var activeKeysetId = _wallet.GetActiveKeysetId()?? - throw new InvalidOperationException("Could not fetch active Keyset ID"); - var activeKeyset = _wallet.GetKeysets().SingleOrDefault(k => k.Id == activeKeysetId, null)?? - throw new InvalidOperationException($"Could not fetch keyset for KeysetId: {activeKeysetId}"); - var mnemonic = _wallet.GetMnemonic(); - var counter = _wallet.GetCounter(); - - if (_blankOutputs == null) - { - if (_amount != null) - { - _blankOutputs = CashuUtils.CreateBlankOutputs(_amount, activeKeysetId, activeKeyset.Keys, mnemonic, counter?.GetCounterForId(activeKeysetId)); - } - // processing without blank outputs - } - var req = new PostMeltQuoteBolt11Request - { - Request = this._invoice, - Unit = _wallet.GetUnit() - }; - var mintResponse = await _wallet.GetMintApi().CreateMeltQuote("bolt11", req, cancellationToken); - - return new MeltQuoteBolt11(mintResponse); + _ensureApiConnected("Can't fetch mint info without mint api!"); + var infoRaw = await _mintApi!.GetInfo(cts); + return new MintInfo(infoRaw); } -} -class CashuWalletRestoreBuilder : ICashuWalletRestoreBuilder -{ - private readonly Wallet _wallet; - private List? _specifiedKeysets; - - private bool _shouldSwap = true; - - public CashuWalletRestoreBuilder(Wallet wallet) + /// + /// Fetches mint info if not present in CashuWallet. + /// + /// + private async Task _lazyFetchMintInfo(CancellationToken cts = default) { - this._wallet = wallet; + if (this._info != null) return this._info; + return await this._fetchMintInfo(cts); } - public ICashuWalletRestoreBuilder ForKeysetIds(IEnumerable keysetIds) - { - this._specifiedKeysets = keysetIds.ToList(); - return this; - } - - public ICashuWalletRestoreBuilder WithSwap(bool shouldSwap = true) - { - this._shouldSwap = shouldSwap; - } - - public async Task> ProcessAsync(CancellationToken cts = default) + /// + /// Local Keys sync. + /// + /// + /// + internal async Task _maybeSyncKeys(CancellationToken cts = default) { - var mnemonic = _wallet.GetMnemonic()?? - throw new ArgumentNullException("Can't restore wallet without Mnemonic"); - if (_specifiedKeysets == null) + if (!_shouldSyncKeyset) { - _specifiedKeysets = (await _wallet.GetKeysets()).Select(k=>k.Id).ToList(); + return; } - var api = _wallet.GetMintApi(); - if (api == null) - { - throw new ArgumentNullException(nameof(api), "Can't restore wallet without MintApi"); + // should sync keysets SINGLE time in the lifespan of object. If already synced - return; + if (_syncThresold == null && _lastSync != DateTime.MinValue) + { + return; } - - var counter = _wallet.GetCounter(); - if (counter == null) + // should sync keysets in some timepsan + if (_syncThresold != null && _lastSync + _syncThresold >= DateTime.Now) { - _wallet.WithCounter(new Counter(new Dictionary())); + return; } - List recoveredProofs = new List(); - foreach (var keysetId in _specifiedKeysets) + this._keysets = await _fetchKeysets(cts); + if (_keys == null) { - bool isKeysetRestored = false; - int batchNumber = 0; - int emptyBatchesRemaining = 3; - - var keyset = await _wallet.GetKeys(keysetId, false, cts); - - while (!isKeysetRestored && emptyBatchesRemaining > 0) - { - var outputs = await _createBatch(mnemonic, keysetId, batchNumber, cts); - await counter!.IncrementCounter(keysetId, batchNumber * 100); - var req = new PostRestoreRequest - { - Outputs = outputs.BlindedMessages - }; - var res = await api.Restore(req, cts); - - if (!res.Signatures.Any()) - { - emptyBatchesRemaining--; - } - - var proofs = CashuUtils.ConstructProofsFromPromises(res.Signatures.ToList(), outputs, keyset.Keys); - recoveredProofs.AddRange(proofs); - } - + this._keys = await _fetchKeys(cts); // we're fetching all keys here, so no need for additional check. + return; } - if (!this._shouldSwap || !recoveredProofs.Any()) + var knownIds = _keys.Select(key => key.Id).ToHashSet(); + var unknownKeysets = _keysets.Where(k => !knownIds.Contains(k.Id)).ToList(); + + if (unknownKeysets.Count > 2) // just make a single request. May override stored keys. { - return recoveredProofs; + this._keys = await _fetchKeys(cts); + return; } - var freshProofs = new List(); - var activeUnits = await this._wallet.GetActiveKeysetIdsWithUnits(); - if (activeUnits != null && !activeUnits.Any()) + foreach (var unknownKeyset in unknownKeysets) { - throw new InvalidOperationException("Could not restore wallet without active keysets"); + var keyset = await this._fetchKeys(unknownKeyset.Id, cts); + this._keys.Add(keyset); } - - foreach (var unitKeyset in activeUnits) - { - var correspondingKeys = await _wallet.GetKeys(unitKeyset.Value, false, cts); - var totalAmount = recoveredProofs.Select(p=>p.Amount).Aggregate((a,c) => a + c); - var amounts = CashuUtils.SplitToProofsAmounts(totalAmount, correspondingKeys.Keys); - var ctr = await counter!.GetCounterForId(unitKeyset.Value, cts); - var newOutputs = CashuUtils.CreateOutputs(amounts, unitKeyset.Value, correspondingKeys.Keys, mnemonic, ctr); - await counter.IncrementCounter(unitKeyset.Value, newOutputs.BlindedMessages.Length, cts); - - var swapRequest = new PostSwapRequest - { - Inputs = recoveredProofs.ToArray(), - Outputs = newOutputs.BlindedMessages, - }; - var swapResult = await _wallet._swap(swapRequest, cts); - - var constructedProofs = CashuUtils.ConstructProofsFromPromises(swapResult.Signatures.ToList(), newOutputs, correspondingKeys.Keys); - - freshProofs.AddRange(constructedProofs); - } - return freshProofs; - } - - private async Task _createBatch(Mnemonic mnemonic, KeysetId keysetId, int batchNubmber, CancellationToken cts) - { - var amounts = Enumerable.Repeat((ulong)1, 100).ToList(); - return mnemonic.DeriveOutputs(amounts, keysetId, batchNubmber*100); + _lastSync = DateTime.Now; } } + diff --git a/DotNut/Abstractions/WalletResults.cs b/DotNut/Abstractions/WalletResults.cs deleted file mode 100644 index 2741927..0000000 --- a/DotNut/Abstractions/WalletResults.cs +++ /dev/null @@ -1,77 +0,0 @@ -namespace DotNut; - -/// -/// Result of a send operation -/// -public class SendResult -{ - public CashuToken Token { get; set; } = null!; - public string TokenString { get; set; } = string.Empty; - public ulong AmountSent { get; set; } - public List RemainingProofs { get; set; } = new(); - public ulong FeesPaid { get; set; } -} - -/// -/// Result of a receive operation -/// -public class ReceiveResult -{ - public List ReceivedProofs { get; set; } = new(); - public ulong AmountReceived { get; set; } - public CashuToken Token { get; set; } = null!; - public bool SignatureVerified { get; set; } -} - -/// -/// Result of a swap operation -/// -public class SwapResult -{ - public List SwappedProofs { get; set; } = new(); - public ulong TotalAmount { get; set; } - public KeysetId TargetKeysetId { get; set; } = new(""); - public ulong FeesPaid { get; set; } -} - -/// -/// Result of a melt operation (paying invoice) -/// -public class MeltResult -{ - public bool Paid { get; set; } - public string? PaymentPreimage { get; set; } - public List ChangeProofs { get; set; } = new(); - public ulong AmountPaid { get; set; } - public ulong FeesPaid { get; set; } - public string QuoteId { get; set; } = string.Empty; -} - -// /// -// /// Result of a mint operation (receiving from invoice) -// /// -// public class MintResult -// { -// public List MintedProofs { get; set; } = new(); -// public ulong AmountMinted { get; set; } -// public string QuoteId { get; set; } = string.Empty; -// public bool QuotePaid { get; set; } -// } - -/// -/// Result of checking proof states -/// -public class StateResult -{ - public Dictionary States { get; set; } = new(); -} - -/// -/// Proof state information -/// -public class ProofState -{ - public bool Spent { get; set; } - public bool Pending { get; set; } - public string? Witness { get; set; } -} diff --git a/DotNut/NUT02/FeeHelper.cs b/DotNut/NUT02/FeeHelper.cs index f82757f..05a6312 100644 --- a/DotNut/NUT02/FeeHelper.cs +++ b/DotNut/NUT02/FeeHelper.cs @@ -18,4 +18,9 @@ public static ulong ComputeFee(this IEnumerable proofsToSpend, Dictionary return (sum + 999) / 1000; } + + public static ulong Sum(this IEnumerable ul) + { + return ul.Aggregate((x, y) => x + y); + } } \ No newline at end of file From 8952d6bb19f90a7fd18e1b1cef9b777da27ccf02 Mon Sep 17 00:00:00 2001 From: d4rp4t Date: Wed, 15 Oct 2025 15:29:38 +0200 Subject: [PATCH 09/70] sig_all --- DotNut/Abstractions/SwapBuilder.cs | 65 ++++++++++++++++++--- DotNut/NUT11/SigAllHandler.cs | 91 ++++++++++++++++++++++++++++++ 2 files changed, 148 insertions(+), 8 deletions(-) create mode 100644 DotNut/NUT11/SigAllHandler.cs diff --git a/DotNut/Abstractions/SwapBuilder.cs b/DotNut/Abstractions/SwapBuilder.cs index 898e3f5..54f0e34 100644 --- a/DotNut/Abstractions/SwapBuilder.cs +++ b/DotNut/Abstractions/SwapBuilder.cs @@ -1,3 +1,4 @@ +using System.Text.Json; using DotNut.Abstractions.Interfaces; using DotNut.ApiModels; @@ -22,6 +23,9 @@ class SwapBuilder : ISwapBuilder private bool _includeFees = true; + //p2pk stuff + private List? _privKeys; + private P2PkBuilder? _p2pkBuilder; public SwapBuilder(Wallet wallet, string tokenString) { @@ -126,15 +130,23 @@ public ISwapBuilder ForKeyset(KeysetId keysetId) } // when proofs were p2pk - public ISwapBuilder FromP2PK() + public ISwapBuilder WithPrivkeys(IEnumerable privKeys) { - throw new NotImplementedException(); + this._privKeys = privKeys.ToList(); + return this; } - // to make p2pk proofs - public ISwapBuilder ToP2PK() + /// + /// Optional. + /// If provided, every proof will be generated with random nonce. + /// + /// + /// + /// + public ISwapBuilder ToP2PK(P2PkBuilder p2pkBuilder) { - throw new NotImplementedException(); + this._p2pkBuilder = p2pkBuilder; + return this; } public ISwapBuilder FromHTLC() @@ -187,7 +199,7 @@ public async Task> ProcessAsync(CancellationToken cts = default) var keysetsFees = (await _wallet.GetKeysets(false, cts)).ToDictionary(k=>k.Id, k=>k.InputFee??0); fee = swapInputs.ComputeFee(keysetsFees); } - + var total = CashuUtils.SumProofs(swapInputs); // Swap received proofs to our keyset @@ -199,12 +211,14 @@ public async Task> ProcessAsync(CancellationToken cts = default) } this._outputs ??= await this._wallet.CreateOutputs(amounts, _keysetId, cts); - + var request = new PostSwapRequest() { Inputs = swapInputs.ToArray(), Outputs = this._outputs.BlindedMessages, }; + + await _maybeProcessP2Pk(); var swapResponse = await mintApi.Swap(request, cts); @@ -213,7 +227,7 @@ public async Task> ProcessAsync(CancellationToken cts = default) return swappedProofs; } - + private async Task> _getSwapProofs(CancellationToken cts = default) { _proofsToSwap ??= new(); @@ -249,6 +263,41 @@ private async Task> _getSwapProofs(CancellationToken cts = default) return _proofsToSwap; } + private async Task _maybeProcessP2Pk() + { + if (_privKeys == null || _privKeys.Count == 0) + { + return; + } + + if (_proofsToSwap == null) + { + throw new ArgumentNullException(nameof(_proofsToSwap), "No proofs to swap!"); + } + + var sigAllHandler = new SigAllHandler + { + Proofs = this._proofsToSwap.ToArray(), + BlindedMessages = this._outputs?.BlindedMessages ?? [], + }; + + if (sigAllHandler.TrySign(out P2PKWitness? witness)) + { + if (witness == null) + { + throw new ArgumentNullException(nameof(witness), "sig_all input was correct, but couldn't create a witness signature!"); + } + this._proofsToSwap[0].Witness = JsonSerializer.Serialize(witness); + } + + foreach (var proof in _proofsToSwap) + { + if (proof.Secret is not Nut10Secret { ProofSecret: P2PKProofSecret p2pk }) continue; + var proofWitness = p2pk.GenerateWitness(proof, _privKeys.Select(p => p.Key).ToArray()); + proof.Witness = JsonSerializer.Serialize(proofWitness); + } + } + private async Task> _getAmounts(ulong total, ulong fee, Keyset keys) { if (_amounts != null) diff --git a/DotNut/NUT11/SigAllHandler.cs b/DotNut/NUT11/SigAllHandler.cs new file mode 100644 index 0000000..385f702 --- /dev/null +++ b/DotNut/NUT11/SigAllHandler.cs @@ -0,0 +1,91 @@ +using System.Security.Cryptography; +using System.Text.Encodings.Web; +using System.Text.Json; +using DotNut; +using NBitcoin.Secp256k1; + +namespace DotNut; + +public class SigAllHandler +{ + public Proof[] Proofs { get; set; } + public PrivKey[] PrivKeys { get; set; } + public BlindedMessage[] BlindedMessages { get; set; } + + public string? MeltQuoteId { get; set; } + + private P2PKProofSecret? _firstProofSecret; + + public bool TrySign(out P2PKWitness? p2pkwitness) + { + p2pkwitness = null; + + if (BlindedMessages.Length == 0) + { + return false; + } + + if (_validateFirstProof() == false) + { + return false; + } + + string message = ""; + + if (Proofs.Length > 0) + { + for (var i = 1; i < Proofs.Length; i++) + { + var p = Proofs[i]; + + if (p.Secret is not Nut10Secret { ProofSecret: P2PKProofSecret p2pk }) + { + throw new ArgumentException($"When signing sig_all, every proof must be sig_all."); + } + + if (!_checkIfEqualToFirst(p2pk)) + { + throw new ArgumentException($"When signing sig_all, every proof must have identical tags and data."); + } + message += JsonSerializer.Serialize(p.Secret); + } + } + + foreach (var b in BlindedMessages) + { + message += b.B_.ToString(); + } + + if (MeltQuoteId is not null) + { + message += MeltQuoteId; + } + var bytesMsg = System.Text.Encoding.UTF8.GetBytes(message); + + p2pkwitness = _firstProofSecret!.GenerateWitness(bytesMsg, PrivKeys.Select(pk => (ECPrivKey)pk).ToArray()); + return true; + } + + + + private bool _validateFirstProof() + { + if (Proofs[0].Secret is not Nut10Secret { ProofSecret: P2PKProofSecret p2pks }) + { + return false; + } + var b = P2PkBuilder.Load(p2pks); + if (b.SigFlag != "SIG_ALL") + { + return false; + } + this._firstProofSecret = p2pks; + + return true; + } + private bool _checkIfEqualToFirst(P2PKProofSecret other) => + _firstProofSecret is { } a && other is { } b && + a.Data == b.Data && + ((a.Tags == null && b.Tags == null) || + (a.Tags != null && b.Tags != null && a.Tags.SequenceEqual(b.Tags))); +} \ No newline at end of file From 06f5564c5b4f82a24b16b8e37c3980ca706c009f Mon Sep 17 00:00:00 2001 From: d4rp4t Date: Thu, 16 Oct 2025 01:34:17 +0200 Subject: [PATCH 10/70] P2PK --- DotNut/Abstractions/CashuUtils.cs | 26 ++++++- .../Handlers/MeltHandlerBolt11.cs | 2 +- .../Handlers/MintHandlerBolt11.cs | 39 ++-------- .../Handlers/MintHandlerBolt12.cs | 25 +++--- .../Interfaces/IMintQuoteBuilder.cs | 3 +- DotNut/Abstractions/MeltQuoteBuilder.cs | 47 +++++++++++ DotNut/Abstractions/MintQuoteBuilder.cs | 78 ++++++++++++++++--- DotNut/Abstractions/OutputData.cs | 6 +- DotNut/Abstractions/RestoreBuilder.cs | 6 +- DotNut/Abstractions/SwapBuilder.cs | 61 +++++++++++---- DotNut/Abstractions/Wallet.cs | 13 ++-- DotNut/NUT11/SigAllHandler.cs | 24 +++--- DotNut/NUT13/Nut13.cs | 6 +- 13 files changed, 232 insertions(+), 104 deletions(-) diff --git a/DotNut/Abstractions/CashuUtils.cs b/DotNut/Abstractions/CashuUtils.cs index 7c57009..c163345 100644 --- a/DotNut/Abstractions/CashuUtils.cs +++ b/DotNut/Abstractions/CashuUtils.cs @@ -130,9 +130,29 @@ public static OutputData CreateOutputs( return new OutputData() { - BlindingFactors = blindingFactors.ToArray(), - BlindedMessages = blindedMessages.ToArray(), - Secrets = secrets.ToArray() + BlindingFactors = blindingFactors, + BlindedMessages = blindedMessages, + Secrets = secrets + }; + } + + public static OutputData CreateP2PkOutput( + ulong amount, + KeysetId keysetId, + Keyset keys, + P2PkBuilder builder + ) + { + var proofSecret = builder.Build(); + var secret = new Nut10Secret("P2PK", proofSecret); + + var r = RandomPrivkey(); + var B_ = DotNut.Cashu.ComputeB_(secret.ToCurve(), r); + return new OutputData() + { + BlindedMessages = [new BlindedMessage() { Amount = amount, B_ = B_, Id = keysetId }], + BlindingFactors = [r], + Secrets = [secret] }; } diff --git a/DotNut/Abstractions/Handlers/MeltHandlerBolt11.cs b/DotNut/Abstractions/Handlers/MeltHandlerBolt11.cs index 244f438..226a16f 100644 --- a/DotNut/Abstractions/Handlers/MeltHandlerBolt11.cs +++ b/DotNut/Abstractions/Handlers/MeltHandlerBolt11.cs @@ -29,7 +29,7 @@ public async Task> Melt(List inputs, CancellationToken cts = { Quote = _quote.Quote, Inputs = inputs.ToArray(), - Outputs = _blankOutputs.BlindedMessages + Outputs = _blankOutputs.BlindedMessages.ToArray(), }; var res = await client.Melt("bolt11", req, cts); diff --git a/DotNut/Abstractions/Handlers/MintHandlerBolt11.cs b/DotNut/Abstractions/Handlers/MintHandlerBolt11.cs index 8334ae1..aa17354 100644 --- a/DotNut/Abstractions/Handlers/MintHandlerBolt11.cs +++ b/DotNut/Abstractions/Handlers/MintHandlerBolt11.cs @@ -11,11 +11,7 @@ public class MintHandlerBolt11: IMintHandler? _amounts; - private OutputData? _outputs; + private readonly OutputData _outputs; private string? SubscriptionId; private WebsocketService? _websocketService; @@ -23,50 +19,25 @@ public class MintHandlerBolt11: IMintHandler? amounts, ulong? amount) - { - this._quote = postMintQuoteBolt11Response; - this._keyset = verifiedKeyset; - this._amounts = amounts; - this._amount = amount; - } - - public async Task GetQuote(CancellationToken cts = default) => _quote; public async Task> Mint(CancellationToken cts = default) { var client = await this._wallet.GetMintApi(); - _amount ??= _quote.Amount ?? throw new ArgumentNullException(nameof(_quote.Amount), "Can't determine amount of quote!"); - - _amounts??= CashuUtils.SplitToProofsAmounts(_amount.Value, _keyset.Keys); - - this._outputs ??= await _wallet.CreateOutputs(_amounts!, _keyset.Id, cts); - var req = new PostMintRequest { - Outputs = this._outputs.BlindedMessages, + Outputs = this._outputs.BlindedMessages.ToArray(), Quote = _quote.Quote }; diff --git a/DotNut/Abstractions/Handlers/MintHandlerBolt12.cs b/DotNut/Abstractions/Handlers/MintHandlerBolt12.cs index 9fc70e5..fd94545 100644 --- a/DotNut/Abstractions/Handlers/MintHandlerBolt12.cs +++ b/DotNut/Abstractions/Handlers/MintHandlerBolt12.cs @@ -10,38 +10,33 @@ public class MintHandlerBolt12: IMintHandler? _amounts; + private readonly PostMintQuoteBolt12Response _quote; + private readonly GetKeysResponse.KeysetItemResponse _keyset; + private readonly OutputData _outputs; + + private string? SubscriptionId; + private WebsocketService? _websocketService; - public MintHandlerBolt12(Wallet wallet, PostMintQuoteBolt12Response quote, GetKeysResponse.KeysetItemResponse keyset) + public MintHandlerBolt12(Wallet wallet, PostMintQuoteBolt12Response quote, GetKeysResponse.KeysetItemResponse keyset, OutputData outputs) { this._wallet = wallet; this._quote = quote; this._keyset = keyset; + this._outputs = outputs; } public async Task GetQuote(CancellationToken cts = default) => this._quote; public async Task> Mint(CancellationToken cts = default) { var client = await this._wallet.GetMintApi(); - - _amount ??= _quote.Amount ?? throw new ArgumentNullException(nameof(_quote.Amount), "Can't determine amount of quote!"); - _amounts??= CashuUtils.SplitToProofsAmounts(_amount.Value, _keyset.Keys); - - this._outputs ??= await _wallet.CreateOutputs(_amounts!, _keyset.Id, cts); - var req = new PostMintRequest { - Outputs = this._outputs.BlindedMessages, + Outputs = this._outputs.BlindedMessages.ToArray(), Quote = _quote.Quote }; - var promises= await client.Mint("bolt11", req, cts); + var promises= await client.Mint("bolt12", req, cts); return CashuUtils.ConstructProofsFromPromises(promises.Signatures.ToList(), _outputs, _keyset.Keys); } diff --git a/DotNut/Abstractions/Interfaces/IMintQuoteBuilder.cs b/DotNut/Abstractions/Interfaces/IMintQuoteBuilder.cs index c0195a8..c280cf4 100644 --- a/DotNut/Abstractions/Interfaces/IMintQuoteBuilder.cs +++ b/DotNut/Abstractions/Interfaces/IMintQuoteBuilder.cs @@ -11,7 +11,8 @@ public interface IMintQuoteBuilder IMintQuoteBuilder WithUnit(string unit); IMintQuoteBuilder WithAmount(ulong amount); IMintQuoteBuilder WithOutputs(OutputData outputs); - // Task ProcessAsync(CancellationToken cancellationToken = default); + + IMintQuoteBuilder WithP2PkLock(P2PkBuilder p2pkBuilder); Task>> ProcessAsyncBolt11(CancellationToken cancellationToken = default); Task>> ProcessAsyncBolt12(CancellationToken cancellationToken = default); diff --git a/DotNut/Abstractions/MeltQuoteBuilder.cs b/DotNut/Abstractions/MeltQuoteBuilder.cs index 0af5aed..82f9e22 100644 --- a/DotNut/Abstractions/MeltQuoteBuilder.cs +++ b/DotNut/Abstractions/MeltQuoteBuilder.cs @@ -1,3 +1,4 @@ +using System.Text.Json; using DotNut.Abstractions.Handlers; using DotNut.Abstractions.Interfaces; using DotNut.ApiModels; @@ -14,6 +15,7 @@ class MeltQuoteBuilder : IMeltQuoteBuilder private ulong? _amount; private string _unit = "sat"; + private List? _privKeys; public MeltQuoteBuilder(Wallet wallet) { _wallet = wallet; @@ -65,6 +67,14 @@ public IMeltQuoteBuilder WithAmount(ulong amount) this._amount = amount; return this; } + + // when proofs were p2pk + public IMeltQuoteBuilder WithPrivkeys(IEnumerable privKeys) + { + this._privKeys = privKeys.ToList(); + return this; + } + public async Task>> ProcessAsyncBolt11(CancellationToken cts = default) { @@ -91,9 +101,46 @@ public async Task>> Proces this._blankOutputs = await this._wallet.CreateOutputs(amounts, this._unit, cts); } + await _maybeProcessP2Pk(quote.Quote); + return new MeltHandlerBolt11(_wallet, quote, _blankOutputs); } + private async Task _maybeProcessP2Pk(string quoteId) + { + if (_privKeys == null || _privKeys.Count == 0) + { + return; + } + + if (_proofs == null) + { + throw new ArgumentNullException(nameof(_proofs), "No proofs to melt!"); + } + + var sigAllHandler = new SigAllHandler + { + Proofs = this._proofs, + BlindedMessages = this._blankOutputs?.BlindedMessages ?? [], + MeltQuoteId = quoteId + }; + + if (sigAllHandler.TrySign(out P2PKWitness? witness)) + { + if (witness == null) + { + throw new ArgumentNullException(nameof(witness), "sig_all input was correct, but couldn't create a witness signature!"); + } + this._proofs[0].Witness = JsonSerializer.Serialize(witness); + } + + foreach (var proof in _proofs) + { + if (proof.Secret is not Nut10Secret { ProofSecret: P2PKProofSecret p2pk }) continue; + var proofWitness = p2pk.GenerateWitness(proof, _privKeys.Select(p => p.Key).ToArray()); + proof.Witness = JsonSerializer.Serialize(proofWitness); + } + } public async Task>> ProcessAsyncBolt12( CancellationToken cts = default) diff --git a/DotNut/Abstractions/MintQuoteBuilder.cs b/DotNut/Abstractions/MintQuoteBuilder.cs index c046fcb..53cd672 100644 --- a/DotNut/Abstractions/MintQuoteBuilder.cs +++ b/DotNut/Abstractions/MintQuoteBuilder.cs @@ -9,7 +9,9 @@ namespace DotNut.Abstractions; class MintQuoteBuilder : IMintQuoteBuilder { private readonly Wallet _wallet; + private ulong? _amount; + private List? _amounts; private string _unit = "sat"; private string? _description; private OutputData? _outputs; @@ -20,6 +22,9 @@ class MintQuoteBuilder : IMintQuoteBuilder private KeysetId? _keysetId; private GetKeysResponse.KeysetItemResponse? _keyset; + + //for p2pk + private P2PkBuilder? _builder; public MintQuoteBuilder(Wallet wallet) { @@ -95,6 +100,20 @@ public IMintQuoteBuilder WithOutputs(OutputData outputs) return this; } + /// + /// Optional. + /// User may provide p2pkbuilder specifying p2pk lock parameters. Nonce from builder will be added _only_ to first proof, + /// since it has to be unique for each proof. + /// P2Pk proofs aren't derived deterministicly, since they can't get restored from seed and they would make restore process longer. + /// + /// + /// + public IMintQuoteBuilder WithP2PkLock(P2PkBuilder p2pkBuilder) + { + this._builder = p2pkBuilder; + return this; + } + /// /// Optional. /// User may provide description for melt quote invoice. @@ -106,12 +125,7 @@ public IMintQuoteBuilder WithDescription(string description) this._description = description; return this; } - - public IMintQuoteBuilder WithP2PK() - { - throw new NotImplementedException(); - } - + public IMintQuoteBuilder WithHTLC() { throw new NotImplementedException(); @@ -146,6 +160,9 @@ public async Task>> Proces throw new ArgumentException($"Cant get keys for keysetId: {_keysetId}"); } + var outputs = await this._createOutputs(); + + var reqBolt11 = new PostMintQuoteBolt11Request() { Amount = this._amount.Value, @@ -156,13 +173,13 @@ public async Task>> Proces await (await this._wallet.GetMintApi()) .CreateMintQuote("bolt11", reqBolt11, cts); - return new MintHandlerBolt11(this._wallet, quoteBolt11, this._keyset); + return new MintHandlerBolt11(this._wallet, quoteBolt11, this._keyset, outputs); } public async Task>> ProcessAsyncBolt12( CancellationToken cts = default) { - await this._wallet._maybeSyncKeys(cts); + await this._wallet._maybeSyncKeys(cts); if (this._pubkey == null) { throw new ArgumentNullException(nameof(_pubkey), "Can't request bolt12 mint quote without pubkey!"); @@ -173,6 +190,9 @@ public async Task>> Proces this._keyset = await this._wallet.GetKeys(this._keysetId, false, cts) ?? throw new ArgumentException($"Cant fetch keys for keysetId: {_keysetId}"); } + + var outputs = await this._createOutputs(); + var req = new PostMintQuoteBolt12Request() { @@ -185,7 +205,47 @@ public async Task>> Proces await (await _wallet.GetMintApi()) .CreateMintQuote("bolt12", req, cts); - return new MintHandlerBolt12(this._wallet, mintQuote, this._keyset); + return new MintHandlerBolt12(this._wallet, mintQuote, this._keyset, outputs); + + } + + async Task _createOutputs() + { + var outputs = new OutputData(); + + if (this._outputs != null) + { + if (this._builder is not null) + { + throw new ArgumentException("Can't create p2pk outputs if outputs provided. Remove either p2pk builder parameter or outputs."); + } + return this._outputs; + } + + if (this._amount is null && this._amounts is null) + { + throw new ArgumentNullException(nameof(_amount), "Amount can't be determined. Make sure to include amount, or amounts parameter!"); + } + _amounts ??= CashuUtils.SplitToProofsAmounts(_amount.Value, _keyset!.Keys); + + var createdOutputs = new List(); + if (this._builder is not null) + { + // skipped checks for keysetid and keys, since its validated before. make sure to remember about it. + foreach (var amount in _amounts) + { + var p2pkOutput = CashuUtils.CreateP2PkOutput(amount, this._keysetId!, this._keyset.Keys, _builder); + outputs.BlindingFactors.Add(p2pkOutput.BlindingFactors[0]); + outputs.BlindedMessages.Add(p2pkOutput.BlindedMessages[0]); + outputs.Secrets.Add(p2pkOutput.Secrets[0]); + } + return outputs; + } + + return await _wallet.CreateOutputs(_amounts, this._keysetId!); + + + } } \ No newline at end of file diff --git a/DotNut/Abstractions/OutputData.cs b/DotNut/Abstractions/OutputData.cs index f359313..98483fd 100644 --- a/DotNut/Abstractions/OutputData.cs +++ b/DotNut/Abstractions/OutputData.cs @@ -2,7 +2,7 @@ namespace DotNut; public class OutputData { - public BlindedMessage[] BlindedMessages { get; set; } - public ISecret[] Secrets { get; set; } - public PrivKey[] BlindingFactors { get; set; } + public List BlindedMessages { get; set; } = []; + public List Secrets { get; set; } = []; + public List BlindingFactors { get; set; } = []; } \ No newline at end of file diff --git a/DotNut/Abstractions/RestoreBuilder.cs b/DotNut/Abstractions/RestoreBuilder.cs index a45bdae..b0e2c62 100644 --- a/DotNut/Abstractions/RestoreBuilder.cs +++ b/DotNut/Abstractions/RestoreBuilder.cs @@ -66,7 +66,7 @@ public async Task> ProcessAsync(CancellationToken cts = defau await counter!.IncrementCounter(keysetId, batchNumber * 100); var req = new PostRestoreRequest { - Outputs = outputs.BlindedMessages + Outputs = outputs.BlindedMessages.ToArray() }; var res = await api.Restore(req, cts); @@ -102,12 +102,12 @@ public async Task> ProcessAsync(CancellationToken cts = defau var amounts = CashuUtils.SplitToProofsAmounts(totalAmount, correspondingKeys.Keys); var ctr = await counter!.GetCounterForId(unitKeyset.Value, cts); var newOutputs = CashuUtils.CreateOutputs(amounts, unitKeyset.Value, correspondingKeys.Keys, mnemonic, ctr); - await counter.IncrementCounter(unitKeyset.Value, newOutputs.BlindedMessages.Length, cts); + await counter.IncrementCounter(unitKeyset.Value, newOutputs.BlindedMessages.Count, cts); var swapRequest = new PostSwapRequest { Inputs = recoveredProofs.ToArray(), - Outputs = newOutputs.BlindedMessages, + Outputs = newOutputs.BlindedMessages.ToArray(), }; var swapResult = await api.Swap(swapRequest, cts); diff --git a/DotNut/Abstractions/SwapBuilder.cs b/DotNut/Abstractions/SwapBuilder.cs index 54f0e34..005f094 100644 --- a/DotNut/Abstractions/SwapBuilder.cs +++ b/DotNut/Abstractions/SwapBuilder.cs @@ -18,6 +18,8 @@ class SwapBuilder : ISwapBuilder private OutputData? _outputs; private List? _amounts; private KeysetId? _keysetId; + + private string _unit = "sat"; private bool _verifySignatures = true; @@ -138,7 +140,8 @@ public ISwapBuilder WithPrivkeys(IEnumerable privKeys) /// /// Optional. - /// If provided, every proof will be generated with random nonce. + /// If provided, every proof will be generated with random nonce. + /// P2Pk tokens aren't deterministic. if lost - ¯\_(ツ)_/¯ /// /// /// @@ -205,17 +208,12 @@ public async Task> ProcessAsync(CancellationToken cts = default) // Swap received proofs to our keyset var amounts = await _getAmounts(total, fee, keysForCurrentId.Keys); - if (amounts.Sum() > total - fee) - { - throw new ArgumentException($"Invalid output amounts! Total output amount requested: ${amounts.Sum()}, total input amount: {total}, fee: ${fee}"); - } - - this._outputs ??= await this._wallet.CreateOutputs(amounts, _keysetId, cts); + var outputs = await this._getOutputs(keysForCurrentId.Keys, cts); var request = new PostSwapRequest() { Inputs = swapInputs.ToArray(), - Outputs = this._outputs.BlindedMessages, + Outputs = outputs.BlindedMessages.ToArray(), }; await _maybeProcessP2Pk(); @@ -263,6 +261,42 @@ private async Task> _getSwapProofs(CancellationToken cts = default) return _proofsToSwap; } + async Task _getOutputs(Keyset keys, CancellationToken cts = default) + { + var outputs = new OutputData(); + + if (this._outputs != null) + { + if (this._p2pkBuilder is not null) + { + throw new ArgumentException("Can't create p2pk outputs if outputs provided. Remove either p2pk builder parameter or outputs."); + } + return this._outputs; + } + + if (this._amounts is null) + { + throw new ArgumentNullException(nameof(_amounts), "Amounts can't be null."); + } + + var createdOutputs = new List(); + if (this._p2pkBuilder is not null) + { + // skipped checks for keysetid and keys, since its validated before. make sure to remember about it. + foreach (var amount in _amounts) + { + var p2pkOutput = CashuUtils.CreateP2PkOutput(amount, this._keysetId!, keys, _p2pkBuilder); + outputs.BlindingFactors.Add(p2pkOutput.BlindingFactors[0]); + outputs.BlindedMessages.Add(p2pkOutput.BlindedMessages[0]); + outputs.Secrets.Add(p2pkOutput.Secrets[0]); + } + return outputs; + } + + return await _wallet.CreateOutputs(_amounts, this._keysetId!, cts); + } + + private async Task _maybeProcessP2Pk() { if (_privKeys == null || _privKeys.Count == 0) @@ -277,7 +311,7 @@ private async Task _maybeProcessP2Pk() var sigAllHandler = new SigAllHandler { - Proofs = this._proofsToSwap.ToArray(), + Proofs = this._proofsToSwap, BlindedMessages = this._outputs?.BlindedMessages ?? [], }; @@ -303,17 +337,18 @@ private async Task> _getAmounts(ulong total, ulong fee, Keyset keys) if (_amounts != null) { var sum = _amounts.Sum(); - var underpay = total - fee - sum; - - if (underpay == 0) + + if (sum + fee == total) { return _amounts; } - if (underpay > 0) + if (sum + fee < total) { + var underpay = total - fee - sum; this._amounts.AddRange(CashuUtils.SplitToProofsAmounts(underpay, keys)); return this._amounts; } + throw new ArgumentException($"Invalid amounts requested. Sum of amounts: {sum}, total input: {total}, fee:{fee}."); } diff --git a/DotNut/Abstractions/Wallet.cs b/DotNut/Abstractions/Wallet.cs index ba97762..01e5aa8 100644 --- a/DotNut/Abstractions/Wallet.cs +++ b/DotNut/Abstractions/Wallet.cs @@ -1,13 +1,8 @@ -using System.Diagnostics; -using System.Runtime.CompilerServices; using DotNut.Abstractions.Interfaces; -using DotNut.Abstractions.Quotes; using DotNut.Api; using DotNut.ApiModels; -using DotNut.ApiModels.Melt.bolt12; -using DotNut.ApiModels.Mint.bolt12; +using DotNut.ApiModels.Info; using DotNut.NBitcoin.BIP39; -using DotNut.NUT13; using NBitcoin.Secp256k1; namespace DotNut.Abstractions; @@ -34,6 +29,8 @@ public class Wallet : IWalletBuilder private bool _shouldBumpCounter = true; private bool _allowInvalidKeysetIds = false; + + /* * Fluent Builder Methods */ @@ -197,6 +194,7 @@ public IWalletBuilder ShouldBumpCounter(bool shouldBumpCounter = true) this._shouldBumpCounter = shouldBumpCounter; return this; } + /// /// Optional. /// Allows user to build stateful wallet, by providing a proof manager - a class allowing wallet to fetch, save and use proofs from desired kind of storage. @@ -427,7 +425,7 @@ public async Task CreateOutputs(List amounts, string unit, Ca } return await this.CreateOutputs(amounts, keysetId, cts); } - + public async Task SelectProofsToSend(List proofs, ulong amount, bool includeFees, CancellationToken cts = default) { if (this._selector == null) @@ -457,6 +455,7 @@ public async Task GetMintApi(CancellationToken cts = default) } public Mnemonic? GetMnemonic() => _mnemonic; public ICounter? GetCounter() => _counter; + internal void _ensureApiConnected(string? msg = null) { diff --git a/DotNut/NUT11/SigAllHandler.cs b/DotNut/NUT11/SigAllHandler.cs index 385f702..84d36bf 100644 --- a/DotNut/NUT11/SigAllHandler.cs +++ b/DotNut/NUT11/SigAllHandler.cs @@ -1,4 +1,5 @@ using System.Security.Cryptography; +using System.Text; using System.Text.Encodings.Web; using System.Text.Json; using DotNut; @@ -8,9 +9,9 @@ namespace DotNut; public class SigAllHandler { - public Proof[] Proofs { get; set; } - public PrivKey[] PrivKeys { get; set; } - public BlindedMessage[] BlindedMessages { get; set; } + public List Proofs { get; set; } + public List PrivKeys { get; set; } + public List BlindedMessages { get; set; } public string? MeltQuoteId { get; set; } @@ -20,7 +21,7 @@ public bool TrySign(out P2PKWitness? p2pkwitness) { p2pkwitness = null; - if (BlindedMessages.Length == 0) + if (BlindedMessages.Count == 0) { return false; } @@ -29,12 +30,11 @@ public bool TrySign(out P2PKWitness? p2pkwitness) { return false; } - - string message = ""; + var msg = new StringBuilder(); - if (Proofs.Length > 0) + if (Proofs.Count > 0) { - for (var i = 1; i < Proofs.Length; i++) + for (var i = 1; i < Proofs.Count; i++) { var p = Proofs[i]; @@ -47,20 +47,20 @@ public bool TrySign(out P2PKWitness? p2pkwitness) { throw new ArgumentException($"When signing sig_all, every proof must have identical tags and data."); } - message += JsonSerializer.Serialize(p.Secret); + msg.Append(JsonSerializer.Serialize(p.Secret)); } } foreach (var b in BlindedMessages) { - message += b.B_.ToString(); + msg.Append(b.B_); } if (MeltQuoteId is not null) { - message += MeltQuoteId; + msg.Append(MeltQuoteId); } - var bytesMsg = System.Text.Encoding.UTF8.GetBytes(message); + var bytesMsg = Encoding.UTF8.GetBytes(msg.ToString()); p2pkwitness = _firstProofSecret!.GenerateWitness(bytesMsg, PrivKeys.Select(pk => (ECPrivKey)pk).ToArray()); return true; diff --git a/DotNut/NUT13/Nut13.cs b/DotNut/NUT13/Nut13.cs index 5fb23b5..f68b2cc 100644 --- a/DotNut/NUT13/Nut13.cs +++ b/DotNut/NUT13/Nut13.cs @@ -47,9 +47,9 @@ public static OutputData DeriveOutputs(this Mnemonic mnemonic, IEnumerable Date: Thu, 16 Oct 2025 10:45:03 +0200 Subject: [PATCH 11/70] HTLC --- DotNut/Abstractions/MeltQuoteBuilder.cs | 55 ++++++++++++----------- DotNut/Abstractions/MintQuoteBuilder.cs | 59 +++++++++++-------------- DotNut/Abstractions/SwapBuilder.cs | 32 +++++++++----- DotNut/NUT11/SigAllHandler.cs | 13 +++++- DotNut/NUT14/HTLCBuilder.cs | 1 - 5 files changed, 88 insertions(+), 72 deletions(-) diff --git a/DotNut/Abstractions/MeltQuoteBuilder.cs b/DotNut/Abstractions/MeltQuoteBuilder.cs index 82f9e22..7fc10d7 100644 --- a/DotNut/Abstractions/MeltQuoteBuilder.cs +++ b/DotNut/Abstractions/MeltQuoteBuilder.cs @@ -12,10 +12,11 @@ class MeltQuoteBuilder : IMeltQuoteBuilder private List? _proofs; private string? _invoice; private OutputData? _blankOutputs; - private ulong? _amount; private string _unit = "sat"; - private List? _privKeys; + private List? _privKeys; + private string? _htlcPreimage; + public MeltQuoteBuilder(Wallet wallet) { _wallet = wallet; @@ -56,17 +57,6 @@ public IMeltQuoteBuilder WithBlankOutputs(OutputData blankOutputs) return this; } - /// - /// Mandatory. - /// User needs to specify the amount to be received. It MUST correspond to invoice amount. - /// - /// - /// - public IMeltQuoteBuilder WithAmount(ulong amount) - { - this._amount = amount; - return this; - } // when proofs were p2pk public IMeltQuoteBuilder WithPrivkeys(IEnumerable privKeys) @@ -75,15 +65,19 @@ public IMeltQuoteBuilder WithPrivkeys(IEnumerable privKeys) return this; } + public IMeltQuoteBuilder WithHTLCPreimage(string preimage) + { + this._htlcPreimage = preimage; + return this; + } + public async Task>> ProcessAsyncBolt11(CancellationToken cts = default) { var mintApi = await _wallet.GetMintApi(); await _wallet._maybeSyncKeys(cts); - // ArgumentNullException.ThrowIfNull(this._amount); ArgumentNullException.ThrowIfNull(this._invoice); - var req = new PostMeltQuoteBolt11Request { Request = this._invoice, @@ -101,12 +95,18 @@ public async Task>> Proces this._blankOutputs = await this._wallet.CreateOutputs(amounts, this._unit, cts); } - await _maybeProcessP2Pk(quote.Quote); + await _maybeProcessP2PkHTLC(quote.Quote); return new MeltHandlerBolt11(_wallet, quote, _blankOutputs); } - - private async Task _maybeProcessP2Pk(string quoteId) + + public async Task>> ProcessAsyncBolt12( + CancellationToken cts = default) + { + throw new NotImplementedException(); + } + + private async Task _maybeProcessP2PkHTLC(string quoteId) { if (_privKeys == null || _privKeys.Count == 0) { @@ -122,7 +122,8 @@ private async Task _maybeProcessP2Pk(string quoteId) { Proofs = this._proofs, BlindedMessages = this._blankOutputs?.BlindedMessages ?? [], - MeltQuoteId = quoteId + MeltQuoteId = quoteId, + HTLCPreimage = this._htlcPreimage, }; if (sigAllHandler.TrySign(out P2PKWitness? witness)) @@ -136,16 +137,18 @@ private async Task _maybeProcessP2Pk(string quoteId) foreach (var proof in _proofs) { - if (proof.Secret is not Nut10Secret { ProofSecret: P2PKProofSecret p2pk }) continue; + + if (proof.Secret is not Nut10Secret { ProofSecret: P2PKProofSecret p2pk, Key: { } key }) continue; + if (proof.Secret is Nut10Secret { ProofSecret: HTLCProofSecret htlc } && _htlcPreimage is {} preimage) + { + var w = htlc.GenerateWitness(proof, _privKeys.Select(p=>p.Key).ToArray(), preimage); + proof.Witness = JsonSerializer.Serialize(w); + continue; + } var proofWitness = p2pk.GenerateWitness(proof, _privKeys.Select(p => p.Key).ToArray()); proof.Witness = JsonSerializer.Serialize(proofWitness); } } - - public async Task>> ProcessAsyncBolt12( - CancellationToken cts = default) - { - throw new NotImplementedException(); - } + } diff --git a/DotNut/Abstractions/MintQuoteBuilder.cs b/DotNut/Abstractions/MintQuoteBuilder.cs index 53cd672..5c276c2 100644 --- a/DotNut/Abstractions/MintQuoteBuilder.cs +++ b/DotNut/Abstractions/MintQuoteBuilder.cs @@ -104,7 +104,7 @@ public IMintQuoteBuilder WithOutputs(OutputData outputs) /// Optional. /// User may provide p2pkbuilder specifying p2pk lock parameters. Nonce from builder will be added _only_ to first proof, /// since it has to be unique for each proof. - /// P2Pk proofs aren't derived deterministicly, since they can't get restored from seed and they would make restore process longer. + /// P2Pk proofs aren't derived deterministicly, since they can't get restored from seed and they would make restore process longer. /// /// /// @@ -114,6 +114,12 @@ public IMintQuoteBuilder WithP2PkLock(P2PkBuilder p2pkBuilder) return this; } + public IMintQuoteBuilder WithHTLCLock(HTLCBuilder htlcBuilder) + { + this._builder = htlcBuilder; + return this; + } + /// /// Optional. /// User may provide description for melt quote invoice. @@ -126,11 +132,6 @@ public IMintQuoteBuilder WithDescription(string description) return this; } - public IMintQuoteBuilder WithHTLC() - { - throw new NotImplementedException(); - } - public async Task>> ProcessAsyncBolt11( CancellationToken cts = default) { @@ -142,23 +143,17 @@ public async Task>> Proces throw new ArgumentNullException(nameof(_amount), "can't create melt quote without amount!"); } - var api = this._wallet.GetMintApi(); + var api = await this._wallet.GetMintApi(); if (api is null) { throw new ArgumentNullException(nameof(ICashuApi), "Can't request mint quote without mint API"); } - if (this._keysetId == null) - { - this._keysetId = await this._wallet.GetActiveKeysetId(this._unit, cts) ?? - throw new ArgumentException($"Can't get active keyset ID for unit: {_unit}"); - } + this._keysetId ??= await this._wallet.GetActiveKeysetId(this._unit, cts) ?? + throw new ArgumentException($"Can't get active keyset ID for unit: {_unit}"); - if (this._keyset == null) - { - this._keyset = await this._wallet.GetKeys(this._keysetId, false, cts) ?? - throw new ArgumentException($"Cant get keys for keysetId: {_keysetId}"); - } + this._keyset ??= await this._wallet.GetKeys(this._keysetId, false, cts) ?? + throw new ArgumentException($"Cant get keys for keysetId: {_keysetId}"); var outputs = await this._createOutputs(); @@ -170,8 +165,7 @@ public async Task>> Proces Description = this._description, }; var quoteBolt11 = - await (await this._wallet.GetMintApi()) - .CreateMintQuote("bolt11", reqBolt11, + await api.CreateMintQuote("bolt11", reqBolt11, cts); return new MintHandlerBolt11(this._wallet, quoteBolt11, this._keyset, outputs); } @@ -229,23 +223,20 @@ async Task _createOutputs() } _amounts ??= CashuUtils.SplitToProofsAmounts(_amount.Value, _keyset!.Keys); - var createdOutputs = new List(); - if (this._builder is not null) + if (this._builder is null) { - // skipped checks for keysetid and keys, since its validated before. make sure to remember about it. - foreach (var amount in _amounts) - { - var p2pkOutput = CashuUtils.CreateP2PkOutput(amount, this._keysetId!, this._keyset.Keys, _builder); - outputs.BlindingFactors.Add(p2pkOutput.BlindingFactors[0]); - outputs.BlindedMessages.Add(p2pkOutput.BlindedMessages[0]); - outputs.Secrets.Add(p2pkOutput.Secrets[0]); - } - return outputs; + return await _wallet.CreateOutputs(_amounts, this._keysetId!); } - return await _wallet.CreateOutputs(_amounts, this._keysetId!); - - - + // skipped checks for keysetid and keys, since its validated before. make sure to remember about it. + foreach (var amount in _amounts) + { + var p2pkOutput = CashuUtils.CreateP2PkOutput(amount, this._keysetId!, this._keyset.Keys, _builder); + outputs.BlindingFactors.Add(p2pkOutput.BlindingFactors[0]); + outputs.BlindedMessages.Add(p2pkOutput.BlindedMessages[0]); + outputs.Secrets.Add(p2pkOutput.Secrets[0]); + } + return outputs; + } } \ No newline at end of file diff --git a/DotNut/Abstractions/SwapBuilder.cs b/DotNut/Abstractions/SwapBuilder.cs index 005f094..662b68c 100644 --- a/DotNut/Abstractions/SwapBuilder.cs +++ b/DotNut/Abstractions/SwapBuilder.cs @@ -27,7 +27,9 @@ class SwapBuilder : ISwapBuilder //p2pk stuff private List? _privKeys; - private P2PkBuilder? _p2pkBuilder; + private P2PkBuilder? _builder; + + private string? _htlcPreimage; public SwapBuilder(Wallet wallet, string tokenString) { @@ -148,18 +150,20 @@ public ISwapBuilder WithPrivkeys(IEnumerable privKeys) /// public ISwapBuilder ToP2PK(P2PkBuilder p2pkBuilder) { - this._p2pkBuilder = p2pkBuilder; + this._builder = p2pkBuilder; return this; } - public ISwapBuilder FromHTLC() + public ISwapBuilder WithHtlcPreimage(string preimage) { - throw new NotImplementedException(); + this._htlcPreimage = preimage; + return this; } - public ISwapBuilder ToHTLC() + public ISwapBuilder ToHTLC(HTLCBuilder htlcBuilder) { - throw new NotImplementedException(); + this._builder = htlcBuilder; + return this; } public async Task> ProcessAsync(CancellationToken cts = default) @@ -267,7 +271,7 @@ async Task _getOutputs(Keyset keys, CancellationToken cts = default) if (this._outputs != null) { - if (this._p2pkBuilder is not null) + if (this._builder is not null) { throw new ArgumentException("Can't create p2pk outputs if outputs provided. Remove either p2pk builder parameter or outputs."); } @@ -280,12 +284,12 @@ async Task _getOutputs(Keyset keys, CancellationToken cts = default) } var createdOutputs = new List(); - if (this._p2pkBuilder is not null) + if (this._builder is not null) { // skipped checks for keysetid and keys, since its validated before. make sure to remember about it. foreach (var amount in _amounts) { - var p2pkOutput = CashuUtils.CreateP2PkOutput(amount, this._keysetId!, keys, _p2pkBuilder); + var p2pkOutput = CashuUtils.CreateP2PkOutput(amount, this._keysetId!, keys, _builder); outputs.BlindingFactors.Add(p2pkOutput.BlindingFactors[0]); outputs.BlindedMessages.Add(p2pkOutput.BlindedMessages[0]); outputs.Secrets.Add(p2pkOutput.Secrets[0]); @@ -313,6 +317,7 @@ private async Task _maybeProcessP2Pk() { Proofs = this._proofsToSwap, BlindedMessages = this._outputs?.BlindedMessages ?? [], + HTLCPreimage = this._htlcPreimage, }; if (sigAllHandler.TrySign(out P2PKWitness? witness)) @@ -326,7 +331,14 @@ private async Task _maybeProcessP2Pk() foreach (var proof in _proofsToSwap) { - if (proof.Secret is not Nut10Secret { ProofSecret: P2PKProofSecret p2pk }) continue; + + if (proof.Secret is not Nut10Secret { ProofSecret: P2PKProofSecret p2pk, Key: { } key }) continue; + if (proof.Secret is Nut10Secret { ProofSecret: HTLCProofSecret htlc } && _htlcPreimage is {} preimage) + { + var w = htlc.GenerateWitness(proof, _privKeys.Select(p=>p.Key).ToArray(), preimage); + proof.Witness = JsonSerializer.Serialize(w); + continue; + } var proofWitness = p2pk.GenerateWitness(proof, _privKeys.Select(p => p.Key).ToArray()); proof.Witness = JsonSerializer.Serialize(proofWitness); } diff --git a/DotNut/NUT11/SigAllHandler.cs b/DotNut/NUT11/SigAllHandler.cs index 84d36bf..3c67aac 100644 --- a/DotNut/NUT11/SigAllHandler.cs +++ b/DotNut/NUT11/SigAllHandler.cs @@ -7,12 +7,15 @@ namespace DotNut; +//Handles both P2PK and HTLC (if preimage added) public class SigAllHandler { public List Proofs { get; set; } public List PrivKeys { get; set; } public List BlindedMessages { get; set; } + public string? HTLCPreimage { get; set; } + public string? MeltQuoteId { get; set; } private P2PKProofSecret? _firstProofSecret; @@ -61,7 +64,15 @@ public bool TrySign(out P2PKWitness? p2pkwitness) msg.Append(MeltQuoteId); } var bytesMsg = Encoding.UTF8.GetBytes(msg.ToString()); - + + if (_firstProofSecret is HTLCProofSecret s && HTLCPreimage is {} preimage) + { + p2pkwitness = + s.GenerateWitness(bytesMsg, PrivKeys.Select(pk => (ECPrivKey)pk).ToArray(), + Encoding.UTF8.GetBytes(preimage) + ); + return true; + } p2pkwitness = _firstProofSecret!.GenerateWitness(bytesMsg, PrivKeys.Select(pk => (ECPrivKey)pk).ToArray()); return true; } diff --git a/DotNut/NUT14/HTLCBuilder.cs b/DotNut/NUT14/HTLCBuilder.cs index f236a71..5fb03bb 100644 --- a/DotNut/NUT14/HTLCBuilder.cs +++ b/DotNut/NUT14/HTLCBuilder.cs @@ -41,7 +41,6 @@ public static HTLCBuilder Load(HTLCProofSecret proofSecret) SigFlag = innerbuilder.SigFlag, Nonce = innerbuilder.Nonce }; - } public new HTLCProofSecret Build() From ae20f3459e6d342abfc52d42d04f5283c096161b Mon Sep 17 00:00:00 2001 From: d4rp4t Date: Fri, 17 Oct 2025 16:07:10 +0200 Subject: [PATCH 12/70] wip --- DotNut.Tests/Integration.cs | 208 ++++++++++- DotNut/Abstractions/CashuUtils.cs | 4 +- .../Handlers/MeltHandlerBolt11.cs | 16 +- .../Handlers/MeltHandlerBolt12.cs | 8 +- .../Handlers/MintHandlerBolt11.cs | 11 +- .../Handlers/MintHandlerBolt12.cs | 11 +- DotNut/Abstractions/InMemoryCounter.cs | 8 +- DotNut/Abstractions/InMemoryProofManager.cs | 8 +- DotNut/Abstractions/Interfaces/ICounter.cs | 6 +- .../Abstractions/Interfaces/IMeltHandler.cs | 6 +- .../Interfaces/IMeltQuoteBuilder.cs | 7 +- .../Abstractions/Interfaces/IMintHandler.cs | 5 +- .../Interfaces/IMintQuoteBuilder.cs | 6 +- .../Abstractions/Interfaces/IProofManager.cs | 8 +- .../Abstractions/Interfaces/IProofSelector.cs | 2 +- .../Interfaces/IRestoreBuilder.cs | 2 +- .../Abstractions/Interfaces/ISwapBuilder.cs | 9 +- .../Abstractions/Interfaces/IWalletBuilder.cs | 25 +- .../Interfaces/IWebsocketService.cs | 16 +- DotNut/Abstractions/MeltQuoteBuilder.cs | 25 +- DotNut/Abstractions/MintQuoteBuilder.cs | 18 +- DotNut/Abstractions/ProofSelector.cs | 2 +- DotNut/Abstractions/RestoreBuilder.cs | 22 +- DotNut/Abstractions/SwapBuilder.cs | 60 ++-- DotNut/Abstractions/Wallet.cs | 106 +++--- DotNut/Abstractions/WebsocketService.cs | 294 --------------- .../Websockets/NotificationParser.cs | 7 +- .../Websockets/NotificationPayloads.cs | 56 --- .../Abstractions/Websockets/Subscription.cs | 20 ++ ...cketEntities.cs => WebsocketConnection.cs} | 18 +- .../Abstractions/Websockets/WebsocketEnums.cs | 10 +- .../Websockets/WebsocketModels.cs | 2 +- .../Websockets/WebsocketService.cs | 340 ++++++++++++++++++ .../Websockets/WebsocketServiceExtensions.cs | 56 +-- DotNut/Api/CashuHttpClient.cs | 6 + DotNut/Api/ICashuApi.cs | 1 + 36 files changed, 811 insertions(+), 598 deletions(-) delete mode 100644 DotNut/Abstractions/WebsocketService.cs delete mode 100644 DotNut/Abstractions/Websockets/NotificationPayloads.cs create mode 100644 DotNut/Abstractions/Websockets/Subscription.cs rename DotNut/Abstractions/Websockets/{WebsocketEntities.cs => WebsocketConnection.cs} (56%) create mode 100644 DotNut/Abstractions/Websockets/WebsocketService.cs diff --git a/DotNut.Tests/Integration.cs b/DotNut.Tests/Integration.cs index 4852bbb..3dba755 100644 --- a/DotNut.Tests/Integration.cs +++ b/DotNut.Tests/Integration.cs @@ -1,6 +1,11 @@ +using System.Security.Cryptography; using DotNut.Abstractions; using DotNut.Abstractions.Interfaces; +using DotNut.Abstractions.Websockets; using DotNut.Api; +using DotNut.ApiModels; +using Newtonsoft.Json; +using NuGet.Frameworks; using Xunit.Sdk; namespace DotNut.Tests; @@ -14,7 +19,9 @@ public class Integration private static readonly Dictionary valuesInvoices = new Dictionary() { - {1000, "lnbc10u1p5w6vggsp5gn5xhswgn5299w6elu2z0vzjxhf9hwd6pwjcgfwphaxunyu0dx6spp5a60trrhce2u6tzqjwjczem8rpdesgzkawcqg2xqaesz2kd50z4uqhp5uwcvgs5clswpfxhm7nyfjmaeysn6us0yvjdexn9yjkv3k7zjhp2sxq9z0rgqcqpnrzjqw22kc09xj0dm65ew5h5r003vtn72eyzchdgjag66l0yhwdudfmuzrwvesqq8qgqqgqqqqqqqqqqzhsq2q9qxpqysgq955gcfr95wwz0ehtnk3xraatkyhj88z44ku7yqutnwnt3gkh82jxehvdff7n2js2p54jgpvg6dmwmq8t9d8x05j63mqjrsr4cwd4lpcpnc39ru"} + {500, "lnbc5u1p50xs65sp5rqewm2jqcddhnynncdx7gtz8qh7q6c9a2tlv6u2efa5qrltla9jqpp5raqnnlucn27y3lswuqafutrnsctcglr5ldv74009jp86cfv6pjyqhp5fszwn06y05csgs2mnn7yn6kn6j9d7m5fv6rw72m8hkp7re0zfflqxq9z0rgqcqpnrzjqdq8jm79ttkfnk83gfjee4n7ryyqzq9f36s5azgk2ftcndt7q48txr0hdyqqdcgqqqqqqqlgqqqqzycqyg9qxpqysgqthz50sp4xdtv2afwj294fd45e4s8q4ptqrn092v36zrs57wyur65lcdkxp53cza9an8z0drxw5lgdcay78plgmfle72vrtjp5266xlgqzsn4ph"}, + {1000, "lnbc10u1p5w6vggsp5gn5xhswgn5299w6elu2z0vzjxhf9hwd6pwjcgfwphaxunyu0dx6spp5a60trrhce2u6tzqjwjczem8rpdesgzkawcqg2xqaesz2kd50z4uqhp5uwcvgs5clswpfxhm7nyfjmaeysn6us0yvjdexn9yjkv3k7zjhp2sxq9z0rgqcqpnrzjqw22kc09xj0dm65ew5h5r003vtn72eyzchdgjag66l0yhwdudfmuzrwvesqq8qgqqgqqqqqqqqqqzhsq2q9qxpqysgq955gcfr95wwz0ehtnk3xraatkyhj88z44ku7yqutnwnt3gkh82jxehvdff7n2js2p54jgpvg6dmwmq8t9d8x05j63mqjrsr4cwd4lpcpnc39ru"}, + {2000, "lnbc20u1p5094fksp54vrdcymel5awhrpc0m6z4kvhhyvqlwkshkyt2wr6eyljkz8c798qpp59f2vc8td8tu62gtf4qfwzkrkxedsey7a5ajrd48a25z2kkwg407shp5nklhn663zgwcdnh7pe5jxt6td0cchhre6hxzdxrjdlfwtpq60f5sxq9z0rgqcqpnrzjqw0de9yc0j8n4hpgm269tm7qph4gwcyf5ys02uaapvpugrva87c7zr045uqq4jsqpsqqqqlgqqqqrcgq2q9qxpqysgq6g2pamgjumh6uw5k5rj2ket44wh8nfzs5gzyygl54hu5cefuxdhxp9h5mrg64rh07znktn9x9d5vg6fc0rw7m63x8rg4qk3kw6d8sycpywn48m"}, }; private static ICounter counter = new InMemoryCounter(); @@ -60,8 +67,7 @@ public async Task MintsSuccessfully() var paymentRequest = (await mintQuote.GetQuote()).Request; Assert.Contains("lnbc1337", paymentRequest); - //We're using fakewallet, so after 3 secs it will get paid automatically - await Task.Delay(3000); + await PayInvoice(); var mintResponse = await mintQuote.Mint(); Assert.NotNull(mintResponse); @@ -87,8 +93,8 @@ public async Task MintsDeterministicSuccessfully() var paymentRequest = (await mintQuote.GetQuote()).Request; Assert.Contains("lnbc1337", paymentRequest); - - await Task.Delay(3000); + + await PayInvoice(); var mintedProofs = await mintQuote.Mint(); var keysetId = mintedProofs.First().Id; @@ -126,8 +132,8 @@ public async Task SwapsSuccessfully() .WithAmount(64) .WithUnit("sat") .ProcessAsyncBolt11(); - - await Task.Delay(3000); + + await PayInvoice(); var mintedProofs = await mintQuote.Mint(); Assert.NotEmpty(mintedProofs); @@ -140,6 +146,7 @@ public async Task SwapsSuccessfully() Assert.NotEmpty(newProofs); } + [Fact] public async Task SwapsDeterministicSuccessfully() { var wallet = Wallet @@ -154,8 +161,8 @@ public async Task SwapsDeterministicSuccessfully() .WithAmount(64) .WithUnit("sat") .ProcessAsyncBolt11(); - - await Task.Delay(3000); + + await PayInvoice(); var mintedProofs = await mintQuote.Mint(); Assert.NotEmpty(mintedProofs); @@ -206,6 +213,189 @@ public async Task MeltsSuccessfully() Assert.NotEmpty(change); } + + [Fact] + public async Task InvoiceWithDescription() + { + var wallet = Wallet + .Create() + .WithMint(MintUrl); + + var quote = await wallet.CreateMintQuote() + .WithDescription("Test Description") + .WithAmount(1337) + .ProcessAsyncBolt11(); + + Assert.NotNull(quote); + } + + [Fact] + public async Task FeeForExternalInvoice() + { + var wallet = Wallet + .Create() + .WithMint(MintUrl); + + var meltHandler = await wallet.CreateMeltQuote() + .WithInvoice(valuesInvoices[2000]) + .ProcessAsyncBolt11(); + + Assert.NotNull(meltHandler); + + var quote = await meltHandler.GetQuote(); + + Assert.NotNull(quote); + Assert.True(quote.FeeReserve > 0); + } + + [Fact] + public async Task SwapP2Pk() + { + // p2pk aren't deterministic, so wallet is initialized without mnemonic and counter + var wallet = Wallet + .Create() + .WithMint(MintUrl); + + var privKeyAlice = new PrivKey(RandomNumberGenerator.GetHexString(64, true)); + var privKeyBob = new PrivKey(RandomNumberGenerator.GetHexString(64, true)); + + var mintHandler = await wallet.CreateMintQuote() + .WithAmount(1337) + .WithP2PkLock(new P2PkBuilder() + { + Pubkeys = [privKeyBob.Key.CreatePubKey()], + SignatureThreshold = 1 + } + ).ProcessAsyncBolt11(); + + await PayInvoice(); + var proofs = await mintHandler.Mint(); + + // no privkeys + await Assert.ThrowsAsync( + async () => await wallet + .Swap() + .FromInputs(proofs) + .ProcessAsync() + ); + + // wrong privkey + await Assert.ThrowsAsync( + async () => await wallet + .Swap() + .FromInputs(proofs) + .WithPrivkeys([privKeyAlice.Key]) + .ProcessAsync() + ); + + var swappedProofs = await wallet. + Swap() + .FromInputs(proofs) + .WithPrivkeys([privKeyBob]) + .ProcessAsync(); + + Assert.NotEmpty(swappedProofs); + } + + + [Fact] + public async Task MintMeltP2PkMultisig() + { + var wallet = Wallet + .Create() + .WithMint(MintUrl); + + var privKeyAlice = new PrivKey(RandomNumberGenerator.GetHexString(64, true)); + var privKeyBob = new PrivKey(RandomNumberGenerator.GetHexString(64, true)); + + var mintHandler = await wallet.CreateMintQuote() + .WithAmount(1337) + .WithP2PkLock(new P2PkBuilder() + { + Pubkeys = [privKeyBob.Key.CreatePubKey(), privKeyAlice.Key.CreatePubKey()], + SignatureThreshold = 2 + } + ).ProcessAsyncBolt11(); + await PayInvoice(); + + var proofs = await mintHandler.Mint(); + + Assert.NotEmpty(proofs); + + // no privkeys + await Assert.ThrowsAsync(async () => + { + var meltHandler = await wallet + .CreateMeltQuote() + .WithInvoice(valuesInvoices[500]) + .ProcessAsyncBolt11(); + await meltHandler.Melt(proofs); + }); + + var handler = await wallet + .CreateMeltQuote() + .WithInvoice(valuesInvoices[500]) + .WithPrivkeys([privKeyBob, privKeyAlice]) + .ProcessAsyncBolt11(); + + var selectorResponse = await wallet.SelectProofsToSend(proofs, 500UL, true); + var change = await handler.Melt(selectorResponse.Send); + + Assert.NotEmpty(change); + } + + [Fact] + public async Task SubscribeToMintMeltQuoteUpdates() + { + // initialize websocket service. it will be a singleton normally. + WebsocketService service = new WebsocketService(); + var connection = await service.ConnectAsync(MintUrl); + Assert.NotNull(connection); + + // create mint quote + var wallet = Wallet + .Create() + .WithMint(MintUrl); + + var mintHandler = await wallet + .CreateMintQuote() + .WithAmount(3338) + .WithUnit("sat") + .ProcessAsyncBolt11(); + + var quote = await mintHandler.GetQuote(); + + var sub = await service.SubscribeToMintQuoteAsync(MintUrl, [quote.Quote]); + + int ctr = 0; + var callback = () => ctr++; + await foreach (var msg in sub.NotificationChannel.Reader.ReadAllAsync()) + { + callback(); + if (ctr > 1) + { + Assert.Equal(sub.Id, msg.SubId); + Assert.True(msg.Payload is PostMeltQuoteBolt11Response); + break; + } + } + + // payQuote + await PayInvoice(); + + Assert.True(ctr > 1); + + var proofs = await mintHandler.Mint(); + + + } + + + private async Task PayInvoice() + { + //We're using fakewallet, so after 3 secs it will get paid automatically. After 3.5 sec its 1000% paid. + await Task.Delay(3500); + } } diff --git a/DotNut/Abstractions/CashuUtils.cs b/DotNut/Abstractions/CashuUtils.cs index c163345..460ca8d 100644 --- a/DotNut/Abstractions/CashuUtils.cs +++ b/DotNut/Abstractions/CashuUtils.cs @@ -147,7 +147,7 @@ P2PkBuilder builder var secret = new Nut10Secret("P2PK", proofSecret); var r = RandomPrivkey(); - var B_ = DotNut.Cashu.ComputeB_(secret.ToCurve(), r); + var B_ = Cashu.ComputeB_(secret.ToCurve(), r); return new OutputData() { BlindedMessages = [new BlindedMessage() { Amount = amount, B_ = B_, Id = keysetId }], @@ -172,7 +172,7 @@ public static Proof ConstructProofFromPromise( { //unblind signature - var C = DotNut.Cashu.ComputeC(promise.C_, r, amountPubkey); + var C = Cashu.ComputeC(promise.C_, r, amountPubkey); if (promise.DLEQ is not null) { diff --git a/DotNut/Abstractions/Handlers/MeltHandlerBolt11.cs b/DotNut/Abstractions/Handlers/MeltHandlerBolt11.cs index 226a16f..1cf5c25 100644 --- a/DotNut/Abstractions/Handlers/MeltHandlerBolt11.cs +++ b/DotNut/Abstractions/Handlers/MeltHandlerBolt11.cs @@ -9,7 +9,8 @@ public class MeltHandlerBolt11 : IMeltHandler GetQuote(CancellationToken cts = default) => this._quote; - public async Task> Melt(List inputs, CancellationToken cts = default) + public async Task GetQuote(CancellationToken ct = default) => this._quote; + public async Task> Melt(List inputs, CancellationToken ct = default) { var client = await _wallet.GetMintApi(); var req = new PostMeltRequest @@ -32,18 +33,13 @@ public async Task> Melt(List inputs, CancellationToken cts = Outputs = _blankOutputs.BlindedMessages.ToArray(), }; - var res = await client.Melt("bolt11", req, cts); + var res = await client.Melt("bolt11", req, ct); if (res.Change == null) { return []; } - var keyset = await _wallet.GetKeys(res.Change.First().Id, false, cts); + var keyset = await _wallet.GetKeys(res.Change.First().Id, false, ct); return CashuUtils.ConstructProofsFromPromises(res.Change.ToList(), _blankOutputs, keyset.Keys); } - - public Task Subscribe(CancellationToken cts = default) - { - throw new NotImplementedException(); - } } \ No newline at end of file diff --git a/DotNut/Abstractions/Handlers/MeltHandlerBolt12.cs b/DotNut/Abstractions/Handlers/MeltHandlerBolt12.cs index 743bfff..80a96b2 100644 --- a/DotNut/Abstractions/Handlers/MeltHandlerBolt12.cs +++ b/DotNut/Abstractions/Handlers/MeltHandlerBolt12.cs @@ -6,18 +6,14 @@ namespace DotNut.Abstractions.Handlers; public class MeltHandlerBolt12: IMeltHandler> { - public Task GetQuote(CancellationToken cts = default) + public Task GetQuote(CancellationToken ct = default) { throw new NotImplementedException(); } - public Task> Melt(List inputs, CancellationToken cts = default) + public Task> Melt(List inputs, CancellationToken ct = default) { throw new NotImplementedException(); } - public Task Subscribe(CancellationToken cts = default) - { - throw new NotImplementedException(); - } } \ No newline at end of file diff --git a/DotNut/Abstractions/Handlers/MintHandlerBolt11.cs b/DotNut/Abstractions/Handlers/MintHandlerBolt11.cs index aa17354..2548a50 100644 --- a/DotNut/Abstractions/Handlers/MintHandlerBolt11.cs +++ b/DotNut/Abstractions/Handlers/MintHandlerBolt11.cs @@ -29,9 +29,9 @@ OutputData outputs this._outputs = outputs; } - public async Task GetQuote(CancellationToken cts = default) => _quote; + public async Task GetQuote(CancellationToken ct = default) => _quote; - public async Task> Mint(CancellationToken cts = default) + public async Task> Mint(CancellationToken ct = default) { var client = await this._wallet.GetMintApi(); @@ -41,13 +41,8 @@ public async Task> Mint(CancellationToken cts = default) Quote = _quote.Quote }; - var promises= await client.Mint("bolt11", req, cts); + var promises= await client.Mint("bolt11", req, ct); return CashuUtils.ConstructProofsFromPromises(promises.Signatures.ToList(), _outputs, _keyset.Keys); } - public Task Subscribe(CancellationToken cts = default) - { - throw new NotImplementedException(); - } - } \ No newline at end of file diff --git a/DotNut/Abstractions/Handlers/MintHandlerBolt12.cs b/DotNut/Abstractions/Handlers/MintHandlerBolt12.cs index fd94545..d15ceaa 100644 --- a/DotNut/Abstractions/Handlers/MintHandlerBolt12.cs +++ b/DotNut/Abstractions/Handlers/MintHandlerBolt12.cs @@ -25,8 +25,8 @@ public MintHandlerBolt12(Wallet wallet, PostMintQuoteBolt12Response quote, GetKe this._outputs = outputs; } - public async Task GetQuote(CancellationToken cts = default) => this._quote; - public async Task> Mint(CancellationToken cts = default) + public async Task GetQuote(CancellationToken ct = default) => this._quote; + public async Task> Mint(CancellationToken ct = default) { var client = await this._wallet.GetMintApi(); @@ -36,7 +36,7 @@ public async Task> Mint(CancellationToken cts = default) Quote = _quote.Quote }; - var promises= await client.Mint("bolt12", req, cts); + var promises= await client.Mint("bolt12", req, ct); return CashuUtils.ConstructProofsFromPromises(promises.Signatures.ToList(), _outputs, _keyset.Keys); } @@ -47,9 +47,4 @@ private async Task _processMint(PostMintRequest req, Cancellat return await client.Mint("bolt12", req, cts); } - public Task Subscribe(CancellationToken cts = default) - { - throw new NotImplementedException(); - } - } \ No newline at end of file diff --git a/DotNut/Abstractions/InMemoryCounter.cs b/DotNut/Abstractions/InMemoryCounter.cs index 74dbdef..76c8dc3 100644 --- a/DotNut/Abstractions/InMemoryCounter.cs +++ b/DotNut/Abstractions/InMemoryCounter.cs @@ -16,7 +16,7 @@ public InMemoryCounter() this._counter = new Dictionary(); } - public async Task GetCounterForId(KeysetId keysetId, CancellationToken cts = default) + public async Task GetCounterForId(KeysetId keysetId, CancellationToken ct = default) { if (_counter.TryGetValue(keysetId, out var counter)) return counter; @@ -24,14 +24,14 @@ public async Task GetCounterForId(KeysetId keysetId, CancellationToken cts return _counter[keysetId] = 0; } - public async Task IncrementCounter(KeysetId keysetId, int bumpBy = 1, CancellationToken cts = default) + public async Task IncrementCounter(KeysetId keysetId, int bumpBy = 1, CancellationToken ct = default) { - var current = await GetCounterForId(keysetId, cts); + var current = await GetCounterForId(keysetId, ct); var next = current + bumpBy; _counter[keysetId] = next; return next; } - public async Task SetCounter(KeysetId keysetId, int counter, CancellationToken cts = default) => _counter[keysetId] = counter; + public async Task SetCounter(KeysetId keysetId, int counter, CancellationToken ct = default) => _counter[keysetId] = counter; } \ No newline at end of file diff --git a/DotNut/Abstractions/InMemoryProofManager.cs b/DotNut/Abstractions/InMemoryProofManager.cs index 9f5c0f6..b6c53de 100644 --- a/DotNut/Abstractions/InMemoryProofManager.cs +++ b/DotNut/Abstractions/InMemoryProofManager.cs @@ -4,7 +4,7 @@ public class InMemoryProofManager: IProofManager { private Dictionary> _proofsDictionary = new(); - public async Task AddProofAsync(Proof proof, CancellationToken cts = default) + public async Task AddProofAsync(Proof proof, CancellationToken ct = default) { if (_proofsDictionary.TryGetValue(proof.Id, out var proofs)) { @@ -14,16 +14,16 @@ public async Task AddProofAsync(Proof proof, CancellationToken cts = default) _proofsDictionary.Add(proof.Id, new List { proof }); } - public async Task> GetProofsForKeysetId(KeysetId ids, CancellationToken cts = default) + public async Task> GetProofsForKeysetId(KeysetId ids, CancellationToken ct = default) { return _proofsDictionary.TryGetValue(ids, out var proofs) ? proofs : new List(); } - public Task> GetProofsForMint(List ids, CancellationToken cts = default) + public Task> GetProofsForMint(List ids, CancellationToken ct = default) { throw new NotImplementedException(); } - public Task MarkProofAsSpent(Proof proof, CancellationToken cts = default) + public Task MarkProofAsSpent(Proof proof, CancellationToken ct = default) { throw new NotImplementedException(); } diff --git a/DotNut/Abstractions/Interfaces/ICounter.cs b/DotNut/Abstractions/Interfaces/ICounter.cs index 0f1f5c7..5694225 100644 --- a/DotNut/Abstractions/Interfaces/ICounter.cs +++ b/DotNut/Abstractions/Interfaces/ICounter.cs @@ -2,7 +2,7 @@ namespace DotNut.Abstractions.Interfaces; public interface ICounter { - public Task GetCounterForId(KeysetId keysetId, CancellationToken cts = default); - public Task IncrementCounter(KeysetId keysetId, int bumpBy = 1, CancellationToken cts = default); - public Task SetCounter(KeysetId keysetId, int counter, CancellationToken cts = default); + public Task GetCounterForId(KeysetId keysetId, CancellationToken ct = default); + public Task IncrementCounter(KeysetId keysetId, int bumpBy = 1, CancellationToken ct = default); + public Task SetCounter(KeysetId keysetId, int counter, CancellationToken ct = default); } \ No newline at end of file diff --git a/DotNut/Abstractions/Interfaces/IMeltHandler.cs b/DotNut/Abstractions/Interfaces/IMeltHandler.cs index 33a6693..047ca59 100644 --- a/DotNut/Abstractions/Interfaces/IMeltHandler.cs +++ b/DotNut/Abstractions/Interfaces/IMeltHandler.cs @@ -8,8 +8,6 @@ public interface IMeltHandler; public interface IMeltHandler: IMeltHandler { - Task GetQuote(CancellationToken cts = default); - Task Melt(List inputs, CancellationToken cts = default); - - Task Subscribe(CancellationToken cts = default); + Task GetQuote(CancellationToken ct = default); + Task Melt(List inputs, CancellationToken ct = default); } \ No newline at end of file diff --git a/DotNut/Abstractions/Interfaces/IMeltQuoteBuilder.cs b/DotNut/Abstractions/Interfaces/IMeltQuoteBuilder.cs index 1f7c470..095e521 100644 --- a/DotNut/Abstractions/Interfaces/IMeltQuoteBuilder.cs +++ b/DotNut/Abstractions/Interfaces/IMeltQuoteBuilder.cs @@ -10,7 +10,10 @@ public interface IMeltQuoteBuilder { IMeltQuoteBuilder WithUnit(string unit); IMeltQuoteBuilder WithInvoice(string bolt11Invoice); - Task>> ProcessAsyncBolt11(CancellationToken cancellationToken = default); - Task>> ProcessAsyncBolt12(CancellationToken cancellationToken = default); + IMeltQuoteBuilder WithBlankOutputs(OutputData blankOutputs); + IMeltQuoteBuilder WithPrivkeys(IEnumerable privKeys); + IMeltQuoteBuilder WithHTLCPreimage(string preimage); + Task>> ProcessAsyncBolt11(CancellationToken ct = default); + Task>> ProcessAsyncBolt12(CancellationToken ct = default); } \ No newline at end of file diff --git a/DotNut/Abstractions/Interfaces/IMintHandler.cs b/DotNut/Abstractions/Interfaces/IMintHandler.cs index d94b097..53973f0 100644 --- a/DotNut/Abstractions/Interfaces/IMintHandler.cs +++ b/DotNut/Abstractions/Interfaces/IMintHandler.cs @@ -5,7 +5,6 @@ namespace DotNut.Abstractions; public interface IMintHandler; public interface IMintHandler: IMintHandler { - Task GetQuote(CancellationToken cts = default); - Task Mint(CancellationToken cts = default); - Task Subscribe(CancellationToken cts = default); + Task GetQuote(CancellationToken ct = default); + Task Mint(CancellationToken ct = default); } \ No newline at end of file diff --git a/DotNut/Abstractions/Interfaces/IMintQuoteBuilder.cs b/DotNut/Abstractions/Interfaces/IMintQuoteBuilder.cs index c280cf4..833b78e 100644 --- a/DotNut/Abstractions/Interfaces/IMintQuoteBuilder.cs +++ b/DotNut/Abstractions/Interfaces/IMintQuoteBuilder.cs @@ -11,9 +11,11 @@ public interface IMintQuoteBuilder IMintQuoteBuilder WithUnit(string unit); IMintQuoteBuilder WithAmount(ulong amount); IMintQuoteBuilder WithOutputs(OutputData outputs); + + IMintQuoteBuilder WithDescription(string description); IMintQuoteBuilder WithP2PkLock(P2PkBuilder p2pkBuilder); - Task>> ProcessAsyncBolt11(CancellationToken cancellationToken = default); - Task>> ProcessAsyncBolt12(CancellationToken cancellationToken = default); + Task>> ProcessAsyncBolt11(CancellationToken ct = default); + Task>> ProcessAsyncBolt12(CancellationToken ct = default); } \ No newline at end of file diff --git a/DotNut/Abstractions/Interfaces/IProofManager.cs b/DotNut/Abstractions/Interfaces/IProofManager.cs index b4efae1..0bfb934 100644 --- a/DotNut/Abstractions/Interfaces/IProofManager.cs +++ b/DotNut/Abstractions/Interfaces/IProofManager.cs @@ -2,8 +2,8 @@ namespace DotNut.Abstractions.Interfaces; public interface IProofManager { - Task AddProofAsync(Proof proof, CancellationToken cts = default); - Task> GetProofsForKeysetId(KeysetId ids, CancellationToken cts = default); - Task> GetProofsForMint(List ids, CancellationToken cts = default); // should still query proofs based on keysetid - Task MarkProofAsSpent(Proof proof, CancellationToken cts = default); + Task AddProofAsync(Proof proof, CancellationToken ct = default); + Task> GetProofsForKeysetId(KeysetId ids, CancellationToken ct = default); + Task> GetProofsForMint(List ids, CancellationToken ct = default); // should still query proofs based on keysetid + Task MarkProofAsSpent(Proof proof, CancellationToken ct = default); } \ No newline at end of file diff --git a/DotNut/Abstractions/Interfaces/IProofSelector.cs b/DotNut/Abstractions/Interfaces/IProofSelector.cs index 398b95c..0639f51 100644 --- a/DotNut/Abstractions/Interfaces/IProofSelector.cs +++ b/DotNut/Abstractions/Interfaces/IProofSelector.cs @@ -2,5 +2,5 @@ namespace DotNut.Abstractions; public interface IProofSelector { - public Task SelectProofsToSend(List proofs, ulong amountToSend, bool includeFees = false, CancellationToken cts = default); + public Task SelectProofsToSend(List proofs, ulong amountToSend, bool includeFees = false, CancellationToken ct = default); } \ No newline at end of file diff --git a/DotNut/Abstractions/Interfaces/IRestoreBuilder.cs b/DotNut/Abstractions/Interfaces/IRestoreBuilder.cs index a896d93..d20e538 100644 --- a/DotNut/Abstractions/Interfaces/IRestoreBuilder.cs +++ b/DotNut/Abstractions/Interfaces/IRestoreBuilder.cs @@ -7,5 +7,5 @@ public interface IRestoreBuilder { RestoreBuilder ForKeysetIds(IEnumerable keysetIds); IRestoreBuilder WithSwap(bool shouldSwap = true); - Task> ProcessAsync(CancellationToken cancellationToken = default); + Task> ProcessAsync(CancellationToken ct = default); } diff --git a/DotNut/Abstractions/Interfaces/ISwapBuilder.cs b/DotNut/Abstractions/Interfaces/ISwapBuilder.cs index 4e98f88..e2b327d 100644 --- a/DotNut/Abstractions/Interfaces/ISwapBuilder.cs +++ b/DotNut/Abstractions/Interfaces/ISwapBuilder.cs @@ -9,5 +9,12 @@ public interface ISwapBuilder ISwapBuilder ForKeyset(KeysetId targetKeysetId); ISwapBuilder FromInputs(IEnumerable inputs); ISwapBuilder ForOutputs(OutputData outputs); - Task> ProcessAsync(CancellationToken cancellationToken = default); + ISwapBuilder WithDLEQVerification(bool verify = true); + ISwapBuilder WithFeeCalculation(bool includeFees = true); + ISwapBuilder WithAmounts(IEnumerable amounts); + ISwapBuilder WithPrivkeys(IEnumerable privKeys); + ISwapBuilder ToP2PK(P2PkBuilder p2pkBuilder); + ISwapBuilder WithHtlcPreimage(string preimage); + ISwapBuilder ToHTLC(HTLCBuilder htlcBuilder); + Task> ProcessAsync(CancellationToken ct = default); } diff --git a/DotNut/Abstractions/Interfaces/IWalletBuilder.cs b/DotNut/Abstractions/Interfaces/IWalletBuilder.cs index 1330d8c..2fb5937 100644 --- a/DotNut/Abstractions/Interfaces/IWalletBuilder.cs +++ b/DotNut/Abstractions/Interfaces/IWalletBuilder.cs @@ -1,4 +1,5 @@ using DotNut.Abstractions.Interfaces; +using DotNut.Abstractions.Websockets; using DotNut.Api; using DotNut.ApiModels; using DotNut.ApiModels.Mint.bolt12; @@ -26,28 +27,30 @@ public interface IWalletBuilder IWalletBuilder WithCounter(ICounter counter); IWalletBuilder WithCounter(IDictionary counter); IWalletBuilder ShouldBumpCounter(bool shouldBumpCounter = true); + IWalletBuilder WithWebsocketService(IWebsocketService websocketService); + Task GetInfo(bool forceReferesh = false, CancellationToken ct = default); + Task CreateOutputs(List amounts, KeysetId id, CancellationToken ct = default); - Task GetInfo(bool forceReferesh = false, CancellationToken cts = default); - Task CreateOutputs(List amounts, KeysetId id, CancellationToken cts = default); + Task?> GetActiveKeysetIdsWithUnits(CancellationToken ct = default); - Task?> GetActiveKeysetIdsWithUnits(CancellationToken cts = default); + Task GetMintApi(CancellationToken ct = default); - Task GetMintApi(CancellationToken cts = default); - - Task GetActiveKeysetId(string unit, CancellationToken cts = default); - Task> GetKeys(bool forceRefresh = false, CancellationToken cts = default); + Task GetActiveKeysetId(string unit, CancellationToken ct = default); + Task> GetKeys(bool forceRefresh = false, CancellationToken ct = default); Task GetKeys(KeysetId id, bool forceRefresh = false, - CancellationToken cts = default); + CancellationToken ct = default); Task> GetKeysets(bool forceRefresh = false, - CancellationToken cts = default); + CancellationToken ct = default); - Task CreateOutputs(List amounts, string unit, CancellationToken cts = default); + Task CreateOutputs(List amounts, string unit, CancellationToken ct = default); Task SelectProofsToSend(List proofs, ulong amount, bool includeFees, - CancellationToken cts = default); + CancellationToken ct = default); + + Task GetWebsocketService(CancellationToken ct = default); // Swap operations ISwapBuilder Swap(); diff --git a/DotNut/Abstractions/Interfaces/IWebsocketService.cs b/DotNut/Abstractions/Interfaces/IWebsocketService.cs index d1d06bc..12e8194 100644 --- a/DotNut/Abstractions/Interfaces/IWebsocketService.cs +++ b/DotNut/Abstractions/Interfaces/IWebsocketService.cs @@ -2,23 +2,25 @@ namespace DotNut.Abstractions.Websockets; -public interface IWebsocketService : IDisposable +public interface IWebsocketService : IAsyncDisposable { - event EventHandler? NotificationReceived; + event EventHandler? OnWsError; event EventHandler? ConnectionStateChanged; - Task ConnectAsync(string mintUrl, CancellationToken cancellationToken = default); + Task ConnectAsync(string mintUrl, CancellationToken ct = default); + + Task DisconnectAsync(string connectionId, CancellationToken ct = default); + - Task SubscribeAsync(string connectionId, SubscriptionKind kind, string[] filters, CancellationToken cancellationToken = default); + Task SubscribeAsync(string connectionId, SubscriptionKind kind, string[] filters, CancellationToken ct = default); - Task UnsubscribeAsync(string connectionId, string subId, CancellationToken cancellationToken = default); + Task UnsubscribeAsync(string subId, CancellationToken ct = default); - Task DisconnectAsync(string connectionId, CancellationToken cancellationToken = default); WebSocketState GetConnectionState(string connectionId); IEnumerable GetSubscriptions(string connectionId); - IEnumerable GetConnections(); + IEnumerable GetConnections(); } diff --git a/DotNut/Abstractions/MeltQuoteBuilder.cs b/DotNut/Abstractions/MeltQuoteBuilder.cs index 7fc10d7..6ccec67 100644 --- a/DotNut/Abstractions/MeltQuoteBuilder.cs +++ b/DotNut/Abstractions/MeltQuoteBuilder.cs @@ -13,9 +13,12 @@ class MeltQuoteBuilder : IMeltQuoteBuilder private string? _invoice; private OutputData? _blankOutputs; private string _unit = "sat"; + private bool _verifyDLEQ = true; private List? _privKeys; private string? _htlcPreimage; + + private Action? _callback; public MeltQuoteBuilder(Wallet wallet) { @@ -71,11 +74,22 @@ public IMeltQuoteBuilder WithHTLCPreimage(string preimage) return this; } + public IMeltQuoteBuilder OnQuoteStateChanged(Action callback) + { + this._callback = callback; + return this; + } + + public IMeltQuoteBuilder WithDLEQVerification(bool verifyDLEQ = true) + { + this._verifyDLEQ = verifyDLEQ; + return this; + } - public async Task>> ProcessAsyncBolt11(CancellationToken cts = default) + public async Task>> ProcessAsyncBolt11(CancellationToken ct = default) { var mintApi = await _wallet.GetMintApi(); - await _wallet._maybeSyncKeys(cts); + await _wallet._maybeSyncKeys(ct); ArgumentNullException.ThrowIfNull(this._invoice); var req = new PostMeltQuoteBolt11Request @@ -85,14 +99,14 @@ public async Task>> Proces }; var quote = - await mintApi.CreateMeltQuote("bolt11", req, cts); + await mintApi.CreateMeltQuote("bolt11", req, ct); if (_blankOutputs == null) { var outputsAmount = CashuUtils.CalculateNumberOfBlankOutputs((ulong)quote.FeeReserve); var amounts = Enumerable.Repeat(1UL, outputsAmount).ToList(); - this._blankOutputs = await this._wallet.CreateOutputs(amounts, this._unit, cts); + this._blankOutputs = await this._wallet.CreateOutputs(amounts, this._unit, ct); } await _maybeProcessP2PkHTLC(quote.Quote); @@ -101,7 +115,7 @@ public async Task>> Proces } public async Task>> ProcessAsyncBolt12( - CancellationToken cts = default) + CancellationToken ct = default) { throw new NotImplementedException(); } @@ -149,6 +163,5 @@ private async Task _maybeProcessP2PkHTLC(string quoteId) proof.Witness = JsonSerializer.Serialize(proofWitness); } } - } diff --git a/DotNut/Abstractions/MintQuoteBuilder.cs b/DotNut/Abstractions/MintQuoteBuilder.cs index 5c276c2..b5887b5 100644 --- a/DotNut/Abstractions/MintQuoteBuilder.cs +++ b/DotNut/Abstractions/MintQuoteBuilder.cs @@ -133,11 +133,11 @@ public IMintQuoteBuilder WithDescription(string description) } public async Task>> ProcessAsyncBolt11( - CancellationToken cts = default) + CancellationToken ct = default) { //todo implement info - await this._wallet._maybeSyncKeys(cts); + await this._wallet._maybeSyncKeys(ct); if (_amount == null) { throw new ArgumentNullException(nameof(_amount), "can't create melt quote without amount!"); @@ -149,10 +149,10 @@ public async Task>> Proces throw new ArgumentNullException(nameof(ICashuApi), "Can't request mint quote without mint API"); } - this._keysetId ??= await this._wallet.GetActiveKeysetId(this._unit, cts) ?? + this._keysetId ??= await this._wallet.GetActiveKeysetId(this._unit, ct) ?? throw new ArgumentException($"Can't get active keyset ID for unit: {_unit}"); - this._keyset ??= await this._wallet.GetKeys(this._keysetId, false, cts) ?? + this._keyset ??= await this._wallet.GetKeys(this._keysetId, false, ct) ?? throw new ArgumentException($"Cant get keys for keysetId: {_keysetId}"); var outputs = await this._createOutputs(); @@ -166,14 +166,14 @@ public async Task>> Proces }; var quoteBolt11 = await api.CreateMintQuote("bolt11", reqBolt11, - cts); + ct); return new MintHandlerBolt11(this._wallet, quoteBolt11, this._keyset, outputs); } public async Task>> ProcessAsyncBolt12( - CancellationToken cts = default) + CancellationToken ct = default) { - await this._wallet._maybeSyncKeys(cts); + await this._wallet._maybeSyncKeys(ct); if (this._pubkey == null) { throw new ArgumentNullException(nameof(_pubkey), "Can't request bolt12 mint quote without pubkey!"); @@ -181,7 +181,7 @@ public async Task>> Proces if (this._keyset == null) { - this._keyset = await this._wallet.GetKeys(this._keysetId, false, cts) ?? + this._keyset = await this._wallet.GetKeys(this._keysetId, false, ct) ?? throw new ArgumentException($"Cant fetch keys for keysetId: {_keysetId}"); } @@ -198,7 +198,7 @@ public async Task>> Proces var mintQuote = await (await _wallet.GetMintApi()) .CreateMintQuote("bolt12", req, - cts); + ct); return new MintHandlerBolt12(this._wallet, mintQuote, this._keyset, outputs); } diff --git a/DotNut/Abstractions/ProofSelector.cs b/DotNut/Abstractions/ProofSelector.cs index 9b1b4d9..3a72eef 100644 --- a/DotNut/Abstractions/ProofSelector.cs +++ b/DotNut/Abstractions/ProofSelector.cs @@ -55,7 +55,7 @@ private ulong GetProofFeePPK(Proof proof) return _keysetFees.TryGetValue(proof.Id, out var fee) ? fee : 0; } - public async Task SelectProofsToSend(List proofs, ulong amountToSend, bool includeFees = false, CancellationToken cts = default) + public async Task SelectProofsToSend(List proofs, ulong amountToSend, bool includeFees = false, CancellationToken ct = default) { // Init vars const int MAX_TRIALS = 60; // 40-80 is optimal (per RGLI paper) diff --git a/DotNut/Abstractions/RestoreBuilder.cs b/DotNut/Abstractions/RestoreBuilder.cs index b0e2c62..f92f93c 100644 --- a/DotNut/Abstractions/RestoreBuilder.cs +++ b/DotNut/Abstractions/RestoreBuilder.cs @@ -29,16 +29,16 @@ public IRestoreBuilder WithSwap(bool shouldSwap = true) return this; } - public async Task> ProcessAsync(CancellationToken cts = default) + public async Task> ProcessAsync(CancellationToken ct = default) { var api = await _wallet.GetMintApi(); - await _wallet._maybeSyncKeys(cts); + await _wallet._maybeSyncKeys(ct); var mnemonic = _wallet.GetMnemonic()?? throw new ArgumentNullException("Can't restore wallet without Mnemonic"); _specifiedKeysets ??= - (await _wallet.GetKeysets(cts: cts)).Select(k => k.Id).ToList(); + (await _wallet.GetKeysets(ct: ct)).Select(k => k.Id).ToList(); if (_specifiedKeysets == null || _specifiedKeysets.Count == 0) { @@ -58,17 +58,17 @@ public async Task> ProcessAsync(CancellationToken cts = defau int batchNumber = 0; int emptyBatchesRemaining = 3; - var keyset = await _wallet.GetKeys(keysetId, false, cts); + var keyset = await _wallet.GetKeys(keysetId, false, ct); while (emptyBatchesRemaining > 0) { - var outputs = await _createBatch(mnemonic, keysetId, batchNumber, cts); + var outputs = await _createBatch(mnemonic, keysetId, batchNumber, ct); await counter!.IncrementCounter(keysetId, batchNumber * 100); var req = new PostRestoreRequest { Outputs = outputs.BlindedMessages.ToArray() }; - var res = await api.Restore(req, cts); + var res = await api.Restore(req, ct); if (!res.Signatures.Any()) { @@ -97,12 +97,12 @@ public async Task> ProcessAsync(CancellationToken cts = defau foreach (var unitKeyset in activeUnits) { - var correspondingKeys = await _wallet.GetKeys(unitKeyset.Value, false, cts); + var correspondingKeys = await _wallet.GetKeys(unitKeyset.Value, false, ct); var totalAmount = recoveredProofs.Select(p=>p.Amount).Aggregate((a,c) => a + c); var amounts = CashuUtils.SplitToProofsAmounts(totalAmount, correspondingKeys.Keys); - var ctr = await counter!.GetCounterForId(unitKeyset.Value, cts); + var ctr = await counter!.GetCounterForId(unitKeyset.Value, ct); var newOutputs = CashuUtils.CreateOutputs(amounts, unitKeyset.Value, correspondingKeys.Keys, mnemonic, ctr); - await counter.IncrementCounter(unitKeyset.Value, newOutputs.BlindedMessages.Count, cts); + await counter.IncrementCounter(unitKeyset.Value, newOutputs.BlindedMessages.Count, ct); var swapRequest = new PostSwapRequest { @@ -110,7 +110,7 @@ public async Task> ProcessAsync(CancellationToken cts = defau Outputs = newOutputs.BlindedMessages.ToArray(), }; - var swapResult = await api.Swap(swapRequest, cts); + var swapResult = await api.Swap(swapRequest, ct); var constructedProofs = CashuUtils.ConstructProofsFromPromises(swapResult.Signatures.ToList(), newOutputs, correspondingKeys.Keys); freshProofs.AddRange(constructedProofs); @@ -118,7 +118,7 @@ public async Task> ProcessAsync(CancellationToken cts = defau return freshProofs; } - private async Task _createBatch(Mnemonic mnemonic, KeysetId keysetId, int batchNubmber, CancellationToken cts) + private async Task _createBatch(Mnemonic mnemonic, KeysetId keysetId, int batchNubmber, CancellationToken ct) { var amounts = Enumerable.Repeat((ulong)1, 100).ToList(); Console.WriteLine(batchNubmber); diff --git a/DotNut/Abstractions/SwapBuilder.cs b/DotNut/Abstractions/SwapBuilder.cs index 662b68c..2dd8cac 100644 --- a/DotNut/Abstractions/SwapBuilder.cs +++ b/DotNut/Abstractions/SwapBuilder.cs @@ -21,7 +21,7 @@ class SwapBuilder : ISwapBuilder private string _unit = "sat"; - private bool _verifySignatures = true; + private bool _verifyDLEQ = true; private bool _includeFees = true; @@ -75,25 +75,13 @@ public ISwapBuilder ForOutputs(OutputData outputs) /// /// Optional. - /// True by default, allows user to turn off signature verification (not advised) + /// True by default, allows user to turn off DLEQ verification (not advised) /// /// /// - public ISwapBuilder WithSignatureVerification(bool verify = true) + public ISwapBuilder WithDLEQVerification(bool verify = true) { - _verifySignatures = verify; - return this; - } - - /// - /// Optional. - /// Provide outputs for a swap. - /// - /// - /// - public ISwapBuilder WithOutputs(OutputData outputs) - { - _outputs = outputs; + _verifyDLEQ = verify; return this; } @@ -166,11 +154,11 @@ public ISwapBuilder ToHTLC(HTLCBuilder htlcBuilder) return this; } - public async Task> ProcessAsync(CancellationToken cts = default) + public async Task> ProcessAsync(CancellationToken ct = default) { - var mintApi = await _wallet.GetMintApi(cts); + var mintApi = await _wallet.GetMintApi(ct); - var swapInputs = await _getSwapProofs(cts); + var swapInputs = await _getSwapProofs(ct); if (swapInputs == null || swapInputs.Count == 0) { throw new ArgumentException("Nothing to swap!"); @@ -179,13 +167,13 @@ public async Task> ProcessAsync(CancellationToken cts = default) // if there's no keysetId specified - let's choose it. if (_keysetId == null) { - _keysetId = await _wallet.GetActiveKeysetId(this._unit, cts) ?? + _keysetId = await _wallet.GetActiveKeysetId(this._unit, ct) ?? throw new InvalidOperationException("Could not fetch Keyset ID"); } - var keys = await _wallet.GetKeys(false, cts); + var keys = await _wallet.GetKeys(false, ct); var keysForCurrentId = keys.Single(k=>k.Id == _keysetId); - if (_verifySignatures) + if (_verifyDLEQ) { foreach (var proof in swapInputs!) { @@ -195,24 +183,25 @@ public async Task> ProcessAsync(CancellationToken cts = default) throw new InvalidOperationException($"Can't find key for amount {proof.Amount} in keyset {keyset.Id}"); } var isValid = proof.Verify(key); - if (!isValid) - throw new InvalidOperationException($"Invalid proof signature for amount {proof.Amount}"); + if (!isValid) + { + throw new InvalidOperationException($"Invalid proof signature for amount {proof.Amount}"); + } } } var fee = 0UL; if (_includeFees) { - var keysetsFees = (await _wallet.GetKeysets(false, cts)).ToDictionary(k=>k.Id, k=>k.InputFee??0); + var keysetsFees = (await _wallet.GetKeysets(false, ct)).ToDictionary(k=>k.Id, k=>k.InputFee??0); fee = swapInputs.ComputeFee(keysetsFees); } var total = CashuUtils.SumProofs(swapInputs); + // Swap received proofs to our keyset - var amounts = await _getAmounts(total, fee, keysForCurrentId.Keys); - - var outputs = await this._getOutputs(keysForCurrentId.Keys, cts); + var outputs = await this._getOutputs(keysForCurrentId.Keys, ct); var request = new PostSwapRequest() { @@ -222,7 +211,7 @@ public async Task> ProcessAsync(CancellationToken cts = default) await _maybeProcessP2Pk(); - var swapResponse = await mintApi.Swap(request, cts); + var swapResponse = await mintApi.Swap(request, ct); var swappedProofs = CashuUtils.ConstructProofsFromPromises(swapResponse.Signatures.ToList(), this._outputs, keysForCurrentId.Keys); @@ -230,7 +219,7 @@ public async Task> ProcessAsync(CancellationToken cts = default) return swappedProofs; } - private async Task> _getSwapProofs(CancellationToken cts = default) + private async Task> _getSwapProofs(CancellationToken ct = default) { _proofsToSwap ??= new(); if (_tokenString != null) @@ -265,7 +254,7 @@ private async Task> _getSwapProofs(CancellationToken cts = default) return _proofsToSwap; } - async Task _getOutputs(Keyset keys, CancellationToken cts = default) + async Task _getOutputs(Keyset keys, CancellationToken ct = default) { var outputs = new OutputData(); @@ -287,20 +276,19 @@ async Task _getOutputs(Keyset keys, CancellationToken cts = default) if (this._builder is not null) { // skipped checks for keysetid and keys, since its validated before. make sure to remember about it. - foreach (var amount in _amounts) + foreach (var p2pkOutput in _amounts.Select(amount => CashuUtils.CreateP2PkOutput(amount, this._keysetId!, keys, _builder))) { - var p2pkOutput = CashuUtils.CreateP2PkOutput(amount, this._keysetId!, keys, _builder); outputs.BlindingFactors.Add(p2pkOutput.BlindingFactors[0]); outputs.BlindedMessages.Add(p2pkOutput.BlindedMessages[0]); outputs.Secrets.Add(p2pkOutput.Secrets[0]); } + return outputs; } - return await _wallet.CreateOutputs(_amounts, this._keysetId!, cts); + return await _wallet.CreateOutputs(_amounts, this._keysetId!, ct); } - - + private async Task _maybeProcessP2Pk() { if (_privKeys == null || _privKeys.Count == 0) diff --git a/DotNut/Abstractions/Wallet.cs b/DotNut/Abstractions/Wallet.cs index 01e5aa8..0889a21 100644 --- a/DotNut/Abstractions/Wallet.cs +++ b/DotNut/Abstractions/Wallet.cs @@ -1,4 +1,5 @@ using DotNut.Abstractions.Interfaces; +using DotNut.Abstractions.Websockets; using DotNut.Api; using DotNut.ApiModels; using DotNut.ApiModels.Info; @@ -21,6 +22,8 @@ public class Wallet : IWalletBuilder private Mnemonic? _mnemonic; private ICounter? _counter; + private IWebsocketService? _wsService; + //flags private bool _shouldSyncKeyset = true; private DateTime? _lastSync = DateTime.MinValue; @@ -30,7 +33,6 @@ public class Wallet : IWalletBuilder private bool _allowInvalidKeysetIds = false; - /* * Fluent Builder Methods */ @@ -194,6 +196,20 @@ public IWalletBuilder ShouldBumpCounter(bool shouldBumpCounter = true) this._shouldBumpCounter = shouldBumpCounter; return this; } + + /// + /// Optional. + /// Adds websocket service. You should use single websocket service (singleton at best) for multiple wallets, in order to handle everything in nice manner. + /// If not set, but requested it'll be created automatically (which won't be so optimal). + /// + /// + /// + public IWalletBuilder WithWebsocketService(IWebsocketService websocketService) + { + this._wsService = websocketService; + return this; + } + /// /// Optional. @@ -206,7 +222,8 @@ public IStatefulWalletBuilder WithProofManager(IProofManager proofManager) { return new StatefulWallet(this, proofManager); } - + + /* * Main api methods */ @@ -277,11 +294,11 @@ public void InvalidateCache() /// Get active keyset id for chosen unit. /// /// keyset unit, e.g. sat - /// + /// /// Active keysetId - public async Task GetActiveKeysetId(string unit, CancellationToken cts = default) + public async Task GetActiveKeysetId(string unit, CancellationToken ct = default) { - await _maybeSyncKeys(cts); + await _maybeSyncKeys(ct); return _keysets? .OrderBy(k => k.InputFee) .FirstOrDefault(k => k is { Active: true } && k.Unit == unit, null) @@ -292,9 +309,9 @@ public void InvalidateCache() /// Get active keyset ids for each unit /// /// Dictionary of (unit, KeysetId) - public async Task?> GetActiveKeysetIdsWithUnits(CancellationToken cts = default) + public async Task?> GetActiveKeysetIdsWithUnits(CancellationToken ct = default) { - await _maybeSyncKeys(cts); + await _maybeSyncKeys(ct); return _keysets? .GroupBy(k => k.Unit) .ToDictionary( @@ -307,16 +324,16 @@ public void InvalidateCache() /// Get keys of current mint stored in wallet. /// /// Refetch flag - /// + /// /// Mints keys - public async Task> GetKeys(bool forceRefresh = false, CancellationToken cts = default) + public async Task> GetKeys(bool forceRefresh = false, CancellationToken ct = default) { if (forceRefresh) { - this._keys = await _fetchKeys(cts); + this._keys = await _fetchKeys(ct); return this._keys ?? []; } - await _maybeSyncKeys(cts); + await _maybeSyncKeys(ct); return this._keys ?? []; } @@ -325,14 +342,14 @@ public void InvalidateCache() /// /// KeysetId /// Refetch flag - /// + /// /// Keys for given keyset /// If wallet doesn't contain keysets for given keysetId - public async Task GetKeys(KeysetId id, bool forceRefresh = false, CancellationToken cts = default) + public async Task GetKeys(KeysetId id, bool forceRefresh = false, CancellationToken ct = default) { if (forceRefresh) { - return await _fetchKeys(id, cts); + return await _fetchKeys(id, ct); } if (this._keys == null) { @@ -345,16 +362,16 @@ public void InvalidateCache() /// Get Keysets stored in wallet /// /// Refetch flag - /// + /// /// List of Keysets - public async Task> GetKeysets(bool forceRefresh = false, CancellationToken cts = default) + public async Task> GetKeysets(bool forceRefresh = false, CancellationToken ct = default) { if (forceRefresh) { - this._keysets = await _fetchKeysets(cts); + this._keysets = await _fetchKeysets(ct); return _keysets ?? []; } - await _maybeSyncKeys(cts); + await _maybeSyncKeys(ct); return _keysets ?? []; } @@ -362,15 +379,15 @@ public void InvalidateCache() /// Get Mints info, supported methods etc. /// /// Refetch flag - /// + /// /// MintInfo object - public async Task GetInfo(bool forceReferesh = false, CancellationToken cts = default) + public async Task GetInfo(bool forceReferesh = false, CancellationToken ct = default) { if (forceReferesh) { - return await _fetchMintInfo(cts); + return await _fetchMintInfo(ct); } - return await _lazyFetchMintInfo(cts); + return await _lazyFetchMintInfo(ct); } /// @@ -379,12 +396,12 @@ public async Task GetInfo(bool forceReferesh = false, CancellationToke /// /// List of amounts in Outputs. /// Keyset ID - /// + /// /// Outputs /// If keys not set. If Mnemonic set, but no Counter. - public async Task CreateOutputs(List amounts, KeysetId id, CancellationToken cts = default) + public async Task CreateOutputs(List amounts, KeysetId id, CancellationToken ct = default) { - await _maybeSyncKeys(cts); + await _maybeSyncKeys(ct); if (this._keys == null) { throw new ArgumentNullException(nameof(this._keys), "No Keys found. Make sure to fetch them!"); @@ -400,10 +417,10 @@ public async Task CreateOutputs(List amounts, KeysetId id, Ca throw new ArgumentNullException(nameof(ICounter), "Can't derive outputs without keyset counter"); } - var counterValue = await this._counter.GetCounterForId(id, cts); + var counterValue = await this._counter.GetCounterForId(id, ct); if (_shouldBumpCounter) { - await this._counter.IncrementCounter(id, amounts.Count, cts); + await this._counter.IncrementCounter(id, amounts.Count, ct); } return CashuUtils.CreateOutputs(amounts, id, keyset.Keys, this._mnemonic, counterValue); } @@ -413,46 +430,51 @@ public async Task CreateOutputs(List amounts, KeysetId id, Ca /// /// List of amounts. /// - /// + /// /// Outputs /// If no keysetID stored in wallet. - public async Task CreateOutputs(List amounts, string unit, CancellationToken cts = default) + public async Task CreateOutputs(List amounts, string unit, CancellationToken ct = default) { - var keysetId = await this.GetActiveKeysetId(unit, cts); + var keysetId = await this.GetActiveKeysetId(unit, ct); if (keysetId == null) { throw new ArgumentNullException(nameof(keysetId)); } - return await this.CreateOutputs(amounts, keysetId, cts); + return await this.CreateOutputs(amounts, keysetId, ct); } - public async Task SelectProofsToSend(List proofs, ulong amount, bool includeFees, CancellationToken cts = default) + public async Task SelectProofsToSend(List proofs, ulong amount, bool includeFees, CancellationToken ct = default) { if (this._selector == null) { - await _maybeSyncKeys(cts); + await _maybeSyncKeys(ct); ArgumentNullException.ThrowIfNull(this._keysetFees); this._selector = new ProofSelector(this._keysetFees); } - return await _selector.SelectProofsToSend(proofs, amount, includeFees, cts); + return await _selector.SelectProofsToSend(proofs, amount, includeFees, ct); } - public async Task GetMintApi(CancellationToken cts = default) + public async Task GetMintApi(CancellationToken ct = default) { _ensureApiConnected(); return _mintApi; } - public async Task? GetSelector(CancellationToken cts = default) + public async Task? GetSelector(CancellationToken ct = default) { if (this._selector == null) { - await _maybeSyncKeys(cts); + await _maybeSyncKeys(ct); ArgumentNullException.ThrowIfNull(this._keysetFees); this._selector = new ProofSelector(this._keysetFees); } return this._selector; } + + public async Task GetWebsocketService(CancellationToken ct = default) + { + return this._wsService ??= new WebsocketService(); + } public Mnemonic? GetMnemonic() => _mnemonic; public ICounter? GetCounter() => _counter; @@ -483,10 +505,10 @@ internal void _ensureApiConnected(string? msg = null) /// /// List of Keysets /// May be thrown if mint is not set. - private async Task> _fetchKeysets(CancellationToken cts = default) + private async Task> _fetchKeysets(CancellationToken ct = default) { _ensureApiConnected("Can't fetch keysets without mint api!"); - var keysetsRaw = await _mintApi!.GetKeysets(cts); + var keysetsRaw = await _mintApi!.GetKeysets(ct); return keysetsRaw.Keysets.ToList(); } @@ -496,10 +518,10 @@ internal void _ensureApiConnected(string? msg = null) /// List of Keys (lists :)) /// May be thrown if mint is not set. /// May be thrown if mint returns invalid keysetId for at least one Keyset - private async Task> _fetchKeys(CancellationToken cts = default) + private async Task> _fetchKeys(CancellationToken ct = default) { _ensureApiConnected("Can't fetch keys without mint api!"); - var keysRaw = await _mintApi!.GetKeys(cts); + var keysRaw = await _mintApi!.GetKeys(ct); foreach (var keysetItemResponse in keysRaw.Keysets) { var isKeysetIdValid = keysetItemResponse.Keys.VerifyKeysetId(keysetItemResponse.Id, keysetItemResponse.Unit, keysetItemResponse.FinalExpiry); @@ -599,5 +621,7 @@ internal async Task _maybeSyncKeys(CancellationToken cts = default) _lastSync = DateTime.Now; } + + } diff --git a/DotNut/Abstractions/WebsocketService.cs b/DotNut/Abstractions/WebsocketService.cs deleted file mode 100644 index 031d370..0000000 --- a/DotNut/Abstractions/WebsocketService.cs +++ /dev/null @@ -1,294 +0,0 @@ -using System.Collections.Concurrent; -using System.Net.WebSockets; -using System.Text; -using System.Text.Json; -using DotNut.Abstractions.Websockets; - -namespace DotNut.Abstractions; - -public class WebsocketService : IWebsocketService -{ - private readonly ConcurrentDictionary _connections = new(); - private readonly ConcurrentDictionary _subscriptions = new(); - private readonly object _lockObject = new(); - private int _nextRequestId = 0; - - public event EventHandler? NotificationReceived; - public event EventHandler? ConnectionStateChanged; - public async Task ConnectAsync(string mintUrl, CancellationToken cancellationToken = default) - { - var connectionId = Guid.NewGuid().ToString(); - var wsUrl = GetWebSocketUrl(mintUrl); - - var clientWebSocket = new ClientWebSocket(); - await clientWebSocket.ConnectAsync(new Uri(wsUrl), cancellationToken); - - var connection = new WebSocketConnection - { - Id = connectionId, - MintUrl = mintUrl, - WebSocket = clientWebSocket, - State = WebSocketState.Open - }; - - _connections[connectionId] = connection; - - _ = Task.Run(async () => await ListenForMessages(connection, cancellationToken), cancellationToken); - - OnConnectionStateChanged(connectionId, WebSocketState.Open); - - return connectionId; - } - public async Task SubscribeAsync(string connectionId, SubscriptionKind kind, string[] filters, CancellationToken cancellationToken = default) - { - if (!_connections.TryGetValue(connectionId, out var connection)) - throw new InvalidOperationException($"Connection {connectionId} not found"); - - if (connection.State != WebSocketState.Open) - throw new InvalidOperationException($"Connection {connectionId} is not open"); - - var subId = Guid.NewGuid().ToString(); - var requestId = GetNextRequestId(); - - var request = new WsRequest - { - JsonRpc = "2.0", - Method = WsRequestMethod.subscribe, - Params = new WsRequestParams - { - Kind = kind, - SubId = subId, - Filters = filters - }, - Id = requestId - }; - - var subscription = new Subscription - { - Id = subId, - ConnectionId = connectionId, - Kind = kind, - Filters = filters, - CreatedAt = DateTime.UtcNow - }; - - _subscriptions[subId] = subscription; - - await SendMessageAsync(connection, request, cancellationToken); - - return subId; - } - public async Task UnsubscribeAsync(string connectionId, string subId, CancellationToken cancellationToken = default) - { - if (!_connections.TryGetValue(connectionId, out var connection)) - throw new InvalidOperationException($"Connection {connectionId} not found"); - - if (connection.State != WebSocketState.Open) - throw new InvalidOperationException($"Connection {connectionId} is not open"); - - var requestId = GetNextRequestId(); - - var request = new WsRequest - { - JsonRpc = "2.0", - Method = WsRequestMethod.unsubscribe, - Params = new WsRequestParams - { - SubId = subId - }, - Id = requestId - }; - - await SendMessageAsync(connection, request, cancellationToken); - - _subscriptions.TryRemove(subId, out _); - } - - public async Task DisconnectAsync(string connectionId, CancellationToken cancellationToken = default) - { - if (!_connections.TryGetValue(connectionId, out var connection)) - return; - - try - { - if (connection.State == WebSocketState.Open) - { - await connection.WebSocket.CloseAsync(WebSocketCloseStatus.NormalClosure, "Client disconnecting", cancellationToken); - } - } - catch (Exception) - { - // Ignore close exceptions - } - finally - { - connection.WebSocket.Dispose(); - _connections.TryRemove(connectionId, out _); - - var subscriptionsToRemove = _subscriptions - .Where(s => s.Value.ConnectionId == connectionId) - .Select(s => s.Key) - .ToList(); - - foreach (var subId in subscriptionsToRemove) - { - _subscriptions.TryRemove(subId, out _); - } - - OnConnectionStateChanged(connectionId, WebSocketState.Closed); - } - } - - public async ValueTask DisposeAsync() - { - var connectionIds = _connections.Keys.ToList(); - foreach (var connectionId in connectionIds) - { - await DisconnectAsync(connectionId); - } - } - - // Use only if necessary. pls use DisposeAsync - public void Dispose() - { - var connectionIds = _connections.Keys.ToList(); - foreach (var connectionId in connectionIds) - { - DisconnectAsync(connectionId).Wait(TimeSpan.FromSeconds(5)); - } - } - - public WebSocketState GetConnectionState(string connectionId) - { - return _connections.TryGetValue(connectionId, out var connection) - ? connection.State - : WebSocketState.None; - } - - public IEnumerable GetSubscriptions(string connectionId) - { - return _subscriptions.Values.Where(s => s.ConnectionId == connectionId); - } - - public IEnumerable GetConnections() - { - return _connections.Values; - } - - private async Task ListenForMessages(WebSocketConnection connection, CancellationToken cancellationToken) - { - var buffer = new byte[4096]; - - try - { - while (connection.State == WebSocketState.Open && !cancellationToken.IsCancellationRequested) - { - var result = await connection.WebSocket.ReceiveAsync(new ArraySegment(buffer), cancellationToken); - - if (result.MessageType == WebSocketMessageType.Close) - { - connection.State = WebSocketState.Closed; - OnConnectionStateChanged(connection.Id, WebSocketState.Closed); - break; - } - - if (result.MessageType == WebSocketMessageType.Text) - { - var message = Encoding.UTF8.GetString(buffer, 0, result.Count); - await ProcessMessage(connection, message); - } - } - } - catch (Exception ex) - { - connection.State = WebSocketState.Aborted; - OnConnectionStateChanged(connection.Id, WebSocketState.Aborted); - // Log exception - } - } - - private async Task ProcessMessage(WebSocketConnection connection, string message) - { - try - { - var jsonElement = JsonSerializer.Deserialize(message); - - if (jsonElement.TryGetProperty("method", out var methodProp) && - methodProp.GetString() == "subscribe") - { - var notification = JsonSerializer.Deserialize(message); - if (notification != null) - { - OnNotificationReceived(connection.Id, notification); - } - } - else if (jsonElement.TryGetProperty("result", out _)) - { - var response = JsonSerializer.Deserialize(message); - } - else if (jsonElement.TryGetProperty("error", out _)) - { - var error = JsonSerializer.Deserialize(message); - } - } - catch (Exception ex) - { - } - } - - private async Task SendMessageAsync(WebSocketConnection connection, T message, CancellationToken cancellationToken) - { - var json = JsonSerializer.Serialize(message); - var bytes = Encoding.UTF8.GetBytes(json); - - await connection.WebSocket.SendAsync( - new ArraySegment(bytes), - WebSocketMessageType.Text, - true, - cancellationToken); - } - - private string GetWebSocketUrl(string mintUrl) - { - var uri = new Uri(mintUrl.TrimEnd('/')); - var scheme = uri.Scheme == "https" ? "wss" : "ws"; - return $"{scheme}://{uri.Host}:{uri.Port}/v1/ws"; - } - - private int GetNextRequestId() - { - lock (_lockObject) - { - return ++_nextRequestId; - } - } - - private void OnNotificationReceived(string connectionId, WsNotification notification) - { - NotificationReceived?.Invoke(this, new NotificationEventArgs - { - ConnectionId = connectionId, - Notification = notification - }); - } - private void OnConnectionStateChanged(string connectionId, WebSocketState state) - { - ConnectionStateChanged?.Invoke(this, new ConnectionStateChangedEventArgs - { - ConnectionId = connectionId, - State = state - }); - } - public bool IsConnected(string mintUrl) - => _connections.Any(x => _normalizeMintUrl(x.Value.MintUrl) == _normalizeMintUrl(mintUrl)); - private string _normalizeMintUrl(string mintUrl) - { - if (Uri.TryCreate(mintUrl.TrimEnd('/'), UriKind.Absolute, out var uri)) - { - var host = uri.Host.ToLowerInvariant(); - var builder = new UriBuilder(uri) { Host = host }; - return builder.Uri.ToString().TrimEnd('/'); - } - return mintUrl.TrimEnd('/').ToLowerInvariant(); - } - } diff --git a/DotNut/Abstractions/Websockets/NotificationParser.cs b/DotNut/Abstractions/Websockets/NotificationParser.cs index 129c1e4..4e0969e 100644 --- a/DotNut/Abstractions/Websockets/NotificationParser.cs +++ b/DotNut/Abstractions/Websockets/NotificationParser.cs @@ -1,4 +1,5 @@ using System.Text.Json; +using DotNut.ApiModels; namespace DotNut.Abstractions.Websockets; @@ -13,9 +14,9 @@ public static class NotificationParser return subscriptionKind switch { - SubscriptionKind.bolt11_mint_quote => jsonElement.Deserialize(), - SubscriptionKind.bolt11_melt_quote => jsonElement.Deserialize(), - SubscriptionKind.proof_state => jsonElement.Deserialize(), + SubscriptionKind.bolt11_mint_quote => jsonElement.Deserialize(), + SubscriptionKind.bolt11_melt_quote => jsonElement.Deserialize(), + SubscriptionKind.proof_state => jsonElement.Deserialize(), _ => notification.Params.Payload }; } diff --git a/DotNut/Abstractions/Websockets/NotificationPayloads.cs b/DotNut/Abstractions/Websockets/NotificationPayloads.cs deleted file mode 100644 index 912b10b..0000000 --- a/DotNut/Abstractions/Websockets/NotificationPayloads.cs +++ /dev/null @@ -1,56 +0,0 @@ -using System.Text.Json.Serialization; - -namespace DotNut.Abstractions.Websockets; - -public class MintQuoteNotificationPayload -{ - [JsonPropertyName("quote")] - public string Quote { get; set; } = string.Empty; - - [JsonPropertyName("request")] - public string Request { get; set; } = string.Empty; - - [JsonPropertyName("paid")] - public bool Paid { get; set; } - - [JsonPropertyName("expiry")] - public long? Expiry { get; set; } -} - -public class MeltQuoteNotificationPayload -{ - [JsonPropertyName("quote")] - public string Quote { get; set; } = string.Empty; - - [JsonPropertyName("amount")] - public ulong Amount { get; set; } - - [JsonPropertyName("fee_reserve")] - public ulong FeeReserve { get; set; } - - [JsonPropertyName("paid")] - public bool Paid { get; set; } - - [JsonPropertyName("expiry")] - public long? Expiry { get; set; } - - [JsonPropertyName("payment_preimage")] - public string? PaymentPreimage { get; set; } - - [JsonPropertyName("change")] - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - public object[]? Change { get; set; } -} - -public class ProofStateNotificationPayload -{ - [JsonPropertyName("Y")] - public string Y { get; set; } = string.Empty; - - [JsonPropertyName("state")] - public ProofState State { get; set; } - - [JsonPropertyName("witness")] - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - public string? Witness { get; set; } -} diff --git a/DotNut/Abstractions/Websockets/Subscription.cs b/DotNut/Abstractions/Websockets/Subscription.cs new file mode 100644 index 0000000..4baf47a --- /dev/null +++ b/DotNut/Abstractions/Websockets/Subscription.cs @@ -0,0 +1,20 @@ +using System.Threading.Channels; + +namespace DotNut.Abstractions.Websockets; + +public class Subscription +{ + public string Id { get; set; } = string.Empty; + public string ConnectionId { get; set; } = string.Empty; + public SubscriptionKind Kind { get; set; } + public string[] Filters { get; set; } = Array.Empty(); + public DateTime CreatedAt { get; set; } = DateTime.UtcNow; + public Channel NotificationChannel { get; set; } + + public EventHandler OnError { get; set; } + + public void Close() + { + NotificationChannel.Writer.TryComplete(); + } +} diff --git a/DotNut/Abstractions/Websockets/WebsocketEntities.cs b/DotNut/Abstractions/Websockets/WebsocketConnection.cs similarity index 56% rename from DotNut/Abstractions/Websockets/WebsocketEntities.cs rename to DotNut/Abstractions/Websockets/WebsocketConnection.cs index 0569a23..5cfa388 100644 --- a/DotNut/Abstractions/Websockets/WebsocketEntities.cs +++ b/DotNut/Abstractions/Websockets/WebsocketConnection.cs @@ -2,7 +2,7 @@ namespace DotNut.Abstractions.Websockets; -public class WebSocketConnection +public class WebsocketConnection { public string Id { get; set; } = string.Empty; public string MintUrl { get; set; } = string.Empty; @@ -10,7 +10,7 @@ public class WebSocketConnection public WebSocketState State { get; set; } public DateTime ConnectedAt { get; set; } = DateTime.UtcNow; - public bool Equals(WebSocketConnection? other) + public bool Equals(WebsocketConnection? other) { if (other is null) return false; if (ReferenceEquals(this, other)) return true; @@ -19,7 +19,7 @@ public bool Equals(WebSocketConnection? other) public override bool Equals(object? obj) { - return obj is WebSocketConnection other && Equals(other); + return obj is WebsocketConnection other && Equals(other); } public override int GetHashCode() @@ -27,22 +27,14 @@ public override int GetHashCode() return MintUrl?.GetHashCode(StringComparison.OrdinalIgnoreCase) ?? 0; } - public static bool operator ==(WebSocketConnection? left, WebSocketConnection? right) + public static bool operator ==(WebsocketConnection? left, WebsocketConnection? right) { return Equals(left, right); } - public static bool operator !=(WebSocketConnection? left, WebSocketConnection? right) + public static bool operator !=(WebsocketConnection? left, WebsocketConnection? right) { return !Equals(left, right); } } -public class Subscription -{ - public string Id { get; set; } = string.Empty; - public string ConnectionId { get; set; } = string.Empty; - public SubscriptionKind Kind { get; set; } - public string[] Filters { get; set; } = Array.Empty(); - public DateTime CreatedAt { get; set; } = DateTime.UtcNow; -} diff --git a/DotNut/Abstractions/Websockets/WebsocketEnums.cs b/DotNut/Abstractions/Websockets/WebsocketEnums.cs index 26bf0d8..4bd9243 100644 --- a/DotNut/Abstractions/Websockets/WebsocketEnums.cs +++ b/DotNut/Abstractions/Websockets/WebsocketEnums.cs @@ -15,12 +15,4 @@ public enum WsRequestMethod { subscribe, unsubscribe -} - -[JsonConverter(typeof(JsonStringEnumConverter))] -public enum ProofState -{ - UNSPENT, - PENDING, - SPENT -} +} \ No newline at end of file diff --git a/DotNut/Abstractions/Websockets/WebsocketModels.cs b/DotNut/Abstractions/Websockets/WebsocketModels.cs index 32ab739..f32e5f8 100644 --- a/DotNut/Abstractions/Websockets/WebsocketModels.cs +++ b/DotNut/Abstractions/Websockets/WebsocketModels.cs @@ -5,7 +5,7 @@ namespace DotNut.Abstractions.Websockets; public class WsRequest { [JsonPropertyName("jsonrpc")] - public string JsonRpc { get; set; } = "2.0"; + public string JsonRpc = "2.0"; [JsonPropertyName("method")] public WsRequestMethod Method { get; set; } diff --git a/DotNut/Abstractions/Websockets/WebsocketService.cs b/DotNut/Abstractions/Websockets/WebsocketService.cs new file mode 100644 index 0000000..9214a81 --- /dev/null +++ b/DotNut/Abstractions/Websockets/WebsocketService.cs @@ -0,0 +1,340 @@ +using System.Collections.Concurrent; +using System.Net.WebSockets; +using System.Text; +using System.Text.Json; +using System.Threading.Channels; +using DotNut.Abstractions.Websockets; + +namespace DotNut.Abstractions; + +public class WebsocketService : IWebsocketService +{ + private readonly ConcurrentDictionary _connections = new(); + private readonly ConcurrentDictionary _subscriptions = new(); + private readonly object _lockObject = new(); + private int _nextRequestId = 0; + + public event EventHandler? ConnectionStateChanged; + public event EventHandler? OnWsError; + + public async Task ConnectAsync(string mintUrl, CancellationToken ct = default) + { + var normalized = _normalizeMintUrl(mintUrl); + + if (_connections.TryGetValue(normalized, out var existing)) + { + return existing; + } + + var connectionId = Guid.NewGuid().ToString(); + var wsUrl = GetWebSocketUrl(mintUrl); + + var clientWebSocket = new ClientWebSocket(); + await clientWebSocket.ConnectAsync(new Uri(wsUrl), ct); + + var connection = new WebsocketConnection + { + Id = connectionId, + MintUrl = normalized, + WebSocket = clientWebSocket, + State = WebSocketState.Open + }; + + _connections[normalized] = connection; + + _ = Task.Run(async () => await ListenForMessages(connection, ct), ct); + + OnConnectionStateChanged(connectionId, WebSocketState.Open); + + return connection; + } + + public async Task DisconnectAsync(string mintUrl, CancellationToken ct = default) + { + var normalized = _normalizeMintUrl(mintUrl); + + if (!_connections.TryGetValue(normalized, out var connection)) + { + return; + } + + try + { + if (connection.State == WebSocketState.Open) + { + await connection.WebSocket.CloseAsync(WebSocketCloseStatus.NormalClosure, "Client disconnecting", ct); + } + } + catch (Exception) + { + // Ignore close exceptions + } + finally + { + connection.WebSocket.Dispose(); + _connections.TryRemove(normalized, out _); + + var subscriptionsToRemove = _subscriptions + .Where(s => s.Value.ConnectionId == connection.Id) + .Select(s => s.Key) + .ToList(); + + foreach (var subId in subscriptionsToRemove) + { + _subscriptions.TryRemove(subId, out _); + } + + OnConnectionStateChanged(connection.Id, WebSocketState.Closed); + } + } + + public async Task SubscribeAsync(string mintUrl, SubscriptionKind kind, string[] filters, CancellationToken ct = default) + { + var normalized = _normalizeMintUrl(mintUrl); + + if (!_connections.TryGetValue(normalized, out var connection)) + { + throw new InvalidOperationException($"Connection for mint {mintUrl} not found"); + } + + if (connection.State != WebSocketState.Open) + { + throw new InvalidOperationException($"Connection for mint {mintUrl} is not open"); + } + + var subId = Guid.NewGuid().ToString(); + var requestId = GetNextRequestId(); + + var channel = Channel.CreateUnbounded(new UnboundedChannelOptions { SingleReader = false }); + + var request = new WsRequest + { + JsonRpc = "2.0", + Method = WsRequestMethod.subscribe, + Params = new WsRequestParams + { + Kind = kind, + SubId = subId, + Filters = filters + }, + Id = requestId + }; + + var subscription = new Subscription + { + Id = subId, + ConnectionId = connection.Id, + Kind = kind, + Filters = filters, + CreatedAt = DateTime.UtcNow, + NotificationChannel = channel, + }; + + _subscriptions[subId] = subscription; + + await SendMessageAsync(connection, request, ct); + + return subscription; + } + + public async Task UnsubscribeAsync(string subId, CancellationToken ct = default) + { + if (!_subscriptions.TryGetValue(subId, out var subscription)) + throw new InvalidOperationException($"Subscription {subId} not found"); + + if (_connections.Values.FirstOrDefault(c => c.Id == subscription.ConnectionId) is not { } connection) + throw new InvalidOperationException($"Connection for subscription {subId} not found"); + + + if (connection.State != WebSocketState.Open) + { + throw new InvalidOperationException($"Connection is not open"); + } + + var requestId = GetNextRequestId(); + + var request = new WsRequest + { + JsonRpc = "2.0", + Method = WsRequestMethod.unsubscribe, + Params = new WsRequestParams + { + SubId = subId + }, + Id = requestId + }; + + await SendMessageAsync(connection, request, ct); + + _subscriptions.TryRemove(subId, out _); + } + + public async ValueTask DisposeAsync() + { + foreach (var sub in _subscriptions.Values) + { + sub.Close(); + } + var mintUrls = _connections.Keys.ToList(); + foreach (var mintUrl in mintUrls) + { + await DisconnectAsync(mintUrl); + } + _subscriptions.Clear(); + _connections.Clear(); + } + + public WebSocketState GetConnectionState(string mintUrl) + { + var normalized = _normalizeMintUrl(mintUrl); + return _connections.TryGetValue(normalized, out var connection) + ? connection.State + : WebSocketState.None; + } + + public IEnumerable GetSubscriptions(string mintUrl) + { + var normalized = _normalizeMintUrl(mintUrl); + if (!_connections.TryGetValue(normalized, out var connection)) + { + throw new Exception($"Connection for mint {mintUrl} not found"); + } + return _subscriptions.Values.Where(s => s.ConnectionId == connection.Id); + } + + public IEnumerable GetConnections() + { + return _connections.Values; + } + + private async Task ListenForMessages(WebsocketConnection connection, CancellationToken ct) + { + var buffer = new byte[4096]; + + try + { + while (connection.State == WebSocketState.Open && !ct.IsCancellationRequested) + { + var result = await connection.WebSocket.ReceiveAsync(new ArraySegment(buffer), ct); + + if (result.MessageType == WebSocketMessageType.Close) + { + connection.State = WebSocketState.Closed; + OnConnectionStateChanged(connection.Id, WebSocketState.Closed); + break; + } + + if (result.MessageType == WebSocketMessageType.Text) + { + var message = Encoding.UTF8.GetString(buffer, 0, result.Count); + await ProcessMessage(connection, message); + } + } + } + catch (Exception ex) + { + connection.State = WebSocketState.Aborted; + OnConnectionStateChanged(connection.Id, WebSocketState.Aborted); + } + } + + private async Task ProcessMessage(WebsocketConnection connection, string message) + { + try + { + var jsonElement = JsonSerializer.Deserialize(message); + + if (jsonElement.TryGetProperty("method", out var methodProp) && + methodProp.GetString() == "subscribe") + { + var notification = JsonSerializer.Deserialize(message); + if (notification != null) + { + _onNotificationReceived(notification.Params); + } + } + else if (jsonElement.TryGetProperty("result", out _)) + { + var response = JsonSerializer.Deserialize(message); + // TODO: Handle response + } + else if (jsonElement.TryGetProperty("error", out _)) + { + var error = JsonSerializer.Deserialize(message); + // TODO: Handle error + } + } + catch (Exception ex) + { + // TODO: Log exception + } + } + + private async Task SendMessageAsync(WebsocketConnection connection, T message, CancellationToken ct) + { + var json = JsonSerializer.Serialize(message); + var bytes = Encoding.UTF8.GetBytes(json); + + await connection.WebSocket.SendAsync( + new ArraySegment(bytes), + WebSocketMessageType.Text, + true, + ct); + } + + private string GetWebSocketUrl(string mintUrl) + { + var uri = new Uri(_normalizeMintUrl(mintUrl)); + var scheme = uri.Scheme == "https" ? "wss" : "ws"; + var hostPort = (uri.IsDefaultPort) ? uri.Host : $"{uri.Host}:{uri.Port}"; + var path = uri.AbsolutePath.TrimEnd('/'); + return $"{scheme}://{hostPort}{path}/v1/ws"; + } + + private int GetNextRequestId() + { + lock (_lockObject) + { + return ++_nextRequestId; + } + } + + + private void OnConnectionStateChanged(string connectionId, WebSocketState state) + { + ConnectionStateChanged?.Invoke(this, new ConnectionStateChangedEventArgs + { + ConnectionId = connectionId, + State = state + }); + } + + public bool IsConnected(string mintUrl) + => _connections.ContainsKey(_normalizeMintUrl(mintUrl)); + + private string _normalizeMintUrl(string mintUrl) + { + if (!Uri.TryCreate(mintUrl.TrimEnd('/'), UriKind.Absolute, out var uri)) + { + return mintUrl.TrimEnd('/').ToLowerInvariant(); + } + var host = uri.Host.ToLowerInvariant(); + var builder = new UriBuilder(uri) { Host = host }; + return builder.Uri.ToString().TrimEnd('/'); + } + + private void _onNotificationReceived(WsNotificationParams notificationParams) + { + if (!_subscriptions.TryGetValue(notificationParams.SubId, out var sub)) + { + //it should never happen + return; + } + sub.NotificationChannel.Writer.WriteAsync(notificationParams); + } + + private WebsocketConnection? _getConnectionById(string connectionId) + { + return this._connections.Values.SingleOrDefault(c=>c.Id == connectionId, null); + } +} \ No newline at end of file diff --git a/DotNut/Abstractions/Websockets/WebsocketServiceExtensions.cs b/DotNut/Abstractions/Websockets/WebsocketServiceExtensions.cs index 168b9e0..884b1af 100644 --- a/DotNut/Abstractions/Websockets/WebsocketServiceExtensions.cs +++ b/DotNut/Abstractions/Websockets/WebsocketServiceExtensions.cs @@ -2,58 +2,58 @@ namespace DotNut.Abstractions.Websockets; public static class WebsocketServiceExtensions { - public static async Task SubscribeToMintQuoteAsync( + public static async Task SubscribeToMintQuoteAsync( this IWebsocketService service, - string connectionId, + string mintUrl, string[] quoteIds, - CancellationToken cancellationToken = default) + CancellationToken ct = default) { - return await service.SubscribeAsync(connectionId, SubscriptionKind.bolt11_mint_quote, quoteIds, cancellationToken); + return await service.SubscribeAsync(mintUrl, SubscriptionKind.bolt11_mint_quote, quoteIds, ct); } - public static async Task SubscribeToMeltQuoteAsync( + public static async Task SubscribeToMeltQuoteAsync( this IWebsocketService service, - string connectionId, + string mintUrl, string[] quoteIds, - CancellationToken cancellationToken = default) + CancellationToken ct = default) { - return await service.SubscribeAsync(connectionId, SubscriptionKind.bolt11_melt_quote, quoteIds, cancellationToken); + return await service.SubscribeAsync(mintUrl, SubscriptionKind.bolt11_melt_quote, quoteIds, ct); } - public static async Task SubscribeToProofStateAsync( + public static async Task SubscribeToProofStateAsync( this IWebsocketService service, - string connectionId, + string mintUrl, string[] proofYs, - CancellationToken cancellationToken = default) + CancellationToken ct = default) { - return await service.SubscribeAsync(connectionId, SubscriptionKind.proof_state, proofYs, cancellationToken); + return await service.SubscribeAsync(mintUrl, SubscriptionKind.proof_state, proofYs, ct); } - public static async Task SubscribeToSingleProofStateAsync( + public static async Task SubscribeToSingleProofStateAsync( this IWebsocketService service, - string connectionId, + string mintUrl, string proofY, - CancellationToken cancellationToken = default) + CancellationToken ct = default) { - return await service.SubscribeToProofStateAsync(connectionId, new[] { proofY }, cancellationToken); + return await service.SubscribeToProofStateAsync(mintUrl, new[] { proofY }, ct); } - public static async Task SubscribeToSingleMintQuoteAsync( + public static async Task SubscribeToSingleMintQuoteAsync( this IWebsocketService service, - string connectionId, + string mintUrl, string quoteId, - CancellationToken cancellationToken = default) + CancellationToken ct = default) { - return await service.SubscribeToMintQuoteAsync(connectionId, new[] { quoteId }, cancellationToken); + return await service.SubscribeToMintQuoteAsync(mintUrl, new[] { quoteId }, ct); } - public static async Task SubscribeToSingleMeltQuoteAsync( + public static async Task SubscribeToSingleMeltQuoteAsync( this IWebsocketService service, - string connectionId, + string mintUrl, string quoteId, - CancellationToken cancellationToken = default) + CancellationToken ct = default) { - return await service.SubscribeToMeltQuoteAsync(connectionId, new[] { quoteId }, cancellationToken); + return await service.SubscribeToMeltQuoteAsync(mintUrl, new[] { quoteId }, ct); } public static bool IsConnectionActive(this IWebsocketService service, string connectionId) @@ -73,12 +73,12 @@ public static IEnumerable GetSubscriptionsByKind( public static async Task UnsubscribeAllAsync( this IWebsocketService service, string connectionId, - CancellationToken cancellationToken = default) + CancellationToken ct = default) { var subscriptions = service.GetSubscriptions(connectionId).ToList(); foreach (var subscription in subscriptions) { - await service.UnsubscribeAsync(connectionId, subscription.Id, cancellationToken); + await service.UnsubscribeAsync(subscription.Id, ct); } } @@ -86,12 +86,12 @@ public static async Task UnsubscribeByKindAsync( this IWebsocketService service, string connectionId, SubscriptionKind kind, - CancellationToken cancellationToken = default) + CancellationToken ct = default) { var subscriptions = service.GetSubscriptionsByKind(connectionId, kind).ToList(); foreach (var subscription in subscriptions) { - await service.UnsubscribeAsync(connectionId, subscription.Id, cancellationToken); + await service.UnsubscribeAsync(subscription.Id, ct); } } } diff --git a/DotNut/Api/CashuHttpClient.cs b/DotNut/Api/CashuHttpClient.cs index a62fc6b..32a9694 100644 --- a/DotNut/Api/CashuHttpClient.cs +++ b/DotNut/Api/CashuHttpClient.cs @@ -12,9 +12,15 @@ public class CashuHttpClient : ICashuApi public CashuHttpClient(HttpClient httpClient) { + ArgumentNullException.ThrowIfNull(httpClient.BaseAddress); _httpClient = httpClient; } + public string GetBaseUrl() + { + ArgumentNullException.ThrowIfNull(_httpClient.BaseAddress); + return _httpClient.BaseAddress.AbsoluteUri; + } public async Task GetKeys(CancellationToken cancellationToken = default) { var response = await _httpClient.GetAsync("v1/keys", cancellationToken); diff --git a/DotNut/Api/ICashuApi.cs b/DotNut/Api/ICashuApi.cs index e7696b4..c73252d 100644 --- a/DotNut/Api/ICashuApi.cs +++ b/DotNut/Api/ICashuApi.cs @@ -4,6 +4,7 @@ namespace DotNut.Api; public interface ICashuApi { + string GetBaseUrl(); Task GetKeys(CancellationToken cancellationToken = default); Task GetKeys(KeysetId keysetId, CancellationToken cancellationToken = default); Task GetKeysets(CancellationToken cancellationToken = default); From 90ef68fb76de74860c9af89b78679bd11dec5d49 Mon Sep 17 00:00:00 2001 From: d4rp4t Date: Fri, 24 Oct 2025 12:45:56 +0200 Subject: [PATCH 13/70] Nut10 Payment Request --- DotNut.Nostr/NostrNip17PaymentRequestInterfaceHandler.cs | 2 +- DotNut/NUT11/SigAllHandler.cs | 5 +---- DotNut/NUT18/Nut10LockingCondition.cs | 8 ++++++++ DotNut/NUT18/PaymentRequestEncoder.cs | 9 ++++----- DotNut/NUT18/PaymentRequestPayload.cs | 1 - 5 files changed, 14 insertions(+), 11 deletions(-) create mode 100644 DotNut/NUT18/Nut10LockingCondition.cs diff --git a/DotNut.Nostr/NostrNip17PaymentRequestInterfaceHandler.cs b/DotNut.Nostr/NostrNip17PaymentRequestInterfaceHandler.cs index 28d25b4..461d5c5 100644 --- a/DotNut.Nostr/NostrNip17PaymentRequestInterfaceHandler.cs +++ b/DotNut.Nostr/NostrNip17PaymentRequestInterfaceHandler.cs @@ -34,7 +34,7 @@ public async Task SendPayment(PaymentRequest request, PaymentRequestPayload payl var nprofileStr = nostrTransport.Target; var nprofile = (NIP19.NosteProfileNote) NIP19.FromNIP19Note(nprofileStr); - using var client = new CompositeNostrClient(nprofile.Relays.Select(r => new Uri(r)).ToArray()); + using var client = new CompositeNostrClient(nprofile.Relays.Select(r => new Uri(r)).ToArray()); await client.Connect(cancellationToken); var ephemeralKey = ECPrivKey.Create(RandomNumberGenerator.GetBytes(32)); var msg = new NostrEvent() diff --git a/DotNut/NUT11/SigAllHandler.cs b/DotNut/NUT11/SigAllHandler.cs index 3c67aac..725a653 100644 --- a/DotNut/NUT11/SigAllHandler.cs +++ b/DotNut/NUT11/SigAllHandler.cs @@ -13,9 +13,7 @@ public class SigAllHandler public List Proofs { get; set; } public List PrivKeys { get; set; } public List BlindedMessages { get; set; } - public string? HTLCPreimage { get; set; } - public string? MeltQuoteId { get; set; } private P2PKProofSecret? _firstProofSecret; @@ -77,8 +75,6 @@ public bool TrySign(out P2PKWitness? p2pkwitness) return true; } - - private bool _validateFirstProof() { if (Proofs[0].Secret is not Nut10Secret { ProofSecret: P2PKProofSecret p2pks }) @@ -94,6 +90,7 @@ private bool _validateFirstProof() return true; } + private bool _checkIfEqualToFirst(P2PKProofSecret other) => _firstProofSecret is { } a && other is { } b && a.Data == b.Data && diff --git a/DotNut/NUT18/Nut10LockingCondition.cs b/DotNut/NUT18/Nut10LockingCondition.cs new file mode 100644 index 0000000..ba89386 --- /dev/null +++ b/DotNut/NUT18/Nut10LockingCondition.cs @@ -0,0 +1,8 @@ +namespace DotNut; + +public class Nut10LockingCondition +{ + public string Kind { get; set; } + public string Data { get; set; } + public Tag[]? Tags { get; set; } +} \ No newline at end of file diff --git a/DotNut/NUT18/PaymentRequestEncoder.cs b/DotNut/NUT18/PaymentRequestEncoder.cs index 5fde53b..d29e88b 100644 --- a/DotNut/NUT18/PaymentRequestEncoder.cs +++ b/DotNut/NUT18/PaymentRequestEncoder.cs @@ -1,4 +1,4 @@ -using PeterO.Cbor; +using PeterO.Cbor; namespace DotNut; @@ -46,6 +46,8 @@ public CBORObject ToCBORObject(PaymentRequest paymentRequest) transports.Add(transportItem); } + cbor.Add("t", transports); + if (paymentRequest.Nut10 is not null) { var nut10Obj = CBORObject.NewMap(); @@ -68,8 +70,6 @@ public CBORObject ToCBORObject(PaymentRequest paymentRequest) } cbor.Add("nut10", nut10Obj); } - - cbor.Add("t", transports); return cbor; } @@ -155,7 +155,6 @@ public PaymentRequest FromCBORObject(CBORObject obj) break; } } - return paymentRequest; } -} \ No newline at end of file +} diff --git a/DotNut/NUT18/PaymentRequestPayload.cs b/DotNut/NUT18/PaymentRequestPayload.cs index 4fe46fc..bef5663 100644 --- a/DotNut/NUT18/PaymentRequestPayload.cs +++ b/DotNut/NUT18/PaymentRequestPayload.cs @@ -8,6 +8,5 @@ public class PaymentRequestPayload [JsonPropertyName("memo")] public string? Memo { get; set; } [JsonPropertyName("mint")] public string Mint { get; set; } [JsonPropertyName("unit")] public string Unit { get; set; } - [JsonPropertyName("proofs")] public Proof[] Proofs { get; set; } } \ No newline at end of file From f624a3e68a88679e19b1a8df2776b571e18224e4 Mon Sep 17 00:00:00 2001 From: d4rp4t Date: Fri, 24 Oct 2025 17:13:16 +0200 Subject: [PATCH 14/70] NUT20 --- .../Handlers/MintHandlerBolt11.cs | 27 +++++++++++-- .../Handlers/MintHandlerBolt12.cs | 40 +++++++++++++------ .../Abstractions/Interfaces/IMintHandler.cs | 6 ++- DotNut/Abstractions/MintQuoteBuilder.cs | 6 +-- DotNut/ApiModels/Mint/PostMintRequest.cs | 4 ++ .../Mint/bolt11/PostMintQuoteBolt11Request.cs | 6 ++- .../bolt11/PostMintQuoteBolt11Response.cs | 4 ++ DotNut/NUT20/MintQuoteSigner.cs | 20 ++++++++++ 8 files changed, 92 insertions(+), 21 deletions(-) create mode 100644 DotNut/NUT20/MintQuoteSigner.cs diff --git a/DotNut/Abstractions/Handlers/MintHandlerBolt11.cs b/DotNut/Abstractions/Handlers/MintHandlerBolt11.cs index 2548a50..922d4b8 100644 --- a/DotNut/Abstractions/Handlers/MintHandlerBolt11.cs +++ b/DotNut/Abstractions/Handlers/MintHandlerBolt11.cs @@ -12,8 +12,8 @@ public class MintHandlerBolt11: IMintHandler> WithSignature(string signature) + { + _signature = signature; + return this; + } + + public IMintHandler> SignWithPrivkey(string privKeyHex) + { + return this.SignWithPrivkey(new PrivKey(privKeyHex)); + } + + public IMintHandler> SignWithPrivkey(PrivKey privkey) + { + this._signature = privkey.SignMintQuote(_quote.Quote, this._outputs.BlindedMessages); + return this; + } + public async Task GetQuote(CancellationToken ct = default) => _quote; public async Task> Mint(CancellationToken ct = default) { + if (this._quote.PubKey is not null && this._signature is null) + { + throw new ArgumentNullException(nameof(_signature),$"Signature for mint quote {this._quote.Quote} is required!" ); + } var client = await this._wallet.GetMintApi(); var req = new PostMintRequest { Outputs = this._outputs.BlindedMessages.ToArray(), - Quote = _quote.Quote + Quote = _quote.Quote, }; var promises= await client.Mint("bolt11", req, ct); diff --git a/DotNut/Abstractions/Handlers/MintHandlerBolt12.cs b/DotNut/Abstractions/Handlers/MintHandlerBolt12.cs index d15ceaa..a123e38 100644 --- a/DotNut/Abstractions/Handlers/MintHandlerBolt12.cs +++ b/DotNut/Abstractions/Handlers/MintHandlerBolt12.cs @@ -1,9 +1,10 @@ +using System.Xml; using DotNut.Abstractions.Websockets; using DotNut.Api; using DotNut.ApiModels; using DotNut.ApiModels.Melt.bolt12; using DotNut.ApiModels.Mint.bolt12; - +using DotNut; namespace DotNut.Abstractions; public class MintHandlerBolt12: IMintHandler> @@ -13,7 +14,7 @@ public class MintHandlerBolt12: IMintHandler> WithSignature(string signature) + { + _signature = signature; + return this; + } + + public IMintHandler> SignWithPrivkey(string privKeyHex) + { + return this.SignWithPrivkey(new PrivKey(privKeyHex)); + } + + public IMintHandler> SignWithPrivkey(PrivKey privkey) + { + this._signature = privkey.SignMintQuote(_quote.Quote, this._outputs.BlindedMessages); + return this; + } + public async Task GetQuote(CancellationToken ct = default) => this._quote; + public async Task> Mint(CancellationToken ct = default) { - var client = await this._wallet.GetMintApi(); + if (this._signature is null) + { + throw new ArgumentNullException(nameof(this._signature), $"Signature for mint quote {this._quote.Quote} is required!"); + } + var client = await this._wallet.GetMintApi(); var req = new PostMintRequest { Outputs = this._outputs.BlindedMessages.ToArray(), - Quote = _quote.Quote + Quote = _quote.Quote, + Signature = _signature, }; var promises= await client.Mint("bolt12", req, ct); return CashuUtils.ConstructProofsFromPromises(promises.Signatures.ToList(), _outputs, _keyset.Keys); } - - private async Task _processMint(PostMintRequest req, CancellationToken cts = default) - { - var client = await this._wallet.GetMintApi(); - - return await client.Mint("bolt12", req, cts); - } - } \ No newline at end of file diff --git a/DotNut/Abstractions/Interfaces/IMintHandler.cs b/DotNut/Abstractions/Interfaces/IMintHandler.cs index 53973f0..c3ff990 100644 --- a/DotNut/Abstractions/Interfaces/IMintHandler.cs +++ b/DotNut/Abstractions/Interfaces/IMintHandler.cs @@ -1,10 +1,12 @@ -using DotNut.Abstractions.Websockets; - namespace DotNut.Abstractions; public interface IMintHandler; public interface IMintHandler: IMintHandler { + public IMintHandler WithSignature(string signature); + public IMintHandler SignWithPrivkey(PrivKey privkey); + public IMintHandler SignWithPrivkey(string privKeyHex); + Task GetQuote(CancellationToken ct = default); Task Mint(CancellationToken ct = default); } \ No newline at end of file diff --git a/DotNut/Abstractions/MintQuoteBuilder.cs b/DotNut/Abstractions/MintQuoteBuilder.cs index b5887b5..4ba4385 100644 --- a/DotNut/Abstractions/MintQuoteBuilder.cs +++ b/DotNut/Abstractions/MintQuoteBuilder.cs @@ -17,7 +17,6 @@ class MintQuoteBuilder : IMintQuoteBuilder private OutputData? _outputs; private string? _method = "bolt11"; - //for bolt12 private string? _pubkey; private KeysetId? _keysetId; @@ -65,8 +64,8 @@ public IMintQuoteBuilder WithUnit(string unit) } /// - /// Optional. Necessary for bolt12 - /// Sets pubkey for bolt12 offer + /// Optional. If specified, to mint the tokens user needs to provide signature on mint quote. + /// Necessary for bolt12 /// /// /// @@ -163,6 +162,7 @@ public async Task>> Proces Amount = this._amount.Value, Unit = this._unit, Description = this._description, + Pubkey = this._pubkey??null, }; var quoteBolt11 = await api.CreateMintQuote("bolt11", reqBolt11, diff --git a/DotNut/ApiModels/Mint/PostMintRequest.cs b/DotNut/ApiModels/Mint/PostMintRequest.cs index 9767af1..4cfd4a2 100644 --- a/DotNut/ApiModels/Mint/PostMintRequest.cs +++ b/DotNut/ApiModels/Mint/PostMintRequest.cs @@ -9,4 +9,8 @@ public class PostMintRequest [JsonPropertyName("outputs")] public BlindedMessage[] Outputs { get; set; } + + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("signature")] + public string? Signature { get; set; } } \ No newline at end of file diff --git a/DotNut/ApiModels/Mint/bolt11/PostMintQuoteBolt11Request.cs b/DotNut/ApiModels/Mint/bolt11/PostMintQuoteBolt11Request.cs index 53b91ce..0faca17 100644 --- a/DotNut/ApiModels/Mint/bolt11/PostMintQuoteBolt11Request.cs +++ b/DotNut/ApiModels/Mint/bolt11/PostMintQuoteBolt11Request.cs @@ -12,7 +12,11 @@ public class PostMintQuoteBolt11Request [JsonPropertyName("unit")] public string Unit {get; set;} - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("pubkey")] + public string? Pubkey {get; set;} + + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] [JsonPropertyName("description")] public string? Description {get; set;} } \ No newline at end of file diff --git a/DotNut/ApiModels/Mint/bolt11/PostMintQuoteBolt11Response.cs b/DotNut/ApiModels/Mint/bolt11/PostMintQuoteBolt11Response.cs index 8980825..cdbeadb 100644 --- a/DotNut/ApiModels/Mint/bolt11/PostMintQuoteBolt11Response.cs +++ b/DotNut/ApiModels/Mint/bolt11/PostMintQuoteBolt11Response.cs @@ -25,4 +25,8 @@ public class PostMintQuoteBolt11Response [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] [JsonPropertyName("unit")] public string? Unit {get; set;} + + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("pubkey")] + public string PubKey {get; set;} } \ No newline at end of file diff --git a/DotNut/NUT20/MintQuoteSigner.cs b/DotNut/NUT20/MintQuoteSigner.cs new file mode 100644 index 0000000..630e68e --- /dev/null +++ b/DotNut/NUT20/MintQuoteSigner.cs @@ -0,0 +1,20 @@ +using System.Text; +using SHA256 = System.Security.Cryptography.SHA256; + +namespace DotNut; + +public static class MintQuoteSigner +{ + public static string SignMintQuote(this PrivKey pk, string quote, List blindedMessages) + { + var sb = new StringBuilder(); + sb.Append(quote); + foreach (var blindedMessage in blindedMessages) + { + sb.Append(blindedMessage.B_); + } + var bytes = Encoding.UTF8.GetBytes(sb.ToString()); + var hash = SHA256.HashData(bytes); + return pk.Key.SignBIP340(hash).ToHex(); + } +} \ No newline at end of file From 05f57decbce5d371c41e76ee21e3a5f4279528d4 Mon Sep 17 00:00:00 2001 From: d4rp4t Date: Fri, 24 Oct 2025 17:13:32 +0200 Subject: [PATCH 15/70] sig_all v2 --- DotNut.Tests/Integration.cs | 128 +++++--- DotNut.Tests/UnitTest1.cs | 20 +- DotNut.Tests/UnitTests2.cs | 20 +- .../Handlers/MeltHandlerBolt11.cs | 73 ++++- .../Handlers/MintHandlerBolt11.cs | 2 +- .../Handlers/MintHandlerBolt12.cs | 2 +- .../Abstractions/Interfaces/IWalletBuilder.cs | 131 +++++++- .../Interfaces/IWebsocketService.cs | 6 +- DotNut/Abstractions/MeltQuoteBuilder.cs | 58 +--- DotNut/Abstractions/MintInfo.cs | 1 - .../Abstractions/MintMeltDisabledException.cs | 3 - DotNut/Abstractions/MintQuoteBuilder.cs | 4 +- DotNut/Abstractions/RestoreBuilder.cs | 8 +- DotNut/Abstractions/SwapBuilder.cs | 27 +- .../Abstractions/{CashuUtils.cs => Utils.cs} | 2 +- DotNut/Abstractions/Wallet.cs | 95 ++---- .../Abstractions/Websockets/Subscription.cs | 16 +- .../Abstractions/Websockets/WebsocketEnums.cs | 2 + .../Websockets/WebsocketModels.cs | 35 ++- .../Websockets/WebsocketService.cs | 280 +++++++++++++----- .../Websockets/WebsocketServiceExtensions.cs | 6 + .../bolt11/PostMintQuoteBolt11Response.cs | 2 +- DotNut/NUT11/P2PKProofSecret.cs | 10 + DotNut/NUT11/SigAllHandler.cs | 146 +++++++-- DotNut/NUT14/HTLCBuilder.cs | 1 + 25 files changed, 729 insertions(+), 349 deletions(-) delete mode 100644 DotNut/Abstractions/MintMeltDisabledException.cs rename DotNut/Abstractions/{CashuUtils.cs => Utils.cs} (99%) diff --git a/DotNut.Tests/Integration.cs b/DotNut.Tests/Integration.cs index 3dba755..8429d65 100644 --- a/DotNut.Tests/Integration.cs +++ b/DotNut.Tests/Integration.cs @@ -16,10 +16,12 @@ public class Integration private static string seed = "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about"; - + + // for now cdk mint returns 500 if there's created melt quote for the same invoice twice private static readonly Dictionary valuesInvoices = new Dictionary() { - {500, "lnbc5u1p50xs65sp5rqewm2jqcddhnynncdx7gtz8qh7q6c9a2tlv6u2efa5qrltla9jqpp5raqnnlucn27y3lswuqafutrnsctcglr5ldv74009jp86cfv6pjyqhp5fszwn06y05csgs2mnn7yn6kn6j9d7m5fv6rw72m8hkp7re0zfflqxq9z0rgqcqpnrzjqdq8jm79ttkfnk83gfjee4n7ryyqzq9f36s5azgk2ftcndt7q48txr0hdyqqdcgqqqqqqqlgqqqqzycqyg9qxpqysgqthz50sp4xdtv2afwj294fd45e4s8q4ptqrn092v36zrs57wyur65lcdkxp53cza9an8z0drxw5lgdcay78plgmfle72vrtjp5266xlgqzsn4ph"}, + {500, "lnbc5u1p5sh0yvsp53seej3qkkxe6xxk9mufaj7y3jc9s9kvfn4g3whppwqcl4vcjraaspp5vtv793xc9ksch8zekkhqtv54a2evh7vq4zuywcmk9nzt69qma5yqhp5uwcvgs5clswpfxhm7nyfjmaeysn6us0yvjdexn9yjkv3k7zjhp2sxq9z0rgqcqpnrzjq0ly0l075re9ltgqzdycartvas6g4c7kwwzpasj7a98c0ss679hdsr080vqqdcgqqqqqqqqnqqqqryqqxv9qxpqysgqwq50283v8asna95fktaeg80kq9evs0chaw44y6y649qsql9vsfc5gfcsp8rdwwyccepwy83n7g0s25n3lpv3hjgcr220n5w806fja8gp2xjvd7"}, + {501, "lnbc5010n1p5shs9rsp5a2qhmn05xsd8vcm5jx9v2aswkz0pxguk4jqlaxsazzcg5rduan2qpp5al2k5zwruvlx34sxxdys2sj696m58uqgjvzxxrxhvuyswhmzg5cqhp5uwcvgs5clswpfxhm7nyfjmaeysn6us0yvjdexn9yjkv3k7zjhp2sxq9z0rgqcqpnrzjqw7c9dkkx4nur9sztw2zzpzj8u8rgsqgdsykylg5pwplh26824lc7rvlqcqqn3gqqyqqqqlgqqqqqqgq2q9qxpqysgqgpj2x2aw2dv5tzhx86th6a5vutpcdxz9htewqgvzjgqkzwmh6xs5mw5xcgrzyq77f35shv0gg5ygtjmn7e73wg8v0a9g836ufszdxmqqqu3642"}, {1000, "lnbc10u1p5w6vggsp5gn5xhswgn5299w6elu2z0vzjxhf9hwd6pwjcgfwphaxunyu0dx6spp5a60trrhce2u6tzqjwjczem8rpdesgzkawcqg2xqaesz2kd50z4uqhp5uwcvgs5clswpfxhm7nyfjmaeysn6us0yvjdexn9yjkv3k7zjhp2sxq9z0rgqcqpnrzjqw22kc09xj0dm65ew5h5r003vtn72eyzchdgjag66l0yhwdudfmuzrwvesqq8qgqqgqqqqqqqqqqzhsq2q9qxpqysgq955gcfr95wwz0ehtnk3xraatkyhj88z44ku7yqutnwnt3gkh82jxehvdff7n2js2p54jgpvg6dmwmq8t9d8x05j63mqjrsr4cwd4lpcpnc39ru"}, {2000, "lnbc20u1p5094fksp54vrdcymel5awhrpc0m6z4kvhhyvqlwkshkyt2wr6eyljkz8c798qpp59f2vc8td8tu62gtf4qfwzkrkxedsey7a5ajrd48a25z2kkwg407shp5nklhn663zgwcdnh7pe5jxt6td0cchhre6hxzdxrjdlfwtpq60f5sxq9z0rgqcqpnrzjqw0de9yc0j8n4hpgm269tm7qph4gwcyf5ys02uaapvpugrva87c7zr045uqq4jsqpsqqqqlgqqqqrcgq2q9qxpqysgq6g2pamgjumh6uw5k5rj2ket44wh8nfzs5gzyygl54hu5cefuxdhxp9h5mrg64rh07znktn9x9d5vg6fc0rw7m63x8rg4qk3kw6d8sycpywn48m"}, }; @@ -71,7 +73,7 @@ public async Task MintsSuccessfully() var mintResponse = await mintQuote.Mint(); Assert.NotNull(mintResponse); - Assert.Equal(1337UL, CashuUtils.SumProofs(mintResponse)); + Assert.Equal(1337UL, Utils.SumProofs(mintResponse)); } [Fact] @@ -115,7 +117,7 @@ public async Task RestoresSuccessfully() .WithSwap(false) .ProcessAsync(); var keyset = (await wallet.GetKeys()).First().Keys; - var expectedAmount = CashuUtils.SplitToProofsAmounts(1337UL, keyset).Count; + var expectedAmount = Utils.SplitToProofsAmounts(1337UL, keyset).Count; Assert.Equal(expectedAmount, restoredProofs.Count()); } @@ -288,8 +290,8 @@ await Assert.ThrowsAsync( .ProcessAsync() ); - var swappedProofs = await wallet. - Swap() + var swappedProofs = await wallet + .Swap() .FromInputs(proofs) .WithPrivkeys([privKeyBob]) .ProcessAsync(); @@ -334,61 +336,89 @@ await Assert.ThrowsAsync(async () => var handler = await wallet .CreateMeltQuote() - .WithInvoice(valuesInvoices[500]) + .WithInvoice(valuesInvoices[501]) .WithPrivkeys([privKeyBob, privKeyAlice]) .ProcessAsyncBolt11(); - var selectorResponse = await wallet.SelectProofsToSend(proofs, 500UL, true); + var q = await handler.GetQuote(); + + var amountToPay = q.Amount + (ulong)q.FeeReserve; + var selectorResponse = await wallet.SelectProofsToSend(proofs, amountToPay, true); var change = await handler.Melt(selectorResponse.Send); Assert.NotEmpty(change); } - [Fact] - public async Task SubscribeToMintMeltQuoteUpdates() - { - // initialize websocket service. it will be a singleton normally. - WebsocketService service = new WebsocketService(); - var connection = await service.ConnectAsync(MintUrl); - Assert.NotNull(connection); - - // create mint quote - var wallet = Wallet - .Create() - .WithMint(MintUrl); +[Fact] +public async Task SubscribeToMintMeltQuoteUpdates() +{ + await using var service = new WebsocketService(); + var connection = await service.ConnectAsync(MintUrl); + Assert.NotNull(connection); - var mintHandler = await wallet - .CreateMintQuote() - .WithAmount(3338) - .WithUnit("sat") - .ProcessAsyncBolt11(); + var wallet = Wallet.Create().WithMint(MintUrl); - var quote = await mintHandler.GetQuote(); + var mintHandler = await wallet + .CreateMintQuote() + .WithAmount(3338) + .WithUnit("sat") + .ProcessAsyncBolt11(); - var sub = await service.SubscribeToMintQuoteAsync(MintUrl, [quote.Quote]); + var quote = await mintHandler.GetQuote(); - int ctr = 0; - var callback = () => ctr++; - await foreach (var msg in sub.NotificationChannel.Reader.ReadAllAsync()) - { - callback(); - if (ctr > 1) - { - Assert.Equal(sub.Id, msg.SubId); - Assert.True(msg.Payload is PostMeltQuoteBolt11Response); - break; - } - } - - // payQuote - await PayInvoice(); - - Assert.True(ctr > 1); - - var proofs = await mintHandler.Mint(); - - - } + var sub = await service.SubscribeToMintQuoteAsync(MintUrl, new[] { quote.Quote }); + + int connectedCount = 0; + int notificationCount = 0; + + using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(60)); + + var connectedTcs = new TaskCompletionSource(); + var paidTcs = new TaskCompletionSource(); + + _ = Task.Run(async () => + { + await connectedTcs.Task.WaitAsync(TimeSpan.FromSeconds(10)); + await Task.Delay(1000, cts.Token); + await PayInvoice(); + }, cts.Token); + + await foreach (var msg in sub.NotificationChannel.Reader.ReadAllAsync(cts.Token)) + { + switch (msg) + { + case WsMessage.Response: + connectedCount++; + connectedTcs.TrySetResult(); + break; + + case WsMessage.Notification notification: + notificationCount++; + + if (notificationCount >= 2) + paidTcs.TrySetResult(); + + break; + + case WsMessage.Error error: + Assert.Fail($"WebSocket error: {error}"); + break; + + default: + Assert.Fail($"Unexpected message type: {msg.GetType().Name}"); + break; + } + + if (paidTcs.Task.IsCompleted) + break; + } + + Assert.Equal(1, connectedCount); + Assert.True(notificationCount >= 2, $"Expected >=2 notifications, got {notificationCount}"); + + var proofs = await mintHandler.Mint(); + Assert.NotEmpty(proofs); +} private async Task PayInvoice() diff --git a/DotNut.Tests/UnitTest1.cs b/DotNut.Tests/UnitTest1.cs index 94b899b..f79ce0f 100644 --- a/DotNut.Tests/UnitTest1.cs +++ b/DotNut.Tests/UnitTest1.cs @@ -5,7 +5,6 @@ using NBip32Fast; using NBitcoin.Secp256k1; using Xunit.Abstractions; - namespace DotNut.Tests; public class UnitTest1 @@ -365,17 +364,17 @@ public void Nut11_Signatures() [Fact] public void Nut11_New_P2PkRules() { - // since https://github.com/cashubtc/nuts/pull/315 p2pk and htlc behavior will be changed. After locktime, the + // since https://github.com/cashubtc/nuts/pull/315 p2pk and htlc behavior will be changed. After locktime, the // proof will be spendable on both (refund and normal) paths. - - var spendableProof = + + var spendableProof = "{\n \"amount\": 64,\n \"C\": \"02d7cd858d866fca404b5cb1ffd813946e6d19efa1af00d654080fd20266bdc0b1\",\n \"id\": \"001b6c716bf42c7e\",\n \"secret\": \"[\\\"P2PK\\\",{\\\"nonce\\\":\\\"395162bf2d0add3c66aea9f22c45251dbee6e04bd9282addbb366a94cd4fb482\\\",\\\"data\\\":\\\"03ab50a667926fac858bac540766254c14b2b0334d10e8ec766455310224bbecf4\\\",\\\"tags\\\":[[\\\"locktime\\\",\\\"21\\\"],[\\\"pubkeys\\\",\\\"0229a91adec8dd9badb228c628a07fc1bf707a9b7d95dd505c490b1766fa7dc541\\\",\\\"033281c37677ea273eb7183b783067f5244933ef78d8c3f15b1a77cb246099c26e\\\"],[\\\"n_sigs\\\",\\\"2\\\"],[\\\"refund\\\",\\\"03ab50a667926fac858bac540766254c14b2b0334d10e8ec766455310224bbecf4\\\",\\\"033281c37677ea273eb7183b783067f5244933ef78d8c3f15b1a77cb246099c26e\\\"]]}]\"\n}"; var spendableProofParsed = JsonSerializer.Deserialize(spendableProof); Assert.NotNull(spendableProofParsed); var spendableProofSecret = Assert.IsType(spendableProofParsed.Secret); Assert.Equal(P2PKProofSecret.Key, spendableProofSecret.Key); var secretValue = Assert.IsType(spendableProofSecret.ProofSecret); - + // "standard path" witness, n_sigs = 2. // since locktime is expired, it would fail under old conditions. now the proof should remain spendable var validWitness1 = @@ -428,6 +427,14 @@ public void Nut11_SIG_ALL() var witness3 = JsonSerializer.Deserialize(validSwapRequestMultisigParsed.Inputs[0].Witness); Assert.True(SigAllHandler.VerifySigAllWitness(validSwapRequestMultisigParsed.Inputs, validSwapRequestMultisigParsed.Outputs, witness3)); Assert.True(SigAllHandler.VerifySigAllWitness(validSwapRequestMultisigParsed.Inputs, validSwapRequestMultisigParsed.Outputs)); + + var validSwapRequestMultisigRefund = + "{\n \"inputs\": [\n {\n \"amount\": 2,\n \"id\": \"00bfa73302d12ffd\",\n \"secret\": \"[\\\"P2PK\\\",{\\\"nonce\\\":\\\"3e9253419a11f0a541dd6baeddecf8356fc864b5d061f12f05632bc3aee6b5c4\\\",\\\"data\\\":\\\"0343cca0e48ce9e3fdcddba4637ff8cdbf6f5ed9cfdf1873e63827e760f0ed4db5\\\",\\\"tags\\\":[[\\\"pubkeys\\\",\\\"0235e0a719f8b046cee90f55a59b1cdd6ca75ce23e49cbcd82c9e5b7310e21ebcd\\\",\\\"020443f98b356e021bae82bdfc05ff433cab21e27fca9ab7b0995aedb2e7aabc43\\\"],[\\\"locktime\\\",\\\"100\\\"],[\\\"n_sigs\\\",\\\"2\\\"],[\\\"refund\\\",\\\"026b432e62b041bf9cdae534203739c73fa506c9a2d6aa58a52bc601a1dec421e1\\\",\\\"02e3494a2e07e7f6e7d4567e0da7a563592bff1e121df2383667f15b83e9168a9e\\\"],[\\\"n_sigs_refund\\\",\\\"2\\\"],[\\\"sigflag\\\",\\\"SIG_ALL\\\"]]}]\",\n \"C\": \"026c12ee3bffa5c617debcf823bf1af6a9b47145b699f2737bba3394f0893eb869\",\n \"witness\": \"{\\\"signatures\\\":[\\\"bfe884145ce6512331324321c3946dfd812428a53656b108b59d26559a186ba2ab45e5be9ce94e2dff0d09078e25ccb82d06a8b3a63cd3dc67065b8f77292776\\\",\\\"236e5cc9c30f85a893a29a4302e41e6f2015caef4229f28fa65e2f5c9d55515cc9a1852093a81a5095055d85fd55bf4da124e55354b56e0a39e83b58b0afc197\\\"]}\"\n }\n ],\n \"outputs\": [\n {\n \"amount\": 1,\n \"id\": \"00bfa73302d12ffd\",\n \"B_\": \"038ec853d65ae1b79b5cdbc2774150b2cb288d6d26e12958a16fb33c32d9a86c39\"\n },\n {\n \"amount\": 1,\n \"id\": \"00bfa73302d12ffd\",\n \"B_\": \"03afe7c87e32d436f0957f1d70a2bca025822a84a8623e3a33aed0a167016e0ca5\"\n }\n ]\n}"; + var validSwapRequestMultisigRefundParsed = + JsonSerializer.Deserialize(validSwapRequestMultisigRefund); + var witness4 = JsonSerializer.Deserialize(validSwapRequestMultisigRefundParsed.Inputs[0].Witness); + Assert.True(SigAllHandler.VerifySigAllWitness(validSwapRequestMultisigRefundParsed.Inputs, validSwapRequestMultisigRefundParsed.Outputs, witness4)); + Assert.True(SigAllHandler.VerifySigAllWitness(validSwapRequestMultisigRefundParsed.Inputs, validSwapRequestMultisigRefundParsed.Outputs)); var validSwapRequestMultisigRefundLocktime = "{\n \"inputs\": [\n {\n \"amount\": 2,\n \"id\": \"00bfa73302d12ffd\",\n \"secret\": \"[\\\"P2PK\\\",{\\\"nonce\\\":\\\"9ea35553beb18d553d0a53120d0175a0991ca6109370338406eed007b26eacd1\\\",\\\"data\\\":\\\"02af21e09300af92e7b48c48afdb12e22933738cfb9bba67b27c00c679aae3ec25\\\",\\\"tags\\\":[[\\\"locktime\\\",\\\"1\\\"],[\\\"refund\\\",\\\"02637c19143c58b2c58bd378400a7b82bdc91d6dedaeb803b28640ef7d28a887ac\\\",\\\"0345c7fdf7ec7c8e746cca264bf27509eb4edb9ac421f8fbfab1dec64945a4d797\\\"],[\\\"n_sigs_refund\\\",\\\"2\\\"],[\\\"sigflag\\\",\\\"SIG_ALL\\\"]]}]\",\n \"C\": \"03dd83536fbbcbb74ccb3c87147df26753fd499cc2c095f74367fff0fb459c312e\",\n \"witness\": \"{\\\"signatures\\\":[\\\"23b58ef28cd22f3dff421121240ddd621deee83a3bc229fd67019c2e338d91e2c61577e081e1375dbab369307bba265e887857110ca3b4bd949211a0a298805f\\\",\\\"7e75948ef1513564fdcecfcbd389deac67c730f7004f8631ba90c0844d3e8c0cf470b656306877df5141f65fd3b7e85445a8452c3323ab273e6d0d44843817ed\\\"]}\"\n }\n ],\n \"outputs\": [\n {\n \"amount\": 2,\n \"id\": \"00bfa73302d12ffd\",\n \"B_\": \"038ec853d65ae1b79b5cdbc2774150b2cb288d6d26e12958a16fb33c32d9a86c39\"\n }\n ]\n}"; @@ -482,7 +489,7 @@ public void Nut11_SIG_ALL() var witness10 = JsonSerializer.Deserialize(meltRequestMultisigParsed.Inputs[0].Witness); Assert.True(SigAllHandler.VerifySigAllWitness(meltRequestMultisigParsed.Inputs, meltRequestMultisigParsed.Outputs, witness10, meltRequestMultisigParsed.Quote)); } - + [Fact] public void Nut12Tests_Hash_e() { @@ -643,6 +650,7 @@ public void Nut13HMACTests() Assert.Equal("099ed70fc2f7ac769bc20b2a75cb662e80779827b7cc358981318643030577d0", Convert.ToHexString(mnemonic.DeriveBlindingFactor(keysetId, 3)).ToLowerInvariant()); Assert.Equal("5550337312d223ba62e3f75cfe2ab70477b046d98e3e71804eade3956c7b98cf", Convert.ToHexString(mnemonic.DeriveBlindingFactor(keysetId, 4)).ToLowerInvariant()); } + [Fact] public void NullExpiryTests_PostMintQuoteBolt11Response() { diff --git a/DotNut.Tests/UnitTests2.cs b/DotNut.Tests/UnitTests2.cs index 3018e5d..53cc1fa 100644 --- a/DotNut.Tests/UnitTests2.cs +++ b/DotNut.Tests/UnitTests2.cs @@ -1,3 +1,5 @@ +using System.Text.Json; +using System.Text.Json.Serialization; using DotNut.Abstractions; namespace DotNut.Tests; @@ -29,5 +31,19 @@ public async Task InMemoryCounter() Assert.Equal(1337, ctrNum); } - -} \ No newline at end of file + /* + * Fee selector + */ + [Fact] + public void SplitAmountsForPayment_ExactAmount_ReturnsCorrectSplit() + { + var amounts = Utils.SplitToProofsAmounts(30, _testKeyset); + Assert.Equal(new List(){16, 8, 4, 2}, amounts); + + } + + private Keyset? _testKeyset = JsonSerializer.Deserialize( + "{\n \"1\": \"03ba786a2c0745f8c30e490288acd7a72dd53d65afd292ddefa326a4a3fa14c566\",\n \"2\": \"03361cd8bd1329fea797a6add1cf1990ffcf2270ceb9fc81eeee0e8e9c1bd0cdf5\",\n \"4\": \"036e378bcf78738ddf68859293c69778035740e41138ab183c94f8fee7572214c7\",\n \"8\": \"03909d73beaf28edfb283dbeb8da321afd40651e8902fcf5454ecc7d69788626c0\",\n \"16\": \"028a36f0e6638ea7466665fe174d958212723019ec08f9ce6898d897f88e68aa5d\",\n \"32\": \"03a97a40e146adee2687ac60c2ba2586a90f970de92a9d0e6cae5a4b9965f54612\",\n \"64\": \"03ce86f0c197aab181ddba0cfc5c5576e11dfd5164d9f3d4a3fc3ffbbf2e069664\",\n \"128\": \"0284f2c06d938a6f78794814c687560a0aabab19fe5e6f30ede38e113b132a3cb9\",\n \"256\": \"03b99f475b68e5b4c0ba809cdecaae64eade2d9787aa123206f91cd61f76c01459\",\n \"512\": \"03d4db82ea19a44d35274de51f78af0a710925fe7d9e03620b84e3e9976e3ac2eb\",\n \"1024\": \"031fbd4ba801870871d46cf62228a1b748905ebc07d3b210daf48de229e683f2dc\",\n \"2048\": \"0276cedb9a3b160db6a158ad4e468d2437f021293204b3cd4bf6247970d8aff54b\",\n \"4096\": \"02fc6b89b403ee9eb8a7ed457cd3973638080d6e04ca8af7307c965c166b555ea2\",\n \"8192\": \"0320265583e916d3a305f0d2687fcf2cd4e3cd03a16ea8261fda309c3ec5721e21\",\n \"16384\": \"036e41de58fdff3cb1d8d713f48c63bc61fa3b3e1631495a444d178363c0d2ed50\",\n \"32768\": \"0365438f613f19696264300b069d1dad93f0c60a37536b72a8ab7c7366a5ee6c04\",\n \"65536\": \"02408426cfb6fc86341bac79624ba8708a4376b2d92debdf4134813f866eb57a8d\",\n \"131072\": \"031063e9f11c94dc778c473e968966eac0e70b7145213fbaff5f7a007e71c65f41\",\n \"262144\": \"02f2a3e808f9cd168ec71b7f328258d0c1dda250659c1aced14c7f5cf05aab4328\",\n \"524288\": \"038ac10de9f1ff9395903bb73077e94dbf91e9ef98fd77d9a2debc5f74c575bc86\",\n \"1048576\": \"0203eaee4db749b0fc7c49870d082024b2c31d889f9bc3b32473d4f1dfa3625788\",\n \"2097152\": \"033cdb9d36e1e82ae652b7b6a08e0204569ec7ff9ebf85d80a02786dc7fe00b04c\",\n \"4194304\": \"02c8b73f4e3a470ae05e5f2fe39984d41e9f6ae7be9f3b09c9ac31292e403ac512\",\n \"8388608\": \"025bbe0cfce8a1f4fbd7f3a0d4a09cb6badd73ef61829dc827aa8a98c270bc25b0\",\n \"16777216\": \"037eec3d1651a30a90182d9287a5c51386fe35d4a96839cf7969c6e2a03db1fc21\",\n \"33554432\": \"03280576b81a04e6abd7197f305506476f5751356b7643988495ca5c3e14e5c262\",\n \"67108864\": \"03268bfb05be1dbb33ab6e7e00e438373ca2c9b9abc018fdb452d0e1a0935e10d3\",\n \"134217728\": \"02573b68784ceba9617bbcc7c9487836d296aa7c628c3199173a841e7a19798020\",\n \"268435456\": \"0234076b6e70f7fbf755d2227ecc8d8169d662518ee3a1401f729e2a12ccb2b276\",\n \"536870912\": \"03015bd88961e2a466a2163bd4248d1d2b42c7c58a157e594785e7eb34d880efc9\",\n \"1073741824\": \"02c9b076d08f9020ebee49ac8ba2610b404d4e553a4f800150ceb539e9421aaeee\",\n \"2147483648\": \"034d592f4c366afddc919a509600af81b489a03caf4f7517c2b3f4f2b558f9a41a\",\n \"4294967296\": \"037c09ecb66da082981e4cbdb1ac65c0eb631fc75d85bed13efb2c6364148879b5\",\n \"8589934592\": \"02b4ebb0dda3b9ad83b39e2e31024b777cc0ac205a96b9a6cfab3edea2912ed1b3\",\n \"17179869184\": \"026cc4dacdced45e63f6e4f62edbc5779ccd802e7fabb82d5123db879b636176e9\",\n \"34359738368\": \"02b2cee01b7d8e90180254459b8f09bbea9aad34c3a2fd98c85517ecfc9805af75\",\n \"68719476736\": \"037a0c0d564540fc574b8bfa0253cca987b75466e44b295ed59f6f8bd41aace754\",\n \"137438953472\": \"021df6585cae9b9ca431318a713fd73dbb76b3ef5667957e8633bca8aaa7214fb6\",\n \"274877906944\": \"02b8f53dde126f8c85fa5bb6061c0be5aca90984ce9b902966941caf963648d53a\",\n \"549755813888\": \"029cc8af2840d59f1d8761779b2496623c82c64be8e15f9ab577c657c6dd453785\",\n \"1099511627776\": \"03e446fdb84fad492ff3a25fc1046fb9a93a5b262ebcd0151caa442ea28959a38a\",\n \"2199023255552\": \"02d6b25bd4ab599dd0818c55f75702fde603c93f259222001246569018842d3258\",\n \"4398046511104\": \"03397b522bb4e156ec3952d3f048e5a986c20a00718e5e52cd5718466bf494156a\",\n \"8796093022208\": \"02d1fb9e78262b5d7d74028073075b80bb5ab281edcfc3191061962c1346340f1e\",\n \"17592186044416\": \"030d3f2ad7a4ca115712ff7f140434f802b19a4c9b2dd1c76f3e8e80c05c6a9310\",\n \"35184372088832\": \"03e325b691f292e1dfb151c3fb7cad440b225795583c32e24e10635a80e4221c06\",\n \"70368744177664\": \"03bee8f64d88de3dee21d61f89efa32933da51152ddbd67466bef815e9f93f8fd1\",\n \"140737488355328\": \"0327244c9019a4892e1f04ba3bf95fe43b327479e2d57c25979446cc508cd379ed\",\n \"281474976710656\": \"02fb58522cd662f2f8b042f8161caae6e45de98283f74d4e99f19b0ea85e08a56d\",\n \"562949953421312\": \"02adde4b466a9d7e59386b6a701a39717c53f30c4810613c1b55e6b6da43b7bc9a\",\n \"1125899906842624\": \"038eeda11f78ce05c774f30e393cda075192b890d68590813ff46362548528dca9\",\n \"2251799813685248\": \"02ec13e0058b196db80f7079d329333b330dc30c000dbdd7397cbbc5a37a664c4f\",\n \"4503599627370496\": \"02d2d162db63675bd04f7d56df04508840f41e2ad87312a3c93041b494efe80a73\",\n \"9007199254740992\": \"0356969d6aef2bb40121dbd07c68b6102339f4ea8e674a9008bb69506795998f49\",\n \"18014398509481984\": \"02f4e667567ebb9f4e6e180a4113bb071c48855f657766bb5e9c776a880335d1d6\",\n \"36028797018963968\": \"0385b4fe35e41703d7a657d957c67bb536629de57b7e6ee6fe2130728ef0fc90b0\",\n \"72057594037927936\": \"02b2bc1968a6fddbcc78fb9903940524824b5f5bed329c6ad48a19b56068c144fd\",\n \"144115188075855872\": \"02e0dbb24f1d288a693e8a49bc14264d1276be16972131520cf9e055ae92fba19a\",\n \"288230376151711744\": \"03efe75c106f931a525dc2d653ebedddc413a2c7d8cb9da410893ae7d2fa7d19cc\",\n \"576460752303423488\": \"02c7ec2bd9508a7fc03f73c7565dc600b30fd86f3d305f8f139c45c404a52d958a\",\n \"1152921504606846976\": \"035a6679c6b25e68ff4e29d1c7ef87f21e0a8fc574f6a08c1aa45ff352c1d59f06\",\n \"2305843009213693952\": \"033cdc225962c052d485f7cfbf55a5b2367d200fe1fe4373a347deb4cc99e9a099\",\n \"4611686018427387904\": \"024a4b806cf413d14b294719090a9da36ba75209c7657135ad09bc65328fba9e6f\",\n \"9223372036854775808\": \"0377a6fe114e291a8d8e991627c38001c8305b23b9e98b1c7b1893f5cd0dda6cad\"\n}"); +} + + diff --git a/DotNut/Abstractions/Handlers/MeltHandlerBolt11.cs b/DotNut/Abstractions/Handlers/MeltHandlerBolt11.cs index 1cf5c25..0caa952 100644 --- a/DotNut/Abstractions/Handlers/MeltHandlerBolt11.cs +++ b/DotNut/Abstractions/Handlers/MeltHandlerBolt11.cs @@ -1,3 +1,4 @@ +using System.Text.Json; using DotNut.Abstractions.Interfaces; using DotNut.Abstractions.Websockets; using DotNut.ApiModels; @@ -10,21 +11,38 @@ public class MeltHandlerBolt11 : IMeltHandler? _privKeys; + private string? _htlcPreimage; + + public MeltHandlerBolt11( + IWalletBuilder wallet, + PostMeltQuoteBolt11Response quote, + List? _privKeys = null, + string? htlcPreimage = null) { _wallet = wallet; _quote = quote; + _privKeys = _privKeys; + _htlcPreimage = htlcPreimage; } - public MeltHandlerBolt11(IWalletBuilder wallet, PostMeltQuoteBolt11Response quote, OutputData blankOutputs) + public MeltHandlerBolt11( + IWalletBuilder wallet, + PostMeltQuoteBolt11Response quote, + OutputData blankOutputs, + List? privKeys = null, + string? htlcPreimage = null) { _wallet = wallet; _quote = quote; - this._blankOutputs = blankOutputs; + _blankOutputs = blankOutputs; + _privKeys = privKeys; + _htlcPreimage = htlcPreimage; } + public async Task GetQuote(CancellationToken ct = default) => this._quote; public async Task> Melt(List inputs, CancellationToken ct = default) { + MaybeProcessP2PkHTLC(inputs); var client = await _wallet.GetMintApi(); var req = new PostMeltRequest { @@ -40,6 +58,51 @@ public async Task> Melt(List inputs, CancellationToken ct = d } var keyset = await _wallet.GetKeys(res.Change.First().Id, false, ct); - return CashuUtils.ConstructProofsFromPromises(res.Change.ToList(), _blankOutputs, keyset.Keys); + return Utils.ConstructProofsFromPromises(res.Change.ToList(), _blankOutputs, keyset.Keys); + } + + private void MaybeProcessP2PkHTLC(List proofs) + { + if (_privKeys == null || _privKeys.Count == 0) + { + return; + } + + if (proofs == null) + { + throw new ArgumentNullException(nameof(proofs), "No proofs to melt!"); + } + + var sigAllHandler = new SigAllHandler + { + Proofs = proofs, + BlindedMessages = this._blankOutputs?.BlindedMessages ?? [], + MeltQuoteId = _quote.Quote, + HTLCPreimage = this._htlcPreimage, + }; + + if (sigAllHandler.TrySign(out P2PKWitness? witness)) + { + if (witness == null) + { + throw new ArgumentNullException(nameof(witness), "sig_all input was correct, but couldn't create a witness signature!"); + } + proofs[0].Witness = JsonSerializer.Serialize(witness); + return; + } + + foreach (var proof in proofs) + { + + if (proof.Secret is not Nut10Secret { ProofSecret: P2PKProofSecret p2pk, Key: { } key }) continue; + if (proof.Secret is Nut10Secret { ProofSecret: HTLCProofSecret htlc } && _htlcPreimage is {} preimage) + { + var w = htlc.GenerateWitness(proof, _privKeys.Select(p=>p.Key).ToArray(), preimage); + proof.Witness = JsonSerializer.Serialize(w); + continue; + } + var proofWitness = p2pk.GenerateWitness(proof, _privKeys.Select(p => p.Key).ToArray()); + proof.Witness = JsonSerializer.Serialize(proofWitness); + } } } \ No newline at end of file diff --git a/DotNut/Abstractions/Handlers/MintHandlerBolt11.cs b/DotNut/Abstractions/Handlers/MintHandlerBolt11.cs index 922d4b8..036cecf 100644 --- a/DotNut/Abstractions/Handlers/MintHandlerBolt11.cs +++ b/DotNut/Abstractions/Handlers/MintHandlerBolt11.cs @@ -63,7 +63,7 @@ public async Task> Mint(CancellationToken ct = default) }; var promises= await client.Mint("bolt11", req, ct); - return CashuUtils.ConstructProofsFromPromises(promises.Signatures.ToList(), _outputs, _keyset.Keys); + return Utils.ConstructProofsFromPromises(promises.Signatures.ToList(), _outputs, _keyset.Keys); } } \ No newline at end of file diff --git a/DotNut/Abstractions/Handlers/MintHandlerBolt12.cs b/DotNut/Abstractions/Handlers/MintHandlerBolt12.cs index a123e38..0015353 100644 --- a/DotNut/Abstractions/Handlers/MintHandlerBolt12.cs +++ b/DotNut/Abstractions/Handlers/MintHandlerBolt12.cs @@ -61,6 +61,6 @@ public async Task> Mint(CancellationToken ct = default) }; var promises= await client.Mint("bolt12", req, ct); - return CashuUtils.ConstructProofsFromPromises(promises.Signatures.ToList(), _outputs, _keyset.Keys); + return Utils.ConstructProofsFromPromises(promises.Signatures.ToList(), _outputs, _keyset.Keys); } } \ No newline at end of file diff --git a/DotNut/Abstractions/Interfaces/IWalletBuilder.cs b/DotNut/Abstractions/Interfaces/IWalletBuilder.cs index 2fb5937..fdc36ef 100644 --- a/DotNut/Abstractions/Interfaces/IWalletBuilder.cs +++ b/DotNut/Abstractions/Interfaces/IWalletBuilder.cs @@ -12,23 +12,131 @@ namespace DotNut.Abstractions; /// public interface IWalletBuilder { + /// + /// Mandatory. Sets a mint in a wallet object + /// + /// Mint API object. IWalletBuilder WithMint(ICashuApi mintApi); + + /// + /// Mandatory. Sets a mint in a wallet object (with default CashuHttpClient) + /// + /// Mint URL string. IWalletBuilder WithMint(string mintUrl); + + /// + /// Optional. Import Mint Info to CashuWallet. Otherwise, it will be fetched from /v1/info endpoint. + /// + /// MintInfo object IWalletBuilder WithInfo(MintInfo info); + + /// + /// Optional. Import Mint Info to CashuWallet. Otherwise, it will be fetched from /v1/info endpoint. + /// + /// GetInfoResponse payload returned from mints API IWalletBuilder WithInfo(GetInfoResponse info); + + /// + /// Optional. Import Keysets into CashuWallet class. Otherwise, they will be fetched from /v1/keysets endpoint. + /// + /// List of Keysets IWalletBuilder WithKeysets(IEnumerable keysets); + + /// + /// Optional. Import Keysets into CashuWallet class. Otherwise, they will be fetched from /v1/keysets endpoint. + /// + /// GetKeysetsResponse payload returned from mints API IWalletBuilder WithKeysets(GetKeysetsResponse keysets); + + /// + /// Optional. Import Keys into CashuWallet class. Otherwise, they will be fetched from /v1/keys endpoint. + /// + /// List of mints Keys IWalletBuilder WithKeys(IEnumerable keys); + + /// + /// Optional. Import Keys into CashuWallet class. Otherwise, they will be fetched from /v1/keys endpoint. + /// + /// GetKeysResponse payload returned from mints API IWalletBuilder WithKeys(GetKeysResponse keys); + + /// + /// Optional. Flag suggesting if CashuWallet should sync provided Keys and Keysets with actual mints state. + /// Very useful if wallet stores keys in storage. + /// + /// boolean, true by default IWalletBuilder WithKeysetSync(bool syncKeyset = true); + + /// + /// Optional. Flag suggesting if CashuWallet should sync provided Keys and Keysets with actual mints state. + /// Has an additional field limiting how often keysets can be refetched. If not set, keysets will be synced only single time, + /// with first operation requiring keysets. (I'd go for like, 60 minutes) + /// + /// + /// + /// + IWalletBuilder WithKeysetSync(bool syncKeyset, TimeSpan syncThreesold); + + /// + /// Optional. Proof selecting algorithm. If not set, defaults to RGLI proof selector. + /// + /// IWalletBuilder WithSelector(IProofSelector selector); + + /// + /// Optional. BIP39 seed for secret and blinding factors derivation. All proofs generated by CashuWallet will be recoverable. + /// + /// Mnemonic object IWalletBuilder WithMnemonic(Mnemonic mnemonic); + + /// + /// Optional. BIP39 seed for secret and blinding factors derivation. All proofs generated by CashuWallet will be recoverable. + /// + /// Bip39 seed string separated by spaces. IWalletBuilder WithMnemonic(string mnemonic); + + /// + /// Optional and mandatory if Mnemonic provided. Counter for each Keyset Id for derivation purposes. + /// + /// Counter object IWalletBuilder WithCounter(ICounter counter); - IWalletBuilder WithCounter(IDictionary counter); + + /// + /// Optional and mandatory if Mnemonic provided. Counter for each Keyset Id for derivation purposes. + /// + /// Counter dictionary + /// + public IWalletBuilder WithCounter(IDictionary counter); + + /// + /// Optional and if not set, always true. Controls automatic counter incrementation for secret generation. + /// + /// If true, counter increments automatically. If false, requires manual management. + /// + /// WARNING: Disabling auto-increment is potentially dangerous. Manual counter management is required + /// to prevent secret reuse, which will cause mint rejection and operation failures. + /// IWalletBuilder ShouldBumpCounter(bool shouldBumpCounter = true); + + /// + /// Optional. + /// Adds websocket service. You should use single websocket service (singleton at best) for multiple wallets, in order to handle everything in nice manner. + /// If not set, but requested it'll be created automatically (which won't be so optimal). + /// + /// + /// IWalletBuilder WithWebsocketService(IWebsocketService websocketService); - + + /// + /// Optional. + /// Allows user to build stateful wallet, by providing a proof manager - a class allowing wallet to fetch, save and use proofs from desired kind of storage. + /// (See InMemoryProofManager.cs) + /// + /// + /// + IStatefulWalletBuilder WithProofManager(IProofManager proofManager); + + Task GetInfo(bool forceReferesh = false, CancellationToken ct = default); Task CreateOutputs(List amounts, KeysetId id, CancellationToken ct = default); @@ -52,15 +160,28 @@ Task SelectProofsToSend(List proofs, ulong amount, bool inc Task GetWebsocketService(CancellationToken ct = default); - // Swap operations + /// + /// Create swap transaction builder. + /// + /// Swap transaction builder ISwapBuilder Swap(); - // Melt operations (pay invoices) + /// + /// Create melt quote builder. + /// + /// IMeltQuoteBuilder CreateMeltQuote(); - // Mint operations (receive from invoice) + /// + /// Create Mint Quote + /// + /// Method-agnostic Mint Quote builder abstraction. IMintQuoteBuilder CreateMintQuote(); + /// + /// Can restoree proofs if mnemonic provided. + /// + /// IRestoreBuilder Restore(); } diff --git a/DotNut/Abstractions/Interfaces/IWebsocketService.cs b/DotNut/Abstractions/Interfaces/IWebsocketService.cs index 12e8194..2e498f4 100644 --- a/DotNut/Abstractions/Interfaces/IWebsocketService.cs +++ b/DotNut/Abstractions/Interfaces/IWebsocketService.cs @@ -4,20 +4,16 @@ namespace DotNut.Abstractions.Websockets; public interface IWebsocketService : IAsyncDisposable { - event EventHandler? OnWsError; - event EventHandler? ConnectionStateChanged; - Task ConnectAsync(string mintUrl, CancellationToken ct = default); + Task LazyConnectAsync(string mintUrl, CancellationToken ct = default); Task DisconnectAsync(string connectionId, CancellationToken ct = default); - Task SubscribeAsync(string connectionId, SubscriptionKind kind, string[] filters, CancellationToken ct = default); Task UnsubscribeAsync(string subId, CancellationToken ct = default); - WebSocketState GetConnectionState(string connectionId); IEnumerable GetSubscriptions(string connectionId); diff --git a/DotNut/Abstractions/MeltQuoteBuilder.cs b/DotNut/Abstractions/MeltQuoteBuilder.cs index 6ccec67..5ffeebd 100644 --- a/DotNut/Abstractions/MeltQuoteBuilder.cs +++ b/DotNut/Abstractions/MeltQuoteBuilder.cs @@ -13,7 +13,6 @@ class MeltQuoteBuilder : IMeltQuoteBuilder private string? _invoice; private OutputData? _blankOutputs; private string _unit = "sat"; - private bool _verifyDLEQ = true; private List? _privKeys; private string? _htlcPreimage; @@ -80,12 +79,6 @@ public IMeltQuoteBuilder OnQuoteStateChanged(Action return this; } - public IMeltQuoteBuilder WithDLEQVerification(bool verifyDLEQ = true) - { - this._verifyDLEQ = verifyDLEQ; - return this; - } - public async Task>> ProcessAsyncBolt11(CancellationToken ct = default) { var mintApi = await _wallet.GetMintApi(); @@ -104,14 +97,11 @@ public async Task>> Proces if (_blankOutputs == null) { - var outputsAmount = CashuUtils.CalculateNumberOfBlankOutputs((ulong)quote.FeeReserve); + var outputsAmount = Utils.CalculateNumberOfBlankOutputs((ulong)quote.FeeReserve); var amounts = Enumerable.Repeat(1UL, outputsAmount).ToList(); this._blankOutputs = await this._wallet.CreateOutputs(amounts, this._unit, ct); } - - await _maybeProcessP2PkHTLC(quote.Quote); - - return new MeltHandlerBolt11(_wallet, quote, _blankOutputs); + return new MeltHandlerBolt11(_wallet, quote, _blankOutputs, _privKeys, _htlcPreimage); } public async Task>> ProcessAsyncBolt12( @@ -120,48 +110,6 @@ public async Task>> Proces throw new NotImplementedException(); } - private async Task _maybeProcessP2PkHTLC(string quoteId) - { - if (_privKeys == null || _privKeys.Count == 0) - { - return; - } - - if (_proofs == null) - { - throw new ArgumentNullException(nameof(_proofs), "No proofs to melt!"); - } - - var sigAllHandler = new SigAllHandler - { - Proofs = this._proofs, - BlindedMessages = this._blankOutputs?.BlindedMessages ?? [], - MeltQuoteId = quoteId, - HTLCPreimage = this._htlcPreimage, - }; - - if (sigAllHandler.TrySign(out P2PKWitness? witness)) - { - if (witness == null) - { - throw new ArgumentNullException(nameof(witness), "sig_all input was correct, but couldn't create a witness signature!"); - } - this._proofs[0].Witness = JsonSerializer.Serialize(witness); - } - - foreach (var proof in _proofs) - { - - if (proof.Secret is not Nut10Secret { ProofSecret: P2PKProofSecret p2pk, Key: { } key }) continue; - if (proof.Secret is Nut10Secret { ProofSecret: HTLCProofSecret htlc } && _htlcPreimage is {} preimage) - { - var w = htlc.GenerateWitness(proof, _privKeys.Select(p=>p.Key).ToArray(), preimage); - proof.Witness = JsonSerializer.Serialize(w); - continue; - } - var proofWitness = p2pk.GenerateWitness(proof, _privKeys.Select(p => p.Key).ToArray()); - proof.Witness = JsonSerializer.Serialize(proofWitness); - } - } + } diff --git a/DotNut/Abstractions/MintInfo.cs b/DotNut/Abstractions/MintInfo.cs index d4b0cec..a34d995 100644 --- a/DotNut/Abstractions/MintInfo.cs +++ b/DotNut/Abstractions/MintInfo.cs @@ -268,7 +268,6 @@ public class ProtectedEndpointSpec public string Path { get; set; } = string.Empty; } -// Internal classes for protected endpoints caching internal class ProtectedEndpoints { public Dictionary Cache { get; set; } = new(); diff --git a/DotNut/Abstractions/MintMeltDisabledException.cs b/DotNut/Abstractions/MintMeltDisabledException.cs deleted file mode 100644 index 4ccbbec..0000000 --- a/DotNut/Abstractions/MintMeltDisabledException.cs +++ /dev/null @@ -1,3 +0,0 @@ -namespace DotNut.Abstractions; - -public class MintMeltDisabledException(string message) : Exception(message); \ No newline at end of file diff --git a/DotNut/Abstractions/MintQuoteBuilder.cs b/DotNut/Abstractions/MintQuoteBuilder.cs index 4ba4385..7966ff6 100644 --- a/DotNut/Abstractions/MintQuoteBuilder.cs +++ b/DotNut/Abstractions/MintQuoteBuilder.cs @@ -221,7 +221,7 @@ async Task _createOutputs() { throw new ArgumentNullException(nameof(_amount), "Amount can't be determined. Make sure to include amount, or amounts parameter!"); } - _amounts ??= CashuUtils.SplitToProofsAmounts(_amount.Value, _keyset!.Keys); + _amounts ??= Utils.SplitToProofsAmounts(_amount.Value, _keyset!.Keys); if (this._builder is null) { @@ -231,7 +231,7 @@ async Task _createOutputs() // skipped checks for keysetid and keys, since its validated before. make sure to remember about it. foreach (var amount in _amounts) { - var p2pkOutput = CashuUtils.CreateP2PkOutput(amount, this._keysetId!, this._keyset.Keys, _builder); + var p2pkOutput = Utils.CreateP2PkOutput(amount, this._keysetId!, this._keyset.Keys, _builder); outputs.BlindingFactors.Add(p2pkOutput.BlindingFactors[0]); outputs.BlindedMessages.Add(p2pkOutput.BlindedMessages[0]); outputs.Secrets.Add(p2pkOutput.Secrets[0]); diff --git a/DotNut/Abstractions/RestoreBuilder.cs b/DotNut/Abstractions/RestoreBuilder.cs index f92f93c..8cdf6cf 100644 --- a/DotNut/Abstractions/RestoreBuilder.cs +++ b/DotNut/Abstractions/RestoreBuilder.cs @@ -75,7 +75,7 @@ public async Task> ProcessAsync(CancellationToken ct = defaul emptyBatchesRemaining--; } - var proofs = CashuUtils.ConstructProofsFromPromises(res.Signatures.ToList(), outputs, keyset.Keys); + var proofs = Utils.ConstructProofsFromPromises(res.Signatures.ToList(), outputs, keyset.Keys); recoveredProofs.AddRange(proofs); batchNumber++; } @@ -99,9 +99,9 @@ public async Task> ProcessAsync(CancellationToken ct = defaul { var correspondingKeys = await _wallet.GetKeys(unitKeyset.Value, false, ct); var totalAmount = recoveredProofs.Select(p=>p.Amount).Aggregate((a,c) => a + c); - var amounts = CashuUtils.SplitToProofsAmounts(totalAmount, correspondingKeys.Keys); + var amounts = Utils.SplitToProofsAmounts(totalAmount, correspondingKeys.Keys); var ctr = await counter!.GetCounterForId(unitKeyset.Value, ct); - var newOutputs = CashuUtils.CreateOutputs(amounts, unitKeyset.Value, correspondingKeys.Keys, mnemonic, ctr); + var newOutputs = Utils.CreateOutputs(amounts, unitKeyset.Value, correspondingKeys.Keys, mnemonic, ctr); await counter.IncrementCounter(unitKeyset.Value, newOutputs.BlindedMessages.Count, ct); var swapRequest = new PostSwapRequest @@ -111,7 +111,7 @@ public async Task> ProcessAsync(CancellationToken ct = defaul }; var swapResult = await api.Swap(swapRequest, ct); - var constructedProofs = CashuUtils.ConstructProofsFromPromises(swapResult.Signatures.ToList(), newOutputs, correspondingKeys.Keys); + var constructedProofs = Utils.ConstructProofsFromPromises(swapResult.Signatures.ToList(), newOutputs, correspondingKeys.Keys); freshProofs.AddRange(constructedProofs); } diff --git a/DotNut/Abstractions/SwapBuilder.cs b/DotNut/Abstractions/SwapBuilder.cs index 2dd8cac..029008d 100644 --- a/DotNut/Abstractions/SwapBuilder.cs +++ b/DotNut/Abstractions/SwapBuilder.cs @@ -158,7 +158,7 @@ public async Task> ProcessAsync(CancellationToken ct = default) { var mintApi = await _wallet.GetMintApi(ct); - var swapInputs = await _getSwapProofs(ct); + var swapInputs = _getSwapProofs(ct); if (swapInputs == null || swapInputs.Count == 0) { throw new ArgumentException("Nothing to swap!"); @@ -197,8 +197,9 @@ public async Task> ProcessAsync(CancellationToken ct = default) fee = swapInputs.ComputeFee(keysetsFees); } - - var total = CashuUtils.SumProofs(swapInputs); + var total = Utils.SumProofs(swapInputs); + + this._amounts ??= this._getAmounts(total, fee, keysForCurrentId.Keys); // Swap received proofs to our keyset var outputs = await this._getOutputs(keysForCurrentId.Keys, ct); @@ -214,20 +215,19 @@ public async Task> ProcessAsync(CancellationToken ct = default) var swapResponse = await mintApi.Swap(request, ct); var swappedProofs = - CashuUtils.ConstructProofsFromPromises(swapResponse.Signatures.ToList(), this._outputs, keysForCurrentId.Keys); + Utils.ConstructProofsFromPromises(swapResponse.Signatures.ToList(), outputs, keysForCurrentId.Keys); return swappedProofs; } - private async Task> _getSwapProofs(CancellationToken ct = default) + private List _getSwapProofs(CancellationToken ct = default) { _proofsToSwap ??= new(); if (_tokenString != null) { var token = CashuTokenHelper.Decode(this._tokenString, out var v); - if (v == "A") // todo ensure + if (v == "A") { - //if token is v1, ensure everything is from the same mint var mints = token.Tokens.Select(t => t.Mint).ToList(); if (mints.Count > 1) { @@ -256,8 +256,6 @@ private async Task> _getSwapProofs(CancellationToken ct = default) async Task _getOutputs(Keyset keys, CancellationToken ct = default) { - var outputs = new OutputData(); - if (this._outputs != null) { if (this._builder is not null) @@ -272,17 +270,16 @@ async Task _getOutputs(Keyset keys, CancellationToken ct = default) throw new ArgumentNullException(nameof(_amounts), "Amounts can't be null."); } - var createdOutputs = new List(); + var outputs = new OutputData(); if (this._builder is not null) { // skipped checks for keysetid and keys, since its validated before. make sure to remember about it. - foreach (var p2pkOutput in _amounts.Select(amount => CashuUtils.CreateP2PkOutput(amount, this._keysetId!, keys, _builder))) + foreach (var p2pkOutput in _amounts.Select(amount => Utils.CreateP2PkOutput(amount, this._keysetId!, keys, _builder))) { outputs.BlindingFactors.Add(p2pkOutput.BlindingFactors[0]); outputs.BlindedMessages.Add(p2pkOutput.BlindedMessages[0]); outputs.Secrets.Add(p2pkOutput.Secrets[0]); } - return outputs; } @@ -332,7 +329,7 @@ private async Task _maybeProcessP2Pk() } } - private async Task> _getAmounts(ulong total, ulong fee, Keyset keys) + private List _getAmounts(ulong total, ulong fee, Keyset keys) { if (_amounts != null) { @@ -345,14 +342,14 @@ private async Task> _getAmounts(ulong total, ulong fee, Keyset keys) if (sum + fee < total) { var underpay = total - fee - sum; - this._amounts.AddRange(CashuUtils.SplitToProofsAmounts(underpay, keys)); + this._amounts.AddRange(Utils.SplitToProofsAmounts(underpay, keys)); return this._amounts; } throw new ArgumentException($"Invalid amounts requested. Sum of amounts: {sum}, total input: {total}, fee:{fee}."); } - this._amounts = CashuUtils.SplitToProofsAmounts(total - fee, keys); + this._amounts = Utils.SplitToProofsAmounts(total - fee, keys); return this._amounts; } diff --git a/DotNut/Abstractions/CashuUtils.cs b/DotNut/Abstractions/Utils.cs similarity index 99% rename from DotNut/Abstractions/CashuUtils.cs rename to DotNut/Abstractions/Utils.cs index 460ca8d..c0c26d7 100644 --- a/DotNut/Abstractions/CashuUtils.cs +++ b/DotNut/Abstractions/Utils.cs @@ -5,7 +5,7 @@ namespace DotNut.Abstractions; -public static class CashuUtils +public static class Utils { /// /// Function mapping payment amount to keyset supported amounts in order to create swap payload. Always tries to fit the biggest proof. diff --git a/DotNut/Abstractions/Wallet.cs b/DotNut/Abstractions/Wallet.cs index 0889a21..655f645 100644 --- a/DotNut/Abstractions/Wallet.cs +++ b/DotNut/Abstractions/Wallet.cs @@ -11,6 +11,8 @@ namespace DotNut.Abstractions; /// /// Main Cashu Wallet class implementing fluent builder pattern /// +/// + public class Wallet : IWalletBuilder { private MintInfo? _info; @@ -38,20 +40,12 @@ public class Wallet : IWalletBuilder */ public static IWalletBuilder Create() => new Wallet(); - /// - /// Mandatory. Sets a mint in a wallet object - /// - /// Mint API object. public IWalletBuilder WithMint(ICashuApi mintApi) { _mintApi = mintApi; return this; } - /// - /// Mandatory. Sets a mint in a wallet object (with default CashuHttpClient) - /// - /// Mint URL string. public IWalletBuilder WithMint(string mintUrl) { var httpClient = new HttpClient{ BaseAddress = new Uri(mintUrl)}; @@ -59,72 +53,36 @@ public IWalletBuilder WithMint(string mintUrl) return this; } - /// - /// Optional. Import Mint Info to CashuWallet. Otherwise, it will be fetched from /v1/info endpoint. - /// - /// MintInfo object public IWalletBuilder WithInfo(MintInfo info) { this._info = info; return this; } - /// - /// Optional. Import Mint Info to CashuWallet. Otherwise, it will be fetched from /v1/info endpoint. - /// - /// GetInfoResponse payload returned from mints API public IWalletBuilder WithInfo(GetInfoResponse info) => this.WithInfo(new MintInfo(info)); - - /// - /// Optional. Import Keysets into CashuWallet class. Otherwise, they will be fetched from /v1/keysets endpoint. - /// - /// List of Keysets + public IWalletBuilder WithKeysets(IEnumerable keysets) { this._keysets = keysets.ToList(); return this; } - /// - /// Optional. Import Keysets into CashuWallet class. Otherwise, they will be fetched from /v1/keysets endpoint. - /// - /// GetKeysetsResponse payload returned from mints API public IWalletBuilder WithKeysets(GetKeysetsResponse keysets) => this.WithKeysets(keysets.Keysets.ToList()); - /// - /// Optional. Import Keys into CashuWallet class. Otherwise, they will be fetched from /v1/keys endpoint. - /// - /// List of mints Keys public IWalletBuilder WithKeys(IEnumerable keys) { this._keys = keys.ToList(); return this; } - - /// - /// Optional. Import Keys into CashuWallet class. Otherwise, they will be fetched from /v1/keys endpoint. - /// - /// GetKeysResponse payload returned from mints API + public IWalletBuilder WithKeys(GetKeysResponse keys) => this.WithKeys(keys.Keysets.ToList()); - /// - /// Optional. Flag suggesting if CashuWallet should sync provided Keys and Keysets with actual mints state. - /// Very useful if wallet stores keys in storage. - /// - /// boolean, true by default public IWalletBuilder WithKeysetSync(bool syncKeyset = true) { this._shouldSyncKeyset = syncKeyset; return this; } - /// - /// Optional. Flag suggesting if CashuWallet should sync provided Keys and Keysets with actual mints state. - /// Has an additional field limiting how often keysets can be refetched. If not set, keysets will be synced only single time, - /// with first operation requiring keysets. (I'd go for like, 60 minutes) - /// - /// - /// - /// + public IWalletBuilder WithKeysetSync(bool syncKeyset, TimeSpan syncThreesold) { this._shouldSyncKeyset = syncKeyset; @@ -132,30 +90,18 @@ public IWalletBuilder WithKeysetSync(bool syncKeyset, TimeSpan syncThreesold) return this; } - /// - /// Optional. Proof selecting algorithm. If not set, defaults to RGLI proof selector. - /// - /// public IWalletBuilder WithSelector(IProofSelector selector) { _selector = selector; return this; } - /// - /// Optional. BIP39 seed for secret and blinding factors derivation. All proofs generated by CashuWallet will be recoverable. - /// - /// Mnemonic object public IWalletBuilder WithMnemonic(Mnemonic mnemonic) { _mnemonic = mnemonic; return this; } - /// - /// Optional. BIP39 seed for secret and blinding factors derivation. All proofs generated by CashuWallet will be recoverable. - /// - /// Bip39 seed string separated by spaces. public IWalletBuilder WithMnemonic(string mnemonic) { _mnemonic = new Mnemonic(mnemonic); @@ -228,20 +174,13 @@ public IStatefulWalletBuilder WithProofManager(IProofManager proofManager) * Main api methods */ - /// - /// Create Mint Quote builder - /// - /// + public IMintQuoteBuilder CreateMintQuote() { _ensureApiConnected(); return new MintQuoteBuilder(this); } - /// - /// Create swap transaction builder. - /// - /// Swap transaction builder public ISwapBuilder Swap() { _ensureApiConnected(); @@ -409,7 +348,7 @@ public async Task CreateOutputs(List amounts, KeysetId id, Ca var keyset = this._keys.Single(k => k.Id == id); if (this._mnemonic == null) { - return CashuUtils.CreateOutputs(amounts, id, keyset.Keys); + return Utils.CreateOutputs(amounts, id, keyset.Keys); } if (this._counter == null) @@ -422,7 +361,7 @@ public async Task CreateOutputs(List amounts, KeysetId id, Ca { await this._counter.IncrementCounter(id, amounts.Count, ct); } - return CashuUtils.CreateOutputs(amounts, id, keyset.Keys, this._mnemonic, counterValue); + return Utils.CreateOutputs(amounts, id, keyset.Keys, this._mnemonic, counterValue); } /// @@ -478,7 +417,16 @@ public async Task GetWebsocketService(CancellationToken ct = public Mnemonic? GetMnemonic() => _mnemonic; public ICounter? GetCounter() => _counter; - + + /* + * Private helpers + */ + + /// + /// Throws exception if api not connected + /// + /// + /// internal void _ensureApiConnected(string? msg = null) { if (_mintApi != null) @@ -494,12 +442,6 @@ internal void _ensureApiConnected(string? msg = null) throw new ArgumentNullException(nameof(this._mintApi)); } - - - /* - * Private helpers - */ - /// /// Wrapper for GetKeysets api endpoint. Formats Keysets to list. /// @@ -621,7 +563,6 @@ internal async Task _maybeSyncKeys(CancellationToken cts = default) _lastSync = DateTime.Now; } - } diff --git a/DotNut/Abstractions/Websockets/Subscription.cs b/DotNut/Abstractions/Websockets/Subscription.cs index 4baf47a..87da2f0 100644 --- a/DotNut/Abstractions/Websockets/Subscription.cs +++ b/DotNut/Abstractions/Websockets/Subscription.cs @@ -9,12 +9,24 @@ public class Subscription public SubscriptionKind Kind { get; set; } public string[] Filters { get; set; } = Array.Empty(); public DateTime CreatedAt { get; set; } = DateTime.UtcNow; - public Channel NotificationChannel { get; set; } + public Channel NotificationChannel { get; set; } public EventHandler OnError { get; set; } - public void Close() + private readonly WeakReference? _serviceRef; + + public Subscription(IWebsocketService? websocketService = null) + { + _serviceRef = websocketService != null ? + new WeakReference(websocketService) : null; + } + + public async Task CloseAsync() { NotificationChannel.Writer.TryComplete(); + if (_serviceRef != null && _serviceRef.TryGetTarget(out var service)) + { + await service.UnsubscribeAsync(Id); + } } } diff --git a/DotNut/Abstractions/Websockets/WebsocketEnums.cs b/DotNut/Abstractions/Websockets/WebsocketEnums.cs index 4bd9243..c3e4b05 100644 --- a/DotNut/Abstractions/Websockets/WebsocketEnums.cs +++ b/DotNut/Abstractions/Websockets/WebsocketEnums.cs @@ -7,6 +7,8 @@ public enum SubscriptionKind { bolt11_melt_quote, bolt11_mint_quote, + bolt12_melt_quote, + bolt12_mint_quote, proof_state } diff --git a/DotNut/Abstractions/Websockets/WebsocketModels.cs b/DotNut/Abstractions/Websockets/WebsocketModels.cs index f32e5f8..98a130d 100644 --- a/DotNut/Abstractions/Websockets/WebsocketModels.cs +++ b/DotNut/Abstractions/Websockets/WebsocketModels.cs @@ -4,8 +4,8 @@ namespace DotNut.Abstractions.Websockets; public class WsRequest { - [JsonPropertyName("jsonrpc")] - public string JsonRpc = "2.0"; + [JsonPropertyName("jsonrpc")] + public string JsonRpc { get; set; } = "2.0"; [JsonPropertyName("method")] public WsRequestMethod Method { get; set; } @@ -34,7 +34,7 @@ public class WsRequestParams public class WsResponse { [JsonPropertyName("jsonrpc")] - public string JsonRpc { get; set; } = "2.0"; + public string JsonRpc { get; } = "2.0"; [JsonPropertyName("result")] public WsResult Result { get; set; } = new(); @@ -45,8 +45,8 @@ public class WsResponse public class WsResult { - [JsonPropertyName("status")] - public string Status { get; set; } = string.Empty; + [JsonPropertyName("status")] + public string Status { get; } = "OK"; [JsonPropertyName("subId")] public string SubId { get; set; } = string.Empty; @@ -55,7 +55,7 @@ public class WsResult public class WsError { [JsonPropertyName("jsonrpc")] - public string JsonRpc { get; set; } = "2.0"; + public string JsonRpc { get; } = "2.0"; [JsonPropertyName("error")] public WsErrorDetails Error { get; set; } = new(); @@ -76,10 +76,10 @@ public class WsErrorDetails public class WsNotification { [JsonPropertyName("jsonrpc")] - public string JsonRpc { get; set; } = "2.0"; + public string JsonRpc { get; } = "2.0"; [JsonPropertyName("method")] - public string Method { get; set; } = "subscribe"; + public string Method { get; } = "subscribe"; [JsonPropertyName("params")] public WsNotificationParams Params { get; set; } = new(); @@ -93,3 +93,22 @@ public class WsNotificationParams [JsonPropertyName("payload")] public object? Payload { get; set; } } + +public abstract record WsMessage +{ + public sealed record Response(WsResponse Value) : WsMessage; + public sealed record Error(WsError Value) : WsMessage; + public sealed record Notification(WsNotification Value) : WsMessage; +} + +public abstract record RequestResult +{ + public sealed record Success(string SubId, string Status) : RequestResult; + public sealed record Failure(int Code, string Message, int RequestId) : RequestResult; +} + +internal class PendingRequest +{ + public TaskCompletionSource Tcs { get; set; } + public string SubscriptionId { get; set; } +} \ No newline at end of file diff --git a/DotNut/Abstractions/Websockets/WebsocketService.cs b/DotNut/Abstractions/Websockets/WebsocketService.cs index 9214a81..0eb3151 100644 --- a/DotNut/Abstractions/Websockets/WebsocketService.cs +++ b/DotNut/Abstractions/Websockets/WebsocketService.cs @@ -1,7 +1,9 @@ using System.Collections.Concurrent; +using System.Net; using System.Net.WebSockets; using System.Text; using System.Text.Json; +using System.Text.Json.Serialization; using System.Threading.Channels; using DotNut.Abstractions.Websockets; @@ -11,26 +13,29 @@ public class WebsocketService : IWebsocketService { private readonly ConcurrentDictionary _connections = new(); private readonly ConcurrentDictionary _subscriptions = new(); - private readonly object _lockObject = new(); + private readonly ConcurrentDictionary _pendingRequests = new(); private int _nextRequestId = 0; - + public event EventHandler? ConnectionStateChanged; - public event EventHandler? OnWsError; + public async Task ConnectAsync(string mintUrl, CancellationToken ct = default) { - var normalized = _normalizeMintUrl(mintUrl); - - if (_connections.TryGetValue(normalized, out var existing)) - { - return existing; - } - + var normalized = NormalizeMintUrl(mintUrl); + var connectionId = Guid.NewGuid().ToString(); var wsUrl = GetWebSocketUrl(mintUrl); var clientWebSocket = new ClientWebSocket(); - await clientWebSocket.ConnectAsync(new Uri(wsUrl), ct); + try + { + await clientWebSocket.ConnectAsync(new Uri(wsUrl), ct); + } + catch (Exception ex) + { + clientWebSocket.Dispose(); + throw; + } var connection = new WebsocketConnection { @@ -41,17 +46,31 @@ public async Task ConnectAsync(string mintUrl, Cancellation }; _connections[normalized] = connection; - - _ = Task.Run(async () => await ListenForMessages(connection, ct), ct); - OnConnectionStateChanged(connectionId, WebSocketState.Open); + + _ = Task.Run(async () => await ListenForMessages(connection, ct), ct); return connection; } + public async Task LazyConnectAsync(string mintUrl, CancellationToken ct = default) + { + var normalized = NormalizeMintUrl(mintUrl); + + if (_connections.TryGetValue(normalized, out var existing)) + { + if (existing.State == WebSocketState.Open) + { + return existing; + } + } + _connections.TryRemove(normalized, out _); + return await ConnectAsync(mintUrl, ct); + } + public async Task DisconnectAsync(string mintUrl, CancellationToken ct = default) { - var normalized = _normalizeMintUrl(mintUrl); + var normalized = NormalizeMintUrl(mintUrl); if (!_connections.TryGetValue(normalized, out var connection)) { @@ -62,18 +81,21 @@ public async Task DisconnectAsync(string mintUrl, CancellationToken ct = default { if (connection.State == WebSocketState.Open) { - await connection.WebSocket.CloseAsync(WebSocketCloseStatus.NormalClosure, "Client disconnecting", ct); + await connection.WebSocket.CloseAsync( + WebSocketCloseStatus.NormalClosure, + "Client disconnecting", + ct); } } - catch (Exception) + catch (Exception _) { - // Ignore close exceptions + // ignored } finally { connection.WebSocket.Dispose(); _connections.TryRemove(normalized, out _); - + var subscriptionsToRemove = _subscriptions .Where(s => s.Value.ConnectionId == connection.Id) .Select(s => s.Key) @@ -81,16 +103,19 @@ public async Task DisconnectAsync(string mintUrl, CancellationToken ct = default foreach (var subId in subscriptionsToRemove) { - _subscriptions.TryRemove(subId, out _); + if (_subscriptions.TryRemove(subId, out var removedSub)) + { + await removedSub.CloseAsync(); + } } OnConnectionStateChanged(connection.Id, WebSocketState.Closed); } } - + public async Task SubscribeAsync(string mintUrl, SubscriptionKind kind, string[] filters, CancellationToken ct = default) { - var normalized = _normalizeMintUrl(mintUrl); + var normalized = NormalizeMintUrl(mintUrl); if (!_connections.TryGetValue(normalized, out var connection)) { @@ -103,9 +128,9 @@ public async Task SubscribeAsync(string mintUrl, SubscriptionKind } var subId = Guid.NewGuid().ToString(); - var requestId = GetNextRequestId(); + var requestId = _getNextRequestId(); - var channel = Channel.CreateUnbounded(new UnboundedChannelOptions { SingleReader = false }); + var channel = Channel.CreateUnbounded(new UnboundedChannelOptions { SingleReader = false }); var request = new WsRequest { @@ -120,7 +145,7 @@ public async Task SubscribeAsync(string mintUrl, SubscriptionKind Id = requestId }; - var subscription = new Subscription + var subscription = new Subscription(this) { Id = subId, ConnectionId = connection.Id, @@ -132,9 +157,37 @@ public async Task SubscribeAsync(string mintUrl, SubscriptionKind _subscriptions[subId] = subscription; - await SendMessageAsync(connection, request, ct); - - return subscription; + var tcs = new TaskCompletionSource(); + + _pendingRequests[requestId] = new PendingRequest + { + Tcs = tcs, + SubscriptionId = subId + }; + + try + { + await SendMessageAsync(connection, request, ct); + + using var cts = CancellationTokenSource.CreateLinkedTokenSource(ct); + cts.CancelAfter(TimeSpan.FromSeconds(30)); + + var result = await tcs.Task.ConfigureAwait(false); + + if (result is RequestResult.Failure failure) + { + _subscriptions.TryRemove(subId, out _); + await subscription.CloseAsync(); + throw new InvalidOperationException( + $"Subscription failed: {failure.Message}"); + } + + return subscription; + } + finally + { + _pendingRequests.TryRemove(requestId, out _); + } } public async Task UnsubscribeAsync(string subId, CancellationToken ct = default) @@ -142,38 +195,59 @@ public async Task UnsubscribeAsync(string subId, CancellationToken ct = default) if (!_subscriptions.TryGetValue(subId, out var subscription)) throw new InvalidOperationException($"Subscription {subId} not found"); - if (_connections.Values.FirstOrDefault(c => c.Id == subscription.ConnectionId) is not { } connection) + var connection = _connections.Values.FirstOrDefault(c => c.Id == subscription.ConnectionId); + if (connection is null) throw new InvalidOperationException($"Connection for subscription {subId} not found"); - if (connection.State != WebSocketState.Open) { throw new InvalidOperationException($"Connection is not open"); } - var requestId = GetNextRequestId(); - - var request = new WsRequest + var requestId = _getNextRequestId(); + var tcs = new TaskCompletionSource(); + _pendingRequests[requestId] = new PendingRequest { - JsonRpc = "2.0", - Method = WsRequestMethod.unsubscribe, - Params = new WsRequestParams - { - SubId = subId - }, - Id = requestId + Tcs = tcs, + SubscriptionId = subId }; + + try + { + var request = new WsRequest + { + JsonRpc = "2.0", + Method = WsRequestMethod.unsubscribe, + Params = new WsRequestParams + { + SubId = subId + }, + Id = requestId + }; - await SendMessageAsync(connection, request, ct); - - _subscriptions.TryRemove(subId, out _); + await SendMessageAsync(connection, request, ct); + + using var cts = CancellationTokenSource.CreateLinkedTokenSource(ct); + cts.CancelAfter(TimeSpan.FromSeconds(30)); + + await tcs.Task.ConfigureAwait(false); + } + finally + { + _pendingRequests.TryRemove(requestId, out _); + _subscriptions.TryRemove(subId, out _); + } } public async ValueTask DisposeAsync() { + foreach (var sub in _subscriptions.Values) { - sub.Close(); + try + { + await sub.CloseAsync(); + } catch { } } var mintUrls = _connections.Keys.ToList(); foreach (var mintUrl in mintUrls) @@ -182,11 +256,12 @@ public async ValueTask DisposeAsync() } _subscriptions.Clear(); _connections.Clear(); + _pendingRequests.Clear(); } public WebSocketState GetConnectionState(string mintUrl) { - var normalized = _normalizeMintUrl(mintUrl); + var normalized = NormalizeMintUrl(mintUrl); return _connections.TryGetValue(normalized, out var connection) ? connection.State : WebSocketState.None; @@ -194,7 +269,7 @@ public WebSocketState GetConnectionState(string mintUrl) public IEnumerable GetSubscriptions(string mintUrl) { - var normalized = _normalizeMintUrl(mintUrl); + var normalized = NormalizeMintUrl(mintUrl); if (!_connections.TryGetValue(normalized, out var connection)) { throw new Exception($"Connection for mint {mintUrl} not found"); @@ -206,7 +281,7 @@ public IEnumerable GetConnections() { return _connections.Values; } - + private async Task ListenForMessages(WebsocketConnection connection, CancellationToken ct) { var buffer = new byte[4096]; @@ -216,7 +291,7 @@ private async Task ListenForMessages(WebsocketConnection connection, Cancellatio while (connection.State == WebSocketState.Open && !ct.IsCancellationRequested) { var result = await connection.WebSocket.ReceiveAsync(new ArraySegment(buffer), ct); - + if (result.MessageType == WebSocketMessageType.Close) { connection.State = WebSocketState.Closed; @@ -226,80 +301,106 @@ private async Task ListenForMessages(WebsocketConnection connection, Cancellatio if (result.MessageType == WebSocketMessageType.Text) { - var message = Encoding.UTF8.GetString(buffer, 0, result.Count); - await ProcessMessage(connection, message); + var message = Encoding.UTF8.GetString(buffer, 0, result.Count); + _processMessage(connection, message); } } } + catch (OperationCanceledException) + { + // Expected + } catch (Exception ex) { connection.State = WebSocketState.Aborted; OnConnectionStateChanged(connection.Id, WebSocketState.Aborted); } + finally + { + // Close all subscriptions for this connection + var subscriptionsToClose = _subscriptions.Values + .Where(s => s.ConnectionId == connection.Id) + .ToList(); + + foreach (var sub in subscriptionsToClose) + { + sub.CloseAsync(); + } + } } - private async Task ProcessMessage(WebsocketConnection connection, string message) + private void _processMessage(WebsocketConnection connection, string message) { try { var jsonElement = JsonSerializer.Deserialize(message); - + if (jsonElement.TryGetProperty("method", out var methodProp) && methodProp.GetString() == "subscribe") { var notification = JsonSerializer.Deserialize(message); if (notification != null) { - _onNotificationReceived(notification.Params); + _onNotificationReceived(notification); } } else if (jsonElement.TryGetProperty("result", out _)) { var response = JsonSerializer.Deserialize(message); - // TODO: Handle response + if (response != null) + { + HandleResponse(response); + } } else if (jsonElement.TryGetProperty("error", out _)) { var error = JsonSerializer.Deserialize(message); - // TODO: Handle error + if (error != null) + { + HandleError(error); + } } } catch (Exception ex) { - // TODO: Log exception + // Could be logged if logging is added later } } private async Task SendMessageAsync(WebsocketConnection connection, T message, CancellationToken ct) { - var json = JsonSerializer.Serialize(message); + var json = JsonSerializer.Serialize(message, new JsonSerializerOptions + { + DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull + }); var bytes = Encoding.UTF8.GetBytes(json); - + await connection.WebSocket.SendAsync( new ArraySegment(bytes), WebSocketMessageType.Text, true, ct); } - + public bool IsConnected(string mintUrl) + { + var normalized = NormalizeMintUrl(mintUrl); + return _connections.TryGetValue(normalized, out var conn) && + conn.State == WebSocketState.Open; + } private string GetWebSocketUrl(string mintUrl) { - var uri = new Uri(_normalizeMintUrl(mintUrl)); + var uri = new Uri(NormalizeMintUrl(mintUrl)); var scheme = uri.Scheme == "https" ? "wss" : "ws"; var hostPort = (uri.IsDefaultPort) ? uri.Host : $"{uri.Host}:{uri.Port}"; var path = uri.AbsolutePath.TrimEnd('/'); return $"{scheme}://{hostPort}{path}/v1/ws"; } - - private int GetNextRequestId() + + private int _getNextRequestId() { - lock (_lockObject) - { - return ++_nextRequestId; - } + return Interlocked.Increment(ref _nextRequestId); } - private void OnConnectionStateChanged(string connectionId, WebSocketState state) { ConnectionStateChanged?.Invoke(this, new ConnectionStateChangedEventArgs @@ -309,10 +410,7 @@ private void OnConnectionStateChanged(string connectionId, WebSocketState state) }); } - public bool IsConnected(string mintUrl) - => _connections.ContainsKey(_normalizeMintUrl(mintUrl)); - - private string _normalizeMintUrl(string mintUrl) + private static string NormalizeMintUrl(string mintUrl) { if (!Uri.TryCreate(mintUrl.TrimEnd('/'), UriKind.Absolute, out var uri)) { @@ -323,18 +421,46 @@ private string _normalizeMintUrl(string mintUrl) return builder.Uri.ToString().TrimEnd('/'); } - private void _onNotificationReceived(WsNotificationParams notificationParams) + private void HandleResponse(WsResponse response) { - if (!_subscriptions.TryGetValue(notificationParams.SubId, out var sub)) + if (!_pendingRequests.TryGetValue(response.Id, out var pr)) { - //it should never happen return; } - sub.NotificationChannel.Writer.WriteAsync(notificationParams); + var result = new RequestResult.Success( + SubId: response.Result.SubId, + Status: response.Result.Status); + pr.Tcs.TrySetResult(result); + + if (!_subscriptions.TryGetValue(pr.SubscriptionId, out var sub)) + { + return; + } + sub.NotificationChannel.Writer.TryWrite(new WsMessage.Response(response)); } + + private void HandleError(WsError error) + { + if (!_pendingRequests.TryGetValue(error.Id, out var pr)){ return;} + var result = new RequestResult.Failure( + Code: error.Error.Code, + Message: error.Error.Message, + RequestId: error.Id); + pr.Tcs.TrySetResult(result); + + if (!_subscriptions.TryGetValue(pr.SubscriptionId, out var sub)) + { + return; + } - private WebsocketConnection? _getConnectionById(string connectionId) + sub.NotificationChannel.Writer.TryWrite(new WsMessage.Error(error)); + } + private void _onNotificationReceived(WsNotification notification) { - return this._connections.Values.SingleOrDefault(c=>c.Id == connectionId, null); + if (!_subscriptions.TryGetValue(notification.Params.SubId, out var sub)) + { + return; + } + sub.NotificationChannel.Writer.TryWrite(new WsMessage.Notification(notification)); } } \ No newline at end of file diff --git a/DotNut/Abstractions/Websockets/WebsocketServiceExtensions.cs b/DotNut/Abstractions/Websockets/WebsocketServiceExtensions.cs index 884b1af..cbad041 100644 --- a/DotNut/Abstractions/Websockets/WebsocketServiceExtensions.cs +++ b/DotNut/Abstractions/Websockets/WebsocketServiceExtensions.cs @@ -8,6 +8,7 @@ public static async Task SubscribeToMintQuoteAsync( string[] quoteIds, CancellationToken ct = default) { + await service.LazyConnectAsync(mintUrl, ct); return await service.SubscribeAsync(mintUrl, SubscriptionKind.bolt11_mint_quote, quoteIds, ct); } @@ -17,6 +18,7 @@ public static async Task SubscribeToMeltQuoteAsync( string[] quoteIds, CancellationToken ct = default) { + await service.LazyConnectAsync(mintUrl, ct); return await service.SubscribeAsync(mintUrl, SubscriptionKind.bolt11_melt_quote, quoteIds, ct); } @@ -26,6 +28,7 @@ public static async Task SubscribeToProofStateAsync( string[] proofYs, CancellationToken ct = default) { + await service.LazyConnectAsync(mintUrl, ct); return await service.SubscribeAsync(mintUrl, SubscriptionKind.proof_state, proofYs, ct); } @@ -35,6 +38,7 @@ public static async Task SubscribeToSingleProofStateAsync( string proofY, CancellationToken ct = default) { + await service.LazyConnectAsync(mintUrl, ct); return await service.SubscribeToProofStateAsync(mintUrl, new[] { proofY }, ct); } @@ -44,6 +48,7 @@ public static async Task SubscribeToSingleMintQuoteAsync( string quoteId, CancellationToken ct = default) { + await service.LazyConnectAsync(mintUrl, ct); return await service.SubscribeToMintQuoteAsync(mintUrl, new[] { quoteId }, ct); } @@ -53,6 +58,7 @@ public static async Task SubscribeToSingleMeltQuoteAsync( string quoteId, CancellationToken ct = default) { + await service.LazyConnectAsync(mintUrl, ct); return await service.SubscribeToMeltQuoteAsync(mintUrl, new[] { quoteId }, ct); } diff --git a/DotNut/ApiModels/Mint/bolt11/PostMintQuoteBolt11Response.cs b/DotNut/ApiModels/Mint/bolt11/PostMintQuoteBolt11Response.cs index cdbeadb..f5d42b2 100644 --- a/DotNut/ApiModels/Mint/bolt11/PostMintQuoteBolt11Response.cs +++ b/DotNut/ApiModels/Mint/bolt11/PostMintQuoteBolt11Response.cs @@ -28,5 +28,5 @@ public class PostMintQuoteBolt11Response [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] [JsonPropertyName("pubkey")] - public string PubKey {get; set;} + public string? PubKey {get; set;} } \ No newline at end of file diff --git a/DotNut/NUT11/P2PKProofSecret.cs b/DotNut/NUT11/P2PKProofSecret.cs index 294c472..aa9af33 100644 --- a/DotNut/NUT11/P2PKProofSecret.cs +++ b/DotNut/NUT11/P2PKProofSecret.cs @@ -1,4 +1,5 @@ using System.Text; +using System.Text.Json; using System.Text.Json.Serialization; using NBitcoin.Secp256k1; using SHA256 = System.Security.Cryptography.SHA256; @@ -116,6 +117,15 @@ public virtual ECPubKey[] GetAllowedRefundPubkeys(out int? requiredSignatures) + public virtual bool VerifyWitness(Proof proof) + { + if (proof.Witness is null) + { + return false; + } + var witness = JsonSerializer.Deserialize(proof.Witness) ?? new P2PKWitness(); + return VerifyWitness(proof.Secret, witness); + } /* * ========================= * NUT-XX Pay to blinded key diff --git a/DotNut/NUT11/SigAllHandler.cs b/DotNut/NUT11/SigAllHandler.cs index 725a653..2b3b865 100644 --- a/DotNut/NUT11/SigAllHandler.cs +++ b/DotNut/NUT11/SigAllHandler.cs @@ -21,78 +21,166 @@ public class SigAllHandler public bool TrySign(out P2PKWitness? p2pkwitness) { p2pkwitness = null; - + if (BlindedMessages.Count == 0) { return false; } - - if (_validateFirstProof() == false) + + byte[] msg; + try + { + var msgStr = GetMessageToSign(Proofs.ToArray(), BlindedMessages.ToArray(), MeltQuoteId); + msg = Encoding.UTF8.GetBytes(msgStr); + } + catch (Exception _) { return false; } + + if (_firstProofSecret is HTLCProofSecret s && HTLCPreimage is {} preimage) + { + p2pkwitness = + s.GenerateWitness(msg, PrivKeys.Select(pk => (ECPrivKey)pk).ToArray(), + Encoding.UTF8.GetBytes(preimage) + ); + return true; + } + p2pkwitness = _firstProofSecret!.GenerateWitness(msg, PrivKeys.Select(pk => (ECPrivKey)pk).ToArray()); + return true; + } + + public static string GetMessageToSign(Proof[] inputs, BlindedMessage[] outputs, string? meltQuoteId = null) + { + if (!ValidateFirstProof(inputs[0], out var firstSecret)) + { + throw new ArgumentException("Provided first proof is invalid"); + } var msg = new StringBuilder(); - if (Proofs.Count > 0) + if (inputs.Length > 0) { - for (var i = 1; i < Proofs.Count; i++) + for (var i = 0; i < inputs.Length; i++) { - var p = Proofs[i]; + var p = inputs[i]; - if (p.Secret is not Nut10Secret { ProofSecret: P2PKProofSecret p2pk }) + if (p.Secret is not Nut10Secret nut10) { throw new ArgumentException($"When signing sig_all, every proof must be sig_all."); } - - if (!_checkIfEqualToFirst(p2pk)) + + if (!CheckIfEqualToFirst(firstSecret, nut10.ProofSecret)) { throw new ArgumentException($"When signing sig_all, every proof must have identical tags and data."); } - msg.Append(JsonSerializer.Serialize(p.Secret)); + // serialize as raw object + var secret = JsonSerializer.Serialize((object)p.Secret); + msg.Append(secret); + msg.Append(p.C); } } - foreach (var b in BlindedMessages) + foreach (var b in outputs) { + msg.Append(b.Amount); msg.Append(b.B_); } - if (MeltQuoteId is not null) + if (meltQuoteId is not null) { - msg.Append(MeltQuoteId); + msg.Append(meltQuoteId); } - var bytesMsg = Encoding.UTF8.GetBytes(msg.ToString()); + return msg.ToString(); + } - if (_firstProofSecret is HTLCProofSecret s && HTLCPreimage is {} preimage) + public static bool VerifySigAllWitness( + Proof[] proofs, + BlindedMessage[] blindedMessages, + P2PKWitness witness, + string? meltQuoteId = null) + { + if (proofs[0].Secret is Nut10Secret nut10_3) + Console.WriteLine($"CP3 ProofSecret: {nut10_3.ProofSecret.GetType()}"); + byte[] msg; + try { - p2pkwitness = - s.GenerateWitness(bytesMsg, PrivKeys.Select(pk => (ECPrivKey)pk).ToArray(), - Encoding.UTF8.GetBytes(preimage) - ); - return true; + var msgStr = meltQuoteId is null + ? GetMessageToSign(proofs, blindedMessages) + : GetMessageToSign(proofs, blindedMessages, meltQuoteId); + + msg = Encoding.UTF8.GetBytes(msgStr); } - p2pkwitness = _firstProofSecret!.GenerateWitness(bytesMsg, PrivKeys.Select(pk => (ECPrivKey)pk).ToArray()); - return true; + catch(Exception ex) + { + Console.WriteLine(ex.Message); + return false; + } + + if (proofs[0].Secret is not Nut10Secret nut10) + return false; + + return nut10.ProofSecret switch + { + HTLCProofSecret htlcs => htlcs.VerifyWitness(msg, witness), + P2PKProofSecret p2pks => p2pks.VerifyWitness(msg, witness), + _ => false + }; } - private bool _validateFirstProof() + public static bool VerifySigAllWitness(Proof[] proofs, BlindedMessage[] blindedMessages, string? meltQuoteId = null) { - if (Proofs[0].Secret is not Nut10Secret { ProofSecret: P2PKProofSecret p2pks }) + var firstProof = proofs.FirstOrDefault(); + if (firstProof?.Secret is not Nut10Secret { ProofSecret: var proofSecret } || firstProof.Witness is null) + return false; + + P2PKWitness? witness; + try + { + var htlcWitness = JsonSerializer.Deserialize(firstProof.Witness); + if (htlcWitness?.Preimage is not null) + { + witness = htlcWitness; + } + else + { + witness = JsonSerializer.Deserialize(firstProof.Witness); + } + } + catch { return false; } - var b = P2PkBuilder.Load(p2pks); - if (b.SigFlag != "SIG_ALL") + return witness is not null && VerifySigAllWitness(proofs, blindedMessages, witness, meltQuoteId); + } + + private static bool ValidateFirstProof(Proof firstProof, out Nut10ProofSecret secret) + { + secret = null; + + if (firstProof.Secret is not Nut10Secret nut10) { return false; } - this._firstProofSecret = p2pks; + + var builder = nut10.ProofSecret switch + { + HTLCProofSecret htlcs => HTLCBuilder.Load(htlcs), + P2PKProofSecret p2pks => P2PkBuilder.Load(p2pks), + // won't throw exception if there will be a new type of nut10 secret, but will return false + _ => new P2PkBuilder(){SigFlag = null} + }; + if (builder.SigFlag != "SIG_ALL") + { + return false; + } + + secret = nut10.ProofSecret; return true; } - private bool _checkIfEqualToFirst(P2PKProofSecret other) => - _firstProofSecret is { } a && other is { } b && + private static bool CheckIfEqualToFirst(Nut10ProofSecret first, Nut10ProofSecret other) => + first is { } a && other is { } b && a.Data == b.Data && ((a.Tags == null && b.Tags == null) || (a.Tags != null && b.Tags != null && a.Tags.SequenceEqual(b.Tags))); diff --git a/DotNut/NUT14/HTLCBuilder.cs b/DotNut/NUT14/HTLCBuilder.cs index 5fb03bb..f236a71 100644 --- a/DotNut/NUT14/HTLCBuilder.cs +++ b/DotNut/NUT14/HTLCBuilder.cs @@ -41,6 +41,7 @@ public static HTLCBuilder Load(HTLCProofSecret proofSecret) SigFlag = innerbuilder.SigFlag, Nonce = innerbuilder.Nonce }; + } public new HTLCProofSecret Build() From 2500811a11ef71c0007273588707329e250c5232 Mon Sep 17 00:00:00 2001 From: d4rp4t Date: Fri, 21 Nov 2025 20:59:16 +0100 Subject: [PATCH 16/70] cleanup & bolt12 --- .../Handlers/MeltHandlerBolt12.cs | 83 ++++++++++++++++++- DotNut/Abstractions/InMemoryProofManager.cs | 30 ------- .../Abstractions/Interfaces/IProofManager.cs | 9 -- .../Interfaces/IStatefulWalletBuilder.cs | 13 --- .../Abstractions/Interfaces/IWalletBuilder.cs | 10 --- DotNut/Abstractions/MeltQuoteBuilder.cs | 22 ++++- DotNut/Abstractions/StatefulWallet.cs | 35 -------- DotNut/Abstractions/Wallet.cs | 13 --- 8 files changed, 100 insertions(+), 115 deletions(-) delete mode 100644 DotNut/Abstractions/InMemoryProofManager.cs delete mode 100644 DotNut/Abstractions/Interfaces/IProofManager.cs delete mode 100644 DotNut/Abstractions/Interfaces/IStatefulWalletBuilder.cs delete mode 100644 DotNut/Abstractions/StatefulWallet.cs diff --git a/DotNut/Abstractions/Handlers/MeltHandlerBolt12.cs b/DotNut/Abstractions/Handlers/MeltHandlerBolt12.cs index 80a96b2..71d6d3d 100644 --- a/DotNut/Abstractions/Handlers/MeltHandlerBolt12.cs +++ b/DotNut/Abstractions/Handlers/MeltHandlerBolt12.cs @@ -1,19 +1,94 @@ +using System.Text.Json; using DotNut.Abstractions.Interfaces; using DotNut.Abstractions.Websockets; +using DotNut.ApiModels; using DotNut.ApiModels.Melt.bolt12; namespace DotNut.Abstractions.Handlers; public class MeltHandlerBolt12: IMeltHandler> { - public Task GetQuote(CancellationToken ct = default) + private IWalletBuilder _wallet; + private PostMeltQuoteBolt12Response _quote; + private OutputData _blankOutputs; + private bool _withSignatureVerification; + private List? _privKeys; + private string? _htlcPreimage; + + public MeltHandlerBolt12( + IWalletBuilder wallet, + PostMeltQuoteBolt12Response quote, + OutputData blankOutputs, + List? privKeys = null, + string? htlcPreimage = null) { - throw new NotImplementedException(); + } + public async Task GetQuote(CancellationToken ct = default) => this._quote; + public async Task> Melt(List inputs, CancellationToken ct = default) + { + MaybeProcessP2PkHTLC(inputs); + var client = await _wallet.GetMintApi(); + var req = new PostMeltRequest + { + Quote = _quote.Quote, + Inputs = inputs.ToArray(), + Outputs = _blankOutputs.BlindedMessages.ToArray(), + }; + + var res = await client.Melt("bolt11", req, ct); + if (res.Change == null) + { + return []; + } - public Task> Melt(List inputs, CancellationToken ct = default) + var keyset = await _wallet.GetKeys(res.Change.First().Id, false, ct); + return Utils.ConstructProofsFromPromises(res.Change.ToList(), _blankOutputs, keyset.Keys); + } + + private void MaybeProcessP2PkHTLC(List proofs) { - throw new NotImplementedException(); + if (_privKeys == null || _privKeys.Count == 0) + { + return; + } + + if (proofs == null) + { + throw new ArgumentNullException(nameof(proofs), "No proofs to melt!"); + } + + var sigAllHandler = new SigAllHandler + { + Proofs = proofs, + BlindedMessages = this._blankOutputs?.BlindedMessages ?? [], + MeltQuoteId = _quote.Quote, + HTLCPreimage = this._htlcPreimage, + }; + + if (sigAllHandler.TrySign(out P2PKWitness? witness)) + { + if (witness == null) + { + throw new ArgumentNullException(nameof(witness), "sig_all input was correct, but couldn't create a witness signature!"); + } + proofs[0].Witness = JsonSerializer.Serialize(witness); + return; + } + + foreach (var proof in proofs) + { + if (proof.Secret is not Nut10Secret { ProofSecret: P2PKProofSecret p2pk, Key: { } key }) continue; + if (proof.Secret is Nut10Secret { ProofSecret: HTLCProofSecret htlc } && _htlcPreimage is {} preimage) + { + var w = htlc.GenerateWitness(proof, _privKeys.Select(p=>p.Key).ToArray(), preimage); + proof.Witness = JsonSerializer.Serialize(w); + continue; + } + var proofWitness = p2pk.GenerateWitness(proof, _privKeys.Select(p => p.Key).ToArray()); + proof.Witness = JsonSerializer.Serialize(proofWitness); + } } + } \ No newline at end of file diff --git a/DotNut/Abstractions/InMemoryProofManager.cs b/DotNut/Abstractions/InMemoryProofManager.cs deleted file mode 100644 index b6c53de..0000000 --- a/DotNut/Abstractions/InMemoryProofManager.cs +++ /dev/null @@ -1,30 +0,0 @@ -namespace DotNut.Abstractions.Interfaces; - -public class InMemoryProofManager: IProofManager -{ - private Dictionary> _proofsDictionary = new(); - - public async Task AddProofAsync(Proof proof, CancellationToken ct = default) - { - if (_proofsDictionary.TryGetValue(proof.Id, out var proofs)) - { - proofs.Add(proof); - return; - } - _proofsDictionary.Add(proof.Id, new List { proof }); - } - - public async Task> GetProofsForKeysetId(KeysetId ids, CancellationToken ct = default) - { - return _proofsDictionary.TryGetValue(ids, out var proofs) ? proofs : new List(); - } - - public Task> GetProofsForMint(List ids, CancellationToken ct = default) - { - throw new NotImplementedException(); - } - public Task MarkProofAsSpent(Proof proof, CancellationToken ct = default) - { - throw new NotImplementedException(); - } -} \ No newline at end of file diff --git a/DotNut/Abstractions/Interfaces/IProofManager.cs b/DotNut/Abstractions/Interfaces/IProofManager.cs deleted file mode 100644 index 0bfb934..0000000 --- a/DotNut/Abstractions/Interfaces/IProofManager.cs +++ /dev/null @@ -1,9 +0,0 @@ -namespace DotNut.Abstractions.Interfaces; - -public interface IProofManager -{ - Task AddProofAsync(Proof proof, CancellationToken ct = default); - Task> GetProofsForKeysetId(KeysetId ids, CancellationToken ct = default); - Task> GetProofsForMint(List ids, CancellationToken ct = default); // should still query proofs based on keysetid - Task MarkProofAsSpent(Proof proof, CancellationToken ct = default); -} \ No newline at end of file diff --git a/DotNut/Abstractions/Interfaces/IStatefulWalletBuilder.cs b/DotNut/Abstractions/Interfaces/IStatefulWalletBuilder.cs deleted file mode 100644 index 135321a..0000000 --- a/DotNut/Abstractions/Interfaces/IStatefulWalletBuilder.cs +++ /dev/null @@ -1,13 +0,0 @@ -namespace DotNut.Abstractions.Interfaces; - -/// -/// Abstraction on WalletBuilder, with Proof Manager. Stateful wallet library, abstracting all operations like melting/minting. -/// -/// -public interface IStatefulWalletBuilder -{ - Task ReceiveLightning(); - Task SendLightning(); - Task ReceiveProofs(); - Task SendProofs(); -} \ No newline at end of file diff --git a/DotNut/Abstractions/Interfaces/IWalletBuilder.cs b/DotNut/Abstractions/Interfaces/IWalletBuilder.cs index fdc36ef..c5c787d 100644 --- a/DotNut/Abstractions/Interfaces/IWalletBuilder.cs +++ b/DotNut/Abstractions/Interfaces/IWalletBuilder.cs @@ -127,16 +127,6 @@ public interface IWalletBuilder /// IWalletBuilder WithWebsocketService(IWebsocketService websocketService); - /// - /// Optional. - /// Allows user to build stateful wallet, by providing a proof manager - a class allowing wallet to fetch, save and use proofs from desired kind of storage. - /// (See InMemoryProofManager.cs) - /// - /// - /// - IStatefulWalletBuilder WithProofManager(IProofManager proofManager); - - Task GetInfo(bool forceReferesh = false, CancellationToken ct = default); Task CreateOutputs(List amounts, KeysetId id, CancellationToken ct = default); diff --git a/DotNut/Abstractions/MeltQuoteBuilder.cs b/DotNut/Abstractions/MeltQuoteBuilder.cs index 5ffeebd..711a6f6 100644 --- a/DotNut/Abstractions/MeltQuoteBuilder.cs +++ b/DotNut/Abstractions/MeltQuoteBuilder.cs @@ -107,7 +107,27 @@ public async Task>> Proces public async Task>> ProcessAsyncBolt12( CancellationToken ct = default) { - throw new NotImplementedException(); + var mintApi = await _wallet.GetMintApi(); + await _wallet._maybeSyncKeys(ct); + ArgumentNullException.ThrowIfNull(this._invoice); + + var req = new PostMeltQuoteBolt12Request() + { + Request = this._invoice, + Unit = this._unit, + // todo melt quote bolt12 options + }; + var quote = + await mintApi.CreateMeltQuote("bolt12", req, ct); + + + if (_blankOutputs == null) + { + var outputsAmount = Utils.CalculateNumberOfBlankOutputs((ulong)quote.FeeReserve); + var amounts = Enumerable.Repeat(1UL, outputsAmount).ToList(); + this._blankOutputs = await this._wallet.CreateOutputs(amounts, this._unit, ct); + } + return new MeltHandlerBolt12(_wallet, quote, _blankOutputs, _privKeys, _htlcPreimage); } diff --git a/DotNut/Abstractions/StatefulWallet.cs b/DotNut/Abstractions/StatefulWallet.cs deleted file mode 100644 index 62949df..0000000 --- a/DotNut/Abstractions/StatefulWallet.cs +++ /dev/null @@ -1,35 +0,0 @@ -using DotNut.Abstractions.Interfaces; - -namespace DotNut.Abstractions; - -public class StatefulWallet: IStatefulWalletBuilder -{ - private IWalletBuilder _wallet; - private IProofManager _proofManager; - - public StatefulWallet(IWalletBuilder wallet, IProofManager proofManager) - { - this._wallet = wallet; - this._proofManager = proofManager; - } - - public Task ReceiveLightning() - { - throw new NotImplementedException(); - } - - public Task SendLightning() - { - throw new NotImplementedException(); - } - - public Task ReceiveProofs() - { - throw new NotImplementedException(); - } - - public Task SendProofs() - { - throw new NotImplementedException(); - } -} \ No newline at end of file diff --git a/DotNut/Abstractions/Wallet.cs b/DotNut/Abstractions/Wallet.cs index 655f645..0f3830b 100644 --- a/DotNut/Abstractions/Wallet.cs +++ b/DotNut/Abstractions/Wallet.cs @@ -157,19 +157,6 @@ public IWalletBuilder WithWebsocketService(IWebsocketService websocketService) } - /// - /// Optional. - /// Allows user to build stateful wallet, by providing a proof manager - a class allowing wallet to fetch, save and use proofs from desired kind of storage. - /// (See InMemoryProofManager.cs) - /// - /// - /// - public IStatefulWalletBuilder WithProofManager(IProofManager proofManager) - { - return new StatefulWallet(this, proofManager); - } - - /* * Main api methods */ From b071ccf5efc6061b18f6574f964c2d2e981c3f6b Mon Sep 17 00:00:00 2001 From: d4rp4t Date: Tue, 25 Nov 2025 16:13:18 +0100 Subject: [PATCH 17/70] XMLDoc --- .../Interfaces/IMeltQuoteBuilder.cs | 27 +++++++++++ .../Interfaces/IMintQuoteBuilder.cs | 30 ++++++++++++ .../Abstractions/Interfaces/ISwapBuilder.cs | 47 +++++++++++++++++++ DotNut/Abstractions/MeltQuoteBuilder.cs | 17 ------- DotNut/Abstractions/MintQuoteBuilder.cs | 40 ---------------- DotNut/Abstractions/SwapBuilder.cs | 41 ---------------- DotNut/NUT00/CashuToken.cs | 10 ++++ 7 files changed, 114 insertions(+), 98 deletions(-) diff --git a/DotNut/Abstractions/Interfaces/IMeltQuoteBuilder.cs b/DotNut/Abstractions/Interfaces/IMeltQuoteBuilder.cs index 095e521..2b4ea98 100644 --- a/DotNut/Abstractions/Interfaces/IMeltQuoteBuilder.cs +++ b/DotNut/Abstractions/Interfaces/IMeltQuoteBuilder.cs @@ -8,12 +8,39 @@ namespace DotNut.Abstractions.Interfaces; /// public interface IMeltQuoteBuilder { + /// + /// Optional. Sets the base unit for the quote; defaults to sat. + /// IMeltQuoteBuilder WithUnit(string unit); + + /// + /// Mandatory. A bolt11 invoice is required to create a melt quote. + /// IMeltQuoteBuilder WithInvoice(string bolt11Invoice); + + /// + /// Optional. Supply previously generated blank outputs instead of deriving them. + /// IMeltQuoteBuilder WithBlankOutputs(OutputData blankOutputs); + + /// + /// Optional. Provide private keys for P2PK proofs associated with the inputs. + /// IMeltQuoteBuilder WithPrivkeys(IEnumerable privKeys); + + /// + /// Optional. Supply HTLC preimage to sign HTLC-based proofs. + /// IMeltQuoteBuilder WithHTLCPreimage(string preimage); + + /// + /// Create a bolt11 melt handler. + /// Task>> ProcessAsyncBolt11(CancellationToken ct = default); + + /// + /// Create a bolt12 melt handler. + /// Task>> ProcessAsyncBolt12(CancellationToken ct = default); } \ No newline at end of file diff --git a/DotNut/Abstractions/Interfaces/IMintQuoteBuilder.cs b/DotNut/Abstractions/Interfaces/IMintQuoteBuilder.cs index 833b78e..3790d9c 100644 --- a/DotNut/Abstractions/Interfaces/IMintQuoteBuilder.cs +++ b/DotNut/Abstractions/Interfaces/IMintQuoteBuilder.cs @@ -8,14 +8,44 @@ namespace DotNut.Abstractions.Interfaces; /// public interface IMintQuoteBuilder { + /// + /// Optional. Sets unit of tokens being minted; defaults to satoshi. + /// IMintQuoteBuilder WithUnit(string unit); + + /// + /// Mandatory. Amount of tokens to mint in the current unit. + /// IMintQuoteBuilder WithAmount(ulong amount); + + /// + /// Optional. Provide precomputed outputs so blinding factors and secrets are reused safely. + /// IMintQuoteBuilder WithOutputs(OutputData outputs); + /// + /// Optional. Provide description for the mint invoice. + /// IMintQuoteBuilder WithDescription(string description); + /// + /// Optional. Allows providing a P2PK builder when a signature is required for minting. + /// IMintQuoteBuilder WithP2PkLock(P2PkBuilder p2pkBuilder); + + /// + /// Optional. Allows adding HTLC-based outputs. + /// + IMintQuoteBuilder WithHTLCLock(HTLCBuilder htlcBuilder); + + /// + /// Creates a bolt11 mint quote and handler. + /// Task>> ProcessAsyncBolt11(CancellationToken ct = default); + + /// + /// Creates a bolt12 mint quote and handler. + /// Task>> ProcessAsyncBolt12(CancellationToken ct = default); } \ No newline at end of file diff --git a/DotNut/Abstractions/Interfaces/ISwapBuilder.cs b/DotNut/Abstractions/Interfaces/ISwapBuilder.cs index e2b327d..fc2feab 100644 --- a/DotNut/Abstractions/Interfaces/ISwapBuilder.cs +++ b/DotNut/Abstractions/Interfaces/ISwapBuilder.cs @@ -5,16 +5,63 @@ namespace DotNut.Abstractions.Interfaces; /// public interface ISwapBuilder { + /// + /// Optional. Sets wallet unit for the swap; defaults to "sat". + /// ISwapBuilder WithUnit(string unit); + + /// + /// Optional. Choose target keyset for the swapped proofs. + /// ISwapBuilder ForKeyset(KeysetId targetKeysetId); + + /// + /// Provide proofs that will be used as inputs for the swap. + /// ISwapBuilder FromInputs(IEnumerable inputs); + + /// + /// Optional. Supply custom blank outputs instead of deriving them automatically. + /// ISwapBuilder ForOutputs(OutputData outputs); + + /// + /// Optional. Toggle DLEQ verification for incoming proofs. + /// ISwapBuilder WithDLEQVerification(bool verify = true); + + /// + /// Optional. Include or skip fee calculations when creating outputs. + /// ISwapBuilder WithFeeCalculation(bool includeFees = true); + + /// + /// Optional. Explicitly select output amounts. + /// ISwapBuilder WithAmounts(IEnumerable amounts); + + /// + /// Optional. Provide private keys associated with the proofs. + /// ISwapBuilder WithPrivkeys(IEnumerable privKeys); + + /// + /// Optional. Generate outputs guarded by P2PK locking. + /// ISwapBuilder ToP2PK(P2PkBuilder p2pkBuilder); + + /// + /// Optional. Supply preimage for HTLC-based proofs. + /// ISwapBuilder WithHtlcPreimage(string preimage); + + /// + /// Optional. Generate HTLC outputs. + /// ISwapBuilder ToHTLC(HTLCBuilder htlcBuilder); + + /// + /// Executes the swap flow and returns newly minted proofs. + /// Task> ProcessAsync(CancellationToken ct = default); } diff --git a/DotNut/Abstractions/MeltQuoteBuilder.cs b/DotNut/Abstractions/MeltQuoteBuilder.cs index 711a6f6..0d9fab0 100644 --- a/DotNut/Abstractions/MeltQuoteBuilder.cs +++ b/DotNut/Abstractions/MeltQuoteBuilder.cs @@ -24,35 +24,18 @@ public MeltQuoteBuilder(Wallet wallet) _wallet = wallet; } - /// - /// Mandatory. - /// Invoice must be provided in order to create (Lightning) MeltQuote. - /// - /// - /// public IMeltQuoteBuilder WithInvoice(string invoice) { this._invoice = invoice; return this; } - /// - /// Optional. - /// If not set, defaults to satoshi. If token has other unit, must be set. - /// - /// - /// public IMeltQuoteBuilder WithUnit(string unit) { this._unit = unit; return this; } - /// - /// Optional. Allows user to specify blank outputs. If not set, these will be generated automatically. - /// - /// - /// public IMeltQuoteBuilder WithBlankOutputs(OutputData blankOutputs) { this._blankOutputs = blankOutputs; diff --git a/DotNut/Abstractions/MintQuoteBuilder.cs b/DotNut/Abstractions/MintQuoteBuilder.cs index 7966ff6..4154bff 100644 --- a/DotNut/Abstractions/MintQuoteBuilder.cs +++ b/DotNut/Abstractions/MintQuoteBuilder.cs @@ -42,44 +42,24 @@ public IMintQuoteBuilder WithMethod(string method) return this; } - /// - /// Mandatory. - /// - /// Amount of token in currently choosen unit to be melted public IMintQuoteBuilder WithAmount(ulong amount) { this._amount = amount; return this; } - /// - /// Optional. - /// Sets unit of tokens being minted. Sat by default. - /// - /// Unit of minted proofs public IMintQuoteBuilder WithUnit(string unit) { this._unit = unit; return this; } - /// - /// Optional. If specified, to mint the tokens user needs to provide signature on mint quote. - /// Necessary for bolt12 - /// - /// - /// public IMintQuoteBuilder WithPubkey(string pubkey) { this._pubkey = pubkey; return this; } - /// - /// Optional. - /// Allows user to set keysetId manually. Otherwise, builder will choose active one manually, with the lowest fees. - /// - /// public IMintQuoteBuilder WithKeyset(KeysetId keysetId) { this._keysetId = keysetId; @@ -87,26 +67,12 @@ public IMintQuoteBuilder WithKeyset(KeysetId keysetId) } - /// - /// Optional. - /// User may provide outputs for mint to sign. Blinding factors and secrets won't be revealed to mint. - /// If not provided, wallet will try to derive them from seed and counter, or create random ones if mnemonic is not avaible. - /// - /// OutputData instance. Enumerables of BlindingFactors, BlindedMessages and Secrets, in right order. public IMintQuoteBuilder WithOutputs(OutputData outputs) { this._outputs = outputs; return this; } - /// - /// Optional. - /// User may provide p2pkbuilder specifying p2pk lock parameters. Nonce from builder will be added _only_ to first proof, - /// since it has to be unique for each proof. - /// P2Pk proofs aren't derived deterministicly, since they can't get restored from seed and they would make restore process longer. - /// - /// - /// public IMintQuoteBuilder WithP2PkLock(P2PkBuilder p2pkBuilder) { this._builder = p2pkBuilder; @@ -119,12 +85,6 @@ public IMintQuoteBuilder WithHTLCLock(HTLCBuilder htlcBuilder) return this; } - /// - /// Optional. - /// User may provide description for melt quote invoice. - /// - /// - /// public IMintQuoteBuilder WithDescription(string description) { this._description = description; diff --git a/DotNut/Abstractions/SwapBuilder.cs b/DotNut/Abstractions/SwapBuilder.cs index 029008d..2519e2a 100644 --- a/DotNut/Abstractions/SwapBuilder.cs +++ b/DotNut/Abstractions/SwapBuilder.cs @@ -46,21 +46,12 @@ public SwapBuilder(Wallet wallet) _wallet = wallet; } - /// - /// Optional. Base unit of wallet instance. If not set defaults to "SAT". - /// - /// public ISwapBuilder WithUnit(string unit) { this._unit = unit; return this; } - /// - /// Provide inputs for a swap. - /// - /// - /// public ISwapBuilder FromInputs(IEnumerable proofs) { this._proofsToSwap = proofs.ToList(); @@ -73,48 +64,24 @@ public ISwapBuilder ForOutputs(OutputData outputs) return this; } - /// - /// Optional. - /// True by default, allows user to turn off DLEQ verification (not advised) - /// - /// - /// public ISwapBuilder WithDLEQVerification(bool verify = true) { _verifyDLEQ = verify; return this; } - /// - /// Optional. - /// Allows user to turn off fee calculation. By default, it will calculate and generate smaller set of outputs. - /// - /// - /// public ISwapBuilder WithFeeCalculation(bool includeFees = true) { this._includeFees = includeFees; return this; } - /// - /// Optional. Allows user to choose amounts he wants to get. - /// If sum of amounts smaller than input size, all proofs will be swapped, but rest of proofs will get - /// standard outputs amounts (biggest proof size possible) - /// - /// - /// public ISwapBuilder WithAmounts(IEnumerable amounts) { _amounts = amounts.ToList(); return this; } - /// - /// Optional. Allows user to choose destination keysetId - /// - /// - /// public ISwapBuilder ForKeyset(KeysetId keysetId) { _keysetId = keysetId; @@ -128,14 +95,6 @@ public ISwapBuilder WithPrivkeys(IEnumerable privKeys) return this; } - /// - /// Optional. - /// If provided, every proof will be generated with random nonce. - /// P2Pk tokens aren't deterministic. if lost - ¯\_(ツ)_/¯ - /// - /// - /// - /// public ISwapBuilder ToP2PK(P2PkBuilder p2pkBuilder) { this._builder = p2pkBuilder; diff --git a/DotNut/NUT00/CashuToken.cs b/DotNut/NUT00/CashuToken.cs index 4efb1cb..7327747 100644 --- a/DotNut/NUT00/CashuToken.cs +++ b/DotNut/NUT00/CashuToken.cs @@ -6,6 +6,16 @@ public class CashuToken { public class Token { + public Token() + { + } + + public Token(string mint, List proofs) + { + Mint = mint; + Proofs = proofs; + } + [JsonPropertyName("mint")] public string Mint { get; set; } [JsonPropertyName("proofs")] public List Proofs { get; set; } } From 6cb0a14ba85570826447092b257288ef251c5893 Mon Sep 17 00:00:00 2001 From: d4rp4t Date: Wed, 26 Nov 2025 18:32:54 +0100 Subject: [PATCH 18/70] Fix P2PkE serialization issue killing Token Encoding / Decoding --- DotNut/Encoding/CashuTokenV4Encoder.cs | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/DotNut/Encoding/CashuTokenV4Encoder.cs b/DotNut/Encoding/CashuTokenV4Encoder.cs index 480cfd4..e4d5ea9 100644 --- a/DotNut/Encoding/CashuTokenV4Encoder.cs +++ b/DotNut/Encoding/CashuTokenV4Encoder.cs @@ -53,7 +53,7 @@ public CBORObject ToCBORObject(CashuToken token) if (proof.P2PkE?.Key is not null) { - proofItem.Add("pe", Convert.FromHexString(proof.P2PkE.Key.ToString())); + proofItem.Add("pe", proof.P2PkE.Key.ToBytes()); } proofSetItemArray.Add(proofItem); @@ -76,6 +76,14 @@ public CBORObject ToCBORObject(CashuToken token) public CashuToken FromCBORObject(CBORObject obj) { + var peValue = obj.GetOrDefault("pe", null); + Console.WriteLine($"pe exists: {peValue != null}"); + if (peValue != null) + { + Console.WriteLine($"pe type: {peValue.Type}"); + Console.WriteLine($"pe bytes length: {peValue.GetByteString()?.Length}"); + } + return new CashuToken { Unit = obj["u"].AsString(), @@ -105,8 +113,9 @@ public CashuToken FromCBORObject(CBORObject obj) : null, Id = id, - P2PkE = proof.GetOrDefault("pe", null) is { } p2pkE? - ECPubKey.Create(p2pkE.GetByteString()) : null + P2PkE = proof.GetOrDefault("pe", null) is { } p2pkE + ? (PubKey?) ECPubKey.Create(p2pkE.GetByteString()) + : null }); }).ToList() From b36150a0a3aab605c2ddf33e3c280cda7d36409d Mon Sep 17 00:00:00 2001 From: d4rp4t Date: Wed, 26 Nov 2025 18:38:10 +0100 Subject: [PATCH 19/70] use hidden methods by specyfing a inherited object type explictly. add check state to wallet --- .../Abstractions/Interfaces/IWalletBuilder.cs | 8 +++++++- DotNut/Abstractions/MintQuoteBuilder.cs | 2 +- DotNut/Abstractions/SwapBuilder.cs | 2 +- DotNut/Abstractions/Utils.cs | 19 ++++++++++++------- DotNut/Abstractions/Wallet.cs | 3 --- 5 files changed, 21 insertions(+), 13 deletions(-) diff --git a/DotNut/Abstractions/Interfaces/IWalletBuilder.cs b/DotNut/Abstractions/Interfaces/IWalletBuilder.cs index c5c787d..a426ae4 100644 --- a/DotNut/Abstractions/Interfaces/IWalletBuilder.cs +++ b/DotNut/Abstractions/Interfaces/IWalletBuilder.cs @@ -173,6 +173,12 @@ Task SelectProofsToSend(List proofs, ulong amount, bool inc /// /// IRestoreBuilder Restore(); - + + /// + /// Check state of proofs + /// + /// + Task CheckState(IEnumerable proofs); + } diff --git a/DotNut/Abstractions/MintQuoteBuilder.cs b/DotNut/Abstractions/MintQuoteBuilder.cs index 4154bff..c58ba48 100644 --- a/DotNut/Abstractions/MintQuoteBuilder.cs +++ b/DotNut/Abstractions/MintQuoteBuilder.cs @@ -191,7 +191,7 @@ async Task _createOutputs() // skipped checks for keysetid and keys, since its validated before. make sure to remember about it. foreach (var amount in _amounts) { - var p2pkOutput = Utils.CreateP2PkOutput(amount, this._keysetId!, this._keyset.Keys, _builder); + var p2pkOutput = Utils.CreateNut10Output(amount, this._keysetId!, _builder); outputs.BlindingFactors.Add(p2pkOutput.BlindingFactors[0]); outputs.BlindedMessages.Add(p2pkOutput.BlindedMessages[0]); outputs.Secrets.Add(p2pkOutput.Secrets[0]); diff --git a/DotNut/Abstractions/SwapBuilder.cs b/DotNut/Abstractions/SwapBuilder.cs index 2519e2a..2811027 100644 --- a/DotNut/Abstractions/SwapBuilder.cs +++ b/DotNut/Abstractions/SwapBuilder.cs @@ -233,7 +233,7 @@ async Task _getOutputs(Keyset keys, CancellationToken ct = default) if (this._builder is not null) { // skipped checks for keysetid and keys, since its validated before. make sure to remember about it. - foreach (var p2pkOutput in _amounts.Select(amount => Utils.CreateP2PkOutput(amount, this._keysetId!, keys, _builder))) + foreach (var p2pkOutput in _amounts.Select(amount => Utils.CreateNut10Output(amount, this._keysetId!, _builder))) { outputs.BlindingFactors.Add(p2pkOutput.BlindingFactors[0]); outputs.BlindedMessages.Add(p2pkOutput.BlindedMessages[0]); diff --git a/DotNut/Abstractions/Utils.cs b/DotNut/Abstractions/Utils.cs index c0c26d7..34edaa5 100644 --- a/DotNut/Abstractions/Utils.cs +++ b/DotNut/Abstractions/Utils.cs @@ -1,7 +1,5 @@ -using System.Runtime.InteropServices.ComTypes; using System.Security.Cryptography; using DotNut.NUT13; -using NBitcoin.Secp256k1; namespace DotNut.Abstractions; @@ -136,19 +134,26 @@ public static OutputData CreateOutputs( }; } - public static OutputData CreateP2PkOutput( + public static OutputData CreateNut10Output( ulong amount, KeysetId keysetId, - Keyset keys, P2PkBuilder builder ) { - var proofSecret = builder.Build(); - var secret = new Nut10Secret("P2PK", proofSecret); + // ugliest hack ever + Nut10Secret secret; + if (builder is HTLCBuilder htlc) + { + secret = new Nut10Secret("HTLC", htlc.Build()); + } + else + { + secret = new Nut10Secret("P2PK", builder.Build()); + } var r = RandomPrivkey(); var B_ = Cashu.ComputeB_(secret.ToCurve(), r); - return new OutputData() + return new OutputData { BlindedMessages = [new BlindedMessage() { Amount = amount, B_ = B_, Id = keysetId }], BlindingFactors = [r], diff --git a/DotNut/Abstractions/Wallet.cs b/DotNut/Abstractions/Wallet.cs index 0f3830b..c842204 100644 --- a/DotNut/Abstractions/Wallet.cs +++ b/DotNut/Abstractions/Wallet.cs @@ -160,8 +160,6 @@ public IWalletBuilder WithWebsocketService(IWebsocketService websocketService) /* * Main api methods */ - - public IMintQuoteBuilder CreateMintQuote() { _ensureApiConnected(); @@ -179,7 +177,6 @@ public IMeltQuoteBuilder CreateMeltQuote() _ensureApiConnected(); return new MeltQuoteBuilder(this); } - public async Task CheckState(IEnumerable proofs) { From bf64fd2796966f7f9385421c23b689fff43e8e45 Mon Sep 17 00:00:00 2001 From: d4rp4t Date: Wed, 26 Nov 2025 18:40:34 +0100 Subject: [PATCH 20/70] integration tests. htlc will likely fail for now. --- DotNut.Tests/Integration.cs | 255 ++++++++++++---- DotNut.Tests/UnitTests2.cs | 566 +++++++++++++++++++++++++++++++++++- 2 files changed, 758 insertions(+), 63 deletions(-) diff --git a/DotNut.Tests/Integration.cs b/DotNut.Tests/Integration.cs index 8429d65..e36ae23 100644 --- a/DotNut.Tests/Integration.cs +++ b/DotNut.Tests/Integration.cs @@ -3,10 +3,7 @@ using DotNut.Abstractions.Interfaces; using DotNut.Abstractions.Websockets; using DotNut.Api; -using DotNut.ApiModels; -using Newtonsoft.Json; -using NuGet.Frameworks; -using Xunit.Sdk; +using SHA256 = System.Security.Cryptography.SHA256; namespace DotNut.Tests; @@ -298,8 +295,7 @@ await Assert.ThrowsAsync( Assert.NotEmpty(swappedProofs); } - - + [Fact] public async Task MintMeltP2PkMultisig() { @@ -349,76 +345,221 @@ await Assert.ThrowsAsync(async () => Assert.NotEmpty(change); } -[Fact] -public async Task SubscribeToMintMeltQuoteUpdates() -{ - await using var service = new WebsocketService(); - var connection = await service.ConnectAsync(MintUrl); - Assert.NotNull(connection); + [Fact] + public async Task MintSwapHTLC() + { + var wallet = Wallet + .Create() + .WithMint(MintUrl); + + var privKeyBob = new PrivKey(RandomNumberGenerator.GetHexString(64, true)); + var preimage = "0000000000000000000000000000000000000000000000000000000000000001"; + var hashLock = Convert.ToHexString(SHA256.HashData(Convert.FromHexString(preimage))); + + var mintHandler = await wallet.CreateMintQuote() + .WithAmount(1337) + .WithHTLCLock(new HTLCBuilder() + { + HashLock = hashLock, + Pubkeys = [privKeyBob.Key.CreatePubKey()], + SignatureThreshold = 1 + }) + .ProcessAsyncBolt11(); + + await PayInvoice(); + var htlcProofs = await mintHandler.Mint(); + + Assert.NotEmpty(htlcProofs); + Assert.Equal(1337UL, Utils.SumProofs(htlcProofs)); + + // try swap without preimage - should fail + await Assert.ThrowsAsync(async () => + { + await wallet.Swap() + .FromInputs(htlcProofs) + .WithPrivkeys([privKeyBob]) + .ProcessAsync(); + }); + + // swap with correct preimage and signature + var swappedProofs = await wallet.Swap() + .FromInputs(htlcProofs) + .WithPrivkeys([privKeyBob]) + .WithHtlcPreimage(preimage) + .ProcessAsync(); + + Assert.NotEmpty(swappedProofs); + Assert.Equal(1337UL, Utils.SumProofs(swappedProofs)); + } + + [Fact] + public async Task SwapWithCustomAmounts() + { + var wallet = Wallet + .Create() + .WithMint(MintUrl); + + // mint some proofs + var mintQuote = await wallet + .CreateMintQuote() + .WithAmount(100) + .WithUnit("sat") + .ProcessAsyncBolt11(); + + await PayInvoice(); + var mintedProofs = await mintQuote.Mint(); + Assert.NotEmpty(mintedProofs); + + // swap with specific amounts + var desiredAmounts = new List { 32, 32, 32, 2, 1 }; // 96 sat (should consume 1 for fees) + var newProofs = await wallet + .Swap() + .FromInputs(mintedProofs) + .WithAmounts(desiredAmounts) + .ProcessAsync(); + + Assert.NotEmpty(newProofs); + // amount should be at least the requested amounts + Assert.True(Utils.SumProofs(newProofs) >= 96); + } + + [Fact] + public async Task SwapToSpecificKeyset() + { + var wallet = Wallet + .Create() + .WithMint(MintUrl); + + // get active keyset + var activeKeysetId = await wallet.GetActiveKeysetId("sat"); + Assert.NotNull(activeKeysetId); + + // mint some proofs + var mintQuote = await wallet + .CreateMintQuote() + .WithAmount(64) + .WithUnit("sat") + .ProcessAsyncBolt11(); - var wallet = Wallet.Create().WithMint(MintUrl); + await PayInvoice(); + var mintedProofs = await mintQuote.Mint(); + Assert.NotEmpty(mintedProofs); + + // swap to specific keyset + var newProofs = await wallet + .Swap() + .FromInputs(mintedProofs) + .ForKeyset(activeKeysetId) + .ProcessAsync(); + + Assert.NotEmpty(newProofs); + Assert.All(newProofs, p => Assert.Equal(activeKeysetId, p.Id)); + } + + [Fact] + public async Task MeltWithInsufficientFunds() + { + var wallet = Wallet + .Create() + .WithMint(MintUrl); + + // mint small amount + var mintQuote = await wallet + .CreateMintQuote() + .WithAmount(10) + .WithUnit("sat") + .ProcessAsyncBolt11(); - var mintHandler = await wallet - .CreateMintQuote() - .WithAmount(3338) - .WithUnit("sat") - .ProcessAsyncBolt11(); + await PayInvoice(); + var mintedProofs = await mintQuote.Mint(); + Assert.NotEmpty(mintedProofs); + + // try to melt for larger invoice - should fail during proof selection + var meltHandler = await wallet + .CreateMeltQuote() + .WithInvoice(valuesInvoices[1000]) // 1000 sat invoice + .WithUnit("sat") + .ProcessAsyncBolt11(); + + var quote = await meltHandler.GetQuote(); + var amountNeeded = quote.Amount + (ulong)quote.FeeReserve; + + // selectProofsToSend should return empty Send list when insufficient + var selection = await wallet.SelectProofsToSend(mintedProofs, amountNeeded, true); + Assert.Empty(selection.Send); + Assert.NotEmpty(selection.Keep); + } + + [Fact] + public async Task SubscribeToMintMeltQuoteUpdates() + { + await using var service = new WebsocketService(); + var connection = await service.ConnectAsync(MintUrl); + Assert.NotNull(connection); - var quote = await mintHandler.GetQuote(); + var wallet = Wallet.Create().WithMint(MintUrl); - var sub = await service.SubscribeToMintQuoteAsync(MintUrl, new[] { quote.Quote }); + var mintHandler = await wallet + .CreateMintQuote() + .WithAmount(3338) + .WithUnit("sat") + .ProcessAsyncBolt11(); - int connectedCount = 0; - int notificationCount = 0; + var quote = await mintHandler.GetQuote(); - using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(60)); + var sub = await service.SubscribeToMintQuoteAsync(MintUrl, new[] { quote.Quote }); - var connectedTcs = new TaskCompletionSource(); - var paidTcs = new TaskCompletionSource(); + int connectedCount = 0; + int notificationCount = 0; - _ = Task.Run(async () => - { - await connectedTcs.Task.WaitAsync(TimeSpan.FromSeconds(10)); - await Task.Delay(1000, cts.Token); - await PayInvoice(); - }, cts.Token); + using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(60)); - await foreach (var msg in sub.NotificationChannel.Reader.ReadAllAsync(cts.Token)) - { - switch (msg) + var connectedTcs = new TaskCompletionSource(); + var paidTcs = new TaskCompletionSource(); + + _ = Task.Run(async () => { - case WsMessage.Response: - connectedCount++; - connectedTcs.TrySetResult(); - break; + await connectedTcs.Task.WaitAsync(TimeSpan.FromSeconds(10)); + await Task.Delay(1000, cts.Token); + await PayInvoice(); + }, cts.Token); - case WsMessage.Notification notification: - notificationCount++; + await foreach (var msg in sub.NotificationChannel.Reader.ReadAllAsync(cts.Token)) + { + switch (msg) + { + case WsMessage.Response: + connectedCount++; + connectedTcs.TrySetResult(); + break; - if (notificationCount >= 2) - paidTcs.TrySetResult(); + case WsMessage.Notification notification: + notificationCount++; - break; + if (notificationCount >= 2) + paidTcs.TrySetResult(); - case WsMessage.Error error: - Assert.Fail($"WebSocket error: {error}"); - break; + break; - default: - Assert.Fail($"Unexpected message type: {msg.GetType().Name}"); + case WsMessage.Error error: + Assert.Fail($"WebSocket error: {error}"); + break; + + default: + Assert.Fail($"Unexpected message type: {msg.GetType().Name}"); + break; + } + + if (paidTcs.Task.IsCompleted) break; } - - if (paidTcs.Task.IsCompleted) - break; - } - Assert.Equal(1, connectedCount); - Assert.True(notificationCount >= 2, $"Expected >=2 notifications, got {notificationCount}"); + Assert.Equal(1, connectedCount); + Assert.True(notificationCount >= 2, $"Expected >=2 notifications, got {notificationCount}"); - var proofs = await mintHandler.Mint(); - Assert.NotEmpty(proofs); -} + var proofs = await mintHandler.Mint(); + Assert.NotEmpty(proofs); + } private async Task PayInvoice() diff --git a/DotNut.Tests/UnitTests2.cs b/DotNut.Tests/UnitTests2.cs index 53cc1fa..656f8e7 100644 --- a/DotNut.Tests/UnitTests2.cs +++ b/DotNut.Tests/UnitTests2.cs @@ -1,13 +1,61 @@ +using System.Diagnostics.Metrics; using System.Text.Json; using System.Text.Json.Serialization; using DotNut.Abstractions; +using DotNut.Api; +using DotNut.ApiModels; +using DotNut.NBitcoin.BIP39; +using NBitcoin.Secp256k1; namespace DotNut.Tests; -/// -/// Tests of higher-level abstractions -/// public class UnitTests2 { + private static string MintUrl = "http://localhost:3338"; + + [Fact] + public void BuilderChainingPreservesAllSettings() + { + var counter = new InMemoryCounter(); + var info = new MintInfo(new GetInfoResponse { Version = "0.15.0" }); + var keysets = new GetKeysetsResponse { Keysets = [] }; + var keys = new GetKeysResponse { Keysets = [] }; + var selector = new ProofSelector(new Dictionary()); + var mnemonic = "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about"; + + var wallet = Wallet.Create() + .WithMint(MintUrl) + .WithInfo(info) + .WithKeysets(keysets) + .WithKeys(keys) + .WithSelector(selector) + .WithMnemonic(mnemonic) + .WithCounter(counter) + .WithKeysetSync(true) + .ShouldBumpCounter(false); + + var mnemonicField = wallet.GetType() + .GetField("_mnemonic", + System.Reflection.BindingFlags.NonPublic | + System.Reflection.BindingFlags.Instance); + var mnemonicRef = (Mnemonic?)mnemonicField?.GetValue(wallet); + + var counterField = wallet.GetType() + .GetField("_counter",System.Reflection.BindingFlags.NonPublic | + System.Reflection.BindingFlags.Instance); + var counterRef = (InMemoryCounter?)counterField?.GetValue(wallet); + + Assert.Equal(mnemonic, mnemonicRef.ToString()); + Assert.Same(counter, counterRef); + Assert.NotNull(wallet.GetInfo()); + } + [Fact] + public void WithMintStringVariantCreatesHttpClient() + { + var wallet = Wallet.Create().WithMint(MintUrl); + var api = wallet.GetMintApi().Result; + Assert.NotNull(api); + } + [Fact] public async Task InMemoryCounter() { @@ -31,9 +79,6 @@ public async Task InMemoryCounter() Assert.Equal(1337, ctrNum); } - /* - * Fee selector - */ [Fact] public void SplitAmountsForPayment_ExactAmount_ReturnsCorrectSplit() { @@ -44,6 +89,515 @@ public void SplitAmountsForPayment_ExactAmount_ReturnsCorrectSplit() private Keyset? _testKeyset = JsonSerializer.Deserialize( "{\n \"1\": \"03ba786a2c0745f8c30e490288acd7a72dd53d65afd292ddefa326a4a3fa14c566\",\n \"2\": \"03361cd8bd1329fea797a6add1cf1990ffcf2270ceb9fc81eeee0e8e9c1bd0cdf5\",\n \"4\": \"036e378bcf78738ddf68859293c69778035740e41138ab183c94f8fee7572214c7\",\n \"8\": \"03909d73beaf28edfb283dbeb8da321afd40651e8902fcf5454ecc7d69788626c0\",\n \"16\": \"028a36f0e6638ea7466665fe174d958212723019ec08f9ce6898d897f88e68aa5d\",\n \"32\": \"03a97a40e146adee2687ac60c2ba2586a90f970de92a9d0e6cae5a4b9965f54612\",\n \"64\": \"03ce86f0c197aab181ddba0cfc5c5576e11dfd5164d9f3d4a3fc3ffbbf2e069664\",\n \"128\": \"0284f2c06d938a6f78794814c687560a0aabab19fe5e6f30ede38e113b132a3cb9\",\n \"256\": \"03b99f475b68e5b4c0ba809cdecaae64eade2d9787aa123206f91cd61f76c01459\",\n \"512\": \"03d4db82ea19a44d35274de51f78af0a710925fe7d9e03620b84e3e9976e3ac2eb\",\n \"1024\": \"031fbd4ba801870871d46cf62228a1b748905ebc07d3b210daf48de229e683f2dc\",\n \"2048\": \"0276cedb9a3b160db6a158ad4e468d2437f021293204b3cd4bf6247970d8aff54b\",\n \"4096\": \"02fc6b89b403ee9eb8a7ed457cd3973638080d6e04ca8af7307c965c166b555ea2\",\n \"8192\": \"0320265583e916d3a305f0d2687fcf2cd4e3cd03a16ea8261fda309c3ec5721e21\",\n \"16384\": \"036e41de58fdff3cb1d8d713f48c63bc61fa3b3e1631495a444d178363c0d2ed50\",\n \"32768\": \"0365438f613f19696264300b069d1dad93f0c60a37536b72a8ab7c7366a5ee6c04\",\n \"65536\": \"02408426cfb6fc86341bac79624ba8708a4376b2d92debdf4134813f866eb57a8d\",\n \"131072\": \"031063e9f11c94dc778c473e968966eac0e70b7145213fbaff5f7a007e71c65f41\",\n \"262144\": \"02f2a3e808f9cd168ec71b7f328258d0c1dda250659c1aced14c7f5cf05aab4328\",\n \"524288\": \"038ac10de9f1ff9395903bb73077e94dbf91e9ef98fd77d9a2debc5f74c575bc86\",\n \"1048576\": \"0203eaee4db749b0fc7c49870d082024b2c31d889f9bc3b32473d4f1dfa3625788\",\n \"2097152\": \"033cdb9d36e1e82ae652b7b6a08e0204569ec7ff9ebf85d80a02786dc7fe00b04c\",\n \"4194304\": \"02c8b73f4e3a470ae05e5f2fe39984d41e9f6ae7be9f3b09c9ac31292e403ac512\",\n \"8388608\": \"025bbe0cfce8a1f4fbd7f3a0d4a09cb6badd73ef61829dc827aa8a98c270bc25b0\",\n \"16777216\": \"037eec3d1651a30a90182d9287a5c51386fe35d4a96839cf7969c6e2a03db1fc21\",\n \"33554432\": \"03280576b81a04e6abd7197f305506476f5751356b7643988495ca5c3e14e5c262\",\n \"67108864\": \"03268bfb05be1dbb33ab6e7e00e438373ca2c9b9abc018fdb452d0e1a0935e10d3\",\n \"134217728\": \"02573b68784ceba9617bbcc7c9487836d296aa7c628c3199173a841e7a19798020\",\n \"268435456\": \"0234076b6e70f7fbf755d2227ecc8d8169d662518ee3a1401f729e2a12ccb2b276\",\n \"536870912\": \"03015bd88961e2a466a2163bd4248d1d2b42c7c58a157e594785e7eb34d880efc9\",\n \"1073741824\": \"02c9b076d08f9020ebee49ac8ba2610b404d4e553a4f800150ceb539e9421aaeee\",\n \"2147483648\": \"034d592f4c366afddc919a509600af81b489a03caf4f7517c2b3f4f2b558f9a41a\",\n \"4294967296\": \"037c09ecb66da082981e4cbdb1ac65c0eb631fc75d85bed13efb2c6364148879b5\",\n \"8589934592\": \"02b4ebb0dda3b9ad83b39e2e31024b777cc0ac205a96b9a6cfab3edea2912ed1b3\",\n \"17179869184\": \"026cc4dacdced45e63f6e4f62edbc5779ccd802e7fabb82d5123db879b636176e9\",\n \"34359738368\": \"02b2cee01b7d8e90180254459b8f09bbea9aad34c3a2fd98c85517ecfc9805af75\",\n \"68719476736\": \"037a0c0d564540fc574b8bfa0253cca987b75466e44b295ed59f6f8bd41aace754\",\n \"137438953472\": \"021df6585cae9b9ca431318a713fd73dbb76b3ef5667957e8633bca8aaa7214fb6\",\n \"274877906944\": \"02b8f53dde126f8c85fa5bb6061c0be5aca90984ce9b902966941caf963648d53a\",\n \"549755813888\": \"029cc8af2840d59f1d8761779b2496623c82c64be8e15f9ab577c657c6dd453785\",\n \"1099511627776\": \"03e446fdb84fad492ff3a25fc1046fb9a93a5b262ebcd0151caa442ea28959a38a\",\n \"2199023255552\": \"02d6b25bd4ab599dd0818c55f75702fde603c93f259222001246569018842d3258\",\n \"4398046511104\": \"03397b522bb4e156ec3952d3f048e5a986c20a00718e5e52cd5718466bf494156a\",\n \"8796093022208\": \"02d1fb9e78262b5d7d74028073075b80bb5ab281edcfc3191061962c1346340f1e\",\n \"17592186044416\": \"030d3f2ad7a4ca115712ff7f140434f802b19a4c9b2dd1c76f3e8e80c05c6a9310\",\n \"35184372088832\": \"03e325b691f292e1dfb151c3fb7cad440b225795583c32e24e10635a80e4221c06\",\n \"70368744177664\": \"03bee8f64d88de3dee21d61f89efa32933da51152ddbd67466bef815e9f93f8fd1\",\n \"140737488355328\": \"0327244c9019a4892e1f04ba3bf95fe43b327479e2d57c25979446cc508cd379ed\",\n \"281474976710656\": \"02fb58522cd662f2f8b042f8161caae6e45de98283f74d4e99f19b0ea85e08a56d\",\n \"562949953421312\": \"02adde4b466a9d7e59386b6a701a39717c53f30c4810613c1b55e6b6da43b7bc9a\",\n \"1125899906842624\": \"038eeda11f78ce05c774f30e393cda075192b890d68590813ff46362548528dca9\",\n \"2251799813685248\": \"02ec13e0058b196db80f7079d329333b330dc30c000dbdd7397cbbc5a37a664c4f\",\n \"4503599627370496\": \"02d2d162db63675bd04f7d56df04508840f41e2ad87312a3c93041b494efe80a73\",\n \"9007199254740992\": \"0356969d6aef2bb40121dbd07c68b6102339f4ea8e674a9008bb69506795998f49\",\n \"18014398509481984\": \"02f4e667567ebb9f4e6e180a4113bb071c48855f657766bb5e9c776a880335d1d6\",\n \"36028797018963968\": \"0385b4fe35e41703d7a657d957c67bb536629de57b7e6ee6fe2130728ef0fc90b0\",\n \"72057594037927936\": \"02b2bc1968a6fddbcc78fb9903940524824b5f5bed329c6ad48a19b56068c144fd\",\n \"144115188075855872\": \"02e0dbb24f1d288a693e8a49bc14264d1276be16972131520cf9e055ae92fba19a\",\n \"288230376151711744\": \"03efe75c106f931a525dc2d653ebedddc413a2c7d8cb9da410893ae7d2fa7d19cc\",\n \"576460752303423488\": \"02c7ec2bd9508a7fc03f73c7565dc600b30fd86f3d305f8f139c45c404a52d958a\",\n \"1152921504606846976\": \"035a6679c6b25e68ff4e29d1c7ef87f21e0a8fc574f6a08c1aa45ff352c1d59f06\",\n \"2305843009213693952\": \"033cdc225962c052d485f7cfbf55a5b2367d200fe1fe4373a347deb4cc99e9a099\",\n \"4611686018427387904\": \"024a4b806cf413d14b294719090a9da36ba75209c7657135ad09bc65328fba9e6f\",\n \"9223372036854775808\": \"0377a6fe114e291a8d8e991627c38001c8305b23b9e98b1c7b1893f5cd0dda6cad\"\n}"); + + private static KeysetId _testKeysetId = new KeysetId("000f01df73ea149a"); + + [Fact] + public void SumProofs_EmptyList_ReturnsZero() + { + var proofs = new List(); + var sum = Utils.SumProofs(proofs); + Assert.Equal(0UL, sum); + } + + [Fact] + public void SumProofs_SingleProof_ReturnsAmount() + { + var proofs = new List + { + new Proof { Amount = 64 } + }; + var sum = Utils.SumProofs(proofs); + Assert.Equal(64UL, sum); + } + + [Fact] + public void SumProofs_MultipleProofs_ReturnsCorrectSum() + { + var proofs = new List + { + new Proof { Amount = 1 }, + new Proof { Amount = 2 }, + new Proof { Amount = 4 }, + new Proof { Amount = 8 }, + new Proof { Amount = 16 } + }; + var sum = Utils.SumProofs(proofs); + Assert.Equal(31UL, sum); + } + + [Theory] + [InlineData(1UL, new ulong[] { 1 })] + [InlineData(2UL, new ulong[] { 2 })] + [InlineData(3UL, new ulong[] { 2, 1 })] + [InlineData(7UL, new ulong[] { 4, 2, 1 })] + [InlineData(15UL, new ulong[] { 8, 4, 2, 1 })] + [InlineData(63UL, new ulong[] { 32, 16, 8, 4, 2, 1 })] + [InlineData(64UL, new ulong[] { 64 })] + [InlineData(100UL, new ulong[] { 64, 32, 4 })] + [InlineData(1337UL, new ulong[] { 1024, 256, 32, 16, 8, 1 })] + public void SplitToProofsAmounts_VariousAmounts_ReturnsCorrectSplit(ulong amount, ulong[] expected) + { + var result = Utils.SplitToProofsAmounts(amount, _testKeyset!); + Assert.Equal(expected.ToList(), result); + } + + [Fact] + public void SplitToProofsAmounts_ZeroAmount_ReturnsEmptyList() + { + var result = Utils.SplitToProofsAmounts(0, _testKeyset!); + Assert.Empty(result); + } + + [Theory] + [InlineData(0UL, 0)] + [InlineData(1UL, 1)] + [InlineData(2UL, 1)] + [InlineData(3UL, 2)] + [InlineData(4UL, 2)] + [InlineData(7UL, 3)] + [InlineData(8UL, 3)] + [InlineData(15UL, 4)] + [InlineData(16UL, 4)] + [InlineData(100UL, 7)] + [InlineData(1000UL, 10)] + public void CalculateNumberOfBlankOutputs_VariousAmounts(ulong amount, int expected) + { + var result = Utils.CalculateNumberOfBlankOutputs(amount); + Assert.Equal(expected, result); + } + + [Fact] + public void CreateOutputs_ValidAmounts_ReturnsCorrectOutputData() + { + var amounts = new List { 1, 2, 4 }; + var outputs = Utils.CreateOutputs(amounts, _testKeysetId, _testKeyset!); + + Assert.Equal(3, outputs.BlindedMessages.Count); + Assert.Equal(3, outputs.BlindingFactors.Count); + Assert.Equal(3, outputs.Secrets.Count); + + Assert.Equal(1UL, outputs.BlindedMessages[0].Amount); + Assert.Equal(2UL, outputs.BlindedMessages[1].Amount); + Assert.Equal(4UL, outputs.BlindedMessages[2].Amount); + + Assert.All(outputs.BlindedMessages, bm => Assert.Equal(_testKeysetId, bm.Id)); + } + + [Fact] + public void CreateOutputs_InvalidAmount_ThrowsException() + { + var amounts = new List { 1, 3 }; // 3 is not a valid amount + Assert.Throws(() => Utils.CreateOutputs(amounts, _testKeysetId, _testKeyset!)); + } + + [Fact] + public void CreateOutputs_DeterministicWithMnemonic() + { + var mnemonic = new Mnemonic("abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about"); + var amounts = new List { 1, 2, 4 }; + + var outputs1 = Utils.CreateOutputs(amounts, _testKeysetId, _testKeyset!, mnemonic, 0); + var outputs2 = Utils.CreateOutputs(amounts, _testKeysetId, _testKeyset!, mnemonic, 0); + + // Same mnemonic and counter should produce same outputs + for (int i = 0; i < outputs1.Secrets.Count; i++) + { + Assert.Equal( + ((StringSecret)outputs1.Secrets[i]).Secret, + ((StringSecret)outputs2.Secrets[i]).Secret + ); + } + } + + [Fact] + public void CreateOutputs_RandomWithoutMnemonic() + { + var amounts = new List { 1 }; + + var outputs1 = Utils.CreateOutputs(amounts, _testKeysetId, _testKeyset!); + var outputs2 = Utils.CreateOutputs(amounts, _testKeysetId, _testKeyset!); + + // without mnemonic, outputs should be random (different) + Assert.NotEqual( + ((StringSecret)outputs1.Secrets[0]).Secret, + ((StringSecret)outputs2.Secrets[0]).Secret + ); + } + + private static PubKey CreateTestPubKey(int seed) + { + var seedBytes = new byte[32]; + BitConverter.GetBytes(seed).CopyTo(seedBytes, 0); + seedBytes[31] = 1; + var privKey = ECPrivKey.Create(seedBytes); + ECPubKey ecPubKey = privKey.CreatePubKey(); + return ecPubKey; + } + + [Fact] + public async Task ProofSelector_ExactMatch_SelectsCorrectProofs() + { + var keysetId = _testKeysetId; + var fees = new Dictionary { { keysetId, 0 } }; + var selector = new ProofSelector(fees); + + var proofs = new List + { + new Proof { Amount = 1, Id = keysetId, C = CreateTestPubKey(1) }, + new Proof { Amount = 2, Id = keysetId, C = CreateTestPubKey(2) }, + new Proof { Amount = 4, Id = keysetId, C = CreateTestPubKey(3) }, + new Proof { Amount = 8, Id = keysetId, C = CreateTestPubKey(4) }, + }; + + var result = await selector.SelectProofsToSend(proofs, 7, false); + + Assert.Equal(7UL, Utils.SumProofs(result.Send)); + Assert.Equal(8UL, Utils.SumProofs(result.Keep)); + } + + [Fact] + public async Task ProofSelector_InsufficientFunds_ReturnsEmptySend() + { + var keysetId = _testKeysetId; + var fees = new Dictionary { { keysetId, 0 } }; + var selector = new ProofSelector(fees); + + var proofs = new List + { + new Proof { Amount = 1, Id = keysetId, C = CreateTestPubKey(1) }, + new Proof { Amount = 2, Id = keysetId, C = CreateTestPubKey(2) }, + }; + + var result = await selector.SelectProofsToSend(proofs, 100, false); + + Assert.Empty(result.Send); + Assert.Equal(2, result.Keep.Count); + } + + [Fact] + public async Task ProofSelector_ZeroAmount_ReturnsEmptySend() + { + var keysetId = _testKeysetId; + var fees = new Dictionary { { keysetId, 0 } }; + var selector = new ProofSelector(fees); + + var proofs = new List + { + new Proof { Amount = 8, Id = keysetId, C = CreateTestPubKey(1) }, + }; + + var result = await selector.SelectProofsToSend(proofs, 0, false); + + Assert.Empty(result.Send); + } + + [Fact] + public async Task ProofSelector_WithFees_AccountsForFees() + { + var keysetId = _testKeysetId; + var fees = new Dictionary { { keysetId, 1000 } }; // 1 sat per proof + var selector = new ProofSelector(fees); + + var proofs = new List + { + new Proof { Amount = 1, Id = keysetId, C = CreateTestPubKey(1) }, + new Proof { Amount = 2, Id = keysetId, C = CreateTestPubKey(2) }, + new Proof { Amount = 4, Id = keysetId, C = CreateTestPubKey(3) }, + new Proof { Amount = 8, Id = keysetId, C = CreateTestPubKey(4) }, + new Proof { Amount = 16, Id = keysetId, C = CreateTestPubKey(5) }, + }; + + var result = await selector.SelectProofsToSend(proofs, 10, true); + + Assert.True(Utils.SumProofs(result.Send) >= 10); + } + + [Fact] + public async Task ProofSelector_SingleLargeProof_SelectsIt() + { + var keysetId = _testKeysetId; + var fees = new Dictionary { { keysetId, 0 } }; + var selector = new ProofSelector(fees); + + var proofs = new List + { + new Proof { Amount = 100, Id = keysetId, C = CreateTestPubKey(1) }, + }; + + var result = await selector.SelectProofsToSend(proofs, 50, false); + + Assert.Single(result.Send); + Assert.Equal(100UL, result.Send[0].Amount); + Assert.Empty(result.Keep); + } + + [Fact] + public void TokenEncode_V4_RoundTrip() + { + var keysetId = new KeysetId("00ffd48b8f5ecf80"); + var proofs = new List + { + new Proof + { + Amount = 1, + Id = keysetId, + Secret = new StringSecret("acc12435e7b8484c3cf1850149218af90f716a52bf4a5ed347e48ecc13f77388"), + C = "0244538319de485d55bed3b29a642bee5879375ab9e7a620e11e48ba482421f3cf".ToPubKey() + } + }; + + var token = new CashuToken + { + Unit = "sat", + Tokens = new List + { + new CashuToken.Token + { + Mint = "http://localhost:3338", + Proofs = proofs + } + } + }; + + var encoded = token.Encode("B", false); + Assert.StartsWith("cashuB", encoded); + + var decoded = CashuTokenHelper.Decode(encoded, out var version); + Assert.Equal("B", version); + Assert.Equal("sat", decoded.Unit); + Assert.Single(decoded.Tokens); + Assert.Equal("http://localhost:3338", decoded.Tokens[0].Mint); + Assert.Single(decoded.Tokens[0].Proofs); + Assert.Equal(1UL, decoded.Tokens[0].Proofs[0].Amount); + } + + [Fact] + public void TokenDecode_InvalidPrefix_ThrowsException() + { + var invalidToken = "invalidTokenString123"; + Assert.Throws(() => CashuTokenHelper.Decode(invalidToken, out _)); + } + + [Fact] + public void KeysetId_Equality() + { + var id1 = new KeysetId("009a1f293253e41e"); + var id2 = new KeysetId("009a1f293253e41e"); + var id3 = new KeysetId("000f01df73ea149a"); + + Assert.Equal(id1, id2); + Assert.NotEqual(id1, id3); + Assert.True(id1 == id2); + Assert.False(id1 == id3); + } + + [Fact] + public void KeysetId_GetVersion() + { + var v0Id = new KeysetId("009a1f293253e41e"); + var v1Id = new KeysetId("01adc013fa9d85171586660abab27579888611659d357bc86bc09cb26eee8bc035"); + + Assert.Equal(0x00, v0Id.GetVersion()); + Assert.Equal(0x01, v1Id.GetVersion()); + } + + [Fact] + public void ComputeFee_NoFees_ReturnsZero() + { + var keysetId = _testKeysetId; + var fees = new Dictionary { { keysetId, 0 } }; + + var proofs = new List + { + new Proof { Amount = 1, Id = keysetId }, + new Proof { Amount = 2, Id = keysetId }, + }; + + var fee = proofs.ComputeFee(fees); + Assert.Equal(0UL, fee); + } + + [Fact] + public void ComputeFee_WithFees_ReturnsCorrectFee() + { + var keysetId = _testKeysetId; + var fees = new Dictionary { { keysetId, 1000 } }; // 1 sat per proof (1000 ppk) + + var proofs = new List + { + new Proof { Amount = 1, Id = keysetId }, + new Proof { Amount = 2, Id = keysetId }, + new Proof { Amount = 4, Id = keysetId }, + }; + + var fee = proofs.ComputeFee(fees); + Assert.Equal(3UL, fee); // 3 proofs * 1 sat + } + + [Fact] + public void SendResponse_DefaultsToEmptyLists() + { + var response = new SendResponse(); + Assert.NotNull(response.Keep); + Assert.NotNull(response.Send); + Assert.Empty(response.Keep); + Assert.Empty(response.Send); + } + + [Fact] + public void Wallet_WithKeysetSyncThreshold_SetsCorrectly() + { + var wallet = Wallet.Create() + .WithMint(MintUrl) + .WithKeysetSync(true, TimeSpan.FromMinutes(30)); + + Assert.NotNull(wallet); + } + + [Fact] + public void Wallet_ShouldBumpCounter_Default() + { + var counter = new InMemoryCounter(); + var wallet = Wallet.Create() + .WithMint(MintUrl) + .WithCounter(counter); + + Assert.NotNull(wallet); + } + + [Fact] + public void Wallet_ShouldBumpCounter_Disabled() + { + var counter = new InMemoryCounter(); + var wallet = Wallet.Create() + .WithMint(MintUrl) + .WithCounter(counter) + .ShouldBumpCounter(false); + + Assert.NotNull(wallet); + } + + [Fact] + public void MintInfo_FromGetInfoResponse() + { + var response = new GetInfoResponse + { + Version = "0.15.0", + Name = "Test Mint", + Description = "A test mint" + }; + + var info = new MintInfo(response); + Assert.NotNull(info); + } + + [Fact] + public void OutputData_EmptyConstructor() + { + var data = new OutputData(); + Assert.NotNull(data.BlindedMessages); + Assert.NotNull(data.BlindingFactors); + Assert.NotNull(data.Secrets); + } + + [Fact] + public void P2PkBuilder_Build_CreatesValidSecret() + { + var privKey = new PrivKey("0000000000000000000000000000000000000000000000000000000000000001"); + var builder = new P2PkBuilder + { + Pubkeys = [privKey.Key.CreatePubKey()], + SignatureThreshold = 1, + SigFlag = "SIG_INPUTS" + }; + + var secret = builder.Build(); + Assert.NotNull(secret); + + var allowedPubkeys = secret.GetAllowedPubkeys(out var threshold); + Assert.Single(allowedPubkeys); + Assert.Equal(1, threshold); + } + + [Fact] + public void P2PkBuilder_WithMultisig_Build() + { + var privKey1 = new PrivKey("0000000000000000000000000000000000000000000000000000000000000001"); + var privKey2 = new PrivKey("0000000000000000000000000000000000000000000000000000000000000002"); + + var builder = new P2PkBuilder + { + Pubkeys = [privKey1.Key.CreatePubKey(), privKey2.Key.CreatePubKey()], + SignatureThreshold = 2, + SigFlag = "SIG_INPUTS" + }; + + var secret = builder.Build(); + var allowedPubkeys = secret.GetAllowedPubkeys(out var threshold); + + Assert.Equal(2, allowedPubkeys.Count()); + Assert.Equal(2, threshold); + } + + [Fact] + public void HTLCBuilder_Build_CreatesValidSecret() + { + var preimage = "0000000000000000000000000000000000000000000000000000000000000001"; + using var sha = System.Security.Cryptography.SHA256.Create(); + var hashLockBytes = sha.ComputeHash(Convert.FromHexString(preimage)); + var hashLock = Convert.ToHexString(hashLockBytes).ToLower(); + var privKey = new PrivKey("0000000000000000000000000000000000000000000000000000000000000001"); + + var builder = new HTLCBuilder + { + HashLock = hashLock, + Pubkeys = [privKey.Key.CreatePubKey()], + SignatureThreshold = 1 + }; + + var secret = builder.Build(); + Assert.NotNull(secret); + + var allowedPubkeys = secret.GetAllowedPubkeys(out var threshold); + Assert.Single(allowedPubkeys); + } + + [Fact] + public async Task Wallet_ThrowsOnMissingMint_ForAllOperations() + { + var wallet = Wallet.Create(); + + await Assert.ThrowsAsync(() => wallet.GetInfo()); + Assert.Throws(() => wallet.CreateMintQuote()); + Assert.Throws(() => wallet.CreateMeltQuote()); + Assert.Throws(() => wallet.Swap()); + Assert.Throws(() => wallet.Restore()); + } + + [Fact] + public async Task Counter_ThrowsOnMissingKeysetId() + { + var counter = new InMemoryCounter(); + var unknownKeysetId = new KeysetId("00unknown1234567"); + + var value = await counter.GetCounterForId(unknownKeysetId); + Assert.Equal(0, value); + } + + [Fact] + public async Task Counter_MultipleKeysets_IndependentCounters() + { + var counter = new InMemoryCounter(); + var keysetId1 = new KeysetId("00keyset11234567"); + var keysetId2 = new KeysetId("00keyset21234567"); + + await counter.IncrementCounter(keysetId1, 10); + await counter.IncrementCounter(keysetId2, 20); + + Assert.Equal(10, await counter.GetCounterForId(keysetId1)); + Assert.Equal(20, await counter.GetCounterForId(keysetId2)); + } } From 0aa2362116dafd438b5bcaa375a1658fc34b6b22 Mon Sep 17 00:00:00 2001 From: d4rp4t Date: Thu, 27 Nov 2025 01:30:32 +0100 Subject: [PATCH 21/70] nut10 refractoring --- DotNut.Tests/Integration.cs | 95 ++++++++++--------- DotNut.Tests/UnitTest1.cs | 4 +- .../Handlers/MeltHandlerBolt11.cs | 53 +---------- .../Handlers/MeltHandlerBolt12.cs | 48 +--------- DotNut/Abstractions/Nut10Helper.cs | 77 +++++++++++++++ DotNut/Abstractions/SwapBuilder.cs | 71 +++++--------- DotNut/Abstractions/Utils.cs | 46 +++++++++ DotNut/NUT11/P2PKProofSecret.cs | 15 +-- DotNut/NUT11/SigAllHandler.cs | 19 +++- DotNut/NUT14/HTLCProofSecret.cs | 14 ++- 10 files changed, 234 insertions(+), 208 deletions(-) create mode 100644 DotNut/Abstractions/Nut10Helper.cs diff --git a/DotNut.Tests/Integration.cs b/DotNut.Tests/Integration.cs index e36ae23..249b0ef 100644 --- a/DotNut.Tests/Integration.cs +++ b/DotNut.Tests/Integration.cs @@ -344,53 +344,54 @@ await Assert.ThrowsAsync(async () => Assert.NotEmpty(change); } - - [Fact] - public async Task MintSwapHTLC() - { - var wallet = Wallet - .Create() - .WithMint(MintUrl); - - var privKeyBob = new PrivKey(RandomNumberGenerator.GetHexString(64, true)); - var preimage = "0000000000000000000000000000000000000000000000000000000000000001"; - var hashLock = Convert.ToHexString(SHA256.HashData(Convert.FromHexString(preimage))); - - var mintHandler = await wallet.CreateMintQuote() - .WithAmount(1337) - .WithHTLCLock(new HTLCBuilder() - { - HashLock = hashLock, - Pubkeys = [privKeyBob.Key.CreatePubKey()], - SignatureThreshold = 1 - }) - .ProcessAsyncBolt11(); - - await PayInvoice(); - var htlcProofs = await mintHandler.Mint(); - - Assert.NotEmpty(htlcProofs); - Assert.Equal(1337UL, Utils.SumProofs(htlcProofs)); - - // try swap without preimage - should fail - await Assert.ThrowsAsync(async () => - { - await wallet.Swap() - .FromInputs(htlcProofs) - .WithPrivkeys([privKeyBob]) - .ProcessAsync(); - }); - - // swap with correct preimage and signature - var swappedProofs = await wallet.Swap() - .FromInputs(htlcProofs) - .WithPrivkeys([privKeyBob]) - .WithHtlcPreimage(preimage) - .ProcessAsync(); - - Assert.NotEmpty(swappedProofs); - Assert.Equal(1337UL, Utils.SumProofs(swappedProofs)); - } + //TODO: CDK MINTD HAS AN ISSUE WITH HTLC SECRETS GENERATED IN DOTNUT - UNCOMMENT IN FUTURE + // [Fact] + // public async Task MintSwapHTLC() + // { + // var wallet = Wallet + // .Create() + // .WithMint(MintUrl); + // + // var privKeyBob = new PrivKey(RandomNumberGenerator.GetHexString(64, true)); + // var preimage = "0000000000000000000000000000000000000000000000000000000000000001"; + // var hashLock = Convert.ToHexString(SHA256.HashData(Convert.FromHexString(preimage))); + // + // var mintHandler = await wallet.CreateMintQuote() + // .WithAmount(1337) + // .WithHTLCLock(new HTLCBuilder() + // { + // HashLock = hashLock, + // Pubkeys = [privKeyBob.Key.CreatePubKey()], + // SignatureThreshold = 1 + // }) + // .ProcessAsyncBolt11(); + // + // await PayInvoice(); + // var htlcProofs = await mintHandler.Mint(); + // + // Assert.NotEmpty(htlcProofs); + // Assert.Equal(1337UL, Utils.SumProofs(htlcProofs)); + // + // + // // try swap without preimage - should fail + // await Assert.ThrowsAsync(async () => + // { + // await wallet.Swap() + // .FromInputs(htlcProofs) + // .WithPrivkeys([privKeyBob]) + // .ProcessAsync(); + // }); + // + // // swap with correct preimage and signature + // var swappedProofs = await wallet.Swap() + // .FromInputs(htlcProofs) + // .WithPrivkeys([privKeyBob]) + // .WithHtlcPreimage(preimage) + // .ProcessAsync(); + // + // Assert.NotEmpty(swappedProofs); + // Assert.Equal(1337UL, Utils.SumProofs(swappedProofs)); + // } [Fact] public async Task SwapWithCustomAmounts() diff --git a/DotNut.Tests/UnitTest1.cs b/DotNut.Tests/UnitTest1.cs index f79ce0f..d6a32de 100644 --- a/DotNut.Tests/UnitTest1.cs +++ b/DotNut.Tests/UnitTest1.cs @@ -937,7 +937,7 @@ public void Nut28_P2BK_Flow() C = "02698c4e2b5f9534cd0687d87513c759790cf829aa5739184a3e3735471fbda904".ToPubKey(), P2PkE = E }; - var witness = p2pkProofSecret.GenerateBlindWitness(proof, new[] {signing_key, signing_key_two}, keysetId, E); + var witness = p2pkProofSecret.GenerateBlindWitness(proof, new[] {signing_key, signing_key_two}, E); Assert.True(p2pkProofSecret.VerifyWitness(secret, witness)); } @@ -975,7 +975,7 @@ public void Nut28_Flow_WithRandomE() C = "02698c4e2b5f9534cd0687d87513c759790cf829aa5739184a3e3735471fbda904".ToPubKey(), P2PkE = E }; - var witness = p2pkProofSecret.GenerateBlindWitness(proof, new[] {signing_key, signing_key_two}, keysetId, E); + var witness = p2pkProofSecret.GenerateBlindWitness(proof, new[] {signing_key, signing_key_two}, E); Assert.True(p2pkProofSecret.VerifyWitness(secret, witness)); } diff --git a/DotNut/Abstractions/Handlers/MeltHandlerBolt11.cs b/DotNut/Abstractions/Handlers/MeltHandlerBolt11.cs index 0caa952..20faef4 100644 --- a/DotNut/Abstractions/Handlers/MeltHandlerBolt11.cs +++ b/DotNut/Abstractions/Handlers/MeltHandlerBolt11.cs @@ -17,12 +17,12 @@ public class MeltHandlerBolt11 : IMeltHandler? _privKeys = null, + List? privKeys = null, string? htlcPreimage = null) { _wallet = wallet; _quote = quote; - _privKeys = _privKeys; + _privKeys = privKeys; _htlcPreimage = htlcPreimage; } public MeltHandlerBolt11( @@ -42,8 +42,8 @@ public MeltHandlerBolt11( public async Task GetQuote(CancellationToken ct = default) => this._quote; public async Task> Melt(List inputs, CancellationToken ct = default) { - MaybeProcessP2PkHTLC(inputs); - var client = await _wallet.GetMintApi(); + Nut10Helper.MaybeProcessNut10(_privKeys??[], inputs, _blankOutputs, _htlcPreimage, _quote.Quote); + var client = await _wallet.GetMintApi(ct); var req = new PostMeltRequest { Quote = _quote.Quote, @@ -60,49 +60,4 @@ public async Task> Melt(List inputs, CancellationToken ct = d var keyset = await _wallet.GetKeys(res.Change.First().Id, false, ct); return Utils.ConstructProofsFromPromises(res.Change.ToList(), _blankOutputs, keyset.Keys); } - - private void MaybeProcessP2PkHTLC(List proofs) - { - if (_privKeys == null || _privKeys.Count == 0) - { - return; - } - - if (proofs == null) - { - throw new ArgumentNullException(nameof(proofs), "No proofs to melt!"); - } - - var sigAllHandler = new SigAllHandler - { - Proofs = proofs, - BlindedMessages = this._blankOutputs?.BlindedMessages ?? [], - MeltQuoteId = _quote.Quote, - HTLCPreimage = this._htlcPreimage, - }; - - if (sigAllHandler.TrySign(out P2PKWitness? witness)) - { - if (witness == null) - { - throw new ArgumentNullException(nameof(witness), "sig_all input was correct, but couldn't create a witness signature!"); - } - proofs[0].Witness = JsonSerializer.Serialize(witness); - return; - } - - foreach (var proof in proofs) - { - - if (proof.Secret is not Nut10Secret { ProofSecret: P2PKProofSecret p2pk, Key: { } key }) continue; - if (proof.Secret is Nut10Secret { ProofSecret: HTLCProofSecret htlc } && _htlcPreimage is {} preimage) - { - var w = htlc.GenerateWitness(proof, _privKeys.Select(p=>p.Key).ToArray(), preimage); - proof.Witness = JsonSerializer.Serialize(w); - continue; - } - var proofWitness = p2pk.GenerateWitness(proof, _privKeys.Select(p => p.Key).ToArray()); - proof.Witness = JsonSerializer.Serialize(proofWitness); - } - } } \ No newline at end of file diff --git a/DotNut/Abstractions/Handlers/MeltHandlerBolt12.cs b/DotNut/Abstractions/Handlers/MeltHandlerBolt12.cs index 71d6d3d..e76645f 100644 --- a/DotNut/Abstractions/Handlers/MeltHandlerBolt12.cs +++ b/DotNut/Abstractions/Handlers/MeltHandlerBolt12.cs @@ -27,7 +27,7 @@ public MeltHandlerBolt12( public async Task GetQuote(CancellationToken ct = default) => this._quote; public async Task> Melt(List inputs, CancellationToken ct = default) { - MaybeProcessP2PkHTLC(inputs); + Nut10Helper.MaybeProcessNut10(_privKeys??[], inputs, _blankOutputs, _htlcPreimage, _quote.Quote); var client = await _wallet.GetMintApi(); var req = new PostMeltRequest { @@ -45,50 +45,4 @@ public async Task> Melt(List inputs, CancellationToken ct = d var keyset = await _wallet.GetKeys(res.Change.First().Id, false, ct); return Utils.ConstructProofsFromPromises(res.Change.ToList(), _blankOutputs, keyset.Keys); } - - private void MaybeProcessP2PkHTLC(List proofs) - { - if (_privKeys == null || _privKeys.Count == 0) - { - return; - } - - if (proofs == null) - { - throw new ArgumentNullException(nameof(proofs), "No proofs to melt!"); - } - - var sigAllHandler = new SigAllHandler - { - Proofs = proofs, - BlindedMessages = this._blankOutputs?.BlindedMessages ?? [], - MeltQuoteId = _quote.Quote, - HTLCPreimage = this._htlcPreimage, - }; - - if (sigAllHandler.TrySign(out P2PKWitness? witness)) - { - if (witness == null) - { - throw new ArgumentNullException(nameof(witness), "sig_all input was correct, but couldn't create a witness signature!"); - } - proofs[0].Witness = JsonSerializer.Serialize(witness); - return; - } - - foreach (var proof in proofs) - { - if (proof.Secret is not Nut10Secret { ProofSecret: P2PKProofSecret p2pk, Key: { } key }) continue; - if (proof.Secret is Nut10Secret { ProofSecret: HTLCProofSecret htlc } && _htlcPreimage is {} preimage) - { - var w = htlc.GenerateWitness(proof, _privKeys.Select(p=>p.Key).ToArray(), preimage); - proof.Witness = JsonSerializer.Serialize(w); - continue; - } - var proofWitness = p2pk.GenerateWitness(proof, _privKeys.Select(p => p.Key).ToArray()); - proof.Witness = JsonSerializer.Serialize(proofWitness); - } - } - - } \ No newline at end of file diff --git a/DotNut/Abstractions/Nut10Helper.cs b/DotNut/Abstractions/Nut10Helper.cs new file mode 100644 index 0000000..1923020 --- /dev/null +++ b/DotNut/Abstractions/Nut10Helper.cs @@ -0,0 +1,77 @@ +using System.Text.Json; +using NBitcoin.Secp256k1; + +namespace DotNut.Abstractions; + +public class Nut10Helper +{ + public static void MaybeProcessNut10( + List privKeys, + List proofs, + OutputData? outputs = null, + string? htlcPreimage = null, + string? meltQuoteId = null + ) + { + if (privKeys.Count == 0 || proofs.Count == 0) + { + return; + } + + var sigAllHandler = new SigAllHandler + { + Proofs = proofs, + PrivKeys = privKeys, + BlindedMessages = outputs?.BlindedMessages ?? [], + HTLCPreimage = htlcPreimage, + MeltQuoteId = meltQuoteId + }; + + if (sigAllHandler.TrySign(out P2PKWitness? witness)) + { + if (witness == null) + { + throw new ArgumentNullException(nameof(witness), + "sig_all input was correct, but couldn't create a witness signature!"); + } + + proofs[0].Witness = JsonSerializer.Serialize(witness); + return; + } + + var keys = privKeys.Select(p => p.Key).ToArray(); + + foreach (var proof in proofs) + { + handleWitnessCreation(proof, keys, htlcPreimage); + } + } + + private static void handleWitnessCreation(Proof proof, ECPrivKey[] keys, string? htlcPreimage) + { + if (proof.Secret is Nut10Secret { ProofSecret: HTLCProofSecret htlc } && htlcPreimage is { } preimage) + { + if (proof.P2PkE is { } E) + { + var blindwitness = htlc.GenerateBlindWitness(proof, keys, preimage); + proof.Witness = JsonSerializer.Serialize(blindwitness); + return; + } + var witness = htlc.GenerateWitness(proof, keys, preimage); + proof.Witness = JsonSerializer.Serialize(witness); + return; + } + + if (proof.Secret is Nut10Secret { ProofSecret: P2PKProofSecret p2pk }) + { + if (proof.P2PkE is { } E) + { + var blindWitness = p2pk.GenerateBlindWitness(proof, keys); + proof.Witness = JsonSerializer.Serialize(blindWitness); + return; + } + var proofWitness = p2pk.GenerateWitness(proof, keys); + proof.Witness = JsonSerializer.Serialize(proofWitness); + } + } +} \ No newline at end of file diff --git a/DotNut/Abstractions/SwapBuilder.cs b/DotNut/Abstractions/SwapBuilder.cs index 2811027..9a28751 100644 --- a/DotNut/Abstractions/SwapBuilder.cs +++ b/DotNut/Abstractions/SwapBuilder.cs @@ -25,11 +25,11 @@ class SwapBuilder : ISwapBuilder private bool _includeFees = true; - //p2pk stuff + //nut10 stuff private List? _privKeys; private P2PkBuilder? _builder; - private string? _htlcPreimage; + private bool _shouldBlind = false; public SwapBuilder(Wallet wallet, string tokenString) { @@ -113,6 +113,13 @@ public ISwapBuilder ToHTLC(HTLCBuilder htlcBuilder) return this; } + // P2Bk should be compatible with both p2pk and HTLC. Not implemented in the second one + public ISwapBuilder ToP2Bk(bool withBlinding = true) + { + this._shouldBlind = true; + return this; + } + public async Task> ProcessAsync(CancellationToken ct = default) { var mintApi = await _wallet.GetMintApi(ct); @@ -169,8 +176,7 @@ public async Task> ProcessAsync(CancellationToken ct = default) Outputs = outputs.BlindedMessages.ToArray(), }; - await _maybeProcessP2Pk(); - + Nut10Helper.MaybeProcessNut10(_privKeys??[], swapInputs, outputs, _htlcPreimage); var swapResponse = await mintApi.Swap(request, ct); var swappedProofs = @@ -232,6 +238,20 @@ async Task _getOutputs(Keyset keys, CancellationToken ct = default) var outputs = new OutputData(); if (this._builder is not null) { + if (this._shouldBlind) + { + if (this._builder.SigFlag == "SIG_ALL") + { + // create first output, then rest of them should have identical E. + + } + foreach (var p2pkOutput in _amounts.Select(amount => Utils.CreateNut10Output(amount, this._keysetId!, _builder))) + { + outputs.BlindingFactors.Add(p2pkOutput.BlindingFactors[0]); + outputs.BlindedMessages.Add(p2pkOutput.BlindedMessages[0]); + outputs.Secrets.Add(p2pkOutput.Secrets[0]); + } + } // skipped checks for keysetid and keys, since its validated before. make sure to remember about it. foreach (var p2pkOutput in _amounts.Select(amount => Utils.CreateNut10Output(amount, this._keysetId!, _builder))) { @@ -244,49 +264,6 @@ async Task _getOutputs(Keyset keys, CancellationToken ct = default) return await _wallet.CreateOutputs(_amounts, this._keysetId!, ct); } - - private async Task _maybeProcessP2Pk() - { - if (_privKeys == null || _privKeys.Count == 0) - { - return; - } - - if (_proofsToSwap == null) - { - throw new ArgumentNullException(nameof(_proofsToSwap), "No proofs to swap!"); - } - - var sigAllHandler = new SigAllHandler - { - Proofs = this._proofsToSwap, - BlindedMessages = this._outputs?.BlindedMessages ?? [], - HTLCPreimage = this._htlcPreimage, - }; - - if (sigAllHandler.TrySign(out P2PKWitness? witness)) - { - if (witness == null) - { - throw new ArgumentNullException(nameof(witness), "sig_all input was correct, but couldn't create a witness signature!"); - } - this._proofsToSwap[0].Witness = JsonSerializer.Serialize(witness); - } - - foreach (var proof in _proofsToSwap) - { - - if (proof.Secret is not Nut10Secret { ProofSecret: P2PKProofSecret p2pk, Key: { } key }) continue; - if (proof.Secret is Nut10Secret { ProofSecret: HTLCProofSecret htlc } && _htlcPreimage is {} preimage) - { - var w = htlc.GenerateWitness(proof, _privKeys.Select(p=>p.Key).ToArray(), preimage); - proof.Witness = JsonSerializer.Serialize(w); - continue; - } - var proofWitness = p2pk.GenerateWitness(proof, _privKeys.Select(p => p.Key).ToArray()); - proof.Witness = JsonSerializer.Serialize(proofWitness); - } - } private List _getAmounts(ulong total, ulong fee, Keyset keys) { diff --git a/DotNut/Abstractions/Utils.cs b/DotNut/Abstractions/Utils.cs index 34edaa5..c8a3f70 100644 --- a/DotNut/Abstractions/Utils.cs +++ b/DotNut/Abstractions/Utils.cs @@ -160,6 +160,52 @@ P2PkBuilder builder Secrets = [secret] }; } + public static OutputData CreateNut10BlindedOutput(ulong amount, KeysetId keysetId, P2PkBuilder builder, out PubKey E) + { + // ugliest hack ever + Nut10Secret secret; + if (builder is HTLCBuilder htlc) + { + secret = new Nut10Secret("HTLC", htlc.BuildBlinded(keysetId, out var e)); + E = e; + } + else + { + secret = new Nut10Secret("P2PK", builder.BuildBlinded(keysetId, out var e)); + E = e; + } + + var r = RandomPrivkey(); + var B_ = Cashu.ComputeB_(secret.ToCurve(), r); + return new OutputData + { + BlindedMessages = [new BlindedMessage() { Amount = amount, B_ = B_, Id = keysetId }], + BlindingFactors = [r], + Secrets = [secret] + }; + } + public static OutputData CreateNut10BlindedOutput(ulong amount, KeysetId keysetId, P2PkBuilder builder, PrivKey e) + { + // ugliest hack ever + Nut10Secret secret; + if (builder is HTLCBuilder htlc) + { + secret = new Nut10Secret("HTLC", htlc.BuildBlinded(keysetId, e)); + } + else + { + secret = new Nut10Secret("P2PK", builder.BuildBlinded(keysetId, e)); + } + + var r = RandomPrivkey(); + var B_ = Cashu.ComputeB_(secret.ToCurve(), r); + return new OutputData + { + BlindedMessages = [new BlindedMessage() { Amount = amount, B_ = B_, Id = keysetId }], + BlindingFactors = [r], + Secrets = [secret] + }; + } /// /// Method creating proofs, from provided promises (blinded signatures) diff --git a/DotNut/NUT11/P2PKProofSecret.cs b/DotNut/NUT11/P2PKProofSecret.cs index aa9af33..9ded423 100644 --- a/DotNut/NUT11/P2PKProofSecret.cs +++ b/DotNut/NUT11/P2PKProofSecret.cs @@ -126,26 +126,27 @@ public virtual bool VerifyWitness(Proof proof) var witness = JsonSerializer.Deserialize(proof.Witness) ?? new P2PKWitness(); return VerifyWitness(proof.Secret, witness); } + /* * ========================= * NUT-XX Pay to blinded key * ========================= */ - public virtual P2PKWitness? GenerateBlindWitness(Proof proof, ECPrivKey[] keys, KeysetId keysetId) + public virtual P2PKWitness GenerateBlindWitness(Proof proof, ECPrivKey[] keys) { ArgumentNullException.ThrowIfNull(proof.P2PkE); - return GenerateBlindWitness(proof.Secret.GetBytes(), keys, keysetId, proof.P2PkE); + return GenerateBlindWitness(proof.Secret.GetBytes(), keys, proof.Id, proof.P2PkE); } - public virtual P2PKWitness? GenerateBlindWitness(Proof proof, ECPrivKey[] keys, KeysetId keysetId, ECPubKey P2PkE) + public virtual P2PKWitness GenerateBlindWitness(Proof proof, ECPrivKey[] keys, ECPubKey P2PkE) { - return GenerateBlindWitness(proof.Secret.GetBytes(), keys, keysetId, P2PkE); + return GenerateBlindWitness(proof.Secret.GetBytes(), keys, proof.Id, P2PkE); } - - public virtual P2PKWitness? GenerateBlindWitness(BlindedMessage message, ECPrivKey[] keys, KeysetId keysetId, ECPubKey P2PkE) + + public virtual P2PKWitness GenerateBlindWitness(BlindedMessage message, ECPrivKey[] keys, ECPubKey P2PkE) { - return GenerateBlindWitness(message.B_.Key.ToBytes(), keys, keysetId, P2PkE); + return GenerateBlindWitness(message.B_.Key.ToBytes(), keys, message.Id, P2PkE); } public virtual P2PKWitness? GenerateBlindWitness(byte[] msg, ECPrivKey[] keys, KeysetId keysetId, ECPubKey P2PkE) diff --git a/DotNut/NUT11/SigAllHandler.cs b/DotNut/NUT11/SigAllHandler.cs index 2b3b865..8f92d13 100644 --- a/DotNut/NUT11/SigAllHandler.cs +++ b/DotNut/NUT11/SigAllHandler.cs @@ -18,11 +18,12 @@ public class SigAllHandler private P2PKProofSecret? _firstProofSecret; + public bool TrySign(out P2PKWitness? p2pkwitness) { p2pkwitness = null; - if (BlindedMessages.Count == 0) + if (BlindedMessages.Count == 0 || Proofs.Count == 0) { return false; } @@ -40,12 +41,28 @@ public bool TrySign(out P2PKWitness? p2pkwitness) if (_firstProofSecret is HTLCProofSecret s && HTLCPreimage is {} preimage) { + if (Proofs.First().P2PkE is { } E) + { + p2pkwitness = s.GenerateBlindWitness(msg, + PrivKeys.Select(pk => (ECPrivKey)pk).ToArray(), + Encoding.UTF8.GetBytes(preimage), + Proofs[0].Id, + E + ); + return true; + } p2pkwitness = s.GenerateWitness(msg, PrivKeys.Select(pk => (ECPrivKey)pk).ToArray(), Encoding.UTF8.GetBytes(preimage) ); return true; } + + if (Proofs.First().P2PkE is { } e2) + { + p2pkwitness = _firstProofSecret!.GenerateBlindWitness(msg, PrivKeys.Select(pk => (ECPrivKey)pk).ToArray(), Proofs[0].Id, e2); + return true; + } p2pkwitness = _firstProofSecret!.GenerateWitness(msg, PrivKeys.Select(pk => (ECPrivKey)pk).ToArray()); return true; } diff --git a/DotNut/NUT14/HTLCProofSecret.cs b/DotNut/NUT14/HTLCProofSecret.cs index 1e646ca..713885a 100644 --- a/DotNut/NUT14/HTLCProofSecret.cs +++ b/DotNut/NUT14/HTLCProofSecret.cs @@ -68,17 +68,16 @@ public HTLCWitness GenerateWitness(ECPrivKey hash, ECPrivKey[] keys, byte[] prei - public HTLCWitness GenerateBlindWitness(Proof proof, ECPrivKey[] keys, byte[] preimage, KeysetId keysetId) + public HTLCWitness GenerateBlindWitness(Proof proof, ECPrivKey[] keys, string preimage) { throw new NotImplementedException(); } - public HTLCWitness GenerateBlindWitness(Proof proof, ECPrivKey[] keys, byte[] preimage, KeysetId keysetId, - ECPubKey P2PkE) + public HTLCWitness GenerateBlindWitness(Proof proof, ECPrivKey[] keys, string preimage, ECPubKey P2PkE) { throw new NotImplementedException(); } - public HTLCWitness GenerateBlindWitness(BlindedMessage message, ECPrivKey[] keys, byte[] preimage, KeysetId keysetId, ECPubKey P2PkE) + public HTLCWitness GenerateBlindWitness(BlindedMessage message, ECPrivKey[] keys, string preimage, ECPubKey P2PkE) { throw new NotImplementedException(); } @@ -144,20 +143,19 @@ public override P2PKWitness GenerateWitness(byte[] msg, ECPrivKey[] keys) [Obsolete("Use GenerateBlindWitness(Proof proof, ECPrivKey[] keys, byte[] preimage, KeysetId keysetId)")] - public override P2PKWitness GenerateBlindWitness(Proof proof, ECPrivKey[] keys, KeysetId keysetId) + public override P2PKWitness GenerateBlindWitness(Proof proof, ECPrivKey[] keys) { throw new InvalidOperationException(); } [Obsolete("Use GenerateBlindWitness(Proof proof, ECPrivKey[] keys, byte[] preimage, KeysetId keysetId, ECPubKey P2PkE)")] - public override P2PKWitness GenerateBlindWitness(Proof proof, ECPrivKey[] keys, KeysetId keysetId, ECPubKey P2PkE) + public override P2PKWitness GenerateBlindWitness(Proof proof, ECPrivKey[] keys, ECPubKey P2PkE) { throw new InvalidOperationException("Use GenerateBlindWitness(Proof proof, ECPrivKey[] keys, byte[] preimage, KeysetId keysetId, ECPubKey P2PkE)"); } [Obsolete("Use GenerateBlindWitness(BlindedMessage message, ECPrivKey[] keys, byte[] preimage, KeysetId keysetId, ECPubKey P2PkE)")] - public override P2PKWitness GenerateBlindWitness(BlindedMessage message, ECPrivKey[] keys, KeysetId keysetId, - ECPubKey P2PkE) + public override P2PKWitness GenerateBlindWitness(BlindedMessage message, ECPrivKey[] keys, ECPubKey P2PkE) { throw new InvalidOperationException("Use GenerateBlindWitness(BlindedMessage message, ECPrivKey[] keys, byte[] preimage, KeysetId keysetId, ECPubKey P2PkE)"); } From c67de7a5b8bfeef6410d6225225abce9af6f8b35 Mon Sep 17 00:00:00 2001 From: d4rp4t Date: Thu, 27 Nov 2025 02:32:28 +0100 Subject: [PATCH 22/70] fix OutputData --- DotNut.Tests/UnitTests2.cs | 32 +++----- .../Handlers/MeltHandlerBolt11.cs | 6 +- .../Handlers/MeltHandlerBolt12.cs | 6 +- .../Handlers/MintHandlerBolt11.cs | 8 +- .../Handlers/MintHandlerBolt12.cs | 8 +- .../Interfaces/IMeltQuoteBuilder.cs | 2 +- .../Interfaces/IMintQuoteBuilder.cs | 2 +- .../Abstractions/Interfaces/ISwapBuilder.cs | 2 +- .../Abstractions/Interfaces/IWalletBuilder.cs | 5 +- DotNut/Abstractions/MeltQuoteBuilder.cs | 4 +- DotNut/Abstractions/MintQuoteBuilder.cs | 62 +++++++------- DotNut/Abstractions/Nut10Helper.cs | 7 +- DotNut/Abstractions/OutputData.cs | 8 +- DotNut/Abstractions/RestoreBuilder.cs | 10 +-- DotNut/Abstractions/SwapBuilder.cs | 32 ++++---- DotNut/Abstractions/Utils.cs | 82 +++++++++---------- DotNut/Abstractions/Wallet.cs | 4 +- DotNut/NUT13/Nut13.cs | 26 +++--- 18 files changed, 145 insertions(+), 161 deletions(-) diff --git a/DotNut.Tests/UnitTests2.cs b/DotNut.Tests/UnitTests2.cs index 656f8e7..6cd7932 100644 --- a/DotNut.Tests/UnitTests2.cs +++ b/DotNut.Tests/UnitTests2.cs @@ -173,15 +173,13 @@ public void CreateOutputs_ValidAmounts_ReturnsCorrectOutputData() var amounts = new List { 1, 2, 4 }; var outputs = Utils.CreateOutputs(amounts, _testKeysetId, _testKeyset!); - Assert.Equal(3, outputs.BlindedMessages.Count); - Assert.Equal(3, outputs.BlindingFactors.Count); - Assert.Equal(3, outputs.Secrets.Count); + Assert.Equal(3, outputs.Count); - Assert.Equal(1UL, outputs.BlindedMessages[0].Amount); - Assert.Equal(2UL, outputs.BlindedMessages[1].Amount); - Assert.Equal(4UL, outputs.BlindedMessages[2].Amount); + Assert.Equal(1UL, outputs[0].BlindedMessage.Amount); + Assert.Equal(2UL, outputs[1].BlindedMessage.Amount); + Assert.Equal(4UL, outputs[2].BlindedMessage.Amount); - Assert.All(outputs.BlindedMessages, bm => Assert.Equal(_testKeysetId, bm.Id)); + Assert.All(outputs, o => Assert.Equal(_testKeysetId, o.BlindedMessage.Id)); } [Fact] @@ -201,11 +199,11 @@ public void CreateOutputs_DeterministicWithMnemonic() var outputs2 = Utils.CreateOutputs(amounts, _testKeysetId, _testKeyset!, mnemonic, 0); // Same mnemonic and counter should produce same outputs - for (int i = 0; i < outputs1.Secrets.Count; i++) + for (int i = 0; i < outputs1.Count; i++) { Assert.Equal( - ((StringSecret)outputs1.Secrets[i]).Secret, - ((StringSecret)outputs2.Secrets[i]).Secret + ((StringSecret)outputs1[i].Secret).Secret, + ((StringSecret)outputs2[i].Secret).Secret ); } } @@ -220,8 +218,8 @@ public void CreateOutputs_RandomWithoutMnemonic() // without mnemonic, outputs should be random (different) Assert.NotEqual( - ((StringSecret)outputs1.Secrets[0]).Secret, - ((StringSecret)outputs2.Secrets[0]).Secret + ((StringSecret)outputs1[0].Secret).Secret, + ((StringSecret)outputs2[0].Secret).Secret ); } @@ -491,15 +489,7 @@ public void MintInfo_FromGetInfoResponse() var info = new MintInfo(response); Assert.NotNull(info); } - - [Fact] - public void OutputData_EmptyConstructor() - { - var data = new OutputData(); - Assert.NotNull(data.BlindedMessages); - Assert.NotNull(data.BlindingFactors); - Assert.NotNull(data.Secrets); - } + [Fact] public void P2PkBuilder_Build_CreatesValidSecret() diff --git a/DotNut/Abstractions/Handlers/MeltHandlerBolt11.cs b/DotNut/Abstractions/Handlers/MeltHandlerBolt11.cs index 20faef4..2a3ee20 100644 --- a/DotNut/Abstractions/Handlers/MeltHandlerBolt11.cs +++ b/DotNut/Abstractions/Handlers/MeltHandlerBolt11.cs @@ -9,7 +9,7 @@ public class MeltHandlerBolt11 : IMeltHandler _blankOutputs; private bool _withSignatureVerification; private List? _privKeys; private string? _htlcPreimage; @@ -28,7 +28,7 @@ public MeltHandlerBolt11( public MeltHandlerBolt11( IWalletBuilder wallet, PostMeltQuoteBolt11Response quote, - OutputData blankOutputs, + List blankOutputs, List? privKeys = null, string? htlcPreimage = null) { @@ -48,7 +48,7 @@ public async Task> Melt(List inputs, CancellationToken ct = d { Quote = _quote.Quote, Inputs = inputs.ToArray(), - Outputs = _blankOutputs.BlindedMessages.ToArray(), + Outputs = _blankOutputs.Select(bo=> bo.BlindedMessage).ToArray(), }; var res = await client.Melt("bolt11", req, ct); diff --git a/DotNut/Abstractions/Handlers/MeltHandlerBolt12.cs b/DotNut/Abstractions/Handlers/MeltHandlerBolt12.cs index e76645f..c906b32 100644 --- a/DotNut/Abstractions/Handlers/MeltHandlerBolt12.cs +++ b/DotNut/Abstractions/Handlers/MeltHandlerBolt12.cs @@ -10,7 +10,7 @@ public class MeltHandlerBolt12: IMeltHandler _blankOutputs; private bool _withSignatureVerification; private List? _privKeys; private string? _htlcPreimage; @@ -18,7 +18,7 @@ public class MeltHandlerBolt12: IMeltHandler blankOutputs, List? privKeys = null, string? htlcPreimage = null) { @@ -33,7 +33,7 @@ public async Task> Melt(List inputs, CancellationToken ct = d { Quote = _quote.Quote, Inputs = inputs.ToArray(), - Outputs = _blankOutputs.BlindedMessages.ToArray(), + Outputs = _blankOutputs.Select(bo=>bo.BlindedMessage).ToArray(), }; var res = await client.Melt("bolt11", req, ct); diff --git a/DotNut/Abstractions/Handlers/MintHandlerBolt11.cs b/DotNut/Abstractions/Handlers/MintHandlerBolt11.cs index 036cecf..ef5ab32 100644 --- a/DotNut/Abstractions/Handlers/MintHandlerBolt11.cs +++ b/DotNut/Abstractions/Handlers/MintHandlerBolt11.cs @@ -11,7 +11,7 @@ public class MintHandlerBolt11: IMintHandler _outputs; private string? _signature; private WebsocketService? _websocketService; @@ -20,7 +20,7 @@ public MintHandlerBolt11( IWalletBuilder wallet, PostMintQuoteBolt11Response postMintQuoteBolt11Response, GetKeysResponse.KeysetItemResponse? verifiedKeyset, - OutputData outputs + List outputs ) { this._wallet = wallet; @@ -42,7 +42,7 @@ public IMintHandler> SignWithPrivkey(st public IMintHandler> SignWithPrivkey(PrivKey privkey) { - this._signature = privkey.SignMintQuote(_quote.Quote, this._outputs.BlindedMessages); + this._signature = privkey.SignMintQuote(_quote.Quote, this._outputs.Select(o=>o.BlindedMessage).ToList()); return this; } @@ -58,7 +58,7 @@ public async Task> Mint(CancellationToken ct = default) var req = new PostMintRequest { - Outputs = this._outputs.BlindedMessages.ToArray(), + Outputs = this._outputs.Select(o=>o.BlindedMessage).ToArray(), Quote = _quote.Quote, }; diff --git a/DotNut/Abstractions/Handlers/MintHandlerBolt12.cs b/DotNut/Abstractions/Handlers/MintHandlerBolt12.cs index 0015353..0e28336 100644 --- a/DotNut/Abstractions/Handlers/MintHandlerBolt12.cs +++ b/DotNut/Abstractions/Handlers/MintHandlerBolt12.cs @@ -13,12 +13,12 @@ public class MintHandlerBolt12: IMintHandler _outputs; private string? _signature; private string? SubscriptionId; private WebsocketService? _websocketService; - public MintHandlerBolt12(Wallet wallet, PostMintQuoteBolt12Response quote, GetKeysResponse.KeysetItemResponse keyset, OutputData outputs) + public MintHandlerBolt12(Wallet wallet, PostMintQuoteBolt12Response quote, GetKeysResponse.KeysetItemResponse keyset, List outputs) { this._wallet = wallet; this._quote = quote; @@ -39,7 +39,7 @@ public IMintHandler> SignWithPrivkey(st public IMintHandler> SignWithPrivkey(PrivKey privkey) { - this._signature = privkey.SignMintQuote(_quote.Quote, this._outputs.BlindedMessages); + this._signature = privkey.SignMintQuote(_quote.Quote, this._outputs.Select(o=>o.BlindedMessage).ToList()); return this; } @@ -55,7 +55,7 @@ public async Task> Mint(CancellationToken ct = default) var client = await this._wallet.GetMintApi(); var req = new PostMintRequest { - Outputs = this._outputs.BlindedMessages.ToArray(), + Outputs = this._outputs.Select(o=>o.BlindedMessage).ToArray(), Quote = _quote.Quote, Signature = _signature, }; diff --git a/DotNut/Abstractions/Interfaces/IMeltQuoteBuilder.cs b/DotNut/Abstractions/Interfaces/IMeltQuoteBuilder.cs index 2b4ea98..2cfab4e 100644 --- a/DotNut/Abstractions/Interfaces/IMeltQuoteBuilder.cs +++ b/DotNut/Abstractions/Interfaces/IMeltQuoteBuilder.cs @@ -21,7 +21,7 @@ public interface IMeltQuoteBuilder /// /// Optional. Supply previously generated blank outputs instead of deriving them. /// - IMeltQuoteBuilder WithBlankOutputs(OutputData blankOutputs); + IMeltQuoteBuilder WithBlankOutputs(List blankOutputs); /// /// Optional. Provide private keys for P2PK proofs associated with the inputs. diff --git a/DotNut/Abstractions/Interfaces/IMintQuoteBuilder.cs b/DotNut/Abstractions/Interfaces/IMintQuoteBuilder.cs index 3790d9c..71203d8 100644 --- a/DotNut/Abstractions/Interfaces/IMintQuoteBuilder.cs +++ b/DotNut/Abstractions/Interfaces/IMintQuoteBuilder.cs @@ -21,7 +21,7 @@ public interface IMintQuoteBuilder /// /// Optional. Provide precomputed outputs so blinding factors and secrets are reused safely. /// - IMintQuoteBuilder WithOutputs(OutputData outputs); + IMintQuoteBuilder WithOutputs(List outputs); /// /// Optional. Provide description for the mint invoice. diff --git a/DotNut/Abstractions/Interfaces/ISwapBuilder.cs b/DotNut/Abstractions/Interfaces/ISwapBuilder.cs index fc2feab..2a9201a 100644 --- a/DotNut/Abstractions/Interfaces/ISwapBuilder.cs +++ b/DotNut/Abstractions/Interfaces/ISwapBuilder.cs @@ -23,7 +23,7 @@ public interface ISwapBuilder /// /// Optional. Supply custom blank outputs instead of deriving them automatically. /// - ISwapBuilder ForOutputs(OutputData outputs); + ISwapBuilder ForOutputs(List outputs); /// /// Optional. Toggle DLEQ verification for incoming proofs. diff --git a/DotNut/Abstractions/Interfaces/IWalletBuilder.cs b/DotNut/Abstractions/Interfaces/IWalletBuilder.cs index a426ae4..242aa3f 100644 --- a/DotNut/Abstractions/Interfaces/IWalletBuilder.cs +++ b/DotNut/Abstractions/Interfaces/IWalletBuilder.cs @@ -128,7 +128,8 @@ public interface IWalletBuilder IWalletBuilder WithWebsocketService(IWebsocketService websocketService); Task GetInfo(bool forceReferesh = false, CancellationToken ct = default); - Task CreateOutputs(List amounts, KeysetId id, CancellationToken ct = default); + Task> CreateOutputs(List amounts, KeysetId id, CancellationToken ct = default); + Task> CreateOutputs(List amounts, string unit, CancellationToken ct = default); Task?> GetActiveKeysetIdsWithUnits(CancellationToken ct = default); @@ -143,8 +144,6 @@ public interface IWalletBuilder Task> GetKeysets(bool forceRefresh = false, CancellationToken ct = default); - Task CreateOutputs(List amounts, string unit, CancellationToken ct = default); - Task SelectProofsToSend(List proofs, ulong amount, bool includeFees, CancellationToken ct = default); diff --git a/DotNut/Abstractions/MeltQuoteBuilder.cs b/DotNut/Abstractions/MeltQuoteBuilder.cs index 0d9fab0..9117313 100644 --- a/DotNut/Abstractions/MeltQuoteBuilder.cs +++ b/DotNut/Abstractions/MeltQuoteBuilder.cs @@ -11,7 +11,7 @@ class MeltQuoteBuilder : IMeltQuoteBuilder private readonly Wallet _wallet; private List? _proofs; private string? _invoice; - private OutputData? _blankOutputs; + private List? _blankOutputs; private string _unit = "sat"; private List? _privKeys; @@ -36,7 +36,7 @@ public IMeltQuoteBuilder WithUnit(string unit) return this; } - public IMeltQuoteBuilder WithBlankOutputs(OutputData blankOutputs) + public IMeltQuoteBuilder WithBlankOutputs(List blankOutputs) { this._blankOutputs = blankOutputs; return this; diff --git a/DotNut/Abstractions/MintQuoteBuilder.cs b/DotNut/Abstractions/MintQuoteBuilder.cs index c58ba48..ce9d158 100644 --- a/DotNut/Abstractions/MintQuoteBuilder.cs +++ b/DotNut/Abstractions/MintQuoteBuilder.cs @@ -14,7 +14,7 @@ class MintQuoteBuilder : IMintQuoteBuilder private List? _amounts; private string _unit = "sat"; private string? _description; - private OutputData? _outputs; + private List? _outputs; private string? _method = "bolt11"; private string? _pubkey; @@ -67,7 +67,7 @@ public IMintQuoteBuilder WithKeyset(KeysetId keysetId) } - public IMintQuoteBuilder WithOutputs(OutputData outputs) + public IMintQuoteBuilder WithOutputs(List outputs) { this._outputs = outputs; return this; @@ -133,40 +133,40 @@ await api.CreateMintQuote>> ProcessAsyncBolt12( CancellationToken ct = default) { - await this._wallet._maybeSyncKeys(ct); - if (this._pubkey == null) - { - throw new ArgumentNullException(nameof(_pubkey), "Can't request bolt12 mint quote without pubkey!"); - } + await this._wallet._maybeSyncKeys(ct); + if (this._pubkey == null) + { + throw new ArgumentNullException(nameof(_pubkey), "Can't request bolt12 mint quote without pubkey!"); + } - if (this._keyset == null) - { - this._keyset = await this._wallet.GetKeys(this._keysetId, false, ct) ?? - throw new ArgumentException($"Cant fetch keys for keysetId: {_keysetId}"); - } + if (this._keyset == null) + { + this._keyset = await this._wallet.GetKeys(this._keysetId, false, ct) ?? + throw new ArgumentException($"Cant fetch keys for keysetId: {_keysetId}"); + } - var outputs = await this._createOutputs(); + var outputs = await this._createOutputs(); - var req = new PostMintQuoteBolt12Request() - { - Amount = this._amount.Value, - Unit = this._unit, - Pubkey = this._pubkey, - Description = this._description, - }; - var mintQuote = - await (await _wallet.GetMintApi()) - .CreateMintQuote("bolt12", req, - ct); - return new MintHandlerBolt12(this._wallet, mintQuote, this._keyset, outputs); + var req = new PostMintQuoteBolt12Request() + { + Amount = this._amount.Value, + Unit = this._unit, + Pubkey = this._pubkey, + Description = this._description, + }; + var mintQuote = + await (await _wallet.GetMintApi()) + .CreateMintQuote("bolt12", req, + ct); + return new MintHandlerBolt12(this._wallet, mintQuote, this._keyset, outputs); } - - async Task _createOutputs() + // skipped checks for keysetid and keys, since its validated before. make sure to remember about it. + async Task> _createOutputs() { - var outputs = new OutputData(); + var outputs = new List(); if (this._outputs != null) { @@ -188,13 +188,11 @@ async Task _createOutputs() return await _wallet.CreateOutputs(_amounts, this._keysetId!); } - // skipped checks for keysetid and keys, since its validated before. make sure to remember about it. + foreach (var amount in _amounts) { var p2pkOutput = Utils.CreateNut10Output(amount, this._keysetId!, _builder); - outputs.BlindingFactors.Add(p2pkOutput.BlindingFactors[0]); - outputs.BlindedMessages.Add(p2pkOutput.BlindedMessages[0]); - outputs.Secrets.Add(p2pkOutput.Secrets[0]); + outputs.Add(p2pkOutput); } return outputs; diff --git a/DotNut/Abstractions/Nut10Helper.cs b/DotNut/Abstractions/Nut10Helper.cs index 1923020..cb543c7 100644 --- a/DotNut/Abstractions/Nut10Helper.cs +++ b/DotNut/Abstractions/Nut10Helper.cs @@ -8,7 +8,7 @@ public class Nut10Helper public static void MaybeProcessNut10( List privKeys, List proofs, - OutputData? outputs = null, + List? outputs = null, string? htlcPreimage = null, string? meltQuoteId = null ) @@ -17,12 +17,13 @@ public static void MaybeProcessNut10( { return; } - + + outputs ??= []; var sigAllHandler = new SigAllHandler { Proofs = proofs, PrivKeys = privKeys, - BlindedMessages = outputs?.BlindedMessages ?? [], + BlindedMessages = outputs.Select(o=>o.BlindedMessage).ToList(), HTLCPreimage = htlcPreimage, MeltQuoteId = meltQuoteId }; diff --git a/DotNut/Abstractions/OutputData.cs b/DotNut/Abstractions/OutputData.cs index 98483fd..34a093b 100644 --- a/DotNut/Abstractions/OutputData.cs +++ b/DotNut/Abstractions/OutputData.cs @@ -2,7 +2,9 @@ namespace DotNut; public class OutputData { - public List BlindedMessages { get; set; } = []; - public List Secrets { get; set; } = []; - public List BlindingFactors { get; set; } = []; + public BlindedMessage BlindedMessage { get; set; } + public ISecret Secret { get; set; } + public PrivKey BlindingFactor { get; set; } + + public PubKey? P2BkE {get; set;} } \ No newline at end of file diff --git a/DotNut/Abstractions/RestoreBuilder.cs b/DotNut/Abstractions/RestoreBuilder.cs index 8cdf6cf..48799b3 100644 --- a/DotNut/Abstractions/RestoreBuilder.cs +++ b/DotNut/Abstractions/RestoreBuilder.cs @@ -66,7 +66,7 @@ public async Task> ProcessAsync(CancellationToken ct = defaul await counter!.IncrementCounter(keysetId, batchNumber * 100); var req = new PostRestoreRequest { - Outputs = outputs.BlindedMessages.ToArray() + Outputs = outputs.Select(o=>o.BlindedMessage).ToArray() }; var res = await api.Restore(req, ct); @@ -88,7 +88,7 @@ public async Task> ProcessAsync(CancellationToken ct = defaul } var freshProofs = new List(); - var activeUnits = await this._wallet.GetActiveKeysetIdsWithUnits(); + var activeUnits = await this._wallet.GetActiveKeysetIdsWithUnits(ct); if (activeUnits == null || !activeUnits.Any()) { @@ -102,12 +102,12 @@ public async Task> ProcessAsync(CancellationToken ct = defaul var amounts = Utils.SplitToProofsAmounts(totalAmount, correspondingKeys.Keys); var ctr = await counter!.GetCounterForId(unitKeyset.Value, ct); var newOutputs = Utils.CreateOutputs(amounts, unitKeyset.Value, correspondingKeys.Keys, mnemonic, ctr); - await counter.IncrementCounter(unitKeyset.Value, newOutputs.BlindedMessages.Count, ct); + await counter.IncrementCounter(unitKeyset.Value, newOutputs.Select(o=>o.BlindedMessage).Count(), ct); var swapRequest = new PostSwapRequest { Inputs = recoveredProofs.ToArray(), - Outputs = newOutputs.BlindedMessages.ToArray(), + Outputs = newOutputs.Select(o=>o.BlindedMessage).ToArray(), }; var swapResult = await api.Swap(swapRequest, ct); @@ -118,7 +118,7 @@ public async Task> ProcessAsync(CancellationToken ct = defaul return freshProofs; } - private async Task _createBatch(Mnemonic mnemonic, KeysetId keysetId, int batchNubmber, CancellationToken ct) + private async Task> _createBatch(Mnemonic mnemonic, KeysetId keysetId, int batchNubmber, CancellationToken ct) { var amounts = Enumerable.Repeat((ulong)1, 100).ToList(); Console.WriteLine(batchNubmber); diff --git a/DotNut/Abstractions/SwapBuilder.cs b/DotNut/Abstractions/SwapBuilder.cs index 9a28751..85cecf8 100644 --- a/DotNut/Abstractions/SwapBuilder.cs +++ b/DotNut/Abstractions/SwapBuilder.cs @@ -1,3 +1,4 @@ +using System.Security.Cryptography; using System.Text.Json; using DotNut.Abstractions.Interfaces; using DotNut.ApiModels; @@ -15,7 +16,7 @@ class SwapBuilder : ISwapBuilder private readonly CashuToken? _token; private List? _proofsToSwap; - private OutputData? _outputs; + private List? _outputs; private List? _amounts; private KeysetId? _keysetId; @@ -58,7 +59,7 @@ public ISwapBuilder FromInputs(IEnumerable proofs) return this; } - public ISwapBuilder ForOutputs(OutputData outputs) + public ISwapBuilder ForOutputs(List outputs) { this._outputs = outputs; return this; @@ -173,7 +174,7 @@ public async Task> ProcessAsync(CancellationToken ct = default) var request = new PostSwapRequest() { Inputs = swapInputs.ToArray(), - Outputs = outputs.BlindedMessages.ToArray(), + Outputs = outputs.Select(o=>o.BlindedMessage).ToArray(), }; Nut10Helper.MaybeProcessNut10(_privKeys??[], swapInputs, outputs, _htlcPreimage); @@ -219,7 +220,7 @@ private List _getSwapProofs(CancellationToken ct = default) return _proofsToSwap; } - async Task _getOutputs(Keyset keys, CancellationToken ct = default) + async Task> _getOutputs(Keyset keys, CancellationToken ct = default) { if (this._outputs != null) { @@ -235,29 +236,30 @@ async Task _getOutputs(Keyset keys, CancellationToken ct = default) throw new ArgumentNullException(nameof(_amounts), "Amounts can't be null."); } - var outputs = new OutputData(); + var outputs = new List(); if (this._builder is not null) { if (this._shouldBlind) { if (this._builder.SigFlag == "SIG_ALL") { - // create first output, then rest of them should have identical E. - + var e = new PrivKey(RandomNumberGenerator.GetHexString(64)); + foreach (var amount in _amounts) + { + outputs.Add(Utils.CreateNut10BlindedOutput(amount, this._keysetId!, _builder, e)); + } + return outputs; } - foreach (var p2pkOutput in _amounts.Select(amount => Utils.CreateNut10Output(amount, this._keysetId!, _builder))) + foreach (var amount in _amounts) { - outputs.BlindingFactors.Add(p2pkOutput.BlindingFactors[0]); - outputs.BlindedMessages.Add(p2pkOutput.BlindedMessages[0]); - outputs.Secrets.Add(p2pkOutput.Secrets[0]); + outputs.Add(Utils.CreateNut10BlindedOutput(amount, this._keysetId!, _builder)); } + return outputs; } // skipped checks for keysetid and keys, since its validated before. make sure to remember about it. - foreach (var p2pkOutput in _amounts.Select(amount => Utils.CreateNut10Output(amount, this._keysetId!, _builder))) + foreach (var amount in _amounts) { - outputs.BlindingFactors.Add(p2pkOutput.BlindingFactors[0]); - outputs.BlindedMessages.Add(p2pkOutput.BlindedMessages[0]); - outputs.Secrets.Add(p2pkOutput.Secrets[0]); + outputs.Add(Utils.CreateNut10Output(amount, this._keysetId!, _builder)); } return outputs; } diff --git a/DotNut/Abstractions/Utils.cs b/DotNut/Abstractions/Utils.cs index c8a3f70..f0ce379 100644 --- a/DotNut/Abstractions/Utils.cs +++ b/DotNut/Abstractions/Utils.cs @@ -39,7 +39,7 @@ public static List SplitToProofsAmounts(ulong paymentAmount, Keyset keyse /// Active keyset id which will sign outputs /// Keys for given KeysetId /// Blank Outputs - public static OutputData CreateBlankOutputs(ulong amount, KeysetId keysetId, Keyset keys, DotNut.NBitcoin.BIP39.Mnemonic? mnemonic = null, int? counter = null) + public static List CreateBlankOutputs(ulong amount, KeysetId keysetId, Keyset keys, DotNut.NBitcoin.BIP39.Mnemonic? mnemonic = null, int? counter = null) { if (amount == 0) { @@ -82,7 +82,7 @@ public static int CalculateNumberOfBlankOutputs(ulong amountToCover) /// Keyset for given ID /// /// - public static OutputData CreateOutputs( + public static List CreateOutputs( List amounts, KeysetId keysetId, Keyset keys, @@ -92,46 +92,41 @@ public static OutputData CreateOutputs( if (amounts.Any(a => !keys.Keys.Contains(a))) throw new ArgumentException("Invalid amounts"); - var blindedMessages = new List(amounts.Count); - var secrets = new List(amounts.Count); - var blindingFactors = new List(amounts.Count); - + var outputs = new List(amounts.Count); + if (mnemonic is not null && counter is { } c) { for (var i = 0; i < amounts.Count; i++) { var secret = mnemonic.DeriveSecret(keysetId, c + i); - secrets.Add(secret); - var r = new PrivKey(mnemonic.DeriveBlindingFactor(keysetId, c + i)); - blindingFactors.Add(r); - var B_ = Cashu.ComputeB_(secret.ToCurve(), r); - blindedMessages.Add(new BlindedMessage {Amount = amounts[i], B_ = B_, Id = keysetId }); + var output = new OutputData + { + BlindedMessage = new BlindedMessage { Amount = amounts[i], B_ = B_, Id = keysetId }, + BlindingFactor = r, + Secret = secret + }; + outputs.Add(output); } + return outputs; } - else + + foreach (var amount in amounts) { - foreach (var amount in amounts) + var secret = RandomSecret(); + var r = RandomPrivkey(); + var B_ = DotNut.Cashu.ComputeB_(secret.ToCurve(), r); + var output = new OutputData { - var secret = RandomSecret(); - secrets.Add(secret); - - var r = RandomPrivkey(); - blindingFactors.Add(r); - - var B_ = DotNut.Cashu.ComputeB_(secret.ToCurve(), r); - blindedMessages.Add(new BlindedMessage() { Amount = amount, B_ = B_, Id = keysetId }); - } + BlindedMessage = new BlindedMessage { Amount = amount, B_ = B_, Id = keysetId }, + BlindingFactor = r, + Secret = secret + }; + outputs.Add(output); } - - return new OutputData() - { - BlindingFactors = blindingFactors, - BlindedMessages = blindedMessages, - Secrets = secrets - }; + return outputs; } public static OutputData CreateNut10Output( @@ -155,15 +150,16 @@ P2PkBuilder builder var B_ = Cashu.ComputeB_(secret.ToCurve(), r); return new OutputData { - BlindedMessages = [new BlindedMessage() { Amount = amount, B_ = B_, Id = keysetId }], - BlindingFactors = [r], - Secrets = [secret] + BlindedMessage = new BlindedMessage() { Amount = amount, B_ = B_, Id = keysetId }, + BlindingFactor = r, + Secret = secret }; } - public static OutputData CreateNut10BlindedOutput(ulong amount, KeysetId keysetId, P2PkBuilder builder, out PubKey E) + public static OutputData CreateNut10BlindedOutput(ulong amount, KeysetId keysetId, P2PkBuilder builder) { // ugliest hack ever Nut10Secret secret; + PubKey E; if (builder is HTLCBuilder htlc) { secret = new Nut10Secret("HTLC", htlc.BuildBlinded(keysetId, out var e)); @@ -179,9 +175,10 @@ public static OutputData CreateNut10BlindedOutput(ulong amount, KeysetId keysetI var B_ = Cashu.ComputeB_(secret.ToCurve(), r); return new OutputData { - BlindedMessages = [new BlindedMessage() { Amount = amount, B_ = B_, Id = keysetId }], - BlindingFactors = [r], - Secrets = [secret] + BlindedMessage = new BlindedMessage() { Amount = amount, B_ = B_, Id = keysetId }, + BlindingFactor = r, + Secret = secret, + P2BkE = E }; } public static OutputData CreateNut10BlindedOutput(ulong amount, KeysetId keysetId, P2PkBuilder builder, PrivKey e) @@ -201,9 +198,10 @@ public static OutputData CreateNut10BlindedOutput(ulong amount, KeysetId keysetI var B_ = Cashu.ComputeB_(secret.ToCurve(), r); return new OutputData { - BlindedMessages = [new BlindedMessage() { Amount = amount, B_ = B_, Id = keysetId }], - BlindingFactors = [r], - Secrets = [secret] + BlindedMessage = new BlindedMessage() { Amount = amount, B_ = B_, Id = keysetId }, + BlindingFactor = r, + Secret = secret, + P2BkE = e.Key.CreatePubKey() }; } @@ -247,7 +245,7 @@ public static Proof ConstructProofFromPromise( public static List ConstructProofsFromPromises( List promises, - OutputData outputs, + List outputs, Keyset keys ) { @@ -260,8 +258,8 @@ Keyset keys } var proof = ConstructProofFromPromise( promises[i], - outputs.BlindingFactors[i], - outputs.Secrets[i], + outputs[i].BlindingFactor, + outputs[i].Secret, key ); proofs.Add(proof); diff --git a/DotNut/Abstractions/Wallet.cs b/DotNut/Abstractions/Wallet.cs index c842204..a868c18 100644 --- a/DotNut/Abstractions/Wallet.cs +++ b/DotNut/Abstractions/Wallet.cs @@ -322,7 +322,7 @@ public async Task GetInfo(bool forceReferesh = false, CancellationToke /// /// Outputs /// If keys not set. If Mnemonic set, but no Counter. - public async Task CreateOutputs(List amounts, KeysetId id, CancellationToken ct = default) + public async Task> CreateOutputs(List amounts, KeysetId id, CancellationToken ct = default) { await _maybeSyncKeys(ct); if (this._keys == null) @@ -356,7 +356,7 @@ public async Task CreateOutputs(List amounts, KeysetId id, Ca /// /// Outputs /// If no keysetID stored in wallet. - public async Task CreateOutputs(List amounts, string unit, CancellationToken ct = default) + public async Task> CreateOutputs(List amounts, string unit, CancellationToken ct = default) { var keysetId = await this.GetActiveKeysetId(unit, ct); if (keysetId == null) diff --git a/DotNut/NUT13/Nut13.cs b/DotNut/NUT13/Nut13.cs index f68b2cc..1332ff9 100644 --- a/DotNut/NUT13/Nut13.cs +++ b/DotNut/NUT13/Nut13.cs @@ -14,12 +14,10 @@ public static StringSecret DeriveSecret(this Mnemonic mnemonic, KeysetId keysetI DeriveSecret(mnemonic.DeriveSeed(), keysetId, counter); - public static OutputData DeriveOutputs(this Mnemonic mnemonic, IEnumerable amounts, KeysetId keysetId, + public static List DeriveOutputs(this Mnemonic mnemonic, IEnumerable amounts, KeysetId keysetId, int counter) { - var blindedMessages = new List(); - var secrets = new List(); - var blindingFactors = new List(); + var outputs = new List(); var amountList = amounts.ToList(); @@ -33,24 +31,20 @@ public static OutputData DeriveOutputs(this Mnemonic mnemonic, IEnumerable Date: Thu, 27 Nov 2025 17:36:27 +0100 Subject: [PATCH 23/70] fix namespaces --- DotNut.Tests/Integration.cs | 4 +- DotNut.sln.DotSettings.user | 19 +++++-- .../Handlers/MeltHandlerBolt11.cs | 57 +++++-------------- .../Handlers/MeltHandlerBolt12.cs | 41 +++++-------- .../Handlers/MintHandlerBolt11.cs | 48 +++++----------- .../Handlers/MintHandlerBolt12.cs | 42 +++++--------- DotNut/Abstractions/InMemoryCounter.cs | 2 - DotNut/Abstractions/Interfaces/ICounter.cs | 2 +- .../Abstractions/Interfaces/IMeltHandler.cs | 6 +- .../Interfaces/IMeltQuoteBuilder.cs | 6 +- .../Interfaces/IMintQuoteBuilder.cs | 2 +- .../Interfaces/IRestoreBuilder.cs | 2 +- .../Abstractions/Interfaces/ISwapBuilder.cs | 2 +- .../Abstractions/Interfaces/IWalletBuilder.cs | 1 - .../Interfaces/IWebsocketService.cs | 3 +- DotNut/Abstractions/MeltQuoteBuilder.cs | 4 +- DotNut/Abstractions/MintInfo.cs | 1 - DotNut/Abstractions/MintQuoteBuilder.cs | 3 +- DotNut/Abstractions/OutputData.cs | 2 +- DotNut/Abstractions/ProofSelector.cs | 3 +- DotNut/Abstractions/RestoreBuilder.cs | 1 - DotNut/Abstractions/SwapBuilder.cs | 2 - DotNut/Abstractions/Utils.cs | 7 ++- DotNut/Abstractions/Wallet.cs | 3 - .../bolt12/PostMeltQuoteBolt12Response.cs | 4 +- DotNut/NUT13/Nut13.cs | 3 +- 26 files changed, 95 insertions(+), 175 deletions(-) diff --git a/DotNut.Tests/Integration.cs b/DotNut.Tests/Integration.cs index 249b0ef..3a38d25 100644 --- a/DotNut.Tests/Integration.cs +++ b/DotNut.Tests/Integration.cs @@ -1,9 +1,7 @@ using System.Security.Cryptography; using DotNut.Abstractions; -using DotNut.Abstractions.Interfaces; using DotNut.Abstractions.Websockets; using DotNut.Api; -using SHA256 = System.Security.Cryptography.SHA256; namespace DotNut.Tests; @@ -333,7 +331,7 @@ await Assert.ThrowsAsync(async () => var handler = await wallet .CreateMeltQuote() .WithInvoice(valuesInvoices[501]) - .WithPrivkeys([privKeyBob, privKeyAlice]) + .WithPrivKeys([privKeyBob, privKeyAlice]) .ProcessAsyncBolt11(); var q = await handler.GetQuote(); diff --git a/DotNut.sln.DotSettings.user b/DotNut.sln.DotSettings.user index 42384bd..a3dd44a 100644 --- a/DotNut.sln.DotSettings.user +++ b/DotNut.sln.DotSettings.user @@ -2,18 +2,22 @@ ForceIncluded ForceIncluded ForceIncluded + ForceIncluded ForceIncluded + ForceIncluded ForceIncluded ForceIncluded ForceIncluded ForceIncluded + ForceIncluded ForceIncluded + ForceIncluded ForceIncluded ForceIncluded ForceIncluded - <SessionState ContinuousTestingMode="0" IsActive="True" Name="Test1" xmlns="urn:schemas-jetbrains-com:jetbrains-ut-session"> + <SessionState ContinuousTestingMode="0" Name="Test1" xmlns="urn:schemas-jetbrains-com:jetbrains-ut-session"> <TestAncestor> <TestId>xUnit::0AAAA85C-9FDC-4FD5-9CC2-ED8385B40106::net8.0::DotNut.Tests.UnitTest1</TestId> <TestId>xUnit::0AAAA85C-9FDC-4FD5-9CC2-ED8385B40106::net8.0::DotNut.Tests.Integration</TestId> @@ -22,9 +26,14 @@ <SessionState ContinuousTestingMode="0" Name="Nut11_Signatures" xmlns="urn:schemas-jetbrains-com:jetbrains-ut-session"> <TestAncestor> <TestId>xUnit::0AAAA85C-9FDC-4FD5-9CC2-ED8385B40106::net8.0::DotNut.Tests.UnitTest1</TestId> - <TestId>xUnit::0AAAA85C-9FDC-4FD5-9CC2-ED8385B40106::net8.0::DotNut.Tests.Integration.ThrowsWhenMintNotFound</TestId> - <TestId>xUnit::0AAAA85C-9FDC-4FD5-9CC2-ED8385B40106::net8.0::DotNut.Tests.Integration.FetchesInfoSuccessfully</TestId> - <TestId>xUnit::0AAAA85C-9FDC-4FD5-9CC2-ED8385B40106::net8.0::DotNut.Tests.Integration.MintsSuccessfully</TestId> - <TestId>xUnit::0AAAA85C-9FDC-4FD5-9CC2-ED8385B40106::net8.0::DotNut.Tests.Integration.MintsDeterministicSuccessfully</TestId> + <TestId>xUnit::0AAAA85C-9FDC-4FD5-9CC2-ED8385B40106::net8.0::DotNut.Tests.UnitTests2.BuilderChainingPreservesAllSettings</TestId> + <TestId>xUnit::0AAAA85C-9FDC-4FD5-9CC2-ED8385B40106::net8.0::DotNut.Tests.UnitTests2.WithMintStringVariantCreatesHttpClient</TestId> + <TestId>xUnit::0AAAA85C-9FDC-4FD5-9CC2-ED8385B40106::net8.0::DotNut.Tests.Integration</TestId> + </TestAncestor> +</SessionState> + <SessionState ContinuousTestingMode="0" IsActive="True" Name="Integration" xmlns="urn:schemas-jetbrains-com:jetbrains-ut-session"> + <TestAncestor> + <TestId>xUnit::0AAAA85C-9FDC-4FD5-9CC2-ED8385B40106::net8.0::DotNut.Tests.Integration</TestId> + <TestId>xUnit::0AAAA85C-9FDC-4FD5-9CC2-ED8385B40106::net8.0::DotNut.Tests.UnitTests2</TestId> </TestAncestor> </SessionState> \ No newline at end of file diff --git a/DotNut/Abstractions/Handlers/MeltHandlerBolt11.cs b/DotNut/Abstractions/Handlers/MeltHandlerBolt11.cs index 2a3ee20..0c1cf40 100644 --- a/DotNut/Abstractions/Handlers/MeltHandlerBolt11.cs +++ b/DotNut/Abstractions/Handlers/MeltHandlerBolt11.cs @@ -1,54 +1,25 @@ -using System.Text.Json; -using DotNut.Abstractions.Interfaces; -using DotNut.Abstractions.Websockets; using DotNut.ApiModels; namespace DotNut.Abstractions.Handlers; -public class MeltHandlerBolt11 : IMeltHandler> +public class MeltHandlerBolt11( + IWalletBuilder wallet, + PostMeltQuoteBolt11Response quote, + List blankOutputs, + List? privKeys = null, + string? htlcPreimage = null) + : IMeltHandler> { - private IWalletBuilder _wallet; - private PostMeltQuoteBolt11Response _quote; - private List _blankOutputs; - private bool _withSignatureVerification; - private List? _privKeys; - private string? _htlcPreimage; - - public MeltHandlerBolt11( - IWalletBuilder wallet, - PostMeltQuoteBolt11Response quote, - List? privKeys = null, - string? htlcPreimage = null) - { - _wallet = wallet; - _quote = quote; - _privKeys = privKeys; - _htlcPreimage = htlcPreimage; - } - public MeltHandlerBolt11( - IWalletBuilder wallet, - PostMeltQuoteBolt11Response quote, - List blankOutputs, - List? privKeys = null, - string? htlcPreimage = null) - { - _wallet = wallet; - _quote = quote; - _blankOutputs = blankOutputs; - _privKeys = privKeys; - _htlcPreimage = htlcPreimage; - } - - public async Task GetQuote(CancellationToken ct = default) => this._quote; + public async Task GetQuote(CancellationToken ct = default) => quote; public async Task> Melt(List inputs, CancellationToken ct = default) { - Nut10Helper.MaybeProcessNut10(_privKeys??[], inputs, _blankOutputs, _htlcPreimage, _quote.Quote); - var client = await _wallet.GetMintApi(ct); + Nut10Helper.MaybeProcessNut10(privKeys??[], inputs, blankOutputs, htlcPreimage, quote.Quote); + var client = await wallet.GetMintApi(ct); var req = new PostMeltRequest { - Quote = _quote.Quote, + Quote = quote.Quote, Inputs = inputs.ToArray(), - Outputs = _blankOutputs.Select(bo=> bo.BlindedMessage).ToArray(), + Outputs = blankOutputs.Select(bo=> bo.BlindedMessage).ToArray(), }; var res = await client.Melt("bolt11", req, ct); @@ -57,7 +28,7 @@ public async Task> Melt(List inputs, CancellationToken ct = d return []; } - var keyset = await _wallet.GetKeys(res.Change.First().Id, false, ct); - return Utils.ConstructProofsFromPromises(res.Change.ToList(), _blankOutputs, keyset.Keys); + var keyset = await wallet.GetKeys(res.Change.First().Id, false, ct); + return Utils.ConstructProofsFromPromises(res.Change.ToList(), blankOutputs, keyset.Keys); } } \ No newline at end of file diff --git a/DotNut/Abstractions/Handlers/MeltHandlerBolt12.cs b/DotNut/Abstractions/Handlers/MeltHandlerBolt12.cs index c906b32..9b9e322 100644 --- a/DotNut/Abstractions/Handlers/MeltHandlerBolt12.cs +++ b/DotNut/Abstractions/Handlers/MeltHandlerBolt12.cs @@ -1,39 +1,26 @@ -using System.Text.Json; -using DotNut.Abstractions.Interfaces; -using DotNut.Abstractions.Websockets; using DotNut.ApiModels; using DotNut.ApiModels.Melt.bolt12; namespace DotNut.Abstractions.Handlers; -public class MeltHandlerBolt12: IMeltHandler> +public class MeltHandlerBolt12( + IWalletBuilder wallet, + PostMeltQuoteBolt12Response quote, + List blankOutputs, + List? privKeys = null, + string? htlcPreimage = null) + : IMeltHandler> { - private IWalletBuilder _wallet; - private PostMeltQuoteBolt12Response _quote; - private List _blankOutputs; - private bool _withSignatureVerification; - private List? _privKeys; - private string? _htlcPreimage; - - public MeltHandlerBolt12( - IWalletBuilder wallet, - PostMeltQuoteBolt12Response quote, - List blankOutputs, - List? privKeys = null, - string? htlcPreimage = null) - { - - } - public async Task GetQuote(CancellationToken ct = default) => this._quote; + public async Task GetQuote(CancellationToken ct = default) => quote; public async Task> Melt(List inputs, CancellationToken ct = default) { - Nut10Helper.MaybeProcessNut10(_privKeys??[], inputs, _blankOutputs, _htlcPreimage, _quote.Quote); - var client = await _wallet.GetMintApi(); + Nut10Helper.MaybeProcessNut10(privKeys??[], inputs, blankOutputs, htlcPreimage, quote.Quote); + var client = await wallet.GetMintApi(ct); var req = new PostMeltRequest { - Quote = _quote.Quote, + Quote = quote.Quote, Inputs = inputs.ToArray(), - Outputs = _blankOutputs.Select(bo=>bo.BlindedMessage).ToArray(), + Outputs = blankOutputs.Select(bo=>bo.BlindedMessage).ToArray(), }; var res = await client.Melt("bolt11", req, ct); @@ -42,7 +29,7 @@ public async Task> Melt(List inputs, CancellationToken ct = d return []; } - var keyset = await _wallet.GetKeys(res.Change.First().Id, false, ct); - return Utils.ConstructProofsFromPromises(res.Change.ToList(), _blankOutputs, keyset.Keys); + var keyset = await wallet.GetKeys(res.Change.First().Id, false, ct); + return Utils.ConstructProofsFromPromises(res.Change.ToList(), blankOutputs, keyset.Keys); } } \ No newline at end of file diff --git a/DotNut/Abstractions/Handlers/MintHandlerBolt11.cs b/DotNut/Abstractions/Handlers/MintHandlerBolt11.cs index ef5ab32..853cc48 100644 --- a/DotNut/Abstractions/Handlers/MintHandlerBolt11.cs +++ b/DotNut/Abstractions/Handlers/MintHandlerBolt11.cs @@ -1,34 +1,16 @@ -using System.Runtime.CompilerServices; -using DotNut.Abstractions.Websockets; -using DotNut.Api; using DotNut.ApiModels; -using DotNut.ApiModels.Mint.bolt12; -namespace DotNut.Abstractions.Quotes; +namespace DotNut.Abstractions.Handlers; -public class MintHandlerBolt11: IMintHandler> +public class MintHandlerBolt11( + IWalletBuilder wallet, + PostMintQuoteBolt11Response postMintQuoteBolt11Response, + GetKeysResponse.KeysetItemResponse keyset, + List outputs) + : IMintHandler> { - private readonly PostMintQuoteBolt11Response _quote; - private readonly IWalletBuilder _wallet; - private readonly GetKeysResponse.KeysetItemResponse _keyset; - private readonly List _outputs; - private string? _signature; - private WebsocketService? _websocketService; - public MintHandlerBolt11( - IWalletBuilder wallet, - PostMintQuoteBolt11Response postMintQuoteBolt11Response, - GetKeysResponse.KeysetItemResponse? verifiedKeyset, - List outputs - ) - { - this._wallet = wallet; - this._quote = postMintQuoteBolt11Response; - this._keyset = verifiedKeyset; - this._outputs = outputs; - } - public IMintHandler> WithSignature(string signature) { _signature = signature; @@ -42,28 +24,28 @@ public IMintHandler> SignWithPrivkey(st public IMintHandler> SignWithPrivkey(PrivKey privkey) { - this._signature = privkey.SignMintQuote(_quote.Quote, this._outputs.Select(o=>o.BlindedMessage).ToList()); + this._signature = privkey.SignMintQuote(postMintQuoteBolt11Response.Quote, outputs.Select(o=>o.BlindedMessage).ToList()); return this; } - public async Task GetQuote(CancellationToken ct = default) => _quote; + public async Task GetQuote(CancellationToken ct = default) => postMintQuoteBolt11Response; public async Task> Mint(CancellationToken ct = default) { - if (this._quote.PubKey is not null && this._signature is null) + if (postMintQuoteBolt11Response.PubKey is not null && this._signature is null) { - throw new ArgumentNullException(nameof(_signature),$"Signature for mint quote {this._quote.Quote} is required!" ); + throw new ArgumentNullException(nameof(_signature),$"Signature for mint quote {postMintQuoteBolt11Response.Quote} is required!" ); } - var client = await this._wallet.GetMintApi(); + var client = await wallet.GetMintApi(ct); var req = new PostMintRequest { - Outputs = this._outputs.Select(o=>o.BlindedMessage).ToArray(), - Quote = _quote.Quote, + Outputs = outputs.Select(o=>o.BlindedMessage).ToArray(), + Quote = postMintQuoteBolt11Response.Quote, }; var promises= await client.Mint("bolt11", req, ct); - return Utils.ConstructProofsFromPromises(promises.Signatures.ToList(), _outputs, _keyset.Keys); + return Utils.ConstructProofsFromPromises(promises.Signatures.ToList(), outputs, keyset.Keys); } } \ No newline at end of file diff --git a/DotNut/Abstractions/Handlers/MintHandlerBolt12.cs b/DotNut/Abstractions/Handlers/MintHandlerBolt12.cs index 0e28336..94833e8 100644 --- a/DotNut/Abstractions/Handlers/MintHandlerBolt12.cs +++ b/DotNut/Abstractions/Handlers/MintHandlerBolt12.cs @@ -1,30 +1,16 @@ -using System.Xml; -using DotNut.Abstractions.Websockets; -using DotNut.Api; using DotNut.ApiModels; -using DotNut.ApiModels.Melt.bolt12; using DotNut.ApiModels.Mint.bolt12; -using DotNut; -namespace DotNut.Abstractions; +namespace DotNut.Abstractions.Handlers; -public class MintHandlerBolt12: IMintHandler> +public class MintHandlerBolt12( + Wallet wallet, + PostMintQuoteBolt12Response quote, + GetKeysResponse.KeysetItemResponse keyset, + List outputs) + : IMintHandler> { - private readonly IWalletBuilder _wallet; - private readonly PostMintQuoteBolt12Response _quote; - private readonly GetKeysResponse.KeysetItemResponse _keyset; - private readonly List _outputs; private string? _signature; - private string? SubscriptionId; - private WebsocketService? _websocketService; - - public MintHandlerBolt12(Wallet wallet, PostMintQuoteBolt12Response quote, GetKeysResponse.KeysetItemResponse keyset, List outputs) - { - this._wallet = wallet; - this._quote = quote; - this._keyset = keyset; - this._outputs = outputs; - } public IMintHandler> WithSignature(string signature) { @@ -39,28 +25,28 @@ public IMintHandler> SignWithPrivkey(st public IMintHandler> SignWithPrivkey(PrivKey privkey) { - this._signature = privkey.SignMintQuote(_quote.Quote, this._outputs.Select(o=>o.BlindedMessage).ToList()); + this._signature = privkey.SignMintQuote(quote.Quote, outputs.Select(o=>o.BlindedMessage).ToList()); return this; } - public async Task GetQuote(CancellationToken ct = default) => this._quote; + public async Task GetQuote(CancellationToken ct = default) => quote; public async Task> Mint(CancellationToken ct = default) { if (this._signature is null) { - throw new ArgumentNullException(nameof(this._signature), $"Signature for mint quote {this._quote.Quote} is required!"); + throw new ArgumentNullException(nameof(this._signature), $"Signature for mint quote {quote.Quote} is required!"); } - var client = await this._wallet.GetMintApi(); + var client = await wallet.GetMintApi(ct); var req = new PostMintRequest { - Outputs = this._outputs.Select(o=>o.BlindedMessage).ToArray(), - Quote = _quote.Quote, + Outputs = outputs.Select(o=>o.BlindedMessage).ToArray(), + Quote = quote.Quote, Signature = _signature, }; var promises= await client.Mint("bolt12", req, ct); - return Utils.ConstructProofsFromPromises(promises.Signatures.ToList(), _outputs, _keyset.Keys); + return Utils.ConstructProofsFromPromises(promises.Signatures.ToList(), outputs, keyset.Keys); } } \ No newline at end of file diff --git a/DotNut/Abstractions/InMemoryCounter.cs b/DotNut/Abstractions/InMemoryCounter.cs index 76c8dc3..31626dd 100644 --- a/DotNut/Abstractions/InMemoryCounter.cs +++ b/DotNut/Abstractions/InMemoryCounter.cs @@ -1,5 +1,3 @@ -using DotNut.Abstractions.Interfaces; - namespace DotNut.Abstractions; public class InMemoryCounter : ICounter diff --git a/DotNut/Abstractions/Interfaces/ICounter.cs b/DotNut/Abstractions/Interfaces/ICounter.cs index 5694225..9e69067 100644 --- a/DotNut/Abstractions/Interfaces/ICounter.cs +++ b/DotNut/Abstractions/Interfaces/ICounter.cs @@ -1,4 +1,4 @@ -namespace DotNut.Abstractions.Interfaces; +namespace DotNut.Abstractions; public interface ICounter { diff --git a/DotNut/Abstractions/Interfaces/IMeltHandler.cs b/DotNut/Abstractions/Interfaces/IMeltHandler.cs index 047ca59..b6b67fd 100644 --- a/DotNut/Abstractions/Interfaces/IMeltHandler.cs +++ b/DotNut/Abstractions/Interfaces/IMeltHandler.cs @@ -1,8 +1,4 @@ -using DotNut.Abstractions.Websockets; -using DotNut.ApiModels; -using DotNut.ApiModels.Melt.bolt12; - -namespace DotNut.Abstractions.Interfaces; +namespace DotNut.Abstractions; public interface IMeltHandler; diff --git a/DotNut/Abstractions/Interfaces/IMeltQuoteBuilder.cs b/DotNut/Abstractions/Interfaces/IMeltQuoteBuilder.cs index 2cfab4e..41a09c6 100644 --- a/DotNut/Abstractions/Interfaces/IMeltQuoteBuilder.cs +++ b/DotNut/Abstractions/Interfaces/IMeltQuoteBuilder.cs @@ -1,7 +1,7 @@ using DotNut.ApiModels; using DotNut.ApiModels.Melt.bolt12; -namespace DotNut.Abstractions.Interfaces; +namespace DotNut.Abstractions; /// /// Melt operation builder (pay invoices) @@ -9,7 +9,7 @@ namespace DotNut.Abstractions.Interfaces; public interface IMeltQuoteBuilder { /// - /// Optional. Sets the base unit for the quote; defaults to sat. + /// Optional. Sets the base unit for the quote; defaults to "sat". /// IMeltQuoteBuilder WithUnit(string unit); @@ -26,7 +26,7 @@ public interface IMeltQuoteBuilder /// /// Optional. Provide private keys for P2PK proofs associated with the inputs. /// - IMeltQuoteBuilder WithPrivkeys(IEnumerable privKeys); + IMeltQuoteBuilder WithPrivKeys(IEnumerable privKeys); /// /// Optional. Supply HTLC preimage to sign HTLC-based proofs. diff --git a/DotNut/Abstractions/Interfaces/IMintQuoteBuilder.cs b/DotNut/Abstractions/Interfaces/IMintQuoteBuilder.cs index 71203d8..feae541 100644 --- a/DotNut/Abstractions/Interfaces/IMintQuoteBuilder.cs +++ b/DotNut/Abstractions/Interfaces/IMintQuoteBuilder.cs @@ -1,7 +1,7 @@ using DotNut.ApiModels; using DotNut.ApiModels.Mint.bolt12; -namespace DotNut.Abstractions.Interfaces; +namespace DotNut.Abstractions; /// /// Mint operation builder (receive from invoice) diff --git a/DotNut/Abstractions/Interfaces/IRestoreBuilder.cs b/DotNut/Abstractions/Interfaces/IRestoreBuilder.cs index d20e538..dd4e4c9 100644 --- a/DotNut/Abstractions/Interfaces/IRestoreBuilder.cs +++ b/DotNut/Abstractions/Interfaces/IRestoreBuilder.cs @@ -1,4 +1,4 @@ -namespace DotNut.Abstractions.Interfaces; +namespace DotNut.Abstractions; /// /// Restore operation builder diff --git a/DotNut/Abstractions/Interfaces/ISwapBuilder.cs b/DotNut/Abstractions/Interfaces/ISwapBuilder.cs index 2a9201a..4400e67 100644 --- a/DotNut/Abstractions/Interfaces/ISwapBuilder.cs +++ b/DotNut/Abstractions/Interfaces/ISwapBuilder.cs @@ -1,4 +1,4 @@ -namespace DotNut.Abstractions.Interfaces; +namespace DotNut.Abstractions; /// /// Swap operation builder diff --git a/DotNut/Abstractions/Interfaces/IWalletBuilder.cs b/DotNut/Abstractions/Interfaces/IWalletBuilder.cs index 242aa3f..fb9a412 100644 --- a/DotNut/Abstractions/Interfaces/IWalletBuilder.cs +++ b/DotNut/Abstractions/Interfaces/IWalletBuilder.cs @@ -1,4 +1,3 @@ -using DotNut.Abstractions.Interfaces; using DotNut.Abstractions.Websockets; using DotNut.Api; using DotNut.ApiModels; diff --git a/DotNut/Abstractions/Interfaces/IWebsocketService.cs b/DotNut/Abstractions/Interfaces/IWebsocketService.cs index 2e498f4..7791036 100644 --- a/DotNut/Abstractions/Interfaces/IWebsocketService.cs +++ b/DotNut/Abstractions/Interfaces/IWebsocketService.cs @@ -1,6 +1,7 @@ using System.Net.WebSockets; +using DotNut.Abstractions.Websockets; -namespace DotNut.Abstractions.Websockets; +namespace DotNut.Abstractions; public interface IWebsocketService : IAsyncDisposable { diff --git a/DotNut/Abstractions/MeltQuoteBuilder.cs b/DotNut/Abstractions/MeltQuoteBuilder.cs index 9117313..1bab438 100644 --- a/DotNut/Abstractions/MeltQuoteBuilder.cs +++ b/DotNut/Abstractions/MeltQuoteBuilder.cs @@ -1,6 +1,4 @@ -using System.Text.Json; using DotNut.Abstractions.Handlers; -using DotNut.Abstractions.Interfaces; using DotNut.ApiModels; using DotNut.ApiModels.Melt.bolt12; @@ -44,7 +42,7 @@ public IMeltQuoteBuilder WithBlankOutputs(List blankOutputs) // when proofs were p2pk - public IMeltQuoteBuilder WithPrivkeys(IEnumerable privKeys) + public IMeltQuoteBuilder WithPrivKeys(IEnumerable privKeys) { this._privKeys = privKeys.ToList(); return this; diff --git a/DotNut/Abstractions/MintInfo.cs b/DotNut/Abstractions/MintInfo.cs index a34d995..28f86de 100644 --- a/DotNut/Abstractions/MintInfo.cs +++ b/DotNut/Abstractions/MintInfo.cs @@ -1,4 +1,3 @@ -using System.Data.Common; using System.Text.Json; using System.Text.Json.Serialization; using DotNut.ApiModels; diff --git a/DotNut/Abstractions/MintQuoteBuilder.cs b/DotNut/Abstractions/MintQuoteBuilder.cs index ce9d158..6c82038 100644 --- a/DotNut/Abstractions/MintQuoteBuilder.cs +++ b/DotNut/Abstractions/MintQuoteBuilder.cs @@ -1,5 +1,4 @@ -using DotNut.Abstractions.Interfaces; -using DotNut.Abstractions.Quotes; +using DotNut.Abstractions.Handlers; using DotNut.Api; using DotNut.ApiModels; using DotNut.ApiModels.Mint.bolt12; diff --git a/DotNut/Abstractions/OutputData.cs b/DotNut/Abstractions/OutputData.cs index 34a093b..75c3399 100644 --- a/DotNut/Abstractions/OutputData.cs +++ b/DotNut/Abstractions/OutputData.cs @@ -1,4 +1,4 @@ -namespace DotNut; +namespace DotNut.Abstractions; public class OutputData { diff --git a/DotNut/Abstractions/ProofSelector.cs b/DotNut/Abstractions/ProofSelector.cs index 3a72eef..9d8dd72 100644 --- a/DotNut/Abstractions/ProofSelector.cs +++ b/DotNut/Abstractions/ProofSelector.cs @@ -1,7 +1,6 @@ using System.Diagnostics; -using DotNut.Abstractions; -namespace DotNut; +namespace DotNut.Abstractions; // Borrowed from cashu-ts // see https://github.com/cashubtc/cashu-ts/pull/314 diff --git a/DotNut/Abstractions/RestoreBuilder.cs b/DotNut/Abstractions/RestoreBuilder.cs index 48799b3..4294659 100644 --- a/DotNut/Abstractions/RestoreBuilder.cs +++ b/DotNut/Abstractions/RestoreBuilder.cs @@ -1,4 +1,3 @@ -using DotNut.Abstractions.Interfaces; using DotNut.ApiModels; using DotNut.NBitcoin.BIP39; using DotNut.NUT13; diff --git a/DotNut/Abstractions/SwapBuilder.cs b/DotNut/Abstractions/SwapBuilder.cs index 85cecf8..5ffa34f 100644 --- a/DotNut/Abstractions/SwapBuilder.cs +++ b/DotNut/Abstractions/SwapBuilder.cs @@ -1,6 +1,4 @@ using System.Security.Cryptography; -using System.Text.Json; -using DotNut.Abstractions.Interfaces; using DotNut.ApiModels; namespace DotNut.Abstractions; diff --git a/DotNut/Abstractions/Utils.cs b/DotNut/Abstractions/Utils.cs index f0ce379..7245317 100644 --- a/DotNut/Abstractions/Utils.cs +++ b/DotNut/Abstractions/Utils.cs @@ -217,7 +217,8 @@ public static Proof ConstructProofFromPromise( BlindSignature promise, PrivKey r, DotNut.ISecret secret, - PubKey amountPubkey) + PubKey amountPubkey, + PubKey? P2PkE = null) { //unblind signature @@ -240,6 +241,7 @@ public static Proof ConstructProofFromPromise( Secret = secret, C = C, DLEQ = promise.DLEQ, + P2PkE = P2PkE }; } @@ -260,7 +262,8 @@ Keyset keys promises[i], outputs[i].BlindingFactor, outputs[i].Secret, - key + key, + outputs[i].P2BkE ); proofs.Add(proof); } diff --git a/DotNut/Abstractions/Wallet.cs b/DotNut/Abstractions/Wallet.cs index a868c18..c8e61fa 100644 --- a/DotNut/Abstractions/Wallet.cs +++ b/DotNut/Abstractions/Wallet.cs @@ -1,8 +1,5 @@ -using DotNut.Abstractions.Interfaces; -using DotNut.Abstractions.Websockets; using DotNut.Api; using DotNut.ApiModels; -using DotNut.ApiModels.Info; using DotNut.NBitcoin.BIP39; using NBitcoin.Secp256k1; diff --git a/DotNut/ApiModels/Melt/bolt12/PostMeltQuoteBolt12Response.cs b/DotNut/ApiModels/Melt/bolt12/PostMeltQuoteBolt12Response.cs index 905f6d5..7a4eaf2 100644 --- a/DotNut/ApiModels/Melt/bolt12/PostMeltQuoteBolt12Response.cs +++ b/DotNut/ApiModels/Melt/bolt12/PostMeltQuoteBolt12Response.cs @@ -19,9 +19,9 @@ public class PostMeltQuoteBolt12Response [JsonPropertyName("expiry")] public int Expiry { get; set; } [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - [JsonPropertyName("payment_preimage")] public string PaymentPreimage { get; set; } + [JsonPropertyName("payment_preimage")] public string? PaymentPreimage { get; set; } [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - [JsonPropertyName("change")] public BlindSignature[] Change { get; set; } + [JsonPropertyName("change")] public BlindSignature[]? Change { get; set; } } \ No newline at end of file diff --git a/DotNut/NUT13/Nut13.cs b/DotNut/NUT13/Nut13.cs index 1332ff9..0337ba6 100644 --- a/DotNut/NUT13/Nut13.cs +++ b/DotNut/NUT13/Nut13.cs @@ -1,4 +1,5 @@ -using System.Security.Cryptography; +using System.Security.Cryptography; +using DotNut.Abstractions; using DotNut.NBitcoin.BIP39; using NBip32Fast; From 267a9315a1806e9982e6e800fc80624eacf7bdf4 Mon Sep 17 00:00:00 2001 From: d4rp4t Date: Fri, 28 Nov 2025 00:33:24 +0100 Subject: [PATCH 24/70] Fix p2bk in abstraction layer. P2PkBuilder state is mutated after blinding so make sure it's cloned while iterating on it --- DotNut.Tests/Integration.cs | 172 +++++++++++++----- .../Interfaces/IMintQuoteBuilder.cs | 7 + .../Abstractions/Interfaces/ISwapBuilder.cs | 7 + DotNut/Abstractions/MintQuoteBuilder.cs | 37 +++- DotNut/Abstractions/Nut10Helper.cs | 11 +- DotNut/Abstractions/SwapBuilder.cs | 11 +- DotNut/NUT11/P2PkBuilder.cs | 13 ++ DotNut/NUT14/HTLCBuilder.cs | 14 ++ DotNut/NUT14/HTLCProofSecret.cs | 41 ++--- 9 files changed, 229 insertions(+), 84 deletions(-) diff --git a/DotNut.Tests/Integration.cs b/DotNut.Tests/Integration.cs index 3a38d25..523a37b 100644 --- a/DotNut.Tests/Integration.cs +++ b/DotNut.Tests/Integration.cs @@ -17,6 +17,7 @@ public class Integration { {500, "lnbc5u1p5sh0yvsp53seej3qkkxe6xxk9mufaj7y3jc9s9kvfn4g3whppwqcl4vcjraaspp5vtv793xc9ksch8zekkhqtv54a2evh7vq4zuywcmk9nzt69qma5yqhp5uwcvgs5clswpfxhm7nyfjmaeysn6us0yvjdexn9yjkv3k7zjhp2sxq9z0rgqcqpnrzjq0ly0l075re9ltgqzdycartvas6g4c7kwwzpasj7a98c0ss679hdsr080vqqdcgqqqqqqqqnqqqqryqqxv9qxpqysgqwq50283v8asna95fktaeg80kq9evs0chaw44y6y649qsql9vsfc5gfcsp8rdwwyccepwy83n7g0s25n3lpv3hjgcr220n5w806fja8gp2xjvd7"}, {501, "lnbc5010n1p5shs9rsp5a2qhmn05xsd8vcm5jx9v2aswkz0pxguk4jqlaxsazzcg5rduan2qpp5al2k5zwruvlx34sxxdys2sj696m58uqgjvzxxrxhvuyswhmzg5cqhp5uwcvgs5clswpfxhm7nyfjmaeysn6us0yvjdexn9yjkv3k7zjhp2sxq9z0rgqcqpnrzjqw7c9dkkx4nur9sztw2zzpzj8u8rgsqgdsykylg5pwplh26824lc7rvlqcqqn3gqqyqqqqlgqqqqqqgq2q9qxpqysgqgpj2x2aw2dv5tzhx86th6a5vutpcdxz9htewqgvzjgqkzwmh6xs5mw5xcgrzyq77f35shv0gg5ygtjmn7e73wg8v0a9g836ufszdxmqqqu3642"}, + {502, "lnbc5020n1p5j3nxasp5qz7utfrp954nxp8049tqzg0t23krdj59thfcrc2g5h6lsemzvyfqpp5ms6xd7grtak0nr8lwytsclmq3d233v7gy7j0kuw32txhjq0f8ngqhp5uwcvgs5clswpfxhm7nyfjmaeysn6us0yvjdexn9yjkv3k7zjhp2sxq9z0rgqcqpnrzjqg587a2yyuqeua9c3j8nw7wwpx709slwl5lzfs0t0vq3kdwemzp67rtwevqq95gqqyqqq5sgqqq9yzqq2q9qxpqysgqgpp0zsetj9fedvr0szpwjfw2weckygmjthhnfpp2cerjtrn8n0pxyvrtc00l0jwzkqhwedcvgqljtwx3a7qplqp43jlxe4mpmw5svlgqfwa9yy"}, {1000, "lnbc10u1p5w6vggsp5gn5xhswgn5299w6elu2z0vzjxhf9hwd6pwjcgfwphaxunyu0dx6spp5a60trrhce2u6tzqjwjczem8rpdesgzkawcqg2xqaesz2kd50z4uqhp5uwcvgs5clswpfxhm7nyfjmaeysn6us0yvjdexn9yjkv3k7zjhp2sxq9z0rgqcqpnrzjqw22kc09xj0dm65ew5h5r003vtn72eyzchdgjag66l0yhwdudfmuzrwvesqq8qgqqgqqqqqqqqqqzhsq2q9qxpqysgq955gcfr95wwz0ehtnk3xraatkyhj88z44ku7yqutnwnt3gkh82jxehvdff7n2js2p54jgpvg6dmwmq8t9d8x05j63mqjrsr4cwd4lpcpnc39ru"}, {2000, "lnbc20u1p5094fksp54vrdcymel5awhrpc0m6z4kvhhyvqlwkshkyt2wr6eyljkz8c798qpp59f2vc8td8tu62gtf4qfwzkrkxedsey7a5ajrd48a25z2kkwg407shp5nklhn663zgwcdnh7pe5jxt6td0cchhre6hxzdxrjdlfwtpq60f5sxq9z0rgqcqpnrzjqw0de9yc0j8n4hpgm269tm7qph4gwcyf5ys02uaapvpugrva87c7zr045uqq4jsqpsqqqqlgqqqqrcgq2q9qxpqysgq6g2pamgjumh6uw5k5rj2ket44wh8nfzs5gzyygl54hu5cefuxdhxp9h5mrg64rh07znktn9x9d5vg6fc0rw7m63x8rg4qk3kw6d8sycpywn48m"}, }; @@ -342,54 +343,129 @@ await Assert.ThrowsAsync(async () => Assert.NotEmpty(change); } - //TODO: CDK MINTD HAS AN ISSUE WITH HTLC SECRETS GENERATED IN DOTNUT - UNCOMMENT IN FUTURE - // [Fact] - // public async Task MintSwapHTLC() - // { - // var wallet = Wallet - // .Create() - // .WithMint(MintUrl); - // - // var privKeyBob = new PrivKey(RandomNumberGenerator.GetHexString(64, true)); - // var preimage = "0000000000000000000000000000000000000000000000000000000000000001"; - // var hashLock = Convert.ToHexString(SHA256.HashData(Convert.FromHexString(preimage))); - // - // var mintHandler = await wallet.CreateMintQuote() - // .WithAmount(1337) - // .WithHTLCLock(new HTLCBuilder() - // { - // HashLock = hashLock, - // Pubkeys = [privKeyBob.Key.CreatePubKey()], - // SignatureThreshold = 1 - // }) - // .ProcessAsyncBolt11(); - // - // await PayInvoice(); - // var htlcProofs = await mintHandler.Mint(); - // - // Assert.NotEmpty(htlcProofs); - // Assert.Equal(1337UL, Utils.SumProofs(htlcProofs)); - // - // - // // try swap without preimage - should fail - // await Assert.ThrowsAsync(async () => - // { - // await wallet.Swap() - // .FromInputs(htlcProofs) - // .WithPrivkeys([privKeyBob]) - // .ProcessAsync(); - // }); - // - // // swap with correct preimage and signature - // var swappedProofs = await wallet.Swap() - // .FromInputs(htlcProofs) - // .WithPrivkeys([privKeyBob]) - // .WithHtlcPreimage(preimage) - // .ProcessAsync(); - // - // Assert.NotEmpty(swappedProofs); - // Assert.Equal(1337UL, Utils.SumProofs(swappedProofs)); - // } + [Fact] + public async Task MintSwapHTLC() + { + var wallet = Wallet + .Create() + .WithMint(MintUrl); + + var privKeyBob = new PrivKey(RandomNumberGenerator.GetHexString(64, true)); + var preimage = "0000000000000000000000000000000000000000000000000000000000000001"; + var hashLock = Convert.ToHexString(SHA256.HashData(Convert.FromHexString(preimage))); + + var mintHandler = await wallet.CreateMintQuote() + .WithAmount(1337) + .WithHTLCLock(new HTLCBuilder() + { + HashLock = hashLock, + Pubkeys = [privKeyBob.Key.CreatePubKey()], + SignatureThreshold = 1 + }) + .ProcessAsyncBolt11(); + + await PayInvoice(); + var htlcProofs = await mintHandler.Mint(); + + Assert.NotEmpty(htlcProofs); + Assert.Equal(1337UL, Utils.SumProofs(htlcProofs)); + + + // try swap without preimage - should fail + await Assert.ThrowsAsync(async () => + { + await wallet.Swap() + .FromInputs(htlcProofs) + .WithPrivkeys([privKeyBob]) + .ProcessAsync(); + }); + + // swap with correct preimage and signature + var swappedProofs = await wallet.Swap() + .FromInputs(htlcProofs) + .WithPrivkeys([privKeyBob]) + .WithHtlcPreimage(preimage) + .ProcessAsync(); + + Assert.NotEmpty(swappedProofs); + // fee is 100 ppk - it can be calculated before but here we don't care + Assert.Equal(1337UL - 1, Utils.SumProofs(swappedProofs)); + } + + [Fact] + public async Task MintMeltP2Bk() + { + var wallet = Wallet + .Create() + .WithMint(MintUrl); + + var privKeyBob = new PrivKey(RandomNumberGenerator.GetHexString(64, true)); + + var builder = new P2PkBuilder() + { + Pubkeys = [privKeyBob.Key.CreatePubKey()], + }; + + var quote = await wallet + .CreateMintQuote() + .WithAmount(1337) + .WithP2PkLock(builder) + .BlindPubkeys() + .ProcessAsyncBolt11(); + + await PayInvoice(); + var proofs = await quote.Mint(); + + Assert.NotEmpty(proofs); + Assert.NotEmpty(proofs.Select(p=>p.P2PkE)); + + var meltHandler = await wallet + .CreateMeltQuote() + .WithInvoice(valuesInvoices[502]) + .WithPrivKeys([privKeyBob]) + .ProcessAsyncBolt11(); + + var change = await meltHandler.Melt(proofs); + + Assert.NotEmpty(change); + } + + [Fact] + public async Task MintSwapP2Bk() + { + var wallet = Wallet + .Create() + .WithMint(MintUrl); + + var privKeyBob = new PrivKey(RandomNumberGenerator.GetHexString(64, true)); + var privKeyAlice = new PrivKey(RandomNumberGenerator.GetHexString(64, true)); + + var builder = new P2PkBuilder() + { + Pubkeys = [privKeyBob.Key.CreatePubKey(), privKeyAlice.Key.CreatePubKey()], + }; + + var quote = await wallet + .CreateMintQuote() + .WithAmount(1337) + .WithP2PkLock(builder) + .BlindPubkeys() + .ProcessAsyncBolt11(); + + await PayInvoice(); + var proofs = await quote.Mint(); + + Assert.NotEmpty(proofs); + Assert.NotEmpty(proofs.Select(p=>p.P2PkE)); + + var newProofs = await wallet + .Swap() + .FromInputs(proofs) + .WithPrivkeys([privKeyBob, privKeyAlice]) + .ProcessAsync(); + + Assert.NotEmpty(newProofs); + } [Fact] public async Task SwapWithCustomAmounts() diff --git a/DotNut/Abstractions/Interfaces/IMintQuoteBuilder.cs b/DotNut/Abstractions/Interfaces/IMintQuoteBuilder.cs index feae541..3ff398f 100644 --- a/DotNut/Abstractions/Interfaces/IMintQuoteBuilder.cs +++ b/DotNut/Abstractions/Interfaces/IMintQuoteBuilder.cs @@ -32,6 +32,13 @@ public interface IMintQuoteBuilder /// Optional. Allows providing a P2PK builder when a signature is required for minting. /// IMintQuoteBuilder WithP2PkLock(P2PkBuilder p2pkBuilder); + + /// + /// Optional. When minting P2Pk / HTLC Proofs allows to blind the pubkeys. + /// + /// + /// + IMintQuoteBuilder BlindPubkeys(bool withBlinding = true); /// /// Optional. Allows adding HTLC-based outputs. diff --git a/DotNut/Abstractions/Interfaces/ISwapBuilder.cs b/DotNut/Abstractions/Interfaces/ISwapBuilder.cs index 4400e67..72fd55b 100644 --- a/DotNut/Abstractions/Interfaces/ISwapBuilder.cs +++ b/DotNut/Abstractions/Interfaces/ISwapBuilder.cs @@ -50,6 +50,13 @@ public interface ISwapBuilder /// ISwapBuilder ToP2PK(P2PkBuilder p2pkBuilder); + /// + /// Optional. Blind P2Pk / HTLC proofs. + /// + /// + /// + ISwapBuilder BlindPubkeys(bool withBlinding = true); + /// /// Optional. Supply preimage for HTLC-based proofs. /// diff --git a/DotNut/Abstractions/MintQuoteBuilder.cs b/DotNut/Abstractions/MintQuoteBuilder.cs index 6c82038..e76f01b 100644 --- a/DotNut/Abstractions/MintQuoteBuilder.cs +++ b/DotNut/Abstractions/MintQuoteBuilder.cs @@ -1,3 +1,6 @@ +using System.Collections.Immutable; +using System.Reflection.Metadata; +using System.Security.Cryptography; using DotNut.Abstractions.Handlers; using DotNut.Api; using DotNut.ApiModels; @@ -23,6 +26,7 @@ class MintQuoteBuilder : IMintQuoteBuilder //for p2pk private P2PkBuilder? _builder; + private bool _shouldBlind = false; public MintQuoteBuilder(Wallet wallet) { @@ -78,6 +82,12 @@ public IMintQuoteBuilder WithP2PkLock(P2PkBuilder p2pkBuilder) return this; } + public IMintQuoteBuilder BlindPubkeys(bool withBlinding = true) + { + this._shouldBlind = withBlinding; + return this; + } + public IMintQuoteBuilder WithHTLCLock(HTLCBuilder htlcBuilder) { this._builder = htlcBuilder; @@ -187,13 +197,36 @@ async Task> _createOutputs() return await _wallet.CreateOutputs(_amounts, this._keysetId!); } + if (this._shouldBlind) + { + if (this._builder.SigFlag == "SIG_ALL") + { + var e = new PrivKey(RandomNumberGenerator.GetHexString(64)); + foreach (var amount in _amounts) + { + var builder = _builder.Clone(); + var p2pkOutput = Utils.CreateNut10BlindedOutput(amount, this._keysetId!, builder, e); + outputs.Add(p2pkOutput); + } + + return outputs; + } + + foreach (var amount in _amounts) + { + var builder = _builder.Clone(); + var p2pkOutput = Utils.CreateNut10BlindedOutput(amount, this._keysetId!, builder); + outputs.Add(p2pkOutput); + } + return outputs; + } foreach (var amount in _amounts) { - var p2pkOutput = Utils.CreateNut10Output(amount, this._keysetId!, _builder); + var builder = _builder.Clone(); + var p2pkOutput = Utils.CreateNut10Output(amount, this._keysetId!, builder); outputs.Add(p2pkOutput); } return outputs; - } } \ No newline at end of file diff --git a/DotNut/Abstractions/Nut10Helper.cs b/DotNut/Abstractions/Nut10Helper.cs index cb543c7..1fc1b30 100644 --- a/DotNut/Abstractions/Nut10Helper.cs +++ b/DotNut/Abstractions/Nut10Helper.cs @@ -3,7 +3,7 @@ namespace DotNut.Abstractions; -public class Nut10Helper +public static class Nut10Helper { public static void MaybeProcessNut10( List privKeys, @@ -50,8 +50,10 @@ public static void MaybeProcessNut10( private static void handleWitnessCreation(Proof proof, ECPrivKey[] keys, string? htlcPreimage) { - if (proof.Secret is Nut10Secret { ProofSecret: HTLCProofSecret htlc } && htlcPreimage is { } preimage) + if (proof.Secret is Nut10Secret { ProofSecret: HTLCProofSecret htlc }) { + // preimage isn't verified after timelock + var preimage = htlcPreimage??""; if (proof.P2PkE is { } E) { var blindwitness = htlc.GenerateBlindWitness(proof, keys, preimage); @@ -67,6 +69,7 @@ private static void handleWitnessCreation(Proof proof, ECPrivKey[] keys, string? { if (proof.P2PkE is { } E) { + Console.WriteLine(); var blindWitness = p2pk.GenerateBlindWitness(proof, keys); proof.Witness = JsonSerializer.Serialize(blindWitness); return; @@ -75,4 +78,8 @@ private static void handleWitnessCreation(Proof proof, ECPrivKey[] keys, string? proof.Witness = JsonSerializer.Serialize(proofWitness); } } + + + + } \ No newline at end of file diff --git a/DotNut/Abstractions/SwapBuilder.cs b/DotNut/Abstractions/SwapBuilder.cs index 5ffa34f..3af1e98 100644 --- a/DotNut/Abstractions/SwapBuilder.cs +++ b/DotNut/Abstractions/SwapBuilder.cs @@ -113,7 +113,7 @@ public ISwapBuilder ToHTLC(HTLCBuilder htlcBuilder) } // P2Bk should be compatible with both p2pk and HTLC. Not implemented in the second one - public ISwapBuilder ToP2Bk(bool withBlinding = true) + public ISwapBuilder BlindPubkeys(bool withBlinding = true) { this._shouldBlind = true; return this; @@ -244,20 +244,23 @@ async Task> _getOutputs(Keyset keys, CancellationToken ct = def var e = new PrivKey(RandomNumberGenerator.GetHexString(64)); foreach (var amount in _amounts) { - outputs.Add(Utils.CreateNut10BlindedOutput(amount, this._keysetId!, _builder, e)); + var builder = _builder.Clone(); + outputs.Add(Utils.CreateNut10BlindedOutput(amount, this._keysetId!, builder, e)); } return outputs; } foreach (var amount in _amounts) { - outputs.Add(Utils.CreateNut10BlindedOutput(amount, this._keysetId!, _builder)); + var builder = _builder.Clone(); + outputs.Add(Utils.CreateNut10BlindedOutput(amount, this._keysetId!, builder)); } return outputs; } // skipped checks for keysetid and keys, since its validated before. make sure to remember about it. foreach (var amount in _amounts) { - outputs.Add(Utils.CreateNut10Output(amount, this._keysetId!, _builder)); + var builder = _builder.Clone(); + outputs.Add(Utils.CreateNut10Output(amount, this._keysetId!, builder)); } return outputs; } diff --git a/DotNut/NUT11/P2PkBuilder.cs b/DotNut/NUT11/P2PkBuilder.cs index eeebbbb..edd9110 100644 --- a/DotNut/NUT11/P2PkBuilder.cs +++ b/DotNut/NUT11/P2PkBuilder.cs @@ -177,4 +177,17 @@ private void _blindPubkeys(ECPrivKey[] rs) } } } + + public virtual P2PkBuilder Clone() + { + return new P2PkBuilder() + { + Lock = Lock, + RefundPubkeys = RefundPubkeys?.ToArray(), + SignatureThreshold = SignatureThreshold, + Pubkeys = Pubkeys.ToArray(), + SigFlag = SigFlag, + Nonce = Nonce, + }; + } } \ No newline at end of file diff --git a/DotNut/NUT14/HTLCBuilder.cs b/DotNut/NUT14/HTLCBuilder.cs index f236a71..4938ec1 100644 --- a/DotNut/NUT14/HTLCBuilder.cs +++ b/DotNut/NUT14/HTLCBuilder.cs @@ -79,4 +79,18 @@ public HTLCProofSecret BuildBlinded(KeysetId keysetId, ECPrivKey p2pke) { throw new NotImplementedException(); } + + public override HTLCBuilder Clone() + { + return new HTLCBuilder() + { + HashLock = HashLock, + Lock = Lock, + RefundPubkeys = RefundPubkeys?.ToArray(), + SignatureThreshold = SignatureThreshold, + Pubkeys = Pubkeys.ToArray(), + SigFlag = SigFlag, + Nonce = Nonce, + }; + } } \ No newline at end of file diff --git a/DotNut/NUT14/HTLCProofSecret.cs b/DotNut/NUT14/HTLCProofSecret.cs index 713885a..ee7fb85 100644 --- a/DotNut/NUT14/HTLCProofSecret.cs +++ b/DotNut/NUT14/HTLCProofSecret.cs @@ -18,26 +18,6 @@ public override ECPubKey[] GetAllowedPubkeys(out int requiredSignatures) return builder.Pubkeys; } - public override ECPubKey[] GetAllowedRefundPubkeys(out int? requiredSignatures) - { - var builder = Builder; - if (builder.Lock.HasValue && builder.Lock.Value.ToUnixTimeSeconds() < DateTimeOffset.Now.ToUnixTimeSeconds()) - { - if (builder.RefundPubkeys == null) - { - requiredSignatures = 0; // proof is spendable without any signature - return []; - } - requiredSignatures = builder.RefundSignatureThreshold ?? 1; - return [..builder.RefundPubkeys??[]]; - } - - requiredSignatures = null; // there's no refund condition :/ - return []; - } - - - public HTLCWitness GenerateWitness(Proof proof, ECPrivKey[] keys, string preimage) { return GenerateWitness(proof.Secret.GetBytes(), keys, Convert.FromHexString(preimage)); @@ -56,12 +36,17 @@ public HTLCWitness GenerateWitness(byte[] msg, ECPrivKey[] keys, byte[] preimage public HTLCWitness GenerateWitness(ECPrivKey hash, ECPrivKey[] keys, byte[] preimage) { - if (!VerifyPreimage(preimage)) - throw new InvalidOperationException("Invalid preimage"); - var p2pkhWitness = base.GenerateWitness(hash, keys); + // validate hash only if there' + var builder = Builder; + if (!builder.Lock.HasValue || builder.Lock.Value.ToUnixTimeSeconds() > DateTimeOffset.Now.ToUnixTimeSeconds()) + { + if (!VerifyPreimage(preimage)) + throw new InvalidOperationException("Invalid preimage"); + } + var witness = base.GenerateWitness(hash, keys); return new HTLCWitness() { - Signatures = p2pkhWitness.Signatures, + Signatures = witness.Signatures, Preimage = Convert.ToHexString(preimage) }; } @@ -199,11 +184,11 @@ public override bool VerifyWitnessHash(byte[] hash, P2PKWitness witness) { return false; } - if (!VerifyPreimage(htlcWitness.Preimage)) + var builder = Builder; + if (builder.Lock.HasValue && builder.Lock.Value.ToUnixTimeSeconds() <= DateTimeOffset.Now.ToUnixTimeSeconds()) { - return false; + return base.VerifyWitnessHash(hash, witness); } - - return base.VerifyWitnessHash(hash, witness); + return VerifyPreimage(htlcWitness.Preimage) && base.VerifyWitnessHash(hash, witness); } } \ No newline at end of file From 2c5f72903cddf283bde8c9834fdd73a69252bbd1 Mon Sep 17 00:00:00 2001 From: d4rp4t Date: Fri, 28 Nov 2025 00:55:29 +0100 Subject: [PATCH 25/70] tests and bugs --- DotNut.Tests/Integration.cs | 28 ++++++++++++++++++- .../Interfaces/IMintQuoteBuilder.cs | 11 ++++++++ DotNut/Abstractions/MintQuoteBuilder.cs | 9 ++++++ DotNut/Abstractions/Wallet.cs | 24 ---------------- 4 files changed, 47 insertions(+), 25 deletions(-) diff --git a/DotNut.Tests/Integration.cs b/DotNut.Tests/Integration.cs index 523a37b..4d70039 100644 --- a/DotNut.Tests/Integration.cs +++ b/DotNut.Tests/Integration.cs @@ -18,6 +18,7 @@ public class Integration {500, "lnbc5u1p5sh0yvsp53seej3qkkxe6xxk9mufaj7y3jc9s9kvfn4g3whppwqcl4vcjraaspp5vtv793xc9ksch8zekkhqtv54a2evh7vq4zuywcmk9nzt69qma5yqhp5uwcvgs5clswpfxhm7nyfjmaeysn6us0yvjdexn9yjkv3k7zjhp2sxq9z0rgqcqpnrzjq0ly0l075re9ltgqzdycartvas6g4c7kwwzpasj7a98c0ss679hdsr080vqqdcgqqqqqqqqnqqqqryqqxv9qxpqysgqwq50283v8asna95fktaeg80kq9evs0chaw44y6y649qsql9vsfc5gfcsp8rdwwyccepwy83n7g0s25n3lpv3hjgcr220n5w806fja8gp2xjvd7"}, {501, "lnbc5010n1p5shs9rsp5a2qhmn05xsd8vcm5jx9v2aswkz0pxguk4jqlaxsazzcg5rduan2qpp5al2k5zwruvlx34sxxdys2sj696m58uqgjvzxxrxhvuyswhmzg5cqhp5uwcvgs5clswpfxhm7nyfjmaeysn6us0yvjdexn9yjkv3k7zjhp2sxq9z0rgqcqpnrzjqw7c9dkkx4nur9sztw2zzpzj8u8rgsqgdsykylg5pwplh26824lc7rvlqcqqn3gqqyqqqqlgqqqqqqgq2q9qxpqysgqgpj2x2aw2dv5tzhx86th6a5vutpcdxz9htewqgvzjgqkzwmh6xs5mw5xcgrzyq77f35shv0gg5ygtjmn7e73wg8v0a9g836ufszdxmqqqu3642"}, {502, "lnbc5020n1p5j3nxasp5qz7utfrp954nxp8049tqzg0t23krdj59thfcrc2g5h6lsemzvyfqpp5ms6xd7grtak0nr8lwytsclmq3d233v7gy7j0kuw32txhjq0f8ngqhp5uwcvgs5clswpfxhm7nyfjmaeysn6us0yvjdexn9yjkv3k7zjhp2sxq9z0rgqcqpnrzjqg587a2yyuqeua9c3j8nw7wwpx709slwl5lzfs0t0vq3kdwemzp67rtwevqq95gqqyqqq5sgqqq9yzqq2q9qxpqysgqgpp0zsetj9fedvr0szpwjfw2weckygmjthhnfpp2cerjtrn8n0pxyvrtc00l0jwzkqhwedcvgqljtwx3a7qplqp43jlxe4mpmw5svlgqfwa9yy"}, + {999, "lnbc9990n1p5j3cf7sp575w4pw93kfrghl2gh68885v76gwjpzuv435t52q846cvx4w7yuvqpp5hdzvm3yf0r3vj99c7esmcv7zuj2fralf2twhl6s9xqcgr8g7nwyqhp5uwcvgs5clswpfxhm7nyfjmaeysn6us0yvjdexn9yjkv3k7zjhp2sxq9z0rgqcqpnrzjqt0mfswatysklf4z358sztscs5t0vdghmd5vfe9c9sa0gy6r5pdugrs7myqqvgqqqyqqqqqqqqqq86qq8s9qxpqysgq5wh9l4fy32ww4770mqm7yqvhwllaqyssvp335gjz6t59ca03gecyvdd9uv0ztrcm2uf2352wvwxcfh7yukucp4p6zu6ll867aj686wsqz0jlmt"}, {1000, "lnbc10u1p5w6vggsp5gn5xhswgn5299w6elu2z0vzjxhf9hwd6pwjcgfwphaxunyu0dx6spp5a60trrhce2u6tzqjwjczem8rpdesgzkawcqg2xqaesz2kd50z4uqhp5uwcvgs5clswpfxhm7nyfjmaeysn6us0yvjdexn9yjkv3k7zjhp2sxq9z0rgqcqpnrzjqw22kc09xj0dm65ew5h5r003vtn72eyzchdgjag66l0yhwdudfmuzrwvesqq8qgqqgqqqqqqqqqqzhsq2q9qxpqysgq955gcfr95wwz0ehtnk3xraatkyhj88z44ku7yqutnwnt3gkh82jxehvdff7n2js2p54jgpvg6dmwmq8t9d8x05j63mqjrsr4cwd4lpcpnc39ru"}, {2000, "lnbc20u1p5094fksp54vrdcymel5awhrpc0m6z4kvhhyvqlwkshkyt2wr6eyljkz8c798qpp59f2vc8td8tu62gtf4qfwzkrkxedsey7a5ajrd48a25z2kkwg407shp5nklhn663zgwcdnh7pe5jxt6td0cchhre6hxzdxrjdlfwtpq60f5sxq9z0rgqcqpnrzjqw0de9yc0j8n4hpgm269tm7qph4gwcyf5ys02uaapvpugrva87c7zr045uqq4jsqpsqqqqlgqqqqrcgq2q9qxpqysgq6g2pamgjumh6uw5k5rj2ket44wh8nfzs5gzyygl54hu5cefuxdhxp9h5mrg64rh07znktn9x9d5vg6fc0rw7m63x8rg4qk3kw6d8sycpywn48m"}, }; @@ -72,6 +73,31 @@ public async Task MintsSuccessfully() Assert.Equal(1337UL, Utils.SumProofs(mintResponse)); } + [Fact] + public async Task MintsBolt12Successfully() + { + var wallet = Wallet.Create().WithMint(MintUrl); + var privkey = new PrivKey(RandomNumberGenerator.GetHexString(64)); + + var mintQuote = await wallet + .CreateMintQuote() + .WithPubkey(privkey.Key.CreatePubKey()) + .WithUnit("sat") + .WithAmount(1337) + .ProcessAsyncBolt12(); + + Assert.NotNull(mintQuote); + + var paymentRequest = (await mintQuote.GetQuote()).Request; + Assert.NotNull(paymentRequest); + mintQuote.SignWithPrivkey(privkey); + + await PayInvoice(); + var mintResponse = await mintQuote.Mint(); + Assert.NotNull(mintResponse); + Assert.Equal(1337UL, Utils.SumProofs(mintResponse)); + } + [Fact] public async Task MintsDeterministicSuccessfully() { @@ -198,7 +224,7 @@ public async Task MeltsSuccessfully() // create melt quote var meltQuote = await wallet .CreateMeltQuote() - .WithInvoice(valuesInvoices[1000]) + .WithInvoice(valuesInvoices[999]) .WithUnit("sat") .ProcessAsyncBolt11(); diff --git a/DotNut/Abstractions/Interfaces/IMintQuoteBuilder.cs b/DotNut/Abstractions/Interfaces/IMintQuoteBuilder.cs index 3ff398f..04c47b5 100644 --- a/DotNut/Abstractions/Interfaces/IMintQuoteBuilder.cs +++ b/DotNut/Abstractions/Interfaces/IMintQuoteBuilder.cs @@ -18,6 +18,17 @@ public interface IMintQuoteBuilder /// IMintQuoteBuilder WithAmount(ulong amount); + /// + /// Optional for bolt11 and mandatory for bolt12. + /// + /// + /// + IMintQuoteBuilder WithPubkey(string pubkey); + /// + /// Optional for bolt11 and mandatory for bolt12. + /// + IMintQuoteBuilder WithPubkey(PubKey pubkey); + /// /// Optional. Provide precomputed outputs so blinding factors and secrets are reused safely. /// diff --git a/DotNut/Abstractions/MintQuoteBuilder.cs b/DotNut/Abstractions/MintQuoteBuilder.cs index e76f01b..6e6d63d 100644 --- a/DotNut/Abstractions/MintQuoteBuilder.cs +++ b/DotNut/Abstractions/MintQuoteBuilder.cs @@ -63,6 +63,12 @@ public IMintQuoteBuilder WithPubkey(string pubkey) return this; } + public IMintQuoteBuilder WithPubkey(PubKey pubkey) + { + this._pubkey = pubkey.ToString(); + return this; + } + public IMintQuoteBuilder WithKeyset(KeysetId keysetId) { this._keysetId = keysetId; @@ -148,6 +154,9 @@ public async Task>> Proces throw new ArgumentNullException(nameof(_pubkey), "Can't request bolt12 mint quote without pubkey!"); } + this._keysetId ??= await this._wallet.GetActiveKeysetId(this._unit, ct) ?? + throw new ArgumentException($"Can't get active keyset ID for unit: {_unit}"); + if (this._keyset == null) { this._keyset = await this._wallet.GetKeys(this._keysetId, false, ct) ?? diff --git a/DotNut/Abstractions/Wallet.cs b/DotNut/Abstractions/Wallet.cs index c8e61fa..6b6e626 100644 --- a/DotNut/Abstractions/Wallet.cs +++ b/DotNut/Abstractions/Wallet.cs @@ -105,48 +105,24 @@ public IWalletBuilder WithMnemonic(string mnemonic) return this; } - /// - /// Optional and mandatory if Mnemonic provided. Counter for each Keyset Id for derivation purposes. - /// - /// Counter object public IWalletBuilder WithCounter(ICounter counter) { this._counter = counter; return this; } - /// - /// Optional and mandatory if Mnemonic provided. Counter for each Keyset Id for derivation purposes. - /// - /// Counter dictionary - /// public IWalletBuilder WithCounter(IDictionary counter) { this._counter = new InMemoryCounter(counter); return this; } - /// - /// Optional and if not set, always true. Controls automatic counter incrementation for secret generation. - /// - /// If true, counter increments automatically. If false, requires manual management. - /// - /// WARNING: Disabling auto-increment is potentially dangerous. Manual counter management is required - /// to prevent secret reuse, which will cause mint rejection and operation failures. - /// public IWalletBuilder ShouldBumpCounter(bool shouldBumpCounter = true) { this._shouldBumpCounter = shouldBumpCounter; return this; } - /// - /// Optional. - /// Adds websocket service. You should use single websocket service (singleton at best) for multiple wallets, in order to handle everything in nice manner. - /// If not set, but requested it'll be created automatically (which won't be so optimal). - /// - /// - /// public IWalletBuilder WithWebsocketService(IWebsocketService websocketService) { this._wsService = websocketService; From 8c10a1fd1751d3782993cdd087c2dc20c15454c5 Mon Sep 17 00:00:00 2001 From: d4rp4t Date: Fri, 28 Nov 2025 13:04:43 +0100 Subject: [PATCH 26/70] try bump timeout --- DotNut.Tests/Integration.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/DotNut.Tests/Integration.cs b/DotNut.Tests/Integration.cs index 4d70039..b00d973 100644 --- a/DotNut.Tests/Integration.cs +++ b/DotNut.Tests/Integration.cs @@ -613,7 +613,7 @@ public async Task SubscribeToMintMeltQuoteUpdates() int connectedCount = 0; int notificationCount = 0; - using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(60)); + using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(120)); var connectedTcs = new TaskCompletionSource(); var paidTcs = new TaskCompletionSource(); From ee05ea83f77a9a5a21c45607c6d30e9609698a29 Mon Sep 17 00:00:00 2001 From: d4rp4t Date: Fri, 28 Nov 2025 22:23:08 +0100 Subject: [PATCH 27/70] p2bk htlc --- DotNut.Tests/Integration.cs | 93 +++++++++++++++++++ DotNut.Tests/UnitTest1.cs | 6 +- DotNut/Abstractions/MeltQuoteBuilder.cs | 6 +- DotNut/Abstractions/Nut10Helper.cs | 4 - .../Melt/bolt12/PostMeltQuoteBolt12Request.cs | 4 +- DotNut/NUT11/P2PKProofSecret.cs | 2 +- DotNut/NUT11/P2PkBuilder.cs | 6 +- DotNut/NUT14/HTLCBuilder.cs | 36 +++++-- DotNut/NUT14/HTLCProofSecret.cs | 26 ++++-- DotNut/SigAllHandler.cs | 4 +- 10 files changed, 153 insertions(+), 34 deletions(-) diff --git a/DotNut.Tests/Integration.cs b/DotNut.Tests/Integration.cs index b00d973..1cd0999 100644 --- a/DotNut.Tests/Integration.cs +++ b/DotNut.Tests/Integration.cs @@ -20,8 +20,17 @@ public class Integration {502, "lnbc5020n1p5j3nxasp5qz7utfrp954nxp8049tqzg0t23krdj59thfcrc2g5h6lsemzvyfqpp5ms6xd7grtak0nr8lwytsclmq3d233v7gy7j0kuw32txhjq0f8ngqhp5uwcvgs5clswpfxhm7nyfjmaeysn6us0yvjdexn9yjkv3k7zjhp2sxq9z0rgqcqpnrzjqg587a2yyuqeua9c3j8nw7wwpx709slwl5lzfs0t0vq3kdwemzp67rtwevqq95gqqyqqq5sgqqq9yzqq2q9qxpqysgqgpp0zsetj9fedvr0szpwjfw2weckygmjthhnfpp2cerjtrn8n0pxyvrtc00l0jwzkqhwedcvgqljtwx3a7qplqp43jlxe4mpmw5svlgqfwa9yy"}, {999, "lnbc9990n1p5j3cf7sp575w4pw93kfrghl2gh68885v76gwjpzuv435t52q846cvx4w7yuvqpp5hdzvm3yf0r3vj99c7esmcv7zuj2fralf2twhl6s9xqcgr8g7nwyqhp5uwcvgs5clswpfxhm7nyfjmaeysn6us0yvjdexn9yjkv3k7zjhp2sxq9z0rgqcqpnrzjqt0mfswatysklf4z358sztscs5t0vdghmd5vfe9c9sa0gy6r5pdugrs7myqqvgqqqyqqqqqqqqqq86qq8s9qxpqysgq5wh9l4fy32ww4770mqm7yqvhwllaqyssvp335gjz6t59ca03gecyvdd9uv0ztrcm2uf2352wvwxcfh7yukucp4p6zu6ll867aj686wsqz0jlmt"}, {1000, "lnbc10u1p5w6vggsp5gn5xhswgn5299w6elu2z0vzjxhf9hwd6pwjcgfwphaxunyu0dx6spp5a60trrhce2u6tzqjwjczem8rpdesgzkawcqg2xqaesz2kd50z4uqhp5uwcvgs5clswpfxhm7nyfjmaeysn6us0yvjdexn9yjkv3k7zjhp2sxq9z0rgqcqpnrzjqw22kc09xj0dm65ew5h5r003vtn72eyzchdgjag66l0yhwdudfmuzrwvesqq8qgqqgqqqqqqqqqqzhsq2q9qxpqysgq955gcfr95wwz0ehtnk3xraatkyhj88z44ku7yqutnwnt3gkh82jxehvdff7n2js2p54jgpvg6dmwmq8t9d8x05j63mqjrsr4cwd4lpcpnc39ru"}, + {1150, "lnbc11500n1p5jnmr7sp5u0s2wpuqn4mp0axyzgmsxzf5v8sy3zmzz9a7jyq38luyx9cntazqpp57j3carehwt4tqthxz9z7ea80t0htklh4v6v96dtn4vxuu4kwsershp53mwsvrcmkv743nyfzjp5a5fqrg2yngda3apf7jf9rzsuwt82wt3sxq9z0rgqcqpnrzjqg587a2yyuqeua9c3j8nw7wwpx709slwl5lzfs0t0vq3kdwemzp67rtwevqq95gqqyqqq5sgqqq9yzqq2q9qxpqysgqe97nwd9q74ua0sl9877sdprjcuc6jpyy8c52azpz8au6ur8q3838c0a0upnahs8w3sec8kxh26m3v9rkgqej36652t3sa5t25svacdcq5qwwjp"}, {2000, "lnbc20u1p5094fksp54vrdcymel5awhrpc0m6z4kvhhyvqlwkshkyt2wr6eyljkz8c798qpp59f2vc8td8tu62gtf4qfwzkrkxedsey7a5ajrd48a25z2kkwg407shp5nklhn663zgwcdnh7pe5jxt6td0cchhre6hxzdxrjdlfwtpq60f5sxq9z0rgqcqpnrzjqw0de9yc0j8n4hpgm269tm7qph4gwcyf5ys02uaapvpugrva87c7zr045uqq4jsqpsqqqqlgqqqqrcgq2q9qxpqysgq6g2pamgjumh6uw5k5rj2ket44wh8nfzs5gzyygl54hu5cefuxdhxp9h5mrg64rh07znktn9x9d5vg6fc0rw7m63x8rg4qk3kw6d8sycpywn48m"}, }; + + private static readonly Dictionary bolt12Invoices = new() + { + { + 1200, + "lno1zrxq8pjw7qjlm68mtp7e3yvxee4y5xrgjhhyf2fxhlphpckrvevh50u0qwumyhd9aa7p77jkp946nkphl2lutxa3e5zp8yx36pyycqas85txgqsre4qsrhyu2jqk5svgnwe5tng78r24dlwwglluetkdv4a5ppc3wanqqvlfkaqp5hhc6jl8eq0mau6wsdxevary7e0e3rpmma7plggygs7fr4e6dj8vflurnt7ajhgwxfu9hmqmf48wqd6tzuxmwdcgk9p6wspfqer0xj883lysflutn8qvudzakypdv8a7kqqsv0vcrt5w208yr5uzregj7whghy" + }, + }; private static ICounter counter = new InMemoryCounter(); [Fact] @@ -238,6 +247,49 @@ public async Task MeltsSuccessfully() Assert.NotEmpty(change); } + // [Fact] + // public async Task MeltsBolt12Successfully() + // { + // var privkeyBob = new PrivKey(RandomNumberGenerator.GetBytes(32)); + // + // // mint proofs + // var wallet = Wallet + // .Create() + // .WithMint(MintUrl); + // + // var mintQuote = await wallet + // .CreateMintQuote() + // .WithUnit("sat") + // .WithAmount(1337) + // .WithPubkey(privkeyBob.Key.CreatePubKey()) + // .ProcessAsyncBolt12(); + // + // await Task.Delay(3000); + // + // mintQuote.SignWithPrivkey(privkeyBob); + // var mintedProofs = await mintQuote.Mint(); + // Assert.NotEmpty(mintedProofs); + // + // var Ids = mintedProofs.Select(proof => proof.Id).Count(); + // + // Console.WriteLine($"amounts {Ids}"); + // // create melt quote + // var meltQuote = await wallet + // .CreateMeltQuote() + // .WithInvoice(bolt12Invoices[1200]) + // .WithUnit("sat") + // .ProcessAsyncBolt12(); + // + // // select proofs to send + // var q = await meltQuote.GetQuote(); + // var selectedProofs = await wallet.SelectProofsToSend(mintedProofs, q.Amount + (ulong)q.FeeReserve, true); + // + // //melt proofs + // var change = await meltQuote.Melt(selectedProofs.Send); + // + // Assert.NotEmpty(change); + // } + [Fact] public async Task InvoiceWithDescription() { @@ -456,6 +508,47 @@ public async Task MintMeltP2Bk() Assert.NotEmpty(change); } + [Fact] + public async Task MintMeltHTLCP2Bk() + { + var wallet = Wallet + .Create() + .WithMint(MintUrl); + + var privKeyBob = new PrivKey(RandomNumberGenerator.GetHexString(64, true)); + var preimage = new string('0', 63) + "1"; + + var builder = new HTLCBuilder() + { + Pubkeys = [privKeyBob.Key.CreatePubKey()], + HashLock = Convert.ToHexString(SHA256.HashData(Convert.FromHexString(preimage))), + }; + + var quote = await wallet + .CreateMintQuote() + .WithAmount(1337) + .WithHTLCLock(builder) + .BlindPubkeys() + .ProcessAsyncBolt11(); + + await PayInvoice(); + var proofs = await quote.Mint(); + + Assert.NotEmpty(proofs); + Assert.NotEmpty(proofs.Select(p=>p.P2PkE)); + + var meltHandler = await wallet + .CreateMeltQuote() + .WithInvoice(valuesInvoices[1150]) + .WithPrivKeys([privKeyBob]) + .WithHTLCPreimage(preimage) + .ProcessAsyncBolt11(); + + var change = await meltHandler.Melt(proofs); + + Assert.NotEmpty(change); + } + [Fact] public async Task MintSwapP2Bk() { diff --git a/DotNut.Tests/UnitTest1.cs b/DotNut.Tests/UnitTest1.cs index d6a32de..e7f8ca3 100644 --- a/DotNut.Tests/UnitTest1.cs +++ b/DotNut.Tests/UnitTest1.cs @@ -275,7 +275,7 @@ public void Nut11_Signatures() var signing_key_three = ECPrivKey.Create(Convert.FromHexString("7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f")); - var conditions = new P2PKBuilder + var conditions = new P2PkBuilder { Lock = DateTimeOffset.FromUnixTimeSeconds(21000000000), Pubkeys = new[] {signing_key_two.CreatePubKey(), signing_key_three.CreatePubKey()}, @@ -917,7 +917,7 @@ public void Nut28_P2BK_Flow() var keysetId = new KeysetId("009a1f293253e41e"); - var conditions = new P2PKBuilder() + var conditions = new P2PkBuilder() { Lock = DateTimeOffset.FromUnixTimeSeconds(21000000000), Pubkeys = new[] {signing_key.CreatePubKey(), signing_key_two.CreatePubKey()}, @@ -955,7 +955,7 @@ public void Nut28_Flow_WithRandomE() var keysetId = new KeysetId("009a1f293253e41e"); - var conditions = new P2PKBuilder() + var conditions = new P2PkBuilder() { Lock = DateTimeOffset.FromUnixTimeSeconds(21000000000), Pubkeys = new[] {signing_key.CreatePubKey(), signing_key_two.CreatePubKey()}, diff --git a/DotNut/Abstractions/MeltQuoteBuilder.cs b/DotNut/Abstractions/MeltQuoteBuilder.cs index 1bab438..d8b8724 100644 --- a/DotNut/Abstractions/MeltQuoteBuilder.cs +++ b/DotNut/Abstractions/MeltQuoteBuilder.cs @@ -62,7 +62,7 @@ public IMeltQuoteBuilder OnQuoteStateChanged(Action public async Task>> ProcessAsyncBolt11(CancellationToken ct = default) { - var mintApi = await _wallet.GetMintApi(); + var mintApi = await _wallet.GetMintApi(ct); await _wallet._maybeSyncKeys(ct); ArgumentNullException.ThrowIfNull(this._invoice); @@ -88,7 +88,7 @@ public async Task>> Proces public async Task>> ProcessAsyncBolt12( CancellationToken ct = default) { - var mintApi = await _wallet.GetMintApi(); + var mintApi = await _wallet.GetMintApi(ct); await _wallet._maybeSyncKeys(ct); ArgumentNullException.ThrowIfNull(this._invoice); @@ -110,7 +110,5 @@ public async Task>> Proces } return new MeltHandlerBolt12(_wallet, quote, _blankOutputs, _privKeys, _htlcPreimage); } - - } diff --git a/DotNut/Abstractions/Nut10Helper.cs b/DotNut/Abstractions/Nut10Helper.cs index 1fc1b30..7fac4af 100644 --- a/DotNut/Abstractions/Nut10Helper.cs +++ b/DotNut/Abstractions/Nut10Helper.cs @@ -78,8 +78,4 @@ private static void handleWitnessCreation(Proof proof, ECPrivKey[] keys, string? proof.Witness = JsonSerializer.Serialize(proofWitness); } } - - - - } \ No newline at end of file diff --git a/DotNut/ApiModels/Melt/bolt12/PostMeltQuoteBolt12Request.cs b/DotNut/ApiModels/Melt/bolt12/PostMeltQuoteBolt12Request.cs index f5b36a2..9544745 100644 --- a/DotNut/ApiModels/Melt/bolt12/PostMeltQuoteBolt12Request.cs +++ b/DotNut/ApiModels/Melt/bolt12/PostMeltQuoteBolt12Request.cs @@ -1,4 +1,5 @@ using System.Text.Json; +using System.Text.Json.Nodes; using System.Text.Json.Serialization; namespace DotNut.ApiModels.Melt.bolt12; @@ -11,6 +12,7 @@ public class PostMeltQuoteBolt12Request [JsonPropertyName("unit")] public string Unit { get; set; } - public JsonDocument? Options { get; set; } + [JsonPropertyName("options")] + public JsonNode? Options { get; set; } } diff --git a/DotNut/NUT11/P2PKProofSecret.cs b/DotNut/NUT11/P2PKProofSecret.cs index 9ded423..6627a97 100644 --- a/DotNut/NUT11/P2PKProofSecret.cs +++ b/DotNut/NUT11/P2PKProofSecret.cs @@ -10,7 +10,7 @@ public class P2PKProofSecret : Nut10ProofSecret { public const string Key = "P2PK"; - [JsonIgnore] P2PKBuilder Builder => P2PKBuilder.Load(this); + [JsonIgnore] public virtual P2PkBuilder Builder => P2PkBuilder.Load(this); public virtual ECPubKey[] GetAllowedPubkeys(out int requiredSignatures) { diff --git a/DotNut/NUT11/P2PkBuilder.cs b/DotNut/NUT11/P2PkBuilder.cs index edd9110..8270f5a 100644 --- a/DotNut/NUT11/P2PkBuilder.cs +++ b/DotNut/NUT11/P2PkBuilder.cs @@ -3,7 +3,7 @@ namespace DotNut; -public class P2PKBuilder +public class P2PkBuilder { public DateTimeOffset? Lock { get; set; } public ECPubKey[]? RefundPubkeys { get; set; } @@ -151,11 +151,11 @@ public P2PKProofSecret BuildBlinded(ECPrivKey p2pke) var Ri = Cashu.ComputeRi(Zx, i); rs.Add(Ri); } - _blindPubkeys(rs.ToArray()); + BlindPubkeys(rs.ToArray()); return Build(); } - private void _blindPubkeys(ECPrivKey[] rs) + protected void BlindPubkeys(ECPrivKey[] rs) { var expectedLength = Pubkeys.Length + (RefundPubkeys?.Length ?? 0); if (expectedLength != rs.Length) diff --git a/DotNut/NUT14/HTLCBuilder.cs b/DotNut/NUT14/HTLCBuilder.cs index 4938ec1..f1e91f6 100644 --- a/DotNut/NUT14/HTLCBuilder.cs +++ b/DotNut/NUT14/HTLCBuilder.cs @@ -1,14 +1,15 @@ -using NBitcoin.Secp256k1; +using System.Security.Cryptography; +using NBitcoin.Secp256k1; namespace DotNut; -public class HTLCBuilder : P2PKBuilder +public class HTLCBuilder : P2PkBuilder { public string HashLock { get; set; } /* - * ugly hack to reuse P2PKBuilder for HTLCs. - * P2PKBuilder expects a pubkey in `data` field, but we need to store a hashlock instead + * ugly hack to reuse P2PkBuilder for HTLCs. + * P2PkBuilder expects a pubkey in `data` field, but we need to store a hashlock instead * * we inject a dummy pubkey so the loader doesn’t break, then remove it after load/build. */ @@ -29,7 +30,7 @@ public static HTLCBuilder Load(HTLCProofSecret proofSecret) Tags = proofSecret.Tags }; - var innerbuilder = P2PKBuilder.Load(tempProof); + var innerbuilder = P2PkBuilder.Load(tempProof); innerbuilder.Pubkeys = innerbuilder.Pubkeys.Except([_dummy.Key]).ToArray(); return new HTLCBuilder() { @@ -50,7 +51,7 @@ public static HTLCBuilder Load(HTLCProofSecret proofSecret) { throw new ArgumentException("HashLock must be 32 bytes (64 chars hex)", nameof(HashLock)); } - var innerBuilder = new P2PKBuilder() + var innerBuilder = new P2PkBuilder() { Lock = Lock, Pubkeys = Pubkeys.ToArray(), @@ -72,12 +73,29 @@ public static HTLCBuilder Load(HTLCProofSecret proofSecret) public new HTLCProofSecret BuildBlinded(KeysetId keysetId, out ECPubKey p2pkE) { - throw new NotImplementedException(); + var e = new PrivKey(RandomNumberGenerator.GetHexString(64)); + p2pkE = e.Key.CreatePubKey(); + return BuildBlinded(keysetId, e); } - public HTLCProofSecret BuildBlinded(KeysetId keysetId, ECPrivKey p2pke) + public new HTLCProofSecret BuildBlinded(KeysetId keysetId, ECPrivKey p2pke) { - throw new NotImplementedException(); + var pubkeys = RefundPubkeys != null ? Pubkeys.Concat(RefundPubkeys).ToArray() : Pubkeys; + var rs = new List(); + bool extraByte = false; + + var keysetIdBytes = keysetId.GetBytes(); + + var e = p2pke; + + for (int i = 0; i < pubkeys.Length; i++) + { + var Zx = Cashu.ComputeZx(e, pubkeys[i]); + var Ri = Cashu.ComputeRi(Zx, keysetIdBytes, i); + rs.Add(Ri); + } + BlindPubkeys(rs.ToArray()); + return Build(); } public override HTLCBuilder Clone() diff --git a/DotNut/NUT14/HTLCProofSecret.cs b/DotNut/NUT14/HTLCProofSecret.cs index ee7fb85..a669718 100644 --- a/DotNut/NUT14/HTLCProofSecret.cs +++ b/DotNut/NUT14/HTLCProofSecret.cs @@ -8,8 +8,7 @@ namespace DotNut; public class HTLCProofSecret : P2PKProofSecret { public const string Key = "HTLC"; - - [JsonIgnore] public HTLCBuilder Builder => HTLCBuilder.Load(this); + [JsonIgnore] public override HTLCBuilder Builder => HTLCBuilder.Load(this); public override ECPubKey[] GetAllowedPubkeys(out int requiredSignatures) { @@ -55,28 +54,41 @@ public HTLCWitness GenerateWitness(ECPrivKey hash, ECPrivKey[] keys, byte[] prei public HTLCWitness GenerateBlindWitness(Proof proof, ECPrivKey[] keys, string preimage) { - throw new NotImplementedException(); + ArgumentNullException.ThrowIfNull(proof.P2PkE); + return GenerateBlindWitness(proof, keys, preimage, proof.P2PkE); } public HTLCWitness GenerateBlindWitness(Proof proof, ECPrivKey[] keys, string preimage, ECPubKey P2PkE) { - throw new NotImplementedException(); + return GenerateBlindWitness(proof.Secret.GetBytes(), keys, Convert.FromHexString(preimage), proof.Id, P2PkE); } public HTLCWitness GenerateBlindWitness(BlindedMessage message, ECPrivKey[] keys, string preimage, ECPubKey P2PkE) { - throw new NotImplementedException(); + return GenerateBlindWitness(message.B_.Key.ToBytes(), keys, Convert.FromHexString(preimage), message.Id, P2PkE); } public HTLCWitness GenerateBlindWitness(byte[] msg, ECPrivKey[] keys, byte[] preimage, KeysetId keysetId, ECPubKey P2PkE) { - throw new NotImplementedException(); + var hash = SHA256.HashData(msg); + return GenerateBlindWitness(ECPrivKey.Create(hash), keys, preimage, keysetId, P2PkE); } public HTLCWitness GenerateBlindWitness(ECPrivKey hash, ECPrivKey[] keys, byte[] preimage, KeysetId keysetId, ECPubKey P2PkE) { - throw new NotImplementedException(); + var builder = Builder; + if (!builder.Lock.HasValue || builder.Lock.Value.ToUnixTimeSeconds() > DateTimeOffset.Now.ToUnixTimeSeconds()) + { + if (!VerifyPreimage(preimage)) + throw new InvalidOperationException("Invalid preimage"); + } + var witness = base.GenerateBlindWitness(hash, keys, keysetId, P2PkE); + return new HTLCWitness() + { + Signatures = witness.Signatures, + Preimage = Convert.ToHexString(preimage) + }; } diff --git a/DotNut/SigAllHandler.cs b/DotNut/SigAllHandler.cs index 641383c..1953cf9 100644 --- a/DotNut/SigAllHandler.cs +++ b/DotNut/SigAllHandler.cs @@ -206,9 +206,9 @@ private static bool ValidateFirstProof(Proof firstProof, out Nut10ProofSecret? s var builder = nut10.ProofSecret switch { HTLCProofSecret htlcs => HTLCBuilder.Load(htlcs), - P2PKProofSecret p2pks => P2PKBuilder.Load(p2pks), + P2PKProofSecret p2pks => P2PkBuilder.Load(p2pks), // won't throw exception if there will be a new type of nut10 secret, but will return false - _ => new P2PKBuilder(){SigFlag = null} + _ => new P2PkBuilder(){SigFlag = null} }; if (builder.SigFlag != "SIG_ALL") From 158448a4384eb25e887a7b67b3f5bc44eaf119ed Mon Sep 17 00:00:00 2001 From: d4rp4t Date: Fri, 28 Nov 2025 22:39:19 +0100 Subject: [PATCH 28/70] finish up xml docs --- .../Abstractions/Interfaces/IWalletBuilder.cs | 67 ++++++++++++++++++- DotNut/Abstractions/MintQuoteBuilder.cs | 13 ---- DotNut/Abstractions/Utils.cs | 24 ++++++- DotNut/Abstractions/Wallet.cs | 63 ++--------------- DotNut/NUT11/P2PkBuilder.cs | 1 - DotNut/NUT14/HTLCBuilder.cs | 1 - 6 files changed, 92 insertions(+), 77 deletions(-) diff --git a/DotNut/Abstractions/Interfaces/IWalletBuilder.cs b/DotNut/Abstractions/Interfaces/IWalletBuilder.cs index fb9a412..5bcf435 100644 --- a/DotNut/Abstractions/Interfaces/IWalletBuilder.cs +++ b/DotNut/Abstractions/Interfaces/IWalletBuilder.cs @@ -126,20 +126,81 @@ public interface IWalletBuilder /// IWalletBuilder WithWebsocketService(IWebsocketService websocketService); + /// + /// Get Mints info, supported methods etc. + /// + /// Refetch flag + /// + /// MintInfo object Task GetInfo(bool forceReferesh = false, CancellationToken ct = default); + + /// + /// Create Outputs (BlindedMessags, Blinding Factors, Secrets), for given keysetId. + /// Deterministic if Mnemonic and Counter set up. + /// + /// List of amounts in Outputs. + /// Keyset ID + /// + /// Outputs + /// If keys not set. If Mnemonic set, but no Counter. Task> CreateOutputs(List amounts, KeysetId id, CancellationToken ct = default); + + /// + /// Create Outputs for active KeysetId for given unit. Fetches a keyset for given unit automatically. + /// + /// List of amounts. + /// + /// + /// Outputs + /// If no keysetID stored in wallet. Task> CreateOutputs(List amounts, string unit, CancellationToken ct = default); + /// + /// Set Last sync date to DateTime.MinValue - keysets will be synced before next operation + /// + public void InvalidateCache(); + + /// + /// Get active keyset id for chosen unit. + /// + /// keyset unit, e.g. sat + /// + /// Active keysetId + Task GetActiveKeysetId(string unit, CancellationToken ct = default); + + /// + /// Get active keyset ids for each supported unit + /// + /// Dictionary of (unit, KeysetId) Task?> GetActiveKeysetIdsWithUnits(CancellationToken ct = default); Task GetMintApi(CancellationToken ct = default); - - Task GetActiveKeysetId(string unit, CancellationToken ct = default); + + /// + /// Get keys of current mint stored in wallet. + /// + /// Refetch flag + /// + /// Mints keys Task> GetKeys(bool forceRefresh = false, CancellationToken ct = default); - + + /// + /// Get Keys for given KeysetID + /// + /// KeysetId + /// Refetch flag + /// + /// Keys for given keyset + /// If wallet doesn't contain keysets for given keysetId Task GetKeys(KeysetId id, bool forceRefresh = false, CancellationToken ct = default); + /// + /// Get Keysets stored in wallet + /// + /// Refetch flag + /// + /// List of Keysets Task> GetKeysets(bool forceRefresh = false, CancellationToken ct = default); diff --git a/DotNut/Abstractions/MintQuoteBuilder.cs b/DotNut/Abstractions/MintQuoteBuilder.cs index 6e6d63d..1c4848e 100644 --- a/DotNut/Abstractions/MintQuoteBuilder.cs +++ b/DotNut/Abstractions/MintQuoteBuilder.cs @@ -17,7 +17,6 @@ class MintQuoteBuilder : IMintQuoteBuilder private string _unit = "sat"; private string? _description; private List? _outputs; - private string? _method = "bolt11"; private string? _pubkey; @@ -33,18 +32,6 @@ public MintQuoteBuilder(Wallet wallet) this._wallet = wallet; } - /// - /// Mandatory. - /// User has to provide Mint method - /// - /// Either MintMeltMethod.Bolt11 or MintMeltMethod.Bolt12 - /// - public IMintQuoteBuilder WithMethod(string method) - { - this._method = method; - return this; - } - public IMintQuoteBuilder WithAmount(ulong amount) { this._amount = amount; diff --git a/DotNut/Abstractions/Utils.cs b/DotNut/Abstractions/Utils.cs index 7245317..897e8e7 100644 --- a/DotNut/Abstractions/Utils.cs +++ b/DotNut/Abstractions/Utils.cs @@ -75,7 +75,7 @@ public static int CalculateNumberOfBlankOutputs(ulong amountToCover) /// - /// Creates outputs for swap/melt fee return. Outputs should have valid amounts. + /// Creates outputs (secrets, proof messages and blinding factors). Outputs should have valid amounts. /// /// Amounts for each output (e.g. [1,2,4,8] /// ID of keyset we want to receive the proofs @@ -129,6 +129,14 @@ public static List CreateOutputs( return outputs; } + + /// + /// Create P2Pk / HTLC outputs. + /// + /// + /// + /// + /// public static OutputData CreateNut10Output( ulong amount, KeysetId keysetId, @@ -155,6 +163,13 @@ P2PkBuilder builder Secret = secret }; } + /// + /// Creates P2Pk / HTLC Blinded Outputs + /// + /// + /// + /// + /// public static OutputData CreateNut10BlindedOutput(ulong amount, KeysetId keysetId, P2PkBuilder builder) { // ugliest hack ever @@ -181,6 +196,13 @@ public static OutputData CreateNut10BlindedOutput(ulong amount, KeysetId keysetI P2BkE = E }; } + /// + /// Creates P2Pk / HTLC Blinded Outputs with specified ephemeral sender keypair. + /// + /// + /// + /// + /// public static OutputData CreateNut10BlindedOutput(ulong amount, KeysetId keysetId, P2PkBuilder builder, PrivKey e) { // ugliest hack ever diff --git a/DotNut/Abstractions/Wallet.cs b/DotNut/Abstractions/Wallet.cs index 6b6e626..c9959b2 100644 --- a/DotNut/Abstractions/Wallet.cs +++ b/DotNut/Abstractions/Wallet.cs @@ -178,20 +178,12 @@ public IRestoreBuilder Restore() * Public Mint utils */ - /// - /// Set Last sync date to DateTime.MinValue - keysets will be synced before next operation - /// public void InvalidateCache() { _lastSync = DateTime.MinValue; } - /// - /// Get active keyset id for chosen unit. - /// - /// keyset unit, e.g. sat - /// - /// Active keysetId + public async Task GetActiveKeysetId(string unit, CancellationToken ct = default) { await _maybeSyncKeys(ct); @@ -201,10 +193,7 @@ public void InvalidateCache() ?.Id; } - /// - /// Get active keyset ids for each unit - /// - /// Dictionary of (unit, KeysetId) + public async Task?> GetActiveKeysetIdsWithUnits(CancellationToken ct = default) { await _maybeSyncKeys(ct); @@ -216,12 +205,6 @@ public void InvalidateCache() ); } - /// - /// Get keys of current mint stored in wallet. - /// - /// Refetch flag - /// - /// Mints keys public async Task> GetKeys(bool forceRefresh = false, CancellationToken ct = default) { if (forceRefresh) @@ -233,14 +216,6 @@ public void InvalidateCache() return this._keys ?? []; } - /// - /// Get Keys for given KeysetID - /// - /// KeysetId - /// Refetch flag - /// - /// Keys for given keyset - /// If wallet doesn't contain keysets for given keysetId public async Task GetKeys(KeysetId id, bool forceRefresh = false, CancellationToken ct = default) { if (forceRefresh) @@ -253,13 +228,7 @@ public void InvalidateCache() } return this._keys.Single(k => k.Id == id); } - - /// - /// Get Keysets stored in wallet - /// - /// Refetch flag - /// - /// List of Keysets + public async Task> GetKeysets(bool forceRefresh = false, CancellationToken ct = default) { if (forceRefresh) @@ -271,12 +240,7 @@ public void InvalidateCache() return _keysets ?? []; } - /// - /// Get Mints info, supported methods etc. - /// - /// Refetch flag - /// - /// MintInfo object + public async Task GetInfo(bool forceReferesh = false, CancellationToken ct = default) { if (forceReferesh) @@ -285,16 +249,7 @@ public async Task GetInfo(bool forceReferesh = false, CancellationToke } return await _lazyFetchMintInfo(ct); } - - /// - /// Create Outputs (BlindedMessags, Blinding Factors, Secrets), for given keysetId. - /// Deterministic if Mnemonic and Counter set up. - /// - /// List of amounts in Outputs. - /// Keyset ID - /// - /// Outputs - /// If keys not set. If Mnemonic set, but no Counter. + public async Task> CreateOutputs(List amounts, KeysetId id, CancellationToken ct = default) { await _maybeSyncKeys(ct); @@ -321,14 +276,6 @@ public async Task> CreateOutputs(List amounts, KeysetId return Utils.CreateOutputs(amounts, id, keyset.Keys, this._mnemonic, counterValue); } - /// - /// Create Outputs for active KeysetId for given unit. - /// - /// List of amounts. - /// - /// - /// Outputs - /// If no keysetID stored in wallet. public async Task> CreateOutputs(List amounts, string unit, CancellationToken ct = default) { var keysetId = await this.GetActiveKeysetId(unit, ct); diff --git a/DotNut/NUT11/P2PkBuilder.cs b/DotNut/NUT11/P2PkBuilder.cs index 8270f5a..4014e7b 100644 --- a/DotNut/NUT11/P2PkBuilder.cs +++ b/DotNut/NUT11/P2PkBuilder.cs @@ -144,7 +144,6 @@ public P2PKProofSecret BuildBlinded(ECPrivKey p2pke) { var pubkeys = RefundPubkeys != null ? Pubkeys.Concat(RefundPubkeys).ToArray() : Pubkeys; var rs = new List(); - for (int i = 0; i < pubkeys.Length; i++) { var Zx = Cashu.ComputeZx(p2pke, pubkeys[i]); diff --git a/DotNut/NUT14/HTLCBuilder.cs b/DotNut/NUT14/HTLCBuilder.cs index f1e91f6..1f94a06 100644 --- a/DotNut/NUT14/HTLCBuilder.cs +++ b/DotNut/NUT14/HTLCBuilder.cs @@ -82,7 +82,6 @@ public static HTLCBuilder Load(HTLCProofSecret proofSecret) { var pubkeys = RefundPubkeys != null ? Pubkeys.Concat(RefundPubkeys).ToArray() : Pubkeys; var rs = new List(); - bool extraByte = false; var keysetIdBytes = keysetId.GetBytes(); From f2727d6b25489c194880ec9c1370dfa25023c70c Mon Sep 17 00:00:00 2001 From: d4rp4t Date: Fri, 28 Nov 2025 23:25:45 +0100 Subject: [PATCH 29/70] fixes --- .github/workflows/ci.yml | 2 +- .gitignore | 4 +- DotNut.Tests/Integration.cs | 21 ++--- DotNut.Tests/UnitTests2.cs | 2 +- .../Handlers/MeltHandlerBolt11.cs | 4 +- .../Handlers/MeltHandlerBolt12.cs | 6 +- .../Handlers/MintHandlerBolt11.cs | 3 +- .../Handlers/MintHandlerBolt12.cs | 4 +- DotNut/Abstractions/InMemoryCounter.cs | 29 ++++--- .../Abstractions/Interfaces/IMeltHandler.cs | 2 +- .../Abstractions/Interfaces/IMintHandler.cs | 6 +- .../Abstractions/Interfaces/IProofSelector.cs | 4 +- .../Interfaces/IRestoreBuilder.cs | 2 +- .../Abstractions/Interfaces/IWalletBuilder.cs | 15 ++-- .../Interfaces/IWebsocketService.cs | 3 + DotNut/Abstractions/MeltQuoteBuilder.cs | 8 -- DotNut/Abstractions/MintInfo.cs | 17 ++-- DotNut/Abstractions/MintQuoteBuilder.cs | 24 ++++-- DotNut/Abstractions/Nut10Helper.cs | 1 - DotNut/Abstractions/ProofSelector.cs | 4 +- DotNut/Abstractions/RestoreBuilder.cs | 44 +++++++--- DotNut/Abstractions/SwapBuilder.cs | 52 ++++++------ DotNut/Abstractions/Utils.cs | 15 ++-- DotNut/Abstractions/Wallet.cs | 37 ++++++--- .../Abstractions/Websockets/Subscription.cs | 4 +- .../Websockets/WebsocketConnection.cs | 4 +- .../Websockets/WebsocketModels.cs | 4 +- .../Websockets/WebsocketService.cs | 46 ++++++++-- DotNut/Api/CashuHttpClient.cs | 1 + DotNut/ApiModels/Info/MPPInfo.cs | 2 +- DotNut/ApiModels/Info/SwapInfo.cs | 2 +- .../Mint/bolt11/PostMintQuoteBolt11Request.cs | 1 - DotNut/Encoding/CashuTokenV4Encoder.cs | 8 -- .../PaymentRequestEncoder.cs | 8 ++ DotNut/NUT01/Keyset.cs | 4 +- DotNut/NUT02/FeeHelper.cs | 6 +- DotNut/NUT11/SigAllHandler.cs | 83 ++++++++++++------- DotNut/NUT13/Nut13.cs | 2 +- DotNut/NUT14/HTLCProofSecret.cs | 5 +- DotNut/NUT18/PaymentRequest.cs | 1 + 40 files changed, 291 insertions(+), 199 deletions(-) rename DotNut/{NUT18 => Encoding}/PaymentRequestEncoder.cs (96%) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a714eb9..cc52ad9 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -25,7 +25,7 @@ jobs: -e CDK_MINTD_LISTEN_PORT=3338 \ -e CDK_MINTD_FAKE_WALLET_MIN_DELAY=0 \ -e CDK_MINTD_MNEMONIC='abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about' \ - cashubtc/mintd:latest-amd64 \ + cashubtc/mintd:latest-amd64 - name: Wait for mint to be ready run: | diff --git a/.gitignore b/.gitignore index cddd443..d0b5773 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,5 @@ **/bin **/obj .idea -.idea -.vs \ No newline at end of file +.vs ++*.DotSettings.user \ No newline at end of file diff --git a/DotNut.Tests/Integration.cs b/DotNut.Tests/Integration.cs index 1cd0999..fe76fd1 100644 --- a/DotNut.Tests/Integration.cs +++ b/DotNut.Tests/Integration.cs @@ -72,7 +72,7 @@ public async Task MintsSuccessfully() Assert.NotNull(mintQuote); - var paymentRequest = (await mintQuote.GetQuote()).Request; + var paymentRequest = mintQuote.GetQuote().Request; Assert.Contains("lnbc1337", paymentRequest); await PayInvoice(); @@ -97,7 +97,7 @@ public async Task MintsBolt12Successfully() Assert.NotNull(mintQuote); - var paymentRequest = (await mintQuote.GetQuote()).Request; + var paymentRequest = mintQuote.GetQuote().Request; Assert.NotNull(paymentRequest); mintQuote.SignWithPrivkey(privkey); @@ -124,7 +124,7 @@ public async Task MintsDeterministicSuccessfully() Assert.NotNull(mintQuote); - var paymentRequest = (await mintQuote.GetQuote()).Request; + var paymentRequest = mintQuote.GetQuote().Request; Assert.Contains("lnbc1337", paymentRequest); await PayInvoice(); @@ -227,9 +227,6 @@ public async Task MeltsSuccessfully() var mintedProofs = await mintQuote.Mint(); Assert.NotEmpty(mintedProofs); - var Ids = mintedProofs.Select(proof => proof.Id).Count(); - - Console.WriteLine($"amounts {Ids}"); // create melt quote var meltQuote = await wallet .CreateMeltQuote() @@ -238,7 +235,7 @@ public async Task MeltsSuccessfully() .ProcessAsyncBolt11(); // select proofs to send - var q = await meltQuote.GetQuote(); + var q = meltQuote.GetQuote(); var selectedProofs = await wallet.SelectProofsToSend(mintedProofs, q.Amount + (ulong)q.FeeReserve, true); //melt proofs @@ -318,7 +315,7 @@ public async Task FeeForExternalInvoice() Assert.NotNull(meltHandler); - var quote = await meltHandler.GetQuote(); + var quote = meltHandler.GetQuote(); Assert.NotNull(quote); Assert.True(quote.FeeReserve > 0); @@ -413,7 +410,7 @@ await Assert.ThrowsAsync(async () => .WithPrivKeys([privKeyBob, privKeyAlice]) .ProcessAsyncBolt11(); - var q = await handler.GetQuote(); + var q = handler.GetQuote(); var amountToPay = q.Amount + (ulong)q.FeeReserve; var selectorResponse = await wallet.SelectProofsToSend(proofs, amountToPay, true); @@ -675,7 +672,7 @@ public async Task MeltWithInsufficientFunds() .WithUnit("sat") .ProcessAsyncBolt11(); - var quote = await meltHandler.GetQuote(); + var quote = meltHandler.GetQuote(); var amountNeeded = quote.Amount + (ulong)quote.FeeReserve; // selectProofsToSend should return empty Send list when insufficient @@ -699,14 +696,14 @@ public async Task SubscribeToMintMeltQuoteUpdates() .WithUnit("sat") .ProcessAsyncBolt11(); - var quote = await mintHandler.GetQuote(); + var quote = mintHandler.GetQuote(); var sub = await service.SubscribeToMintQuoteAsync(MintUrl, new[] { quote.Quote }); int connectedCount = 0; int notificationCount = 0; - using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(120)); + using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(240)); var connectedTcs = new TaskCompletionSource(); var paidTcs = new TaskCompletionSource(); diff --git a/DotNut.Tests/UnitTests2.cs b/DotNut.Tests/UnitTests2.cs index 6cd7932..605c97c 100644 --- a/DotNut.Tests/UnitTests2.cs +++ b/DotNut.Tests/UnitTests2.cs @@ -566,7 +566,7 @@ public async Task Wallet_ThrowsOnMissingMint_ForAllOperations() } [Fact] - public async Task Counter_ThrowsOnMissingKeysetId() + public async Task Counter_ReturnsZeroForUnknownKeysetId() { var counter = new InMemoryCounter(); var unknownKeysetId = new KeysetId("00unknown1234567"); diff --git a/DotNut/Abstractions/Handlers/MeltHandlerBolt11.cs b/DotNut/Abstractions/Handlers/MeltHandlerBolt11.cs index 0c1cf40..e625cd9 100644 --- a/DotNut/Abstractions/Handlers/MeltHandlerBolt11.cs +++ b/DotNut/Abstractions/Handlers/MeltHandlerBolt11.cs @@ -10,7 +10,7 @@ public class MeltHandlerBolt11( string? htlcPreimage = null) : IMeltHandler> { - public async Task GetQuote(CancellationToken ct = default) => quote; + public PostMeltQuoteBolt11Response GetQuote() => quote; public async Task> Melt(List inputs, CancellationToken ct = default) { Nut10Helper.MaybeProcessNut10(privKeys??[], inputs, blankOutputs, htlcPreimage, quote.Quote); @@ -23,7 +23,7 @@ public async Task> Melt(List inputs, CancellationToken ct = d }; var res = await client.Melt("bolt11", req, ct); - if (res.Change == null) + if (res.Change == null || res.Change.Length == 0) { return []; } diff --git a/DotNut/Abstractions/Handlers/MeltHandlerBolt12.cs b/DotNut/Abstractions/Handlers/MeltHandlerBolt12.cs index 9b9e322..fb80e2e 100644 --- a/DotNut/Abstractions/Handlers/MeltHandlerBolt12.cs +++ b/DotNut/Abstractions/Handlers/MeltHandlerBolt12.cs @@ -11,7 +11,7 @@ public class MeltHandlerBolt12( string? htlcPreimage = null) : IMeltHandler> { - public async Task GetQuote(CancellationToken ct = default) => quote; + public PostMeltQuoteBolt12Response GetQuote() => quote; public async Task> Melt(List inputs, CancellationToken ct = default) { Nut10Helper.MaybeProcessNut10(privKeys??[], inputs, blankOutputs, htlcPreimage, quote.Quote); @@ -23,8 +23,8 @@ public async Task> Melt(List inputs, CancellationToken ct = d Outputs = blankOutputs.Select(bo=>bo.BlindedMessage).ToArray(), }; - var res = await client.Melt("bolt11", req, ct); - if (res.Change == null) + var res = await client.Melt("bolt12", req, ct); + if (res.Change == null || res.Change.Length == 0) { return []; } diff --git a/DotNut/Abstractions/Handlers/MintHandlerBolt11.cs b/DotNut/Abstractions/Handlers/MintHandlerBolt11.cs index 853cc48..9f9a8fa 100644 --- a/DotNut/Abstractions/Handlers/MintHandlerBolt11.cs +++ b/DotNut/Abstractions/Handlers/MintHandlerBolt11.cs @@ -28,7 +28,7 @@ public IMintHandler> SignWithPrivkey(Pr return this; } - public async Task GetQuote(CancellationToken ct = default) => postMintQuoteBolt11Response; + public PostMintQuoteBolt11Response GetQuote() => postMintQuoteBolt11Response; public async Task> Mint(CancellationToken ct = default) { @@ -42,6 +42,7 @@ public async Task> Mint(CancellationToken ct = default) { Outputs = outputs.Select(o=>o.BlindedMessage).ToArray(), Quote = postMintQuoteBolt11Response.Quote, + Signature = _signature, }; var promises= await client.Mint("bolt11", req, ct); diff --git a/DotNut/Abstractions/Handlers/MintHandlerBolt12.cs b/DotNut/Abstractions/Handlers/MintHandlerBolt12.cs index 94833e8..2661f17 100644 --- a/DotNut/Abstractions/Handlers/MintHandlerBolt12.cs +++ b/DotNut/Abstractions/Handlers/MintHandlerBolt12.cs @@ -3,7 +3,7 @@ namespace DotNut.Abstractions.Handlers; public class MintHandlerBolt12( - Wallet wallet, + IWalletBuilder wallet, PostMintQuoteBolt12Response quote, GetKeysResponse.KeysetItemResponse keyset, List outputs) @@ -29,7 +29,7 @@ public IMintHandler> SignWithPrivkey(Pr return this; } - public async Task GetQuote(CancellationToken ct = default) => quote; + public PostMintQuoteBolt12Response GetQuote() => quote; public async Task> Mint(CancellationToken ct = default) { diff --git a/DotNut/Abstractions/InMemoryCounter.cs b/DotNut/Abstractions/InMemoryCounter.cs index 31626dd..5012839 100644 --- a/DotNut/Abstractions/InMemoryCounter.cs +++ b/DotNut/Abstractions/InMemoryCounter.cs @@ -1,35 +1,34 @@ +using System.Collections.Concurrent; + namespace DotNut.Abstractions; public class InMemoryCounter : ICounter { - private IDictionary _counter; - + private readonly ConcurrentDictionary _counter; public InMemoryCounter(IDictionary counter) { - this._counter = counter; + this._counter = new ConcurrentDictionary(counter); } public InMemoryCounter() { - this._counter = new Dictionary(); + this._counter = new ConcurrentDictionary(); } - public async Task GetCounterForId(KeysetId keysetId, CancellationToken ct = default) + public Task GetCounterForId(KeysetId keysetId, CancellationToken ct = default) { - if (_counter.TryGetValue(keysetId, out var counter)) - return counter; - - return _counter[keysetId] = 0; + return Task.FromResult(_counter.GetOrAdd(keysetId, 0)); } - public async Task IncrementCounter(KeysetId keysetId, int bumpBy = 1, CancellationToken ct = default) + public Task IncrementCounter(KeysetId keysetId, int bumpBy = 1, CancellationToken ct = default) { - var current = await GetCounterForId(keysetId, ct); - var next = current + bumpBy; - _counter[keysetId] = next; - return next; + var next = _counter.AddOrUpdate(keysetId, bumpBy, (_, current) => current + bumpBy); + return Task.FromResult(next); } - public async Task SetCounter(KeysetId keysetId, int counter, CancellationToken ct = default) => _counter[keysetId] = counter; + public Task SetCounter(KeysetId keysetId, int counter, CancellationToken ct = default) { + _counter[keysetId] = counter; + return Task.CompletedTask; + } } \ No newline at end of file diff --git a/DotNut/Abstractions/Interfaces/IMeltHandler.cs b/DotNut/Abstractions/Interfaces/IMeltHandler.cs index b6b67fd..98166be 100644 --- a/DotNut/Abstractions/Interfaces/IMeltHandler.cs +++ b/DotNut/Abstractions/Interfaces/IMeltHandler.cs @@ -4,6 +4,6 @@ public interface IMeltHandler; public interface IMeltHandler: IMeltHandler { - Task GetQuote(CancellationToken ct = default); + TQuote GetQuote(); Task Melt(List inputs, CancellationToken ct = default); } \ No newline at end of file diff --git a/DotNut/Abstractions/Interfaces/IMintHandler.cs b/DotNut/Abstractions/Interfaces/IMintHandler.cs index c3ff990..b5c0cc9 100644 --- a/DotNut/Abstractions/Interfaces/IMintHandler.cs +++ b/DotNut/Abstractions/Interfaces/IMintHandler.cs @@ -4,9 +4,9 @@ public interface IMintHandler; public interface IMintHandler: IMintHandler { public IMintHandler WithSignature(string signature); - public IMintHandler SignWithPrivkey(PrivKey privkey); - public IMintHandler SignWithPrivkey(string privKeyHex); + public IMintHandler SignWithPrivkey(PrivKey privkey); + public IMintHandler SignWithPrivkey(string privKeyHex); - Task GetQuote(CancellationToken ct = default); + TQuote GetQuote(); Task Mint(CancellationToken ct = default); } \ No newline at end of file diff --git a/DotNut/Abstractions/Interfaces/IProofSelector.cs b/DotNut/Abstractions/Interfaces/IProofSelector.cs index 0639f51..9b7f76d 100644 --- a/DotNut/Abstractions/Interfaces/IProofSelector.cs +++ b/DotNut/Abstractions/Interfaces/IProofSelector.cs @@ -1,6 +1,6 @@ namespace DotNut.Abstractions; public interface IProofSelector -{ - public Task SelectProofsToSend(List proofs, ulong amountToSend, bool includeFees = false, CancellationToken ct = default); +{ + Task SelectProofsToSend(List proofs, ulong amountToSend, bool includeFees = false, CancellationToken ct = default); } \ No newline at end of file diff --git a/DotNut/Abstractions/Interfaces/IRestoreBuilder.cs b/DotNut/Abstractions/Interfaces/IRestoreBuilder.cs index dd4e4c9..b6a81ea 100644 --- a/DotNut/Abstractions/Interfaces/IRestoreBuilder.cs +++ b/DotNut/Abstractions/Interfaces/IRestoreBuilder.cs @@ -5,7 +5,7 @@ namespace DotNut.Abstractions; /// public interface IRestoreBuilder { - RestoreBuilder ForKeysetIds(IEnumerable keysetIds); + IRestoreBuilder ForKeysetIds(IEnumerable keysetIds); IRestoreBuilder WithSwap(bool shouldSwap = true); Task> ProcessAsync(CancellationToken ct = default); } diff --git a/DotNut/Abstractions/Interfaces/IWalletBuilder.cs b/DotNut/Abstractions/Interfaces/IWalletBuilder.cs index 5bcf435..f0077d6 100644 --- a/DotNut/Abstractions/Interfaces/IWalletBuilder.cs +++ b/DotNut/Abstractions/Interfaces/IWalletBuilder.cs @@ -1,7 +1,5 @@ -using DotNut.Abstractions.Websockets; using DotNut.Api; using DotNut.ApiModels; -using DotNut.ApiModels.Mint.bolt12; using DotNut.NBitcoin.BIP39; namespace DotNut.Abstractions; @@ -72,9 +70,9 @@ public interface IWalletBuilder /// with first operation requiring keysets. (I'd go for like, 60 minutes) /// /// - /// + /// /// - IWalletBuilder WithKeysetSync(bool syncKeyset, TimeSpan syncThreesold); + IWalletBuilder WithKeysetSync(bool syncKeyset, TimeSpan syncThreshold); /// /// Optional. Proof selecting algorithm. If not set, defaults to RGLI proof selector. @@ -129,10 +127,10 @@ public interface IWalletBuilder /// /// Get Mints info, supported methods etc. /// - /// Refetch flag + /// Refetch flag /// /// MintInfo object - Task GetInfo(bool forceReferesh = false, CancellationToken ct = default); + Task GetInfo(bool forceRefresh = false, CancellationToken ct = default); /// /// Create Outputs (BlindedMessags, Blinding Factors, Secrets), for given keysetId. @@ -208,6 +206,9 @@ Task SelectProofsToSend(List proofs, ulong amount, bool inc CancellationToken ct = default); Task GetWebsocketService(CancellationToken ct = default); + + Task GetSelector(CancellationToken ct = default); + /// /// Create swap transaction builder. @@ -228,7 +229,7 @@ Task SelectProofsToSend(List proofs, ulong amount, bool inc IMintQuoteBuilder CreateMintQuote(); /// - /// Can restoree proofs if mnemonic provided. + /// Can restore proofs if mnemonic provided. /// /// IRestoreBuilder Restore(); diff --git a/DotNut/Abstractions/Interfaces/IWebsocketService.cs b/DotNut/Abstractions/Interfaces/IWebsocketService.cs index 7791036..1d6333b 100644 --- a/DotNut/Abstractions/Interfaces/IWebsocketService.cs +++ b/DotNut/Abstractions/Interfaces/IWebsocketService.cs @@ -5,6 +5,9 @@ namespace DotNut.Abstractions; public interface IWebsocketService : IAsyncDisposable { + /// + /// Raised when a connection's state changes. Handlers should be thread-safe. + /// event EventHandler? ConnectionStateChanged; Task LazyConnectAsync(string mintUrl, CancellationToken ct = default); diff --git a/DotNut/Abstractions/MeltQuoteBuilder.cs b/DotNut/Abstractions/MeltQuoteBuilder.cs index d8b8724..5282a15 100644 --- a/DotNut/Abstractions/MeltQuoteBuilder.cs +++ b/DotNut/Abstractions/MeltQuoteBuilder.cs @@ -14,8 +14,6 @@ class MeltQuoteBuilder : IMeltQuoteBuilder private List? _privKeys; private string? _htlcPreimage; - - private Action? _callback; public MeltQuoteBuilder(Wallet wallet) { @@ -54,12 +52,6 @@ public IMeltQuoteBuilder WithHTLCPreimage(string preimage) return this; } - public IMeltQuoteBuilder OnQuoteStateChanged(Action callback) - { - this._callback = callback; - return this; - } - public async Task>> ProcessAsyncBolt11(CancellationToken ct = default) { var mintApi = await _wallet.GetMintApi(ct); diff --git a/DotNut/Abstractions/MintInfo.cs b/DotNut/Abstractions/MintInfo.cs index 28f86de..e92a4ec 100644 --- a/DotNut/Abstractions/MintInfo.cs +++ b/DotNut/Abstractions/MintInfo.cs @@ -1,5 +1,7 @@ +using System.Collections.Concurrent; using System.Text.Json; using System.Text.Json.Serialization; +using System.Text.RegularExpressions; using DotNut.ApiModels; using DotNut.ApiModels.Info; @@ -23,11 +25,14 @@ public MintInfo(GetInfoResponse info) { _protectedEndpoints = new ProtectedEndpoints { - Cache = new Dictionary(), + Cache = new ConcurrentDictionary(), ApiReturn = nut22.ProtectedEndpoints.Select(o => new ProtectedEndpoint { Method = o.Method, - Regex = new System.Text.RegularExpressions.Regex(o.Path) + Regex = new Regex( + o.Path, + RegexOptions.None, + TimeSpan.FromMilliseconds(100)) }).ToArray() }; } @@ -180,7 +185,7 @@ private MppSupport CheckNut15() { try { - var nut = JsonSerializer.Deserialize(nutJson.RootElement.GetRawText()); + var nut = JsonSerializer.Deserialize(nutJson.RootElement.GetRawText()); if (nut?.Methods != null && nut.Methods.Length > 0) { return new MppSupport @@ -269,14 +274,14 @@ public class ProtectedEndpointSpec internal class ProtectedEndpoints { - public Dictionary Cache { get; set; } = new(); + public ConcurrentDictionary Cache { get; set; } = new(); public ProtectedEndpoint[] ApiReturn { get; set; } = Array.Empty(); } internal class ProtectedEndpoint { public string Method { get; set; } = string.Empty; - public System.Text.RegularExpressions.Regex Regex { get; set; } = null!; + public System.Text.RegularExpressions.Regex Regex { get; init; } } public class WebSocketSupportResult @@ -285,7 +290,7 @@ public class WebSocketSupportResult public WebSocketSupport[]? Params { get; set; } } -public class MppSupport : MmpInfo +public class MppSupport : MPPInfo { public bool Supported { get; set; } } diff --git a/DotNut/Abstractions/MintQuoteBuilder.cs b/DotNut/Abstractions/MintQuoteBuilder.cs index 1c4848e..505d7ef 100644 --- a/DotNut/Abstractions/MintQuoteBuilder.cs +++ b/DotNut/Abstractions/MintQuoteBuilder.cs @@ -1,5 +1,3 @@ -using System.Collections.Immutable; -using System.Reflection.Metadata; using System.Security.Cryptography; using DotNut.Abstractions.Handlers; using DotNut.Api; @@ -101,7 +99,7 @@ public async Task>> Proces await this._wallet._maybeSyncKeys(ct); if (_amount == null) { - throw new ArgumentNullException(nameof(_amount), "can't create melt quote without amount!"); + throw new ArgumentNullException(nameof(_amount), "can't create mint quote without amount!"); } var api = await this._wallet.GetMintApi(); @@ -124,7 +122,7 @@ public async Task>> Proces Amount = this._amount.Value, Unit = this._unit, Description = this._description, - Pubkey = this._pubkey??null, + Pubkey = this._pubkey, }; var quoteBolt11 = await api.CreateMintQuote("bolt11", reqBolt11, @@ -136,10 +134,22 @@ public async Task>> Proces CancellationToken ct = default) { await this._wallet._maybeSyncKeys(ct); + + var api = await this._wallet.GetMintApi(); + if (api is null) + { + throw new ArgumentNullException(nameof(ICashuApi), "Can't request mint quote without mint API"); + } + if (this._pubkey == null) { throw new ArgumentNullException(nameof(_pubkey), "Can't request bolt12 mint quote without pubkey!"); } + if (this._amount == null) + { + throw new ArgumentNullException(nameof(_amount), "Can't create bolt12 mint quote without amount!"); + + } this._keysetId ??= await this._wallet.GetActiveKeysetId(this._unit, ct) ?? throw new ArgumentException($"Can't get active keyset ID for unit: {_unit}"); @@ -160,10 +170,8 @@ public async Task>> Proces Pubkey = this._pubkey, Description = this._description, }; - var mintQuote = - await (await _wallet.GetMintApi()) - .CreateMintQuote("bolt12", req, - ct); + var mintQuote = await api.CreateMintQuote( + "bolt12", req, ct); return new MintHandlerBolt12(this._wallet, mintQuote, this._keyset, outputs); } diff --git a/DotNut/Abstractions/Nut10Helper.cs b/DotNut/Abstractions/Nut10Helper.cs index 7fac4af..40e35a5 100644 --- a/DotNut/Abstractions/Nut10Helper.cs +++ b/DotNut/Abstractions/Nut10Helper.cs @@ -69,7 +69,6 @@ private static void handleWitnessCreation(Proof proof, ECPrivKey[] keys, string? { if (proof.P2PkE is { } E) { - Console.WriteLine(); var blindWitness = p2pk.GenerateBlindWitness(proof, keys); proof.Witness = JsonSerializer.Serialize(blindWitness); return; diff --git a/DotNut/Abstractions/ProofSelector.cs b/DotNut/Abstractions/ProofSelector.cs index 9d8dd72..63f7760 100644 --- a/DotNut/Abstractions/ProofSelector.cs +++ b/DotNut/Abstractions/ProofSelector.cs @@ -84,10 +84,9 @@ double SumExFees(ulong amount, ulong feePPK) List ShuffleArray(IEnumerable array) { var shuffled = array.ToList(); - var random = new Random(); for (int i = shuffled.Count - 1; i > 0; i--) { - int j = random.Next(i + 1); + int j = Random.Shared.Next(i + 1); (shuffled[i], shuffled[j]) = (shuffled[j], shuffled[i]); } return shuffled; @@ -241,6 +240,7 @@ double CalculateDelta(ulong amount, ulong feePPK) */ for (int trial = 0; trial < MAX_TRIALS; trial++) { + ct.ThrowIfCancellationRequested(); // PHASE 1: Randomized Greedy Selection // Add proofs up to amountToSend (after adjusting for fees) // for exact match or the first amount over target otherwise diff --git a/DotNut/Abstractions/RestoreBuilder.cs b/DotNut/Abstractions/RestoreBuilder.cs index 4294659..8416976 100644 --- a/DotNut/Abstractions/RestoreBuilder.cs +++ b/DotNut/Abstractions/RestoreBuilder.cs @@ -16,7 +16,7 @@ public RestoreBuilder(Wallet wallet) this._wallet = wallet; } - public RestoreBuilder ForKeysetIds(IEnumerable keysetIds) + public IRestoreBuilder ForKeysetIds(IEnumerable keysetIds) { this._specifiedKeysets = keysetIds.ToList(); return this; @@ -30,18 +30,18 @@ public IRestoreBuilder WithSwap(bool shouldSwap = true) public async Task> ProcessAsync(CancellationToken ct = default) { - var api = await _wallet.GetMintApi(); + var api = await _wallet.GetMintApi(ct); await _wallet._maybeSyncKeys(ct); var mnemonic = _wallet.GetMnemonic()?? - throw new ArgumentNullException("Can't restore wallet without Mnemonic"); + throw new ArgumentNullException(nameof(Mnemonic), "Can't restore wallet without Mnemonic"); _specifiedKeysets ??= (await _wallet.GetKeysets(ct: ct)).Select(k => k.Id).ToList(); if (_specifiedKeysets == null || _specifiedKeysets.Count == 0) { - throw new ArgumentNullException(nameof(_specifiedKeysets)); + throw new InvalidOperationException("No keysets available for restoration. Ensure the mint has at least one keyset or specify keysets explicitly."); } var counter = _wallet.GetCounter(); @@ -62,12 +62,12 @@ public async Task> ProcessAsync(CancellationToken ct = defaul while (emptyBatchesRemaining > 0) { var outputs = await _createBatch(mnemonic, keysetId, batchNumber, ct); - await counter!.IncrementCounter(keysetId, batchNumber * 100); var req = new PostRestoreRequest { Outputs = outputs.Select(o=>o.BlindedMessage).ToArray() }; var res = await api.Restore(req, ct); + await counter!.IncrementCounter(keysetId, 100, ct); if (!res.Signatures.Any()) { @@ -87,8 +87,23 @@ public async Task> ProcessAsync(CancellationToken ct = defaul } var freshProofs = new List(); - var activeUnits = await this._wallet.GetActiveKeysetIdsWithUnits(ct); + // create hash table for every KeysetId : unit. + var allKeysetsUnits = await _wallet.GetKeysetIdsWithUnits(ct); + var unitsForKeysets = new Dictionary(); + if (allKeysetsUnits == null) + { + throw new InvalidOperationException("No keysets available for restoration."); + } + foreach (var unit in allKeysetsUnits) + { + foreach (var keysetId in unit.Value) + { + unitsForKeysets.Add(keysetId, unit.Key); + } + } + + var activeUnits = await this._wallet.GetActiveKeysetIdsWithUnits(ct); if (activeUnits == null || !activeUnits.Any()) { throw new InvalidOperationException("Could not restore wallet without active keysets"); @@ -97,7 +112,14 @@ public async Task> ProcessAsync(CancellationToken ct = defaul foreach (var unitKeyset in activeUnits) { var correspondingKeys = await _wallet.GetKeys(unitKeyset.Value, false, ct); - var totalAmount = recoveredProofs.Select(p=>p.Amount).Aggregate((a,c) => a + c); + + var unit = unitKeyset.Key; + var proofsForUnit = recoveredProofs + .Where(p => unitsForKeysets.TryGetValue(p.Id, out var proofUnit) && proofUnit == unit) + .ToList(); + if (!proofsForUnit.Any()) continue; + var totalAmount = proofsForUnit.Select(p => p.Amount).Sum(); + var amounts = Utils.SplitToProofsAmounts(totalAmount, correspondingKeys.Keys); var ctr = await counter!.GetCounterForId(unitKeyset.Value, ct); var newOutputs = Utils.CreateOutputs(amounts, unitKeyset.Value, correspondingKeys.Keys, mnemonic, ctr); @@ -105,7 +127,7 @@ public async Task> ProcessAsync(CancellationToken ct = defaul var swapRequest = new PostSwapRequest { - Inputs = recoveredProofs.ToArray(), + Inputs = proofsForUnit.ToArray(), Outputs = newOutputs.Select(o=>o.BlindedMessage).ToArray(), }; @@ -117,11 +139,9 @@ public async Task> ProcessAsync(CancellationToken ct = defaul return freshProofs; } - private async Task> _createBatch(Mnemonic mnemonic, KeysetId keysetId, int batchNubmber, CancellationToken ct) + private async Task> _createBatch(Mnemonic mnemonic, KeysetId keysetId, int batchNumber, CancellationToken ct) { var amounts = Enumerable.Repeat((ulong)1, 100).ToList(); - Console.WriteLine(batchNubmber); - Console.WriteLine($"Where does batch start: {batchNubmber*100}"); - return mnemonic.DeriveOutputs(amounts, keysetId, batchNubmber*100); + return mnemonic.DeriveOutputs(amounts, keysetId, batchNumber*100); } } \ No newline at end of file diff --git a/DotNut/Abstractions/SwapBuilder.cs b/DotNut/Abstractions/SwapBuilder.cs index 3af1e98..caaaf62 100644 --- a/DotNut/Abstractions/SwapBuilder.cs +++ b/DotNut/Abstractions/SwapBuilder.cs @@ -115,7 +115,7 @@ public ISwapBuilder ToHTLC(HTLCBuilder htlcBuilder) // P2Bk should be compatible with both p2pk and HTLC. Not implemented in the second one public ISwapBuilder BlindPubkeys(bool withBlinding = true) { - this._shouldBlind = true; + this._shouldBlind = withBlinding; return this; } @@ -136,13 +136,15 @@ public async Task> ProcessAsync(CancellationToken ct = default) throw new InvalidOperationException("Could not fetch Keyset ID"); } var keys = await _wallet.GetKeys(false, ct); - var keysForCurrentId = keys.Single(k=>k.Id == _keysetId); + var keysForCurrentId = keys.SingleOrDefault(k => k.Id == _keysetId) + ?? throw new InvalidOperationException($"Keys for id: {_keysetId} not found in wallet keys"); if (_verifyDLEQ) { foreach (var proof in swapInputs!) { - var keyset = keys.Single(k => k.Id == proof.Id); + var keyset = keys.SingleOrDefault(k => k.Id == proof.Id) + ?? throw new InvalidOperationException($"Keyset with ID {proof.Id} not found for proof verification."); if (!keyset.Keys.TryGetValue(proof.Amount, out var key)) { throw new InvalidOperationException($"Can't find key for amount {proof.Amount} in keyset {keyset.Id}"); @@ -187,33 +189,20 @@ public async Task> ProcessAsync(CancellationToken ct = default) private List _getSwapProofs(CancellationToken ct = default) { _proofsToSwap ??= new(); + if (_tokenString != null) - { + { var token = CashuTokenHelper.Decode(this._tokenString, out var v); - if (v == "A") - { - var mints = token.Tokens.Select(t => t.Mint).ToList(); - if (mints.Count > 1) - { - throw new ArgumentException("Only swap from single mint is allowed"); - } - - } + ValidateSingleMint(token); this._proofsToSwap.AddRange(token.Tokens.SelectMany(t=>t.Proofs)); } - if (_token == null) + if (_token != null) { - return _proofsToSwap; + ValidateSingleMint(_token); + this._proofsToSwap.AddRange(_token.Tokens.SelectMany(t=>t.Proofs)); } - //if token is v1, ensure everything is from the same mint - var tokenMints = _token.Tokens.Select(t => t.Mint).ToList(); - if (tokenMints.Count > 1) - { - throw new ArgumentException("Only swap from single mint is allowed"); - } - this._proofsToSwap.AddRange(_token.Tokens.SelectMany(t=>t.Proofs)); return _proofsToSwap; } @@ -224,7 +213,7 @@ async Task> _getOutputs(Keyset keys, CancellationToken ct = def { if (this._builder is not null) { - throw new ArgumentException("Can't create p2pk outputs if outputs provided. Remove either p2pk builder parameter or outputs."); + throw new ArgumentException("Can't create nut10 outputs by builder if outputs provided. Remove either p2pk builder parameter or outputs."); } return this._outputs; } @@ -272,15 +261,15 @@ private List _getAmounts(ulong total, ulong fee, Keyset keys) { if (_amounts != null) { - var sum = _amounts.Sum(); + var sum = checked(_amounts.Aggregate(0UL, (acc, val) => acc + val)); - if (sum + fee == total) + if (checked(sum + fee) == total) { return _amounts; } if (sum + fee < total) { - var underpay = total - fee - sum; + var underpay = checked(total - fee - sum); this._amounts.AddRange(Utils.SplitToProofsAmounts(underpay, keys)); return this._amounts; } @@ -288,8 +277,17 @@ private List _getAmounts(ulong total, ulong fee, Keyset keys) throw new ArgumentException($"Invalid amounts requested. Sum of amounts: {sum}, total input: {total}, fee:{fee}."); } - this._amounts = Utils.SplitToProofsAmounts(total - fee, keys); + this._amounts = Utils.SplitToProofsAmounts(checked(total - fee), keys); return this._amounts; } + private static void ValidateSingleMint(CashuToken token) + { + var distinctMints = token.Tokens.Select(t => t.Mint).Distinct().ToList(); + if (distinctMints.Count > 1) + { + throw new ArgumentException("Only swap from single mint is allowed"); + } + } + } \ No newline at end of file diff --git a/DotNut/Abstractions/Utils.cs b/DotNut/Abstractions/Utils.cs index 897e8e7..270627e 100644 --- a/DotNut/Abstractions/Utils.cs +++ b/DotNut/Abstractions/Utils.cs @@ -39,7 +39,7 @@ public static List SplitToProofsAmounts(ulong paymentAmount, Keyset keyse /// Active keyset id which will sign outputs /// Keys for given KeysetId /// Blank Outputs - public static List CreateBlankOutputs(ulong amount, KeysetId keysetId, Keyset keys, DotNut.NBitcoin.BIP39.Mnemonic? mnemonic = null, int? counter = null) + public static List CreateBlankOutputs(ulong amount, KeysetId keysetId, Keyset keys, NBitcoin.BIP39.Mnemonic? mnemonic = null, int? counter = null) { if (amount == 0) { @@ -117,7 +117,7 @@ public static List CreateOutputs( { var secret = RandomSecret(); var r = RandomPrivkey(); - var B_ = DotNut.Cashu.ComputeB_(secret.ToCurve(), r); + var B_ = Cashu.ComputeB_(secret.ToCurve(), r); var output = new OutputData { BlindedMessage = new BlindedMessage { Amount = amount, B_ = B_, Id = keysetId }, @@ -238,7 +238,7 @@ public static OutputData CreateNut10BlindedOutput(ulong amount, KeysetId keysetI public static Proof ConstructProofFromPromise( BlindSignature promise, PrivKey r, - DotNut.ISecret secret, + ISecret secret, PubKey amountPubkey, PubKey? P2PkE = null) { @@ -246,9 +246,10 @@ public static Proof ConstructProofFromPromise( //unblind signature var C = Cashu.ComputeC(promise.C_, r, amountPubkey); + DLEQProof? dleq = null; if (promise.DLEQ is not null) { - promise.DLEQ = new DLEQProof + dleq = new DLEQProof { E = promise.DLEQ.E, S = promise.DLEQ.S, @@ -262,7 +263,7 @@ public static Proof ConstructProofFromPromise( Amount = promise.Amount, Secret = secret, C = C, - DLEQ = promise.DLEQ, + DLEQ = dleq, P2PkE = P2PkE }; } @@ -274,9 +275,9 @@ Keyset keys ) { List proofs = new List(); - for (int i = promises.Count() - 1; i >= 0; i--) + for (int i = 0; i < promises.Count; i++) { - if (!keys.TryGetValue(promises[i].Amount, out PubKey key)) + if (!keys.TryGetValue(promises[i].Amount, out var key)) { throw new ArgumentException($"Provided keyset doesn't contain PubKey for amount {promises[i].Amount}" ); } diff --git a/DotNut/Abstractions/Wallet.cs b/DotNut/Abstractions/Wallet.cs index c9959b2..f22e099 100644 --- a/DotNut/Abstractions/Wallet.cs +++ b/DotNut/Abstractions/Wallet.cs @@ -26,7 +26,7 @@ public class Wallet : IWalletBuilder //flags private bool _shouldSyncKeyset = true; private DateTime? _lastSync = DateTime.MinValue; - private TimeSpan? _syncThresold; // if null sync only once + private TimeSpan? _syncThreshold; // if null sync only once private bool _shouldBumpCounter = true; private bool _allowInvalidKeysetIds = false; @@ -80,10 +80,10 @@ public IWalletBuilder WithKeysetSync(bool syncKeyset = true) return this; } - public IWalletBuilder WithKeysetSync(bool syncKeyset, TimeSpan syncThreesold) + public IWalletBuilder WithKeysetSync(bool syncKeyset, TimeSpan syncThreshold) { this._shouldSyncKeyset = syncKeyset; - this._syncThresold = syncThreesold; + this._syncThreshold = syncThreshold; return this; } @@ -192,12 +192,20 @@ public void InvalidateCache() .FirstOrDefault(k => k is { Active: true } && k.Unit == unit, null) ?.Id; } - - - public async Task?> GetActiveKeysetIdsWithUnits(CancellationToken ct = default) + public async Task>?> GetKeysetIdsWithUnits(CancellationToken ct = default) { await _maybeSyncKeys(ct); return _keysets? + .GroupBy(k => k.Unit) + .ToDictionary( + g => g.Key, + g => g.OrderBy(k => k.InputFee).Select(k => k.Id).ToList() + ); + } + public async Task?> GetActiveKeysetIdsWithUnits(CancellationToken ct = default) + { + await _maybeSyncKeys(ct); + return _keysets?.Where(k=>k.Active) .GroupBy(k => k.Unit) .ToDictionary( g => g.Key, @@ -226,7 +234,9 @@ public void InvalidateCache() { throw new ArgumentNullException(nameof(this._keys), "Wallet doesn't contain keys for this keyset!"); } - return this._keys.Single(k => k.Id == id); + + return this._keys.SingleOrDefault(k => k.Id == id) + ?? throw new InvalidOperationException($"Keys for keyset ID {id} not found in wallet"); } public async Task> GetKeysets(bool forceRefresh = false, CancellationToken ct = default) @@ -241,9 +251,9 @@ public void InvalidateCache() } - public async Task GetInfo(bool forceReferesh = false, CancellationToken ct = default) + public async Task GetInfo(bool forceRefresh = false, CancellationToken ct = default) { - if (forceReferesh) + if (forceRefresh) { return await _fetchMintInfo(ct); } @@ -303,7 +313,7 @@ public async Task GetMintApi(CancellationToken ct = default) _ensureApiConnected(); return _mintApi; } - public async Task? GetSelector(CancellationToken ct = default) + public async Task GetSelector(CancellationToken ct = default) { if (this._selector == null) { @@ -433,12 +443,12 @@ internal async Task _maybeSyncKeys(CancellationToken cts = default) return; } // should sync keysets SINGLE time in the lifespan of object. If already synced - return; - if (_syncThresold == null && _lastSync != DateTime.MinValue) + if (_syncThreshold == null && _lastSync != DateTime.MinValue) { return; } // should sync keysets in some timepsan - if (_syncThresold != null && _lastSync + _syncThresold >= DateTime.Now) + if (_syncThreshold != null && _lastSync + _syncThreshold >= DateTime.UtcNow) { return; } @@ -462,10 +472,11 @@ internal async Task _maybeSyncKeys(CancellationToken cts = default) foreach (var unknownKeyset in unknownKeysets) { var keyset = await this._fetchKeys(unknownKeyset.Id, cts); + _lastSync = DateTime.UtcNow; this._keys.Add(keyset); } - _lastSync = DateTime.Now; + _lastSync = DateTime.UtcNow; } } diff --git a/DotNut/Abstractions/Websockets/Subscription.cs b/DotNut/Abstractions/Websockets/Subscription.cs index 87da2f0..3e6e46b 100644 --- a/DotNut/Abstractions/Websockets/Subscription.cs +++ b/DotNut/Abstractions/Websockets/Subscription.cs @@ -9,9 +9,9 @@ public class Subscription public SubscriptionKind Kind { get; set; } public string[] Filters { get; set; } = Array.Empty(); public DateTime CreatedAt { get; set; } = DateTime.UtcNow; - public Channel NotificationChannel { get; set; } + public Channel NotificationChannel { get; set; } = Channel.CreateUnbounded(); - public EventHandler OnError { get; set; } + public EventHandler? OnError { get; set; } private readonly WeakReference? _serviceRef; diff --git a/DotNut/Abstractions/Websockets/WebsocketConnection.cs b/DotNut/Abstractions/Websockets/WebsocketConnection.cs index 5cfa388..9c7315f 100644 --- a/DotNut/Abstractions/Websockets/WebsocketConnection.cs +++ b/DotNut/Abstractions/Websockets/WebsocketConnection.cs @@ -29,12 +29,12 @@ public override int GetHashCode() public static bool operator ==(WebsocketConnection? left, WebsocketConnection? right) { - return Equals(left, right); + return object.Equals(left, right); } public static bool operator !=(WebsocketConnection? left, WebsocketConnection? right) { - return !Equals(left, right); + return !object.Equals(left, right); } } diff --git a/DotNut/Abstractions/Websockets/WebsocketModels.cs b/DotNut/Abstractions/Websockets/WebsocketModels.cs index 98a130d..8df82f6 100644 --- a/DotNut/Abstractions/Websockets/WebsocketModels.cs +++ b/DotNut/Abstractions/Websockets/WebsocketModels.cs @@ -109,6 +109,6 @@ public sealed record Failure(int Code, string Message, int RequestId) : RequestR internal class PendingRequest { - public TaskCompletionSource Tcs { get; set; } - public string SubscriptionId { get; set; } + public required TaskCompletionSource Tcs { get; set; } + public required string SubscriptionId { get; set; } } \ No newline at end of file diff --git a/DotNut/Abstractions/Websockets/WebsocketService.cs b/DotNut/Abstractions/Websockets/WebsocketService.cs index 0eb3151..5005b34 100644 --- a/DotNut/Abstractions/Websockets/WebsocketService.cs +++ b/DotNut/Abstractions/Websockets/WebsocketService.cs @@ -1,5 +1,4 @@ using System.Collections.Concurrent; -using System.Net; using System.Net.WebSockets; using System.Text; using System.Text.Json; @@ -48,7 +47,7 @@ public async Task ConnectAsync(string mintUrl, Cancellation _connections[normalized] = connection; OnConnectionStateChanged(connectionId, WebSocketState.Open); - _ = Task.Run(async () => await ListenForMessages(connection, ct), ct); + _ = Task.Run(async () => await ListenForMessages(connection, CancellationToken.None)); return connection; } @@ -59,7 +58,7 @@ public async Task LazyConnectAsync(string mintUrl, Cancella if (_connections.TryGetValue(normalized, out var existing)) { - if (existing.State == WebSocketState.Open) + if (existing is { State: WebSocketState.Open, WebSocket.State: WebSocketState.Open }) { return existing; } @@ -93,6 +92,7 @@ await connection.WebSocket.CloseAsync( } finally { + connection.State = WebSocketState.Closed; connection.WebSocket.Dispose(); _connections.TryRemove(normalized, out _); @@ -172,7 +172,19 @@ public async Task SubscribeAsync(string mintUrl, SubscriptionKind using var cts = CancellationTokenSource.CreateLinkedTokenSource(ct); cts.CancelAfter(TimeSpan.FromSeconds(30)); - var result = await tcs.Task.ConfigureAwait(false); + var completedTask = await Task.WhenAny( + tcs.Task, + Task.Delay(Timeout.Infinite, cts.Token) + ).ConfigureAwait(false); + + if (completedTask != tcs.Task) + { + _subscriptions.TryRemove(subId, out _); + await subscription.CloseAsync(); + throw new TimeoutException("Subscription request timed out"); + } + + var result = await tcs.Task; if (result is RequestResult.Failure failure) { @@ -230,6 +242,16 @@ public async Task UnsubscribeAsync(string subId, CancellationToken ct = default) using var cts = CancellationTokenSource.CreateLinkedTokenSource(ct); cts.CancelAfter(TimeSpan.FromSeconds(30)); + var completed = await Task.WhenAny( + tcs.Task, + Task.Delay(Timeout.Infinite, cts.Token) + ).ConfigureAwait(false); + + if (completed != tcs.Task) + { + throw new TimeoutException("Unsubscribe request timed out"); + } + await tcs.Task.ConfigureAwait(false); } finally @@ -285,6 +307,7 @@ public IEnumerable GetConnections() private async Task ListenForMessages(WebsocketConnection connection, CancellationToken ct) { var buffer = new byte[4096]; + var messageBuffer = new MemoryStream(); try { @@ -301,8 +324,13 @@ private async Task ListenForMessages(WebsocketConnection connection, Cancellatio if (result.MessageType == WebSocketMessageType.Text) { - var message = Encoding.UTF8.GetString(buffer, 0, result.Count); - _processMessage(connection, message); + messageBuffer.Write(buffer, 0, result.Count); + if (result.EndOfMessage) + { + var message = Encoding.UTF8.GetString(messageBuffer.ToArray()); + messageBuffer.SetLength(0); + _processMessage(connection, message); + } } } } @@ -324,7 +352,11 @@ private async Task ListenForMessages(WebsocketConnection connection, Cancellatio foreach (var sub in subscriptionsToClose) { - sub.CloseAsync(); + try + { + await sub.CloseAsync(); + } catch {} + _subscriptions.TryRemove(sub.Id, out _); } } } diff --git a/DotNut/Api/CashuHttpClient.cs b/DotNut/Api/CashuHttpClient.cs index 32a9694..9b70e72 100644 --- a/DotNut/Api/CashuHttpClient.cs +++ b/DotNut/Api/CashuHttpClient.cs @@ -12,6 +12,7 @@ public class CashuHttpClient : ICashuApi public CashuHttpClient(HttpClient httpClient) { + ArgumentNullException.ThrowIfNull(httpClient); ArgumentNullException.ThrowIfNull(httpClient.BaseAddress); _httpClient = httpClient; } diff --git a/DotNut/ApiModels/Info/MPPInfo.cs b/DotNut/ApiModels/Info/MPPInfo.cs index d1dce29..55aa887 100644 --- a/DotNut/ApiModels/Info/MPPInfo.cs +++ b/DotNut/ApiModels/Info/MPPInfo.cs @@ -2,7 +2,7 @@ namespace DotNut.ApiModels.Info; -public class MmpInfo +public class MPPInfo { [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] [JsonPropertyName("methods")] diff --git a/DotNut/ApiModels/Info/SwapInfo.cs b/DotNut/ApiModels/Info/SwapInfo.cs index d0b3a8b..27ddb78 100644 --- a/DotNut/ApiModels/Info/SwapInfo.cs +++ b/DotNut/ApiModels/Info/SwapInfo.cs @@ -31,7 +31,7 @@ public class SwapMethod public class SwapOptions { [JsonPropertyName("description")] - public bool? Description; + public bool? Description {get; set;} } } } diff --git a/DotNut/ApiModels/Mint/bolt11/PostMintQuoteBolt11Request.cs b/DotNut/ApiModels/Mint/bolt11/PostMintQuoteBolt11Request.cs index 0faca17..dc73c43 100644 --- a/DotNut/ApiModels/Mint/bolt11/PostMintQuoteBolt11Request.cs +++ b/DotNut/ApiModels/Mint/bolt11/PostMintQuoteBolt11Request.cs @@ -1,4 +1,3 @@ -using System.Diagnostics; using System.Text.Json.Serialization; namespace DotNut.ApiModels; diff --git a/DotNut/Encoding/CashuTokenV4Encoder.cs b/DotNut/Encoding/CashuTokenV4Encoder.cs index e4d5ea9..1f39bc6 100644 --- a/DotNut/Encoding/CashuTokenV4Encoder.cs +++ b/DotNut/Encoding/CashuTokenV4Encoder.cs @@ -76,14 +76,6 @@ public CBORObject ToCBORObject(CashuToken token) public CashuToken FromCBORObject(CBORObject obj) { - var peValue = obj.GetOrDefault("pe", null); - Console.WriteLine($"pe exists: {peValue != null}"); - if (peValue != null) - { - Console.WriteLine($"pe type: {peValue.Type}"); - Console.WriteLine($"pe bytes length: {peValue.GetByteString()?.Length}"); - } - return new CashuToken { Unit = obj["u"].AsString(), diff --git a/DotNut/NUT18/PaymentRequestEncoder.cs b/DotNut/Encoding/PaymentRequestEncoder.cs similarity index 96% rename from DotNut/NUT18/PaymentRequestEncoder.cs rename to DotNut/Encoding/PaymentRequestEncoder.cs index d29e88b..779cbd4 100644 --- a/DotNut/NUT18/PaymentRequestEncoder.cs +++ b/DotNut/Encoding/PaymentRequestEncoder.cs @@ -70,6 +70,11 @@ public CBORObject ToCBORObject(PaymentRequest paymentRequest) } cbor.Add("nut10", nut10Obj); } + + if (paymentRequest.Nut26 is {} nut26) + { + cbor.Add("nut26", nut26); + } return cbor; } @@ -153,6 +158,9 @@ public PaymentRequest FromCBORObject(CBORObject obj) } paymentRequest.Nut10 = lockingCondition; break; + case "nut26": + paymentRequest.Nut26 = value.AsBoolean(); + break; } } return paymentRequest; diff --git a/DotNut/NUT01/Keyset.cs b/DotNut/NUT01/Keyset.cs index aceb73a..20e4329 100644 --- a/DotNut/NUT01/Keyset.cs +++ b/DotNut/NUT01/Keyset.cs @@ -96,7 +96,7 @@ public bool VerifyKeysetId(KeysetId keysetId, string? unit = null, ulong? inputF var derived = GetKeysetId(version, unit, inputFeePpk, finalExpiration).ToString(); var presented = keysetId.ToString(); if (presented.Length > derived.Length) return false; - return string.Equals(derived, presented, StringComparison.Ordinal) || - derived.StartsWith(presented, StringComparison.Ordinal); + return string.Equals(derived, presented, StringComparison.InvariantCultureIgnoreCase) || + derived.StartsWith(presented, StringComparison.InvariantCultureIgnoreCase); } } \ No newline at end of file diff --git a/DotNut/NUT02/FeeHelper.cs b/DotNut/NUT02/FeeHelper.cs index 05a6312..23802d0 100644 --- a/DotNut/NUT02/FeeHelper.cs +++ b/DotNut/NUT02/FeeHelper.cs @@ -19,8 +19,10 @@ public static ulong ComputeFee(this IEnumerable proofsToSpend, Dictionary return (sum + 999) / 1000; } - public static ulong Sum(this IEnumerable ul) + public static ulong Sum(this IEnumerable values) { - return ul.Aggregate((x, y) => x + y); + ArgumentNullException.ThrowIfNull(values); + return values.Aggregate(0, (current, v) => current + v); } + } \ No newline at end of file diff --git a/DotNut/NUT11/SigAllHandler.cs b/DotNut/NUT11/SigAllHandler.cs index 8f92d13..bc5e3e1 100644 --- a/DotNut/NUT11/SigAllHandler.cs +++ b/DotNut/NUT11/SigAllHandler.cs @@ -1,8 +1,5 @@ -using System.Security.Cryptography; using System.Text; -using System.Text.Encodings.Web; using System.Text.Json; -using DotNut; using NBitcoin.Secp256k1; namespace DotNut; @@ -16,14 +13,15 @@ public class SigAllHandler public string? HTLCPreimage { get; set; } public string? MeltQuoteId { get; set; } - private P2PKProofSecret? _firstProofSecret; + private Nut10ProofSecret? _firstProofSecret; public bool TrySign(out P2PKWitness? p2pkwitness) { p2pkwitness = null; - if (BlindedMessages.Count == 0 || Proofs.Count == 0) + if ( BlindedMessages is null || Proofs is null || PrivKeys is null || + BlindedMessages.Count == 0 || Proofs.Count == 0 || PrivKeys.Count == 0) { return false; } @@ -32,14 +30,24 @@ public bool TrySign(out P2PKWitness? p2pkwitness) try { var msgStr = GetMessageToSign(Proofs.ToArray(), BlindedMessages.ToArray(), MeltQuoteId); + if (!ValidateFirstProof(Proofs[0], out var sec) || sec is null) + { + return false; + } + _firstProofSecret = sec; msg = Encoding.UTF8.GetBytes(msgStr); } - catch (Exception _) + catch (ArgumentException) + { + return false; + } + + if (_firstProofSecret is not P2PKProofSecret ps) { return false; } - if (_firstProofSecret is HTLCProofSecret s && HTLCPreimage is {} preimage) + if (ps is HTLCProofSecret s && HTLCPreimage is {} preimage) { if (Proofs.First().P2PkE is { } E) { @@ -58,43 +66,49 @@ public bool TrySign(out P2PKWitness? p2pkwitness) return true; } + if (Proofs.First().P2PkE is { } e2) { - p2pkwitness = _firstProofSecret!.GenerateBlindWitness(msg, PrivKeys.Select(pk => (ECPrivKey)pk).ToArray(), Proofs[0].Id, e2); + p2pkwitness = ps.GenerateBlindWitness(msg, PrivKeys.Select(pk => (ECPrivKey)pk).ToArray(), Proofs[0].Id, e2); return true; } - p2pkwitness = _firstProofSecret!.GenerateWitness(msg, PrivKeys.Select(pk => (ECPrivKey)pk).ToArray()); + p2pkwitness = ps.GenerateWitness(msg, PrivKeys.Select(pk => (ECPrivKey)pk).ToArray()); return true; } public static string GetMessageToSign(Proof[] inputs, BlindedMessage[] outputs, string? meltQuoteId = null) { + if (inputs is null || inputs.Length == 0) + { + throw new ArgumentException("At least one proof is required for SIG_ALL.", nameof(inputs)); + } + if (outputs is null || outputs.Length == 0) + { + throw new ArgumentException("At least one blinded output is required for SIG_ALL.", nameof(outputs)); + } if (!ValidateFirstProof(inputs[0], out var firstSecret)) { throw new ArgumentException("Provided first proof is invalid"); } var msg = new StringBuilder(); - if (inputs.Length > 0) + for (var i = 0; i < inputs.Length; i++) { - for (var i = 0; i < inputs.Length; i++) - { - var p = inputs[i]; + var p = inputs[i]; - if (p.Secret is not Nut10Secret nut10) - { - throw new ArgumentException($"When signing sig_all, every proof must be sig_all."); - } + if (p.Secret is not Nut10Secret nut10) + { + throw new ArgumentException("When signing sig_all, every proof must be a nut 10 secret."); + } - if (!CheckIfEqualToFirst(firstSecret, nut10.ProofSecret)) - { - throw new ArgumentException($"When signing sig_all, every proof must have identical tags and data."); - } - // serialize as raw object - var secret = JsonSerializer.Serialize((object)p.Secret); - msg.Append(secret); - msg.Append(p.C); + if (!CheckIfEqualToFirst(firstSecret, nut10.ProofSecret)) + { + throw new ArgumentException("When signing sig_all, every proof must have identical tags and data."); } + // serialize as raw object + var secret = JsonSerializer.Serialize((object)p.Secret); + msg.Append(secret); + msg.Append(p.C); } foreach (var b in outputs) @@ -116,8 +130,10 @@ public static bool VerifySigAllWitness( P2PKWitness witness, string? meltQuoteId = null) { - if (proofs[0].Secret is Nut10Secret nut10_3) - Console.WriteLine($"CP3 ProofSecret: {nut10_3.ProofSecret.GetType()}"); + if (proofs is null || proofs.Length == 0) + { + return false; + } byte[] msg; try { @@ -129,7 +145,6 @@ public static bool VerifySigAllWitness( } catch(Exception ex) { - Console.WriteLine(ex.Message); return false; } @@ -146,6 +161,10 @@ public static bool VerifySigAllWitness( public static bool VerifySigAllWitness(Proof[] proofs, BlindedMessage[] blindedMessages, string? meltQuoteId = null) { + if (proofs is null || proofs.Length == 0) + { + return false; + } var firstProof = proofs.FirstOrDefault(); if (firstProof?.Secret is not Nut10Secret { ProofSecret: var proofSecret } || firstProof.Witness is null) return false; @@ -170,7 +189,7 @@ public static bool VerifySigAllWitness(Proof[] proofs, BlindedMessage[] blindedM return witness is not null && VerifySigAllWitness(proofs, blindedMessages, witness, meltQuoteId); } - private static bool ValidateFirstProof(Proof firstProof, out Nut10ProofSecret secret) + private static bool ValidateFirstProof(Proof firstProof, out Nut10ProofSecret? secret) { secret = null; @@ -199,6 +218,8 @@ private static bool ValidateFirstProof(Proof firstProof, out Nut10ProofSecret se private static bool CheckIfEqualToFirst(Nut10ProofSecret first, Nut10ProofSecret other) => first is { } a && other is { } b && a.Data == b.Data && - ((a.Tags == null && b.Tags == null) || - (a.Tags != null && b.Tags != null && a.Tags.SequenceEqual(b.Tags))); + ((a.Tags == null && b.Tags == null) || + (a.Tags != null && b.Tags != null && + a.Tags.Length == b.Tags.Length && + a.Tags.Zip(b.Tags).All(pair => pair.First.SequenceEqual(pair.Second)))); } \ No newline at end of file diff --git a/DotNut/NUT13/Nut13.cs b/DotNut/NUT13/Nut13.cs index 0337ba6..f018c24 100644 --- a/DotNut/NUT13/Nut13.cs +++ b/DotNut/NUT13/Nut13.cs @@ -36,7 +36,7 @@ public static List DeriveOutputs(this Mnemonic mnemonic, IEnumerable { BlindedMessage = new BlindedMessage() { - Amount = amountList.ElementAt(i), + Amount = amountList[i], Id = keysetId, B_ = B_ }, diff --git a/DotNut/NUT14/HTLCProofSecret.cs b/DotNut/NUT14/HTLCProofSecret.cs index a669718..19ded8f 100644 --- a/DotNut/NUT14/HTLCProofSecret.cs +++ b/DotNut/NUT14/HTLCProofSecret.cs @@ -35,9 +35,10 @@ public HTLCWitness GenerateWitness(byte[] msg, ECPrivKey[] keys, byte[] preimage public HTLCWitness GenerateWitness(ECPrivKey hash, ECPrivKey[] keys, byte[] preimage) { - // validate hash only if there' + // validate hash only if secret is not expired. var builder = Builder; - if (!builder.Lock.HasValue || builder.Lock.Value.ToUnixTimeSeconds() > DateTimeOffset.Now.ToUnixTimeSeconds()) + if (!builder.Lock.HasValue || + builder.Lock.Value.ToUnixTimeSeconds() > DateTimeOffset.Now.ToUnixTimeSeconds()) { if (!VerifyPreimage(preimage)) throw new InvalidOperationException("Invalid preimage"); diff --git a/DotNut/NUT18/PaymentRequest.cs b/DotNut/NUT18/PaymentRequest.cs index 2f9af49..526c178 100644 --- a/DotNut/NUT18/PaymentRequest.cs +++ b/DotNut/NUT18/PaymentRequest.cs @@ -12,6 +12,7 @@ public class PaymentRequest public string? Memo { get; set; } public PaymentRequestTransport[] Transports { get; set; } public Nut10LockingCondition? Nut10 { get; set; } + public bool? Nut26 { get; set; } public override string ToString() { From 4323a5d2f6f0841a7d8d1ebdafb29330dede66bf Mon Sep 17 00:00:00 2001 From: d4rp4t Date: Mon, 1 Dec 2025 01:18:58 +0100 Subject: [PATCH 30/70] fix: expired keysets --- DotNut.Tests/Integration.cs | 2 +- .../Handlers/MeltHandlerBolt11.cs | 2 +- .../Handlers/MeltHandlerBolt12.cs | 2 +- .../Abstractions/Interfaces/IWalletBuilder.cs | 3 +- DotNut/Abstractions/MintQuoteBuilder.cs | 6 +-- DotNut/Abstractions/RestoreBuilder.cs | 18 +++++++- DotNut/Abstractions/Wallet.cs | 42 ++++++++++++------- 7 files changed, 51 insertions(+), 24 deletions(-) diff --git a/DotNut.Tests/Integration.cs b/DotNut.Tests/Integration.cs index fe76fd1..bbfa1f9 100644 --- a/DotNut.Tests/Integration.cs +++ b/DotNut.Tests/Integration.cs @@ -278,7 +278,7 @@ public async Task MeltsSuccessfully() // .ProcessAsyncBolt12(); // // // select proofs to send - // var q = await meltQuote.GetQuote(); + // var q = meltQuote.GetQuote(); // var selectedProofs = await wallet.SelectProofsToSend(mintedProofs, q.Amount + (ulong)q.FeeReserve, true); // // //melt proofs diff --git a/DotNut/Abstractions/Handlers/MeltHandlerBolt11.cs b/DotNut/Abstractions/Handlers/MeltHandlerBolt11.cs index e625cd9..3b190fb 100644 --- a/DotNut/Abstractions/Handlers/MeltHandlerBolt11.cs +++ b/DotNut/Abstractions/Handlers/MeltHandlerBolt11.cs @@ -28,7 +28,7 @@ public async Task> Melt(List inputs, CancellationToken ct = d return []; } - var keyset = await wallet.GetKeys(res.Change.First().Id, false, ct); + var keyset = await wallet.GetKeys(res.Change.First().Id, true, false, ct); return Utils.ConstructProofsFromPromises(res.Change.ToList(), blankOutputs, keyset.Keys); } } \ No newline at end of file diff --git a/DotNut/Abstractions/Handlers/MeltHandlerBolt12.cs b/DotNut/Abstractions/Handlers/MeltHandlerBolt12.cs index fb80e2e..dec15a8 100644 --- a/DotNut/Abstractions/Handlers/MeltHandlerBolt12.cs +++ b/DotNut/Abstractions/Handlers/MeltHandlerBolt12.cs @@ -29,7 +29,7 @@ public async Task> Melt(List inputs, CancellationToken ct = d return []; } - var keyset = await wallet.GetKeys(res.Change.First().Id, false, ct); + var keyset = await wallet.GetKeys(res.Change.First().Id, true, false, ct); return Utils.ConstructProofsFromPromises(res.Change.ToList(), blankOutputs, keyset.Keys); } } \ No newline at end of file diff --git a/DotNut/Abstractions/Interfaces/IWalletBuilder.cs b/DotNut/Abstractions/Interfaces/IWalletBuilder.cs index f0077d6..3645fd6 100644 --- a/DotNut/Abstractions/Interfaces/IWalletBuilder.cs +++ b/DotNut/Abstractions/Interfaces/IWalletBuilder.cs @@ -186,11 +186,12 @@ public interface IWalletBuilder /// Get Keys for given KeysetID /// /// KeysetId + /// If keyset not present not in db, it can be fetched /// Refetch flag /// /// Keys for given keyset /// If wallet doesn't contain keysets for given keysetId - Task GetKeys(KeysetId id, bool forceRefresh = false, + Task GetKeys(KeysetId id, bool allowFetch, bool forceRefresh = false, CancellationToken ct = default); /// diff --git a/DotNut/Abstractions/MintQuoteBuilder.cs b/DotNut/Abstractions/MintQuoteBuilder.cs index 505d7ef..05b88e6 100644 --- a/DotNut/Abstractions/MintQuoteBuilder.cs +++ b/DotNut/Abstractions/MintQuoteBuilder.cs @@ -102,7 +102,7 @@ public async Task>> Proces throw new ArgumentNullException(nameof(_amount), "can't create mint quote without amount!"); } - var api = await this._wallet.GetMintApi(); + var api = await this._wallet.GetMintApi(ct); if (api is null) { throw new ArgumentNullException(nameof(ICashuApi), "Can't request mint quote without mint API"); @@ -111,7 +111,7 @@ public async Task>> Proces this._keysetId ??= await this._wallet.GetActiveKeysetId(this._unit, ct) ?? throw new ArgumentException($"Can't get active keyset ID for unit: {_unit}"); - this._keyset ??= await this._wallet.GetKeys(this._keysetId, false, ct) ?? + this._keyset ??= await this._wallet.GetKeys(this._keysetId, true, false, ct) ?? throw new ArgumentException($"Cant get keys for keysetId: {_keysetId}"); var outputs = await this._createOutputs(); @@ -156,7 +156,7 @@ public async Task>> Proces if (this._keyset == null) { - this._keyset = await this._wallet.GetKeys(this._keysetId, false, ct) ?? + this._keyset = await this._wallet.GetKeys(this._keysetId, true, false, ct) ?? throw new ArgumentException($"Cant fetch keys for keysetId: {_keysetId}"); } diff --git a/DotNut/Abstractions/RestoreBuilder.cs b/DotNut/Abstractions/RestoreBuilder.cs index 8416976..dee7d28 100644 --- a/DotNut/Abstractions/RestoreBuilder.cs +++ b/DotNut/Abstractions/RestoreBuilder.cs @@ -57,7 +57,21 @@ public async Task> ProcessAsync(CancellationToken ct = defaul int batchNumber = 0; int emptyBatchesRemaining = 3; - var keyset = await _wallet.GetKeys(keysetId, false, ct); + GetKeysResponse.KeysetItemResponse? keyset; + + try + { + keyset = await _wallet.GetKeys(keysetId, true, false, ct); + } + catch (Exception e) + { + continue; + } + + if (keyset == null) + { + continue; + } while (emptyBatchesRemaining > 0) { @@ -111,7 +125,7 @@ public async Task> ProcessAsync(CancellationToken ct = defaul foreach (var unitKeyset in activeUnits) { - var correspondingKeys = await _wallet.GetKeys(unitKeyset.Value, false, ct); + var correspondingKeys = await _wallet.GetKeys(unitKeyset.Value, true, false, ct); var unit = unitKeyset.Key; var proofsForUnit = recoveredProofs diff --git a/DotNut/Abstractions/Wallet.cs b/DotNut/Abstractions/Wallet.cs index f22e099..11c0444 100644 --- a/DotNut/Abstractions/Wallet.cs +++ b/DotNut/Abstractions/Wallet.cs @@ -29,7 +29,6 @@ public class Wallet : IWalletBuilder private TimeSpan? _syncThreshold; // if null sync only once private bool _shouldBumpCounter = true; - private bool _allowInvalidKeysetIds = false; /* @@ -224,19 +223,29 @@ public void InvalidateCache() return this._keys ?? []; } - public async Task GetKeys(KeysetId id, bool forceRefresh = false, CancellationToken ct = default) + public async Task GetKeys(KeysetId id, bool allowFetch = true, bool forceRefresh = false, CancellationToken ct = default) { if (forceRefresh) { return await _fetchKeys(id, ct); } - if (this._keys == null) + + var localKeyset = this._keys?.SingleOrDefault(k => k.Id == id); + if (localKeyset != null) { - throw new ArgumentNullException(nameof(this._keys), "Wallet doesn't contain keys for this keyset!"); + return localKeyset; } - return this._keys.SingleOrDefault(k => k.Id == id) - ?? throw new InvalidOperationException($"Keys for keyset ID {id} not found in wallet"); + if (allowFetch) + { + var keyset = await _fetchKeys(id, ct); + if (keyset != null && _keys != null) + { + _keys.Add(keyset); + } + } + + throw new ArgumentException("No keys found for this keyset!"); } public async Task> GetKeysets(bool forceRefresh = false, CancellationToken ct = default) @@ -311,7 +320,7 @@ public async Task SelectProofsToSend(List proofs, ulong amo public async Task GetMintApi(CancellationToken ct = default) { _ensureApiConnected(); - return _mintApi; + return _mintApi!; } public async Task GetSelector(CancellationToken ct = default) { @@ -396,17 +405,19 @@ internal void _ensureApiConnected(string? msg = null) /// Keys /// May be thrown if mint returns invalid keysetId for at least one Keyset /// May be thrown if mint is not set. - private async Task _fetchKeys(KeysetId id, CancellationToken cts = default) + private async Task _fetchKeys(KeysetId id, CancellationToken cts = default) { _ensureApiConnected("Can't fetch keys without mint api!"); - var keysRaw = (await _mintApi!.GetKeys(id, cts)).Keysets.Single(); - + var keysRaw = (await _mintApi!.GetKeys(id, cts)).Keysets.SingleOrDefault(); + if (keysRaw == null) + { + return null; + } var isKeysetIdValid = keysRaw.Keys.VerifyKeysetId(keysRaw.Id, keysRaw.Unit, keysRaw.FinalExpiry); if (!isKeysetIdValid) { throw new ArgumentException($"Mint provided invalid keysetId. Provided: {keysRaw.Id}, derived: {keysRaw.Keys.GetKeysetId()} "); } - return keysRaw; } @@ -461,8 +472,7 @@ internal async Task _maybeSyncKeys(CancellationToken cts = default) } var knownIds = _keys.Select(key => key.Id).ToHashSet(); - var unknownKeysets = _keysets.Where(k => !knownIds.Contains(k.Id)).ToList(); - + var unknownKeysets = _keysets.Where(k => !knownIds.Contains(k.Id) && k.Active).ToList(); if (unknownKeysets.Count > 2) // just make a single request. May override stored keys. { this._keys = await _fetchKeys(cts); @@ -473,11 +483,13 @@ internal async Task _maybeSyncKeys(CancellationToken cts = default) { var keyset = await this._fetchKeys(unknownKeyset.Id, cts); _lastSync = DateTime.UtcNow; - this._keys.Add(keyset); + if (keyset != null) + { + _keys.Add(keyset); + } } _lastSync = DateTime.UtcNow; } - } From 2a8bcf4fb7cc6f6d4f5b94dff3fa13684999614e Mon Sep 17 00:00:00 2001 From: d4rp4t Date: Mon, 1 Dec 2025 15:48:29 +0100 Subject: [PATCH 31/70] don't assume the order of returned restored signatures --- DotNut.Tests/Integration.cs | 4 +- .../Interfaces/IRestoreBuilder.cs | 1 - .../Abstractions/Interfaces/IWalletBuilder.cs | 5 +- DotNut/Abstractions/RestoreBuilder.cs | 110 +++++++++--------- DotNut/Abstractions/SwapBuilder.cs | 49 ++++---- DotNut/Abstractions/Utils.cs | 1 - DotNut/Abstractions/Wallet.cs | 8 +- 7 files changed, 94 insertions(+), 84 deletions(-) diff --git a/DotNut.Tests/Integration.cs b/DotNut.Tests/Integration.cs index bbfa1f9..d41a340 100644 --- a/DotNut.Tests/Integration.cs +++ b/DotNut.Tests/Integration.cs @@ -8,6 +8,7 @@ namespace DotNut.Tests; public class Integration { private static string MintUrl = "http://localhost:3338"; + // private static string MintUrl = "https://testnut.cashu.space"; private static string seed = "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about"; @@ -145,10 +146,9 @@ public async Task RestoresSuccessfully() .WithMnemonic(seed); var restoredProofs = await wallet .Restore() - .WithSwap(false) .ProcessAsync(); var keyset = (await wallet.GetKeys()).First().Keys; - var expectedAmount = Utils.SplitToProofsAmounts(1337UL, keyset).Count; + var expectedAmount = Utils.SplitToProofsAmounts(1336UL, keyset).Count; // (one for fee) Assert.Equal(expectedAmount, restoredProofs.Count()); } diff --git a/DotNut/Abstractions/Interfaces/IRestoreBuilder.cs b/DotNut/Abstractions/Interfaces/IRestoreBuilder.cs index b6a81ea..973250a 100644 --- a/DotNut/Abstractions/Interfaces/IRestoreBuilder.cs +++ b/DotNut/Abstractions/Interfaces/IRestoreBuilder.cs @@ -6,6 +6,5 @@ namespace DotNut.Abstractions; public interface IRestoreBuilder { IRestoreBuilder ForKeysetIds(IEnumerable keysetIds); - IRestoreBuilder WithSwap(bool shouldSwap = true); Task> ProcessAsync(CancellationToken ct = default); } diff --git a/DotNut/Abstractions/Interfaces/IWalletBuilder.cs b/DotNut/Abstractions/Interfaces/IWalletBuilder.cs index 3645fd6..20d79a7 100644 --- a/DotNut/Abstractions/Interfaces/IWalletBuilder.cs +++ b/DotNut/Abstractions/Interfaces/IWalletBuilder.cs @@ -1,6 +1,7 @@ using DotNut.Api; using DotNut.ApiModels; using DotNut.NBitcoin.BIP39; +using NBitcoin.Secp256k1; namespace DotNut.Abstractions; @@ -239,7 +240,9 @@ Task SelectProofsToSend(List proofs, ulong amount, bool inc /// Check state of proofs /// /// - Task CheckState(IEnumerable proofs); + Task CheckState(IEnumerable proofs, CancellationToken ct = default); + + Task CheckState(IEnumerable Ys, CancellationToken ct = default); } diff --git a/DotNut/Abstractions/RestoreBuilder.cs b/DotNut/Abstractions/RestoreBuilder.cs index dee7d28..4924a2d 100644 --- a/DotNut/Abstractions/RestoreBuilder.cs +++ b/DotNut/Abstractions/RestoreBuilder.cs @@ -8,8 +8,7 @@ public class RestoreBuilder : IRestoreBuilder { private readonly Wallet _wallet; private List? _specifiedKeysets; - - private bool _shouldSwap = true; + private static int BATCH_SIZE = 100; public RestoreBuilder(Wallet wallet) { @@ -22,43 +21,35 @@ public IRestoreBuilder ForKeysetIds(IEnumerable keysetIds) return this; } - public IRestoreBuilder WithSwap(bool shouldSwap = true) - { - this._shouldSwap = shouldSwap; - return this; - } public async Task> ProcessAsync(CancellationToken ct = default) { var api = await _wallet.GetMintApi(ct); await _wallet._maybeSyncKeys(ct); - var mnemonic = _wallet.GetMnemonic()?? throw new ArgumentNullException(nameof(Mnemonic), "Can't restore wallet without Mnemonic"); + // keyset ids we want to grind our counter on _specifiedKeysets ??= (await _wallet.GetKeysets(ct: ct)).Select(k => k.Id).ToList(); - if (_specifiedKeysets == null || _specifiedKeysets.Count == 0) { - throw new InvalidOperationException("No keysets available for restoration. Ensure the mint has at least one keyset or specify keysets explicitly."); + throw new InvalidOperationException("No keysets available for restoration. Ensure the mint has at least one active keyset or specify keysets explicitly."); } + // init brand new counter + _wallet.WithCounter(new InMemoryCounter()); var counter = _wallet.GetCounter(); - if (counter == null) - { - _wallet.WithCounter(new InMemoryCounter()); - counter = _wallet.GetCounter(); - } + // fetch all batches List recoveredProofs = new List(); foreach (var keysetId in _specifiedKeysets) { int batchNumber = 0; int emptyBatchesRemaining = 3; - + + // don't care about invalid / non existent source keyset ids. let's fetch what we can GetKeysResponse.KeysetItemResponse? keyset; - try { keyset = await _wallet.GetKeys(keysetId, true, false, ct); @@ -67,12 +58,12 @@ public async Task> ProcessAsync(CancellationToken ct = defaul { continue; } - if (keyset == null) { continue; } + // proofs for keysetid are considered restored after 3 empty batches. while (emptyBatchesRemaining > 0) { var outputs = await _createBatch(mnemonic, keysetId, batchNumber, ct); @@ -81,41 +72,46 @@ public async Task> ProcessAsync(CancellationToken ct = defaul Outputs = outputs.Select(o=>o.BlindedMessage).ToArray() }; var res = await api.Restore(req, ct); - await counter!.IncrementCounter(keysetId, 100, ct); - - if (!res.Signatures.Any()) + await counter!.IncrementCounter(keysetId, BATCH_SIZE, ct); + batchNumber++; + if (res.Signatures.Length == 0) { emptyBatchesRemaining--; + continue; } - var proofs = Utils.ConstructProofsFromPromises(res.Signatures.ToList(), outputs, keyset.Keys); + var returnedOutputs = new List(); + + foreach (var output in res.Outputs) + { + returnedOutputs.Add(outputs.Single(o=>Equals(o.BlindedMessage.B_, output.B_))); + } + + var proofs = Utils.ConstructProofsFromPromises(res.Signatures.ToList(), returnedOutputs , keyset.Keys); recoveredProofs.AddRange(proofs); - batchNumber++; } } - if (!this._shouldSwap || !recoveredProofs.Any()) + // if nothing found - return empty collection + if (recoveredProofs.Count == 0) { - return recoveredProofs; + return []; } - + var freshProofs = new List(); // create hash table for every KeysetId : unit. var allKeysetsUnits = await _wallet.GetKeysetIdsWithUnits(ct); - var unitsForKeysets = new Dictionary(); if (allKeysetsUnits == null) { throw new InvalidOperationException("No keysets available for restoration."); } - foreach (var unit in allKeysetsUnits) - { - foreach (var keysetId in unit.Value) - { - unitsForKeysets.Add(keysetId, unit.Key); - } - } + var unitsForKeysets = allKeysetsUnits + .SelectMany(unit => + unit.Value.Select(keysetId => + new { KeysetId = keysetId, Unit = unit.Key })) + .ToDictionary(x => x.KeysetId, x => x.Unit); var activeUnits = await this._wallet.GetActiveKeysetIdsWithUnits(ct); if (activeUnits == null || !activeUnits.Any()) @@ -125,37 +121,43 @@ public async Task> ProcessAsync(CancellationToken ct = defaul foreach (var unitKeyset in activeUnits) { - var correspondingKeys = await _wallet.GetKeys(unitKeyset.Value, true, false, ct); - var unit = unitKeyset.Key; var proofsForUnit = recoveredProofs .Where(p => unitsForKeysets.TryGetValue(p.Id, out var proofUnit) && proofUnit == unit) .ToList(); - if (!proofsForUnit.Any()) continue; - var totalAmount = proofsForUnit.Select(p => p.Amount).Sum(); - - var amounts = Utils.SplitToProofsAmounts(totalAmount, correspondingKeys.Keys); - var ctr = await counter!.GetCounterForId(unitKeyset.Value, ct); - var newOutputs = Utils.CreateOutputs(amounts, unitKeyset.Value, correspondingKeys.Keys, mnemonic, ctr); - await counter.IncrementCounter(unitKeyset.Value, newOutputs.Select(o=>o.BlindedMessage).Count(), ct); + if (proofsForUnit.Count == 0) + { + continue; + } - var swapRequest = new PostSwapRequest + // check proofs state: + var unspentProofsForUnit = new List(); + var state = await _wallet.CheckState(proofsForUnit, ct); + for (int i = 0; i < proofsForUnit.Count; i++) { - Inputs = proofsForUnit.ToArray(), - Outputs = newOutputs.Select(o=>o.BlindedMessage).ToArray(), - }; - - var swapResult = await api.Swap(swapRequest, ct); - var constructedProofs = Utils.ConstructProofsFromPromises(swapResult.Signatures.ToList(), newOutputs, correspondingKeys.Keys); + if (state.States[i].State != StateResponseItem.TokenState.UNSPENT) + { + continue; + } + unspentProofsForUnit.Add(proofsForUnit[i]); + } + + // swap unspent tokens to single keyset + var proofs = await _wallet + .Swap() + .ForKeyset(unitKeyset.Value) + .WithDLEQVerification() + .FromInputs(unspentProofsForUnit) + .ProcessAsync(ct); - freshProofs.AddRange(constructedProofs); + freshProofs.AddRange(proofs); } return freshProofs; } - private async Task> _createBatch(Mnemonic mnemonic, KeysetId keysetId, int batchNumber, CancellationToken ct) + private static async Task> _createBatch(Mnemonic mnemonic, KeysetId keysetId, int batchNumber, CancellationToken ct) { - var amounts = Enumerable.Repeat((ulong)1, 100).ToList(); - return mnemonic.DeriveOutputs(amounts, keysetId, batchNumber*100); + var amounts = Enumerable.Repeat((ulong)0, BATCH_SIZE).ToList(); + return mnemonic.DeriveOutputs(amounts, keysetId, batchNumber*BATCH_SIZE); } } \ No newline at end of file diff --git a/DotNut/Abstractions/SwapBuilder.cs b/DotNut/Abstractions/SwapBuilder.cs index caaaf62..c077457 100644 --- a/DotNut/Abstractions/SwapBuilder.cs +++ b/DotNut/Abstractions/SwapBuilder.cs @@ -16,11 +16,11 @@ class SwapBuilder : ISwapBuilder private List? _outputs; private List? _amounts; - private KeysetId? _keysetId; + private KeysetId? _targetKeysetId; private string _unit = "sat"; - private bool _verifyDLEQ = true; + private bool _verifyDleq = true; private bool _includeFees = true; @@ -65,7 +65,7 @@ public ISwapBuilder ForOutputs(List outputs) public ISwapBuilder WithDLEQVerification(bool verify = true) { - _verifyDLEQ = verify; + _verifyDleq = verify; return this; } @@ -83,7 +83,7 @@ public ISwapBuilder WithAmounts(IEnumerable amounts) public ISwapBuilder ForKeyset(KeysetId keysetId) { - _keysetId = keysetId; + _targetKeysetId = keysetId; return this; } @@ -123,28 +123,35 @@ public async Task> ProcessAsync(CancellationToken ct = default) { var mintApi = await _wallet.GetMintApi(ct); - var swapInputs = _getSwapProofs(ct); + var swapInputs = _getSwapProofs(); if (swapInputs == null || swapInputs.Count == 0) { throw new ArgumentException("Nothing to swap!"); } // if there's no keysetId specified - let's choose it. - if (_keysetId == null) + if (_targetKeysetId == null) { - _keysetId = await _wallet.GetActiveKeysetId(this._unit, ct) ?? + _targetKeysetId = await _wallet.GetActiveKeysetId(this._unit, ct) ?? throw new InvalidOperationException("Could not fetch Keyset ID"); } - var keys = await _wallet.GetKeys(false, ct); - var keysForCurrentId = keys.SingleOrDefault(k => k.Id == _keysetId) - ?? throw new InvalidOperationException($"Keys for id: {_keysetId} not found in wallet keys"); - - if (_verifyDLEQ) + var keysForCurrentId = await + _wallet.GetKeys(_targetKeysetId, true, false, ct); + + if (keysForCurrentId == null) + { + throw new InvalidOperationException($"Can't find keys for keyset {_targetKeysetId}"); + } + if (_verifyDleq) { - foreach (var proof in swapInputs!) + foreach (var proof in swapInputs) { - var keyset = keys.SingleOrDefault(k => k.Id == proof.Id) - ?? throw new InvalidOperationException($"Keyset with ID {proof.Id} not found for proof verification."); + // proof may be already inactive - make sure to fetch + var keyset = await _wallet.GetKeys(proof.Id, true, false, ct); + if (keyset == null) + { + throw new InvalidOperationException($"Can't find keys for keyset id ${proof.Id}"); + } if (!keyset.Keys.TryGetValue(proof.Amount, out var key)) { throw new InvalidOperationException($"Can't find key for amount {proof.Amount} in keyset {keyset.Id}"); @@ -186,7 +193,7 @@ public async Task> ProcessAsync(CancellationToken ct = default) return swappedProofs; } - private List _getSwapProofs(CancellationToken ct = default) + private List _getSwapProofs() { _proofsToSwap ??= new(); @@ -207,7 +214,7 @@ private List _getSwapProofs(CancellationToken ct = default) return _proofsToSwap; } - async Task> _getOutputs(Keyset keys, CancellationToken ct = default) + private async Task> _getOutputs(Keyset keys, CancellationToken ct = default) { if (this._outputs != null) { @@ -234,14 +241,14 @@ async Task> _getOutputs(Keyset keys, CancellationToken ct = def foreach (var amount in _amounts) { var builder = _builder.Clone(); - outputs.Add(Utils.CreateNut10BlindedOutput(amount, this._keysetId!, builder, e)); + outputs.Add(Utils.CreateNut10BlindedOutput(amount, this._targetKeysetId!, builder, e)); } return outputs; } foreach (var amount in _amounts) { var builder = _builder.Clone(); - outputs.Add(Utils.CreateNut10BlindedOutput(amount, this._keysetId!, builder)); + outputs.Add(Utils.CreateNut10BlindedOutput(amount, this._targetKeysetId!, builder)); } return outputs; } @@ -249,12 +256,12 @@ async Task> _getOutputs(Keyset keys, CancellationToken ct = def foreach (var amount in _amounts) { var builder = _builder.Clone(); - outputs.Add(Utils.CreateNut10Output(amount, this._keysetId!, builder)); + outputs.Add(Utils.CreateNut10Output(amount, this._targetKeysetId!, builder)); } return outputs; } - return await _wallet.CreateOutputs(_amounts, this._keysetId!, ct); + return await _wallet.CreateOutputs(_amounts, this._targetKeysetId!, ct); } private List _getAmounts(ulong total, ulong fee, Keyset keys) diff --git a/DotNut/Abstractions/Utils.cs b/DotNut/Abstractions/Utils.cs index 270627e..1cb37db 100644 --- a/DotNut/Abstractions/Utils.cs +++ b/DotNut/Abstractions/Utils.cs @@ -297,7 +297,6 @@ public static ulong SumProofs(List proofs) { return proofs.Aggregate(0UL, (current, proof) => current + proof.Amount); } - public static ISecret RandomSecret() { diff --git a/DotNut/Abstractions/Wallet.cs b/DotNut/Abstractions/Wallet.cs index 11c0444..fd2403d 100644 --- a/DotNut/Abstractions/Wallet.cs +++ b/DotNut/Abstractions/Wallet.cs @@ -150,19 +150,19 @@ public IMeltQuoteBuilder CreateMeltQuote() return new MeltQuoteBuilder(this); } - public async Task CheckState(IEnumerable proofs) + public async Task CheckState(IEnumerable proofs, CancellationToken ct = default) { - return await CheckState(proofs.Select(p => p.Secret.ToCurve())); + return await CheckState(proofs.Select(p => (PubKey) p.Secret.ToCurve()), ct); } - public async Task CheckState(IEnumerable Ys) + public async Task CheckState(IEnumerable Ys, CancellationToken ct = default) { _ensureApiConnected(); var req = new PostCheckStateRequest() { Ys = Ys.Select(y=>y.ToString()).ToArray(), }; - return await _mintApi!.CheckState(req); + return await _mintApi!.CheckState(req, ct); } public IRestoreBuilder Restore() From 19486064af9d7518c04c2f13492478fb8f4974fa Mon Sep 17 00:00:00 2001 From: d4rp4t Date: Mon, 1 Dec 2025 16:06:42 +0100 Subject: [PATCH 32/70] Strip DLEQ and P2PkE before interacting with mint --- DotNut/Abstractions/Handlers/MeltHandlerBolt11.cs | 3 +++ DotNut/Abstractions/Handlers/MeltHandlerBolt12.cs | 2 ++ DotNut/Abstractions/SwapBuilder.cs | 6 +++--- DotNut/Abstractions/Utils.cs | 15 +++++++++++++++ DotNut/NUT12/DLEQProof.cs | 2 +- 5 files changed, 24 insertions(+), 4 deletions(-) diff --git a/DotNut/Abstractions/Handlers/MeltHandlerBolt11.cs b/DotNut/Abstractions/Handlers/MeltHandlerBolt11.cs index 3b190fb..83c351f 100644 --- a/DotNut/Abstractions/Handlers/MeltHandlerBolt11.cs +++ b/DotNut/Abstractions/Handlers/MeltHandlerBolt11.cs @@ -14,6 +14,9 @@ public class MeltHandlerBolt11( public async Task> Melt(List inputs, CancellationToken ct = default) { Nut10Helper.MaybeProcessNut10(privKeys??[], inputs, blankOutputs, htlcPreimage, quote.Quote); + //since nut10 (with p2bk) is processed, now it's safe to strip P2PkE + inputs.ForEach(i=>i.StripFingerprints()); + var client = await wallet.GetMintApi(ct); var req = new PostMeltRequest { diff --git a/DotNut/Abstractions/Handlers/MeltHandlerBolt12.cs b/DotNut/Abstractions/Handlers/MeltHandlerBolt12.cs index dec15a8..d1616be 100644 --- a/DotNut/Abstractions/Handlers/MeltHandlerBolt12.cs +++ b/DotNut/Abstractions/Handlers/MeltHandlerBolt12.cs @@ -15,6 +15,8 @@ public class MeltHandlerBolt12( public async Task> Melt(List inputs, CancellationToken ct = default) { Nut10Helper.MaybeProcessNut10(privKeys??[], inputs, blankOutputs, htlcPreimage, quote.Quote); + inputs.ForEach(i=>i.StripFingerprints()); + var client = await wallet.GetMintApi(ct); var req = new PostMeltRequest { diff --git a/DotNut/Abstractions/SwapBuilder.cs b/DotNut/Abstractions/SwapBuilder.cs index c077457..48eef26 100644 --- a/DotNut/Abstractions/SwapBuilder.cs +++ b/DotNut/Abstractions/SwapBuilder.cs @@ -178,13 +178,14 @@ public async Task> ProcessAsync(CancellationToken ct = default) // Swap received proofs to our keyset var outputs = await this._getOutputs(keysForCurrentId.Keys, ct); + Nut10Helper.MaybeProcessNut10(_privKeys??[], swapInputs, outputs, _htlcPreimage); + swapInputs.ForEach(i=>i.StripFingerprints()); var request = new PostSwapRequest() { Inputs = swapInputs.ToArray(), Outputs = outputs.Select(o=>o.BlindedMessage).ToArray(), }; - - Nut10Helper.MaybeProcessNut10(_privKeys??[], swapInputs, outputs, _htlcPreimage); + var swapResponse = await mintApi.Swap(request, ct); var swappedProofs = @@ -210,7 +211,6 @@ private List _getSwapProofs() this._proofsToSwap.AddRange(_token.Tokens.SelectMany(t=>t.Proofs)); } - return _proofsToSwap; } diff --git a/DotNut/Abstractions/Utils.cs b/DotNut/Abstractions/Utils.cs index 1cb37db..fc78789 100644 --- a/DotNut/Abstractions/Utils.cs +++ b/DotNut/Abstractions/Utils.cs @@ -281,6 +281,7 @@ Keyset keys { throw new ArgumentException($"Provided keyset doesn't contain PubKey for amount {promises[i].Amount}" ); } + var proof = ConstructProofFromPromise( promises[i], outputs[i].BlindingFactor, @@ -309,4 +310,18 @@ public static PrivKey RandomPrivkey() var bytes = RandomNumberGenerator.GetBytes(32); return new PrivKey(Convert.ToHexString(bytes)); } + + /// + /// Should be called before every interaction with mint. Strips info that could fingerprint user. + /// It musn't be called before sending token to someone - may make it unspendable. + /// + /// Proofs to clean + public static void StripFingerprints(this Proof proof) + { + if (proof.DLEQ != null) + { + proof.DLEQ.R = null; + } + proof.P2PkE = null; + } } \ No newline at end of file diff --git a/DotNut/NUT12/DLEQProof.cs b/DotNut/NUT12/DLEQProof.cs index a569e12..1815c15 100644 --- a/DotNut/NUT12/DLEQProof.cs +++ b/DotNut/NUT12/DLEQProof.cs @@ -4,5 +4,5 @@ namespace DotNut; public class DLEQProof: DLEQ { - [JsonPropertyName("r")] public PrivKey R { get; set; } + [JsonPropertyName("r")] public PrivKey? R { get; set; } } \ No newline at end of file From ecff01bce3576841c2473dfe9e4dbbc3e1193918 Mon Sep 17 00:00:00 2001 From: d4rp4t Date: Mon, 1 Dec 2025 16:20:47 +0100 Subject: [PATCH 33/70] Verify Proof after creation --- DotNut/Abstractions/Utils.cs | 31 ++++++++++++++++++------------ DotNut/NUT00/Cashu.cs | 37 ++++++++++++++++++++++++++++++++++-- 2 files changed, 54 insertions(+), 14 deletions(-) diff --git a/DotNut/Abstractions/Utils.cs b/DotNut/Abstractions/Utils.cs index fc78789..ba8f7dd 100644 --- a/DotNut/Abstractions/Utils.cs +++ b/DotNut/Abstractions/Utils.cs @@ -247,25 +247,32 @@ public static Proof ConstructProofFromPromise( var C = Cashu.ComputeC(promise.C_, r, amountPubkey); DLEQProof? dleq = null; - if (promise.DLEQ is not null) - { - dleq = new DLEQProof - { - E = promise.DLEQ.E, - S = promise.DLEQ.S, - R = r - }; - } - - return new Proof + + var proof = new Proof { Id = promise.Id, Amount = promise.Amount, Secret = secret, C = C, - DLEQ = dleq, P2PkE = P2PkE }; + + if (promise.DLEQ is null) + { + return proof; + } + + proof.DLEQ = new DLEQProof + { + E = promise.DLEQ.E, + S = promise.DLEQ.S, + R = r + }; + if (!proof.Verify(amountPubkey)) + { + throw new InvalidOperationException($"Could not verify mint signature on proof"); + } + return proof; } public static List ConstructProofsFromPromises( diff --git a/DotNut/NUT00/Cashu.cs b/DotNut/NUT00/Cashu.cs index c7a00e9..3f25e18 100644 --- a/DotNut/NUT00/Cashu.cs +++ b/DotNut/NUT00/Cashu.cs @@ -117,25 +117,51 @@ public static Scalar ComputeE(ECPubKey R1, ECPubKey R2, ECPubKey K, ECPubKey C_) /// Verify DLEQ proof of Cashu proof. /// /// Cashu Proof - /// + /// Amount Pubkey /// public static bool Verify(this Proof proof, ECPubKey A) { return VerifyProof(proof.Secret.ToCurve(),proof.DLEQ.R, proof.C, proof.DLEQ.E, proof.DLEQ.S, A); } + + /// + /// Verify DLEQ proof of Blinded signature + /// + /// Blind Signature + /// Amount Pubkey + /// Blinded Message + /// public static bool Verify(this BlindSignature blindSig, ECPubKey A, ECPubKey B_) { return Cashu.VerifyProof(B_, blindSig.C_, blindSig.DLEQ.E, blindSig.DLEQ.S, A); } + /// + /// Verify DLEQ proof + /// + /// Blinded Message + /// Blinded Signature + /// Dleq.E returned by mint + /// Dleq.S returned by mint + /// Amount pubkey + /// public static bool VerifyProof(ECPubKey B_, ECPubKey C_, ECPrivKey e, ECPrivKey s, ECPubKey A) { - var r1 = s.CreatePubKey().Q.ToGroupElementJacobian().Add((A.Q * e.sec.Negate()).ToGroupElement()).ToPubkey(); var r2 = (B_.Q * s.sec).Add((C_.Q * e.sec.Negate()).ToGroupElement()).ToPubkey(); var e_ = ComputeE(r1, r2, A, C_); return e.sec.Equals(e_); } + /// + /// Verify DLEQ proof + /// + /// Hash to curve result + /// Blinidng Factor + /// Amount Pubkey + /// Dleq.E returned by mint + /// Dleq.S returned by mint + /// Amount pubkey + /// public static bool VerifyProof(ECPubKey Y, ECPrivKey r, ECPubKey C, ECPrivKey e, ECPrivKey s, ECPubKey A) { @@ -172,6 +198,13 @@ public static ECPubKey ToPubkey(this GE ge) return new ECPubKey(ge, Context.Instance); } + /// + /// Compute shared secret for P2Bk (ECDH) + /// + /// Privkey of Alice + /// Pubkey of Bob + /// Zx = x(e·P) or x(p·E) + /// If can't create xOnly pubkey with derived shared secret public static byte[] ComputeZx(ECPrivKey e, ECPubKey P) { var x = (e.sec * P.Q).ToGroupElement().x; From 35a1b1da972e3df8bfc711bee059587ff61533c4 Mon Sep 17 00:00:00 2001 From: d4rp4t Date: Fri, 5 Dec 2025 14:06:02 +0100 Subject: [PATCH 34/70] fix bolt12 fix SIG_ALL --- DotNut.Tests/Integration.cs | 191 ++++++++++++------ DotNut.Tests/UnitTests2.cs | 18 ++ .../Handlers/MeltHandlerBolt11.cs | 9 +- .../Handlers/MeltHandlerBolt12.cs | 9 +- .../Interfaces/IMeltQuoteBuilder.cs | 7 + .../Abstractions/Interfaces/IWalletBuilder.cs | 35 +++- DotNut/Abstractions/MeltQuoteBuilder.cs | 34 +++- DotNut/Abstractions/Nut10Helper.cs | 8 +- DotNut/Abstractions/SwapBuilder.cs | 3 +- DotNut/Abstractions/Utils.cs | 31 ++- DotNut/Abstractions/Wallet.cs | 4 +- .../ApiModels/Melt/MeltQuoteRequestOptions.cs | 16 ++ .../Melt/bolt11/PostMeltQuoteBolt11Request.cs | 5 + .../Melt/bolt12/PostMeltQuoteBolt12Request.cs | 3 +- DotNut/NUT11/SigAllHandler.cs | 25 ++- DotNut/NUT12/DLEQProof.cs | 2 +- 16 files changed, 301 insertions(+), 99 deletions(-) create mode 100644 DotNut/ApiModels/Melt/MeltQuoteRequestOptions.cs diff --git a/DotNut.Tests/Integration.cs b/DotNut.Tests/Integration.cs index d41a340..31f4389 100644 --- a/DotNut.Tests/Integration.cs +++ b/DotNut.Tests/Integration.cs @@ -8,6 +8,7 @@ namespace DotNut.Tests; public class Integration { private static string MintUrl = "http://localhost:3338"; + // private static string MintUrl = "https://fake.thesimplekid.dev"; // private static string MintUrl = "https://testnut.cashu.space"; private static string seed = @@ -33,24 +34,7 @@ public class Integration }, }; private static ICounter counter = new InMemoryCounter(); - - [Fact] - public void CreatesWalletSuccesfully() - { - var wallet = Wallet.Create(); - Assert.NotNull(wallet); - } - [Fact] - public async Task ThrowsWhenMintNotFound() - { - var wallet = Wallet.Create(); - await Assert.ThrowsAsync(async () => await wallet.GetInfo()); - await Assert.ThrowsAsync(async () => wallet.Restore()); - await Assert.ThrowsAsync(async () => wallet.Swap()); - await Assert.ThrowsAsync(async () => wallet.CreateMeltQuote()); - await Assert.ThrowsAsync(async () => wallet.CreateMintQuote()); - } [Fact] public async Task FetchesInfoSuccessfully() @@ -244,48 +228,49 @@ public async Task MeltsSuccessfully() Assert.NotEmpty(change); } - // [Fact] - // public async Task MeltsBolt12Successfully() - // { - // var privkeyBob = new PrivKey(RandomNumberGenerator.GetBytes(32)); - // - // // mint proofs - // var wallet = Wallet - // .Create() - // .WithMint(MintUrl); - // - // var mintQuote = await wallet - // .CreateMintQuote() - // .WithUnit("sat") - // .WithAmount(1337) - // .WithPubkey(privkeyBob.Key.CreatePubKey()) - // .ProcessAsyncBolt12(); - // - // await Task.Delay(3000); - // - // mintQuote.SignWithPrivkey(privkeyBob); - // var mintedProofs = await mintQuote.Mint(); - // Assert.NotEmpty(mintedProofs); - // - // var Ids = mintedProofs.Select(proof => proof.Id).Count(); - // - // Console.WriteLine($"amounts {Ids}"); - // // create melt quote - // var meltQuote = await wallet - // .CreateMeltQuote() - // .WithInvoice(bolt12Invoices[1200]) - // .WithUnit("sat") - // .ProcessAsyncBolt12(); - // - // // select proofs to send - // var q = meltQuote.GetQuote(); - // var selectedProofs = await wallet.SelectProofsToSend(mintedProofs, q.Amount + (ulong)q.FeeReserve, true); - // - // //melt proofs - // var change = await meltQuote.Melt(selectedProofs.Send); - // - // Assert.NotEmpty(change); - // } + [Fact] + public async Task MeltsBolt12Successfully() + { + var privkeyBob = new PrivKey(RandomNumberGenerator.GetBytes(32)); + + // mint proofs + var wallet = Wallet + .Create() + .WithMint(MintUrl); + + var mintQuote = await wallet + .CreateMintQuote() + .WithUnit("sat") + .WithAmount(1337) + .WithPubkey(privkeyBob.Key.CreatePubKey()) + .ProcessAsyncBolt12(); + + await Task.Delay(3000); + + mintQuote.SignWithPrivkey(privkeyBob); + var mintedProofs = await mintQuote.Mint(); + Assert.NotEmpty(mintedProofs); + + var Ids = mintedProofs.Select(proof => proof.Id).Count(); + + Console.WriteLine($"amounts {Ids}"); + // create melt quote + var meltQuote = await wallet + .CreateMeltQuote() + .WithInvoice(bolt12Invoices[1200]) + .WithUnit("sat") + .WithAmount(1200)// it turns out that this invoice is amountless + .ProcessAsyncBolt12(); + + // select proofs to send + var q = meltQuote.GetQuote(); + var selectedProofs = await wallet.SelectProofsToSend(mintedProofs, q.Amount + (ulong)q.FeeReserve, true); + + //melt proofs + var change = await meltQuote.Melt(selectedProofs.Send); + + Assert.NotEmpty(change); + } [Fact] public async Task InvoiceWithDescription() @@ -344,20 +329,49 @@ public async Task SwapP2Pk() await PayInvoice(); var proofs = await mintHandler.Mint(); - // no privkeys await Assert.ThrowsAsync( async () => await wallet .Swap() .FromInputs(proofs) .ProcessAsync() ); + + var swappedProofs = await wallet + .Swap() + .FromInputs(proofs) + .WithPrivkeys([privKeyBob]) + .ProcessAsync(); + + Assert.NotEmpty(swappedProofs); + } + + [Fact] + public async Task MintSwapP2PkSigAll() + { + var wallet = Wallet + .Create() + .WithMint(MintUrl); + + var privKeyAlice = new PrivKey(RandomNumberGenerator.GetHexString(64, true)); + var privKeyBob = new PrivKey(RandomNumberGenerator.GetHexString(64, true)); + + var mintHandler = await wallet.CreateMintQuote() + .WithAmount(1337) + .WithP2PkLock(new P2PkBuilder() + { + SigFlag = "SIG_ALL", + Pubkeys = [privKeyBob.Key.CreatePubKey()], + SignatureThreshold = 1 + } + ).ProcessAsyncBolt11(); - // wrong privkey - await Assert.ThrowsAsync( + await PayInvoice(); + var proofs = await mintHandler.Mint(); + + await Assert.ThrowsAsync( async () => await wallet .Swap() .FromInputs(proofs) - .WithPrivkeys([privKeyAlice.Key]) .ProcessAsync() ); @@ -418,6 +432,7 @@ await Assert.ThrowsAsync(async () => Assert.NotEmpty(change); } + [Fact] public async Task MintSwapHTLC() { @@ -467,6 +482,52 @@ await wallet.Swap() Assert.Equal(1337UL - 1, Utils.SumProofs(swappedProofs)); } + [Fact] + public async Task MintSwapHTLCSigAll() + { + var wallet = Wallet + .Create() + .WithMint(MintUrl); + + var privKeyBob = new PrivKey(RandomNumberGenerator.GetHexString(64, true)); + var preimage = "0000000000000000000000000000000000000000000000000000000000000001"; + var hashLock = Convert.ToHexString(SHA256.HashData(Convert.FromHexString(preimage))); + + var mintHandler = await wallet.CreateMintQuote() + .WithAmount(1337) + .WithHTLCLock(new HTLCBuilder() + { + HashLock = hashLock, + Pubkeys = [privKeyBob.Key.CreatePubKey()], + SignatureThreshold = 1, + SigFlag = "SIG_ALL" + }) + .ProcessAsyncBolt11(); + + await PayInvoice(); + var htlcProofs = await mintHandler.Mint(); + + Assert.NotEmpty(htlcProofs); + Assert.Equal(1337UL, Utils.SumProofs(htlcProofs)); + + await Assert.ThrowsAsync(async () => + { + await wallet.Swap() + .FromInputs(htlcProofs) + .WithPrivkeys([privKeyBob]) + .ProcessAsync(); + }); + + var swappedProofs = await wallet.Swap() + .FromInputs(htlcProofs) + .WithPrivkeys([privKeyBob]) + .WithHtlcPreimage(preimage) + .ProcessAsync(); + + Assert.NotEmpty(swappedProofs); + Assert.Equal(1337UL - 1, Utils.SumProofs(swappedProofs)); + } + [Fact] public async Task MintMeltP2Bk() { @@ -545,7 +606,7 @@ public async Task MintMeltHTLCP2Bk() Assert.NotEmpty(change); } - + [Fact] public async Task MintSwapP2Bk() { @@ -753,7 +814,7 @@ public async Task SubscribeToMintMeltQuoteUpdates() } - private async Task PayInvoice() + private async Task PayInvoice() { //We're using fakewallet, so after 3 secs it will get paid automatically. After 3.5 sec its 1000% paid. await Task.Delay(3500); diff --git a/DotNut.Tests/UnitTests2.cs b/DotNut.Tests/UnitTests2.cs index 605c97c..c6cf799 100644 --- a/DotNut.Tests/UnitTests2.cs +++ b/DotNut.Tests/UnitTests2.cs @@ -12,6 +12,24 @@ public class UnitTests2 { private static string MintUrl = "http://localhost:3338"; + [Fact] + public void CreatesWalletSuccesfully() + { + var wallet = Wallet.Create(); + Assert.NotNull(wallet); + } + + [Fact] + public async Task ThrowsWhenMintNotFound() + { + var wallet = Wallet.Create(); + await Assert.ThrowsAsync(async () => await wallet.GetInfo()); + await Assert.ThrowsAsync(async () => wallet.Restore()); + await Assert.ThrowsAsync(async () => wallet.Swap()); + await Assert.ThrowsAsync(async () => wallet.CreateMeltQuote()); + await Assert.ThrowsAsync(async () => wallet.CreateMintQuote()); + } + [Fact] public void BuilderChainingPreservesAllSettings() { diff --git a/DotNut/Abstractions/Handlers/MeltHandlerBolt11.cs b/DotNut/Abstractions/Handlers/MeltHandlerBolt11.cs index 83c351f..5e1ba3c 100644 --- a/DotNut/Abstractions/Handlers/MeltHandlerBolt11.cs +++ b/DotNut/Abstractions/Handlers/MeltHandlerBolt11.cs @@ -13,15 +13,18 @@ public class MeltHandlerBolt11( public PostMeltQuoteBolt11Response GetQuote() => quote; public async Task> Melt(List inputs, CancellationToken ct = default) { - Nut10Helper.MaybeProcessNut10(privKeys??[], inputs, blankOutputs, htlcPreimage, quote.Quote); + //we're operating on copy here since later the proof state is mutated in stripFingerprints + var proofs = inputs.DeepCopyList(); + + Nut10Helper.MaybeProcessNut10(privKeys??[], proofs, blankOutputs, htlcPreimage, quote.Quote); //since nut10 (with p2bk) is processed, now it's safe to strip P2PkE - inputs.ForEach(i=>i.StripFingerprints()); + proofs.ForEach(i=>i.StripFingerprints()); var client = await wallet.GetMintApi(ct); var req = new PostMeltRequest { Quote = quote.Quote, - Inputs = inputs.ToArray(), + Inputs = proofs.ToArray(), Outputs = blankOutputs.Select(bo=> bo.BlindedMessage).ToArray(), }; diff --git a/DotNut/Abstractions/Handlers/MeltHandlerBolt12.cs b/DotNut/Abstractions/Handlers/MeltHandlerBolt12.cs index d1616be..e9d7a57 100644 --- a/DotNut/Abstractions/Handlers/MeltHandlerBolt12.cs +++ b/DotNut/Abstractions/Handlers/MeltHandlerBolt12.cs @@ -14,14 +14,17 @@ public class MeltHandlerBolt12( public PostMeltQuoteBolt12Response GetQuote() => quote; public async Task> Melt(List inputs, CancellationToken ct = default) { - Nut10Helper.MaybeProcessNut10(privKeys??[], inputs, blankOutputs, htlcPreimage, quote.Quote); - inputs.ForEach(i=>i.StripFingerprints()); + //we're operating on copy here since later the proof state is mutated in stripFingerprints + var proofs = inputs.DeepCopyList(); + + Nut10Helper.MaybeProcessNut10(privKeys??[], proofs, blankOutputs, htlcPreimage, quote.Quote); + proofs.ForEach(i=>i.StripFingerprints()); var client = await wallet.GetMintApi(ct); var req = new PostMeltRequest { Quote = quote.Quote, - Inputs = inputs.ToArray(), + Inputs = proofs.ToArray(), Outputs = blankOutputs.Select(bo=>bo.BlindedMessage).ToArray(), }; diff --git a/DotNut/Abstractions/Interfaces/IMeltQuoteBuilder.cs b/DotNut/Abstractions/Interfaces/IMeltQuoteBuilder.cs index 41a09c6..ad133d0 100644 --- a/DotNut/Abstractions/Interfaces/IMeltQuoteBuilder.cs +++ b/DotNut/Abstractions/Interfaces/IMeltQuoteBuilder.cs @@ -28,6 +28,13 @@ public interface IMeltQuoteBuilder /// IMeltQuoteBuilder WithPrivKeys(IEnumerable privKeys); + /// + /// Optional and mandatory if amountless invoice provided. + /// + /// Melt quote amount in millisatoshis + /// + IMeltQuoteBuilder WithAmount(ulong msat); + /// /// Optional. Supply HTLC preimage to sign HTLC-based proofs. /// diff --git a/DotNut/Abstractions/Interfaces/IWalletBuilder.cs b/DotNut/Abstractions/Interfaces/IWalletBuilder.cs index 20d79a7..507312a 100644 --- a/DotNut/Abstractions/Interfaces/IWalletBuilder.cs +++ b/DotNut/Abstractions/Interfaces/IWalletBuilder.cs @@ -184,14 +184,14 @@ public interface IWalletBuilder Task> GetKeys(bool forceRefresh = false, CancellationToken ct = default); /// - /// Get Keys for given KeysetID + /// Get Keys for given KeysetID. At first it tries to find corresponding keys, if allowFetch is true, will try to + /// fetch keys if not present in wallet. /// /// KeysetId /// If keyset not present not in db, it can be fetched /// Refetch flag /// /// Keys for given keyset - /// If wallet doesn't contain keysets for given keysetId Task GetKeys(KeysetId id, bool allowFetch, bool forceRefresh = false, CancellationToken ct = default); @@ -203,13 +203,34 @@ public interface IWalletBuilder /// List of Keysets Task> GetKeysets(bool forceRefresh = false, CancellationToken ct = default); + + /// + /// Select proofs for sending purposes. By default uses RGLI algorithm, unless another one provided. + /// + /// + /// + /// + /// + /// Task SelectProofsToSend(List proofs, ulong amount, bool includeFees, CancellationToken ct = default); + /// + /// Getter for proof selector. If not set, returns RGLI algorithm by default. + /// + /// + /// + Task GetSelector(CancellationToken ct = default); + + /// + /// Returns websocket service, that can be shared between multiple wallets. + /// + /// + /// Task GetWebsocketService(CancellationToken ct = default); - Task GetSelector(CancellationToken ct = default); + /// @@ -237,11 +258,15 @@ Task SelectProofsToSend(List proofs, ulong amount, bool inc IRestoreBuilder Restore(); /// - /// Check state of proofs + /// Check state of provided proofs. /// /// Task CheckState(IEnumerable proofs, CancellationToken ct = default); - + + /// + /// Check state of provided proofs. + /// + /// Task CheckState(IEnumerable Ys, CancellationToken ct = default); } diff --git a/DotNut/Abstractions/MeltQuoteBuilder.cs b/DotNut/Abstractions/MeltQuoteBuilder.cs index 5282a15..3ab9628 100644 --- a/DotNut/Abstractions/MeltQuoteBuilder.cs +++ b/DotNut/Abstractions/MeltQuoteBuilder.cs @@ -1,5 +1,6 @@ using DotNut.Abstractions.Handlers; using DotNut.ApiModels; +using DotNut.ApiModels.Melt; using DotNut.ApiModels.Melt.bolt12; namespace DotNut.Abstractions; @@ -7,10 +8,11 @@ namespace DotNut.Abstractions; class MeltQuoteBuilder : IMeltQuoteBuilder { private readonly Wallet _wallet; - private List? _proofs; private string? _invoice; private List? _blankOutputs; private string _unit = "sat"; + + private ulong? _amount; private List? _privKeys; private string? _htlcPreimage; @@ -52,6 +54,12 @@ public IMeltQuoteBuilder WithHTLCPreimage(string preimage) return this; } + public IMeltQuoteBuilder WithAmount(ulong msat) + { + this._amount = msat; + return this; + } + public async Task>> ProcessAsyncBolt11(CancellationToken ct = default) { var mintApi = await _wallet.GetMintApi(ct); @@ -64,6 +72,17 @@ public async Task>> Proces Unit = this._unit, }; + if (this._amount != null) + { + req.Options = new MeltQuoteRequestOptions + { + Amountless = new AmountlessMeltQuoteOptions + { + AmountMsat = this._amount.Value, + } + }; + } + var quote = await mintApi.CreateMeltQuote("bolt11", req, ct); @@ -88,8 +107,19 @@ public async Task>> Proces { Request = this._invoice, Unit = this._unit, - // todo melt quote bolt12 options }; + + if (this._amount != null) + { + req.Options = new MeltQuoteRequestOptions + { + Amountless = new AmountlessMeltQuoteOptions + { + AmountMsat = this._amount.Value, + } + }; + } + var quote = await mintApi.CreateMeltQuote("bolt12", req, ct); diff --git a/DotNut/Abstractions/Nut10Helper.cs b/DotNut/Abstractions/Nut10Helper.cs index 40e35a5..7a15755 100644 --- a/DotNut/Abstractions/Nut10Helper.cs +++ b/DotNut/Abstractions/Nut10Helper.cs @@ -28,7 +28,7 @@ public static void MaybeProcessNut10( MeltQuoteId = meltQuoteId }; - if (sigAllHandler.TrySign(out P2PKWitness? witness)) + if (sigAllHandler.TrySign(out string? witness)) { if (witness == null) { @@ -36,7 +36,7 @@ public static void MaybeProcessNut10( "sig_all input was correct, but couldn't create a witness signature!"); } - proofs[0].Witness = JsonSerializer.Serialize(witness); + proofs[0].Witness = witness; return; } @@ -44,11 +44,11 @@ public static void MaybeProcessNut10( foreach (var proof in proofs) { - handleWitnessCreation(proof, keys, htlcPreimage); + HandleWitnessCreation(proof, keys, htlcPreimage); } } - private static void handleWitnessCreation(Proof proof, ECPrivKey[] keys, string? htlcPreimage) + private static void HandleWitnessCreation(Proof proof, ECPrivKey[] keys, string? htlcPreimage) { if (proof.Secret is Nut10Secret { ProofSecret: HTLCProofSecret htlc }) { diff --git a/DotNut/Abstractions/SwapBuilder.cs b/DotNut/Abstractions/SwapBuilder.cs index 48eef26..d361804 100644 --- a/DotNut/Abstractions/SwapBuilder.cs +++ b/DotNut/Abstractions/SwapBuilder.cs @@ -53,7 +53,7 @@ public ISwapBuilder WithUnit(string unit) public ISwapBuilder FromInputs(IEnumerable proofs) { - this._proofsToSwap = proofs.ToList(); + this._proofsToSwap = proofs.DeepCopyList(); return this; } @@ -167,6 +167,7 @@ public async Task> ProcessAsync(CancellationToken ct = default) var fee = 0UL; if (_includeFees) { + // returns also non-active keysets. var keysetsFees = (await _wallet.GetKeysets(false, ct)).ToDictionary(k=>k.Id, k=>k.InputFee??0); fee = swapInputs.ComputeFee(keysetsFees); } diff --git a/DotNut/Abstractions/Utils.cs b/DotNut/Abstractions/Utils.cs index ba8f7dd..b37e0b4 100644 --- a/DotNut/Abstractions/Utils.cs +++ b/DotNut/Abstractions/Utils.cs @@ -1,4 +1,5 @@ using System.Security.Cryptography; +using System.Text.Json; using DotNut.NUT13; namespace DotNut.Abstractions; @@ -266,7 +267,7 @@ public static Proof ConstructProofFromPromise( { E = promise.DLEQ.E, S = promise.DLEQ.S, - R = r + R = r.Key.Clone() }; if (!proof.Verify(amountPubkey)) { @@ -320,15 +321,39 @@ public static PrivKey RandomPrivkey() /// /// Should be called before every interaction with mint. Strips info that could fingerprint user. - /// It musn't be called before sending token to someone - may make it unspendable. + /// It mustn't be called before sending token to someone - may make it unspendable. /// /// Proofs to clean public static void StripFingerprints(this Proof proof) { if (proof.DLEQ != null) { - proof.DLEQ.R = null; + proof.DLEQ = null; } proof.P2PkE = null; } + + /// + /// Create deep copy of the object, so original one won't get mutated by reference. + /// + /// Object to clone + /// Object type + /// Deep copy of the object + public static T DeepCopy(this T obj) where T : class + { + return JsonSerializer.Deserialize( + JsonSerializer.Serialize(obj) + )!; + } + + /// + /// Create deep copy of the list + /// + /// + /// + /// + public static List DeepCopyList(this IEnumerable list) where T : class + { + return list.Select(item => item.DeepCopy()).ToList(); + } } \ No newline at end of file diff --git a/DotNut/Abstractions/Wallet.cs b/DotNut/Abstractions/Wallet.cs index fd2403d..e30b7d7 100644 --- a/DotNut/Abstractions/Wallet.cs +++ b/DotNut/Abstractions/Wallet.cs @@ -152,6 +152,7 @@ public IMeltQuoteBuilder CreateMeltQuote() public async Task CheckState(IEnumerable proofs, CancellationToken ct = default) { + // no need for striping DLEQ r, or p2pkE, since only Ys are being sent. return await CheckState(proofs.Select(p => (PubKey) p.Secret.ToCurve()), ct); } @@ -443,7 +444,8 @@ private async Task _lazyFetchMintInfo(CancellationToken cts = default) } /// - /// Local Keys sync. + /// Local Keys sync. Will fetch _all_ keys if more than 2 unknown keysets are returned. + /// Doesn't sync fetch non-active keys. If you want to fetch keys for inactive keyset, you will need to use GetKeys. /// /// /// diff --git a/DotNut/ApiModels/Melt/MeltQuoteRequestOptions.cs b/DotNut/ApiModels/Melt/MeltQuoteRequestOptions.cs new file mode 100644 index 0000000..fd0b944 --- /dev/null +++ b/DotNut/ApiModels/Melt/MeltQuoteRequestOptions.cs @@ -0,0 +1,16 @@ +using System.Text.Json.Serialization; + +namespace DotNut.ApiModels.Melt; + +public class MeltQuoteRequestOptions +{ + [JsonPropertyName("amountless")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public AmountlessMeltQuoteOptions? Amountless { get; set; } +} + +public class AmountlessMeltQuoteOptions +{ + [JsonPropertyName("amount_msat")] + public ulong AmountMsat { get; set; } +} \ No newline at end of file diff --git a/DotNut/ApiModels/Melt/bolt11/PostMeltQuoteBolt11Request.cs b/DotNut/ApiModels/Melt/bolt11/PostMeltQuoteBolt11Request.cs index 6c57f5d..d207c5d 100644 --- a/DotNut/ApiModels/Melt/bolt11/PostMeltQuoteBolt11Request.cs +++ b/DotNut/ApiModels/Melt/bolt11/PostMeltQuoteBolt11Request.cs @@ -1,4 +1,5 @@ using System.Text.Json.Serialization; +using DotNut.ApiModels.Melt; namespace DotNut.ApiModels; @@ -10,4 +11,8 @@ public class PostMeltQuoteBolt11Request [JsonPropertyName("unit")] public string Unit { get; set; } + + [JsonPropertyName("options")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public MeltQuoteRequestOptions? Options { get; set; } } \ No newline at end of file diff --git a/DotNut/ApiModels/Melt/bolt12/PostMeltQuoteBolt12Request.cs b/DotNut/ApiModels/Melt/bolt12/PostMeltQuoteBolt12Request.cs index 9544745..5d8a5ed 100644 --- a/DotNut/ApiModels/Melt/bolt12/PostMeltQuoteBolt12Request.cs +++ b/DotNut/ApiModels/Melt/bolt12/PostMeltQuoteBolt12Request.cs @@ -13,6 +13,7 @@ public class PostMeltQuoteBolt12Request public string Unit { get; set; } [JsonPropertyName("options")] - public JsonNode? Options { get; set; } + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public MeltQuoteRequestOptions? Options { get; set; } } diff --git a/DotNut/NUT11/SigAllHandler.cs b/DotNut/NUT11/SigAllHandler.cs index bc5e3e1..b976a11 100644 --- a/DotNut/NUT11/SigAllHandler.cs +++ b/DotNut/NUT11/SigAllHandler.cs @@ -16,9 +16,9 @@ public class SigAllHandler private Nut10ProofSecret? _firstProofSecret; - public bool TrySign(out P2PKWitness? p2pkwitness) + public bool TrySign(out string? witness) { - p2pkwitness = null; + witness = null; if ( BlindedMessages is null || Proofs is null || PrivKeys is null || BlindedMessages.Count == 0 || Proofs.Count == 0 || PrivKeys.Count == 0) @@ -42,37 +42,42 @@ public bool TrySign(out P2PKWitness? p2pkwitness) return false; } - if (_firstProofSecret is not P2PKProofSecret ps) + if (_firstProofSecret is not P2PKProofSecret fps) { return false; } - if (ps is HTLCProofSecret s && HTLCPreimage is {} preimage) + P2PKWitness witnessObj; + if (fps is HTLCProofSecret s && HTLCPreimage is {} preimage) { if (Proofs.First().P2PkE is { } E) { - p2pkwitness = s.GenerateBlindWitness(msg, + witnessObj = s.GenerateBlindWitness(msg, PrivKeys.Select(pk => (ECPrivKey)pk).ToArray(), - Encoding.UTF8.GetBytes(preimage), + Convert.FromHexString(preimage), Proofs[0].Id, E ); + witness = JsonSerializer.Serialize((HTLCWitness)witnessObj); return true; } - p2pkwitness = + witnessObj = s.GenerateWitness(msg, PrivKeys.Select(pk => (ECPrivKey)pk).ToArray(), - Encoding.UTF8.GetBytes(preimage) + Convert.FromHexString(preimage) ); + witness = JsonSerializer.Serialize((HTLCWitness)witnessObj); return true; } if (Proofs.First().P2PkE is { } e2) { - p2pkwitness = ps.GenerateBlindWitness(msg, PrivKeys.Select(pk => (ECPrivKey)pk).ToArray(), Proofs[0].Id, e2); + witnessObj = fps.GenerateBlindWitness(msg, PrivKeys.Select(pk => (ECPrivKey)pk).ToArray(), Proofs[0].Id, e2); + witness = JsonSerializer.Serialize(witnessObj); return true; } - p2pkwitness = ps.GenerateWitness(msg, PrivKeys.Select(pk => (ECPrivKey)pk).ToArray()); + witnessObj = fps.GenerateWitness(msg, PrivKeys.Select(pk => (ECPrivKey)pk).ToArray()); + witness = JsonSerializer.Serialize(witnessObj); return true; } diff --git a/DotNut/NUT12/DLEQProof.cs b/DotNut/NUT12/DLEQProof.cs index 1815c15..a569e12 100644 --- a/DotNut/NUT12/DLEQProof.cs +++ b/DotNut/NUT12/DLEQProof.cs @@ -4,5 +4,5 @@ namespace DotNut; public class DLEQProof: DLEQ { - [JsonPropertyName("r")] public PrivKey? R { get; set; } + [JsonPropertyName("r")] public PrivKey R { get; set; } } \ No newline at end of file From 7817346caf4644f8f3a6aa4033865a1c5ef389f6 Mon Sep 17 00:00:00 2001 From: d4rp4t Date: Fri, 5 Dec 2025 14:24:52 +0100 Subject: [PATCH 35/70] add more tests --- DotNut.Tests/Integration.cs | 304 ++++++++++++++++++++++++++++-------- 1 file changed, 239 insertions(+), 65 deletions(-) diff --git a/DotNut.Tests/Integration.cs b/DotNut.Tests/Integration.cs index 31f4389..e4f987c 100644 --- a/DotNut.Tests/Integration.cs +++ b/DotNut.Tests/Integration.cs @@ -20,9 +20,11 @@ public class Integration {500, "lnbc5u1p5sh0yvsp53seej3qkkxe6xxk9mufaj7y3jc9s9kvfn4g3whppwqcl4vcjraaspp5vtv793xc9ksch8zekkhqtv54a2evh7vq4zuywcmk9nzt69qma5yqhp5uwcvgs5clswpfxhm7nyfjmaeysn6us0yvjdexn9yjkv3k7zjhp2sxq9z0rgqcqpnrzjq0ly0l075re9ltgqzdycartvas6g4c7kwwzpasj7a98c0ss679hdsr080vqqdcgqqqqqqqqnqqqqryqqxv9qxpqysgqwq50283v8asna95fktaeg80kq9evs0chaw44y6y649qsql9vsfc5gfcsp8rdwwyccepwy83n7g0s25n3lpv3hjgcr220n5w806fja8gp2xjvd7"}, {501, "lnbc5010n1p5shs9rsp5a2qhmn05xsd8vcm5jx9v2aswkz0pxguk4jqlaxsazzcg5rduan2qpp5al2k5zwruvlx34sxxdys2sj696m58uqgjvzxxrxhvuyswhmzg5cqhp5uwcvgs5clswpfxhm7nyfjmaeysn6us0yvjdexn9yjkv3k7zjhp2sxq9z0rgqcqpnrzjqw7c9dkkx4nur9sztw2zzpzj8u8rgsqgdsykylg5pwplh26824lc7rvlqcqqn3gqqyqqqqlgqqqqqqgq2q9qxpqysgqgpj2x2aw2dv5tzhx86th6a5vutpcdxz9htewqgvzjgqkzwmh6xs5mw5xcgrzyq77f35shv0gg5ygtjmn7e73wg8v0a9g836ufszdxmqqqu3642"}, {502, "lnbc5020n1p5j3nxasp5qz7utfrp954nxp8049tqzg0t23krdj59thfcrc2g5h6lsemzvyfqpp5ms6xd7grtak0nr8lwytsclmq3d233v7gy7j0kuw32txhjq0f8ngqhp5uwcvgs5clswpfxhm7nyfjmaeysn6us0yvjdexn9yjkv3k7zjhp2sxq9z0rgqcqpnrzjqg587a2yyuqeua9c3j8nw7wwpx709slwl5lzfs0t0vq3kdwemzp67rtwevqq95gqqyqqq5sgqqq9yzqq2q9qxpqysgqgpp0zsetj9fedvr0szpwjfw2weckygmjthhnfpp2cerjtrn8n0pxyvrtc00l0jwzkqhwedcvgqljtwx3a7qplqp43jlxe4mpmw5svlgqfwa9yy"}, + {503, "lnbc5030n1p5n9kk6sp5ee6rsflv9rnnyt80ucc0fzlwa975nmufs2dn0x3u0hlerxxtc4nqpp5ew5mxfmu966c8wywvnvtgsljduq0jduvdpc3jzqzq9uqafx7ejgshp5uwcvgs5clswpfxhm7nyfjmaeysn6us0yvjdexn9yjkv3k7zjhp2sxq9z0rgqcqpnrzjqdqne4nrkxmz96ktnngat4nzx7sv0kf5uqmgfvqvvars7pac7fn9wr8fjvqq3csqqqqqqqqqqqqq0scqvs9qxpqysgq5lqwgfk6vv36tnlx2tv6reu2587x8ha2wsht0s75dpzvmknpgepsqaq9wnlx7n87j3x6w0vvkvc4qgda6mhacygn9f0xgagwt84uxtcqgtcpfs"}, {999, "lnbc9990n1p5j3cf7sp575w4pw93kfrghl2gh68885v76gwjpzuv435t52q846cvx4w7yuvqpp5hdzvm3yf0r3vj99c7esmcv7zuj2fralf2twhl6s9xqcgr8g7nwyqhp5uwcvgs5clswpfxhm7nyfjmaeysn6us0yvjdexn9yjkv3k7zjhp2sxq9z0rgqcqpnrzjqt0mfswatysklf4z358sztscs5t0vdghmd5vfe9c9sa0gy6r5pdugrs7myqqvgqqqyqqqqqqqqqq86qq8s9qxpqysgq5wh9l4fy32ww4770mqm7yqvhwllaqyssvp335gjz6t59ca03gecyvdd9uv0ztrcm2uf2352wvwxcfh7yukucp4p6zu6ll867aj686wsqz0jlmt"}, {1000, "lnbc10u1p5w6vggsp5gn5xhswgn5299w6elu2z0vzjxhf9hwd6pwjcgfwphaxunyu0dx6spp5a60trrhce2u6tzqjwjczem8rpdesgzkawcqg2xqaesz2kd50z4uqhp5uwcvgs5clswpfxhm7nyfjmaeysn6us0yvjdexn9yjkv3k7zjhp2sxq9z0rgqcqpnrzjqw22kc09xj0dm65ew5h5r003vtn72eyzchdgjag66l0yhwdudfmuzrwvesqq8qgqqgqqqqqqqqqqzhsq2q9qxpqysgq955gcfr95wwz0ehtnk3xraatkyhj88z44ku7yqutnwnt3gkh82jxehvdff7n2js2p54jgpvg6dmwmq8t9d8x05j63mqjrsr4cwd4lpcpnc39ru"}, {1150, "lnbc11500n1p5jnmr7sp5u0s2wpuqn4mp0axyzgmsxzf5v8sy3zmzz9a7jyq38luyx9cntazqpp57j3carehwt4tqthxz9z7ea80t0htklh4v6v96dtn4vxuu4kwsershp53mwsvrcmkv743nyfzjp5a5fqrg2yngda3apf7jf9rzsuwt82wt3sxq9z0rgqcqpnrzjqg587a2yyuqeua9c3j8nw7wwpx709slwl5lzfs0t0vq3kdwemzp67rtwevqq95gqqyqqq5sgqqq9yzqq2q9qxpqysgqe97nwd9q74ua0sl9877sdprjcuc6jpyy8c52azpz8au6ur8q3838c0a0upnahs8w3sec8kxh26m3v9rkgqej36652t3sa5t25svacdcq5qwwjp"}, + {1151, "lnbc11510n1p5n9hzpsp5ey8npxa4nsaet73nc74lky0mv780h6890ua3kqhffvn8heqzk33spp5df0xt9s0e0kh0rx7dcy39u4q3g7cknk88wr4s90cldv6z2vwspgqhp5uwcvgs5clswpfxhm7nyfjmaeysn6us0yvjdexn9yjkv3k7zjhp2sxq9z0rgqcqpnrzjq0xp6zfjhwvmq6tltd09jcdc82ml6eh3alzvnaw8httxcx7tu78syrvfkqqqm0qqqyqqqqlgqqqvx5qqjq9qxpqysgqwnys3mklnsnrw5ysa8cjtynlqnllyxskcsamr7x96nl5kllyqcznlyeeuklr3zydeq43k6ckyrgqqfg965dsdjc675lvlssn0z4sxusq0lzrx6"}, {2000, "lnbc20u1p5094fksp54vrdcymel5awhrpc0m6z4kvhhyvqlwkshkyt2wr6eyljkz8c798qpp59f2vc8td8tu62gtf4qfwzkrkxedsey7a5ajrd48a25z2kkwg407shp5nklhn663zgwcdnh7pe5jxt6td0cchhre6hxzdxrjdlfwtpq60f5sxq9z0rgqcqpnrzjqw0de9yc0j8n4hpgm269tm7qph4gwcyf5ys02uaapvpugrva87c7zr045uqq4jsqpsqqqqlgqqqqrcgq2q9qxpqysgq6g2pamgjumh6uw5k5rj2ket44wh8nfzs5gzyygl54hu5cefuxdhxp9h5mrg64rh07znktn9x9d5vg6fc0rw7m63x8rg4qk3kw6d8sycpywn48m"}, }; @@ -45,7 +47,7 @@ public async Task FetchesInfoSuccessfully() } [Fact] - public async Task MintsSuccessfully() + public async Task MintsBolt11Successfully() { var wallet = Wallet.Create().WithMint(MintUrl); @@ -193,7 +195,7 @@ public async Task SwapsDeterministicSuccessfully() } [Fact] - public async Task MeltsSuccessfully() + public async Task MeltsBolt11Successfully() { // mint proofs var wallet = Wallet @@ -344,6 +346,55 @@ await Assert.ThrowsAsync( Assert.NotEmpty(swappedProofs); } + + [Fact] + public async Task MintMeltP2PkMultisig() + { + var wallet = Wallet + .Create() + .WithMint(MintUrl); + + var privKeyAlice = new PrivKey(RandomNumberGenerator.GetHexString(64, true)); + var privKeyBob = new PrivKey(RandomNumberGenerator.GetHexString(64, true)); + + var mintHandler = await wallet.CreateMintQuote() + .WithAmount(1337) + .WithP2PkLock(new P2PkBuilder() + { + Pubkeys = [privKeyBob.Key.CreatePubKey(), privKeyAlice.Key.CreatePubKey()], + SignatureThreshold = 2 + } + ).ProcessAsyncBolt11(); + await PayInvoice(); + + var proofs = await mintHandler.Mint(); + + Assert.NotEmpty(proofs); + + // no privkeys + await Assert.ThrowsAsync(async () => + { + var meltHandler = await wallet + .CreateMeltQuote() + .WithInvoice(valuesInvoices[500]) + .ProcessAsyncBolt11(); + await meltHandler.Melt(proofs); + }); + + var handler = await wallet + .CreateMeltQuote() + .WithInvoice(valuesInvoices[501]) + .WithPrivKeys([privKeyBob, privKeyAlice]) + .ProcessAsyncBolt11(); + + var q = handler.GetQuote(); + + var amountToPay = q.Amount + (ulong)q.FeeReserve; + var selectorResponse = await wallet.SelectProofsToSend(proofs, amountToPay, true); + var change = await handler.Melt(selectorResponse.Send); + + Assert.NotEmpty(change); + } [Fact] public async Task MintSwapP2PkSigAll() @@ -385,53 +436,159 @@ await Assert.ThrowsAsync( } [Fact] - public async Task MintMeltP2PkMultisig() + public async Task MintSwapP2Bk() { var wallet = Wallet .Create() .WithMint(MintUrl); - - var privKeyAlice = new PrivKey(RandomNumberGenerator.GetHexString(64, true)); + var privKeyBob = new PrivKey(RandomNumberGenerator.GetHexString(64, true)); + var privKeyAlice = new PrivKey(RandomNumberGenerator.GetHexString(64, true)); - var mintHandler = await wallet.CreateMintQuote() + var builder = new P2PkBuilder() + { + Pubkeys = [privKeyBob.Key.CreatePubKey(), privKeyAlice.Key.CreatePubKey()], + }; + + var quote = await wallet + .CreateMintQuote() .WithAmount(1337) - .WithP2PkLock(new P2PkBuilder() - { - Pubkeys = [privKeyBob.Key.CreatePubKey(), privKeyAlice.Key.CreatePubKey()], - SignatureThreshold = 2 - } - ).ProcessAsyncBolt11(); - await PayInvoice(); + .WithP2PkLock(builder) + .BlindPubkeys() + .ProcessAsyncBolt11(); - var proofs = await mintHandler.Mint(); + await PayInvoice(); + var proofs = await quote.Mint(); Assert.NotEmpty(proofs); + Assert.NotEmpty(proofs.Select(p=>p.P2PkE)); - // no privkeys - await Assert.ThrowsAsync(async () => + var newProofs = await wallet + .Swap() + .FromInputs(proofs) + .WithPrivkeys([privKeyBob, privKeyAlice]) + .ProcessAsync(); + + Assert.NotEmpty(newProofs); + } + + [Fact] + public async Task MintMeltP2Bk() + { + var wallet = Wallet + .Create() + .WithMint(MintUrl); + + var privKeyBob = new PrivKey(RandomNumberGenerator.GetHexString(64, true)); + + var builder = new P2PkBuilder() { - var meltHandler = await wallet - .CreateMeltQuote() - .WithInvoice(valuesInvoices[500]) - .ProcessAsyncBolt11(); - await meltHandler.Melt(proofs); - }); + Pubkeys = [privKeyBob.Key.CreatePubKey()], + }; - var handler = await wallet + var quote = await wallet + .CreateMintQuote() + .WithAmount(1337) + .WithP2PkLock(builder) + .BlindPubkeys() + .ProcessAsyncBolt11(); + + await PayInvoice(); + var proofs = await quote.Mint(); + + Assert.NotEmpty(proofs); + Assert.NotEmpty(proofs.Select(p=>p.P2PkE)); + + var meltHandler = await wallet .CreateMeltQuote() - .WithInvoice(valuesInvoices[501]) - .WithPrivKeys([privKeyBob, privKeyAlice]) + .WithInvoice(valuesInvoices[502]) + .WithPrivKeys([privKeyBob]) .ProcessAsyncBolt11(); + + var change = await meltHandler.Melt(proofs); + + Assert.NotEmpty(change); + } - var q = handler.GetQuote(); + [Fact] + public async Task MintMeltP2BkSigAll() + { + var wallet = Wallet + .Create() + .WithMint(MintUrl); - var amountToPay = q.Amount + (ulong)q.FeeReserve; - var selectorResponse = await wallet.SelectProofsToSend(proofs, amountToPay, true); - var change = await handler.Melt(selectorResponse.Send); + var privKeyBob = new PrivKey(RandomNumberGenerator.GetHexString(64, true)); + + var builder = new P2PkBuilder() + { + Pubkeys = [privKeyBob.Key.CreatePubKey()], + SigFlag = "SIG_ALL", + }; + + var quote = await wallet + .CreateMintQuote() + .WithAmount(1337) + .WithP2PkLock(builder) + .BlindPubkeys() + .ProcessAsyncBolt11(); + + await PayInvoice(); + var proofs = await quote.Mint(); + + Assert.NotEmpty(proofs); + Assert.NotEmpty(proofs.Select(p=>p.P2PkE)); + + var meltHandler = await wallet + .CreateMeltQuote() + .WithInvoice(valuesInvoices[503]) + .WithPrivKeys([privKeyBob]) + .ProcessAsyncBolt11(); + var change = await meltHandler.Melt(proofs); + Assert.NotEmpty(change); } + + [Fact] + public async Task MintSwapP2BkSigAll() + { + var wallet = Wallet + .Create() + .WithMint(MintUrl); + + var privKeyAlice = new PrivKey(RandomNumberGenerator.GetHexString(64, true)); + var privKeyBob = new PrivKey(RandomNumberGenerator.GetHexString(64, true)); + + var mintHandler = await wallet.CreateMintQuote() + .WithAmount(1337) + .WithP2PkLock(new P2PkBuilder() + { + SigFlag = "SIG_ALL", + Pubkeys = [privKeyBob.Key.CreatePubKey()], + SignatureThreshold = 1 + } + ) + .BlindPubkeys() + .ProcessAsyncBolt11(); + + await PayInvoice(); + var proofs = await mintHandler.Mint(); + + await Assert.ThrowsAsync( + async () => await wallet + .Swap() + .FromInputs(proofs) + .ProcessAsync() + ); + + var swappedProofs = await wallet + .Swap() + .FromInputs(proofs) + .WithPrivkeys([privKeyBob]) + .ProcessAsync(); + + Assert.NotEmpty(swappedProofs); + } [Fact] public async Task MintSwapHTLC() @@ -529,43 +686,53 @@ await wallet.Swap() } [Fact] - public async Task MintMeltP2Bk() + public async Task MintSwapHtlcP2BkSigAll() { var wallet = Wallet .Create() .WithMint(MintUrl); var privKeyBob = new PrivKey(RandomNumberGenerator.GetHexString(64, true)); - - var builder = new P2PkBuilder() - { - Pubkeys = [privKeyBob.Key.CreatePubKey()], - }; + var preimage = "0000000000000000000000000000000000000000000000000000000000000001"; + var hashLock = Convert.ToHexString(SHA256.HashData(Convert.FromHexString(preimage))); - var quote = await wallet - .CreateMintQuote() + var mintHandler = await wallet.CreateMintQuote() .WithAmount(1337) - .WithP2PkLock(builder) + .WithHTLCLock(new HTLCBuilder() + { + HashLock = hashLock, + Pubkeys = [privKeyBob.Key.CreatePubKey()], + SignatureThreshold = 1, + SigFlag = "SIG_ALL" + }) .BlindPubkeys() .ProcessAsyncBolt11(); - + await PayInvoice(); - var proofs = await quote.Mint(); + var htlcProofs = await mintHandler.Mint(); - Assert.NotEmpty(proofs); - Assert.NotEmpty(proofs.Select(p=>p.P2PkE)); + Assert.NotEmpty(htlcProofs); + Assert.Equal(1337UL, Utils.SumProofs(htlcProofs)); - var meltHandler = await wallet - .CreateMeltQuote() - .WithInvoice(valuesInvoices[502]) - .WithPrivKeys([privKeyBob]) - .ProcessAsyncBolt11(); - var change = await meltHandler.Melt(proofs); - - Assert.NotEmpty(change); + await Assert.ThrowsAsync(async () => + { + await wallet.Swap() + .FromInputs(htlcProofs) + .WithPrivkeys([privKeyBob]) + .ProcessAsync(); + }); + + var swappedProofs = await wallet.Swap() + .FromInputs(htlcProofs) + .WithPrivkeys([privKeyBob]) + .WithHtlcPreimage(preimage) + .ProcessAsync(); + + Assert.NotEmpty(swappedProofs); + Assert.Equal(1337UL - 1, Utils.SumProofs(swappedProofs)); } - + [Fact] public async Task MintMeltHTLCP2Bk() { @@ -606,26 +773,28 @@ public async Task MintMeltHTLCP2Bk() Assert.NotEmpty(change); } - + [Fact] - public async Task MintSwapP2Bk() + public async Task MintMeltHTLCP2BkSigAll() { var wallet = Wallet .Create() .WithMint(MintUrl); var privKeyBob = new PrivKey(RandomNumberGenerator.GetHexString(64, true)); - var privKeyAlice = new PrivKey(RandomNumberGenerator.GetHexString(64, true)); - - var builder = new P2PkBuilder() + var preimage = new string('0', 63) + "1"; + + var builder = new HTLCBuilder() { - Pubkeys = [privKeyBob.Key.CreatePubKey(), privKeyAlice.Key.CreatePubKey()], + SigFlag = "SIG_ALL", + Pubkeys = [privKeyBob.Key.CreatePubKey()], + HashLock = Convert.ToHexString(SHA256.HashData(Convert.FromHexString(preimage))), }; var quote = await wallet .CreateMintQuote() .WithAmount(1337) - .WithP2PkLock(builder) + .WithHTLCLock(builder) .BlindPubkeys() .ProcessAsyncBolt11(); @@ -634,16 +803,21 @@ public async Task MintSwapP2Bk() Assert.NotEmpty(proofs); Assert.NotEmpty(proofs.Select(p=>p.P2PkE)); - - var newProofs = await wallet - .Swap() - .FromInputs(proofs) - .WithPrivkeys([privKeyBob, privKeyAlice]) - .ProcessAsync(); - Assert.NotEmpty(newProofs); + var meltHandler = await wallet + .CreateMeltQuote() + .WithInvoice(valuesInvoices[1151]) + .WithPrivKeys([privKeyBob]) + .WithHTLCPreimage(preimage) + .ProcessAsyncBolt11(); + + var change = await meltHandler.Melt(proofs); + + Assert.NotEmpty(change); } + + [Fact] public async Task SwapWithCustomAmounts() { From 50b34899171391a63381718949c6deb32104d4df Mon Sep 17 00:00:00 2001 From: d4rp4t Date: Sun, 7 Dec 2025 07:52:18 +0100 Subject: [PATCH 36/70] add counter getter --- DotNut/Abstractions/Interfaces/IWalletBuilder.cs | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/DotNut/Abstractions/Interfaces/IWalletBuilder.cs b/DotNut/Abstractions/Interfaces/IWalletBuilder.cs index 507312a..34e509c 100644 --- a/DotNut/Abstractions/Interfaces/IWalletBuilder.cs +++ b/DotNut/Abstractions/Interfaces/IWalletBuilder.cs @@ -230,8 +230,11 @@ Task SelectProofsToSend(List proofs, ulong amount, bool inc /// Task GetWebsocketService(CancellationToken ct = default); - - + /// + /// Returns current Counter instance. + /// + /// + ICounter? GetCounter(); /// /// Create swap transaction builder. @@ -268,6 +271,7 @@ Task SelectProofsToSend(List proofs, ulong amount, bool inc /// /// Task CheckState(IEnumerable Ys, CancellationToken ct = default); + } From ff56229e28a7e058f7f52d4998770c9ca1923a62 Mon Sep 17 00:00:00 2001 From: d4rp4t Date: Sun, 7 Dec 2025 08:05:50 +0100 Subject: [PATCH 37/70] add counter output for restore --- DotNut/Abstractions/InMemoryCounter.cs | 5 +++++ DotNut/Abstractions/Interfaces/ICounter.cs | 1 + .../Abstractions/Interfaces/IRestoreBuilder.cs | 17 ++++++++++++++++- DotNut/Abstractions/RestoreBuilder.cs | 17 +++++++++++------ 4 files changed, 33 insertions(+), 7 deletions(-) diff --git a/DotNut/Abstractions/InMemoryCounter.cs b/DotNut/Abstractions/InMemoryCounter.cs index 5012839..7522900 100644 --- a/DotNut/Abstractions/InMemoryCounter.cs +++ b/DotNut/Abstractions/InMemoryCounter.cs @@ -30,5 +30,10 @@ public Task SetCounter(KeysetId keysetId, int counter, CancellationToken ct = de _counter[keysetId] = counter; return Task.CompletedTask; } + + public IReadOnlyDictionary Export() + { + return new Dictionary(_counter); + } } \ No newline at end of file diff --git a/DotNut/Abstractions/Interfaces/ICounter.cs b/DotNut/Abstractions/Interfaces/ICounter.cs index 9e69067..952ea1b 100644 --- a/DotNut/Abstractions/Interfaces/ICounter.cs +++ b/DotNut/Abstractions/Interfaces/ICounter.cs @@ -5,4 +5,5 @@ public interface ICounter public Task GetCounterForId(KeysetId keysetId, CancellationToken ct = default); public Task IncrementCounter(KeysetId keysetId, int bumpBy = 1, CancellationToken ct = default); public Task SetCounter(KeysetId keysetId, int counter, CancellationToken ct = default); + public IReadOnlyDictionary Export(); } \ No newline at end of file diff --git a/DotNut/Abstractions/Interfaces/IRestoreBuilder.cs b/DotNut/Abstractions/Interfaces/IRestoreBuilder.cs index 973250a..e9f780a 100644 --- a/DotNut/Abstractions/Interfaces/IRestoreBuilder.cs +++ b/DotNut/Abstractions/Interfaces/IRestoreBuilder.cs @@ -5,6 +5,21 @@ namespace DotNut.Abstractions; /// public interface IRestoreBuilder { - IRestoreBuilder ForKeysetIds(IEnumerable keysetIds); + /// + /// Optional and usually not-advised. Allows to specify keysets that we want to restore. + /// If not set, every keyset is grinded. + /// + /// + /// + IRestoreBuilder FromKeysetIds(IEnumerable keysetIds); + + /// + /// Optional, allows to set counter which will hold the state after restore. + /// If not set, defaults to InMemoryCounter that is not returned. + /// + /// + /// + public IRestoreBuilder WithCounter(ICounter counter); + Task> ProcessAsync(CancellationToken ct = default); } diff --git a/DotNut/Abstractions/RestoreBuilder.cs b/DotNut/Abstractions/RestoreBuilder.cs index 4924a2d..5021021 100644 --- a/DotNut/Abstractions/RestoreBuilder.cs +++ b/DotNut/Abstractions/RestoreBuilder.cs @@ -10,17 +10,25 @@ public class RestoreBuilder : IRestoreBuilder private List? _specifiedKeysets; private static int BATCH_SIZE = 100; + private ICounter? _counter; + public RestoreBuilder(Wallet wallet) { this._wallet = wallet; } - public IRestoreBuilder ForKeysetIds(IEnumerable keysetIds) + public IRestoreBuilder FromKeysetIds(IEnumerable keysetIds) { this._specifiedKeysets = keysetIds.ToList(); return this; } + public IRestoreBuilder WithCounter(ICounter counter) + { + this._counter = counter; + return this; + } + public async Task> ProcessAsync(CancellationToken ct = default) { @@ -36,11 +44,8 @@ public async Task> ProcessAsync(CancellationToken ct = defaul { throw new InvalidOperationException("No keysets available for restoration. Ensure the mint has at least one active keyset or specify keysets explicitly."); } - - // init brand new counter - _wallet.WithCounter(new InMemoryCounter()); - var counter = _wallet.GetCounter(); + _counter ??= new InMemoryCounter(); // fetch all batches List recoveredProofs = new List(); foreach (var keysetId in _specifiedKeysets) @@ -72,7 +77,7 @@ public async Task> ProcessAsync(CancellationToken ct = defaul Outputs = outputs.Select(o=>o.BlindedMessage).ToArray() }; var res = await api.Restore(req, ct); - await counter!.IncrementCounter(keysetId, BATCH_SIZE, ct); + await _counter!.IncrementCounter(keysetId, BATCH_SIZE, ct); batchNumber++; if (res.Signatures.Length == 0) { From 46d723acb83ab9b692e6edcba019b451c6943640 Mon Sep 17 00:00:00 2001 From: d4rp4t Date: Sun, 7 Dec 2025 08:07:40 +0100 Subject: [PATCH 38/70] make export counter async --- DotNut/Abstractions/InMemoryCounter.cs | 2 +- DotNut/Abstractions/Interfaces/ICounter.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/DotNut/Abstractions/InMemoryCounter.cs b/DotNut/Abstractions/InMemoryCounter.cs index 7522900..a1d9ffa 100644 --- a/DotNut/Abstractions/InMemoryCounter.cs +++ b/DotNut/Abstractions/InMemoryCounter.cs @@ -31,7 +31,7 @@ public Task SetCounter(KeysetId keysetId, int counter, CancellationToken ct = de return Task.CompletedTask; } - public IReadOnlyDictionary Export() + public async Task> Export() { return new Dictionary(_counter); } diff --git a/DotNut/Abstractions/Interfaces/ICounter.cs b/DotNut/Abstractions/Interfaces/ICounter.cs index 952ea1b..9b941af 100644 --- a/DotNut/Abstractions/Interfaces/ICounter.cs +++ b/DotNut/Abstractions/Interfaces/ICounter.cs @@ -5,5 +5,5 @@ public interface ICounter public Task GetCounterForId(KeysetId keysetId, CancellationToken ct = default); public Task IncrementCounter(KeysetId keysetId, int bumpBy = 1, CancellationToken ct = default); public Task SetCounter(KeysetId keysetId, int counter, CancellationToken ct = default); - public IReadOnlyDictionary Export(); + public Task> Export(); } \ No newline at end of file From 5f73a18b81b09c5ac9aca98edca687bc09484e61 Mon Sep 17 00:00:00 2001 From: d4rp4t Date: Sun, 21 Dec 2025 13:23:56 +0100 Subject: [PATCH 39/70] Make wallet and cashu http client disposable --- DotNut/Abstractions/Interfaces/IWalletBuilder.cs | 2 +- DotNut/Abstractions/Wallet.cs | 12 ++++++++---- DotNut/Api/CashuHttpClient.cs | 12 +++++++++++- DotNut/Api/ICashuApi.cs | 2 +- 4 files changed, 21 insertions(+), 7 deletions(-) diff --git a/DotNut/Abstractions/Interfaces/IWalletBuilder.cs b/DotNut/Abstractions/Interfaces/IWalletBuilder.cs index 34e509c..053fed4 100644 --- a/DotNut/Abstractions/Interfaces/IWalletBuilder.cs +++ b/DotNut/Abstractions/Interfaces/IWalletBuilder.cs @@ -8,7 +8,7 @@ namespace DotNut.Abstractions; /// /// Fluent builder interface for Cashu Wallet operations /// -public interface IWalletBuilder +public interface IWalletBuilder : IDisposable { /// /// Mandatory. Sets a mint in a wallet object diff --git a/DotNut/Abstractions/Wallet.cs b/DotNut/Abstractions/Wallet.cs index e30b7d7..7a9dc8a 100644 --- a/DotNut/Abstractions/Wallet.cs +++ b/DotNut/Abstractions/Wallet.cs @@ -27,9 +27,7 @@ public class Wallet : IWalletBuilder private bool _shouldSyncKeyset = true; private DateTime? _lastSync = DateTime.MinValue; private TimeSpan? _syncThreshold; // if null sync only once - private bool _shouldBumpCounter = true; - /* * Fluent Builder Methods @@ -45,7 +43,7 @@ public IWalletBuilder WithMint(ICashuApi mintApi) public IWalletBuilder WithMint(string mintUrl) { var httpClient = new HttpClient{ BaseAddress = new Uri(mintUrl)}; - _mintApi = new CashuHttpClient(httpClient); + _mintApi = new CashuHttpClient(httpClient, true); return this; } @@ -335,9 +333,10 @@ public async Task GetSelector(CancellationToken ct = default) } public async Task GetWebsocketService(CancellationToken ct = default) - { + { return this._wsService ??= new WebsocketService(); } + public Mnemonic? GetMnemonic() => _mnemonic; public ICounter? GetCounter() => _counter; @@ -493,5 +492,10 @@ internal async Task _maybeSyncKeys(CancellationToken cts = default) _lastSync = DateTime.UtcNow; } + + public void Dispose() + { + _mintApi?.Dispose(); + } } diff --git a/DotNut/Api/CashuHttpClient.cs b/DotNut/Api/CashuHttpClient.cs index 9b70e72..cf28069 100644 --- a/DotNut/Api/CashuHttpClient.cs +++ b/DotNut/Api/CashuHttpClient.cs @@ -9,12 +9,14 @@ namespace DotNut.Api; public class CashuHttpClient : ICashuApi { private readonly HttpClient _httpClient; + private readonly bool _ownsHttpClient; - public CashuHttpClient(HttpClient httpClient) + public CashuHttpClient(HttpClient httpClient, bool ownsHttpClient = false) { ArgumentNullException.ThrowIfNull(httpClient); ArgumentNullException.ThrowIfNull(httpClient.BaseAddress); _httpClient = httpClient; + _ownsHttpClient = ownsHttpClient; } public string GetBaseUrl() @@ -138,4 +140,12 @@ protected async Task HandleResponse(HttpResponseMessage response, Cancella return result!; } + + public void Dispose() + { + if (_ownsHttpClient) + { + _httpClient.Dispose(); + } + } } \ No newline at end of file diff --git a/DotNut/Api/ICashuApi.cs b/DotNut/Api/ICashuApi.cs index c73252d..3513d18 100644 --- a/DotNut/Api/ICashuApi.cs +++ b/DotNut/Api/ICashuApi.cs @@ -2,7 +2,7 @@ namespace DotNut.Api; -public interface ICashuApi +public interface ICashuApi: IDisposable { string GetBaseUrl(); Task GetKeys(CancellationToken cancellationToken = default); From 6d74434a33319b9533cc78e5b194fcc45b13b993 Mon Sep 17 00:00:00 2001 From: d4rp4t Date: Sun, 21 Dec 2025 20:46:55 +0100 Subject: [PATCH 40/70] add URI mint initialization --- DotNut/Abstractions/Interfaces/IWalletBuilder.cs | 6 ++++++ DotNut/Abstractions/Wallet.cs | 7 +++++++ 2 files changed, 13 insertions(+) diff --git a/DotNut/Abstractions/Interfaces/IWalletBuilder.cs b/DotNut/Abstractions/Interfaces/IWalletBuilder.cs index 053fed4..6523753 100644 --- a/DotNut/Abstractions/Interfaces/IWalletBuilder.cs +++ b/DotNut/Abstractions/Interfaces/IWalletBuilder.cs @@ -21,6 +21,12 @@ public interface IWalletBuilder : IDisposable /// /// Mint URL string. IWalletBuilder WithMint(string mintUrl); + + /// + /// Mandatory. Sets a mint in a wallet object (with default CashuHttpClient) + /// + /// Mint URI. + IWalletBuilder WithMint(Uri mintUri); /// /// Optional. Import Mint Info to CashuWallet. Otherwise, it will be fetched from /v1/info endpoint. diff --git a/DotNut/Abstractions/Wallet.cs b/DotNut/Abstractions/Wallet.cs index 7a9dc8a..bf2cabb 100644 --- a/DotNut/Abstractions/Wallet.cs +++ b/DotNut/Abstractions/Wallet.cs @@ -46,6 +46,13 @@ public IWalletBuilder WithMint(string mintUrl) _mintApi = new CashuHttpClient(httpClient, true); return this; } + + public IWalletBuilder WithMint(Uri mintUri) + { + var httpClient = new HttpClient { BaseAddress = mintUri }; + _mintApi = new CashuHttpClient(httpClient, true); + return this; + } public IWalletBuilder WithInfo(MintInfo info) { From 36c6ecad0b682658fba2f6cc2a9e40eb268e99bf Mon Sep 17 00:00:00 2001 From: d4rp4t Date: Mon, 22 Dec 2025 16:20:09 +0100 Subject: [PATCH 41/70] Remove redundant converter --- DotNut/NUT00/Proof.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/DotNut/NUT00/Proof.cs b/DotNut/NUT00/Proof.cs index 5c9dd78..e332d02 100644 --- a/DotNut/NUT00/Proof.cs +++ b/DotNut/NUT00/Proof.cs @@ -8,7 +8,6 @@ public class Proof { [JsonPropertyName("amount")] public ulong Amount { get; set; } - [JsonConverter(typeof(KeysetIdJsonConverter))] [JsonPropertyName("id")] public KeysetId Id { get; set; } From 90d5427e8327c832673d96be5dba921d8553d61d Mon Sep 17 00:00:00 2001 From: d4rp4t Date: Mon, 22 Dec 2025 16:23:54 +0100 Subject: [PATCH 42/70] jsonignore on memo --- DotNut/NUT18/PaymentRequestPayload.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/DotNut/NUT18/PaymentRequestPayload.cs b/DotNut/NUT18/PaymentRequestPayload.cs index bef5663..65c9e3c 100644 --- a/DotNut/NUT18/PaymentRequestPayload.cs +++ b/DotNut/NUT18/PaymentRequestPayload.cs @@ -5,6 +5,7 @@ namespace DotNut; public class PaymentRequestPayload { [JsonPropertyName("id")] public string PaymentId { get; set; } + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] [JsonPropertyName("memo")] public string? Memo { get; set; } [JsonPropertyName("mint")] public string Mint { get; set; } [JsonPropertyName("unit")] public string Unit { get; set; } From e685e85cb5c749f825073d2d02a06349907a21a5 Mon Sep 17 00:00:00 2001 From: d4rp4t Date: Sun, 28 Dec 2025 23:20:35 +0100 Subject: [PATCH 43/70] fix restore --- DotNut.Tests/Integration.cs | 16 ++++++- DotNut/Abstractions/Interfaces/ICounter.cs | 7 +++ .../Interfaces/IRestoreBuilder.cs | 8 ---- DotNut/Abstractions/RestoreBuilder.cs | 48 +++++++++++++------ 4 files changed, 54 insertions(+), 25 deletions(-) diff --git a/DotNut.Tests/Integration.cs b/DotNut.Tests/Integration.cs index e4f987c..75e1745 100644 --- a/DotNut.Tests/Integration.cs +++ b/DotNut.Tests/Integration.cs @@ -126,15 +126,27 @@ public async Task MintsDeterministicSuccessfully() [Fact] public async Task RestoresSuccessfully() { + var phreshCounter = new InMemoryCounter(); + var wallet = Wallet .Create() + .WithCounter(phreshCounter) .WithMint(MintUrl) .WithMnemonic(seed); + var restoredProofs = await wallet .Restore() .ProcessAsync(); - var keyset = (await wallet.GetKeys()).First().Keys; - var expectedAmount = Utils.SplitToProofsAmounts(1336UL, keyset).Count; // (one for fee) + + var keys = (await wallet.GetKeys()).First().Keys; + var expectedAmount = Utils.SplitToProofsAmounts(1336UL, keys).Count; // (one for fee) + var keysets = await wallet.GetKeysets(); + + foreach (var keyset in keysets) + { + // new counter will be bumped to newest state + Assert.Equal(await counter.GetCounterForId(keyset.Id) + expectedAmount, await phreshCounter.GetCounterForId(keyset.Id)); + } Assert.Equal(expectedAmount, restoredProofs.Count()); } diff --git a/DotNut/Abstractions/Interfaces/ICounter.cs b/DotNut/Abstractions/Interfaces/ICounter.cs index 9b941af..a49b78f 100644 --- a/DotNut/Abstractions/Interfaces/ICounter.cs +++ b/DotNut/Abstractions/Interfaces/ICounter.cs @@ -2,6 +2,13 @@ namespace DotNut.Abstractions; public interface ICounter { + /// + /// Gets counter for current keysetID. This counter will be used for next proof generation, so make sure it's + /// always set to last used proof + 1 + /// + /// + /// + /// public Task GetCounterForId(KeysetId keysetId, CancellationToken ct = default); public Task IncrementCounter(KeysetId keysetId, int bumpBy = 1, CancellationToken ct = default); public Task SetCounter(KeysetId keysetId, int counter, CancellationToken ct = default); diff --git a/DotNut/Abstractions/Interfaces/IRestoreBuilder.cs b/DotNut/Abstractions/Interfaces/IRestoreBuilder.cs index e9f780a..27a48c9 100644 --- a/DotNut/Abstractions/Interfaces/IRestoreBuilder.cs +++ b/DotNut/Abstractions/Interfaces/IRestoreBuilder.cs @@ -13,13 +13,5 @@ public interface IRestoreBuilder /// IRestoreBuilder FromKeysetIds(IEnumerable keysetIds); - /// - /// Optional, allows to set counter which will hold the state after restore. - /// If not set, defaults to InMemoryCounter that is not returned. - /// - /// - /// - public IRestoreBuilder WithCounter(ICounter counter); - Task> ProcessAsync(CancellationToken ct = default); } diff --git a/DotNut/Abstractions/RestoreBuilder.cs b/DotNut/Abstractions/RestoreBuilder.cs index 5021021..6924d47 100644 --- a/DotNut/Abstractions/RestoreBuilder.cs +++ b/DotNut/Abstractions/RestoreBuilder.cs @@ -9,8 +9,7 @@ public class RestoreBuilder : IRestoreBuilder private readonly Wallet _wallet; private List? _specifiedKeysets; private static int BATCH_SIZE = 100; - - private ICounter? _counter; + private static int EMPTY_BATCHES_ALLOWED = 3; public RestoreBuilder(Wallet wallet) { @@ -22,12 +21,6 @@ public IRestoreBuilder FromKeysetIds(IEnumerable keysetIds) this._specifiedKeysets = keysetIds.ToList(); return this; } - - public IRestoreBuilder WithCounter(ICounter counter) - { - this._counter = counter; - return this; - } public async Task> ProcessAsync(CancellationToken ct = default) @@ -45,13 +38,20 @@ public async Task> ProcessAsync(CancellationToken ct = defaul throw new InvalidOperationException("No keysets available for restoration. Ensure the mint has at least one active keyset or specify keysets explicitly."); } - _counter ??= new InMemoryCounter(); + var counter = _wallet.GetCounter(); + if (counter == null) + { + throw new ArgumentNullException(nameof(counter), "Counter cannot be null."); + } + // fetch all batches List recoveredProofs = new List(); foreach (var keysetId in _specifiedKeysets) { int batchNumber = 0; - int emptyBatchesRemaining = 3; + int emptyBatchesRemaining = EMPTY_BATCHES_ALLOWED; + int lastUsedCounter = 0; + int tempCounter = 0; // don't care about invalid / non existent source keyset ids. let's fetch what we can GetKeysResponse.KeysetItemResponse? keyset; @@ -59,7 +59,7 @@ public async Task> ProcessAsync(CancellationToken ct = defaul { keyset = await _wallet.GetKeys(keysetId, true, false, ct); } - catch (Exception e) + catch { continue; } @@ -71,31 +71,49 @@ public async Task> ProcessAsync(CancellationToken ct = defaul // proofs for keysetid are considered restored after 3 empty batches. while (emptyBatchesRemaining > 0) { + // create batch of 100, and request restore for whole batch var outputs = await _createBatch(mnemonic, keysetId, batchNumber, ct); var req = new PostRestoreRequest { Outputs = outputs.Select(o=>o.BlindedMessage).ToArray() }; var res = await api.Restore(req, ct); - await _counter!.IncrementCounter(keysetId, BATCH_SIZE, ct); - batchNumber++; + + if (res.Signatures.Length == 0) { emptyBatchesRemaining--; + batchNumber++; continue; } + + // find last restored index of batch + var lastUsedIndexInBatch = outputs.Select((o, i) => new { o, i }) + .Where(x => res.Outputs.Any(r => Equals(r.B_, x.o.BlindedMessage.B_))) + .MaxBy(x => x.i)!.i; + + // set last used counter value for this batch + lastUsedCounter = BATCH_SIZE * batchNumber + lastUsedIndexInBatch; + + // bump batch number after calculating last used counter + batchNumber++; + + // if anything found, reset batches counter + emptyBatchesRemaining = EMPTY_BATCHES_ALLOWED; var returnedOutputs = new List(); - foreach (var output in res.Outputs) { + // there can't be any dupes here returnedOutputs.Add(outputs.Single(o=>Equals(o.BlindedMessage.B_, output.B_))); } var proofs = Utils.ConstructProofsFromPromises(res.Signatures.ToList(), returnedOutputs , keyset.Keys); recoveredProofs.AddRange(proofs); } - + + // 1 is added so we'll be consistent with counter usage. it will be ready for next use + await counter.SetCounter(keysetId, lastUsedCounter + 1, ct); } // if nothing found - return empty collection From 7e085b35c8a19506d5ccaf4a68b2911b2e7cad2c Mon Sep 17 00:00:00 2001 From: d4rp4t Date: Mon, 29 Dec 2025 00:12:51 +0100 Subject: [PATCH 44/70] fix tests --- DotNut.Tests/Integration.cs | 147 +++++++++++++++++++----------------- 1 file changed, 77 insertions(+), 70 deletions(-) diff --git a/DotNut.Tests/Integration.cs b/DotNut.Tests/Integration.cs index 75e1745..1943739 100644 --- a/DotNut.Tests/Integration.cs +++ b/DotNut.Tests/Integration.cs @@ -148,6 +148,9 @@ public async Task RestoresSuccessfully() Assert.Equal(await counter.GetCounterForId(keyset.Id) + expectedAmount, await phreshCounter.GetCounterForId(keyset.Id)); } Assert.Equal(expectedAmount, restoredProofs.Count()); + + // assign restored counter to previous one, so next tests can use it safely + counter = phreshCounter; } [Fact] @@ -285,6 +288,80 @@ public async Task MeltsBolt12Successfully() Assert.NotEmpty(change); } + + [Fact] + public async Task SubscribeToMintMeltQuoteUpdates() + { + await using var service = new WebsocketService(); + var connection = await service.ConnectAsync(MintUrl); + Assert.NotNull(connection); + + var wallet = Wallet + .Create() + .WithMint(MintUrl) + .WithWebsocketService(service); + + var mintHandler = await wallet + .CreateMintQuote() + .WithAmount(3338) + .WithUnit("sat") + .ProcessAsyncBolt11(); + + var quote = mintHandler.GetQuote(); + + var sub = await service.SubscribeToMintQuoteAsync(MintUrl, new[] { quote.Quote }); + + int connectedCount = 0; + int notificationCount = 0; + + using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(240)); + + var connectedTcs = new TaskCompletionSource(); + var paidTcs = new TaskCompletionSource(); + + _ = Task.Run(async () => + { + await connectedTcs.Task.WaitAsync(TimeSpan.FromSeconds(10)); + await Task.Delay(1000, cts.Token); + await PayInvoice(); + }, cts.Token); + + await foreach (var msg in sub.NotificationChannel.Reader.ReadAllAsync(cts.Token)) + { + switch (msg) + { + case WsMessage.Response: + connectedCount++; + connectedTcs.TrySetResult(); + break; + + case WsMessage.Notification notification: + notificationCount++; + + if (notificationCount >= 2) + paidTcs.TrySetResult(); + + break; + + case WsMessage.Error error: + Assert.Fail($"WebSocket error: {error}"); + break; + + default: + Assert.Fail($"Unexpected message type: {msg.GetType().Name}"); + break; + } + + if (paidTcs.Task.IsCompleted) + break; + } + + Assert.Equal(1, connectedCount); + Assert.True(notificationCount >= 2, $"Expected >=2 notifications, got {notificationCount}"); + + var proofs = await mintHandler.Mint(); + Assert.NotEmpty(proofs); + } [Fact] public async Task InvoiceWithDescription() @@ -927,77 +1004,7 @@ public async Task MeltWithInsufficientFunds() Assert.Empty(selection.Send); Assert.NotEmpty(selection.Keep); } - - [Fact] - public async Task SubscribeToMintMeltQuoteUpdates() - { - await using var service = new WebsocketService(); - var connection = await service.ConnectAsync(MintUrl); - Assert.NotNull(connection); - - var wallet = Wallet.Create().WithMint(MintUrl); - - var mintHandler = await wallet - .CreateMintQuote() - .WithAmount(3338) - .WithUnit("sat") - .ProcessAsyncBolt11(); - - var quote = mintHandler.GetQuote(); - - var sub = await service.SubscribeToMintQuoteAsync(MintUrl, new[] { quote.Quote }); - - int connectedCount = 0; - int notificationCount = 0; - - using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(240)); - - var connectedTcs = new TaskCompletionSource(); - var paidTcs = new TaskCompletionSource(); - - _ = Task.Run(async () => - { - await connectedTcs.Task.WaitAsync(TimeSpan.FromSeconds(10)); - await Task.Delay(1000, cts.Token); - await PayInvoice(); - }, cts.Token); - - await foreach (var msg in sub.NotificationChannel.Reader.ReadAllAsync(cts.Token)) - { - switch (msg) - { - case WsMessage.Response: - connectedCount++; - connectedTcs.TrySetResult(); - break; - - case WsMessage.Notification notification: - notificationCount++; - - if (notificationCount >= 2) - paidTcs.TrySetResult(); - - break; - - case WsMessage.Error error: - Assert.Fail($"WebSocket error: {error}"); - break; - - default: - Assert.Fail($"Unexpected message type: {msg.GetType().Name}"); - break; - } - - if (paidTcs.Task.IsCompleted) - break; - } - Assert.Equal(1, connectedCount); - Assert.True(notificationCount >= 2, $"Expected >=2 notifications, got {notificationCount}"); - - var proofs = await mintHandler.Mint(); - Assert.NotEmpty(proofs); - } private async Task PayInvoice() From 904b567b46e9d9135628bedf49ccec857a55abf2 Mon Sep 17 00:00:00 2001 From: d4rp4t Date: Tue, 6 Jan 2026 14:05:14 +0100 Subject: [PATCH 45/70] fix contact info --- DotNut/Abstractions/MintInfo.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/DotNut/Abstractions/MintInfo.cs b/DotNut/Abstractions/MintInfo.cs index e92a4ec..b897e7e 100644 --- a/DotNut/Abstractions/MintInfo.cs +++ b/DotNut/Abstractions/MintInfo.cs @@ -225,7 +225,7 @@ public bool SupportsBolt12Description } - public string? Contact => _mintInfo.Contact?.FirstOrDefault()?.ToString(); + public List? Contact => _mintInfo.Contact; public string? Description => _mintInfo.Description; public string? DescriptionLong => _mintInfo.DescriptionLong; public string? Name => _mintInfo.Name; From 5a912ae601d47acd052dc1007e1ed7a99117df3c Mon Sep 17 00:00:00 2001 From: d4rp4t Date: Sun, 11 Jan 2026 17:21:33 +0100 Subject: [PATCH 46/70] fix counter to always be uint --- DotNut.Tests/Integration.cs | 2 +- DotNut.Tests/UnitTests2.cs | 16 ++++++------- DotNut/Abstractions/InMemoryCounter.cs | 18 +++++++------- DotNut/Abstractions/Interfaces/ICounter.cs | 8 +++---- .../Abstractions/Interfaces/IWalletBuilder.cs | 2 +- DotNut/Abstractions/RestoreBuilder.cs | 24 +++++++++++-------- DotNut/Abstractions/Utils.cs | 8 +++---- DotNut/Abstractions/Wallet.cs | 8 ++++--- DotNut/NUT13/Nut13.cs | 8 +++---- 9 files changed, 50 insertions(+), 44 deletions(-) diff --git a/DotNut.Tests/Integration.cs b/DotNut.Tests/Integration.cs index 1943739..2c138eb 100644 --- a/DotNut.Tests/Integration.cs +++ b/DotNut.Tests/Integration.cs @@ -120,7 +120,7 @@ public async Task MintsDeterministicSuccessfully() var keysetId = mintedProofs.First().Id; var currentCounter = await counter.GetCounterForId(keysetId); // counter is bumped after every use, so its already one more - Assert.Equal(currentCounter, mintedProofs.Count); + Assert.Equal(currentCounter, (uint)mintedProofs.Count); } [Fact] diff --git a/DotNut.Tests/UnitTests2.cs b/DotNut.Tests/UnitTests2.cs index c6cf799..948974c 100644 --- a/DotNut.Tests/UnitTests2.cs +++ b/DotNut.Tests/UnitTests2.cs @@ -81,20 +81,20 @@ public async Task InMemoryCounter() Assert.NotNull(ctr); var testId1 = new KeysetId("00qwertyuiopasdf"); var ctrNum = await ctr.GetCounterForId(testId1); - Assert.Equal(0, ctrNum); + Assert.Equal((uint)0, ctrNum); await ctr.IncrementCounter(testId1); - Assert.Equal(0, ctrNum); + Assert.Equal((uint)0, ctrNum); ctrNum = await ctr.GetCounterForId(testId1); - Assert.Equal(1, ctrNum); + Assert.Equal((uint)1, ctrNum); await ctr.IncrementCounter(testId1, 5); ctrNum = await ctr.GetCounterForId(testId1); - Assert.Equal(6, ctrNum); + Assert.Equal((uint)6, ctrNum); await ctr.SetCounter(testId1, 1337); ctrNum = await ctr.GetCounterForId(testId1); - Assert.Equal(1337, ctrNum); + Assert.Equal((uint)1337, ctrNum); } [Fact] @@ -590,7 +590,7 @@ public async Task Counter_ReturnsZeroForUnknownKeysetId() var unknownKeysetId = new KeysetId("00unknown1234567"); var value = await counter.GetCounterForId(unknownKeysetId); - Assert.Equal(0, value); + Assert.Equal((uint)0, value); } [Fact] @@ -603,8 +603,8 @@ public async Task Counter_MultipleKeysets_IndependentCounters() await counter.IncrementCounter(keysetId1, 10); await counter.IncrementCounter(keysetId2, 20); - Assert.Equal(10, await counter.GetCounterForId(keysetId1)); - Assert.Equal(20, await counter.GetCounterForId(keysetId2)); + Assert.Equal((uint)10, await counter.GetCounterForId(keysetId1)); + Assert.Equal((uint)20, await counter.GetCounterForId(keysetId2)); } } diff --git a/DotNut/Abstractions/InMemoryCounter.cs b/DotNut/Abstractions/InMemoryCounter.cs index a1d9ffa..ee0e9fe 100644 --- a/DotNut/Abstractions/InMemoryCounter.cs +++ b/DotNut/Abstractions/InMemoryCounter.cs @@ -4,36 +4,36 @@ namespace DotNut.Abstractions; public class InMemoryCounter : ICounter { - private readonly ConcurrentDictionary _counter; - public InMemoryCounter(IDictionary counter) + private readonly ConcurrentDictionary _counter; + public InMemoryCounter(IDictionary counter) { - this._counter = new ConcurrentDictionary(counter); + this._counter = new ConcurrentDictionary(counter); } public InMemoryCounter() { - this._counter = new ConcurrentDictionary(); + this._counter = new ConcurrentDictionary(); } - public Task GetCounterForId(KeysetId keysetId, CancellationToken ct = default) + public Task GetCounterForId(KeysetId keysetId, CancellationToken ct = default) { return Task.FromResult(_counter.GetOrAdd(keysetId, 0)); } - public Task IncrementCounter(KeysetId keysetId, int bumpBy = 1, CancellationToken ct = default) + public Task IncrementCounter(KeysetId keysetId, uint bumpBy = 1, CancellationToken ct = default) { var next = _counter.AddOrUpdate(keysetId, bumpBy, (_, current) => current + bumpBy); return Task.FromResult(next); } - public Task SetCounter(KeysetId keysetId, int counter, CancellationToken ct = default) { + public Task SetCounter(KeysetId keysetId, uint counter, CancellationToken ct = default) { _counter[keysetId] = counter; return Task.CompletedTask; } - public async Task> Export() + public async Task> Export() { - return new Dictionary(_counter); + return new Dictionary(_counter); } } \ No newline at end of file diff --git a/DotNut/Abstractions/Interfaces/ICounter.cs b/DotNut/Abstractions/Interfaces/ICounter.cs index a49b78f..a460331 100644 --- a/DotNut/Abstractions/Interfaces/ICounter.cs +++ b/DotNut/Abstractions/Interfaces/ICounter.cs @@ -9,8 +9,8 @@ public interface ICounter /// /// /// - public Task GetCounterForId(KeysetId keysetId, CancellationToken ct = default); - public Task IncrementCounter(KeysetId keysetId, int bumpBy = 1, CancellationToken ct = default); - public Task SetCounter(KeysetId keysetId, int counter, CancellationToken ct = default); - public Task> Export(); + public Task GetCounterForId(KeysetId keysetId, CancellationToken ct = default); + public Task IncrementCounter(KeysetId keysetId, uint bumpBy = 1, CancellationToken ct = default); + public Task SetCounter(KeysetId keysetId, uint counter, CancellationToken ct = default); + public Task> Export(); } \ No newline at end of file diff --git a/DotNut/Abstractions/Interfaces/IWalletBuilder.cs b/DotNut/Abstractions/Interfaces/IWalletBuilder.cs index 6523753..fba913f 100644 --- a/DotNut/Abstractions/Interfaces/IWalletBuilder.cs +++ b/DotNut/Abstractions/Interfaces/IWalletBuilder.cs @@ -110,7 +110,7 @@ public interface IWalletBuilder : IDisposable /// /// Counter dictionary /// - public IWalletBuilder WithCounter(IDictionary counter); + public IWalletBuilder WithCounter(IDictionary counter); /// /// Optional and if not set, always true. Controls automatic counter incrementation for secret generation. diff --git a/DotNut/Abstractions/RestoreBuilder.cs b/DotNut/Abstractions/RestoreBuilder.cs index 6924d47..92f7624 100644 --- a/DotNut/Abstractions/RestoreBuilder.cs +++ b/DotNut/Abstractions/RestoreBuilder.cs @@ -8,8 +8,8 @@ public class RestoreBuilder : IRestoreBuilder { private readonly Wallet _wallet; private List? _specifiedKeysets; - private static int BATCH_SIZE = 100; - private static int EMPTY_BATCHES_ALLOWED = 3; + private static uint BATCH_SIZE = 100; + private static uint EMPTY_BATCHES_ALLOWED = 3; public RestoreBuilder(Wallet wallet) { @@ -48,10 +48,10 @@ public async Task> ProcessAsync(CancellationToken ct = defaul List recoveredProofs = new List(); foreach (var keysetId in _specifiedKeysets) { - int batchNumber = 0; - int emptyBatchesRemaining = EMPTY_BATCHES_ALLOWED; - int lastUsedCounter = 0; - int tempCounter = 0; + uint batchNumber = 0; + uint emptyBatchesRemaining = EMPTY_BATCHES_ALLOWED; + uint lastUsedCounter = 0; + uint tempCounter = 0; // don't care about invalid / non existent source keyset ids. let's fetch what we can GetKeysResponse.KeysetItemResponse? keyset; @@ -72,7 +72,7 @@ public async Task> ProcessAsync(CancellationToken ct = defaul while (emptyBatchesRemaining > 0) { // create batch of 100, and request restore for whole batch - var outputs = await _createBatch(mnemonic, keysetId, batchNumber, ct); + var outputs = await _createBatch(mnemonic, keysetId, (int)batchNumber, ct); var req = new PostRestoreRequest { Outputs = outputs.Select(o=>o.BlindedMessage).ToArray() @@ -88,7 +88,7 @@ public async Task> ProcessAsync(CancellationToken ct = defaul } // find last restored index of batch - var lastUsedIndexInBatch = outputs.Select((o, i) => new { o, i }) + uint lastUsedIndexInBatch = (uint)outputs.Select((o, i) => new { o, i }) .Where(x => res.Outputs.Any(r => Equals(r.B_, x.o.BlindedMessage.B_))) .MaxBy(x => x.i)!.i; @@ -180,7 +180,11 @@ public async Task> ProcessAsync(CancellationToken ct = defaul private static async Task> _createBatch(Mnemonic mnemonic, KeysetId keysetId, int batchNumber, CancellationToken ct) { - var amounts = Enumerable.Repeat((ulong)0, BATCH_SIZE).ToList(); - return mnemonic.DeriveOutputs(amounts, keysetId, batchNumber*BATCH_SIZE); + if (batchNumber < 0) + { + throw new ArgumentOutOfRangeException(nameof(batchNumber)); + } + var amounts = Enumerable.Repeat((ulong)0, (int)BATCH_SIZE).ToList(); + return mnemonic.DeriveOutputs(amounts, keysetId, (uint)(batchNumber * BATCH_SIZE)); } } \ No newline at end of file diff --git a/DotNut/Abstractions/Utils.cs b/DotNut/Abstractions/Utils.cs index b37e0b4..c5092a5 100644 --- a/DotNut/Abstractions/Utils.cs +++ b/DotNut/Abstractions/Utils.cs @@ -40,7 +40,7 @@ public static List SplitToProofsAmounts(ulong paymentAmount, Keyset keyse /// Active keyset id which will sign outputs /// Keys for given KeysetId /// Blank Outputs - public static List CreateBlankOutputs(ulong amount, KeysetId keysetId, Keyset keys, NBitcoin.BIP39.Mnemonic? mnemonic = null, int? counter = null) + public static List CreateBlankOutputs(ulong amount, KeysetId keysetId, Keyset keys, NBitcoin.BIP39.Mnemonic? mnemonic = null, uint? counter = null) { if (amount == 0) { @@ -88,7 +88,7 @@ public static List CreateOutputs( KeysetId keysetId, Keyset keys, NBitcoin.BIP39.Mnemonic? mnemonic = null, - int? counter = null) + uint? counter = null) { if (amounts.Any(a => !keys.Keys.Contains(a))) throw new ArgumentException("Invalid amounts"); @@ -98,14 +98,14 @@ public static List CreateOutputs( if (mnemonic is not null && counter is { } c) { - for (var i = 0; i < amounts.Count; i++) + for (uint i = 0; i < amounts.Count; i++) { var secret = mnemonic.DeriveSecret(keysetId, c + i); var r = new PrivKey(mnemonic.DeriveBlindingFactor(keysetId, c + i)); var B_ = Cashu.ComputeB_(secret.ToCurve(), r); var output = new OutputData { - BlindedMessage = new BlindedMessage { Amount = amounts[i], B_ = B_, Id = keysetId }, + BlindedMessage = new BlindedMessage { Amount = amounts[(int)i], B_ = B_, Id = keysetId }, BlindingFactor = r, Secret = secret }; diff --git a/DotNut/Abstractions/Wallet.cs b/DotNut/Abstractions/Wallet.cs index bf2cabb..d3235bb 100644 --- a/DotNut/Abstractions/Wallet.cs +++ b/DotNut/Abstractions/Wallet.cs @@ -115,7 +115,7 @@ public IWalletBuilder WithCounter(ICounter counter) return this; } - public IWalletBuilder WithCounter(IDictionary counter) + public IWalletBuilder WithCounter(IDictionary counter) { this._counter = new InMemoryCounter(counter); return this; @@ -294,10 +294,12 @@ public async Task> CreateOutputs(List amounts, KeysetId } var counterValue = await this._counter.GetCounterForId(id, ct); - if (_shouldBumpCounter) + if (!_shouldBumpCounter) { - await this._counter.IncrementCounter(id, amounts.Count, ct); + return Utils.CreateOutputs(amounts, id, keyset.Keys, this._mnemonic, counterValue); } + + await this._counter.IncrementCounter(id, (uint)amounts.Count, ct); return Utils.CreateOutputs(amounts, id, keyset.Keys, this._mnemonic, counterValue); } diff --git a/DotNut/NUT13/Nut13.cs b/DotNut/NUT13/Nut13.cs index f018c24..a47278f 100644 --- a/DotNut/NUT13/Nut13.cs +++ b/DotNut/NUT13/Nut13.cs @@ -16,13 +16,13 @@ public static StringSecret DeriveSecret(this Mnemonic mnemonic, KeysetId keysetI public static List DeriveOutputs(this Mnemonic mnemonic, IEnumerable amounts, KeysetId keysetId, - int counter) + uint counter) { var outputs = new List(); var amountList = amounts.ToList(); - for (int i = 0; i < amountList.Count; i++) + for (uint i = 0; i < amountList.Count; i++) { var secret = DeriveSecret(mnemonic, keysetId, counter + i); var r = new PrivKey( @@ -36,7 +36,7 @@ public static List DeriveOutputs(this Mnemonic mnemonic, IEnumerable { BlindedMessage = new BlindedMessage() { - Amount = amountList[i], + Amount = amountList[(int)i], Id = keysetId, B_ = B_ }, @@ -47,7 +47,7 @@ public static List DeriveOutputs(this Mnemonic mnemonic, IEnumerable return outputs; } - public static byte[] DeriveBlindingFactor(this byte[] seed, KeysetId keysetId, int counter) + public static byte[] DeriveBlindingFactor(this byte[] seed, KeysetId keysetId, uint counter) { switch (keysetId.GetVersion()) { From f9c88e1ec864b725c250e4f77e57c5a3a468e49e Mon Sep 17 00:00:00 2001 From: d4rp4t Date: Sun, 11 Jan 2026 18:06:47 +0100 Subject: [PATCH 47/70] cleanup restorebuilder --- DotNut/Abstractions/RestoreBuilder.cs | 210 +++++++++++++++----------- 1 file changed, 121 insertions(+), 89 deletions(-) diff --git a/DotNut/Abstractions/RestoreBuilder.cs b/DotNut/Abstractions/RestoreBuilder.cs index 92f7624..57d3897 100644 --- a/DotNut/Abstractions/RestoreBuilder.cs +++ b/DotNut/Abstractions/RestoreBuilder.cs @@ -1,3 +1,4 @@ +using DotNut.Api; using DotNut.ApiModels; using DotNut.NBitcoin.BIP39; using DotNut.NUT13; @@ -15,27 +16,31 @@ public RestoreBuilder(Wallet wallet) { this._wallet = wallet; } - + public IRestoreBuilder FromKeysetIds(IEnumerable keysetIds) { this._specifiedKeysets = keysetIds.ToList(); return this; } - public async Task> ProcessAsync(CancellationToken ct = default) { var api = await _wallet.GetMintApi(ct); await _wallet._maybeSyncKeys(ct); - var mnemonic = _wallet.GetMnemonic()?? - throw new ArgumentNullException(nameof(Mnemonic), "Can't restore wallet without Mnemonic"); - + var mnemonic = + _wallet.GetMnemonic() + ?? throw new ArgumentNullException( + nameof(Mnemonic), + "Can't restore wallet without Mnemonic" + ); + // keyset ids we want to grind our counter on - _specifiedKeysets ??= - (await _wallet.GetKeysets(ct: ct)).Select(k => k.Id).ToList(); + _specifiedKeysets ??= (await _wallet.GetKeysets(ct: ct)).Select(k => k.Id).ToList(); if (_specifiedKeysets == null || _specifiedKeysets.Count == 0) { - throw new InvalidOperationException("No keysets available for restoration. Ensure the mint has at least one active keyset or specify keysets explicitly."); + throw new InvalidOperationException( + "No keysets available for restoration. Ensure the mint has at least one active keyset or specify keysets explicitly." + ); } var counter = _wallet.GetCounter(); @@ -43,79 +48,15 @@ public async Task> ProcessAsync(CancellationToken ct = defaul { throw new ArgumentNullException(nameof(counter), "Counter cannot be null."); } - + // fetch all batches List recoveredProofs = new List(); foreach (var keysetId in _specifiedKeysets) { - uint batchNumber = 0; - uint emptyBatchesRemaining = EMPTY_BATCHES_ALLOWED; - uint lastUsedCounter = 0; - uint tempCounter = 0; - - // don't care about invalid / non existent source keyset ids. let's fetch what we can - GetKeysResponse.KeysetItemResponse? keyset; - try - { - keyset = await _wallet.GetKeys(keysetId, true, false, ct); - } - catch - { - continue; - } - if (keyset == null) - { - continue; - } - - // proofs for keysetid are considered restored after 3 empty batches. - while (emptyBatchesRemaining > 0) - { - // create batch of 100, and request restore for whole batch - var outputs = await _createBatch(mnemonic, keysetId, (int)batchNumber, ct); - var req = new PostRestoreRequest - { - Outputs = outputs.Select(o=>o.BlindedMessage).ToArray() - }; - var res = await api.Restore(req, ct); - - - if (res.Signatures.Length == 0) - { - emptyBatchesRemaining--; - batchNumber++; - continue; - } - - // find last restored index of batch - uint lastUsedIndexInBatch = (uint)outputs.Select((o, i) => new { o, i }) - .Where(x => res.Outputs.Any(r => Equals(r.B_, x.o.BlindedMessage.B_))) - .MaxBy(x => x.i)!.i; - - // set last used counter value for this batch - lastUsedCounter = BATCH_SIZE * batchNumber + lastUsedIndexInBatch; - - // bump batch number after calculating last used counter - batchNumber++; - - // if anything found, reset batches counter - emptyBatchesRemaining = EMPTY_BATCHES_ALLOWED; - - var returnedOutputs = new List(); - foreach (var output in res.Outputs) - { - // there can't be any dupes here - returnedOutputs.Add(outputs.Single(o=>Equals(o.BlindedMessage.B_, output.B_))); - } - - var proofs = Utils.ConstructProofsFromPromises(res.Signatures.ToList(), returnedOutputs , keyset.Keys); - recoveredProofs.AddRange(proofs); - } - - // 1 is added so we'll be consistent with counter usage. it will be ready for next use - await counter.SetCounter(keysetId, lastUsedCounter + 1, ct); + var keysetProofs = await GrindKeyset(keysetId, mnemonic, counter, api, ct); + recoveredProofs.AddRange(keysetProofs); } - + // if nothing found - return empty collection if (recoveredProofs.Count == 0) { @@ -123,19 +64,19 @@ public async Task> ProcessAsync(CancellationToken ct = defaul } var freshProofs = new List(); - - // create hash table for every KeysetId : unit. + + // create hash table for every KeysetId : unit. var allKeysetsUnits = await _wallet.GetKeysetIdsWithUnits(ct); if (allKeysetsUnits == null) { throw new InvalidOperationException("No keysets available for restoration."); } var unitsForKeysets = allKeysetsUnits - .SelectMany(unit => - unit.Value.Select(keysetId => - new { KeysetId = keysetId, Unit = unit.Key })) - .ToDictionary(x => x.KeysetId, x => x.Unit); - + .SelectMany(unit => + unit.Value.Select(keysetId => new { KeysetId = keysetId, Unit = unit.Key }) + ) + .ToDictionary(x => x.KeysetId, x => x.Unit); + var activeUnits = await this._wallet.GetActiveKeysetIdsWithUnits(ct); if (activeUnits == null || !activeUnits.Any()) { @@ -146,13 +87,15 @@ public async Task> ProcessAsync(CancellationToken ct = defaul { var unit = unitKeyset.Key; var proofsForUnit = recoveredProofs - .Where(p => unitsForKeysets.TryGetValue(p.Id, out var proofUnit) && proofUnit == unit) + .Where(p => + unitsForKeysets.TryGetValue(p.Id, out var proofUnit) && proofUnit == unit + ) .ToList(); if (proofsForUnit.Count == 0) { continue; } - + // check proofs state: var unspentProofsForUnit = new List(); var state = await _wallet.CheckState(proofsForUnit, ct); @@ -164,7 +107,7 @@ public async Task> ProcessAsync(CancellationToken ct = defaul } unspentProofsForUnit.Add(proofsForUnit[i]); } - + // swap unspent tokens to single keyset var proofs = await _wallet .Swap() @@ -172,13 +115,18 @@ public async Task> ProcessAsync(CancellationToken ct = defaul .WithDLEQVerification() .FromInputs(unspentProofsForUnit) .ProcessAsync(ct); - + freshProofs.AddRange(proofs); } return freshProofs; } - private static async Task> _createBatch(Mnemonic mnemonic, KeysetId keysetId, int batchNumber, CancellationToken ct) + private static List CreateBatch( + Mnemonic mnemonic, + KeysetId keysetId, + int batchNumber, + CancellationToken ct + ) { if (batchNumber < 0) { @@ -187,4 +135,88 @@ private static async Task> _createBatch(Mnemonic mnemonic, Keys var amounts = Enumerable.Repeat((ulong)0, (int)BATCH_SIZE).ToList(); return mnemonic.DeriveOutputs(amounts, keysetId, (uint)(batchNumber * BATCH_SIZE)); } -} \ No newline at end of file + + private async Task> GrindKeyset( + KeysetId keysetId, + Mnemonic mnemonic, + ICounter counter, + ICashuApi api, + CancellationToken ct + ) + { + uint batchNumber = 0; + uint emptyBatchesRemaining = EMPTY_BATCHES_ALLOWED; + uint lastUsedCounter = 0; + List recoveredProofs = new List(); + + // don't care about invalid / non existent source keyset ids. let's fetch what we can + GetKeysResponse.KeysetItemResponse? keyset; + try + { + keyset = await _wallet.GetKeys(keysetId, true, false, ct); + } + catch + { + return []; + } + if (keyset == null) + { + return []; + } + + // proofs for keysetid are considered restored after 3 empty batches. + while (emptyBatchesRemaining > 0) + { + // create batch of 100, and request restore for whole batch + var outputs = CreateBatch(mnemonic, keysetId, (int)batchNumber, ct); + var req = new PostRestoreRequest + { + Outputs = outputs.Select(o => o.BlindedMessage).ToArray(), + }; + var res = await api.Restore(req, ct); + + if (res.Signatures.Length == 0) + { + emptyBatchesRemaining--; + batchNumber++; + continue; + } + + // find last restored index of batch + uint lastUsedIndexInBatch = (uint) + outputs + .Select((o, i) => new { o, i }) + .Where(x => res.Outputs.Any(r => Equals(r.B_, x.o.BlindedMessage.B_))) + .MaxBy(x => x.i)! + .i; + + // set last used counter value for this batch + lastUsedCounter = BATCH_SIZE * batchNumber + lastUsedIndexInBatch; + + // bump batch number after calculating last used counter + batchNumber++; + + // if anything found, reset batches counter + emptyBatchesRemaining = EMPTY_BATCHES_ALLOWED; + + var returnedOutputs = new List(); + foreach (var output in res.Outputs) + { + // there can't be any dupes here + returnedOutputs.Add(outputs.Single(o => Equals(o.BlindedMessage.B_, output.B_))); + } + + var proofs = Utils.ConstructProofsFromPromises( + res.Signatures.ToList(), + returnedOutputs, + keyset.Keys + ); + recoveredProofs.AddRange(proofs); + } + + // 1 is added so we'll be consistent with counter usage. it will be ready for next use + await counter.SetCounter(keysetId, lastUsedCounter + 1, ct); + return recoveredProofs; + } + // in future it may be also usefult to add restore by binary search +} From be64d3ad64133f116dd34de50cbfc1de40b77182 Mon Sep 17 00:00:00 2001 From: d4rp4t Date: Sun, 11 Jan 2026 18:07:02 +0100 Subject: [PATCH 48/70] CSharpier format --- DotNut.Demo/DotNut.Demo.csproj | 2 - DotNut.Demo/Program.cs | 279 +-- DotNut.Nostr/DotNut.Nostr.csproj | 44 +- ...ostrNip17PaymentRequestInterfaceHandler.cs | 31 +- DotNut.Tests/DotNut.Tests.csproj | 46 +- DotNut.Tests/GlobalUsings.cs | 2 +- DotNut.Tests/Integration.cs | 1588 ++++++++--------- DotNut.Tests/UnitTest1.cs | 965 +++++++--- DotNut.Tests/UnitTests2.cs | 332 ++-- .../Handlers/MeltHandlerBolt11.cs | 43 +- .../Handlers/MeltHandlerBolt12.cs | 31 +- .../Handlers/MintHandlerBolt11.cs | 31 +- .../Handlers/MintHandlerBolt12.cs | 36 +- DotNut/Abstractions/InMemoryCounter.cs | 15 +- DotNut/Abstractions/Interfaces/ICounter.cs | 8 +- .../Abstractions/Interfaces/IMeltHandler.cs | 4 +- .../Interfaces/IMeltQuoteBuilder.cs | 11 +- .../Abstractions/Interfaces/IMintHandler.cs | 7 +- .../Interfaces/IMintQuoteBuilder.cs | 22 +- .../Abstractions/Interfaces/IProofSelector.cs | 11 +- .../Interfaces/IRestoreBuilder.cs | 2 +- .../Abstractions/Interfaces/IWalletBuilder.cs | 118 +- .../Interfaces/IWebsocketService.cs | 9 +- DotNut/Abstractions/MeltQuoteBuilder.cs | 65 +- DotNut/Abstractions/MintInfo.cs | 98 +- DotNut/Abstractions/MintQuoteBuilder.cs | 117 +- DotNut/Abstractions/Nut10Helper.cs | 22 +- DotNut/Abstractions/OutputData.cs | 6 +- DotNut/Abstractions/ProofSelector.cs | 117 +- DotNut/Abstractions/SendResponse.cs | 2 +- DotNut/Abstractions/SwapBuilder.cs | 136 +- DotNut/Abstractions/Utils.cs | 137 +- DotNut/Abstractions/Wallet.cs | 256 +-- .../Websockets/NotificationParser.cs | 19 +- .../Abstractions/Websockets/Subscription.cs | 15 +- .../Websockets/WebsocketConnection.cs | 9 +- .../Abstractions/Websockets/WebsocketEnums.cs | 6 +- .../Websockets/WebsocketModels.cs | 9 +- .../Websockets/WebsocketService.cs | 275 +-- .../Websockets/WebsocketServiceExtensions.cs | 41 +- DotNut/Api/CashuHttpClient.cs | 138 +- DotNut/Api/CashuProtocolError.cs | 9 +- DotNut/Api/CashuProtocolException.cs | 5 +- DotNut/Api/ICashuApi.cs | 53 +- DotNut/ApiModels/GetKeysResponse.cs | 5 +- DotNut/ApiModels/GetKeysetsResponse.cs | 24 +- DotNut/ApiModels/Info/ContactInfo.cs | 9 +- DotNut/ApiModels/Info/GetInfoResponse.cs | 8 +- DotNut/ApiModels/Info/MPPInfo.cs | 7 +- DotNut/ApiModels/Info/SwapInfo.cs | 22 +- DotNut/ApiModels/Info/WebSocketSupport.cs | 6 +- .../ApiModels/Melt/MeltQuoteRequestOptions.cs | 2 +- DotNut/ApiModels/Melt/PostMeltRequest.cs | 7 +- .../Melt/bolt11/PostMeltQuoteBolt11Request.cs | 9 +- .../bolt11/PostMeltQuoteBolt11Response.cs | 26 +- .../Melt/bolt12/PostMeltQuoteBolt12Request.cs | 5 +- .../bolt12/PostMeltQuoteBolt12Response.cs | 38 +- DotNut/ApiModels/Mint/PostMintRequest.cs | 4 +- DotNut/ApiModels/Mint/PostMintResponse.cs | 2 +- .../Mint/bolt11/PostMintQuoteBolt11Request.cs | 23 +- .../bolt11/PostMintQuoteBolt11Response.cs | 26 +- .../Mint/bolt12/PostMintQuoteBolt12Request.cs | 11 +- .../bolt12/PostMintQuoteBolt12Response.cs | 28 +- DotNut/ApiModels/PostCheckStateRequest.cs | 2 +- DotNut/ApiModels/PostCheckStateResponse.cs | 3 +- DotNut/ApiModels/PostRestoreRequest.cs | 2 +- DotNut/ApiModels/PostRestoreResponse.cs | 3 +- DotNut/ApiModels/PostSwapRequest.cs | 9 +- DotNut/ApiModels/PostSwapResponse.cs | 5 +- DotNut/ApiModels/StateResponseItem.cs | 6 +- DotNut/DotNut.csproj | 45 +- DotNut/Encoding/Base64UrlSafe.cs | 12 +- DotNut/Encoding/CashuTokenHelper.cs | 85 +- DotNut/Encoding/CashuTokenV3Encoder.cs | 2 +- DotNut/Encoding/CashuTokenV4Encoder.cs | 89 +- DotNut/Encoding/ConvertUtils.cs | 2 +- DotNut/Encoding/ICashuTokenEncoder.cs | 3 +- DotNut/Encoding/PaymentRequestEncoder.cs | 57 +- .../JsonConverters/KeysetIdJsonConverter.cs | 22 +- DotNut/JsonConverters/KeysetJsonConverter.cs | 14 +- .../Nut10SecretJsonConverter.cs | 28 +- DotNut/JsonConverters/PrivKeyJsonConverter.cs | 16 +- DotNut/JsonConverters/PubKeyJsonConverter.cs | 16 +- DotNut/JsonConverters/SecretJsonConverter.cs | 11 +- .../UnixDateTimeOffsetConverter.cs | 20 +- .../NBitcoin/BIP39/HardcodedWordlistSource.cs | 60 +- DotNut/NBitcoin/BIP39/IWordlistSource.cs | 2 +- DotNut/NBitcoin/BIP39/KDTable.cs | 129 +- DotNut/NBitcoin/BIP39/Language.cs | 24 +- DotNut/NBitcoin/BIP39/Mnemonic.cs | 405 +++-- DotNut/NBitcoin/BIP39/WordCount.cs | 4 +- DotNut/NBitcoin/BIP39/Wordlist.cs | 882 +++++---- DotNut/NBitcoin/BitWriter.cs | 159 +- DotNut/NUT00/BlindSignature.cs | 11 +- DotNut/NUT00/BlindedMessage.cs | 18 +- DotNut/NUT00/Cashu.cs | 101 +- DotNut/NUT00/CashuToken.cs | 16 +- DotNut/NUT00/ISecret.cs | 2 +- DotNut/NUT00/Proof.cs | 17 +- DotNut/NUT00/StringSecret.cs | 3 +- DotNut/NUT01/Keyset.cs | 77 +- DotNut/NUT02/FeeHelper.cs | 9 +- DotNut/NUT02/KeysetId.cs | 25 +- DotNut/NUT04/MintMethodSetting.cs | 20 +- DotNut/NUT05/MeltMethodSetting.cs | 17 +- DotNut/NUT10/Nut10ProofSecret.cs | 22 +- DotNut/NUT10/Nut10Secret.cs | 3 +- DotNut/NUT11/P2PKProofSecret.cs | 162 +- DotNut/NUT11/P2PKWitness.cs | 5 +- DotNut/NUT11/P2PkBuilder.cs | 76 +- DotNut/NUT11/SigAllHandler.cs | 128 +- DotNut/NUT12/DLEQ.cs | 9 +- DotNut/NUT12/DLEQProof.cs | 7 +- DotNut/NUT13/BIP32.cs | 18 +- DotNut/NUT13/Nut13.cs | 85 +- DotNut/NUT14/HTLCBuilder.cs | 37 +- DotNut/NUT14/HTLCProofSecret.cs | 186 +- DotNut/NUT14/HTLCWitness.cs | 8 +- DotNut/NUT15/MultipathPaymentSetting.cs | 15 +- .../HttpPaymentRequestInterfaceHandler.cs | 14 +- DotNut/NUT18/Nut10LockingCondition.cs | 2 +- DotNut/NUT18/PaymentRequest.cs | 2 +- .../NUT18/PaymentRequestInterfaceHandler.cs | 8 +- DotNut/NUT18/PaymentRequestPayload.cs | 21 +- DotNut/NUT18/PaymentRequestTransport.cs | 2 +- .../NUT18/PaymentRequestTransportInitiator.cs | 6 +- DotNut/NUT18/PaymentRequestTransportTag.cs | 2 +- DotNut/NUT20/MintQuoteSigner.cs | 12 +- DotNut/PrivKey.cs | 7 +- DotNut/PubKey.cs | 18 +- 130 files changed, 5006 insertions(+), 3716 deletions(-) diff --git a/DotNut.Demo/DotNut.Demo.csproj b/DotNut.Demo/DotNut.Demo.csproj index bb84a1f..232ca1c 100644 --- a/DotNut.Demo/DotNut.Demo.csproj +++ b/DotNut.Demo/DotNut.Demo.csproj @@ -1,5 +1,4 @@  - Exe net8.0 @@ -10,5 +9,4 @@ - diff --git a/DotNut.Demo/Program.cs b/DotNut.Demo/Program.cs index 9dd99ee..06ed48b 100644 --- a/DotNut.Demo/Program.cs +++ b/DotNut.Demo/Program.cs @@ -1,9 +1,9 @@ +using System.Security.Cryptography; using DotNut.Api; using DotNut.ApiModels; -using NBitcoin.Secp256k1; -using System.Security.Cryptography; using DotNut.NBitcoin.BIP39; using DotNut.NUT13; +using NBitcoin.Secp256k1; namespace DotNut.Demo; @@ -12,20 +12,20 @@ class Program private static readonly string DefaultMintUrl = "https://testnut.cashu.space"; private static CashuHttpClient? _client; private static List _wallet = new(); - + static async Task Main(string[] args) { Console.WriteLine("🥜 DotNut - Cashu Library Demo"); Console.WriteLine("=============================="); Console.WriteLine(); - + await InitializeMint(); - + while (true) { ShowMenu(); var choice = Console.ReadLine(); - + try { switch (choice?.ToLower()) @@ -81,13 +81,13 @@ static async Task Main(string[] args) Console.WriteLine($" Detail: {cashuEx.Error.Detail}"); } } - + Console.WriteLine("\nPress any key to continue..."); Console.Read(); Console.Clear(); } } - + private static void ShowMenu() { Console.WriteLine("📋 Available Demos:"); @@ -106,7 +106,7 @@ private static void ShowMenu() Console.WriteLine(); Console.Write("Choose an option: "); } - + private static async Task InitializeMint() { try @@ -114,7 +114,7 @@ private static async Task InitializeMint() var httpClient = new HttpClient(); httpClient.BaseAddress = new Uri(DefaultMintUrl); _client = new CashuHttpClient(httpClient); - + Console.WriteLine($"🔗 Initialized connection to: {DefaultMintUrl}"); } catch (Exception ex) @@ -122,18 +122,18 @@ private static async Task InitializeMint() Console.WriteLine($"❌ Failed to initialize mint connection: {ex.Message}"); } } - + private static async Task ConnectToMintDemo() { Console.WriteLine("🔗 Connect to Mint & Get Info Demo"); Console.WriteLine("=================================="); - + if (_client == null) { Console.WriteLine("❌ Client not initialized"); return; } - + try { // Get mint information @@ -141,24 +141,26 @@ private static async Task ConnectToMintDemo() Console.WriteLine($"✅ Connected to mint: {info.Name}"); Console.WriteLine($" Description: {info.Description}"); Console.WriteLine($" Version: {info.Version}"); - Console.WriteLine($" Contact: {string.Join(", ", info.Contact?.Select(c => $"{c.Method}: {c.Info}") ?? new[] { "N/A" })}"); - + Console.WriteLine( + $" Contact: {string.Join(", ", info.Contact?.Select(c => $"{c.Method}: {c.Info}") ?? new[] { "N/A" })}" + ); + // Get available keysets var keysets = await _client.GetKeysets(); Console.WriteLine($" Available keysets: {keysets.Keysets.Length}"); - + foreach (var keyset in keysets.Keysets.Take(3)) { Console.WriteLine($" - {keyset.Id} ({keyset.Unit}) [{keyset.Active}]"); } - + // Get keys for the first active keyset var activeKeyset = keysets.Keysets.FirstOrDefault(k => k.Active); if (activeKeyset != null) { var keys = await _client.GetKeys(activeKeyset.Id); Console.WriteLine($" Keys in active keyset ({activeKeyset.Id}):"); - + foreach (var key in keys.Keysets.First().Keys.Take(5)) { Console.WriteLine($" - Amount {key.Key}: {key.Value}"); @@ -170,16 +172,16 @@ private static async Task ConnectToMintDemo() Console.WriteLine($"❌ Failed to connect to mint: {ex.Message}"); } } - + private static async Task TokenCreationDemo() { Console.WriteLine("🪙 Token Creation Demo"); Console.WriteLine("======================"); - + // Create some example proofs for demonstration var proofs = CreateExampleProofs(); _wallet.AddRange(proofs); - + // Create a token var token = new CashuToken { @@ -187,19 +189,15 @@ private static async Task TokenCreationDemo() Memo = "Demo payment - Coffee ☕", Tokens = new List { - new CashuToken.Token - { - Mint = DefaultMintUrl, - Proofs = proofs - } - } + new CashuToken.Token { Mint = DefaultMintUrl, Proofs = proofs }, + }, }; - + Console.WriteLine($"✅ Created token with {proofs.Count} proofs"); Console.WriteLine($" Total amount: {token.TotalAmount()} sats"); Console.WriteLine($" Memo: {token.Memo}"); Console.WriteLine($" Mint: {token.Tokens.First().Mint}"); - + // Show proof details Console.WriteLine(" Proofs:"); foreach (var proof in proofs) @@ -207,12 +205,12 @@ private static async Task TokenCreationDemo() Console.WriteLine($" - {proof.Amount} sats (ID: {proof.Id})"); } } - + private static async Task TokenEncodingDemo() { Console.WriteLine("🔄 Token Encoding/Decoding Demo"); Console.WriteLine("==============================="); - + var proofs = CreateExampleProofs(); var token = new CashuToken { @@ -220,31 +218,27 @@ private static async Task TokenEncodingDemo() Memo = "Encoding demo token", Tokens = new List { - new CashuToken.Token - { - Mint = DefaultMintUrl, - Proofs = proofs - } - } + new CashuToken.Token { Mint = DefaultMintUrl, Proofs = proofs }, + }, }; - + // V3 Encoding (JSON-based) Console.WriteLine("📝 V3 Encoding (JSON-based):"); var v3Token = token.Encode("A"); Console.WriteLine($" Length: {v3Token.Length} characters"); Console.WriteLine($" Token: {v3Token.Substring(0, Math.Min(80, v3Token.Length))}..."); - + // V4 Encoding (CBOR-based, more compact) Console.WriteLine("\n📦 V4 Encoding (CBOR-based, compact):"); var v4Token = token.Encode("B"); Console.WriteLine($" Length: {v4Token.Length} characters"); Console.WriteLine($" Token: {v4Token.Substring(0, Math.Min(80, v4Token.Length))}..."); - + // URI format Console.WriteLine("\n🔗 URI Format:"); var uriToken = token.Encode("B", makeUri: true); Console.WriteLine($" URI: {uriToken.Substring(0, Math.Min(80, uriToken.Length))}..."); - + // Decode and verify Console.WriteLine("\n🔍 Decoding V4 token:"); var decoded = CashuTokenHelper.Decode(v4Token, out string version); @@ -252,47 +246,57 @@ private static async Task TokenEncodingDemo() Console.WriteLine($" Amount: {decoded.TotalAmount()} sats"); Console.WriteLine($" Memo: {decoded.Memo}"); Console.WriteLine($" Proofs: {decoded.Tokens.First().Proofs.Count}"); - - Console.WriteLine($"\n💾 Space savings: V4 is {((double)(v3Token.Length - v4Token.Length) / v3Token.Length * 100):F1}% smaller than V3"); + + Console.WriteLine( + $"\n💾 Space savings: V4 is {((double)(v3Token.Length - v4Token.Length) / v3Token.Length * 100):F1}% smaller than V3" + ); } - + private static async Task LightningMintDemo() { Console.WriteLine("⚡ Lightning Mint Quote Demo"); Console.WriteLine("============================"); - Console.WriteLine("ℹ️ This demo shows how to create mint quotes - actual minting requires paying a real Lightning invoice"); - + Console.WriteLine( + "ℹ️ This demo shows how to create mint quotes - actual minting requires paying a real Lightning invoice" + ); + if (_client == null) { Console.WriteLine("❌ Client not initialized"); return; } - + try { // Create mint quote var mintRequest = new PostMintQuoteBolt11Request { Amount = 1000, // 1000 sats - Unit = "sat" + Unit = "sat", }; - - var mintQuote = await _client.CreateMintQuote( - "bolt11", mintRequest); - + + var mintQuote = await _client.CreateMintQuote< + PostMintQuoteBolt11Response, + PostMintQuoteBolt11Request + >("bolt11", mintRequest); + Console.WriteLine("✅ Mint quote created successfully!"); Console.WriteLine($" Quote ID: {mintQuote.Quote}"); Console.WriteLine($" Amount: {mintQuote.Amount} {mintRequest.Unit}"); Console.WriteLine($" Unit: {mintQuote.Unit ?? mintRequest.Unit}"); - Console.WriteLine($" Expiry: {DateTimeOffset.FromUnixTimeSeconds(mintQuote.Expiry ?? 0).UtcDateTime}"); + Console.WriteLine( + $" Expiry: {DateTimeOffset.FromUnixTimeSeconds(mintQuote.Expiry ?? 0).UtcDateTime}" + ); Console.WriteLine($" State: {mintQuote.State}"); Console.WriteLine("\n📄 Lightning Invoice:"); Console.WriteLine($" {mintQuote.Request}"); - + Console.WriteLine("\n💡 To complete minting:"); Console.WriteLine(" 1. Pay the Lightning invoice above"); Console.WriteLine(" 2. Create blinded messages for desired denominations"); - Console.WriteLine(" 3. Call the mint endpoint with the quote ID and blinded messages"); + Console.WriteLine( + " 3. Call the mint endpoint with the quote ID and blinded messages" + ); Console.WriteLine(" 4. Unblind the returned signatures to get your proofs"); } catch (Exception ex) @@ -300,44 +304,51 @@ private static async Task LightningMintDemo() Console.WriteLine($"❌ Failed to create mint quote: {ex.Message}"); } } - + private static async Task LightningMeltDemo() { Console.WriteLine("⚡ Lightning Melt Quote Demo"); Console.WriteLine("============================"); - Console.WriteLine("ℹ️ This demo shows how to create melt quotes - actual melting requires valid proofs"); - + Console.WriteLine( + "ℹ️ This demo shows how to create melt quotes - actual melting requires valid proofs" + ); + if (_client == null) { Console.WriteLine("❌ Client not initialized"); return; } - + // Example Lightning invoice (fake for demo purposes) - var exampleInvoice = "lnbc10n1pj9x8x8pp5k6k6k6k6k6k6k6k6k6k6k6k6k6k6k6k6k6k6k6k6k6k6k6k6k6qdqqcqzpgxqyz5vqsp5example"; - + var exampleInvoice = + "lnbc10n1pj9x8x8pp5k6k6k6k6k6k6k6k6k6k6k6k6k6k6k6k6k6k6k6k6k6k6k6k6k6qdqqcqzpgxqyz5vqsp5example"; + try { var meltRequest = new PostMeltQuoteBolt11Request { Request = exampleInvoice, - Unit = "sat" + Unit = "sat", }; - + // Note: This will likely fail with the example invoice, but shows the API usage Console.WriteLine("📤 Attempting to create melt quote..."); Console.WriteLine($" Invoice: {exampleInvoice.Substring(0, 50)}..."); - - var meltQuote = await _client.CreateMeltQuote( - "bolt11", meltRequest); - + + var meltQuote = await _client.CreateMeltQuote< + PostMeltQuoteBolt11Response, + PostMeltQuoteBolt11Request + >("bolt11", meltRequest); + Console.WriteLine("✅ Melt quote created successfully!"); Console.WriteLine($" Quote ID: {meltQuote.Quote}"); Console.WriteLine($" Amount: {meltQuote.Amount} {meltRequest.Unit}"); Console.WriteLine($" Fee reserve: {meltQuote.FeeReserve} {meltRequest.Unit}"); - Console.WriteLine($" Expiry: {DateTimeOffset.FromUnixTimeSeconds(meltQuote.Expiry ?? 0).UtcDateTime}"); + Console.WriteLine( + $" Expiry: {DateTimeOffset.FromUnixTimeSeconds(meltQuote.Expiry ?? 0).UtcDateTime}" + ); Console.WriteLine($" State: {meltQuote.State}"); - + Console.WriteLine("\n💡 To complete melting:"); Console.WriteLine(" 1. Provide proofs with sufficient value (amount + fee)"); Console.WriteLine(" 2. Call the melt endpoint with quote ID and proofs"); @@ -352,58 +363,64 @@ private static async Task LightningMeltDemo() Console.WriteLine(" 3. Ensure you have sufficient proofs in your wallet"); } } - + private static async Task TokenSwapDemo() { Console.WriteLine("🔄 Token Swapping Demo"); Console.WriteLine("======================"); - Console.WriteLine("ℹ️ Swapping allows you to change token denominations or refresh secrets"); - + Console.WriteLine( + "ℹ️ Swapping allows you to change token denominations or refresh secrets" + ); + if (_wallet.Count == 0) { Console.WriteLine("⚠️ No proofs in wallet. Creating example proofs for demo..."); _wallet.AddRange(CreateExampleProofs()); } - + var inputProofs = _wallet.Take(2).ToList(); - Console.WriteLine($"📥 Input proofs: {inputProofs.Count} proofs totaling {inputProofs.Sum(p => (long)p.Amount)} sats"); - + Console.WriteLine( + $"📥 Input proofs: {inputProofs.Count} proofs totaling {inputProofs.Sum(p => (long)p.Amount)} sats" + ); + foreach (var proof in inputProofs) { Console.WriteLine($" - {proof.Amount} sats (Secret: {proof.Secret})"); } - + // In a real implementation, you would: // 1. Create blinded messages for new denominations // 2. Send swap request to mint // 3. Unblind the returned signatures - + Console.WriteLine("\n💡 Swap process would involve:"); Console.WriteLine(" 1. Creating blinded messages for desired output amounts"); - Console.WriteLine(" 2. Sending PostSwapRequest with input proofs and output blinded messages"); + Console.WriteLine( + " 2. Sending PostSwapRequest with input proofs and output blinded messages" + ); Console.WriteLine(" 3. Receiving BlindSignatures from the mint"); Console.WriteLine(" 4. Unblinding signatures to get new proofs with fresh secrets"); Console.WriteLine(" 5. The old proofs become invalid, new proofs are added to wallet"); } - + private static async Task SecretsDemo() { Console.WriteLine("🔐 Working with Secrets Demo"); Console.WriteLine("============================"); - + // Simple string secret Console.WriteLine("1️⃣ Simple String Secret:"); var stringSecret = new StringSecret("my-random-secret-12345"); Console.WriteLine($" Secret: {stringSecret}"); Console.WriteLine($" Curve point: {stringSecret.ToCurve().ToHex()}"); - + // Random secret generation Console.WriteLine("\n2️⃣ Random Secret Generation:"); var randomBytes = new byte[32]; RandomNumberGenerator.Fill(randomBytes); var randomSecret = new StringSecret(Convert.ToHexString(randomBytes).ToLower()); Console.WriteLine($" Random secret: {randomSecret}"); - + // Demonstrate secret uniqueness Console.WriteLine("\n3️⃣ Secret Uniqueness:"); var secret1 = new StringSecret("test-secret-1"); @@ -411,84 +428,86 @@ private static async Task SecretsDemo() Console.WriteLine($" Secret 1 → Curve: {secret1.ToCurve().ToHex()}"); Console.WriteLine($" Secret 2 → Curve: {secret2.ToCurve().ToHex()}"); Console.WriteLine($" Different secrets produce different curve points ✅"); - + Console.WriteLine("\n💡 Key points about secrets:"); Console.WriteLine(" - Secrets are hashed to elliptic curve points"); Console.WriteLine(" - Each secret maps to a unique point on the curve"); Console.WriteLine(" - Changing even one character creates a completely different point"); Console.WriteLine(" - Secrets should be random and unpredictable"); } - + private static async Task MnemonicDemo() { Console.WriteLine("🎲 Mnemonic Secrets Demo (NUT-13)"); Console.WriteLine("=================================="); - + // Create a mnemonic var mnemonic = new Mnemonic(Wordlist.English, WordCount.Twelve); Console.WriteLine($"📝 Generated mnemonic:"); Console.WriteLine($" {mnemonic}"); - + // Example keyset ID (normally you'd get this from the mint) var keysetId = new KeysetId("009a1f293253e41e"); - + Console.WriteLine($"\n🔑 Deriving secrets from mnemonic:"); Console.WriteLine($" Keyset ID: {keysetId}"); - + // Derive multiple secrets for (uint i = 0; i < 5; i++) { var secret = mnemonic.DeriveSecret(keysetId, counter: i); var blindingFactor = mnemonic.DeriveBlindingFactor(keysetId, counter: i); - + Console.WriteLine($" Counter {i}:"); Console.WriteLine($" Secret: {secret}"); Console.WriteLine($" Blinding: {Convert.ToHexString(blindingFactor).ToLower()}"); } - + Console.WriteLine("\n💡 Benefits of deterministic secrets:"); Console.WriteLine(" - Reproducible from mnemonic phrase"); Console.WriteLine(" - No need to store individual secrets"); Console.WriteLine(" - Can recover proofs if you lose wallet data"); Console.WriteLine(" - Counter ensures each secret is unique"); - + Console.WriteLine("\n⚠️ Security considerations:"); Console.WriteLine(" - Keep your mnemonic phrase secure"); Console.WriteLine(" - Anyone with the mnemonic can recreate your secrets"); Console.WriteLine(" - Use proper entropy when generating mnemonics"); } - + private static async Task P2PKDemo() { Console.WriteLine("🔒 Pay-to-Public-Key Demo (NUT-11)"); Console.WriteLine("==================================="); - + // Create some public keys for the demo var privKey1 = ECPrivKey.Create(RandomNumberGenerator.GetBytes(32)); var privKey2 = ECPrivKey.Create(RandomNumberGenerator.GetBytes(32)); var pubKey1 = privKey1.CreatePubKey(); var pubKey2 = privKey2.CreatePubKey(); - + Console.WriteLine("🔑 Generated demo keys:"); Console.WriteLine($" PubKey 1: {pubKey1}"); Console.WriteLine($" PubKey 2: {pubKey2}"); - + // Create a 1-of-2 multisig P2PK secret Console.WriteLine("\n🏗️ Creating 1-of-2 multisig P2PK:"); var p2pkBuilder = new P2PKBuilder { Pubkeys = new[] { pubKey1, pubKey2 }, SignatureThreshold = 1, // 1-of-2 multisig - SigFlag = "SIG_INPUTS" + SigFlag = "SIG_INPUTS", }; - + var p2pkSecret = p2pkBuilder.Build(); var nut10Secret = new Nut10Secret(P2PKProofSecret.Key, p2pkSecret); - - Console.WriteLine($" Signature threshold: {p2pkBuilder.SignatureThreshold}-of-{p2pkBuilder.Pubkeys.Length}"); + + Console.WriteLine( + $" Signature threshold: {p2pkBuilder.SignatureThreshold}-of-{p2pkBuilder.Pubkeys.Length}" + ); Console.WriteLine($" Signature flag: {p2pkBuilder.SigFlag}"); Console.WriteLine($" P2PK secret created ✅"); - + // Create a time-locked P2PK Console.WriteLine("\n⏰ Creating time-locked P2PK:"); var timeLockedBuilder = new P2PKBuilder @@ -497,59 +516,59 @@ private static async Task P2PKDemo() SignatureThreshold = 1, SigFlag = "SIG_INPUTS", Lock = DateTimeOffset.UtcNow.AddHours(1), // Lock for 1 hour - RefundPubkeys = new[] { pubKey2 } // Refund key after timeout + RefundPubkeys = new[] { pubKey2 }, // Refund key after timeout }; - + var timeLockedSecret = timeLockedBuilder.Build(); var timeLockedNut10 = new Nut10Secret(P2PKProofSecret.Key, timeLockedSecret); - + Console.WriteLine($" Lock time: {timeLockedBuilder.Lock}"); Console.WriteLine($" Refund key: {pubKey2}"); Console.WriteLine($" Time-locked P2PK secret created ✅"); - + Console.WriteLine("\n💡 P2PK use cases:"); Console.WriteLine(" - Multisignature wallets"); Console.WriteLine(" - Escrow services"); Console.WriteLine(" - Time-locked payments"); Console.WriteLine(" - Conditional spending"); - + Console.WriteLine("\n🔓 To spend P2PK proofs:"); Console.WriteLine(" - Create signatures with required private keys"); Console.WriteLine(" - Include witness data in the proof"); Console.WriteLine(" - Mint validates signatures against public keys"); } - + private static async Task CheckProofStatesDemo() { Console.WriteLine("🔍 Check Proof States Demo"); Console.WriteLine("=========================="); - + if (_client == null) { Console.WriteLine("❌ Client not initialized"); return; } - + if (_wallet.Count == 0) { Console.WriteLine("⚠️ No proofs in wallet. Creating example proofs..."); _wallet.AddRange(CreateExampleProofs()); } - + try { var proofsToCheck = _wallet.Take(3).ToList(); Console.WriteLine($"📋 Checking state of {proofsToCheck.Count} proofs..."); - + // Create check state request var stateRequest = new PostCheckStateRequest { - Ys = proofsToCheck.Select(p => p.C.ToString()).ToArray() + Ys = proofsToCheck.Select(p => p.C.ToString()).ToArray(), }; - + // Note: This will likely fail with fake proofs, but shows the API usage var stateResponse = await _client.CheckState(stateRequest); - + Console.WriteLine("✅ State check successful:"); for (int i = 0; i < stateResponse.States.Length; i++) { @@ -572,58 +591,62 @@ private static async Task CheckProofStatesDemo() Console.WriteLine(" - Check states before attempting to spend proofs"); } } - + private static void ShowWallet() { Console.WriteLine("💰 Current Wallet"); Console.WriteLine("================="); - + if (_wallet.Count == 0) { Console.WriteLine(" Empty wallet - no proofs stored"); return; } - + var totalAmount = _wallet.Sum(p => (long)p.Amount); Console.WriteLine($" Total balance: {totalAmount} sats"); Console.WriteLine($" Number of proofs: {_wallet.Count}"); Console.WriteLine(); - + Console.WriteLine(" Proof details:"); foreach (var proof in _wallet.Take(10)) // Show first 10 proofs { - Console.WriteLine($" - {proof.Amount,4} sats | ID: {proof.Id} | Secret: {proof.Secret.ToString().Substring(0, Math.Min(20, proof.Secret.ToString().Length))}..."); + Console.WriteLine( + $" - {proof.Amount, 4} sats | ID: {proof.Id} | Secret: {proof.Secret.ToString().Substring(0, Math.Min(20, proof.Secret.ToString().Length))}..." + ); } - + if (_wallet.Count > 10) { Console.WriteLine($" ... and {_wallet.Count - 10} more proofs"); } - + // Show denomination breakdown var denominations = _wallet.GroupBy(p => p.Amount).OrderBy(g => g.Key); Console.WriteLine("\n Denomination breakdown:"); foreach (var denom in denominations) { - Console.WriteLine($" {denom.Key,4} sats: {denom.Count()} proofs = {denom.Key * (ulong)denom.Count()} sats"); + Console.WriteLine( + $" {denom.Key, 4} sats: {denom.Count()} proofs = {denom.Key * (ulong)denom.Count()} sats" + ); } } - + private static List CreateExampleProofs() { // Create example proofs for demonstration // In a real application, these would come from minting operations var keysetId = new KeysetId("009a1f293253e41e"); - + var proofs = new List(); var amounts = new ulong[] { 1, 2, 4, 8, 16, 32, 64, 128, 256, 512 }; - + foreach (var amount in amounts.Take(5)) // Create 5 demo proofs { var secret = new StringSecret($"demo-secret-{amount}-{Guid.NewGuid()}"); var privKey = ECPrivKey.Create(RandomNumberGenerator.GetBytes(32)); var pubKey = privKey.CreatePubKey(); - + var proof = new Proof { Amount = amount, @@ -632,10 +655,10 @@ private static List CreateExampleProofs() C = pubKey, // Note: In real usage, these would be proper cryptographic proofs from the mint }; - + proofs.Add(proof); } - + return proofs; } } @@ -647,4 +670,4 @@ public static ulong TotalAmount(this CashuToken token) { return (ulong)token.Tokens.SelectMany(t => t.Proofs).Sum(p => (long)p.Amount); } -} \ No newline at end of file +} diff --git a/DotNut.Nostr/DotNut.Nostr.csproj b/DotNut.Nostr/DotNut.Nostr.csproj index 4bed5fa..05d32c2 100644 --- a/DotNut.Nostr/DotNut.Nostr.csproj +++ b/DotNut.Nostr/DotNut.Nostr.csproj @@ -1,27 +1,23 @@  + + net8.0 + enable + enable + true + DotNut.Nostr + Kukks + Support Cashu payment requests through Nostr + MIT + https://github.com/Kukks/DotNut + 1.0.0 + https://github.com/Kukks/DotNut + git + bitcoin cashu ecash secp256k1 nostr + https://github.com/Kukks/DotNut/blob/master/LICENSE + - - net8.0 - enable - enable - true - DotNut.Nostr - Kukks - Support Cashu payment requests through Nostr - MIT - https://github.com/Kukks/DotNut - 1.0.0 - https://github.com/Kukks/DotNut - git - bitcoin cashu ecash secp256k1 nostr - https://github.com/Kukks/DotNut/blob/master/LICENSE - - - - - - - - - + + + + diff --git a/DotNut.Nostr/NostrNip17PaymentRequestInterfaceHandler.cs b/DotNut.Nostr/NostrNip17PaymentRequestInterfaceHandler.cs index 461d5c5..7c43f12 100644 --- a/DotNut.Nostr/NostrNip17PaymentRequestInterfaceHandler.cs +++ b/DotNut.Nostr/NostrNip17PaymentRequestInterfaceHandler.cs @@ -10,7 +10,9 @@ public class NostrNip17PaymentRequestInterfaceHandler : PaymentRequestInterfaceH { public static void Register() { - PaymentRequestTransportInitiator.Handlers.Add(new NostrNip17PaymentRequestInterfaceHandler()); + PaymentRequestTransportInitiator.Handlers.Add( + new NostrNip17PaymentRequestInterfaceHandler() + ); } public bool CanHandle(PaymentRequest request) @@ -18,13 +20,15 @@ public bool CanHandle(PaymentRequest request) return request.Transports.Any(t => t.Type == "nostr" && t.Tags?.Any(tag => tag.Key == "n" && tag.Value.Any(v => v == "17")) == true); - } - public async Task SendPayment(PaymentRequest request, PaymentRequestPayload payload, - CancellationToken cancellationToken = default) - { - var nostrTransport = request.Transports.FirstOrDefault(t => + public async Task SendPayment( + PaymentRequest request, + PaymentRequestPayload payload, + CancellationToken cancellationToken = default + ) + { + var nostrTransport = request.Transports.FirstOrDefault(t => t.Type == "nostr" && t.Tags?.Any(tag => tag.Key == "n" && tag.Value.Any(v => v == "17")) == true); if (nostrTransport is null) @@ -32,8 +36,7 @@ public async Task SendPayment(PaymentRequest request, PaymentRequestPayload payl throw new InvalidOperationException("No NIP17 nostr transport found."); } var nprofileStr = nostrTransport.Target; - - var nprofile = (NIP19.NosteProfileNote) NIP19.FromNIP19Note(nprofileStr); + var nprofile = (NIP19.NosteProfileNote)NIP19.FromNIP19Note(nprofileStr); using var client = new CompositeNostrClient(nprofile.Relays.Select(r => new Uri(r)).ToArray()); await client.Connect(cancellationToken); var ephemeralKey = ECPrivKey.Create(RandomNumberGenerator.GetBytes(32)); @@ -46,9 +49,13 @@ public async Task SendPayment(PaymentRequest request, PaymentRequestPayload payl Tags = new(), }; msg.Id = msg.ComputeId(); - - var giftWrap = await NIP17.Create(msg, ephemeralKey,ECXOnlyPubKey.Create(Convert.FromHexString(nprofile.PubKey)), null); - await client.SendEventsAndWaitUntilReceived(new []{giftWrap}, cancellationToken); + var giftWrap = await NIP17.Create( + msg, + ephemeralKey, + ECXOnlyPubKey.Create(Convert.FromHexString(nprofile.PubKey)), + null + ); + await client.SendEventsAndWaitUntilReceived(new[] { giftWrap }, cancellationToken); } -} \ No newline at end of file +} diff --git a/DotNut.Tests/DotNut.Tests.csproj b/DotNut.Tests/DotNut.Tests.csproj index 122cff3..fcb505b 100644 --- a/DotNut.Tests/DotNut.Tests.csproj +++ b/DotNut.Tests/DotNut.Tests.csproj @@ -1,29 +1,27 @@ + + net8.0 + enable + enable - - net8.0 - enable - enable + false + true + - false - true - - - - - - - runtime; build; native; contentfiles; analyzers; buildtransitive - all - - - runtime; build; native; contentfiles; analyzers; buildtransitive - all - - - - - - + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + diff --git a/DotNut.Tests/GlobalUsings.cs b/DotNut.Tests/GlobalUsings.cs index 8c927eb..c802f44 100644 --- a/DotNut.Tests/GlobalUsings.cs +++ b/DotNut.Tests/GlobalUsings.cs @@ -1 +1 @@ -global using Xunit; \ No newline at end of file +global using Xunit; diff --git a/DotNut.Tests/Integration.cs b/DotNut.Tests/Integration.cs index 2c138eb..0268304 100644 --- a/DotNut.Tests/Integration.cs +++ b/DotNut.Tests/Integration.cs @@ -8,24 +8,52 @@ namespace DotNut.Tests; public class Integration { private static string MintUrl = "http://localhost:3338"; + // private static string MintUrl = "https://fake.thesimplekid.dev"; // private static string MintUrl = "https://testnut.cashu.space"; private static string seed = "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about"; - + // for now cdk mint returns 500 if there's created melt quote for the same invoice twice private static readonly Dictionary valuesInvoices = new Dictionary() { - {500, "lnbc5u1p5sh0yvsp53seej3qkkxe6xxk9mufaj7y3jc9s9kvfn4g3whppwqcl4vcjraaspp5vtv793xc9ksch8zekkhqtv54a2evh7vq4zuywcmk9nzt69qma5yqhp5uwcvgs5clswpfxhm7nyfjmaeysn6us0yvjdexn9yjkv3k7zjhp2sxq9z0rgqcqpnrzjq0ly0l075re9ltgqzdycartvas6g4c7kwwzpasj7a98c0ss679hdsr080vqqdcgqqqqqqqqnqqqqryqqxv9qxpqysgqwq50283v8asna95fktaeg80kq9evs0chaw44y6y649qsql9vsfc5gfcsp8rdwwyccepwy83n7g0s25n3lpv3hjgcr220n5w806fja8gp2xjvd7"}, - {501, "lnbc5010n1p5shs9rsp5a2qhmn05xsd8vcm5jx9v2aswkz0pxguk4jqlaxsazzcg5rduan2qpp5al2k5zwruvlx34sxxdys2sj696m58uqgjvzxxrxhvuyswhmzg5cqhp5uwcvgs5clswpfxhm7nyfjmaeysn6us0yvjdexn9yjkv3k7zjhp2sxq9z0rgqcqpnrzjqw7c9dkkx4nur9sztw2zzpzj8u8rgsqgdsykylg5pwplh26824lc7rvlqcqqn3gqqyqqqqlgqqqqqqgq2q9qxpqysgqgpj2x2aw2dv5tzhx86th6a5vutpcdxz9htewqgvzjgqkzwmh6xs5mw5xcgrzyq77f35shv0gg5ygtjmn7e73wg8v0a9g836ufszdxmqqqu3642"}, - {502, "lnbc5020n1p5j3nxasp5qz7utfrp954nxp8049tqzg0t23krdj59thfcrc2g5h6lsemzvyfqpp5ms6xd7grtak0nr8lwytsclmq3d233v7gy7j0kuw32txhjq0f8ngqhp5uwcvgs5clswpfxhm7nyfjmaeysn6us0yvjdexn9yjkv3k7zjhp2sxq9z0rgqcqpnrzjqg587a2yyuqeua9c3j8nw7wwpx709slwl5lzfs0t0vq3kdwemzp67rtwevqq95gqqyqqq5sgqqq9yzqq2q9qxpqysgqgpp0zsetj9fedvr0szpwjfw2weckygmjthhnfpp2cerjtrn8n0pxyvrtc00l0jwzkqhwedcvgqljtwx3a7qplqp43jlxe4mpmw5svlgqfwa9yy"}, - {503, "lnbc5030n1p5n9kk6sp5ee6rsflv9rnnyt80ucc0fzlwa975nmufs2dn0x3u0hlerxxtc4nqpp5ew5mxfmu966c8wywvnvtgsljduq0jduvdpc3jzqzq9uqafx7ejgshp5uwcvgs5clswpfxhm7nyfjmaeysn6us0yvjdexn9yjkv3k7zjhp2sxq9z0rgqcqpnrzjqdqne4nrkxmz96ktnngat4nzx7sv0kf5uqmgfvqvvars7pac7fn9wr8fjvqq3csqqqqqqqqqqqqq0scqvs9qxpqysgq5lqwgfk6vv36tnlx2tv6reu2587x8ha2wsht0s75dpzvmknpgepsqaq9wnlx7n87j3x6w0vvkvc4qgda6mhacygn9f0xgagwt84uxtcqgtcpfs"}, - {999, "lnbc9990n1p5j3cf7sp575w4pw93kfrghl2gh68885v76gwjpzuv435t52q846cvx4w7yuvqpp5hdzvm3yf0r3vj99c7esmcv7zuj2fralf2twhl6s9xqcgr8g7nwyqhp5uwcvgs5clswpfxhm7nyfjmaeysn6us0yvjdexn9yjkv3k7zjhp2sxq9z0rgqcqpnrzjqt0mfswatysklf4z358sztscs5t0vdghmd5vfe9c9sa0gy6r5pdugrs7myqqvgqqqyqqqqqqqqqq86qq8s9qxpqysgq5wh9l4fy32ww4770mqm7yqvhwllaqyssvp335gjz6t59ca03gecyvdd9uv0ztrcm2uf2352wvwxcfh7yukucp4p6zu6ll867aj686wsqz0jlmt"}, - {1000, "lnbc10u1p5w6vggsp5gn5xhswgn5299w6elu2z0vzjxhf9hwd6pwjcgfwphaxunyu0dx6spp5a60trrhce2u6tzqjwjczem8rpdesgzkawcqg2xqaesz2kd50z4uqhp5uwcvgs5clswpfxhm7nyfjmaeysn6us0yvjdexn9yjkv3k7zjhp2sxq9z0rgqcqpnrzjqw22kc09xj0dm65ew5h5r003vtn72eyzchdgjag66l0yhwdudfmuzrwvesqq8qgqqgqqqqqqqqqqzhsq2q9qxpqysgq955gcfr95wwz0ehtnk3xraatkyhj88z44ku7yqutnwnt3gkh82jxehvdff7n2js2p54jgpvg6dmwmq8t9d8x05j63mqjrsr4cwd4lpcpnc39ru"}, - {1150, "lnbc11500n1p5jnmr7sp5u0s2wpuqn4mp0axyzgmsxzf5v8sy3zmzz9a7jyq38luyx9cntazqpp57j3carehwt4tqthxz9z7ea80t0htklh4v6v96dtn4vxuu4kwsershp53mwsvrcmkv743nyfzjp5a5fqrg2yngda3apf7jf9rzsuwt82wt3sxq9z0rgqcqpnrzjqg587a2yyuqeua9c3j8nw7wwpx709slwl5lzfs0t0vq3kdwemzp67rtwevqq95gqqyqqq5sgqqq9yzqq2q9qxpqysgqe97nwd9q74ua0sl9877sdprjcuc6jpyy8c52azpz8au6ur8q3838c0a0upnahs8w3sec8kxh26m3v9rkgqej36652t3sa5t25svacdcq5qwwjp"}, - {1151, "lnbc11510n1p5n9hzpsp5ey8npxa4nsaet73nc74lky0mv780h6890ua3kqhffvn8heqzk33spp5df0xt9s0e0kh0rx7dcy39u4q3g7cknk88wr4s90cldv6z2vwspgqhp5uwcvgs5clswpfxhm7nyfjmaeysn6us0yvjdexn9yjkv3k7zjhp2sxq9z0rgqcqpnrzjq0xp6zfjhwvmq6tltd09jcdc82ml6eh3alzvnaw8httxcx7tu78syrvfkqqqm0qqqyqqqqlgqqqvx5qqjq9qxpqysgqwnys3mklnsnrw5ysa8cjtynlqnllyxskcsamr7x96nl5kllyqcznlyeeuklr3zydeq43k6ckyrgqqfg965dsdjc675lvlssn0z4sxusq0lzrx6"}, - {2000, "lnbc20u1p5094fksp54vrdcymel5awhrpc0m6z4kvhhyvqlwkshkyt2wr6eyljkz8c798qpp59f2vc8td8tu62gtf4qfwzkrkxedsey7a5ajrd48a25z2kkwg407shp5nklhn663zgwcdnh7pe5jxt6td0cchhre6hxzdxrjdlfwtpq60f5sxq9z0rgqcqpnrzjqw0de9yc0j8n4hpgm269tm7qph4gwcyf5ys02uaapvpugrva87c7zr045uqq4jsqpsqqqqlgqqqqrcgq2q9qxpqysgq6g2pamgjumh6uw5k5rj2ket44wh8nfzs5gzyygl54hu5cefuxdhxp9h5mrg64rh07znktn9x9d5vg6fc0rw7m63x8rg4qk3kw6d8sycpywn48m"}, + { + 500, + "lnbc5u1p5sh0yvsp53seej3qkkxe6xxk9mufaj7y3jc9s9kvfn4g3whppwqcl4vcjraaspp5vtv793xc9ksch8zekkhqtv54a2evh7vq4zuywcmk9nzt69qma5yqhp5uwcvgs5clswpfxhm7nyfjmaeysn6us0yvjdexn9yjkv3k7zjhp2sxq9z0rgqcqpnrzjq0ly0l075re9ltgqzdycartvas6g4c7kwwzpasj7a98c0ss679hdsr080vqqdcgqqqqqqqqnqqqqryqqxv9qxpqysgqwq50283v8asna95fktaeg80kq9evs0chaw44y6y649qsql9vsfc5gfcsp8rdwwyccepwy83n7g0s25n3lpv3hjgcr220n5w806fja8gp2xjvd7" + }, + { + 501, + "lnbc5010n1p5shs9rsp5a2qhmn05xsd8vcm5jx9v2aswkz0pxguk4jqlaxsazzcg5rduan2qpp5al2k5zwruvlx34sxxdys2sj696m58uqgjvzxxrxhvuyswhmzg5cqhp5uwcvgs5clswpfxhm7nyfjmaeysn6us0yvjdexn9yjkv3k7zjhp2sxq9z0rgqcqpnrzjqw7c9dkkx4nur9sztw2zzpzj8u8rgsqgdsykylg5pwplh26824lc7rvlqcqqn3gqqyqqqqlgqqqqqqgq2q9qxpqysgqgpj2x2aw2dv5tzhx86th6a5vutpcdxz9htewqgvzjgqkzwmh6xs5mw5xcgrzyq77f35shv0gg5ygtjmn7e73wg8v0a9g836ufszdxmqqqu3642" + }, + { + 502, + "lnbc5020n1p5j3nxasp5qz7utfrp954nxp8049tqzg0t23krdj59thfcrc2g5h6lsemzvyfqpp5ms6xd7grtak0nr8lwytsclmq3d233v7gy7j0kuw32txhjq0f8ngqhp5uwcvgs5clswpfxhm7nyfjmaeysn6us0yvjdexn9yjkv3k7zjhp2sxq9z0rgqcqpnrzjqg587a2yyuqeua9c3j8nw7wwpx709slwl5lzfs0t0vq3kdwemzp67rtwevqq95gqqyqqq5sgqqq9yzqq2q9qxpqysgqgpp0zsetj9fedvr0szpwjfw2weckygmjthhnfpp2cerjtrn8n0pxyvrtc00l0jwzkqhwedcvgqljtwx3a7qplqp43jlxe4mpmw5svlgqfwa9yy" + }, + { + 503, + "lnbc5030n1p5n9kk6sp5ee6rsflv9rnnyt80ucc0fzlwa975nmufs2dn0x3u0hlerxxtc4nqpp5ew5mxfmu966c8wywvnvtgsljduq0jduvdpc3jzqzq9uqafx7ejgshp5uwcvgs5clswpfxhm7nyfjmaeysn6us0yvjdexn9yjkv3k7zjhp2sxq9z0rgqcqpnrzjqdqne4nrkxmz96ktnngat4nzx7sv0kf5uqmgfvqvvars7pac7fn9wr8fjvqq3csqqqqqqqqqqqqq0scqvs9qxpqysgq5lqwgfk6vv36tnlx2tv6reu2587x8ha2wsht0s75dpzvmknpgepsqaq9wnlx7n87j3x6w0vvkvc4qgda6mhacygn9f0xgagwt84uxtcqgtcpfs" + }, + { + 999, + "lnbc9990n1p5j3cf7sp575w4pw93kfrghl2gh68885v76gwjpzuv435t52q846cvx4w7yuvqpp5hdzvm3yf0r3vj99c7esmcv7zuj2fralf2twhl6s9xqcgr8g7nwyqhp5uwcvgs5clswpfxhm7nyfjmaeysn6us0yvjdexn9yjkv3k7zjhp2sxq9z0rgqcqpnrzjqt0mfswatysklf4z358sztscs5t0vdghmd5vfe9c9sa0gy6r5pdugrs7myqqvgqqqyqqqqqqqqqq86qq8s9qxpqysgq5wh9l4fy32ww4770mqm7yqvhwllaqyssvp335gjz6t59ca03gecyvdd9uv0ztrcm2uf2352wvwxcfh7yukucp4p6zu6ll867aj686wsqz0jlmt" + }, + { + 1000, + "lnbc10u1p5w6vggsp5gn5xhswgn5299w6elu2z0vzjxhf9hwd6pwjcgfwphaxunyu0dx6spp5a60trrhce2u6tzqjwjczem8rpdesgzkawcqg2xqaesz2kd50z4uqhp5uwcvgs5clswpfxhm7nyfjmaeysn6us0yvjdexn9yjkv3k7zjhp2sxq9z0rgqcqpnrzjqw22kc09xj0dm65ew5h5r003vtn72eyzchdgjag66l0yhwdudfmuzrwvesqq8qgqqgqqqqqqqqqqzhsq2q9qxpqysgq955gcfr95wwz0ehtnk3xraatkyhj88z44ku7yqutnwnt3gkh82jxehvdff7n2js2p54jgpvg6dmwmq8t9d8x05j63mqjrsr4cwd4lpcpnc39ru" + }, + { + 1150, + "lnbc11500n1p5jnmr7sp5u0s2wpuqn4mp0axyzgmsxzf5v8sy3zmzz9a7jyq38luyx9cntazqpp57j3carehwt4tqthxz9z7ea80t0htklh4v6v96dtn4vxuu4kwsershp53mwsvrcmkv743nyfzjp5a5fqrg2yngda3apf7jf9rzsuwt82wt3sxq9z0rgqcqpnrzjqg587a2yyuqeua9c3j8nw7wwpx709slwl5lzfs0t0vq3kdwemzp67rtwevqq95gqqyqqq5sgqqq9yzqq2q9qxpqysgqe97nwd9q74ua0sl9877sdprjcuc6jpyy8c52azpz8au6ur8q3838c0a0upnahs8w3sec8kxh26m3v9rkgqej36652t3sa5t25svacdcq5qwwjp" + }, + { + 1151, + "lnbc11510n1p5n9hzpsp5ey8npxa4nsaet73nc74lky0mv780h6890ua3kqhffvn8heqzk33spp5df0xt9s0e0kh0rx7dcy39u4q3g7cknk88wr4s90cldv6z2vwspgqhp5uwcvgs5clswpfxhm7nyfjmaeysn6us0yvjdexn9yjkv3k7zjhp2sxq9z0rgqcqpnrzjq0xp6zfjhwvmq6tltd09jcdc82ml6eh3alzvnaw8httxcx7tu78syrvfkqqqm0qqqyqqqqlgqqqvx5qqjq9qxpqysgqwnys3mklnsnrw5ysa8cjtynlqnllyxskcsamr7x96nl5kllyqcznlyeeuklr3zydeq43k6ckyrgqqfg965dsdjc675lvlssn0z4sxusq0lzrx6" + }, + { + 2000, + "lnbc20u1p5094fksp54vrdcymel5awhrpc0m6z4kvhhyvqlwkshkyt2wr6eyljkz8c798qpp59f2vc8td8tu62gtf4qfwzkrkxedsey7a5ajrd48a25z2kkwg407shp5nklhn663zgwcdnh7pe5jxt6td0cchhre6hxzdxrjdlfwtpq60f5sxq9z0rgqcqpnrzjqw0de9yc0j8n4hpgm269tm7qph4gwcyf5ys02uaapvpugrva87c7zr045uqq4jsqpsqqqqlgqqqqrcgq2q9qxpqysgq6g2pamgjumh6uw5k5rj2ket44wh8nfzs5gzyygl54hu5cefuxdhxp9h5mrg64rh07znktn9x9d5vg6fc0rw7m63x8rg4qk3kw6d8sycpywn48m" + }, }; private static readonly Dictionary bolt12Invoices = new() @@ -36,8 +64,7 @@ public class Integration }, }; private static ICounter counter = new InMemoryCounter(); - - + [Fact] public async Task FetchesInfoSuccessfully() { @@ -56,12 +83,12 @@ public async Task MintsBolt11Successfully() .WithUnit("sat") .WithAmount(1337) .ProcessAsyncBolt11(); - + Assert.NotNull(mintQuote); - + var paymentRequest = mintQuote.GetQuote().Request; Assert.Contains("lnbc1337", paymentRequest); - + await PayInvoice(); var mintResponse = await mintQuote.Mint(); @@ -74,20 +101,20 @@ public async Task MintsBolt12Successfully() { var wallet = Wallet.Create().WithMint(MintUrl); var privkey = new PrivKey(RandomNumberGenerator.GetHexString(64)); - + var mintQuote = await wallet .CreateMintQuote() .WithPubkey(privkey.Key.CreatePubKey()) .WithUnit("sat") .WithAmount(1337) .ProcessAsyncBolt12(); - + Assert.NotNull(mintQuote); - + var paymentRequest = mintQuote.GetQuote().Request; Assert.NotNull(paymentRequest); mintQuote.SignWithPrivkey(privkey); - + await PayInvoice(); var mintResponse = await mintQuote.Mint(); Assert.NotNull(mintResponse); @@ -97,69 +124,64 @@ public async Task MintsBolt12Successfully() [Fact] public async Task MintsDeterministicSuccessfully() { - var wallet = Wallet - .Create() - .WithMint(MintUrl) - .WithMnemonic(seed) - .WithCounter(counter); + var wallet = Wallet.Create().WithMint(MintUrl).WithMnemonic(seed).WithCounter(counter); var mintQuote = await wallet .CreateMintQuote() .WithUnit("sat") .WithAmount(1337) .ProcessAsyncBolt11(); - + Assert.NotNull(mintQuote); - + var paymentRequest = mintQuote.GetQuote().Request; Assert.Contains("lnbc1337", paymentRequest); await PayInvoice(); var mintedProofs = await mintQuote.Mint(); - + var keysetId = mintedProofs.First().Id; var currentCounter = await counter.GetCounterForId(keysetId); // counter is bumped after every use, so its already one more Assert.Equal(currentCounter, (uint)mintedProofs.Count); } - + [Fact] public async Task RestoresSuccessfully() { var phreshCounter = new InMemoryCounter(); - + var wallet = Wallet .Create() .WithCounter(phreshCounter) .WithMint(MintUrl) .WithMnemonic(seed); - - var restoredProofs = await wallet - .Restore() - .ProcessAsync(); - - var keys = (await wallet.GetKeys()).First().Keys; - var expectedAmount = Utils.SplitToProofsAmounts(1336UL, keys).Count; // (one for fee) - var keysets = await wallet.GetKeysets(); - - foreach (var keyset in keysets) - { - // new counter will be bumped to newest state - Assert.Equal(await counter.GetCounterForId(keyset.Id) + expectedAmount, await phreshCounter.GetCounterForId(keyset.Id)); - } - Assert.Equal(expectedAmount, restoredProofs.Count()); - - // assign restored counter to previous one, so next tests can use it safely - counter = phreshCounter; + + var restoredProofs = await wallet.Restore().ProcessAsync(); + + var keys = (await wallet.GetKeys()).First().Keys; + var expectedAmount = Utils.SplitToProofsAmounts(1336UL, keys).Count; // (one for fee) + var keysets = await wallet.GetKeysets(); + + foreach (var keyset in keysets) + { + // new counter will be bumped to newest state + Assert.Equal( + await counter.GetCounterForId(keyset.Id) + expectedAmount, + await phreshCounter.GetCounterForId(keyset.Id) + ); + } + Assert.Equal(expectedAmount, restoredProofs.Count()); + + // assign restored counter to previous one, so next tests can use it safely + counter = phreshCounter; } - + [Fact] public async Task SwapsSuccessfully() { - var wallet = Wallet - .Create() - .WithMint(MintUrl); - + var wallet = Wallet.Create().WithMint(MintUrl); + // 1. mint some proofs (deterministic, because why not) var mintQuote = await wallet .CreateMintQuote() @@ -170,25 +192,18 @@ public async Task SwapsSuccessfully() await PayInvoice(); var mintedProofs = await mintQuote.Mint(); Assert.NotEmpty(mintedProofs); - + //2. Swap them - var newProofs = await wallet - .Swap() - .FromInputs(mintedProofs) - .ProcessAsync(); - + var newProofs = await wallet.Swap().FromInputs(mintedProofs).ProcessAsync(); + Assert.NotEmpty(newProofs); } [Fact] public async Task SwapsDeterministicSuccessfully() { - var wallet = Wallet - .Create() - .WithMint(MintUrl) - .WithMnemonic(seed) - .WithCounter(counter); - + var wallet = Wallet.Create().WithMint(MintUrl).WithMnemonic(seed).WithCounter(counter); + // 1. mint some proofs (deterministic, because why not) var mintQuote = await wallet .CreateMintQuote() @@ -199,107 +214,103 @@ public async Task SwapsDeterministicSuccessfully() await PayInvoice(); var mintedProofs = await mintQuote.Mint(); Assert.NotEmpty(mintedProofs); - + //2. Swap them - var newProofs = await wallet - .Swap() - .FromInputs(mintedProofs) - .ProcessAsync(); - + var newProofs = await wallet.Swap().FromInputs(mintedProofs).ProcessAsync(); + Assert.NotEmpty(newProofs); } - [Fact] + [Fact] public async Task MeltsBolt11Successfully() - { - // mint proofs - var wallet = Wallet - .Create() - .WithMint(MintUrl) - .WithMnemonic(seed) - .WithCounter(counter); - - var mintQuote = await wallet - .CreateMintQuote() - .WithUnit("sat") - .WithAmount(1337) - .ProcessAsyncBolt11(); - await Task.Delay(3000); - var mintedProofs = await mintQuote.Mint(); - Assert.NotEmpty(mintedProofs); - - // create melt quote - var meltQuote = await wallet - .CreateMeltQuote() - .WithInvoice(valuesInvoices[999]) - .WithUnit("sat") - .ProcessAsyncBolt11(); - - // select proofs to send - var q = meltQuote.GetQuote(); - var selectedProofs = await wallet.SelectProofsToSend(mintedProofs, q.Amount + (ulong)q.FeeReserve, true); - - //melt proofs - var change = await meltQuote.Melt(selectedProofs.Send); - - Assert.NotEmpty(change); - } - - [Fact] - public async Task MeltsBolt12Successfully() - { - var privkeyBob = new PrivKey(RandomNumberGenerator.GetBytes(32)); - - // mint proofs - var wallet = Wallet - .Create() - .WithMint(MintUrl); - - var mintQuote = await wallet - .CreateMintQuote() - .WithUnit("sat") - .WithAmount(1337) - .WithPubkey(privkeyBob.Key.CreatePubKey()) - .ProcessAsyncBolt12(); - - await Task.Delay(3000); - - mintQuote.SignWithPrivkey(privkeyBob); - var mintedProofs = await mintQuote.Mint(); - Assert.NotEmpty(mintedProofs); - - var Ids = mintedProofs.Select(proof => proof.Id).Count(); - - Console.WriteLine($"amounts {Ids}"); - // create melt quote - var meltQuote = await wallet - .CreateMeltQuote() - .WithInvoice(bolt12Invoices[1200]) - .WithUnit("sat") - .WithAmount(1200)// it turns out that this invoice is amountless - .ProcessAsyncBolt12(); - - // select proofs to send - var q = meltQuote.GetQuote(); - var selectedProofs = await wallet.SelectProofsToSend(mintedProofs, q.Amount + (ulong)q.FeeReserve, true); - - //melt proofs - var change = await meltQuote.Melt(selectedProofs.Send); - - Assert.NotEmpty(change); - } - - [Fact] + { + // mint proofs + var wallet = Wallet.Create().WithMint(MintUrl).WithMnemonic(seed).WithCounter(counter); + + var mintQuote = await wallet + .CreateMintQuote() + .WithUnit("sat") + .WithAmount(1337) + .ProcessAsyncBolt11(); + await Task.Delay(3000); + var mintedProofs = await mintQuote.Mint(); + Assert.NotEmpty(mintedProofs); + + // create melt quote + var meltQuote = await wallet + .CreateMeltQuote() + .WithInvoice(valuesInvoices[999]) + .WithUnit("sat") + .ProcessAsyncBolt11(); + + // select proofs to send + var q = meltQuote.GetQuote(); + var selectedProofs = await wallet.SelectProofsToSend( + mintedProofs, + q.Amount + (ulong)q.FeeReserve, + true + ); + + //melt proofs + var change = await meltQuote.Melt(selectedProofs.Send); + + Assert.NotEmpty(change); + } + + [Fact] + public async Task MeltsBolt12Successfully() + { + var privkeyBob = new PrivKey(RandomNumberGenerator.GetBytes(32)); + + // mint proofs + var wallet = Wallet.Create().WithMint(MintUrl); + + var mintQuote = await wallet + .CreateMintQuote() + .WithUnit("sat") + .WithAmount(1337) + .WithPubkey(privkeyBob.Key.CreatePubKey()) + .ProcessAsyncBolt12(); + + await Task.Delay(3000); + + mintQuote.SignWithPrivkey(privkeyBob); + var mintedProofs = await mintQuote.Mint(); + Assert.NotEmpty(mintedProofs); + + var Ids = mintedProofs.Select(proof => proof.Id).Count(); + + Console.WriteLine($"amounts {Ids}"); + // create melt quote + var meltQuote = await wallet + .CreateMeltQuote() + .WithInvoice(bolt12Invoices[1200]) + .WithUnit("sat") + .WithAmount(1200) // it turns out that this invoice is amountless + .ProcessAsyncBolt12(); + + // select proofs to send + var q = meltQuote.GetQuote(); + var selectedProofs = await wallet.SelectProofsToSend( + mintedProofs, + q.Amount + (ulong)q.FeeReserve, + true + ); + + //melt proofs + var change = await meltQuote.Melt(selectedProofs.Send); + + Assert.NotEmpty(change); + } + + [Fact] public async Task SubscribeToMintMeltQuoteUpdates() { await using var service = new WebsocketService(); var connection = await service.ConnectAsync(MintUrl); Assert.NotNull(connection); - var wallet = Wallet - .Create() - .WithMint(MintUrl) - .WithWebsocketService(service); + var wallet = Wallet.Create().WithMint(MintUrl).WithWebsocketService(service); var mintHandler = await wallet .CreateMintQuote() @@ -319,12 +330,15 @@ public async Task SubscribeToMintMeltQuoteUpdates() var connectedTcs = new TaskCompletionSource(); var paidTcs = new TaskCompletionSource(); - _ = Task.Run(async () => - { - await connectedTcs.Task.WaitAsync(TimeSpan.FromSeconds(10)); - await Task.Delay(1000, cts.Token); - await PayInvoice(); - }, cts.Token); + _ = Task.Run( + async () => + { + await connectedTcs.Task.WaitAsync(TimeSpan.FromSeconds(10)); + await Task.Delay(1000, cts.Token); + await PayInvoice(); + }, + cts.Token + ); await foreach (var msg in sub.NotificationChannel.Reader.ReadAllAsync(cts.Token)) { @@ -355,7 +369,7 @@ public async Task SubscribeToMintMeltQuoteUpdates() if (paidTcs.Task.IsCompleted) break; } - + Assert.Equal(1, connectedCount); Assert.True(notificationCount >= 2, $"Expected >=2 notifications, got {notificationCount}"); @@ -363,655 +377,617 @@ public async Task SubscribeToMintMeltQuoteUpdates() Assert.NotEmpty(proofs); } - [Fact] - public async Task InvoiceWithDescription() - { - var wallet = Wallet - .Create() - .WithMint(MintUrl); - - var quote = await wallet.CreateMintQuote() - .WithDescription("Test Description") - .WithAmount(1337) - .ProcessAsyncBolt11(); - - Assert.NotNull(quote); - } - - [Fact] - public async Task FeeForExternalInvoice() - { - var wallet = Wallet - .Create() - .WithMint(MintUrl); - - var meltHandler = await wallet.CreateMeltQuote() - .WithInvoice(valuesInvoices[2000]) - .ProcessAsyncBolt11(); - - Assert.NotNull(meltHandler); - - var quote = meltHandler.GetQuote(); - - Assert.NotNull(quote); - Assert.True(quote.FeeReserve > 0); - } - - [Fact] - public async Task SwapP2Pk() - { - // p2pk aren't deterministic, so wallet is initialized without mnemonic and counter - var wallet = Wallet - .Create() - .WithMint(MintUrl); - - var privKeyAlice = new PrivKey(RandomNumberGenerator.GetHexString(64, true)); - var privKeyBob = new PrivKey(RandomNumberGenerator.GetHexString(64, true)); - - var mintHandler = await wallet.CreateMintQuote() - .WithAmount(1337) - .WithP2PkLock(new P2PkBuilder() + [Fact] + public async Task InvoiceWithDescription() + { + var wallet = Wallet.Create().WithMint(MintUrl); + + var quote = await wallet + .CreateMintQuote() + .WithDescription("Test Description") + .WithAmount(1337) + .ProcessAsyncBolt11(); + + Assert.NotNull(quote); + } + + [Fact] + public async Task FeeForExternalInvoice() + { + var wallet = Wallet.Create().WithMint(MintUrl); + + var meltHandler = await wallet + .CreateMeltQuote() + .WithInvoice(valuesInvoices[2000]) + .ProcessAsyncBolt11(); + + Assert.NotNull(meltHandler); + + var quote = meltHandler.GetQuote(); + + Assert.NotNull(quote); + Assert.True(quote.FeeReserve > 0); + } + + [Fact] + public async Task SwapP2Pk() + { + // p2pk aren't deterministic, so wallet is initialized without mnemonic and counter + var wallet = Wallet.Create().WithMint(MintUrl); + + var privKeyAlice = new PrivKey(RandomNumberGenerator.GetHexString(64, true)); + var privKeyBob = new PrivKey(RandomNumberGenerator.GetHexString(64, true)); + + var mintHandler = await wallet + .CreateMintQuote() + .WithAmount(1337) + .WithP2PkLock( + new P2PkBuilder() { Pubkeys = [privKeyBob.Key.CreatePubKey()], - SignatureThreshold = 1 + SignatureThreshold = 1, } - ).ProcessAsyncBolt11(); - - await PayInvoice(); - var proofs = await mintHandler.Mint(); - - await Assert.ThrowsAsync( - async () => await wallet - .Swap() - .FromInputs(proofs) - .ProcessAsync() - ); - - var swappedProofs = await wallet - .Swap() - .FromInputs(proofs) - .WithPrivkeys([privKeyBob]) - .ProcessAsync(); - - Assert.NotEmpty(swappedProofs); - } - - [Fact] - public async Task MintMeltP2PkMultisig() - { - var wallet = Wallet - .Create() - .WithMint(MintUrl); - - var privKeyAlice = new PrivKey(RandomNumberGenerator.GetHexString(64, true)); - var privKeyBob = new PrivKey(RandomNumberGenerator.GetHexString(64, true)); - - var mintHandler = await wallet.CreateMintQuote() - .WithAmount(1337) - .WithP2PkLock(new P2PkBuilder() - { - Pubkeys = [privKeyBob.Key.CreatePubKey(), privKeyAlice.Key.CreatePubKey()], - SignatureThreshold = 2 - } - ).ProcessAsyncBolt11(); - await PayInvoice(); - - var proofs = await mintHandler.Mint(); - - Assert.NotEmpty(proofs); - - // no privkeys - await Assert.ThrowsAsync(async () => - { - var meltHandler = await wallet - .CreateMeltQuote() - .WithInvoice(valuesInvoices[500]) - .ProcessAsyncBolt11(); - await meltHandler.Melt(proofs); - }); - - var handler = await wallet - .CreateMeltQuote() - .WithInvoice(valuesInvoices[501]) - .WithPrivKeys([privKeyBob, privKeyAlice]) - .ProcessAsyncBolt11(); - - var q = handler.GetQuote(); - - var amountToPay = q.Amount + (ulong)q.FeeReserve; - var selectorResponse = await wallet.SelectProofsToSend(proofs, amountToPay, true); - var change = await handler.Melt(selectorResponse.Send); - - Assert.NotEmpty(change); - } - - [Fact] - public async Task MintSwapP2PkSigAll() - { - var wallet = Wallet - .Create() - .WithMint(MintUrl); - - var privKeyAlice = new PrivKey(RandomNumberGenerator.GetHexString(64, true)); - var privKeyBob = new PrivKey(RandomNumberGenerator.GetHexString(64, true)); - - var mintHandler = await wallet.CreateMintQuote() - .WithAmount(1337) - .WithP2PkLock(new P2PkBuilder() - { - SigFlag = "SIG_ALL", - Pubkeys = [privKeyBob.Key.CreatePubKey()], - SignatureThreshold = 1 - } - ).ProcessAsyncBolt11(); - - await PayInvoice(); - var proofs = await mintHandler.Mint(); - - await Assert.ThrowsAsync( - async () => await wallet - .Swap() - .FromInputs(proofs) - .ProcessAsync() - ); - - var swappedProofs = await wallet - .Swap() - .FromInputs(proofs) - .WithPrivkeys([privKeyBob]) - .ProcessAsync(); - - Assert.NotEmpty(swappedProofs); - } - - [Fact] - public async Task MintSwapP2Bk() - { - var wallet = Wallet - .Create() - .WithMint(MintUrl); - - var privKeyBob = new PrivKey(RandomNumberGenerator.GetHexString(64, true)); - var privKeyAlice = new PrivKey(RandomNumberGenerator.GetHexString(64, true)); - - var builder = new P2PkBuilder() - { - Pubkeys = [privKeyBob.Key.CreatePubKey(), privKeyAlice.Key.CreatePubKey()], - }; - - var quote = await wallet - .CreateMintQuote() - .WithAmount(1337) - .WithP2PkLock(builder) - .BlindPubkeys() - .ProcessAsyncBolt11(); - - await PayInvoice(); - var proofs = await quote.Mint(); - - Assert.NotEmpty(proofs); - Assert.NotEmpty(proofs.Select(p=>p.P2PkE)); - - var newProofs = await wallet - .Swap() - .FromInputs(proofs) - .WithPrivkeys([privKeyBob, privKeyAlice]) - .ProcessAsync(); - - Assert.NotEmpty(newProofs); - } - - [Fact] - public async Task MintMeltP2Bk() - { - var wallet = Wallet - .Create() - .WithMint(MintUrl); - - var privKeyBob = new PrivKey(RandomNumberGenerator.GetHexString(64, true)); - - var builder = new P2PkBuilder() - { - Pubkeys = [privKeyBob.Key.CreatePubKey()], - }; - - var quote = await wallet - .CreateMintQuote() - .WithAmount(1337) - .WithP2PkLock(builder) - .BlindPubkeys() - .ProcessAsyncBolt11(); - - await PayInvoice(); - var proofs = await quote.Mint(); - - Assert.NotEmpty(proofs); - Assert.NotEmpty(proofs.Select(p=>p.P2PkE)); - - var meltHandler = await wallet - .CreateMeltQuote() - .WithInvoice(valuesInvoices[502]) - .WithPrivKeys([privKeyBob]) - .ProcessAsyncBolt11(); - - var change = await meltHandler.Melt(proofs); - - Assert.NotEmpty(change); - } - - [Fact] - public async Task MintMeltP2BkSigAll() - { - var wallet = Wallet - .Create() - .WithMint(MintUrl); - - var privKeyBob = new PrivKey(RandomNumberGenerator.GetHexString(64, true)); - - var builder = new P2PkBuilder() - { - Pubkeys = [privKeyBob.Key.CreatePubKey()], - SigFlag = "SIG_ALL", - }; - - var quote = await wallet - .CreateMintQuote() - .WithAmount(1337) - .WithP2PkLock(builder) - .BlindPubkeys() - .ProcessAsyncBolt11(); - - await PayInvoice(); - var proofs = await quote.Mint(); - - Assert.NotEmpty(proofs); - Assert.NotEmpty(proofs.Select(p=>p.P2PkE)); - - var meltHandler = await wallet - .CreateMeltQuote() - .WithInvoice(valuesInvoices[503]) - .WithPrivKeys([privKeyBob]) - .ProcessAsyncBolt11(); - - var change = await meltHandler.Melt(proofs); - - Assert.NotEmpty(change); - } - - [Fact] - public async Task MintSwapP2BkSigAll() - { - var wallet = Wallet - .Create() - .WithMint(MintUrl); - - var privKeyAlice = new PrivKey(RandomNumberGenerator.GetHexString(64, true)); - var privKeyBob = new PrivKey(RandomNumberGenerator.GetHexString(64, true)); - - var mintHandler = await wallet.CreateMintQuote() - .WithAmount(1337) - .WithP2PkLock(new P2PkBuilder() - { - SigFlag = "SIG_ALL", - Pubkeys = [privKeyBob.Key.CreatePubKey()], - SignatureThreshold = 1 - } - ) - .BlindPubkeys() - .ProcessAsyncBolt11(); - - await PayInvoice(); - var proofs = await mintHandler.Mint(); - - await Assert.ThrowsAsync( - async () => await wallet - .Swap() - .FromInputs(proofs) - .ProcessAsync() - ); - - var swappedProofs = await wallet - .Swap() - .FromInputs(proofs) - .WithPrivkeys([privKeyBob]) - .ProcessAsync(); - - Assert.NotEmpty(swappedProofs); - } - - [Fact] - public async Task MintSwapHTLC() - { - var wallet = Wallet - .Create() - .WithMint(MintUrl); - - var privKeyBob = new PrivKey(RandomNumberGenerator.GetHexString(64, true)); - var preimage = "0000000000000000000000000000000000000000000000000000000000000001"; - var hashLock = Convert.ToHexString(SHA256.HashData(Convert.FromHexString(preimage))); - - var mintHandler = await wallet.CreateMintQuote() - .WithAmount(1337) - .WithHTLCLock(new HTLCBuilder() - { - HashLock = hashLock, - Pubkeys = [privKeyBob.Key.CreatePubKey()], - SignatureThreshold = 1 - }) - .ProcessAsyncBolt11(); - - await PayInvoice(); - var htlcProofs = await mintHandler.Mint(); - - Assert.NotEmpty(htlcProofs); - Assert.Equal(1337UL, Utils.SumProofs(htlcProofs)); - - - // try swap without preimage - should fail - await Assert.ThrowsAsync(async () => - { - await wallet.Swap() - .FromInputs(htlcProofs) - .WithPrivkeys([privKeyBob]) - .ProcessAsync(); - }); - - // swap with correct preimage and signature - var swappedProofs = await wallet.Swap() - .FromInputs(htlcProofs) - .WithPrivkeys([privKeyBob]) - .WithHtlcPreimage(preimage) - .ProcessAsync(); - - Assert.NotEmpty(swappedProofs); - // fee is 100 ppk - it can be calculated before but here we don't care - Assert.Equal(1337UL - 1, Utils.SumProofs(swappedProofs)); - } - - [Fact] - public async Task MintSwapHTLCSigAll() - { - var wallet = Wallet - .Create() - .WithMint(MintUrl); - - var privKeyBob = new PrivKey(RandomNumberGenerator.GetHexString(64, true)); - var preimage = "0000000000000000000000000000000000000000000000000000000000000001"; - var hashLock = Convert.ToHexString(SHA256.HashData(Convert.FromHexString(preimage))); - - var mintHandler = await wallet.CreateMintQuote() - .WithAmount(1337) - .WithHTLCLock(new HTLCBuilder() - { - HashLock = hashLock, - Pubkeys = [privKeyBob.Key.CreatePubKey()], - SignatureThreshold = 1, - SigFlag = "SIG_ALL" - }) - .ProcessAsyncBolt11(); - - await PayInvoice(); - var htlcProofs = await mintHandler.Mint(); - - Assert.NotEmpty(htlcProofs); - Assert.Equal(1337UL, Utils.SumProofs(htlcProofs)); - - await Assert.ThrowsAsync(async () => - { - await wallet.Swap() - .FromInputs(htlcProofs) - .WithPrivkeys([privKeyBob]) - .ProcessAsync(); - }); - - var swappedProofs = await wallet.Swap() - .FromInputs(htlcProofs) - .WithPrivkeys([privKeyBob]) - .WithHtlcPreimage(preimage) - .ProcessAsync(); - - Assert.NotEmpty(swappedProofs); - Assert.Equal(1337UL - 1, Utils.SumProofs(swappedProofs)); - } - - [Fact] - public async Task MintSwapHtlcP2BkSigAll() - { - var wallet = Wallet - .Create() - .WithMint(MintUrl); - - var privKeyBob = new PrivKey(RandomNumberGenerator.GetHexString(64, true)); - var preimage = "0000000000000000000000000000000000000000000000000000000000000001"; - var hashLock = Convert.ToHexString(SHA256.HashData(Convert.FromHexString(preimage))); - - var mintHandler = await wallet.CreateMintQuote() - .WithAmount(1337) - .WithHTLCLock(new HTLCBuilder() - { - HashLock = hashLock, - Pubkeys = [privKeyBob.Key.CreatePubKey()], - SignatureThreshold = 1, - SigFlag = "SIG_ALL" - }) - .BlindPubkeys() - .ProcessAsyncBolt11(); - - await PayInvoice(); - var htlcProofs = await mintHandler.Mint(); - - Assert.NotEmpty(htlcProofs); - Assert.Equal(1337UL, Utils.SumProofs(htlcProofs)); - - - await Assert.ThrowsAsync(async () => - { - await wallet.Swap() - .FromInputs(htlcProofs) - .WithPrivkeys([privKeyBob]) - .ProcessAsync(); - }); - - var swappedProofs = await wallet.Swap() - .FromInputs(htlcProofs) - .WithPrivkeys([privKeyBob]) - .WithHtlcPreimage(preimage) - .ProcessAsync(); - - Assert.NotEmpty(swappedProofs); - Assert.Equal(1337UL - 1, Utils.SumProofs(swappedProofs)); - } - - [Fact] - public async Task MintMeltHTLCP2Bk() - { - var wallet = Wallet - .Create() - .WithMint(MintUrl); - - var privKeyBob = new PrivKey(RandomNumberGenerator.GetHexString(64, true)); - var preimage = new string('0', 63) + "1"; - - var builder = new HTLCBuilder() - { - Pubkeys = [privKeyBob.Key.CreatePubKey()], - HashLock = Convert.ToHexString(SHA256.HashData(Convert.FromHexString(preimage))), - }; - - var quote = await wallet - .CreateMintQuote() - .WithAmount(1337) - .WithHTLCLock(builder) - .BlindPubkeys() - .ProcessAsyncBolt11(); - - await PayInvoice(); - var proofs = await quote.Mint(); - - Assert.NotEmpty(proofs); - Assert.NotEmpty(proofs.Select(p=>p.P2PkE)); - - var meltHandler = await wallet - .CreateMeltQuote() - .WithInvoice(valuesInvoices[1150]) - .WithPrivKeys([privKeyBob]) - .WithHTLCPreimage(preimage) - .ProcessAsyncBolt11(); - - var change = await meltHandler.Melt(proofs); - - Assert.NotEmpty(change); - } - - [Fact] - public async Task MintMeltHTLCP2BkSigAll() - { - var wallet = Wallet - .Create() - .WithMint(MintUrl); - - var privKeyBob = new PrivKey(RandomNumberGenerator.GetHexString(64, true)); - var preimage = new string('0', 63) + "1"; - - var builder = new HTLCBuilder() - { - SigFlag = "SIG_ALL", - Pubkeys = [privKeyBob.Key.CreatePubKey()], - HashLock = Convert.ToHexString(SHA256.HashData(Convert.FromHexString(preimage))), - }; - - var quote = await wallet - .CreateMintQuote() - .WithAmount(1337) - .WithHTLCLock(builder) - .BlindPubkeys() - .ProcessAsyncBolt11(); - - await PayInvoice(); - var proofs = await quote.Mint(); - - Assert.NotEmpty(proofs); - Assert.NotEmpty(proofs.Select(p=>p.P2PkE)); - - var meltHandler = await wallet - .CreateMeltQuote() - .WithInvoice(valuesInvoices[1151]) - .WithPrivKeys([privKeyBob]) - .WithHTLCPreimage(preimage) - .ProcessAsyncBolt11(); - - var change = await meltHandler.Melt(proofs); - - Assert.NotEmpty(change); - } - - - - [Fact] - public async Task SwapWithCustomAmounts() - { - var wallet = Wallet - .Create() - .WithMint(MintUrl); - - // mint some proofs - var mintQuote = await wallet - .CreateMintQuote() - .WithAmount(100) - .WithUnit("sat") - .ProcessAsyncBolt11(); - - await PayInvoice(); - var mintedProofs = await mintQuote.Mint(); - Assert.NotEmpty(mintedProofs); - - // swap with specific amounts - var desiredAmounts = new List { 32, 32, 32, 2, 1 }; // 96 sat (should consume 1 for fees) - var newProofs = await wallet - .Swap() - .FromInputs(mintedProofs) - .WithAmounts(desiredAmounts) - .ProcessAsync(); - - Assert.NotEmpty(newProofs); - // amount should be at least the requested amounts - Assert.True(Utils.SumProofs(newProofs) >= 96); - } - - [Fact] - public async Task SwapToSpecificKeyset() - { - var wallet = Wallet - .Create() - .WithMint(MintUrl); - - // get active keyset - var activeKeysetId = await wallet.GetActiveKeysetId("sat"); - Assert.NotNull(activeKeysetId); - - // mint some proofs - var mintQuote = await wallet - .CreateMintQuote() - .WithAmount(64) - .WithUnit("sat") - .ProcessAsyncBolt11(); - - await PayInvoice(); - var mintedProofs = await mintQuote.Mint(); - Assert.NotEmpty(mintedProofs); - - // swap to specific keyset - var newProofs = await wallet - .Swap() - .FromInputs(mintedProofs) - .ForKeyset(activeKeysetId) - .ProcessAsync(); - - Assert.NotEmpty(newProofs); - Assert.All(newProofs, p => Assert.Equal(activeKeysetId, p.Id)); - } - - [Fact] - public async Task MeltWithInsufficientFunds() - { - var wallet = Wallet - .Create() - .WithMint(MintUrl); - - // mint small amount - var mintQuote = await wallet - .CreateMintQuote() - .WithAmount(10) - .WithUnit("sat") - .ProcessAsyncBolt11(); - - await PayInvoice(); - var mintedProofs = await mintQuote.Mint(); - Assert.NotEmpty(mintedProofs); - - // try to melt for larger invoice - should fail during proof selection - var meltHandler = await wallet - .CreateMeltQuote() - .WithInvoice(valuesInvoices[1000]) // 1000 sat invoice - .WithUnit("sat") - .ProcessAsyncBolt11(); - - var quote = meltHandler.GetQuote(); - var amountNeeded = quote.Amount + (ulong)quote.FeeReserve; - - // selectProofsToSend should return empty Send list when insufficient - var selection = await wallet.SelectProofsToSend(mintedProofs, amountNeeded, true); - Assert.Empty(selection.Send); - Assert.NotEmpty(selection.Keep); - } - + ) + .ProcessAsyncBolt11(); + await PayInvoice(); + var proofs = await mintHandler.Mint(); - private async Task PayInvoice() - { - //We're using fakewallet, so after 3 secs it will get paid automatically. After 3.5 sec its 1000% paid. - await Task.Delay(3500); - } -} + await Assert.ThrowsAsync(async () => + await wallet.Swap().FromInputs(proofs).ProcessAsync() + ); + + var swappedProofs = await wallet + .Swap() + .FromInputs(proofs) + .WithPrivkeys([privKeyBob]) + .ProcessAsync(); + + Assert.NotEmpty(swappedProofs); + } + + [Fact] + public async Task MintMeltP2PkMultisig() + { + var wallet = Wallet.Create().WithMint(MintUrl); + + var privKeyAlice = new PrivKey(RandomNumberGenerator.GetHexString(64, true)); + var privKeyBob = new PrivKey(RandomNumberGenerator.GetHexString(64, true)); + + var mintHandler = await wallet + .CreateMintQuote() + .WithAmount(1337) + .WithP2PkLock( + new P2PkBuilder() + { + Pubkeys = [privKeyBob.Key.CreatePubKey(), privKeyAlice.Key.CreatePubKey()], + SignatureThreshold = 2, + } + ) + .ProcessAsyncBolt11(); + await PayInvoice(); + + var proofs = await mintHandler.Mint(); + + Assert.NotEmpty(proofs); + + // no privkeys + await Assert.ThrowsAsync(async () => + { + var meltHandler = await wallet + .CreateMeltQuote() + .WithInvoice(valuesInvoices[500]) + .ProcessAsyncBolt11(); + await meltHandler.Melt(proofs); + }); + + var handler = await wallet + .CreateMeltQuote() + .WithInvoice(valuesInvoices[501]) + .WithPrivKeys([privKeyBob, privKeyAlice]) + .ProcessAsyncBolt11(); + + var q = handler.GetQuote(); + + var amountToPay = q.Amount + (ulong)q.FeeReserve; + var selectorResponse = await wallet.SelectProofsToSend(proofs, amountToPay, true); + var change = await handler.Melt(selectorResponse.Send); + + Assert.NotEmpty(change); + } + + [Fact] + public async Task MintSwapP2PkSigAll() + { + var wallet = Wallet.Create().WithMint(MintUrl); + + var privKeyAlice = new PrivKey(RandomNumberGenerator.GetHexString(64, true)); + var privKeyBob = new PrivKey(RandomNumberGenerator.GetHexString(64, true)); + + var mintHandler = await wallet + .CreateMintQuote() + .WithAmount(1337) + .WithP2PkLock( + new P2PkBuilder() + { + SigFlag = "SIG_ALL", + Pubkeys = [privKeyBob.Key.CreatePubKey()], + SignatureThreshold = 1, + } + ) + .ProcessAsyncBolt11(); + + await PayInvoice(); + var proofs = await mintHandler.Mint(); + + await Assert.ThrowsAsync(async () => + await wallet.Swap().FromInputs(proofs).ProcessAsync() + ); + + var swappedProofs = await wallet + .Swap() + .FromInputs(proofs) + .WithPrivkeys([privKeyBob]) + .ProcessAsync(); + + Assert.NotEmpty(swappedProofs); + } + + [Fact] + public async Task MintSwapP2Bk() + { + var wallet = Wallet.Create().WithMint(MintUrl); + + var privKeyBob = new PrivKey(RandomNumberGenerator.GetHexString(64, true)); + var privKeyAlice = new PrivKey(RandomNumberGenerator.GetHexString(64, true)); + + var builder = new P2PkBuilder() + { + Pubkeys = [privKeyBob.Key.CreatePubKey(), privKeyAlice.Key.CreatePubKey()], + }; + + var quote = await wallet + .CreateMintQuote() + .WithAmount(1337) + .WithP2PkLock(builder) + .BlindPubkeys() + .ProcessAsyncBolt11(); + + await PayInvoice(); + var proofs = await quote.Mint(); + + Assert.NotEmpty(proofs); + Assert.NotEmpty(proofs.Select(p => p.P2PkE)); + + var newProofs = await wallet + .Swap() + .FromInputs(proofs) + .WithPrivkeys([privKeyBob, privKeyAlice]) + .ProcessAsync(); + + Assert.NotEmpty(newProofs); + } + + [Fact] + public async Task MintMeltP2Bk() + { + var wallet = Wallet.Create().WithMint(MintUrl); + + var privKeyBob = new PrivKey(RandomNumberGenerator.GetHexString(64, true)); + + var builder = new P2PkBuilder() { Pubkeys = [privKeyBob.Key.CreatePubKey()] }; + + var quote = await wallet + .CreateMintQuote() + .WithAmount(1337) + .WithP2PkLock(builder) + .BlindPubkeys() + .ProcessAsyncBolt11(); + + await PayInvoice(); + var proofs = await quote.Mint(); + + Assert.NotEmpty(proofs); + Assert.NotEmpty(proofs.Select(p => p.P2PkE)); + + var meltHandler = await wallet + .CreateMeltQuote() + .WithInvoice(valuesInvoices[502]) + .WithPrivKeys([privKeyBob]) + .ProcessAsyncBolt11(); + + var change = await meltHandler.Melt(proofs); + + Assert.NotEmpty(change); + } + + [Fact] + public async Task MintMeltP2BkSigAll() + { + var wallet = Wallet.Create().WithMint(MintUrl); + + var privKeyBob = new PrivKey(RandomNumberGenerator.GetHexString(64, true)); + + var builder = new P2PkBuilder() + { + Pubkeys = [privKeyBob.Key.CreatePubKey()], + SigFlag = "SIG_ALL", + }; + + var quote = await wallet + .CreateMintQuote() + .WithAmount(1337) + .WithP2PkLock(builder) + .BlindPubkeys() + .ProcessAsyncBolt11(); + + await PayInvoice(); + var proofs = await quote.Mint(); + + Assert.NotEmpty(proofs); + Assert.NotEmpty(proofs.Select(p => p.P2PkE)); + + var meltHandler = await wallet + .CreateMeltQuote() + .WithInvoice(valuesInvoices[503]) + .WithPrivKeys([privKeyBob]) + .ProcessAsyncBolt11(); + + var change = await meltHandler.Melt(proofs); + + Assert.NotEmpty(change); + } + + [Fact] + public async Task MintSwapP2BkSigAll() + { + var wallet = Wallet.Create().WithMint(MintUrl); + + var privKeyAlice = new PrivKey(RandomNumberGenerator.GetHexString(64, true)); + var privKeyBob = new PrivKey(RandomNumberGenerator.GetHexString(64, true)); + + var mintHandler = await wallet + .CreateMintQuote() + .WithAmount(1337) + .WithP2PkLock( + new P2PkBuilder() + { + SigFlag = "SIG_ALL", + Pubkeys = [privKeyBob.Key.CreatePubKey()], + SignatureThreshold = 1, + } + ) + .BlindPubkeys() + .ProcessAsyncBolt11(); + + await PayInvoice(); + var proofs = await mintHandler.Mint(); + + await Assert.ThrowsAsync(async () => + await wallet.Swap().FromInputs(proofs).ProcessAsync() + ); + + var swappedProofs = await wallet + .Swap() + .FromInputs(proofs) + .WithPrivkeys([privKeyBob]) + .ProcessAsync(); + + Assert.NotEmpty(swappedProofs); + } + + [Fact] + public async Task MintSwapHTLC() + { + var wallet = Wallet.Create().WithMint(MintUrl); + + var privKeyBob = new PrivKey(RandomNumberGenerator.GetHexString(64, true)); + var preimage = "0000000000000000000000000000000000000000000000000000000000000001"; + var hashLock = Convert.ToHexString(SHA256.HashData(Convert.FromHexString(preimage))); + var mintHandler = await wallet + .CreateMintQuote() + .WithAmount(1337) + .WithHTLCLock( + new HTLCBuilder() + { + HashLock = hashLock, + Pubkeys = [privKeyBob.Key.CreatePubKey()], + SignatureThreshold = 1, + } + ) + .ProcessAsyncBolt11(); + + await PayInvoice(); + var htlcProofs = await mintHandler.Mint(); + + Assert.NotEmpty(htlcProofs); + Assert.Equal(1337UL, Utils.SumProofs(htlcProofs)); + + // try swap without preimage - should fail + await Assert.ThrowsAsync(async () => + { + await wallet.Swap().FromInputs(htlcProofs).WithPrivkeys([privKeyBob]).ProcessAsync(); + }); + + // swap with correct preimage and signature + var swappedProofs = await wallet + .Swap() + .FromInputs(htlcProofs) + .WithPrivkeys([privKeyBob]) + .WithHtlcPreimage(preimage) + .ProcessAsync(); + Assert.NotEmpty(swappedProofs); + // fee is 100 ppk - it can be calculated before but here we don't care + Assert.Equal(1337UL - 1, Utils.SumProofs(swappedProofs)); + } + + [Fact] + public async Task MintSwapHTLCSigAll() + { + var wallet = Wallet.Create().WithMint(MintUrl); + + var privKeyBob = new PrivKey(RandomNumberGenerator.GetHexString(64, true)); + var preimage = "0000000000000000000000000000000000000000000000000000000000000001"; + var hashLock = Convert.ToHexString(SHA256.HashData(Convert.FromHexString(preimage))); + + var mintHandler = await wallet + .CreateMintQuote() + .WithAmount(1337) + .WithHTLCLock( + new HTLCBuilder() + { + HashLock = hashLock, + Pubkeys = [privKeyBob.Key.CreatePubKey()], + SignatureThreshold = 1, + SigFlag = "SIG_ALL", + } + ) + .ProcessAsyncBolt11(); + + await PayInvoice(); + var htlcProofs = await mintHandler.Mint(); + + Assert.NotEmpty(htlcProofs); + Assert.Equal(1337UL, Utils.SumProofs(htlcProofs)); + + await Assert.ThrowsAsync(async () => + { + await wallet.Swap().FromInputs(htlcProofs).WithPrivkeys([privKeyBob]).ProcessAsync(); + }); + + var swappedProofs = await wallet + .Swap() + .FromInputs(htlcProofs) + .WithPrivkeys([privKeyBob]) + .WithHtlcPreimage(preimage) + .ProcessAsync(); + + Assert.NotEmpty(swappedProofs); + Assert.Equal(1337UL - 1, Utils.SumProofs(swappedProofs)); + } + + [Fact] + public async Task MintSwapHtlcP2BkSigAll() + { + var wallet = Wallet.Create().WithMint(MintUrl); + + var privKeyBob = new PrivKey(RandomNumberGenerator.GetHexString(64, true)); + var preimage = "0000000000000000000000000000000000000000000000000000000000000001"; + var hashLock = Convert.ToHexString(SHA256.HashData(Convert.FromHexString(preimage))); + + var mintHandler = await wallet + .CreateMintQuote() + .WithAmount(1337) + .WithHTLCLock( + new HTLCBuilder() + { + HashLock = hashLock, + Pubkeys = [privKeyBob.Key.CreatePubKey()], + SignatureThreshold = 1, + SigFlag = "SIG_ALL", + } + ) + .BlindPubkeys() + .ProcessAsyncBolt11(); + + await PayInvoice(); + var htlcProofs = await mintHandler.Mint(); + + Assert.NotEmpty(htlcProofs); + Assert.Equal(1337UL, Utils.SumProofs(htlcProofs)); + + await Assert.ThrowsAsync(async () => + { + await wallet.Swap().FromInputs(htlcProofs).WithPrivkeys([privKeyBob]).ProcessAsync(); + }); + + var swappedProofs = await wallet + .Swap() + .FromInputs(htlcProofs) + .WithPrivkeys([privKeyBob]) + .WithHtlcPreimage(preimage) + .ProcessAsync(); + + Assert.NotEmpty(swappedProofs); + Assert.Equal(1337UL - 1, Utils.SumProofs(swappedProofs)); + } + + [Fact] + public async Task MintMeltHTLCP2Bk() + { + var wallet = Wallet.Create().WithMint(MintUrl); + + var privKeyBob = new PrivKey(RandomNumberGenerator.GetHexString(64, true)); + var preimage = new string('0', 63) + "1"; + + var builder = new HTLCBuilder() + { + Pubkeys = [privKeyBob.Key.CreatePubKey()], + HashLock = Convert.ToHexString(SHA256.HashData(Convert.FromHexString(preimage))), + }; + + var quote = await wallet + .CreateMintQuote() + .WithAmount(1337) + .WithHTLCLock(builder) + .BlindPubkeys() + .ProcessAsyncBolt11(); + + await PayInvoice(); + var proofs = await quote.Mint(); + + Assert.NotEmpty(proofs); + Assert.NotEmpty(proofs.Select(p => p.P2PkE)); + + var meltHandler = await wallet + .CreateMeltQuote() + .WithInvoice(valuesInvoices[1150]) + .WithPrivKeys([privKeyBob]) + .WithHTLCPreimage(preimage) + .ProcessAsyncBolt11(); + + var change = await meltHandler.Melt(proofs); + + Assert.NotEmpty(change); + } + + [Fact] + public async Task MintMeltHTLCP2BkSigAll() + { + var wallet = Wallet.Create().WithMint(MintUrl); + + var privKeyBob = new PrivKey(RandomNumberGenerator.GetHexString(64, true)); + var preimage = new string('0', 63) + "1"; + + var builder = new HTLCBuilder() + { + SigFlag = "SIG_ALL", + Pubkeys = [privKeyBob.Key.CreatePubKey()], + HashLock = Convert.ToHexString(SHA256.HashData(Convert.FromHexString(preimage))), + }; + + var quote = await wallet + .CreateMintQuote() + .WithAmount(1337) + .WithHTLCLock(builder) + .BlindPubkeys() + .ProcessAsyncBolt11(); + + await PayInvoice(); + var proofs = await quote.Mint(); + + Assert.NotEmpty(proofs); + Assert.NotEmpty(proofs.Select(p => p.P2PkE)); + + var meltHandler = await wallet + .CreateMeltQuote() + .WithInvoice(valuesInvoices[1151]) + .WithPrivKeys([privKeyBob]) + .WithHTLCPreimage(preimage) + .ProcessAsyncBolt11(); + + var change = await meltHandler.Melt(proofs); + + Assert.NotEmpty(change); + } + + [Fact] + public async Task SwapWithCustomAmounts() + { + var wallet = Wallet.Create().WithMint(MintUrl); + + // mint some proofs + var mintQuote = await wallet + .CreateMintQuote() + .WithAmount(100) + .WithUnit("sat") + .ProcessAsyncBolt11(); + + await PayInvoice(); + var mintedProofs = await mintQuote.Mint(); + Assert.NotEmpty(mintedProofs); + + // swap with specific amounts + var desiredAmounts = new List { 32, 32, 32, 2, 1 }; // 96 sat (should consume 1 for fees) + var newProofs = await wallet + .Swap() + .FromInputs(mintedProofs) + .WithAmounts(desiredAmounts) + .ProcessAsync(); + + Assert.NotEmpty(newProofs); + // amount should be at least the requested amounts + Assert.True(Utils.SumProofs(newProofs) >= 96); + } + + [Fact] + public async Task SwapToSpecificKeyset() + { + var wallet = Wallet.Create().WithMint(MintUrl); + + // get active keyset + var activeKeysetId = await wallet.GetActiveKeysetId("sat"); + Assert.NotNull(activeKeysetId); + + // mint some proofs + var mintQuote = await wallet + .CreateMintQuote() + .WithAmount(64) + .WithUnit("sat") + .ProcessAsyncBolt11(); + + await PayInvoice(); + var mintedProofs = await mintQuote.Mint(); + Assert.NotEmpty(mintedProofs); + + // swap to specific keyset + var newProofs = await wallet + .Swap() + .FromInputs(mintedProofs) + .ForKeyset(activeKeysetId) + .ProcessAsync(); + + Assert.NotEmpty(newProofs); + Assert.All(newProofs, p => Assert.Equal(activeKeysetId, p.Id)); + } + + [Fact] + public async Task MeltWithInsufficientFunds() + { + var wallet = Wallet.Create().WithMint(MintUrl); + + // mint small amount + var mintQuote = await wallet + .CreateMintQuote() + .WithAmount(10) + .WithUnit("sat") + .ProcessAsyncBolt11(); + + await PayInvoice(); + var mintedProofs = await mintQuote.Mint(); + Assert.NotEmpty(mintedProofs); + + // try to melt for larger invoice - should fail during proof selection + var meltHandler = await wallet + .CreateMeltQuote() + .WithInvoice(valuesInvoices[1000]) // 1000 sat invoice + .WithUnit("sat") + .ProcessAsyncBolt11(); + + var quote = meltHandler.GetQuote(); + var amountNeeded = quote.Amount + (ulong)quote.FeeReserve; + + // selectProofsToSend should return empty Send list when insufficient + var selection = await wallet.SelectProofsToSend(mintedProofs, amountNeeded, true); + Assert.Empty(selection.Send); + Assert.NotEmpty(selection.Keep); + } + + private async Task PayInvoice() + { + //We're using fakewallet, so after 3 secs it will get paid automatically. After 3.5 sec its 1000% paid. + await Task.Delay(3500); + } +} diff --git a/DotNut.Tests/UnitTest1.cs b/DotNut.Tests/UnitTest1.cs index e7f8ca3..eb604c7 100644 --- a/DotNut.Tests/UnitTest1.cs +++ b/DotNut.Tests/UnitTest1.cs @@ -5,6 +5,7 @@ using NBip32Fast; using NBitcoin.Secp256k1; using Xunit.Abstractions; + namespace DotNut.Tests; public class UnitTest1 @@ -16,12 +17,18 @@ public UnitTest1(ITestOutputHelper testOutputHelper) _testOutputHelper = testOutputHelper; } - [InlineData("0000000000000000000000000000000000000000000000000000000000000000", - "024cce997d3b518f739663b757deaec95bcd9473c30a14ac2fd04023a739d1a725")] - [InlineData("0000000000000000000000000000000000000000000000000000000000000001", - "022e7158e11c9506f1aa4248bf531298daa7febd6194f003edcd9b93ade6253acf")] - [InlineData("0000000000000000000000000000000000000000000000000000000000000002", - "026cdbe15362df59cd1dd3c9c11de8aedac2106eca69236ecd9fbe117af897be4f")] + [InlineData( + "0000000000000000000000000000000000000000000000000000000000000000", + "024cce997d3b518f739663b757deaec95bcd9473c30a14ac2fd04023a739d1a725" + )] + [InlineData( + "0000000000000000000000000000000000000000000000000000000000000001", + "022e7158e11c9506f1aa4248bf531298daa7febd6194f003edcd9b93ade6253acf" + )] + [InlineData( + "0000000000000000000000000000000000000000000000000000000000000002", + "026cdbe15362df59cd1dd3c9c11de8aedac2106eca69236ecd9fbe117af897be4f" + )] [Theory] public void Nut00Tests_HashToCurve(string message, string point) { @@ -29,13 +36,16 @@ public void Nut00Tests_HashToCurve(string message, string point) Assert.Equal(point, result.ToHex()); } - - [InlineData("d341ee4871f1f889041e63cf0d3823c713eea6aff01e80f1719f08f9e5be98f6", + [InlineData( + "d341ee4871f1f889041e63cf0d3823c713eea6aff01e80f1719f08f9e5be98f6", "99fce58439fc37412ab3468b73db0569322588f62fb3a49182d67e23d877824a", - "033b1a9737a40cc3fd9b6af4b723632b76a67a36782596304612a6c2bfb5197e6d")] - [InlineData("f1aaf16c2239746f369572c0784d9dd3d032d952c2d992175873fb58fae31a60", + "033b1a9737a40cc3fd9b6af4b723632b76a67a36782596304612a6c2bfb5197e6d" + )] + [InlineData( + "f1aaf16c2239746f369572c0784d9dd3d032d952c2d992175873fb58fae31a60", "f78476ea7cc9ade20f9e05e58a804cf19533f03ea805ece5fee88c8e2874ba50", - "029bdf2d716ee366eddf599ba252786c1033f47e230248a4612a5670ab931f1763")] + "029bdf2d716ee366eddf599ba252786c1033f47e230248a4612a5670ab931f1763" + )] [Theory] public void Nut00Tests_BlindedMessages(string x, string r, string b) { @@ -46,12 +56,16 @@ public void Nut00Tests_BlindedMessages(string x, string r, string b) Assert.Equal(b, computedB.ToHex()); } - [InlineData("0000000000000000000000000000000000000000000000000000000000000001", + [InlineData( + "0000000000000000000000000000000000000000000000000000000000000001", "02a9acc1e48c25eeeb9289b5031cc57da9fe72f3fe2861d264bdc074209b107ba2", - "02a9acc1e48c25eeeb9289b5031cc57da9fe72f3fe2861d264bdc074209b107ba2")] - [InlineData("7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f", + "02a9acc1e48c25eeeb9289b5031cc57da9fe72f3fe2861d264bdc074209b107ba2" + )] + [InlineData( + "7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f", "02a9acc1e48c25eeeb9289b5031cc57da9fe72f3fe2861d264bdc074209b107ba2", - "0398bc70ce8184d27ba89834d19f5199c84443c31131e48d3c1214db24247d005d")] + "0398bc70ce8184d27ba89834d19f5199c84443c31131e48d3c1214db24247d005d" + )] [Theory] public void Nut00Tests_BlindedSignatures(string k, string b_, string blindedKey) { @@ -68,94 +82,122 @@ public void Nut00Tests_TokenSerialization() { string originalToken = "cashuAeyJ0b2tlbiI6W3sibWludCI6Imh0dHBzOi8vODMzMy5zcGFjZTozMzM4IiwicHJvb2ZzIjpbeyJhbW91bnQiOjIsImlkIjoiMDA5YTFmMjkzMjUzZTQxZSIsInNlY3JldCI6IjQwNzkxNWJjMjEyYmU2MWE3N2UzZTZkMmFlYjRjNzI3OTgwYmRhNTFjZDA2YTZhZmMyOWUyODYxNzY4YTc4MzciLCJDIjoiMDJiYzkwOTc5OTdkODFhZmIyY2M3MzQ2YjVlNDM0NWE5MzQ2YmQyYTUwNmViNzk1ODU5OGE3MmYwY2Y4NTE2M2VhIn0seyJhbW91bnQiOjgsImlkIjoiMDA5YTFmMjkzMjUzZTQxZSIsInNlY3JldCI6ImZlMTUxMDkzMTRlNjFkNzc1NmIwZjhlZTBmMjNhNjI0YWNhYTNmNGUwNDJmNjE0MzNjNzI4YzcwNTdiOTMxYmUiLCJDIjoiMDI5ZThlNTA1MGI4OTBhN2Q2YzA5NjhkYjE2YmMxZDVkNWZhMDQwZWExZGUyODRmNmVjNjlkNjEyOTlmNjcxMDU5In1dfV0sInVuaXQiOiJzYXQiLCJtZW1vIjoiVGhhbmsgeW91LiJ9"; - var result = CashuTokenHelper.Decode( - originalToken, - out var v); + var result = CashuTokenHelper.Decode(originalToken, out var v); Assert.Equal("A", v); Assert.Equal("Thank you.", result.Memo); Assert.Equal("sat", result.Unit); var token = Assert.Single(result.Tokens); Assert.Equal("https://8333.space:3338", token.Mint); Assert.Equal(2, token.Proofs.Count); - Assert.Collection(token.Proofs, proof => + Assert.Collection( + token.Proofs, + proof => { Assert.Equal((ulong)2, proof.Amount); Assert.Equal(new KeysetId("009a1f293253e41e"), proof.Id); - Assert.Equal("407915bc212be61a77e3e6d2aeb4c727980bda51cd06a6afc29e2861768a7837", - Assert.IsType(proof.Secret).Secret); - Assert.Equal("02bc9097997d81afb2cc7346b5e4345a9346bd2a506eb7958598a72f0cf85163ea".ToPubKey(), - (ECPubKey) proof.C); - }, proof => + Assert.Equal( + "407915bc212be61a77e3e6d2aeb4c727980bda51cd06a6afc29e2861768a7837", + Assert.IsType(proof.Secret).Secret + ); + Assert.Equal( + "02bc9097997d81afb2cc7346b5e4345a9346bd2a506eb7958598a72f0cf85163ea".ToPubKey(), + (ECPubKey)proof.C + ); + }, + proof => { Assert.Equal((ulong)8, proof.Amount); Assert.Equal(new KeysetId("009a1f293253e41e"), proof.Id); - Assert.Equal("fe15109314e61d7756b0f8ee0f23a624acaa3f4e042f61433c728c7057b931be", - Assert.IsType(proof.Secret).Secret); - Assert.Equal("029e8e5050b890a7d6c0968db16bc1d5d5fa040ea1de284f6ec69d61299f671059".ToPubKey(), - (ECPubKey) proof.C); + Assert.Equal( + "fe15109314e61d7756b0f8ee0f23a624acaa3f4e042f61433c728c7057b931be", + Assert.IsType(proof.Secret).Secret + ); + Assert.Equal( + "029e8e5050b890a7d6c0968db16bc1d5d5fa040ea1de284f6ec69d61299f671059".ToPubKey(), + (ECPubKey)proof.C + ); } ); Assert.Equal(originalToken, result.Encode("A", false)); - Assert.Throws(() => CashuTokenHelper.Decode( - "casshuAeyJ0b2tlbiI6W3sibWludCI6Imh0dHBzOi8vODMzMy5zcGFjZTozMzM4IiwicHJvb2ZzIjpbeyJhbW91bnQiOjIsImlkIjoiMDA5YTFmMjkzMjUzZTQxZSIsInNlY3JldCI6IjQwNzkxNWJjMjEyYmU2MWE3N2UzZTZkMmFlYjRjNzI3OTgwYmRhNTFjZDA2YTZhZmMyOWUyODYxNzY4YTc4MzciLCJDIjoiMDJiYzkwOTc5OTdkODFhZmIyY2M3MzQ2YjVlNDM0NWE5MzQ2YmQyYTUwNmViNzk1ODU5OGE3MmYwY2Y4NTE2M2VhIn0seyJhbW91bnQiOjgsImlkIjoiMDA5YTFmMjkzMjUzZTQxZSIsInNlY3JldCI6ImZlMTUxMDkzMTRlNjFkNzc1NmIwZjhlZTBmMjNhNjI0YWNhYTNmNGUwNDJmNjE0MzNjNzI4YzcwNTdiOTMxYmUiLCJDIjoiMDI5ZThlNTA1MGI4OTBhN2Q2YzA5NjhkYjE2YmMxZDVkNWZhMDQwZWExZGUyODRmNmVjNjlkNjEyOTlmNjcxMDU5In1dfV0sInVuaXQiOiJzYXQiLCJtZW1vIjoiVGhhbmsgeW91LiJ9", - out _)); - Assert.Throws(() => CashuTokenHelper.Decode( - "eyJ0b2tlbiI6W3sibWludCI6Imh0dHBzOi8vODMzMy5zcGFjZTozMzM4IiwicHJvb2ZzIjpbeyJhbW91bnQiOjIsImlkIjoiMDA5YTFmMjkzMjUzZTQxZSIsInNlY3JldCI6IjQwNzkxNWJjMjEyYmU2MWE3N2UzZTZkMmFlYjRjNzI3OTgwYmRhNTFjZDA2YTZhZmMyOWUyODYxNzY4YTc4MzciLCJDIjoiMDJiYzkwOTc5OTdkODFhZmIyY2M3MzQ2YjVlNDM0NWE5MzQ2YmQyYTUwNmViNzk1ODU5OGE3MmYwY2Y4NTE2M2VhIn0seyJhbW91bnQiOjgsImlkIjoiMDA5YTFmMjkzMjUzZTQxZSIsInNlY3JldCI6ImZlMTUxMDkzMTRlNjFkNzc1NmIwZjhlZTBmMjNhNjI0YWNhYTNmNGUwNDJmNjE0MzNjNzI4YzcwNTdiOTMxYmUiLCJDIjoiMDI5ZThlNTA1MGI4OTBhN2Q2YzA5NjhkYjE2YmMxZDVkNWZhMDQwZWExZGUyODRmNmVjNjlkNjEyOTlmNjcxMDU5In1dfV0sInVuaXQiOiJzYXQiLCJtZW1vIjoiVGhhbmsgeW91LiJ9", - out _)); - - + Assert.Throws(() => + CashuTokenHelper.Decode( + "casshuAeyJ0b2tlbiI6W3sibWludCI6Imh0dHBzOi8vODMzMy5zcGFjZTozMzM4IiwicHJvb2ZzIjpbeyJhbW91bnQiOjIsImlkIjoiMDA5YTFmMjkzMjUzZTQxZSIsInNlY3JldCI6IjQwNzkxNWJjMjEyYmU2MWE3N2UzZTZkMmFlYjRjNzI3OTgwYmRhNTFjZDA2YTZhZmMyOWUyODYxNzY4YTc4MzciLCJDIjoiMDJiYzkwOTc5OTdkODFhZmIyY2M3MzQ2YjVlNDM0NWE5MzQ2YmQyYTUwNmViNzk1ODU5OGE3MmYwY2Y4NTE2M2VhIn0seyJhbW91bnQiOjgsImlkIjoiMDA5YTFmMjkzMjUzZTQxZSIsInNlY3JldCI6ImZlMTUxMDkzMTRlNjFkNzc1NmIwZjhlZTBmMjNhNjI0YWNhYTNmNGUwNDJmNjE0MzNjNzI4YzcwNTdiOTMxYmUiLCJDIjoiMDI5ZThlNTA1MGI4OTBhN2Q2YzA5NjhkYjE2YmMxZDVkNWZhMDQwZWExZGUyODRmNmVjNjlkNjEyOTlmNjcxMDU5In1dfV0sInVuaXQiOiJzYXQiLCJtZW1vIjoiVGhhbmsgeW91LiJ9", + out _ + ) + ); + Assert.Throws(() => + CashuTokenHelper.Decode( + "eyJ0b2tlbiI6W3sibWludCI6Imh0dHBzOi8vODMzMy5zcGFjZTozMzM4IiwicHJvb2ZzIjpbeyJhbW91bnQiOjIsImlkIjoiMDA5YTFmMjkzMjUzZTQxZSIsInNlY3JldCI6IjQwNzkxNWJjMjEyYmU2MWE3N2UzZTZkMmFlYjRjNzI3OTgwYmRhNTFjZDA2YTZhZmMyOWUyODYxNzY4YTc4MzciLCJDIjoiMDJiYzkwOTc5OTdkODFhZmIyY2M3MzQ2YjVlNDM0NWE5MzQ2YmQyYTUwNmViNzk1ODU5OGE3MmYwY2Y4NTE2M2VhIn0seyJhbW91bnQiOjgsImlkIjoiMDA5YTFmMjkzMjUzZTQxZSIsInNlY3JldCI6ImZlMTUxMDkzMTRlNjFkNzc1NmIwZjhlZTBmMjNhNjI0YWNhYTNmNGUwNDJmNjE0MzNjNzI4YzcwNTdiOTMxYmUiLCJDIjoiMDI5ZThlNTA1MGI4OTBhN2Q2YzA5NjhkYjE2YmMxZDVkNWZhMDQwZWExZGUyODRmNmVjNjlkNjEyOTlmNjcxMDU5In1dfV0sInVuaXQiOiJzYXQiLCJtZW1vIjoiVGhhbmsgeW91LiJ9", + out _ + ) + ); - var v4Token = "cashuBo2F0gqJhaUgA_9SLj17PgGFwgaNhYQFhc3hAYWNjMTI0MzVlN2I4NDg0YzNjZjE4NTAxNDkyMThhZjkwZjcxNmE1MmJmNGE1ZWQzNDdlNDhlY2MxM2Y3NzM4OGFjWCECRFODGd5IXVW-07KaZCvuWHk3WrnnpiDhHki6SCQh88-iYWlIAK0mjE0fWCZhcIKjYWECYXN4QDEzMjNkM2Q0NzA3YTU4YWQyZTIzYWRhNGU5ZjFmNDlmNWE1YjRhYzdiNzA4ZWIwZDYxZjczOGY0ODMwN2U4ZWVhY1ghAjRWqhENhLSsdHrr2Cw7AFrKUL9Ffr1XN6RBT6w659lNo2FhAWFzeEA1NmJjYmNiYjdjYzY0MDZiM2ZhNWQ1N2QyMTc0ZjRlZmY4YjQ0MDJiMTc2OTI2ZDNhNTdkM2MzZGNiYjU5ZDU3YWNYIQJzEpxXGeWZN5qXSmJjY8MzxWyvwObQGr5G1YCCgHicY2FtdWh0dHA6Ly9sb2NhbGhvc3Q6MzMzOGF1Y3NhdA"; result = CashuTokenHelper.Decode(v4Token, out v); - + Assert.Equal("B", v); Assert.Null(result.Memo); Assert.Equal("sat", result.Unit); token = Assert.Single(result.Tokens); Assert.Equal("http://localhost:3338", token.Mint); Assert.Equal(3, token.Proofs.Count); - Assert.Collection(token.Proofs, proof => + Assert.Collection( + token.Proofs, + proof => { Assert.Equal((ulong)1, proof.Amount); Assert.Equal(new KeysetId("00ffd48b8f5ecf80"), proof.Id); - Assert.Equal("acc12435e7b8484c3cf1850149218af90f716a52bf4a5ed347e48ecc13f77388", - Assert.IsType(proof.Secret).Secret); - Assert.Equal("0244538319de485d55bed3b29a642bee5879375ab9e7a620e11e48ba482421f3cf".ToPubKey(), - (ECPubKey) proof.C); - }, proof => + Assert.Equal( + "acc12435e7b8484c3cf1850149218af90f716a52bf4a5ed347e48ecc13f77388", + Assert.IsType(proof.Secret).Secret + ); + Assert.Equal( + "0244538319de485d55bed3b29a642bee5879375ab9e7a620e11e48ba482421f3cf".ToPubKey(), + (ECPubKey)proof.C + ); + }, + proof => { Assert.Equal((ulong)2, proof.Amount); Assert.Equal(new KeysetId("00ad268c4d1f5826"), proof.Id); - Assert.Equal("1323d3d4707a58ad2e23ada4e9f1f49f5a5b4ac7b708eb0d61f738f48307e8ee", - Assert.IsType(proof.Secret).Secret); - Assert.Equal("023456aa110d84b4ac747aebd82c3b005aca50bf457ebd5737a4414fac3ae7d94d".ToPubKey(), - (ECPubKey) proof.C); - }, proof => + Assert.Equal( + "1323d3d4707a58ad2e23ada4e9f1f49f5a5b4ac7b708eb0d61f738f48307e8ee", + Assert.IsType(proof.Secret).Secret + ); + Assert.Equal( + "023456aa110d84b4ac747aebd82c3b005aca50bf457ebd5737a4414fac3ae7d94d".ToPubKey(), + (ECPubKey)proof.C + ); + }, + proof => { Assert.Equal((ulong)1, proof.Amount); Assert.Equal(new KeysetId("00ad268c4d1f5826"), proof.Id); - Assert.Equal("56bcbcbb7cc6406b3fa5d57d2174f4eff8b4402b176926d3a57d3c3dcbb59d57", - Assert.IsType(proof.Secret).Secret); - Assert.Equal("0273129c5719e599379a974a626363c333c56cafc0e6d01abe46d5808280789c63".ToPubKey(), - (ECPubKey) proof.C); + Assert.Equal( + "56bcbcbb7cc6406b3fa5d57d2174f4eff8b4402b176926d3a57d3c3dcbb59d57", + Assert.IsType(proof.Secret).Secret + ); + Assert.Equal( + "0273129c5719e599379a974a626363c333c56cafc0e6d01abe46d5808280789c63".ToPubKey(), + (ECPubKey)proof.C + ); } ); Assert.Equal(v4Token, result.Encode("B", false)); - - } [Theory] [InlineData( - "{\n \"1\":\"03a40f20667ed53513075dc51e715ff2046cad64eb68960632269ba7f0210e38\",\"2\":\"03fd4ce5a16b65576145949e6f99f445f8249fee17c606b688b504a849cdc452de\",\"4\":\"02648eccfa4c026960966276fa5a4cae46ce0fd432211a4f449bf84f13aa5f8303\",\"8\":\"02fdfd6796bfeac490cbee12f778f867f0a2c68f6508d17c649759ea0dc3547528\"\n}")] + "{\n \"1\":\"03a40f20667ed53513075dc51e715ff2046cad64eb68960632269ba7f0210e38\",\"2\":\"03fd4ce5a16b65576145949e6f99f445f8249fee17c606b688b504a849cdc452de\",\"4\":\"02648eccfa4c026960966276fa5a4cae46ce0fd432211a4f449bf84f13aa5f8303\",\"8\":\"02fdfd6796bfeac490cbee12f778f867f0a2c68f6508d17c649759ea0dc3547528\"\n}" + )] [InlineData( - "{\n \"1\":\"03a40f20667ed53513075dc51e715ff2046cad64eb68960632269ba7f0210e38bc\",\"2\":\"04fd4ce5a16b65576145949e6f99f445f8249fee17c606b688b504a849cdc452de3625246cb2c27dac965cb7200a5986467eee92eb7d496bbf1453b074e223e481\",\"4\":\"02648eccfa4c026960966276fa5a4cae46ce0fd432211a4f449bf84f13aa5f8303\",\"8\":\"02fdfd6796bfeac490cbee12f778f867f0a2c68f6508d17c649759ea0dc3547528\"\n}")] + "{\n \"1\":\"03a40f20667ed53513075dc51e715ff2046cad64eb68960632269ba7f0210e38bc\",\"2\":\"04fd4ce5a16b65576145949e6f99f445f8249fee17c606b688b504a849cdc452de3625246cb2c27dac965cb7200a5986467eee92eb7d496bbf1453b074e223e481\",\"4\":\"02648eccfa4c026960966276fa5a4cae46ce0fd432211a4f449bf84f13aa5f8303\",\"8\":\"02fdfd6796bfeac490cbee12f778f867f0a2c68f6508d17c649759ea0dc3547528\"\n}" + )] public void Nut01Tests_Keysets_Invalid(string keyset) { Assert.ThrowsAny(() => JsonSerializer.Deserialize(keyset)); @@ -163,9 +205,11 @@ public void Nut01Tests_Keysets_Invalid(string keyset) [Theory] [InlineData( - "{\n \"1\":\"03a40f20667ed53513075dc51e715ff2046cad64eb68960632269ba7f0210e38bc\",\"2\":\"03fd4ce5a16b65576145949e6f99f445f8249fee17c606b688b504a849cdc452de\",\"4\":\"02648eccfa4c026960966276fa5a4cae46ce0fd432211a4f449bf84f13aa5f8303\",\"8\":\"02fdfd6796bfeac490cbee12f778f867f0a2c68f6508d17c649759ea0dc3547528\"\n}")] + "{\n \"1\":\"03a40f20667ed53513075dc51e715ff2046cad64eb68960632269ba7f0210e38bc\",\"2\":\"03fd4ce5a16b65576145949e6f99f445f8249fee17c606b688b504a849cdc452de\",\"4\":\"02648eccfa4c026960966276fa5a4cae46ce0fd432211a4f449bf84f13aa5f8303\",\"8\":\"02fdfd6796bfeac490cbee12f778f867f0a2c68f6508d17c649759ea0dc3547528\"\n}" + )] [InlineData( - "{\n \"1\":\"03ba786a2c0745f8c30e490288acd7a72dd53d65afd292ddefa326a4a3fa14c566\",\"2\":\"03361cd8bd1329fea797a6add1cf1990ffcf2270ceb9fc81eeee0e8e9c1bd0cdf5\",\"4\":\"036e378bcf78738ddf68859293c69778035740e41138ab183c94f8fee7572214c7\",\"8\":\"03909d73beaf28edfb283dbeb8da321afd40651e8902fcf5454ecc7d69788626c0\",\"16\":\"028a36f0e6638ea7466665fe174d958212723019ec08f9ce6898d897f88e68aa5d\",\"32\":\"03a97a40e146adee2687ac60c2ba2586a90f970de92a9d0e6cae5a4b9965f54612\",\"64\":\"03ce86f0c197aab181ddba0cfc5c5576e11dfd5164d9f3d4a3fc3ffbbf2e069664\",\"128\":\"0284f2c06d938a6f78794814c687560a0aabab19fe5e6f30ede38e113b132a3cb9\",\"256\":\"03b99f475b68e5b4c0ba809cdecaae64eade2d9787aa123206f91cd61f76c01459\",\"512\":\"03d4db82ea19a44d35274de51f78af0a710925fe7d9e03620b84e3e9976e3ac2eb\",\"1024\":\"031fbd4ba801870871d46cf62228a1b748905ebc07d3b210daf48de229e683f2dc\",\"2048\":\"0276cedb9a3b160db6a158ad4e468d2437f021293204b3cd4bf6247970d8aff54b\",\"4096\":\"02fc6b89b403ee9eb8a7ed457cd3973638080d6e04ca8af7307c965c166b555ea2\",\"8192\":\"0320265583e916d3a305f0d2687fcf2cd4e3cd03a16ea8261fda309c3ec5721e21\",\"16384\":\"036e41de58fdff3cb1d8d713f48c63bc61fa3b3e1631495a444d178363c0d2ed50\",\"32768\":\"0365438f613f19696264300b069d1dad93f0c60a37536b72a8ab7c7366a5ee6c04\",\"65536\":\"02408426cfb6fc86341bac79624ba8708a4376b2d92debdf4134813f866eb57a8d\",\"131072\":\"031063e9f11c94dc778c473e968966eac0e70b7145213fbaff5f7a007e71c65f41\",\"262144\":\"02f2a3e808f9cd168ec71b7f328258d0c1dda250659c1aced14c7f5cf05aab4328\",\"524288\":\"038ac10de9f1ff9395903bb73077e94dbf91e9ef98fd77d9a2debc5f74c575bc86\",\"1048576\":\"0203eaee4db749b0fc7c49870d082024b2c31d889f9bc3b32473d4f1dfa3625788\",\"2097152\":\"033cdb9d36e1e82ae652b7b6a08e0204569ec7ff9ebf85d80a02786dc7fe00b04c\",\"4194304\":\"02c8b73f4e3a470ae05e5f2fe39984d41e9f6ae7be9f3b09c9ac31292e403ac512\",\"8388608\":\"025bbe0cfce8a1f4fbd7f3a0d4a09cb6badd73ef61829dc827aa8a98c270bc25b0\",\"16777216\":\"037eec3d1651a30a90182d9287a5c51386fe35d4a96839cf7969c6e2a03db1fc21\",\"33554432\":\"03280576b81a04e6abd7197f305506476f5751356b7643988495ca5c3e14e5c262\",\"67108864\":\"03268bfb05be1dbb33ab6e7e00e438373ca2c9b9abc018fdb452d0e1a0935e10d3\",\"134217728\":\"02573b68784ceba9617bbcc7c9487836d296aa7c628c3199173a841e7a19798020\",\"268435456\":\"0234076b6e70f7fbf755d2227ecc8d8169d662518ee3a1401f729e2a12ccb2b276\",\"536870912\":\"03015bd88961e2a466a2163bd4248d1d2b42c7c58a157e594785e7eb34d880efc9\",\"1073741824\":\"02c9b076d08f9020ebee49ac8ba2610b404d4e553a4f800150ceb539e9421aaeee\",\"2147483648\":\"034d592f4c366afddc919a509600af81b489a03caf4f7517c2b3f4f2b558f9a41a\",\"4294967296\":\"037c09ecb66da082981e4cbdb1ac65c0eb631fc75d85bed13efb2c6364148879b5\",\"8589934592\":\"02b4ebb0dda3b9ad83b39e2e31024b777cc0ac205a96b9a6cfab3edea2912ed1b3\",\"17179869184\":\"026cc4dacdced45e63f6e4f62edbc5779ccd802e7fabb82d5123db879b636176e9\",\"34359738368\":\"02b2cee01b7d8e90180254459b8f09bbea9aad34c3a2fd98c85517ecfc9805af75\",\"68719476736\":\"037a0c0d564540fc574b8bfa0253cca987b75466e44b295ed59f6f8bd41aace754\",\"137438953472\":\"021df6585cae9b9ca431318a713fd73dbb76b3ef5667957e8633bca8aaa7214fb6\",\"274877906944\":\"02b8f53dde126f8c85fa5bb6061c0be5aca90984ce9b902966941caf963648d53a\",\"549755813888\":\"029cc8af2840d59f1d8761779b2496623c82c64be8e15f9ab577c657c6dd453785\",\"1099511627776\":\"03e446fdb84fad492ff3a25fc1046fb9a93a5b262ebcd0151caa442ea28959a38a\",\"2199023255552\":\"02d6b25bd4ab599dd0818c55f75702fde603c93f259222001246569018842d3258\",\"4398046511104\":\"03397b522bb4e156ec3952d3f048e5a986c20a00718e5e52cd5718466bf494156a\",\"8796093022208\":\"02d1fb9e78262b5d7d74028073075b80bb5ab281edcfc3191061962c1346340f1e\",\"17592186044416\":\"030d3f2ad7a4ca115712ff7f140434f802b19a4c9b2dd1c76f3e8e80c05c6a9310\",\"35184372088832\":\"03e325b691f292e1dfb151c3fb7cad440b225795583c32e24e10635a80e4221c06\",\"70368744177664\":\"03bee8f64d88de3dee21d61f89efa32933da51152ddbd67466bef815e9f93f8fd1\",\"140737488355328\":\"0327244c9019a4892e1f04ba3bf95fe43b327479e2d57c25979446cc508cd379ed\",\"281474976710656\":\"02fb58522cd662f2f8b042f8161caae6e45de98283f74d4e99f19b0ea85e08a56d\",\"562949953421312\":\"02adde4b466a9d7e59386b6a701a39717c53f30c4810613c1b55e6b6da43b7bc9a\",\"1125899906842624\":\"038eeda11f78ce05c774f30e393cda075192b890d68590813ff46362548528dca9\",\"2251799813685248\":\"02ec13e0058b196db80f7079d329333b330dc30c000dbdd7397cbbc5a37a664c4f\",\"4503599627370496\":\"02d2d162db63675bd04f7d56df04508840f41e2ad87312a3c93041b494efe80a73\",\"9007199254740992\":\"0356969d6aef2bb40121dbd07c68b6102339f4ea8e674a9008bb69506795998f49\",\"18014398509481984\":\"02f4e667567ebb9f4e6e180a4113bb071c48855f657766bb5e9c776a880335d1d6\",\"36028797018963968\":\"0385b4fe35e41703d7a657d957c67bb536629de57b7e6ee6fe2130728ef0fc90b0\",\"72057594037927936\":\"02b2bc1968a6fddbcc78fb9903940524824b5f5bed329c6ad48a19b56068c144fd\",\"144115188075855872\":\"02e0dbb24f1d288a693e8a49bc14264d1276be16972131520cf9e055ae92fba19a\",\"288230376151711744\":\"03efe75c106f931a525dc2d653ebedddc413a2c7d8cb9da410893ae7d2fa7d19cc\",\"576460752303423488\":\"02c7ec2bd9508a7fc03f73c7565dc600b30fd86f3d305f8f139c45c404a52d958a\",\"1152921504606846976\":\"035a6679c6b25e68ff4e29d1c7ef87f21e0a8fc574f6a08c1aa45ff352c1d59f06\",\"2305843009213693952\":\"033cdc225962c052d485f7cfbf55a5b2367d200fe1fe4373a347deb4cc99e9a099\",\"4611686018427387904\":\"024a4b806cf413d14b294719090a9da36ba75209c7657135ad09bc65328fba9e6f\",\"9223372036854775808\":\"0377a6fe114e291a8d8e991627c38001c8305b23b9e98b1c7b1893f5cd0dda6cad\"\n}")] + "{\n \"1\":\"03ba786a2c0745f8c30e490288acd7a72dd53d65afd292ddefa326a4a3fa14c566\",\"2\":\"03361cd8bd1329fea797a6add1cf1990ffcf2270ceb9fc81eeee0e8e9c1bd0cdf5\",\"4\":\"036e378bcf78738ddf68859293c69778035740e41138ab183c94f8fee7572214c7\",\"8\":\"03909d73beaf28edfb283dbeb8da321afd40651e8902fcf5454ecc7d69788626c0\",\"16\":\"028a36f0e6638ea7466665fe174d958212723019ec08f9ce6898d897f88e68aa5d\",\"32\":\"03a97a40e146adee2687ac60c2ba2586a90f970de92a9d0e6cae5a4b9965f54612\",\"64\":\"03ce86f0c197aab181ddba0cfc5c5576e11dfd5164d9f3d4a3fc3ffbbf2e069664\",\"128\":\"0284f2c06d938a6f78794814c687560a0aabab19fe5e6f30ede38e113b132a3cb9\",\"256\":\"03b99f475b68e5b4c0ba809cdecaae64eade2d9787aa123206f91cd61f76c01459\",\"512\":\"03d4db82ea19a44d35274de51f78af0a710925fe7d9e03620b84e3e9976e3ac2eb\",\"1024\":\"031fbd4ba801870871d46cf62228a1b748905ebc07d3b210daf48de229e683f2dc\",\"2048\":\"0276cedb9a3b160db6a158ad4e468d2437f021293204b3cd4bf6247970d8aff54b\",\"4096\":\"02fc6b89b403ee9eb8a7ed457cd3973638080d6e04ca8af7307c965c166b555ea2\",\"8192\":\"0320265583e916d3a305f0d2687fcf2cd4e3cd03a16ea8261fda309c3ec5721e21\",\"16384\":\"036e41de58fdff3cb1d8d713f48c63bc61fa3b3e1631495a444d178363c0d2ed50\",\"32768\":\"0365438f613f19696264300b069d1dad93f0c60a37536b72a8ab7c7366a5ee6c04\",\"65536\":\"02408426cfb6fc86341bac79624ba8708a4376b2d92debdf4134813f866eb57a8d\",\"131072\":\"031063e9f11c94dc778c473e968966eac0e70b7145213fbaff5f7a007e71c65f41\",\"262144\":\"02f2a3e808f9cd168ec71b7f328258d0c1dda250659c1aced14c7f5cf05aab4328\",\"524288\":\"038ac10de9f1ff9395903bb73077e94dbf91e9ef98fd77d9a2debc5f74c575bc86\",\"1048576\":\"0203eaee4db749b0fc7c49870d082024b2c31d889f9bc3b32473d4f1dfa3625788\",\"2097152\":\"033cdb9d36e1e82ae652b7b6a08e0204569ec7ff9ebf85d80a02786dc7fe00b04c\",\"4194304\":\"02c8b73f4e3a470ae05e5f2fe39984d41e9f6ae7be9f3b09c9ac31292e403ac512\",\"8388608\":\"025bbe0cfce8a1f4fbd7f3a0d4a09cb6badd73ef61829dc827aa8a98c270bc25b0\",\"16777216\":\"037eec3d1651a30a90182d9287a5c51386fe35d4a96839cf7969c6e2a03db1fc21\",\"33554432\":\"03280576b81a04e6abd7197f305506476f5751356b7643988495ca5c3e14e5c262\",\"67108864\":\"03268bfb05be1dbb33ab6e7e00e438373ca2c9b9abc018fdb452d0e1a0935e10d3\",\"134217728\":\"02573b68784ceba9617bbcc7c9487836d296aa7c628c3199173a841e7a19798020\",\"268435456\":\"0234076b6e70f7fbf755d2227ecc8d8169d662518ee3a1401f729e2a12ccb2b276\",\"536870912\":\"03015bd88961e2a466a2163bd4248d1d2b42c7c58a157e594785e7eb34d880efc9\",\"1073741824\":\"02c9b076d08f9020ebee49ac8ba2610b404d4e553a4f800150ceb539e9421aaeee\",\"2147483648\":\"034d592f4c366afddc919a509600af81b489a03caf4f7517c2b3f4f2b558f9a41a\",\"4294967296\":\"037c09ecb66da082981e4cbdb1ac65c0eb631fc75d85bed13efb2c6364148879b5\",\"8589934592\":\"02b4ebb0dda3b9ad83b39e2e31024b777cc0ac205a96b9a6cfab3edea2912ed1b3\",\"17179869184\":\"026cc4dacdced45e63f6e4f62edbc5779ccd802e7fabb82d5123db879b636176e9\",\"34359738368\":\"02b2cee01b7d8e90180254459b8f09bbea9aad34c3a2fd98c85517ecfc9805af75\",\"68719476736\":\"037a0c0d564540fc574b8bfa0253cca987b75466e44b295ed59f6f8bd41aace754\",\"137438953472\":\"021df6585cae9b9ca431318a713fd73dbb76b3ef5667957e8633bca8aaa7214fb6\",\"274877906944\":\"02b8f53dde126f8c85fa5bb6061c0be5aca90984ce9b902966941caf963648d53a\",\"549755813888\":\"029cc8af2840d59f1d8761779b2496623c82c64be8e15f9ab577c657c6dd453785\",\"1099511627776\":\"03e446fdb84fad492ff3a25fc1046fb9a93a5b262ebcd0151caa442ea28959a38a\",\"2199023255552\":\"02d6b25bd4ab599dd0818c55f75702fde603c93f259222001246569018842d3258\",\"4398046511104\":\"03397b522bb4e156ec3952d3f048e5a986c20a00718e5e52cd5718466bf494156a\",\"8796093022208\":\"02d1fb9e78262b5d7d74028073075b80bb5ab281edcfc3191061962c1346340f1e\",\"17592186044416\":\"030d3f2ad7a4ca115712ff7f140434f802b19a4c9b2dd1c76f3e8e80c05c6a9310\",\"35184372088832\":\"03e325b691f292e1dfb151c3fb7cad440b225795583c32e24e10635a80e4221c06\",\"70368744177664\":\"03bee8f64d88de3dee21d61f89efa32933da51152ddbd67466bef815e9f93f8fd1\",\"140737488355328\":\"0327244c9019a4892e1f04ba3bf95fe43b327479e2d57c25979446cc508cd379ed\",\"281474976710656\":\"02fb58522cd662f2f8b042f8161caae6e45de98283f74d4e99f19b0ea85e08a56d\",\"562949953421312\":\"02adde4b466a9d7e59386b6a701a39717c53f30c4810613c1b55e6b6da43b7bc9a\",\"1125899906842624\":\"038eeda11f78ce05c774f30e393cda075192b890d68590813ff46362548528dca9\",\"2251799813685248\":\"02ec13e0058b196db80f7079d329333b330dc30c000dbdd7397cbbc5a37a664c4f\",\"4503599627370496\":\"02d2d162db63675bd04f7d56df04508840f41e2ad87312a3c93041b494efe80a73\",\"9007199254740992\":\"0356969d6aef2bb40121dbd07c68b6102339f4ea8e674a9008bb69506795998f49\",\"18014398509481984\":\"02f4e667567ebb9f4e6e180a4113bb071c48855f657766bb5e9c776a880335d1d6\",\"36028797018963968\":\"0385b4fe35e41703d7a657d957c67bb536629de57b7e6ee6fe2130728ef0fc90b0\",\"72057594037927936\":\"02b2bc1968a6fddbcc78fb9903940524824b5f5bed329c6ad48a19b56068c144fd\",\"144115188075855872\":\"02e0dbb24f1d288a693e8a49bc14264d1276be16972131520cf9e055ae92fba19a\",\"288230376151711744\":\"03efe75c106f931a525dc2d653ebedddc413a2c7d8cb9da410893ae7d2fa7d19cc\",\"576460752303423488\":\"02c7ec2bd9508a7fc03f73c7565dc600b30fd86f3d305f8f139c45c404a52d958a\",\"1152921504606846976\":\"035a6679c6b25e68ff4e29d1c7ef87f21e0a8fc574f6a08c1aa45ff352c1d59f06\",\"2305843009213693952\":\"033cdc225962c052d485f7cfbf55a5b2367d200fe1fe4373a347deb4cc99e9a099\",\"4611686018427387904\":\"024a4b806cf413d14b294719090a9da36ba75209c7657135ad09bc65328fba9e6f\",\"9223372036854775808\":\"0377a6fe114e291a8d8e991627c38001c8305b23b9e98b1c7b1893f5cd0dda6cad\"\n}" + )] public void Nut01Tests_Keysets_Valid(string keyset) { JsonSerializer.Deserialize(keyset); @@ -173,35 +217,53 @@ public void Nut01Tests_Keysets_Valid(string keyset) [Theory] // v1 - [InlineData("00456a94ab4e1c46", - "{\n \"1\":\"03a40f20667ed53513075dc51e715ff2046cad64eb68960632269ba7f0210e38bc\",\"2\":\"03fd4ce5a16b65576145949e6f99f445f8249fee17c606b688b504a849cdc452de\",\"4\":\"02648eccfa4c026960966276fa5a4cae46ce0fd432211a4f449bf84f13aa5f8303\",\"8\":\"02fdfd6796bfeac490cbee12f778f867f0a2c68f6508d17c649759ea0dc3547528\"\n}")] - [InlineData("000f01df73ea149a", - "{\n \"1\":\"03ba786a2c0745f8c30e490288acd7a72dd53d65afd292ddefa326a4a3fa14c566\",\"2\":\"03361cd8bd1329fea797a6add1cf1990ffcf2270ceb9fc81eeee0e8e9c1bd0cdf5\",\"4\":\"036e378bcf78738ddf68859293c69778035740e41138ab183c94f8fee7572214c7\",\"8\":\"03909d73beaf28edfb283dbeb8da321afd40651e8902fcf5454ecc7d69788626c0\",\"16\":\"028a36f0e6638ea7466665fe174d958212723019ec08f9ce6898d897f88e68aa5d\",\"32\":\"03a97a40e146adee2687ac60c2ba2586a90f970de92a9d0e6cae5a4b9965f54612\",\"64\":\"03ce86f0c197aab181ddba0cfc5c5576e11dfd5164d9f3d4a3fc3ffbbf2e069664\",\"128\":\"0284f2c06d938a6f78794814c687560a0aabab19fe5e6f30ede38e113b132a3cb9\",\"256\":\"03b99f475b68e5b4c0ba809cdecaae64eade2d9787aa123206f91cd61f76c01459\",\"512\":\"03d4db82ea19a44d35274de51f78af0a710925fe7d9e03620b84e3e9976e3ac2eb\",\"1024\":\"031fbd4ba801870871d46cf62228a1b748905ebc07d3b210daf48de229e683f2dc\",\"2048\":\"0276cedb9a3b160db6a158ad4e468d2437f021293204b3cd4bf6247970d8aff54b\",\"4096\":\"02fc6b89b403ee9eb8a7ed457cd3973638080d6e04ca8af7307c965c166b555ea2\",\"8192\":\"0320265583e916d3a305f0d2687fcf2cd4e3cd03a16ea8261fda309c3ec5721e21\",\"16384\":\"036e41de58fdff3cb1d8d713f48c63bc61fa3b3e1631495a444d178363c0d2ed50\",\"32768\":\"0365438f613f19696264300b069d1dad93f0c60a37536b72a8ab7c7366a5ee6c04\",\"65536\":\"02408426cfb6fc86341bac79624ba8708a4376b2d92debdf4134813f866eb57a8d\",\"131072\":\"031063e9f11c94dc778c473e968966eac0e70b7145213fbaff5f7a007e71c65f41\",\"262144\":\"02f2a3e808f9cd168ec71b7f328258d0c1dda250659c1aced14c7f5cf05aab4328\",\"524288\":\"038ac10de9f1ff9395903bb73077e94dbf91e9ef98fd77d9a2debc5f74c575bc86\",\"1048576\":\"0203eaee4db749b0fc7c49870d082024b2c31d889f9bc3b32473d4f1dfa3625788\",\"2097152\":\"033cdb9d36e1e82ae652b7b6a08e0204569ec7ff9ebf85d80a02786dc7fe00b04c\",\"4194304\":\"02c8b73f4e3a470ae05e5f2fe39984d41e9f6ae7be9f3b09c9ac31292e403ac512\",\"8388608\":\"025bbe0cfce8a1f4fbd7f3a0d4a09cb6badd73ef61829dc827aa8a98c270bc25b0\",\"16777216\":\"037eec3d1651a30a90182d9287a5c51386fe35d4a96839cf7969c6e2a03db1fc21\",\"33554432\":\"03280576b81a04e6abd7197f305506476f5751356b7643988495ca5c3e14e5c262\",\"67108864\":\"03268bfb05be1dbb33ab6e7e00e438373ca2c9b9abc018fdb452d0e1a0935e10d3\",\"134217728\":\"02573b68784ceba9617bbcc7c9487836d296aa7c628c3199173a841e7a19798020\",\"268435456\":\"0234076b6e70f7fbf755d2227ecc8d8169d662518ee3a1401f729e2a12ccb2b276\",\"536870912\":\"03015bd88961e2a466a2163bd4248d1d2b42c7c58a157e594785e7eb34d880efc9\",\"1073741824\":\"02c9b076d08f9020ebee49ac8ba2610b404d4e553a4f800150ceb539e9421aaeee\",\"2147483648\":\"034d592f4c366afddc919a509600af81b489a03caf4f7517c2b3f4f2b558f9a41a\",\"4294967296\":\"037c09ecb66da082981e4cbdb1ac65c0eb631fc75d85bed13efb2c6364148879b5\",\"8589934592\":\"02b4ebb0dda3b9ad83b39e2e31024b777cc0ac205a96b9a6cfab3edea2912ed1b3\",\"17179869184\":\"026cc4dacdced45e63f6e4f62edbc5779ccd802e7fabb82d5123db879b636176e9\",\"34359738368\":\"02b2cee01b7d8e90180254459b8f09bbea9aad34c3a2fd98c85517ecfc9805af75\",\"68719476736\":\"037a0c0d564540fc574b8bfa0253cca987b75466e44b295ed59f6f8bd41aace754\",\"137438953472\":\"021df6585cae9b9ca431318a713fd73dbb76b3ef5667957e8633bca8aaa7214fb6\",\"274877906944\":\"02b8f53dde126f8c85fa5bb6061c0be5aca90984ce9b902966941caf963648d53a\",\"549755813888\":\"029cc8af2840d59f1d8761779b2496623c82c64be8e15f9ab577c657c6dd453785\",\"1099511627776\":\"03e446fdb84fad492ff3a25fc1046fb9a93a5b262ebcd0151caa442ea28959a38a\",\"2199023255552\":\"02d6b25bd4ab599dd0818c55f75702fde603c93f259222001246569018842d3258\",\"4398046511104\":\"03397b522bb4e156ec3952d3f048e5a986c20a00718e5e52cd5718466bf494156a\",\"8796093022208\":\"02d1fb9e78262b5d7d74028073075b80bb5ab281edcfc3191061962c1346340f1e\",\"17592186044416\":\"030d3f2ad7a4ca115712ff7f140434f802b19a4c9b2dd1c76f3e8e80c05c6a9310\",\"35184372088832\":\"03e325b691f292e1dfb151c3fb7cad440b225795583c32e24e10635a80e4221c06\",\"70368744177664\":\"03bee8f64d88de3dee21d61f89efa32933da51152ddbd67466bef815e9f93f8fd1\",\"140737488355328\":\"0327244c9019a4892e1f04ba3bf95fe43b327479e2d57c25979446cc508cd379ed\",\"281474976710656\":\"02fb58522cd662f2f8b042f8161caae6e45de98283f74d4e99f19b0ea85e08a56d\",\"562949953421312\":\"02adde4b466a9d7e59386b6a701a39717c53f30c4810613c1b55e6b6da43b7bc9a\",\"1125899906842624\":\"038eeda11f78ce05c774f30e393cda075192b890d68590813ff46362548528dca9\",\"2251799813685248\":\"02ec13e0058b196db80f7079d329333b330dc30c000dbdd7397cbbc5a37a664c4f\",\"4503599627370496\":\"02d2d162db63675bd04f7d56df04508840f41e2ad87312a3c93041b494efe80a73\",\"9007199254740992\":\"0356969d6aef2bb40121dbd07c68b6102339f4ea8e674a9008bb69506795998f49\",\"18014398509481984\":\"02f4e667567ebb9f4e6e180a4113bb071c48855f657766bb5e9c776a880335d1d6\",\"36028797018963968\":\"0385b4fe35e41703d7a657d957c67bb536629de57b7e6ee6fe2130728ef0fc90b0\",\"72057594037927936\":\"02b2bc1968a6fddbcc78fb9903940524824b5f5bed329c6ad48a19b56068c144fd\",\"144115188075855872\":\"02e0dbb24f1d288a693e8a49bc14264d1276be16972131520cf9e055ae92fba19a\",\"288230376151711744\":\"03efe75c106f931a525dc2d653ebedddc413a2c7d8cb9da410893ae7d2fa7d19cc\",\"576460752303423488\":\"02c7ec2bd9508a7fc03f73c7565dc600b30fd86f3d305f8f139c45c404a52d958a\",\"1152921504606846976\":\"035a6679c6b25e68ff4e29d1c7ef87f21e0a8fc574f6a08c1aa45ff352c1d59f06\",\"2305843009213693952\":\"033cdc225962c052d485f7cfbf55a5b2367d200fe1fe4373a347deb4cc99e9a099\",\"4611686018427387904\":\"024a4b806cf413d14b294719090a9da36ba75209c7657135ad09bc65328fba9e6f\",\"9223372036854775808\":\"0377a6fe114e291a8d8e991627c38001c8305b23b9e98b1c7b1893f5cd0dda6cad\"\n}")] + [InlineData( + "00456a94ab4e1c46", + "{\n \"1\":\"03a40f20667ed53513075dc51e715ff2046cad64eb68960632269ba7f0210e38bc\",\"2\":\"03fd4ce5a16b65576145949e6f99f445f8249fee17c606b688b504a849cdc452de\",\"4\":\"02648eccfa4c026960966276fa5a4cae46ce0fd432211a4f449bf84f13aa5f8303\",\"8\":\"02fdfd6796bfeac490cbee12f778f867f0a2c68f6508d17c649759ea0dc3547528\"\n}" + )] + [InlineData( + "000f01df73ea149a", + "{\n \"1\":\"03ba786a2c0745f8c30e490288acd7a72dd53d65afd292ddefa326a4a3fa14c566\",\"2\":\"03361cd8bd1329fea797a6add1cf1990ffcf2270ceb9fc81eeee0e8e9c1bd0cdf5\",\"4\":\"036e378bcf78738ddf68859293c69778035740e41138ab183c94f8fee7572214c7\",\"8\":\"03909d73beaf28edfb283dbeb8da321afd40651e8902fcf5454ecc7d69788626c0\",\"16\":\"028a36f0e6638ea7466665fe174d958212723019ec08f9ce6898d897f88e68aa5d\",\"32\":\"03a97a40e146adee2687ac60c2ba2586a90f970de92a9d0e6cae5a4b9965f54612\",\"64\":\"03ce86f0c197aab181ddba0cfc5c5576e11dfd5164d9f3d4a3fc3ffbbf2e069664\",\"128\":\"0284f2c06d938a6f78794814c687560a0aabab19fe5e6f30ede38e113b132a3cb9\",\"256\":\"03b99f475b68e5b4c0ba809cdecaae64eade2d9787aa123206f91cd61f76c01459\",\"512\":\"03d4db82ea19a44d35274de51f78af0a710925fe7d9e03620b84e3e9976e3ac2eb\",\"1024\":\"031fbd4ba801870871d46cf62228a1b748905ebc07d3b210daf48de229e683f2dc\",\"2048\":\"0276cedb9a3b160db6a158ad4e468d2437f021293204b3cd4bf6247970d8aff54b\",\"4096\":\"02fc6b89b403ee9eb8a7ed457cd3973638080d6e04ca8af7307c965c166b555ea2\",\"8192\":\"0320265583e916d3a305f0d2687fcf2cd4e3cd03a16ea8261fda309c3ec5721e21\",\"16384\":\"036e41de58fdff3cb1d8d713f48c63bc61fa3b3e1631495a444d178363c0d2ed50\",\"32768\":\"0365438f613f19696264300b069d1dad93f0c60a37536b72a8ab7c7366a5ee6c04\",\"65536\":\"02408426cfb6fc86341bac79624ba8708a4376b2d92debdf4134813f866eb57a8d\",\"131072\":\"031063e9f11c94dc778c473e968966eac0e70b7145213fbaff5f7a007e71c65f41\",\"262144\":\"02f2a3e808f9cd168ec71b7f328258d0c1dda250659c1aced14c7f5cf05aab4328\",\"524288\":\"038ac10de9f1ff9395903bb73077e94dbf91e9ef98fd77d9a2debc5f74c575bc86\",\"1048576\":\"0203eaee4db749b0fc7c49870d082024b2c31d889f9bc3b32473d4f1dfa3625788\",\"2097152\":\"033cdb9d36e1e82ae652b7b6a08e0204569ec7ff9ebf85d80a02786dc7fe00b04c\",\"4194304\":\"02c8b73f4e3a470ae05e5f2fe39984d41e9f6ae7be9f3b09c9ac31292e403ac512\",\"8388608\":\"025bbe0cfce8a1f4fbd7f3a0d4a09cb6badd73ef61829dc827aa8a98c270bc25b0\",\"16777216\":\"037eec3d1651a30a90182d9287a5c51386fe35d4a96839cf7969c6e2a03db1fc21\",\"33554432\":\"03280576b81a04e6abd7197f305506476f5751356b7643988495ca5c3e14e5c262\",\"67108864\":\"03268bfb05be1dbb33ab6e7e00e438373ca2c9b9abc018fdb452d0e1a0935e10d3\",\"134217728\":\"02573b68784ceba9617bbcc7c9487836d296aa7c628c3199173a841e7a19798020\",\"268435456\":\"0234076b6e70f7fbf755d2227ecc8d8169d662518ee3a1401f729e2a12ccb2b276\",\"536870912\":\"03015bd88961e2a466a2163bd4248d1d2b42c7c58a157e594785e7eb34d880efc9\",\"1073741824\":\"02c9b076d08f9020ebee49ac8ba2610b404d4e553a4f800150ceb539e9421aaeee\",\"2147483648\":\"034d592f4c366afddc919a509600af81b489a03caf4f7517c2b3f4f2b558f9a41a\",\"4294967296\":\"037c09ecb66da082981e4cbdb1ac65c0eb631fc75d85bed13efb2c6364148879b5\",\"8589934592\":\"02b4ebb0dda3b9ad83b39e2e31024b777cc0ac205a96b9a6cfab3edea2912ed1b3\",\"17179869184\":\"026cc4dacdced45e63f6e4f62edbc5779ccd802e7fabb82d5123db879b636176e9\",\"34359738368\":\"02b2cee01b7d8e90180254459b8f09bbea9aad34c3a2fd98c85517ecfc9805af75\",\"68719476736\":\"037a0c0d564540fc574b8bfa0253cca987b75466e44b295ed59f6f8bd41aace754\",\"137438953472\":\"021df6585cae9b9ca431318a713fd73dbb76b3ef5667957e8633bca8aaa7214fb6\",\"274877906944\":\"02b8f53dde126f8c85fa5bb6061c0be5aca90984ce9b902966941caf963648d53a\",\"549755813888\":\"029cc8af2840d59f1d8761779b2496623c82c64be8e15f9ab577c657c6dd453785\",\"1099511627776\":\"03e446fdb84fad492ff3a25fc1046fb9a93a5b262ebcd0151caa442ea28959a38a\",\"2199023255552\":\"02d6b25bd4ab599dd0818c55f75702fde603c93f259222001246569018842d3258\",\"4398046511104\":\"03397b522bb4e156ec3952d3f048e5a986c20a00718e5e52cd5718466bf494156a\",\"8796093022208\":\"02d1fb9e78262b5d7d74028073075b80bb5ab281edcfc3191061962c1346340f1e\",\"17592186044416\":\"030d3f2ad7a4ca115712ff7f140434f802b19a4c9b2dd1c76f3e8e80c05c6a9310\",\"35184372088832\":\"03e325b691f292e1dfb151c3fb7cad440b225795583c32e24e10635a80e4221c06\",\"70368744177664\":\"03bee8f64d88de3dee21d61f89efa32933da51152ddbd67466bef815e9f93f8fd1\",\"140737488355328\":\"0327244c9019a4892e1f04ba3bf95fe43b327479e2d57c25979446cc508cd379ed\",\"281474976710656\":\"02fb58522cd662f2f8b042f8161caae6e45de98283f74d4e99f19b0ea85e08a56d\",\"562949953421312\":\"02adde4b466a9d7e59386b6a701a39717c53f30c4810613c1b55e6b6da43b7bc9a\",\"1125899906842624\":\"038eeda11f78ce05c774f30e393cda075192b890d68590813ff46362548528dca9\",\"2251799813685248\":\"02ec13e0058b196db80f7079d329333b330dc30c000dbdd7397cbbc5a37a664c4f\",\"4503599627370496\":\"02d2d162db63675bd04f7d56df04508840f41e2ad87312a3c93041b494efe80a73\",\"9007199254740992\":\"0356969d6aef2bb40121dbd07c68b6102339f4ea8e674a9008bb69506795998f49\",\"18014398509481984\":\"02f4e667567ebb9f4e6e180a4113bb071c48855f657766bb5e9c776a880335d1d6\",\"36028797018963968\":\"0385b4fe35e41703d7a657d957c67bb536629de57b7e6ee6fe2130728ef0fc90b0\",\"72057594037927936\":\"02b2bc1968a6fddbcc78fb9903940524824b5f5bed329c6ad48a19b56068c144fd\",\"144115188075855872\":\"02e0dbb24f1d288a693e8a49bc14264d1276be16972131520cf9e055ae92fba19a\",\"288230376151711744\":\"03efe75c106f931a525dc2d653ebedddc413a2c7d8cb9da410893ae7d2fa7d19cc\",\"576460752303423488\":\"02c7ec2bd9508a7fc03f73c7565dc600b30fd86f3d305f8f139c45c404a52d958a\",\"1152921504606846976\":\"035a6679c6b25e68ff4e29d1c7ef87f21e0a8fc574f6a08c1aa45ff352c1d59f06\",\"2305843009213693952\":\"033cdc225962c052d485f7cfbf55a5b2367d200fe1fe4373a347deb4cc99e9a099\",\"4611686018427387904\":\"024a4b806cf413d14b294719090a9da36ba75209c7657135ad09bc65328fba9e6f\",\"9223372036854775808\":\"0377a6fe114e291a8d8e991627c38001c8305b23b9e98b1c7b1893f5cd0dda6cad\"\n}" + )] // v2 - [InlineData("015ba18a8adcd02e715a58358eb618da4a4b3791151a4bee5e968bb88406ccf76a", + [InlineData( + "015ba18a8adcd02e715a58358eb618da4a4b3791151a4bee5e968bb88406ccf76a", "{\n \"1\": \"03a40f20667ed53513075dc51e715ff2046cad64eb68960632269ba7f0210e38bc\",\n \"2\": \"03fd4ce5a16b65576145949e6f99f445f8249fee17c606b688b504a849cdc452de\",\n \"4\": \"02648eccfa4c026960966276fa5a4cae46ce0fd432211a4f449bf84f13aa5f8303\",\n \"8\": \"02fdfd6796bfeac490cbee12f778f867f0a2c68f6508d17c649759ea0dc3547528\"\n}", - (byte)1, + (byte)1, "sat", - 100UL, + 100UL, "2059210353" - )] - [InlineData("01ab6aa4ff30390da34986d84be5274b48ad7a74265d791095bfc39f4098d9764f", + )] + [InlineData( + "01ab6aa4ff30390da34986d84be5274b48ad7a74265d791095bfc39f4098d9764f", "{\n \"1\": \"03ba786a2c0745f8c30e490288acd7a72dd53d65afd292ddefa326a4a3fa14c566\",\n \"2\": \"03361cd8bd1329fea797a6add1cf1990ffcf2270ceb9fc81eeee0e8e9c1bd0cdf5\",\n \"4\": \"036e378bcf78738ddf68859293c69778035740e41138ab183c94f8fee7572214c7\",\n \"8\": \"03909d73beaf28edfb283dbeb8da321afd40651e8902fcf5454ecc7d69788626c0\",\n \"16\": \"028a36f0e6638ea7466665fe174d958212723019ec08f9ce6898d897f88e68aa5d\",\n \"32\": \"03a97a40e146adee2687ac60c2ba2586a90f970de92a9d0e6cae5a4b9965f54612\",\n \"64\": \"03ce86f0c197aab181ddba0cfc5c5576e11dfd5164d9f3d4a3fc3ffbbf2e069664\",\n \"128\": \"0284f2c06d938a6f78794814c687560a0aabab19fe5e6f30ede38e113b132a3cb9\",\n \"256\": \"03b99f475b68e5b4c0ba809cdecaae64eade2d9787aa123206f91cd61f76c01459\",\n \"512\": \"03d4db82ea19a44d35274de51f78af0a710925fe7d9e03620b84e3e9976e3ac2eb\",\n \"1024\": \"031fbd4ba801870871d46cf62228a1b748905ebc07d3b210daf48de229e683f2dc\",\n \"2048\": \"0276cedb9a3b160db6a158ad4e468d2437f021293204b3cd4bf6247970d8aff54b\",\n \"4096\": \"02fc6b89b403ee9eb8a7ed457cd3973638080d6e04ca8af7307c965c166b555ea2\",\n \"8192\": \"0320265583e916d3a305f0d2687fcf2cd4e3cd03a16ea8261fda309c3ec5721e21\",\n \"16384\": \"036e41de58fdff3cb1d8d713f48c63bc61fa3b3e1631495a444d178363c0d2ed50\",\n \"32768\": \"0365438f613f19696264300b069d1dad93f0c60a37536b72a8ab7c7366a5ee6c04\",\n \"65536\": \"02408426cfb6fc86341bac79624ba8708a4376b2d92debdf4134813f866eb57a8d\",\n \"131072\": \"031063e9f11c94dc778c473e968966eac0e70b7145213fbaff5f7a007e71c65f41\",\n \"262144\": \"02f2a3e808f9cd168ec71b7f328258d0c1dda250659c1aced14c7f5cf05aab4328\",\n \"524288\": \"038ac10de9f1ff9395903bb73077e94dbf91e9ef98fd77d9a2debc5f74c575bc86\",\n \"1048576\": \"0203eaee4db749b0fc7c49870d082024b2c31d889f9bc3b32473d4f1dfa3625788\",\n \"2097152\": \"033cdb9d36e1e82ae652b7b6a08e0204569ec7ff9ebf85d80a02786dc7fe00b04c\",\n \"4194304\": \"02c8b73f4e3a470ae05e5f2fe39984d41e9f6ae7be9f3b09c9ac31292e403ac512\",\n \"8388608\": \"025bbe0cfce8a1f4fbd7f3a0d4a09cb6badd73ef61829dc827aa8a98c270bc25b0\",\n \"16777216\": \"037eec3d1651a30a90182d9287a5c51386fe35d4a96839cf7969c6e2a03db1fc21\",\n \"33554432\": \"03280576b81a04e6abd7197f305506476f5751356b7643988495ca5c3e14e5c262\",\n \"67108864\": \"03268bfb05be1dbb33ab6e7e00e438373ca2c9b9abc018fdb452d0e1a0935e10d3\",\n \"134217728\": \"02573b68784ceba9617bbcc7c9487836d296aa7c628c3199173a841e7a19798020\",\n \"268435456\": \"0234076b6e70f7fbf755d2227ecc8d8169d662518ee3a1401f729e2a12ccb2b276\",\n \"536870912\": \"03015bd88961e2a466a2163bd4248d1d2b42c7c58a157e594785e7eb34d880efc9\",\n \"1073741824\": \"02c9b076d08f9020ebee49ac8ba2610b404d4e553a4f800150ceb539e9421aaeee\",\n \"2147483648\": \"034d592f4c366afddc919a509600af81b489a03caf4f7517c2b3f4f2b558f9a41a\",\n \"4294967296\": \"037c09ecb66da082981e4cbdb1ac65c0eb631fc75d85bed13efb2c6364148879b5\",\n \"8589934592\": \"02b4ebb0dda3b9ad83b39e2e31024b777cc0ac205a96b9a6cfab3edea2912ed1b3\",\n \"17179869184\": \"026cc4dacdced45e63f6e4f62edbc5779ccd802e7fabb82d5123db879b636176e9\",\n \"34359738368\": \"02b2cee01b7d8e90180254459b8f09bbea9aad34c3a2fd98c85517ecfc9805af75\",\n \"68719476736\": \"037a0c0d564540fc574b8bfa0253cca987b75466e44b295ed59f6f8bd41aace754\",\n \"137438953472\": \"021df6585cae9b9ca431318a713fd73dbb76b3ef5667957e8633bca8aaa7214fb6\",\n \"274877906944\": \"02b8f53dde126f8c85fa5bb6061c0be5aca90984ce9b902966941caf963648d53a\",\n \"549755813888\": \"029cc8af2840d59f1d8761779b2496623c82c64be8e15f9ab577c657c6dd453785\",\n \"1099511627776\": \"03e446fdb84fad492ff3a25fc1046fb9a93a5b262ebcd0151caa442ea28959a38a\",\n \"2199023255552\": \"02d6b25bd4ab599dd0818c55f75702fde603c93f259222001246569018842d3258\",\n \"4398046511104\": \"03397b522bb4e156ec3952d3f048e5a986c20a00718e5e52cd5718466bf494156a\",\n \"8796093022208\": \"02d1fb9e78262b5d7d74028073075b80bb5ab281edcfc3191061962c1346340f1e\",\n \"17592186044416\": \"030d3f2ad7a4ca115712ff7f140434f802b19a4c9b2dd1c76f3e8e80c05c6a9310\",\n \"35184372088832\": \"03e325b691f292e1dfb151c3fb7cad440b225795583c32e24e10635a80e4221c06\",\n \"70368744177664\": \"03bee8f64d88de3dee21d61f89efa32933da51152ddbd67466bef815e9f93f8fd1\",\n \"140737488355328\": \"0327244c9019a4892e1f04ba3bf95fe43b327479e2d57c25979446cc508cd379ed\",\n \"281474976710656\": \"02fb58522cd662f2f8b042f8161caae6e45de98283f74d4e99f19b0ea85e08a56d\",\n \"562949953421312\": \"02adde4b466a9d7e59386b6a701a39717c53f30c4810613c1b55e6b6da43b7bc9a\",\n \"1125899906842624\": \"038eeda11f78ce05c774f30e393cda075192b890d68590813ff46362548528dca9\",\n \"2251799813685248\": \"02ec13e0058b196db80f7079d329333b330dc30c000dbdd7397cbbc5a37a664c4f\",\n \"4503599627370496\": \"02d2d162db63675bd04f7d56df04508840f41e2ad87312a3c93041b494efe80a73\",\n \"9007199254740992\": \"0356969d6aef2bb40121dbd07c68b6102339f4ea8e674a9008bb69506795998f49\",\n \"18014398509481984\": \"02f4e667567ebb9f4e6e180a4113bb071c48855f657766bb5e9c776a880335d1d6\",\n \"36028797018963968\": \"0385b4fe35e41703d7a657d957c67bb536629de57b7e6ee6fe2130728ef0fc90b0\",\n \"72057594037927936\": \"02b2bc1968a6fddbcc78fb9903940524824b5f5bed329c6ad48a19b56068c144fd\",\n \"144115188075855872\": \"02e0dbb24f1d288a693e8a49bc14264d1276be16972131520cf9e055ae92fba19a\",\n \"288230376151711744\": \"03efe75c106f931a525dc2d653ebedddc413a2c7d8cb9da410893ae7d2fa7d19cc\",\n \"576460752303423488\": \"02c7ec2bd9508a7fc03f73c7565dc600b30fd86f3d305f8f139c45c404a52d958a\",\n \"1152921504606846976\": \"035a6679c6b25e68ff4e29d1c7ef87f21e0a8fc574f6a08c1aa45ff352c1d59f06\",\n \"2305843009213693952\": \"033cdc225962c052d485f7cfbf55a5b2367d200fe1fe4373a347deb4cc99e9a099\",\n \"4611686018427387904\": \"024a4b806cf413d14b294719090a9da36ba75209c7657135ad09bc65328fba9e6f\",\n \"9223372036854775808\": \"0377a6fe114e291a8d8e991627c38001c8305b23b9e98b1c7b1893f5cd0dda6cad\"\n}", (byte)0x01, "sat", 0UL, "2059210353" - )] - [InlineData("012fbb01a4e200c76df911eeba3b8fe1831202914b24664f4bccbd25852a6708f8", + )] + [InlineData( + "012fbb01a4e200c76df911eeba3b8fe1831202914b24664f4bccbd25852a6708f8", "{\n \"1\": \"03ba786a2c0745f8c30e490288acd7a72dd53d65afd292ddefa326a4a3fa14c566\",\n \"2\": \"03361cd8bd1329fea797a6add1cf1990ffcf2270ceb9fc81eeee0e8e9c1bd0cdf5\",\n \"4\": \"036e378bcf78738ddf68859293c69778035740e41138ab183c94f8fee7572214c7\",\n \"8\": \"03909d73beaf28edfb283dbeb8da321afd40651e8902fcf5454ecc7d69788626c0\",\n \"16\": \"028a36f0e6638ea7466665fe174d958212723019ec08f9ce6898d897f88e68aa5d\",\n \"32\": \"03a97a40e146adee2687ac60c2ba2586a90f970de92a9d0e6cae5a4b9965f54612\",\n \"64\": \"03ce86f0c197aab181ddba0cfc5c5576e11dfd5164d9f3d4a3fc3ffbbf2e069664\",\n \"128\": \"0284f2c06d938a6f78794814c687560a0aabab19fe5e6f30ede38e113b132a3cb9\",\n \"256\": \"03b99f475b68e5b4c0ba809cdecaae64eade2d9787aa123206f91cd61f76c01459\",\n \"512\": \"03d4db82ea19a44d35274de51f78af0a710925fe7d9e03620b84e3e9976e3ac2eb\",\n \"1024\": \"031fbd4ba801870871d46cf62228a1b748905ebc07d3b210daf48de229e683f2dc\",\n \"2048\": \"0276cedb9a3b160db6a158ad4e468d2437f021293204b3cd4bf6247970d8aff54b\",\n \"4096\": \"02fc6b89b403ee9eb8a7ed457cd3973638080d6e04ca8af7307c965c166b555ea2\",\n \"8192\": \"0320265583e916d3a305f0d2687fcf2cd4e3cd03a16ea8261fda309c3ec5721e21\",\n \"16384\": \"036e41de58fdff3cb1d8d713f48c63bc61fa3b3e1631495a444d178363c0d2ed50\",\n \"32768\": \"0365438f613f19696264300b069d1dad93f0c60a37536b72a8ab7c7366a5ee6c04\",\n \"65536\": \"02408426cfb6fc86341bac79624ba8708a4376b2d92debdf4134813f866eb57a8d\",\n \"131072\": \"031063e9f11c94dc778c473e968966eac0e70b7145213fbaff5f7a007e71c65f41\",\n \"262144\": \"02f2a3e808f9cd168ec71b7f328258d0c1dda250659c1aced14c7f5cf05aab4328\",\n \"524288\": \"038ac10de9f1ff9395903bb73077e94dbf91e9ef98fd77d9a2debc5f74c575bc86\",\n \"1048576\": \"0203eaee4db749b0fc7c49870d082024b2c31d889f9bc3b32473d4f1dfa3625788\",\n \"2097152\": \"033cdb9d36e1e82ae652b7b6a08e0204569ec7ff9ebf85d80a02786dc7fe00b04c\",\n \"4194304\": \"02c8b73f4e3a470ae05e5f2fe39984d41e9f6ae7be9f3b09c9ac31292e403ac512\",\n \"8388608\": \"025bbe0cfce8a1f4fbd7f3a0d4a09cb6badd73ef61829dc827aa8a98c270bc25b0\",\n \"16777216\": \"037eec3d1651a30a90182d9287a5c51386fe35d4a96839cf7969c6e2a03db1fc21\",\n \"33554432\": \"03280576b81a04e6abd7197f305506476f5751356b7643988495ca5c3e14e5c262\",\n \"67108864\": \"03268bfb05be1dbb33ab6e7e00e438373ca2c9b9abc018fdb452d0e1a0935e10d3\",\n \"134217728\": \"02573b68784ceba9617bbcc7c9487836d296aa7c628c3199173a841e7a19798020\",\n \"268435456\": \"0234076b6e70f7fbf755d2227ecc8d8169d662518ee3a1401f729e2a12ccb2b276\",\n \"536870912\": \"03015bd88961e2a466a2163bd4248d1d2b42c7c58a157e594785e7eb34d880efc9\",\n \"1073741824\": \"02c9b076d08f9020ebee49ac8ba2610b404d4e553a4f800150ceb539e9421aaeee\",\n \"2147483648\": \"034d592f4c366afddc919a509600af81b489a03caf4f7517c2b3f4f2b558f9a41a\",\n \"4294967296\": \"037c09ecb66da082981e4cbdb1ac65c0eb631fc75d85bed13efb2c6364148879b5\",\n \"8589934592\": \"02b4ebb0dda3b9ad83b39e2e31024b777cc0ac205a96b9a6cfab3edea2912ed1b3\",\n \"17179869184\": \"026cc4dacdced45e63f6e4f62edbc5779ccd802e7fabb82d5123db879b636176e9\",\n \"34359738368\": \"02b2cee01b7d8e90180254459b8f09bbea9aad34c3a2fd98c85517ecfc9805af75\",\n \"68719476736\": \"037a0c0d564540fc574b8bfa0253cca987b75466e44b295ed59f6f8bd41aace754\",\n \"137438953472\": \"021df6585cae9b9ca431318a713fd73dbb76b3ef5667957e8633bca8aaa7214fb6\",\n \"274877906944\": \"02b8f53dde126f8c85fa5bb6061c0be5aca90984ce9b902966941caf963648d53a\",\n \"549755813888\": \"029cc8af2840d59f1d8761779b2496623c82c64be8e15f9ab577c657c6dd453785\",\n \"1099511627776\": \"03e446fdb84fad492ff3a25fc1046fb9a93a5b262ebcd0151caa442ea28959a38a\",\n \"2199023255552\": \"02d6b25bd4ab599dd0818c55f75702fde603c93f259222001246569018842d3258\",\n \"4398046511104\": \"03397b522bb4e156ec3952d3f048e5a986c20a00718e5e52cd5718466bf494156a\",\n \"8796093022208\": \"02d1fb9e78262b5d7d74028073075b80bb5ab281edcfc3191061962c1346340f1e\",\n \"17592186044416\": \"030d3f2ad7a4ca115712ff7f140434f802b19a4c9b2dd1c76f3e8e80c05c6a9310\",\n \"35184372088832\": \"03e325b691f292e1dfb151c3fb7cad440b225795583c32e24e10635a80e4221c06\",\n \"70368744177664\": \"03bee8f64d88de3dee21d61f89efa32933da51152ddbd67466bef815e9f93f8fd1\",\n \"140737488355328\": \"0327244c9019a4892e1f04ba3bf95fe43b327479e2d57c25979446cc508cd379ed\",\n \"281474976710656\": \"02fb58522cd662f2f8b042f8161caae6e45de98283f74d4e99f19b0ea85e08a56d\",\n \"562949953421312\": \"02adde4b466a9d7e59386b6a701a39717c53f30c4810613c1b55e6b6da43b7bc9a\",\n \"1125899906842624\": \"038eeda11f78ce05c774f30e393cda075192b890d68590813ff46362548528dca9\",\n \"2251799813685248\": \"02ec13e0058b196db80f7079d329333b330dc30c000dbdd7397cbbc5a37a664c4f\",\n \"4503599627370496\": \"02d2d162db63675bd04f7d56df04508840f41e2ad87312a3c93041b494efe80a73\",\n \"9007199254740992\": \"0356969d6aef2bb40121dbd07c68b6102339f4ea8e674a9008bb69506795998f49\",\n \"18014398509481984\": \"02f4e667567ebb9f4e6e180a4113bb071c48855f657766bb5e9c776a880335d1d6\",\n \"36028797018963968\": \"0385b4fe35e41703d7a657d957c67bb536629de57b7e6ee6fe2130728ef0fc90b0\",\n \"72057594037927936\": \"02b2bc1968a6fddbcc78fb9903940524824b5f5bed329c6ad48a19b56068c144fd\",\n \"144115188075855872\": \"02e0dbb24f1d288a693e8a49bc14264d1276be16972131520cf9e055ae92fba19a\",\n \"288230376151711744\": \"03efe75c106f931a525dc2d653ebedddc413a2c7d8cb9da410893ae7d2fa7d19cc\",\n \"576460752303423488\": \"02c7ec2bd9508a7fc03f73c7565dc600b30fd86f3d305f8f139c45c404a52d958a\",\n \"1152921504606846976\": \"035a6679c6b25e68ff4e29d1c7ef87f21e0a8fc574f6a08c1aa45ff352c1d59f06\",\n \"2305843009213693952\": \"033cdc225962c052d485f7cfbf55a5b2367d200fe1fe4373a347deb4cc99e9a099\",\n \"4611686018427387904\": \"024a4b806cf413d14b294719090a9da36ba75209c7657135ad09bc65328fba9e6f\",\n \"9223372036854775808\": \"0377a6fe114e291a8d8e991627c38001c8305b23b9e98b1c7b1893f5cd0dda6cad\"\n}", (byte)0x01, "sat", - 0UL)] - public void Nut02Tests_KeysetIdMatch(string keysetId, string keyset, byte? version = null, string? unit = null, ulong? inputFee = null, string? finalExpiration = null) + 0UL + )] + public void Nut02Tests_KeysetIdMatch( + string keysetId, + string keyset, + byte? version = null, + string? unit = null, + ulong? inputFee = null, + string? finalExpiration = null + ) { var keysetIdParsed = new KeysetId(keysetId); var keysetParsed = JsonSerializer.Deserialize(keyset); - Assert.Equal(keysetIdParsed, keysetParsed.GetKeysetId(version ?? 0x00, unit, inputFee, finalExpiration)); + Assert.Equal( + keysetIdParsed, + keysetParsed.GetKeysetId(version ?? 0x00, unit, inputFee, finalExpiration) + ); } [Theory] @@ -213,23 +275,25 @@ public void Nut02Tests_KeysetIdVersion(string keysetId, byte version) var keysetIdParsed = new KeysetId(keysetId); Assert.Equal(version, keysetIdParsed.GetVersion()); } - - [Fact] public void Nut04Tests_Proofs_1() { var a = "0000000000000000000000000000000000000000000000000000000000000001".ToPrivKey(); var A = a.CreatePubKey(); - Assert.Equal("0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798".ToPubKey(), A); + Assert.Equal( + "0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798".ToPubKey(), + A + ); var message = new StringSecret("secret_msg"); - var blindingFactor = "0000000000000000000000000000000000000000000000000000000000000001".ToPrivKey(); + var blindingFactor = + "0000000000000000000000000000000000000000000000000000000000000001".ToPrivKey(); // var Y = Cashu.MessageToCurve(message); var Y = message.ToCurve(); var B_ = Cashu.ComputeB_(Y, blindingFactor); var C_ = Cashu.ComputeC_(B_, a); - //p doesn;t have to be blinding factor. in fact it should be random nonce - + //p doesn;t have to be blinding factor. in fact it should be random nonce + var proof = Cashu.ComputeProof(B_, a, blindingFactor); Cashu.VerifyProof(B_, C_, proof.e, proof.s, A); var C = Cashu.ComputeC(C_, blindingFactor, A); @@ -237,12 +301,12 @@ public void Nut04Tests_Proofs_1() Cashu.VerifyProof(Y, blindingFactor, C, proof.e, proof.s, A); } - [Fact] public void Nut04Tests_Proofs_2() { var A = "0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798".ToPubKey(); - var proof = JsonSerializer.Deserialize(@" + var proof = JsonSerializer.Deserialize( + @" { ""amount"": 1, @@ -256,32 +320,48 @@ public void Nut04Tests_Proofs_2() } } -"); +" + ); Assert.NotNull(proof?.DLEQ); - Cashu.VerifyProof(Cashu.HexToCurve(Assert.IsType(proof.Secret).Secret), proof.DLEQ.R, proof.C, - proof.DLEQ.E, proof.DLEQ.S, A); + Cashu.VerifyProof( + Cashu.HexToCurve(Assert.IsType(proof.Secret).Secret), + proof.DLEQ.R, + proof.C, + proof.DLEQ.E, + proof.DLEQ.S, + A + ); } [Fact] public void Nut11_Signatures() { - var secretKey = - ECPrivKey.Create(Convert.FromHexString("99590802251e78ee1051648439eedb003dc539093a48a44e7b8f2642c909ea37")); + var secretKey = ECPrivKey.Create( + Convert.FromHexString( + "99590802251e78ee1051648439eedb003dc539093a48a44e7b8f2642c909ea37" + ) + ); - var signing_key_two = - ECPrivKey.Create(Convert.FromHexString("0000000000000000000000000000000000000000000000000000000000000001")); + var signing_key_two = ECPrivKey.Create( + Convert.FromHexString( + "0000000000000000000000000000000000000000000000000000000000000001" + ) + ); - var signing_key_three = - ECPrivKey.Create(Convert.FromHexString("7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f")); + var signing_key_three = ECPrivKey.Create( + Convert.FromHexString( + "7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f" + ) + ); var conditions = new P2PkBuilder { Lock = DateTimeOffset.FromUnixTimeSeconds(21000000000), - Pubkeys = new[] {signing_key_two.CreatePubKey(), signing_key_three.CreatePubKey()}, - RefundPubkeys = new[] {secretKey.CreatePubKey()}, + Pubkeys = new[] { signing_key_two.CreatePubKey(), signing_key_three.CreatePubKey() }, + RefundPubkeys = new[] { secretKey.CreatePubKey() }, SignatureThreshold = 2, - SigFlag = "SIG_INPUTS" + SigFlag = "SIG_INPUTS", }; var p2pkProofSecret = conditions.Build(); @@ -294,10 +374,13 @@ public void Nut11_Signatures() Secret = secret, C = "02698c4e2b5f9534cd0687d87513c759790cf829aa5739184a3e3735471fbda904".ToPubKey(), }; - var witness = p2pkProofSecret.GenerateWitness(proof, new[] {signing_key_two, signing_key_three}); + var witness = p2pkProofSecret.GenerateWitness( + proof, + new[] { signing_key_two, signing_key_three } + ); proof.Witness = JsonSerializer.Serialize(witness); Assert.True(p2pkProofSecret.VerifyWitness(proof.Secret, witness)); - + // SIG_INPUTS var valid1 = @@ -305,60 +388,103 @@ public void Nut11_Signatures() var valid1Proof = JsonSerializer.Deserialize(valid1); var valid1ProofSecret = Assert.IsType(valid1Proof.Secret); Assert.Equal(P2PKProofSecret.Key, valid1ProofSecret!.Key); - var valid1ProofSecretp2pkValue = Assert.IsType(valid1ProofSecret.ProofSecret); + var valid1ProofSecretp2pkValue = Assert.IsType( + valid1ProofSecret.ProofSecret + ); var valid1ProofWitnessP2pk = JsonSerializer.Deserialize(valid1Proof.Witness); - Assert.True(valid1ProofSecretp2pkValue.VerifyWitness(valid1Proof.Secret, valid1ProofWitnessP2pk)); + Assert.True( + valid1ProofSecretp2pkValue.VerifyWitness(valid1Proof.Secret, valid1ProofWitnessP2pk) + ); var invalid1 = "{\n \"amount\": 1,\n \"secret\": \"[\\\"P2PK\\\",{\\\"nonce\\\":\\\"859d4935c4907062a6297cf4e663e2835d90d97ecdd510745d32f6816323a41f\\\",\\\"data\\\":\\\"0249098aa8b9d2fbec49ff8598feb17b592b986e62319a4fa488a3dc36387157a7\\\",\\\"tags\\\":[[\\\"sigflag\\\",\\\"SIG_INPUTS\\\"]]}]\",\n \"C\": \"02698c4e2b5f9534cd0687d87513c759790cf829aa5739184a3e3735471fbda904\",\n \"id\": \"009a1f293253e41e\",\n \"witness\": \"{\\\"signatures\\\":[\\\"83564aca48c668f50d022a426ce0ed19d3a9bdcffeeaee0dc1e7ea7e98e9eff1840fcc821724f623468c94f72a8b0a7280fa9ef5a54a1b130ef3055217f467b3\\\"]}\"\n}"; var invalid1Proof = JsonSerializer.Deserialize(invalid1); var invalid1ProofSecret = Assert.IsType(invalid1Proof.Secret); Assert.Equal(P2PKProofSecret.Key, invalid1ProofSecret!.Key); - var invalid1ProofSecretp2pkValue = Assert.IsType(invalid1ProofSecret.ProofSecret); - var invalid1ProofWitnessP2pk = JsonSerializer.Deserialize(invalid1Proof.Witness); - Assert.False(invalid1ProofSecretp2pkValue.VerifyWitness(invalid1Proof.Secret, invalid1ProofWitnessP2pk)); + var invalid1ProofSecretp2pkValue = Assert.IsType( + invalid1ProofSecret.ProofSecret + ); + var invalid1ProofWitnessP2pk = JsonSerializer.Deserialize( + invalid1Proof.Witness + ); + Assert.False( + invalid1ProofSecretp2pkValue.VerifyWitness( + invalid1Proof.Secret, + invalid1ProofWitnessP2pk + ) + ); var validMultisig = "{\"amount\":1,\"secret\":\"[\\\"P2PK\\\",{\\\"nonce\\\":\\\"0ed3fcb22c649dd7bbbdcca36e0c52d4f0187dd3b6a19efcc2bfbebb5f85b2a1\\\",\\\"data\\\":\\\"0249098aa8b9d2fbec49ff8598feb17b592b986e62319a4fa488a3dc36387157a7\\\",\\\"tags\\\":[[\\\"pubkeys\\\",\\\"0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798\\\",\\\"02142715675faf8da1ecc4d51e0b9e539fa0d52fdd96ed60dbe99adb15d6b05ad9\\\"],[\\\"n_sigs\\\",\\\"2\\\"],[\\\"sigflag\\\",\\\"SIG_INPUTS\\\"]]}]\",\"C\":\"02698c4e2b5f9534cd0687d87513c759790cf829aa5739184a3e3735471fbda904\",\"id\":\"009a1f293253e41e\",\"witness\":\"{\\\"signatures\\\":[\\\"83564aca48c668f50d022a426ce0ed19d3a9bdcffeeaee0dc1e7ea7e98e9eff1840fcc821724f623468c94f72a8b0a7280fa9ef5a54a1b130ef3055217f467b3\\\",\\\"9a72ca2d4d5075be5b511ee48dbc5e45f259bcf4a4e8bf18587f433098a9cd61ff9737dc6e8022de57c76560214c4568377792d4c2c6432886cc7050487a1f22\\\"]}\"}"; var validMultisigProof = JsonSerializer.Deserialize(validMultisig); var validMultisigProofSecret = Assert.IsType(validMultisigProof.Secret); Assert.Equal(P2PKProofSecret.Key, validMultisigProofSecret!.Key); - var validMultisigProofSecretp2pkValue = Assert.IsType(validMultisigProofSecret.ProofSecret); - var validMultisigProofWitnessP2pk = JsonSerializer.Deserialize(validMultisigProof.Witness); + var validMultisigProofSecretp2pkValue = Assert.IsType( + validMultisigProofSecret.ProofSecret + ); + var validMultisigProofWitnessP2pk = JsonSerializer.Deserialize( + validMultisigProof.Witness + ); Assert.True( - validMultisigProofSecretp2pkValue.VerifyWitness(validMultisigProof.Secret, validMultisigProofWitnessP2pk)); + validMultisigProofSecretp2pkValue.VerifyWitness( + validMultisigProof.Secret, + validMultisigProofWitnessP2pk + ) + ); var invalidMultisig = "{\"amount\":1,\"secret\":\"[\\\"P2PK\\\",{\\\"nonce\\\":\\\"0ed3fcb22c649dd7bbbdcca36e0c52d4f0187dd3b6a19efcc2bfbebb5f85b2a1\\\",\\\"data\\\":\\\"0249098aa8b9d2fbec49ff8598feb17b592b986e62319a4fa488a3dc36387157a7\\\",\\\"tags\\\":[[\\\"pubkeys\\\",\\\"0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798\\\",\\\"02142715675faf8da1ecc4d51e0b9e539fa0d52fdd96ed60dbe99adb15d6b05ad9\\\"],[\\\"n_sigs\\\",\\\"2\\\"],[\\\"sigflag\\\",\\\"SIG_INPUTS\\\"]]}]\",\"C\":\"02698c4e2b5f9534cd0687d87513c759790cf829aa5739184a3e3735471fbda904\",\"id\":\"009a1f293253e41e\",\"witness\":\"{\\\"signatures\\\":[\\\"83564aca48c668f50d022a426ce0ed19d3a9bdcffeeaee0dc1e7ea7e98e9eff1840fcc821724f623468c94f72a8b0a7280fa9ef5a54a1b130ef3055217f467b3\\\"]}\"}"; var invalidMultisigProof = JsonSerializer.Deserialize(invalidMultisig); var invalidMultisigProofSecret = Assert.IsType(invalidMultisigProof.Secret); Assert.Equal(P2PKProofSecret.Key, invalidMultisigProofSecret!.Key); - var invalidMultisigProofSecretp2pkValue = - Assert.IsType(invalidMultisigProofSecret.ProofSecret); - var invalidMultisigProofWitnessP2pk = JsonSerializer.Deserialize(invalidMultisigProof.Witness); - Assert.False(invalidMultisigProofSecretp2pkValue.VerifyWitness(invalidMultisigProof.Secret, - invalidMultisigProofWitnessP2pk)); + var invalidMultisigProofSecretp2pkValue = Assert.IsType( + invalidMultisigProofSecret.ProofSecret + ); + var invalidMultisigProofWitnessP2pk = JsonSerializer.Deserialize( + invalidMultisigProof.Witness + ); + Assert.False( + invalidMultisigProofSecretp2pkValue.VerifyWitness( + invalidMultisigProof.Secret, + invalidMultisigProofWitnessP2pk + ) + ); var validProofRefund = "{\n \"amount\": 64,\n \"C\": \"0257353051c02e2d650dede3159915c8be123ba4f47cf33183c7fedd20bd91a79b\",\n \"id\": \"001b6c716bf42c7e\",\n \"secret\": \"[\\\"P2PK\\\",{\\\"nonce\\\":\\\"4bc88ee09d1886c7461d45da205ca3274e1e3d9da2667c4865045cb18265a407\\\",\\\"data\\\":\\\"03d5edeb839be873df2348785506d36565f3b8f390fb931709a422b5a247ddefb1\\\",\\\"tags\\\":[[\\\"locktime\\\",\\\"21\\\"],[\\\"refund\\\",\\\"0234ad87e907e117db1590cc20a3942ffdfd5137aa563d36095d5cf5f96bada122\\\"]]}]\",\n \"witness\": \"{\\\"signatures\\\":[\\\"b316c2ff9c15f0c5c3d230e99ad94bc76a11dfccbdc820366a3db7210288f22ef6cedcded1152904ec31056d1d5176d83a2d96df5cd4ff86afdde1c90c63af5e\\\"]}\"\n}"; var validProofRefundParsed = JsonSerializer.Deserialize(validProofRefund); var validProofRefundSecret = Assert.IsType(validProofRefundParsed.Secret); Assert.Equal(P2PKProofSecret.Key, validProofRefundSecret!.Key); - var validProofRefundSecretp2pkValue = Assert.IsType(validProofRefundSecret.ProofSecret); - var validProofRefundWitnessP2pk = JsonSerializer.Deserialize(validProofRefundParsed.Witness); + var validProofRefundSecretp2pkValue = Assert.IsType( + validProofRefundSecret.ProofSecret + ); + var validProofRefundWitnessP2pk = JsonSerializer.Deserialize( + validProofRefundParsed.Witness + ); Assert.True( - validProofRefundSecretp2pkValue.VerifyWitness(validProofRefundParsed.Secret, validProofRefundWitnessP2pk)); - + validProofRefundSecretp2pkValue.VerifyWitness( + validProofRefundParsed.Secret, + validProofRefundWitnessP2pk + ) + ); var invalidProofRefund = "{\n \"amount\": 64,\n \"C\": \"0215865e3b30bdf6f5cdc1ee2c33379d5629bdf2eff2595603d939ff8c65d80586\",\n \"id\": \"001b6c716bf42c7e\",\n \"secret\": \"[\\\"P2PK\\\",{\\\"nonce\\\":\\\"0c3d085898f1abf2b5521035f4d0f4ecf68c6a5109f6bc836833a1188f06be65\\\",\\\"data\\\":\\\"03206e0d488387a816bbafd957be51b073432c6c7a403ec4c2a0b27647326c5150\\\",\\\"tags\\\":[[\\\"locktime\\\",\\\"99999999999\\\"],[\\\"refund\\\",\\\"026acbcd0fff3a424499c83ec892d3155c9d1984438659f448d9d0f1af3e92276a\\\"]]}]\",\n \"witness\": \"{\\\"signatures\\\":[\\\"e5b10d7627ab39bd0cefa219c63752a0026aa5ae754b91a0c7ee2596222f87942c442aca2957166a6b468350c09c9968792784d2ae7c42fc91739b55689f4c7a\\\"]}\"\n}"; var invalidProofRefundParsed = JsonSerializer.Deserialize(invalidProofRefund); var invalidProofRefundSecret = Assert.IsType(invalidProofRefundParsed.Secret); Assert.Equal(P2PKProofSecret.Key, invalidProofRefundSecret!.Key); - var invalidProofRefundSecretp2pkValue = Assert.IsType(invalidProofRefundSecret.ProofSecret); - var invalidProofRefundWitnessP2pk = JsonSerializer.Deserialize(invalidProofRefundParsed.Witness); - Assert.False(invalidProofRefundSecretp2pkValue.VerifyWitness(invalidProofRefundParsed.Secret, - invalidProofRefundWitnessP2pk)); + var invalidProofRefundSecretp2pkValue = Assert.IsType( + invalidProofRefundSecret.ProofSecret + ); + var invalidProofRefundWitnessP2pk = JsonSerializer.Deserialize( + invalidProofRefundParsed.Witness + ); + Assert.False( + invalidProofRefundSecretp2pkValue.VerifyWitness( + invalidProofRefundParsed.Secret, + invalidProofRefundWitnessP2pk + ) + ); } [Fact] @@ -397,97 +523,267 @@ public void Nut11_SIG_ALL() var swapRequest = "{\n \"inputs\": [\n {\n \"amount\": 2,\n \"id\": \"00bfa73302d12ffd\",\n \"secret\": \"[\\\"P2PK\\\",{\\\"nonce\\\":\\\"c7f280eb55c1e8564e03db06973e94bc9b666d9e1ca42ad278408fe625950303\\\",\\\"data\\\":\\\"030d8acedfe072c9fa449a1efe0817157403fbec460d8e79f957966056e5dd76c1\\\",\\\"tags\\\":[[\\\"sigflag\\\",\\\"SIG_ALL\\\"]]}]\",\n \"C\": \"02c97ee3d1db41cf0a3ddb601724be8711a032950811bf326f8219c50c4808d3cd\",\n \"witness\": \"{\\\"signatures\\\":[\\\"ce017ca25b1b97df2f72e4b49f69ac26a240ce14b3690a8fe619d41ccc42d3c1282e073f85acd36dc50011638906f35b56615f24e4d03e8effe8257f6a808538\\\"]}\"\n }\n ],\n \"outputs\": [\n {\n \"amount\": 2,\n \"id\": \"00bfa73302d12ffd\",\n \"B_\": \"038ec853d65ae1b79b5cdbc2774150b2cb288d6d26e12958a16fb33c32d9a86c39\"\n }\n ]\n}"; var swapRequestParsed = JsonSerializer.Deserialize(swapRequest); - var msgToSign = "[\"P2PK\",{\"nonce\":\"c7f280eb55c1e8564e03db06973e94bc9b666d9e1ca42ad278408fe625950303\",\"data\":\"030d8acedfe072c9fa449a1efe0817157403fbec460d8e79f957966056e5dd76c1\",\"tags\":[[\"sigflag\",\"SIG_ALL\"]]}]02c97ee3d1db41cf0a3ddb601724be8711a032950811bf326f8219c50c4808d3cd2038ec853d65ae1b79b5cdbc2774150b2cb288d6d26e12958a16fb33c32d9a86c39"; - Assert.Equal(msgToSign, SigAllHandler.GetMessageToSign(swapRequestParsed.Inputs, swapRequestParsed.Outputs)); + var msgToSign = + "[\"P2PK\",{\"nonce\":\"c7f280eb55c1e8564e03db06973e94bc9b666d9e1ca42ad278408fe625950303\",\"data\":\"030d8acedfe072c9fa449a1efe0817157403fbec460d8e79f957966056e5dd76c1\",\"tags\":[[\"sigflag\",\"SIG_ALL\"]]}]02c97ee3d1db41cf0a3ddb601724be8711a032950811bf326f8219c50c4808d3cd2038ec853d65ae1b79b5cdbc2774150b2cb288d6d26e12958a16fb33c32d9a86c39"; + Assert.Equal( + msgToSign, + SigAllHandler.GetMessageToSign(swapRequestParsed.Inputs, swapRequestParsed.Outputs) + ); var signedSwapRequest = "{\n \"inputs\": [\n {\n \"amount\": 2,\n \"id\": \"00bfa73302d12ffd\",\n \"secret\": \"[\\\"P2PK\\\",{\\\"nonce\\\":\\\"c7f280eb55c1e8564e03db06973e94bc9b666d9e1ca42ad278408fe625950303\\\",\\\"data\\\":\\\"030d8acedfe072c9fa449a1efe0817157403fbec460d8e79f957966056e5dd76c1\\\",\\\"tags\\\":[[\\\"sigflag\\\",\\\"SIG_ALL\\\"]]}]\",\n \"C\": \"02c97ee3d1db41cf0a3ddb601724be8711a032950811bf326f8219c50c4808d3cd\",\n \"witness\": \"{\\\"signatures\\\":[\\\"ce017ca25b1b97df2f72e4b49f69ac26a240ce14b3690a8fe619d41ccc42d3c1282e073f85acd36dc50011638906f35b56615f24e4d03e8effe8257f6a808538\\\"]}\"\n }\n ],\n \"outputs\": [\n {\n \"amount\": 2,\n \"id\": \"00bfa73302d12ffd\",\n \"B_\": \"038ec853d65ae1b79b5cdbc2774150b2cb288d6d26e12958a16fb33c32d9a86c39\"\n }\n ]\n}"; - var signedSwapRequestParsed = JsonSerializer.Deserialize(signedSwapRequest); - Assert.True(SigAllHandler.VerifySigAllWitness(signedSwapRequestParsed.Inputs, signedSwapRequestParsed.Outputs)); - var witness = JsonSerializer.Deserialize(signedSwapRequestParsed.Inputs.First().Witness); - Assert.True(SigAllHandler.VerifySigAllWitness(signedSwapRequestParsed.Inputs, signedSwapRequestParsed.Outputs, witness)); + var signedSwapRequestParsed = JsonSerializer.Deserialize( + signedSwapRequest + ); + Assert.True( + SigAllHandler.VerifySigAllWitness( + signedSwapRequestParsed.Inputs, + signedSwapRequestParsed.Outputs + ) + ); + var witness = JsonSerializer.Deserialize( + signedSwapRequestParsed.Inputs.First().Witness + ); + Assert.True( + SigAllHandler.VerifySigAllWitness( + signedSwapRequestParsed.Inputs, + signedSwapRequestParsed.Outputs, + witness + ) + ); var validSwapRequest = "{\n \"inputs\": [\n {\n \"amount\": 2,\n \"id\": \"00bfa73302d12ffd\",\n \"secret\": \"[\\\"P2PK\\\",{\\\"nonce\\\":\\\"c7f280eb55c1e8564e03db06973e94bc9b666d9e1ca42ad278408fe625950303\\\",\\\"data\\\":\\\"030d8acedfe072c9fa449a1efe0817157403fbec460d8e79f957966056e5dd76c1\\\",\\\"tags\\\":[[\\\"sigflag\\\",\\\"SIG_ALL\\\"]]}]\",\n \"C\": \"02c97ee3d1db41cf0a3ddb601724be8711a032950811bf326f8219c50c4808d3cd\",\n \"witness\": \"{\\\"signatures\\\":[\\\"ce017ca25b1b97df2f72e4b49f69ac26a240ce14b3690a8fe619d41ccc42d3c1282e073f85acd36dc50011638906f35b56615f24e4d03e8effe8257f6a808538\\\"]}\"\n }\n ],\n \"outputs\": [\n {\n \"amount\": 2,\n \"id\": \"00bfa73302d12ffd\",\n \"B_\": \"038ec853d65ae1b79b5cdbc2774150b2cb288d6d26e12958a16fb33c32d9a86c39\"\n }\n ]\n}"; var validSwapRequestParsed = JsonSerializer.Deserialize(validSwapRequest); - var witness1 = JsonSerializer.Deserialize(validSwapRequestParsed?.Inputs[0].Witness); - Assert.True(SigAllHandler.VerifySigAllWitness(validSwapRequestParsed.Inputs, validSwapRequestParsed.Outputs, witness1)); - Assert.True(SigAllHandler.VerifySigAllWitness(validSwapRequestParsed.Inputs, validSwapRequestParsed.Outputs)); - + var witness1 = JsonSerializer.Deserialize( + validSwapRequestParsed?.Inputs[0].Witness + ); + Assert.True( + SigAllHandler.VerifySigAllWitness( + validSwapRequestParsed.Inputs, + validSwapRequestParsed.Outputs, + witness1 + ) + ); + Assert.True( + SigAllHandler.VerifySigAllWitness( + validSwapRequestParsed.Inputs, + validSwapRequestParsed.Outputs + ) + ); + var invalidSwapRequest = "{\n \"inputs\": [\n {\n \"amount\": 1,\n \"id\": \"00bfa73302d12ffd\",\n \"secret\": \"[\\\"P2PK\\\",{\\\"nonce\\\":\\\"fa6dd3fac9086c153878dec90b9e37163d38ff2ecf8b37db6470e9d185abbbae\\\",\\\"data\\\":\\\"033b42b04e659fed13b669f8b16cdaffc3ee5738608810cf97a7631d09bd01399d\\\",\\\"tags\\\":[[\\\"sigflag\\\",\\\"SIG_ALL\\\"]]}]\",\n \"C\": \"024d232312bab25af2e73f41d56864d378edca9109ae8f76e1030e02e585847786\",\n \"witness\": \"{\\\"signatures\\\":[\\\"27b4d260a1186e3b62a26c0d14ffeab3b9f7c3889e78707b8fd3836b473a00601afbd53a2288ad20a624a8bbe3344453215ea075fc0ce479dd8666fd3d9162cc\\\"]}\"\n },\n {\n \"amount\": 2,\n \"id\": \"00bfa73302d12ffd\",\n \"secret\": \"[\\\"P2PK\\\",{\\\"nonce\\\":\\\"4007b21fc5f5b1d4920bc0a08b158d98fd0fb2b0b0262b57ff53c6c5d6c2ae8c\\\",\\\"data\\\":\\\"033b42b04e659fed13b669f8b16cdaffc3ee5738608810cf97a7631d09bd01399d\\\",\\\"tags\\\":[[\\\"locktime\\\",\\\"122222222222222\\\"],[\\\"sigflag\\\",\\\"SIG_ALL\\\"]]}]\",\n \"C\": \"02417400f2af09772219c831501afcbab4efb3b2e75175635d5474069608deb641\"\n }\n ],\n \"outputs\": [\n {\n \"amount\": 1,\n \"id\": \"00bfa73302d12ffd\",\n \"B_\": \"038ec853d65ae1b79b5cdbc2774150b2cb288d6d26e12958a16fb33c32d9a86c39\"\n },\n {\n \"amount\": 1,\n \"id\": \"00bfa73302d12ffd\",\n \"B_\": \"03afe7c87e32d436f0957f1d70a2bca025822a84a8623e3a33aed0a167016e0ca5\"\n },\n {\n \"amount\": 1,\n \"id\": \"00bfa73302d12ffd\",\n \"B_\": \"02c0d4fce02a7a0f09e3f1bca952db910b17e81a7ebcbce62cd8dcfb127d21e37b\"\n }\n ]\n}"; - var invalidSwapRequestParsed = JsonSerializer.Deserialize(invalidSwapRequest); - Assert.False(SigAllHandler.VerifySigAllWitness(invalidSwapRequestParsed.Inputs, invalidSwapRequestParsed.Outputs)); - var witness2 = JsonSerializer.Deserialize(invalidSwapRequestParsed?.Inputs[0].Witness); - Assert.False(SigAllHandler.VerifySigAllWitness(invalidSwapRequestParsed.Inputs, invalidSwapRequestParsed.Outputs, witness2)); - - var validSwapRequestMultisig = + var invalidSwapRequestParsed = JsonSerializer.Deserialize( + invalidSwapRequest + ); + Assert.False( + SigAllHandler.VerifySigAllWitness( + invalidSwapRequestParsed.Inputs, + invalidSwapRequestParsed.Outputs + ) + ); + var witness2 = JsonSerializer.Deserialize( + invalidSwapRequestParsed?.Inputs[0].Witness + ); + Assert.False( + SigAllHandler.VerifySigAllWitness( + invalidSwapRequestParsed.Inputs, + invalidSwapRequestParsed.Outputs, + witness2 + ) + ); + + var validSwapRequestMultisig = "{\n \"inputs\": [\n {\n \"amount\": 2,\n \"id\": \"00bfa73302d12ffd\",\n \"secret\": \"[\\\"P2PK\\\",{\\\"nonce\\\":\\\"04bfd885fc982d553711092d037fdceb7320fd8f96b0d4fd6d31a65b83b94272\\\",\\\"data\\\":\\\"0275e78025b558dbe6cb8fdd032a2e7613ca14fda5c1f4c4e3427f5077a7bd90e4\\\",\\\"tags\\\":[[\\\"pubkeys\\\",\\\"035163650bbd5ed4be7693f40f340346ba548b941074e9138b67ef6c42755f3449\\\",\\\"02817d22a8edc44c4141e192995a7976647c335092199f9e076a170c7336e2f5cc\\\"],[\\\"n_sigs\\\",\\\"2\\\"],[\\\"sigflag\\\",\\\"SIG_ALL\\\"]]}]\",\n \"C\": \"03866a09946562482c576ca989d06371e412b221890804c7da8887d321380755be\",\n \"witness\": \"{\\\"signatures\\\":[\\\"be1d72c5ca16a93c5a34f25ec63ce632ddc3176787dac363321af3fd0f55d1927e07451bc451ffe5c682d76688ea9925d7977dffbb15bd79763b527f474734b0\\\",\\\"669d6d10d7ed35395009f222f6c7bdc28a378a1ebb72ee43117be5754648501da3bedf2fd6ff0c7849ac92683538c60af0af504102e40f2d8daca8e08b1ca16b\\\"]}\"\n }\n ],\n \"outputs\": [\n {\n \"amount\": 2,\n \"id\": \"00bfa73302d12ffd\",\n \"B_\": \"038ec853d65ae1b79b5cdbc2774150b2cb288d6d26e12958a16fb33c32d9a86c39\"\n }\n ]\n}"; - var validSwapRequestMultisigParsed = JsonSerializer.Deserialize(validSwapRequestMultisig); - var witness3 = JsonSerializer.Deserialize(validSwapRequestMultisigParsed.Inputs[0].Witness); - Assert.True(SigAllHandler.VerifySigAllWitness(validSwapRequestMultisigParsed.Inputs, validSwapRequestMultisigParsed.Outputs, witness3)); - Assert.True(SigAllHandler.VerifySigAllWitness(validSwapRequestMultisigParsed.Inputs, validSwapRequestMultisigParsed.Outputs)); + var validSwapRequestMultisigParsed = JsonSerializer.Deserialize( + validSwapRequestMultisig + ); + var witness3 = JsonSerializer.Deserialize( + validSwapRequestMultisigParsed.Inputs[0].Witness + ); + Assert.True( + SigAllHandler.VerifySigAllWitness( + validSwapRequestMultisigParsed.Inputs, + validSwapRequestMultisigParsed.Outputs, + witness3 + ) + ); + Assert.True( + SigAllHandler.VerifySigAllWitness( + validSwapRequestMultisigParsed.Inputs, + validSwapRequestMultisigParsed.Outputs + ) + ); + var validSwapRequestMultisigRefund = "{\n \"inputs\": [\n {\n \"amount\": 2,\n \"id\": \"00bfa73302d12ffd\",\n \"secret\": \"[\\\"P2PK\\\",{\\\"nonce\\\":\\\"3e9253419a11f0a541dd6baeddecf8356fc864b5d061f12f05632bc3aee6b5c4\\\",\\\"data\\\":\\\"0343cca0e48ce9e3fdcddba4637ff8cdbf6f5ed9cfdf1873e63827e760f0ed4db5\\\",\\\"tags\\\":[[\\\"pubkeys\\\",\\\"0235e0a719f8b046cee90f55a59b1cdd6ca75ce23e49cbcd82c9e5b7310e21ebcd\\\",\\\"020443f98b356e021bae82bdfc05ff433cab21e27fca9ab7b0995aedb2e7aabc43\\\"],[\\\"locktime\\\",\\\"100\\\"],[\\\"n_sigs\\\",\\\"2\\\"],[\\\"refund\\\",\\\"026b432e62b041bf9cdae534203739c73fa506c9a2d6aa58a52bc601a1dec421e1\\\",\\\"02e3494a2e07e7f6e7d4567e0da7a563592bff1e121df2383667f15b83e9168a9e\\\"],[\\\"n_sigs_refund\\\",\\\"2\\\"],[\\\"sigflag\\\",\\\"SIG_ALL\\\"]]}]\",\n \"C\": \"026c12ee3bffa5c617debcf823bf1af6a9b47145b699f2737bba3394f0893eb869\",\n \"witness\": \"{\\\"signatures\\\":[\\\"bfe884145ce6512331324321c3946dfd812428a53656b108b59d26559a186ba2ab45e5be9ce94e2dff0d09078e25ccb82d06a8b3a63cd3dc67065b8f77292776\\\",\\\"236e5cc9c30f85a893a29a4302e41e6f2015caef4229f28fa65e2f5c9d55515cc9a1852093a81a5095055d85fd55bf4da124e55354b56e0a39e83b58b0afc197\\\"]}\"\n }\n ],\n \"outputs\": [\n {\n \"amount\": 1,\n \"id\": \"00bfa73302d12ffd\",\n \"B_\": \"038ec853d65ae1b79b5cdbc2774150b2cb288d6d26e12958a16fb33c32d9a86c39\"\n },\n {\n \"amount\": 1,\n \"id\": \"00bfa73302d12ffd\",\n \"B_\": \"03afe7c87e32d436f0957f1d70a2bca025822a84a8623e3a33aed0a167016e0ca5\"\n }\n ]\n}"; - var validSwapRequestMultisigRefundParsed = - JsonSerializer.Deserialize(validSwapRequestMultisigRefund); - var witness4 = JsonSerializer.Deserialize(validSwapRequestMultisigRefundParsed.Inputs[0].Witness); - Assert.True(SigAllHandler.VerifySigAllWitness(validSwapRequestMultisigRefundParsed.Inputs, validSwapRequestMultisigRefundParsed.Outputs, witness4)); - Assert.True(SigAllHandler.VerifySigAllWitness(validSwapRequestMultisigRefundParsed.Inputs, validSwapRequestMultisigRefundParsed.Outputs)); - - var validSwapRequestMultisigRefundLocktime = + var validSwapRequestMultisigRefundParsed = JsonSerializer.Deserialize( + validSwapRequestMultisigRefund + ); + var witness4 = JsonSerializer.Deserialize( + validSwapRequestMultisigRefundParsed.Inputs[0].Witness + ); + Assert.True( + SigAllHandler.VerifySigAllWitness( + validSwapRequestMultisigRefundParsed.Inputs, + validSwapRequestMultisigRefundParsed.Outputs, + witness4 + ) + ); + Assert.True( + SigAllHandler.VerifySigAllWitness( + validSwapRequestMultisigRefundParsed.Inputs, + validSwapRequestMultisigRefundParsed.Outputs + ) + ); + + var validSwapRequestMultisigRefundLocktime = "{\n \"inputs\": [\n {\n \"amount\": 2,\n \"id\": \"00bfa73302d12ffd\",\n \"secret\": \"[\\\"P2PK\\\",{\\\"nonce\\\":\\\"9ea35553beb18d553d0a53120d0175a0991ca6109370338406eed007b26eacd1\\\",\\\"data\\\":\\\"02af21e09300af92e7b48c48afdb12e22933738cfb9bba67b27c00c679aae3ec25\\\",\\\"tags\\\":[[\\\"locktime\\\",\\\"1\\\"],[\\\"refund\\\",\\\"02637c19143c58b2c58bd378400a7b82bdc91d6dedaeb803b28640ef7d28a887ac\\\",\\\"0345c7fdf7ec7c8e746cca264bf27509eb4edb9ac421f8fbfab1dec64945a4d797\\\"],[\\\"n_sigs_refund\\\",\\\"2\\\"],[\\\"sigflag\\\",\\\"SIG_ALL\\\"]]}]\",\n \"C\": \"03dd83536fbbcbb74ccb3c87147df26753fd499cc2c095f74367fff0fb459c312e\",\n \"witness\": \"{\\\"signatures\\\":[\\\"23b58ef28cd22f3dff421121240ddd621deee83a3bc229fd67019c2e338d91e2c61577e081e1375dbab369307bba265e887857110ca3b4bd949211a0a298805f\\\",\\\"7e75948ef1513564fdcecfcbd389deac67c730f7004f8631ba90c0844d3e8c0cf470b656306877df5141f65fd3b7e85445a8452c3323ab273e6d0d44843817ed\\\"]}\"\n }\n ],\n \"outputs\": [\n {\n \"amount\": 2,\n \"id\": \"00bfa73302d12ffd\",\n \"B_\": \"038ec853d65ae1b79b5cdbc2774150b2cb288d6d26e12958a16fb33c32d9a86c39\"\n }\n ]\n}"; var validSwapRequestMultisigRefundLocktimeParsed = JsonSerializer.Deserialize(validSwapRequestMultisigRefundLocktime); - var witness5 = JsonSerializer.Deserialize(validSwapRequestMultisigRefundLocktimeParsed.Inputs[0].Witness); - Assert.True(SigAllHandler.VerifySigAllWitness(validSwapRequestMultisigRefundLocktimeParsed.Inputs, validSwapRequestMultisigRefundLocktimeParsed.Outputs, witness5)); - Assert.True(SigAllHandler.VerifySigAllWitness(validSwapRequestMultisigRefundLocktimeParsed.Inputs, validSwapRequestMultisigRefundLocktimeParsed.Outputs)); - - var validSwapRequestHTLC = + var witness5 = JsonSerializer.Deserialize( + validSwapRequestMultisigRefundLocktimeParsed.Inputs[0].Witness + ); + Assert.True( + SigAllHandler.VerifySigAllWitness( + validSwapRequestMultisigRefundLocktimeParsed.Inputs, + validSwapRequestMultisigRefundLocktimeParsed.Outputs, + witness5 + ) + ); + Assert.True( + SigAllHandler.VerifySigAllWitness( + validSwapRequestMultisigRefundLocktimeParsed.Inputs, + validSwapRequestMultisigRefundLocktimeParsed.Outputs + ) + ); + + var validSwapRequestHTLC = "{\n \"inputs\": [\n {\n \"amount\": 2,\n \"id\": \"00bfa73302d12ffd\",\n \"secret\": \"[\\\"HTLC\\\",{\\\"nonce\\\":\\\"d730dd70cd7ec6e687829857de8e70aab2b970712f4dbe288343eca20e63c28c\\\",\\\"data\\\":\\\"ec4916dd28fc4c10d78e287ca5d9cc51ee1ae73cbfde08c6b37324cbfaac8bc5\\\",\\\"tags\\\":[[\\\"pubkeys\\\",\\\"0350cda8a1d5257dbd6ba8401a9a27384b9ab699e636e986101172167799469b14\\\"],[\\\"sigflag\\\",\\\"SIG_ALL\\\"]]}]\",\n \"C\": \"03ff6567e2e6c31db5cb7189dab2b5121930086791c93899e4eff3dda61cb57273\",\n \"witness\": \"{\\\"preimage\\\":\\\"0000000000000000000000000000000000000000000000000000000000000001\\\",\\\"signatures\\\":[\\\"a4c00a9ad07f9936e404494fda99a9b935c82d7c053173b304b8663124c81d4b00f64a225f5acf41043ca52b06382722bd04ded0fbeb0fcc404eed3b24778b88\\\"]}\"\n }\n ],\n \"outputs\": [\n {\n \"amount\": 2,\n \"id\": \"00bfa73302d12ffd\",\n \"B_\": \"038ec853d65ae1b79b5cdbc2774150b2cb288d6d26e12958a16fb33c32d9a86c39\"\n }\n ]\n}"; - var validSwapRequestHTLCParsed = - JsonSerializer.Deserialize(validSwapRequestHTLC); - var witness6 = JsonSerializer.Deserialize(validSwapRequestHTLCParsed.Inputs[0].Witness); - var b = SigAllHandler.VerifySigAllWitness(validSwapRequestHTLCParsed.Inputs, validSwapRequestHTLCParsed.Outputs, - witness6); + var validSwapRequestHTLCParsed = JsonSerializer.Deserialize( + validSwapRequestHTLC + ); + var witness6 = JsonSerializer.Deserialize( + validSwapRequestHTLCParsed.Inputs[0].Witness + ); + var b = SigAllHandler.VerifySigAllWitness( + validSwapRequestHTLCParsed.Inputs, + validSwapRequestHTLCParsed.Outputs, + witness6 + ); Assert.True(b); - Assert.True(SigAllHandler.VerifySigAllWitness(validSwapRequestHTLCParsed.Inputs, validSwapRequestHTLCParsed.Outputs)); + Assert.True( + SigAllHandler.VerifySigAllWitness( + validSwapRequestHTLCParsed.Inputs, + validSwapRequestHTLCParsed.Outputs + ) + ); - var invalidSwapRequestHTLC = + var invalidSwapRequestHTLC = "{\n \"inputs\": [\n {\n \"amount\": 2,\n \"id\": \"00bfa73302d12ffd\",\n \"secret\": \"[\\\"HTLC\\\",{\\\"nonce\\\":\\\"512c4045f12fdfd6f55059669c189e040c37c1ce2f8be104ed6aec296acce4e9\\\",\\\"data\\\":\\\"ec4916dd28fc4c10d78e287ca5d9cc51ee1ae73cbfde08c6b37324cbfaac8bc5\\\",\\\"tags\\\":[[\\\"pubkeys\\\",\\\"03ba83defd31c63f8841d188f0d41b5bb3af1bb3c08d0ba46f8f1d26a4d45e8cad\\\"],[\\\"locktime\\\",\\\"4854185133\\\"],[\\\"refund\\\",\\\"032f1008a79c722e93a1b4b853f85f38283f9ef74ee4c5c91293eb1cc3c5e46e34\\\"],[\\\"sigflag\\\",\\\"SIG_ALL\\\"]]}]\",\n \"C\": \"02207abeff828146f1fc3909c74613d5605bd057f16791994b3c91f045b39a6939\",\n \"witness\": \"{\\\"preimage\\\":\\\"0000000000000000000000000000000000000000000000000000000000000001\\\",\\\"signatures\\\":[\\\"7816d57871bde5be2e4281065dbe5b15f641d8f1ed9437a3ae556464d6f9b8a0a2e6660337a915f2c26dce1453a416daf682b8fb593b67a0750fce071e0759b9\\\"]}\"\n }\n ],\n \"outputs\": [\n {\n \"amount\": 1,\n \"id\": \"00bfa73302d12ffd\",\n \"B_\": \"038ec853d65ae1b79b5cdbc2774150b2cb288d6d26e12958a16fb33c32d9a86c39\"\n },\n {\n \"amount\": 1,\n \"id\": \"00bfa73302d12ffd\",\n \"B_\": \"03afe7c87e32d436f0957f1d70a2bca025822a84a8623e3a33aed0a167016e0ca5\"\n }\n ]\n}"; - var invalidSwapRequestHTLCParsed = JsonSerializer.Deserialize(invalidSwapRequestHTLC); - Assert.False(SigAllHandler.VerifySigAllWitness(invalidSwapRequestHTLCParsed.Inputs, invalidSwapRequestHTLCParsed.Outputs)); - var witness7 = JsonSerializer.Deserialize(invalidSwapRequestHTLCParsed.Inputs[0].Witness); - Assert.False(SigAllHandler.VerifySigAllWitness(invalidSwapRequestHTLCParsed.Inputs, invalidSwapRequestHTLCParsed.Outputs, witness7)); + var invalidSwapRequestHTLCParsed = JsonSerializer.Deserialize( + invalidSwapRequestHTLC + ); + Assert.False( + SigAllHandler.VerifySigAllWitness( + invalidSwapRequestHTLCParsed.Inputs, + invalidSwapRequestHTLCParsed.Outputs + ) + ); + var witness7 = JsonSerializer.Deserialize( + invalidSwapRequestHTLCParsed.Inputs[0].Witness + ); + Assert.False( + SigAllHandler.VerifySigAllWitness( + invalidSwapRequestHTLCParsed.Inputs, + invalidSwapRequestHTLCParsed.Outputs, + witness7 + ) + ); var validSwapRequestHTLCMultisig = "{\n \"inputs\": [\n {\n \"amount\": 2,\n \"id\": \"00bfa73302d12ffd\",\n \"secret\": \"[\\\"HTLC\\\",{\\\"nonce\\\":\\\"c9b0fabb8007c0db4bef64d5d128cdcf3c79e8bb780c3294adf4c88e96c32647\\\",\\\"data\\\":\\\"ec4916dd28fc4c10d78e287ca5d9cc51ee1ae73cbfde08c6b37324cbfaac8bc5\\\",\\\"tags\\\":[[\\\"pubkeys\\\",\\\"039e6ec7e922abb4162235b3a42965eb11510b07b7461f6b1a17478b1c9c64d100\\\"],[\\\"locktime\\\",\\\"1\\\"],[\\\"refund\\\",\\\"02ce1bbd2c9a4be8029c9a6435ad601c45677f5cde81f8a7f0ed535e0039d0eb6c\\\",\\\"03c43c00ff57f63cfa9e732f0520c342123e21331d0121139f1b636921eeec095f\\\"],[\\\"n_sigs_refund\\\",\\\"2\\\"],[\\\"sigflag\\\",\\\"SIG_ALL\\\"]]}]\",\n \"C\": \"0344b6f1471cf18a8cbae0e624018c816be5e3a9b04dcb7689f64173c1ae90a3a5\",\n \"witness\": \"{\\\"preimage\\\":\\\"0000000000000000000000000000000000000000000000000000000000000001\\\",\\\"signatures\\\":[\\\"98e21672d409cc782c720f203d8284f0af0c8713f18167499f9f101b7050c3e657fb0e57478ebd8bd561c31aa6c30f4cd20ec38c73f5755b7b4ddee693bca5a5\\\",\\\"693f40129dbf905ed9c8008081c694f72a36de354f9f4fa7a61b389cf781f62a0ae0586612fb2eb504faaf897fefb6742309186117f4743bcebcb8e350e975e2\\\"]}\"\n }\n ],\n \"outputs\": [\n {\n \"amount\": 2,\n \"id\": \"00bfa73302d12ffd\",\n \"B_\": \"038ec853d65ae1b79b5cdbc2774150b2cb288d6d26e12958a16fb33c32d9a86c39\"\n }\n ]\n}"; - var validSwapRequestHTLCMultisigParsed = JsonSerializer.Deserialize(validSwapRequestHTLCMultisig); - var witness8 = JsonSerializer.Deserialize(validSwapRequestHTLCMultisigParsed.Inputs[0].Witness); - Assert.True(SigAllHandler.VerifySigAllWitness(validSwapRequestHTLCMultisigParsed.Inputs, validSwapRequestHTLCMultisigParsed.Outputs, witness8)); - Assert.True(SigAllHandler.VerifySigAllWitness(validSwapRequestHTLCMultisigParsed.Inputs, validSwapRequestHTLCMultisigParsed.Outputs)); + var validSwapRequestHTLCMultisigParsed = JsonSerializer.Deserialize( + validSwapRequestHTLCMultisig + ); + var witness8 = JsonSerializer.Deserialize( + validSwapRequestHTLCMultisigParsed.Inputs[0].Witness + ); + Assert.True( + SigAllHandler.VerifySigAllWitness( + validSwapRequestHTLCMultisigParsed.Inputs, + validSwapRequestHTLCMultisigParsed.Outputs, + witness8 + ) + ); + Assert.True( + SigAllHandler.VerifySigAllWitness( + validSwapRequestHTLCMultisigParsed.Inputs, + validSwapRequestHTLCMultisigParsed.Outputs + ) + ); - var meltRequest = + var meltRequest = "{\n \"quote\": \"cF8911fzT88aEi1d-6boZZkq5lYxbUSVs-HbJxK0\",\n \"inputs\": [\n {\n \"amount\": 2,\n \"id\": \"00bfa73302d12ffd\",\n \"secret\": \"[\\\"P2PK\\\",{\\\"nonce\\\":\\\"bbf9edf441d17097e39f5095a3313ba24d3055ab8a32f758ff41c10d45c4f3de\\\",\\\"data\\\":\\\"029116d32e7da635c8feeb9f1f4559eb3d9b42d400f9d22a64834d89cde0eb6835\\\",\\\"tags\\\":[[\\\"sigflag\\\",\\\"SIG_ALL\\\"]]}]\",\n \"C\": \"02a9d461ff36448469dccf828fa143833ae71c689886ac51b62c8d61ddaa10028b\",\n \"witness\": \"{\\\"signatures\\\":[\\\"478224fbe715e34f78cb33451db6fcf8ab948afb8bd04ff1a952c92e562ac0f7c1cb5e61809410635be0aa94d0448f7f7959bd5762cc3802b0a00ff58b2da747\\\"]}\"\n }\n ],\n \"outputs\": [\n {\n \"amount\": 0,\n \"id\": \"00bfa73302d12ffd\",\n \"B_\": \"038ec853d65ae1b79b5cdbc2774150b2cb288d6d26e12958a16fb33c32d9a86c39\"\n }\n ]\n}"; var meltRequestParsed = JsonSerializer.Deserialize(meltRequest); var msg2 = "[\"P2PK\",{\"nonce\":\"bbf9edf441d17097e39f5095a3313ba24d3055ab8a32f758ff41c10d45c4f3de\",\"data\":\"029116d32e7da635c8feeb9f1f4559eb3d9b42d400f9d22a64834d89cde0eb6835\",\"tags\":[[\"sigflag\",\"SIG_ALL\"]]}]02a9d461ff36448469dccf828fa143833ae71c689886ac51b62c8d61ddaa10028b0038ec853d65ae1b79b5cdbc2774150b2cb288d6d26e12958a16fb33c32d9a86c39cF8911fzT88aEi1d-6boZZkq5lYxbUSVs-HbJxK0"; - Assert.Equal(SigAllHandler.GetMessageToSign(meltRequestParsed.Inputs, meltRequestParsed.Outputs, meltRequestParsed.Quote), msg2); + Assert.Equal( + SigAllHandler.GetMessageToSign( + meltRequestParsed.Inputs, + meltRequestParsed.Outputs, + meltRequestParsed.Quote + ), + msg2 + ); - var meltRequestValid = + var meltRequestValid = "{\n \"quote\": \"cF8911fzT88aEi1d-6boZZkq5lYxbUSVs-HbJxK0\",\n \"inputs\": [\n {\n \"amount\": 2,\n \"id\": \"00bfa73302d12ffd\",\n \"secret\": \"[\\\"P2PK\\\",{\\\"nonce\\\":\\\"bbf9edf441d17097e39f5095a3313ba24d3055ab8a32f758ff41c10d45c4f3de\\\",\\\"data\\\":\\\"029116d32e7da635c8feeb9f1f4559eb3d9b42d400f9d22a64834d89cde0eb6835\\\",\\\"tags\\\":[[\\\"sigflag\\\",\\\"SIG_ALL\\\"]]}]\",\n \"C\": \"02a9d461ff36448469dccf828fa143833ae71c689886ac51b62c8d61ddaa10028b\",\n \"witness\": \"{\\\"signatures\\\":[\\\"478224fbe715e34f78cb33451db6fcf8ab948afb8bd04ff1a952c92e562ac0f7c1cb5e61809410635be0aa94d0448f7f7959bd5762cc3802b0a00ff58b2da747\\\"]}\"\n }\n ],\n \"outputs\": [\n {\n \"amount\": 0,\n \"id\": \"00bfa73302d12ffd\",\n \"B_\": \"038ec853d65ae1b79b5cdbc2774150b2cb288d6d26e12958a16fb33c32d9a86c39\"\n }\n ]\n}"; var meltRequestValidParsed = JsonSerializer.Deserialize(meltRequestValid); - Assert.True(SigAllHandler.VerifySigAllWitness(meltRequestValidParsed.Inputs, meltRequestValidParsed.Outputs, meltRequestValidParsed.Quote)); - var witness9 = JsonSerializer.Deserialize(meltRequestValidParsed.Inputs[0].Witness); - Assert.True(SigAllHandler.VerifySigAllWitness(meltRequestValidParsed.Inputs, meltRequestValidParsed.Outputs, witness9, meltRequestValidParsed.Quote)); + Assert.True( + SigAllHandler.VerifySigAllWitness( + meltRequestValidParsed.Inputs, + meltRequestValidParsed.Outputs, + meltRequestValidParsed.Quote + ) + ); + var witness9 = JsonSerializer.Deserialize( + meltRequestValidParsed.Inputs[0].Witness + ); + Assert.True( + SigAllHandler.VerifySigAllWitness( + meltRequestValidParsed.Inputs, + meltRequestValidParsed.Outputs, + witness9, + meltRequestValidParsed.Quote + ) + ); var meltRequestMultisig = "{\n \"quote\": \"Db3qEMVwFN2tf_1JxbZp29aL5cVXpSMIwpYfyOVF\",\n \"inputs\": [\n {\n \"amount\": 2,\n \"id\": \"00bfa73302d12ffd\",\n \"secret\": \"[\\\"P2PK\\\",{\\\"nonce\\\":\\\"68d7822538740e4f9c9ebf5183ef6c4501c7a9bca4e509ce2e41e1d62e7b8a99\\\",\\\"data\\\":\\\"0394e841bd59aeadce16380df6174cb29c9fea83b0b65b226575e6d73cc5a1bd59\\\",\\\"tags\\\":[[\\\"pubkeys\\\",\\\"033d892d7ad2a7d53708b7a5a2af101cbcef69522bd368eacf55fcb4f1b0494058\\\"],[\\\"n_sigs\\\",\\\"2\\\"],[\\\"sigflag\\\",\\\"SIG_ALL\\\"]]}]\",\n \"C\": \"03a70c42ec9d7192422c7f7a3ad017deda309fb4a2453fcf9357795ea706cc87a9\",\n \"witness\": \"{\\\"signatures\\\":[\\\"ed739970d003f703da2f101a51767b63858f4894468cc334be04aa3befab1617a81e3eef093441afb499974152d279e59d9582a31dc68adbc17ffc22a2516086\\\",\\\"f9efe1c70eb61e7ad8bd615c50ff850410a4135ea73ba5fd8e12a734743ad045e575e9e76ea5c52c8e7908d3ad5c0eaae93337e5c11109e52848dc328d6757a2\\\"]}\"\n }\n ],\n \"outputs\": [\n {\n \"amount\": 0,\n \"id\": \"00bfa73302d12ffd\",\n \"B_\": \"038ec853d65ae1b79b5cdbc2774150b2cb288d6d26e12958a16fb33c32d9a86c39\"\n }\n ]\n}"; - var meltRequestMultisigParsed = JsonSerializer.Deserialize(meltRequestMultisig); - Assert.True(SigAllHandler.VerifySigAllWitness(meltRequestMultisigParsed.Inputs, meltRequestMultisigParsed.Outputs, meltRequestMultisigParsed.Quote)); - var witness10 = JsonSerializer.Deserialize(meltRequestMultisigParsed.Inputs[0].Witness); - Assert.True(SigAllHandler.VerifySigAllWitness(meltRequestMultisigParsed.Inputs, meltRequestMultisigParsed.Outputs, witness10, meltRequestMultisigParsed.Quote)); + var meltRequestMultisigParsed = JsonSerializer.Deserialize( + meltRequestMultisig + ); + Assert.True( + SigAllHandler.VerifySigAllWitness( + meltRequestMultisigParsed.Inputs, + meltRequestMultisigParsed.Outputs, + meltRequestMultisigParsed.Quote + ) + ); + var witness10 = JsonSerializer.Deserialize( + meltRequestMultisigParsed.Inputs[0].Witness + ); + Assert.True( + SigAllHandler.VerifySigAllWitness( + meltRequestMultisigParsed.Inputs, + meltRequestMultisigParsed.Outputs, + witness10, + meltRequestMultisigParsed.Quote + ) + ); } [Fact] @@ -507,7 +803,8 @@ public void Nut12Tests_BlindSignaturesDLEQ() var A = "0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798".ToPubKey(); var B_ = "02a9acc1e48c25eeeb9289b5031cc57da9fe72f3fe2861d264bdc074209b107ba2".ToPubKey(); var blindSig = JsonSerializer.Deserialize( - "{\n \"amount\": 8,\n \"id\": \"00882760bfa2eb41\",\n \"C_\": \"02a9acc1e48c25eeeb9289b5031cc57da9fe72f3fe2861d264bdc074209b107ba2\",\n \"dleq\": {\n \"e\": \"9818e061ee51d5c8edc3342369a554998ff7b4381c8652d724cdf46429be73d9\",\n \"s\": \"9818e061ee51d5c8edc3342369a554998ff7b4381c8652d724cdf46429be73da\"\n }\n}"); + "{\n \"amount\": 8,\n \"id\": \"00882760bfa2eb41\",\n \"C_\": \"02a9acc1e48c25eeeb9289b5031cc57da9fe72f3fe2861d264bdc074209b107ba2\",\n \"dleq\": {\n \"e\": \"9818e061ee51d5c8edc3342369a554998ff7b4381c8652d724cdf46429be73d9\",\n \"s\": \"9818e061ee51d5c8edc3342369a554998ff7b4381c8652d724cdf46429be73da\"\n }\n}" + ); Assert.NotNull(blindSig?.DLEQ); blindSig.Verify(A, B_); @@ -518,10 +815,13 @@ public void Nut12Tests_ProofDLEQ() { var A = "0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798".ToPubKey(); var proof = JsonSerializer.Deserialize( - "{\"amount\": 1,\"id\": \"00882760bfa2eb41\",\"secret\": \"daf4dd00a2b68a0858a80450f52c8a7d2ccf87d375e43e216e0c571f089f63e9\",\"C\": \"024369d2d22a80ecf78f3937da9d5f30c1b9f74f0c32684d583cca0fa6a61cdcfc\",\"dleq\": {\"e\": \"b31e58ac6527f34975ffab13e70a48b6d2b0d35abc4b03f0151f09ee1a9763d4\",\"s\": \"8fbae004c59e754d71df67e392b6ae4e29293113ddc2ec86592a0431d16306d8\",\"r\": \"a6d13fcd7a18442e6076f5e1e7c887ad5de40a019824bdfa9fe740d302e8d861\"}}"); + "{\"amount\": 1,\"id\": \"00882760bfa2eb41\",\"secret\": \"daf4dd00a2b68a0858a80450f52c8a7d2ccf87d375e43e216e0c571f089f63e9\",\"C\": \"024369d2d22a80ecf78f3937da9d5f30c1b9f74f0c32684d583cca0fa6a61cdcfc\",\"dleq\": {\"e\": \"b31e58ac6527f34975ffab13e70a48b6d2b0d35abc4b03f0151f09ee1a9763d4\",\"s\": \"8fbae004c59e754d71df67e392b6ae4e29293113ddc2ec86592a0431d16306d8\",\"r\": \"a6d13fcd7a18442e6076f5e1e7c887ad5de40a019824bdfa9fe740d302e8d861\"}}" + ); Assert.NotNull(proof?.DLEQ); - Assert.Equal("024369d2d22a80ecf78f3937da9d5f30c1b9f74f0c32684d583cca0fa6a61cdcfc".ToPubKey(), - proof.Secret.ToCurve()); + Assert.Equal( + "024369d2d22a80ecf78f3937da9d5f30c1b9f74f0c32684d583cca0fa6a61cdcfc".ToPubKey(), + proof.Secret.ToCurve() + ); Assert.True(proof.Verify(A)); } @@ -547,46 +847,58 @@ public void Nut18Tests() var creqA = "creqApWF0gaNhdGVub3N0cmFheKlucHJvZmlsZTFxeTI4d3VtbjhnaGo3dW45ZDNzaGp0bnl2OWtoMnVld2Q5aHN6OW1od2RlbjV0ZTB3ZmprY2N0ZTljdXJ4dmVuOWVlaHFjdHJ2NWhzenJ0aHdkZW41dGUwZGVoaHh0bnZkYWtxcWd5ZGFxeTdjdXJrNDM5eWtwdGt5c3Y3dWRoZGh1NjhzdWNtMjk1YWtxZWZkZWhrZjBkNDk1Y3d1bmw1YWeBgmFuYjE3YWloYjdhOTAxNzZhYQphdWNzYXRhbYF4Imh0dHBzOi8vbm9mZWVzLnRlc3RudXQuY2FzaHUuc3BhY2U="; var pr = PaymentRequest.Parse(creqA); - Assert.Equal("https://nofees.testnut.cashu.space", Assert.Single( pr.Mints)); + Assert.Equal("https://nofees.testnut.cashu.space", Assert.Single(pr.Mints)); Assert.Equal((ulong)10, pr.Amount); Assert.Equal("b7a90176", pr.PaymentId); Assert.Equal("sat", pr.Unit); var t = Assert.Single(pr.Transports); Assert.Equal("nostr", t.Type); - Assert.Equal("nprofile1qy28wumn8ghj7un9d3shjtnyv9kh2uewd9hsz9mhwden5te0wfjkccte9curxven9eehqctrv5hszrthwden5te0dehhxtnvdakqqgydaqy7curk439ykptkysv7udhdhu68sucm295akqefdehkf0d495cwunl5", t.Target); - Assert.Equal("n",Assert.Single(t.Tags).Key ); - Assert.Equal("17",Assert.Single(t.Tags).Value[0] ); + Assert.Equal( + "nprofile1qy28wumn8ghj7un9d3shjtnyv9kh2uewd9hsz9mhwden5te0wfjkccte9curxven9eehqctrv5hszrthwden5te0dehhxtnvdakqqgydaqy7curk439ykptkysv7udhdhu68sucm295akqefdehkf0d495cwunl5", + t.Target + ); + Assert.Equal("n", Assert.Single(t.Tags).Key); + Assert.Equal("17", Assert.Single(t.Tags).Value[0]); // Assert.Equal(creqA, pr.ToString()); - } - + internal readonly struct TestCase(in string path, in string keyHex, in string ccHex) { internal static readonly ReadOnlyMemory Seed = Convert.FromHexString( - "e4a964f4973ce5750a6a5a5126e8258442c197b2e71b683ccba58688f21242eae1b0f12bee21d6e983d4a5c61f081bf3f0669546eb576dec1b22ec8d481b00fb"); + "e4a964f4973ce5750a6a5a5126e8258442c197b2e71b683ccba58688f21242eae1b0f12bee21d6e983d4a5c61f081bf3f0669546eb576dec1b22ec8d481b00fb" + ); internal readonly ReadOnlyMemory Key = Convert.FromHexString(keyHex); internal readonly ReadOnlyMemory ChainCode = Convert.FromHexString(ccHex); internal readonly KeyPath Path = path; } - + private static readonly TestCase Case1SecP256K1 = new( "m/0'/0/0", "6144c1daf8222d6dab77e7a20c2f338519b83bd1423602c56c7dfb5e9ea99c02", - "55b36970e7ab8434f9b04f1c2e52da7422d2bce7e284ca353419dddfa2e34bdb"); + "55b36970e7ab8434f9b04f1c2e52da7422d2bce7e284ca353419dddfa2e34bdb" + ); [Fact] public void Bip32Test() { var masterKeyFromSeed = BIP32.Instance.GetMasterKeyFromSeed(TestCase.Seed.Span); - - Assert.Equal("5A876CC4B4AB2F6717951AEE7F97AB69844DBFFFF7074E6E6F71D2BA04BD6EC9", Convert.ToHexString( masterKeyFromSeed.ChainCode)); - Assert.Equal("8D18D3F0CF9D74B53A935D97E8DE85955ED9F6EEFC6D6D45F0C169031A11B669", Convert.ToHexString( masterKeyFromSeed.PrivateKey)); - - - Assert.Equal("026cf0d14fcfa930347e7da26281319ac5959d02f1b6331812261efdb7e347788b",ECPrivKey.Create(masterKeyFromSeed.PrivateKey).CreatePubKey().ToHex()); - + + Assert.Equal( + "5A876CC4B4AB2F6717951AEE7F97AB69844DBFFFF7074E6E6F71D2BA04BD6EC9", + Convert.ToHexString(masterKeyFromSeed.ChainCode) + ); + Assert.Equal( + "8D18D3F0CF9D74B53A935D97E8DE85955ED9F6EEFC6D6D45F0C169031A11B669", + Convert.ToHexString(masterKeyFromSeed.PrivateKey) + ); + + Assert.Equal( + "026cf0d14fcfa930347e7da26281319ac5959d02f1b6331812261efdb7e347788b", + ECPrivKey.Create(masterKeyFromSeed.PrivateKey).CreatePubKey().ToHex() + ); + var der1 = BIP32.Instance.DerivePath(Case1SecP256K1.Path, TestCase.Seed.Span); Assert.True(der1.PrivateKey.SequenceEqual(Case1SecP256K1.Key.Span)); Assert.True(der1.ChainCode.SequenceEqual(Case1SecP256K1.ChainCode.Span)); @@ -599,58 +911,119 @@ public void Nut13Tests() Assert.Equal(864559728, Nut13.GetKeysetIdInt(keysetId)); var path = "m/129372'/0'/864559728'/{counter}'"; - var mnemonicPhrase = "half depart obvious quality work element tank gorilla view sugar picture humble"; + var mnemonicPhrase = + "half depart obvious quality work element tank gorilla view sugar picture humble"; var mnemonic = new Mnemonic(mnemonicPhrase); - Assert.Equal("dd44ee516b0647e80b488e8dcc56d736a148f15276bef588b37057476d4b2b25780d3688a32b37353d6995997842c0fd8b412475c891c16310471fbc86dcbda8", - Convert.ToHexString(mnemonic.DeriveSeed()).ToLowerInvariant()); - - Assert.Equal("m/129372'/0'/864559728'/0'/0", Nut13.GetNut13DerivationPath(keysetId, 0, true)); - Assert.Equal("m/129372'/0'/864559728'/0'/1", Nut13.GetNut13DerivationPath(keysetId, 0, false)); - - Assert.Equal("485875df74771877439ac06339e284c3acfcd9be7abf3bc20b516faeadfe77ae", - mnemonic.DeriveSecret(keysetId, 0).Secret); - Assert.Equal("8f2b39e8e594a4056eb1e6dbb4b0c38ef13b1b2c751f64f810ec04ee35b77270", - mnemonic.DeriveSecret(keysetId, 1).Secret); - Assert.Equal("bc628c79accd2364fd31511216a0fab62afd4a18ff77a20deded7b858c9860c8", - mnemonic.DeriveSecret(keysetId, 2).Secret); - Assert.Equal("59284fd1650ea9fa17db2b3acf59ecd0f2d52ec3261dd4152785813ff27a33bf", - mnemonic.DeriveSecret(keysetId, 3).Secret); - Assert.Equal("576c23393a8b31cc8da6688d9c9a96394ec74b40fdaf1f693a6bb84284334ea0", - mnemonic.DeriveSecret(keysetId, 4).Secret); - - Assert.Equal("ad00d431add9c673e843d4c2bf9a778a5f402b985b8da2d5550bf39cda41d679", - Convert.ToHexString(mnemonic.DeriveBlindingFactor(keysetId, 0)).ToLowerInvariant()); - Assert.Equal("967d5232515e10b81ff226ecf5a9e2e2aff92d66ebc3edf0987eb56357fd6248", - Convert.ToHexString(mnemonic.DeriveBlindingFactor(keysetId, 1)).ToLowerInvariant()); - Assert.Equal("b20f47bb6ae083659f3aa986bfa0435c55c6d93f687d51a01f26862d9b9a4899", - Convert.ToHexString(mnemonic.DeriveBlindingFactor(keysetId, 2)).ToLowerInvariant()); - Assert.Equal("fb5fca398eb0b1deb955a2988b5ac77d32956155f1c002a373535211a2dfdc29", - Convert.ToHexString(mnemonic.DeriveBlindingFactor(keysetId, 3)).ToLowerInvariant()); - Assert.Equal("5f09bfbfe27c439a597719321e061e2e40aad4a36768bb2bcc3de547c9644bf9", - Convert.ToHexString(mnemonic.DeriveBlindingFactor(keysetId, 4)).ToLowerInvariant()); + Assert.Equal( + "dd44ee516b0647e80b488e8dcc56d736a148f15276bef588b37057476d4b2b25780d3688a32b37353d6995997842c0fd8b412475c891c16310471fbc86dcbda8", + Convert.ToHexString(mnemonic.DeriveSeed()).ToLowerInvariant() + ); + + Assert.Equal( + "m/129372'/0'/864559728'/0'/0", + Nut13.GetNut13DerivationPath(keysetId, 0, true) + ); + Assert.Equal( + "m/129372'/0'/864559728'/0'/1", + Nut13.GetNut13DerivationPath(keysetId, 0, false) + ); + + Assert.Equal( + "485875df74771877439ac06339e284c3acfcd9be7abf3bc20b516faeadfe77ae", + mnemonic.DeriveSecret(keysetId, 0).Secret + ); + Assert.Equal( + "8f2b39e8e594a4056eb1e6dbb4b0c38ef13b1b2c751f64f810ec04ee35b77270", + mnemonic.DeriveSecret(keysetId, 1).Secret + ); + Assert.Equal( + "bc628c79accd2364fd31511216a0fab62afd4a18ff77a20deded7b858c9860c8", + mnemonic.DeriveSecret(keysetId, 2).Secret + ); + Assert.Equal( + "59284fd1650ea9fa17db2b3acf59ecd0f2d52ec3261dd4152785813ff27a33bf", + mnemonic.DeriveSecret(keysetId, 3).Secret + ); + Assert.Equal( + "576c23393a8b31cc8da6688d9c9a96394ec74b40fdaf1f693a6bb84284334ea0", + mnemonic.DeriveSecret(keysetId, 4).Secret + ); + + Assert.Equal( + "ad00d431add9c673e843d4c2bf9a778a5f402b985b8da2d5550bf39cda41d679", + Convert.ToHexString(mnemonic.DeriveBlindingFactor(keysetId, 0)).ToLowerInvariant() + ); + Assert.Equal( + "967d5232515e10b81ff226ecf5a9e2e2aff92d66ebc3edf0987eb56357fd6248", + Convert.ToHexString(mnemonic.DeriveBlindingFactor(keysetId, 1)).ToLowerInvariant() + ); + Assert.Equal( + "b20f47bb6ae083659f3aa986bfa0435c55c6d93f687d51a01f26862d9b9a4899", + Convert.ToHexString(mnemonic.DeriveBlindingFactor(keysetId, 2)).ToLowerInvariant() + ); + Assert.Equal( + "fb5fca398eb0b1deb955a2988b5ac77d32956155f1c002a373535211a2dfdc29", + Convert.ToHexString(mnemonic.DeriveBlindingFactor(keysetId, 3)).ToLowerInvariant() + ); + Assert.Equal( + "5f09bfbfe27c439a597719321e061e2e40aad4a36768bb2bcc3de547c9644bf9", + Convert.ToHexString(mnemonic.DeriveBlindingFactor(keysetId, 4)).ToLowerInvariant() + ); } [Fact] public void Nut13HMACTests() { - KeysetId keysetId = new KeysetId("015ba18a8adcd02e715a58358eb618da4a4b3791151a4bee5e968bb88406ccf76a"); - Mnemonic mnemonic = - new Mnemonic("half depart obvious quality work element tank gorilla view sugar picture humble"); - - Assert.Equal("db5561a07a6e6490f8dadeef5be4e92f7cebaecf2f245356b5b2a4ec40687298", mnemonic.DeriveSecret(keysetId, 0).Secret); - Assert.Equal("b70e7b10683da3bf1cdf0411206f8180c463faa16014663f39f2529b2fda922e", mnemonic.DeriveSecret(keysetId, 1).Secret); - Assert.Equal("78a7ac32ccecc6b83311c6081b89d84bb4128f5a0d0c5e1af081f301c7a513f5", mnemonic.DeriveSecret(keysetId, 2).Secret); - Assert.Equal("094a2b6c63bfa7970bc09cda0e1cfc9cd3d7c619b8e98fabcfc60aea9e4963e5", mnemonic.DeriveSecret(keysetId, 3).Secret); - Assert.Equal("5e89fc5d30d0bf307ddf0a3ac34aa7a8ee3702169dafa3d3fe1d0cae70ecd5ef", mnemonic.DeriveSecret(keysetId, 4).Secret); - - - Assert.Equal("6d26181a3695e32e9f88b80f039ba1ae2ab5a200ad4ce9dbc72c6d3769f2b035", Convert.ToHexString(mnemonic.DeriveBlindingFactor(keysetId, 0)).ToLowerInvariant()); - Assert.Equal("bde4354cee75545bea1a2eee035a34f2d524cee2bb01613823636e998386952e", Convert.ToHexString(mnemonic.DeriveBlindingFactor(keysetId, 1)).ToLowerInvariant()); - Assert.Equal("f40cc1218f085b395c8e1e5aaa25dccc851be3c6c7526a0f4e57108f12d6dac4", Convert.ToHexString(mnemonic.DeriveBlindingFactor(keysetId, 2)).ToLowerInvariant()); - Assert.Equal("099ed70fc2f7ac769bc20b2a75cb662e80779827b7cc358981318643030577d0", Convert.ToHexString(mnemonic.DeriveBlindingFactor(keysetId, 3)).ToLowerInvariant()); - Assert.Equal("5550337312d223ba62e3f75cfe2ab70477b046d98e3e71804eade3956c7b98cf", Convert.ToHexString(mnemonic.DeriveBlindingFactor(keysetId, 4)).ToLowerInvariant()); + KeysetId keysetId = new KeysetId( + "015ba18a8adcd02e715a58358eb618da4a4b3791151a4bee5e968bb88406ccf76a" + ); + Mnemonic mnemonic = new Mnemonic( + "half depart obvious quality work element tank gorilla view sugar picture humble" + ); + + Assert.Equal( + "db5561a07a6e6490f8dadeef5be4e92f7cebaecf2f245356b5b2a4ec40687298", + mnemonic.DeriveSecret(keysetId, 0).Secret + ); + Assert.Equal( + "b70e7b10683da3bf1cdf0411206f8180c463faa16014663f39f2529b2fda922e", + mnemonic.DeriveSecret(keysetId, 1).Secret + ); + Assert.Equal( + "78a7ac32ccecc6b83311c6081b89d84bb4128f5a0d0c5e1af081f301c7a513f5", + mnemonic.DeriveSecret(keysetId, 2).Secret + ); + Assert.Equal( + "094a2b6c63bfa7970bc09cda0e1cfc9cd3d7c619b8e98fabcfc60aea9e4963e5", + mnemonic.DeriveSecret(keysetId, 3).Secret + ); + Assert.Equal( + "5e89fc5d30d0bf307ddf0a3ac34aa7a8ee3702169dafa3d3fe1d0cae70ecd5ef", + mnemonic.DeriveSecret(keysetId, 4).Secret + ); + + Assert.Equal( + "6d26181a3695e32e9f88b80f039ba1ae2ab5a200ad4ce9dbc72c6d3769f2b035", + Convert.ToHexString(mnemonic.DeriveBlindingFactor(keysetId, 0)).ToLowerInvariant() + ); + Assert.Equal( + "bde4354cee75545bea1a2eee035a34f2d524cee2bb01613823636e998386952e", + Convert.ToHexString(mnemonic.DeriveBlindingFactor(keysetId, 1)).ToLowerInvariant() + ); + Assert.Equal( + "f40cc1218f085b395c8e1e5aaa25dccc851be3c6c7526a0f4e57108f12d6dac4", + Convert.ToHexString(mnemonic.DeriveBlindingFactor(keysetId, 2)).ToLowerInvariant() + ); + Assert.Equal( + "099ed70fc2f7ac769bc20b2a75cb662e80779827b7cc358981318643030577d0", + Convert.ToHexString(mnemonic.DeriveBlindingFactor(keysetId, 3)).ToLowerInvariant() + ); + Assert.Equal( + "5550337312d223ba62e3f75cfe2ab70477b046d98e3e71804eade3956c7b98cf", + Convert.ToHexString(mnemonic.DeriveBlindingFactor(keysetId, 4)).ToLowerInvariant() + ); } - + [Fact] public void NullExpiryTests_PostMintQuoteBolt11Response() { @@ -667,7 +1040,7 @@ public void NullExpiryTests_PostMintQuoteBolt11Response() """; var response = JsonSerializer.Deserialize(jsonWithNullExpiry); - + Assert.NotNull(response); Assert.Equal("test-quote-id", response.Quote); Assert.Equal("test-request", response.Request); @@ -688,7 +1061,7 @@ public void NullExpiryTests_PostMintQuoteBolt11Response() """; var response2 = JsonSerializer.Deserialize(jsonWithoutExpiry); - + Assert.NotNull(response2); Assert.Equal("test-quote-id-2", response2.Quote); Assert.Equal("test-request-2", response2.Request); @@ -710,7 +1083,7 @@ public void NullExpiryTests_PostMintQuoteBolt11Response() """; var response3 = JsonSerializer.Deserialize(jsonWithExpiry); - + Assert.NotNull(response3); Assert.Equal("test-quote-id-3", response3.Quote); Assert.Equal("test-request-3", response3.Request); @@ -736,7 +1109,7 @@ public void NullExpiryTests_PostMeltQuoteBolt11Response() """; var response = JsonSerializer.Deserialize(jsonWithNullExpiry); - + Assert.NotNull(response); Assert.Equal("melt-quote-id", response.Quote); Assert.Equal((ulong)1000, response.Amount); @@ -756,7 +1129,7 @@ public void NullExpiryTests_PostMeltQuoteBolt11Response() """; var response2 = JsonSerializer.Deserialize(jsonWithoutExpiry); - + Assert.NotNull(response2); Assert.Equal("melt-quote-id-2", response2.Quote); Assert.Equal((ulong)500, response2.Amount); @@ -777,7 +1150,7 @@ public void NullExpiryTests_PostMeltQuoteBolt11Response() """; var response3 = JsonSerializer.Deserialize(jsonWithExpiry); - + Assert.NotNull(response3); Assert.Equal("melt-quote-id-3", response3.Quote); Assert.Equal((ulong)2000, response3.Amount); @@ -786,6 +1159,7 @@ public void NullExpiryTests_PostMeltQuoteBolt11Response() Assert.Equal(1640995200, response3.Expiry); Assert.Null(response3.PaymentPreimage); } + private static readonly byte[] P2BK_PREFIX = "Cashu_P2BK_v1"u8.ToArray(); [Fact] @@ -794,14 +1168,17 @@ public void Nut28_P2BK_Tests() // sender ephemeral keypair var e = new PrivKey("1cedb9df0c6872188b560ace9e35fd55c2532d53e19ae65b46159073886482ca"); var E = new PubKey("02a8cda4cf448bfce9a9e46e588c06ea1780fcb94e3bbdf3277f42995d403a8b0c"); - Assert.Equal(E.Key.ToString()?.ToLowerInvariant(), e.Key.CreatePubKey().ToString()?.ToLowerInvariant()); + Assert.Equal( + E.Key.ToString()?.ToLowerInvariant(), + e.Key.CreatePubKey().ToString()?.ToLowerInvariant() + ); // receiver keypair var p = new PrivKey("ad37e8abd800be3e8272b14045873f4353327eedeb702b72ddcc5c5adff5129c"); var P = new PubKey("02771fed6cb88aaac38b8b32104a942bf4b8f4696bc361171b3c7d06fa2ebddf06"); Assert.Equal(P.Key.ToString()?.ToLowerInvariant(), p.Key.CreatePubKey().ToString()?.ToLowerInvariant()); - + // var kid = new KeysetId("009a1f293253e41e"); var zx = "40d6ba4430a6dfa915bb441579b0f4dee032307434e9957a092bbca73151df8b"; @@ -829,7 +1206,6 @@ public void Nut28_P2BK_Tests() Assert.Equal(rs[i], ri.ToString()); } - string[] blindedPublicKeys = [ "03b7c03eb05a0a539cfc438e81bcf38b65b7bb8685e8790f9b853bfe3d77ad5315", @@ -847,7 +1223,10 @@ public void Nut28_P2BK_Tests() //it's the same blinding as with computeB_ for (int i = 0; i <= 10; i++) { - Assert.Equal(blindedPublicKeys[i], ((PubKey)Cashu.ComputeB_(P, new PrivKey(rs[i]))).ToString()); + Assert.Equal( + blindedPublicKeys[i], + ((PubKey)Cashu.ComputeB_(P, new PrivKey(rs[i]))).ToString() + ); } string[] skStd = @@ -895,7 +1274,6 @@ public void Nut28_P2BK_Tests() Assert.Equal(skNeg[i], Convert.ToHexString(derivedKeyNeg.ToBytes()).ToLowerInvariant()); } - } [Fact] @@ -904,26 +1282,36 @@ public void Nut28_P2BK_Flow() // sender generates ephermal keypair var e = new PrivKey("1cedb9df0c6872188b560ace9e35fd55c2532d53e19ae65b46159073886482ca"); var E = new PubKey("02a8cda4cf448bfce9a9e46e588c06ea1780fcb94e3bbdf3277f42995d403a8b0c"); - - + // receiver privkeys, with corresponding pubkeys that will get blinded - var signing_key = - ECPrivKey.Create(Convert.FromHexString("0000000000000000000000000000000000000000000000000000000000000001")); - var signing_key_two = - ECPrivKey.Create(Convert.FromHexString("7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f")); - - var refundPubkey = - ECPrivKey.Create(Convert.FromHexString("99590802251e78ee1051648439eedb003dc539093a48a44e7b8f2642c909ea37")).CreatePubKey(); + var signing_key = ECPrivKey.Create( + Convert.FromHexString( + "0000000000000000000000000000000000000000000000000000000000000001" + ) + ); + var signing_key_two = ECPrivKey.Create( + Convert.FromHexString( + "7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f" + ) + ); + + var refundPubkey = ECPrivKey + .Create( + Convert.FromHexString( + "99590802251e78ee1051648439eedb003dc539093a48a44e7b8f2642c909ea37" + ) + ) + .CreatePubKey(); var keysetId = new KeysetId("009a1f293253e41e"); - + var conditions = new P2PkBuilder() { Lock = DateTimeOffset.FromUnixTimeSeconds(21000000000), - Pubkeys = new[] {signing_key.CreatePubKey(), signing_key_two.CreatePubKey()}, - RefundPubkeys = new[] {refundPubkey}, + Pubkeys = new[] { signing_key.CreatePubKey(), signing_key_two.CreatePubKey() }, + RefundPubkeys = new[] { refundPubkey }, SignatureThreshold = 2, - SigFlag = "SIG_INPUTS" + SigFlag = "SIG_INPUTS", }; var p2pkProofSecret = conditions.BuildBlinded(e); @@ -935,33 +1323,48 @@ public void Nut28_P2BK_Flow() Amount = 0, Secret = secret, C = "02698c4e2b5f9534cd0687d87513c759790cf829aa5739184a3e3735471fbda904".ToPubKey(), - P2PkE = E + P2PkE = E, }; - var witness = p2pkProofSecret.GenerateBlindWitness(proof, new[] {signing_key, signing_key_two}, E); - + var witness = p2pkProofSecret.GenerateBlindWitness( + proof, + new[] { signing_key, signing_key_two }, + E + ); + Assert.True(p2pkProofSecret.VerifyWitness(secret, witness)); } [Fact] public void Nut28_Flow_WithRandomE() { - var signing_key = - ECPrivKey.Create(Convert.FromHexString("0000000000000000000000000000000000000000000000000000000000000001")); - var signing_key_two = - ECPrivKey.Create(Convert.FromHexString("7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f")); - - var refundPubkey = - ECPrivKey.Create(Convert.FromHexString("99590802251e78ee1051648439eedb003dc539093a48a44e7b8f2642c909ea37")).CreatePubKey(); + var signing_key = ECPrivKey.Create( + Convert.FromHexString( + "0000000000000000000000000000000000000000000000000000000000000001" + ) + ); + var signing_key_two = ECPrivKey.Create( + Convert.FromHexString( + "7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f" + ) + ); + + var refundPubkey = ECPrivKey + .Create( + Convert.FromHexString( + "99590802251e78ee1051648439eedb003dc539093a48a44e7b8f2642c909ea37" + ) + ) + .CreatePubKey(); var keysetId = new KeysetId("009a1f293253e41e"); - + var conditions = new P2PkBuilder() { Lock = DateTimeOffset.FromUnixTimeSeconds(21000000000), - Pubkeys = new[] {signing_key.CreatePubKey(), signing_key_two.CreatePubKey()}, - RefundPubkeys = new[] {refundPubkey}, + Pubkeys = new[] { signing_key.CreatePubKey(), signing_key_two.CreatePubKey() }, + RefundPubkeys = new[] { refundPubkey }, SignatureThreshold = 2, - SigFlag = "SIG_INPUTS" + SigFlag = "SIG_INPUTS", }; var p2pkProofSecret = conditions.BuildBlinded(out var E); @@ -973,10 +1376,14 @@ public void Nut28_Flow_WithRandomE() Amount = 0, Secret = secret, C = "02698c4e2b5f9534cd0687d87513c759790cf829aa5739184a3e3735471fbda904".ToPubKey(), - P2PkE = E + P2PkE = E, }; - var witness = p2pkProofSecret.GenerateBlindWitness(proof, new[] {signing_key, signing_key_two}, E); - + var witness = p2pkProofSecret.GenerateBlindWitness( + proof, + new[] { signing_key, signing_key_two }, + E + ); + Assert.True(p2pkProofSecret.VerifyWitness(secret, witness)); } diff --git a/DotNut.Tests/UnitTests2.cs b/DotNut.Tests/UnitTests2.cs index 948974c..2677df7 100644 --- a/DotNut.Tests/UnitTests2.cs +++ b/DotNut.Tests/UnitTests2.cs @@ -1,13 +1,11 @@ -using System.Diagnostics.Metrics; using System.Text.Json; -using System.Text.Json.Serialization; using DotNut.Abstractions; -using DotNut.Api; using DotNut.ApiModels; using DotNut.NBitcoin.BIP39; using NBitcoin.Secp256k1; namespace DotNut.Tests; + public class UnitTests2 { private static string MintUrl = "http://localhost:3338"; @@ -18,7 +16,7 @@ public void CreatesWalletSuccesfully() var wallet = Wallet.Create(); Assert.NotNull(wallet); } - + [Fact] public async Task ThrowsWhenMintNotFound() { @@ -29,7 +27,7 @@ public async Task ThrowsWhenMintNotFound() await Assert.ThrowsAsync(async () => wallet.CreateMeltQuote()); await Assert.ThrowsAsync(async () => wallet.CreateMintQuote()); } - + [Fact] public void BuilderChainingPreservesAllSettings() { @@ -38,9 +36,11 @@ public void BuilderChainingPreservesAllSettings() var keysets = new GetKeysetsResponse { Keysets = [] }; var keys = new GetKeysResponse { Keysets = [] }; var selector = new ProofSelector(new Dictionary()); - var mnemonic = "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about"; - - var wallet = Wallet.Create() + var mnemonic = + "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about"; + + var wallet = Wallet + .Create() .WithMint(MintUrl) .WithInfo(info) .WithKeysets(keysets) @@ -50,22 +50,28 @@ public void BuilderChainingPreservesAllSettings() .WithCounter(counter) .WithKeysetSync(true) .ShouldBumpCounter(false); - - var mnemonicField = wallet.GetType() - .GetField("_mnemonic", - System.Reflection.BindingFlags.NonPublic | - System.Reflection.BindingFlags.Instance); + + var mnemonicField = wallet + .GetType() + .GetField( + "_mnemonic", + System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance + ); var mnemonicRef = (Mnemonic?)mnemonicField?.GetValue(wallet); - - var counterField = wallet.GetType() - .GetField("_counter",System.Reflection.BindingFlags.NonPublic | - System.Reflection.BindingFlags.Instance); + + var counterField = wallet + .GetType() + .GetField( + "_counter", + System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance + ); var counterRef = (InMemoryCounter?)counterField?.GetValue(wallet); - + Assert.Equal(mnemonic, mnemonicRef.ToString()); Assert.Same(counter, counterRef); Assert.NotNull(wallet.GetInfo()); } + [Fact] public void WithMintStringVariantCreatesHttpClient() { @@ -82,31 +88,31 @@ public async Task InMemoryCounter() var testId1 = new KeysetId("00qwertyuiopasdf"); var ctrNum = await ctr.GetCounterForId(testId1); Assert.Equal((uint)0, ctrNum); - + await ctr.IncrementCounter(testId1); Assert.Equal((uint)0, ctrNum); ctrNum = await ctr.GetCounterForId(testId1); Assert.Equal((uint)1, ctrNum); - + await ctr.IncrementCounter(testId1, 5); ctrNum = await ctr.GetCounterForId(testId1); Assert.Equal((uint)6, ctrNum); - + await ctr.SetCounter(testId1, 1337); ctrNum = await ctr.GetCounterForId(testId1); Assert.Equal((uint)1337, ctrNum); } - + [Fact] public void SplitAmountsForPayment_ExactAmount_ReturnsCorrectSplit() { var amounts = Utils.SplitToProofsAmounts(30, _testKeyset); - Assert.Equal(new List(){16, 8, 4, 2}, amounts); - + Assert.Equal(new List() { 16, 8, 4, 2 }, amounts); } private Keyset? _testKeyset = JsonSerializer.Deserialize( - "{\n \"1\": \"03ba786a2c0745f8c30e490288acd7a72dd53d65afd292ddefa326a4a3fa14c566\",\n \"2\": \"03361cd8bd1329fea797a6add1cf1990ffcf2270ceb9fc81eeee0e8e9c1bd0cdf5\",\n \"4\": \"036e378bcf78738ddf68859293c69778035740e41138ab183c94f8fee7572214c7\",\n \"8\": \"03909d73beaf28edfb283dbeb8da321afd40651e8902fcf5454ecc7d69788626c0\",\n \"16\": \"028a36f0e6638ea7466665fe174d958212723019ec08f9ce6898d897f88e68aa5d\",\n \"32\": \"03a97a40e146adee2687ac60c2ba2586a90f970de92a9d0e6cae5a4b9965f54612\",\n \"64\": \"03ce86f0c197aab181ddba0cfc5c5576e11dfd5164d9f3d4a3fc3ffbbf2e069664\",\n \"128\": \"0284f2c06d938a6f78794814c687560a0aabab19fe5e6f30ede38e113b132a3cb9\",\n \"256\": \"03b99f475b68e5b4c0ba809cdecaae64eade2d9787aa123206f91cd61f76c01459\",\n \"512\": \"03d4db82ea19a44d35274de51f78af0a710925fe7d9e03620b84e3e9976e3ac2eb\",\n \"1024\": \"031fbd4ba801870871d46cf62228a1b748905ebc07d3b210daf48de229e683f2dc\",\n \"2048\": \"0276cedb9a3b160db6a158ad4e468d2437f021293204b3cd4bf6247970d8aff54b\",\n \"4096\": \"02fc6b89b403ee9eb8a7ed457cd3973638080d6e04ca8af7307c965c166b555ea2\",\n \"8192\": \"0320265583e916d3a305f0d2687fcf2cd4e3cd03a16ea8261fda309c3ec5721e21\",\n \"16384\": \"036e41de58fdff3cb1d8d713f48c63bc61fa3b3e1631495a444d178363c0d2ed50\",\n \"32768\": \"0365438f613f19696264300b069d1dad93f0c60a37536b72a8ab7c7366a5ee6c04\",\n \"65536\": \"02408426cfb6fc86341bac79624ba8708a4376b2d92debdf4134813f866eb57a8d\",\n \"131072\": \"031063e9f11c94dc778c473e968966eac0e70b7145213fbaff5f7a007e71c65f41\",\n \"262144\": \"02f2a3e808f9cd168ec71b7f328258d0c1dda250659c1aced14c7f5cf05aab4328\",\n \"524288\": \"038ac10de9f1ff9395903bb73077e94dbf91e9ef98fd77d9a2debc5f74c575bc86\",\n \"1048576\": \"0203eaee4db749b0fc7c49870d082024b2c31d889f9bc3b32473d4f1dfa3625788\",\n \"2097152\": \"033cdb9d36e1e82ae652b7b6a08e0204569ec7ff9ebf85d80a02786dc7fe00b04c\",\n \"4194304\": \"02c8b73f4e3a470ae05e5f2fe39984d41e9f6ae7be9f3b09c9ac31292e403ac512\",\n \"8388608\": \"025bbe0cfce8a1f4fbd7f3a0d4a09cb6badd73ef61829dc827aa8a98c270bc25b0\",\n \"16777216\": \"037eec3d1651a30a90182d9287a5c51386fe35d4a96839cf7969c6e2a03db1fc21\",\n \"33554432\": \"03280576b81a04e6abd7197f305506476f5751356b7643988495ca5c3e14e5c262\",\n \"67108864\": \"03268bfb05be1dbb33ab6e7e00e438373ca2c9b9abc018fdb452d0e1a0935e10d3\",\n \"134217728\": \"02573b68784ceba9617bbcc7c9487836d296aa7c628c3199173a841e7a19798020\",\n \"268435456\": \"0234076b6e70f7fbf755d2227ecc8d8169d662518ee3a1401f729e2a12ccb2b276\",\n \"536870912\": \"03015bd88961e2a466a2163bd4248d1d2b42c7c58a157e594785e7eb34d880efc9\",\n \"1073741824\": \"02c9b076d08f9020ebee49ac8ba2610b404d4e553a4f800150ceb539e9421aaeee\",\n \"2147483648\": \"034d592f4c366afddc919a509600af81b489a03caf4f7517c2b3f4f2b558f9a41a\",\n \"4294967296\": \"037c09ecb66da082981e4cbdb1ac65c0eb631fc75d85bed13efb2c6364148879b5\",\n \"8589934592\": \"02b4ebb0dda3b9ad83b39e2e31024b777cc0ac205a96b9a6cfab3edea2912ed1b3\",\n \"17179869184\": \"026cc4dacdced45e63f6e4f62edbc5779ccd802e7fabb82d5123db879b636176e9\",\n \"34359738368\": \"02b2cee01b7d8e90180254459b8f09bbea9aad34c3a2fd98c85517ecfc9805af75\",\n \"68719476736\": \"037a0c0d564540fc574b8bfa0253cca987b75466e44b295ed59f6f8bd41aace754\",\n \"137438953472\": \"021df6585cae9b9ca431318a713fd73dbb76b3ef5667957e8633bca8aaa7214fb6\",\n \"274877906944\": \"02b8f53dde126f8c85fa5bb6061c0be5aca90984ce9b902966941caf963648d53a\",\n \"549755813888\": \"029cc8af2840d59f1d8761779b2496623c82c64be8e15f9ab577c657c6dd453785\",\n \"1099511627776\": \"03e446fdb84fad492ff3a25fc1046fb9a93a5b262ebcd0151caa442ea28959a38a\",\n \"2199023255552\": \"02d6b25bd4ab599dd0818c55f75702fde603c93f259222001246569018842d3258\",\n \"4398046511104\": \"03397b522bb4e156ec3952d3f048e5a986c20a00718e5e52cd5718466bf494156a\",\n \"8796093022208\": \"02d1fb9e78262b5d7d74028073075b80bb5ab281edcfc3191061962c1346340f1e\",\n \"17592186044416\": \"030d3f2ad7a4ca115712ff7f140434f802b19a4c9b2dd1c76f3e8e80c05c6a9310\",\n \"35184372088832\": \"03e325b691f292e1dfb151c3fb7cad440b225795583c32e24e10635a80e4221c06\",\n \"70368744177664\": \"03bee8f64d88de3dee21d61f89efa32933da51152ddbd67466bef815e9f93f8fd1\",\n \"140737488355328\": \"0327244c9019a4892e1f04ba3bf95fe43b327479e2d57c25979446cc508cd379ed\",\n \"281474976710656\": \"02fb58522cd662f2f8b042f8161caae6e45de98283f74d4e99f19b0ea85e08a56d\",\n \"562949953421312\": \"02adde4b466a9d7e59386b6a701a39717c53f30c4810613c1b55e6b6da43b7bc9a\",\n \"1125899906842624\": \"038eeda11f78ce05c774f30e393cda075192b890d68590813ff46362548528dca9\",\n \"2251799813685248\": \"02ec13e0058b196db80f7079d329333b330dc30c000dbdd7397cbbc5a37a664c4f\",\n \"4503599627370496\": \"02d2d162db63675bd04f7d56df04508840f41e2ad87312a3c93041b494efe80a73\",\n \"9007199254740992\": \"0356969d6aef2bb40121dbd07c68b6102339f4ea8e674a9008bb69506795998f49\",\n \"18014398509481984\": \"02f4e667567ebb9f4e6e180a4113bb071c48855f657766bb5e9c776a880335d1d6\",\n \"36028797018963968\": \"0385b4fe35e41703d7a657d957c67bb536629de57b7e6ee6fe2130728ef0fc90b0\",\n \"72057594037927936\": \"02b2bc1968a6fddbcc78fb9903940524824b5f5bed329c6ad48a19b56068c144fd\",\n \"144115188075855872\": \"02e0dbb24f1d288a693e8a49bc14264d1276be16972131520cf9e055ae92fba19a\",\n \"288230376151711744\": \"03efe75c106f931a525dc2d653ebedddc413a2c7d8cb9da410893ae7d2fa7d19cc\",\n \"576460752303423488\": \"02c7ec2bd9508a7fc03f73c7565dc600b30fd86f3d305f8f139c45c404a52d958a\",\n \"1152921504606846976\": \"035a6679c6b25e68ff4e29d1c7ef87f21e0a8fc574f6a08c1aa45ff352c1d59f06\",\n \"2305843009213693952\": \"033cdc225962c052d485f7cfbf55a5b2367d200fe1fe4373a347deb4cc99e9a099\",\n \"4611686018427387904\": \"024a4b806cf413d14b294719090a9da36ba75209c7657135ad09bc65328fba9e6f\",\n \"9223372036854775808\": \"0377a6fe114e291a8d8e991627c38001c8305b23b9e98b1c7b1893f5cd0dda6cad\"\n}"); + "{\n \"1\": \"03ba786a2c0745f8c30e490288acd7a72dd53d65afd292ddefa326a4a3fa14c566\",\n \"2\": \"03361cd8bd1329fea797a6add1cf1990ffcf2270ceb9fc81eeee0e8e9c1bd0cdf5\",\n \"4\": \"036e378bcf78738ddf68859293c69778035740e41138ab183c94f8fee7572214c7\",\n \"8\": \"03909d73beaf28edfb283dbeb8da321afd40651e8902fcf5454ecc7d69788626c0\",\n \"16\": \"028a36f0e6638ea7466665fe174d958212723019ec08f9ce6898d897f88e68aa5d\",\n \"32\": \"03a97a40e146adee2687ac60c2ba2586a90f970de92a9d0e6cae5a4b9965f54612\",\n \"64\": \"03ce86f0c197aab181ddba0cfc5c5576e11dfd5164d9f3d4a3fc3ffbbf2e069664\",\n \"128\": \"0284f2c06d938a6f78794814c687560a0aabab19fe5e6f30ede38e113b132a3cb9\",\n \"256\": \"03b99f475b68e5b4c0ba809cdecaae64eade2d9787aa123206f91cd61f76c01459\",\n \"512\": \"03d4db82ea19a44d35274de51f78af0a710925fe7d9e03620b84e3e9976e3ac2eb\",\n \"1024\": \"031fbd4ba801870871d46cf62228a1b748905ebc07d3b210daf48de229e683f2dc\",\n \"2048\": \"0276cedb9a3b160db6a158ad4e468d2437f021293204b3cd4bf6247970d8aff54b\",\n \"4096\": \"02fc6b89b403ee9eb8a7ed457cd3973638080d6e04ca8af7307c965c166b555ea2\",\n \"8192\": \"0320265583e916d3a305f0d2687fcf2cd4e3cd03a16ea8261fda309c3ec5721e21\",\n \"16384\": \"036e41de58fdff3cb1d8d713f48c63bc61fa3b3e1631495a444d178363c0d2ed50\",\n \"32768\": \"0365438f613f19696264300b069d1dad93f0c60a37536b72a8ab7c7366a5ee6c04\",\n \"65536\": \"02408426cfb6fc86341bac79624ba8708a4376b2d92debdf4134813f866eb57a8d\",\n \"131072\": \"031063e9f11c94dc778c473e968966eac0e70b7145213fbaff5f7a007e71c65f41\",\n \"262144\": \"02f2a3e808f9cd168ec71b7f328258d0c1dda250659c1aced14c7f5cf05aab4328\",\n \"524288\": \"038ac10de9f1ff9395903bb73077e94dbf91e9ef98fd77d9a2debc5f74c575bc86\",\n \"1048576\": \"0203eaee4db749b0fc7c49870d082024b2c31d889f9bc3b32473d4f1dfa3625788\",\n \"2097152\": \"033cdb9d36e1e82ae652b7b6a08e0204569ec7ff9ebf85d80a02786dc7fe00b04c\",\n \"4194304\": \"02c8b73f4e3a470ae05e5f2fe39984d41e9f6ae7be9f3b09c9ac31292e403ac512\",\n \"8388608\": \"025bbe0cfce8a1f4fbd7f3a0d4a09cb6badd73ef61829dc827aa8a98c270bc25b0\",\n \"16777216\": \"037eec3d1651a30a90182d9287a5c51386fe35d4a96839cf7969c6e2a03db1fc21\",\n \"33554432\": \"03280576b81a04e6abd7197f305506476f5751356b7643988495ca5c3e14e5c262\",\n \"67108864\": \"03268bfb05be1dbb33ab6e7e00e438373ca2c9b9abc018fdb452d0e1a0935e10d3\",\n \"134217728\": \"02573b68784ceba9617bbcc7c9487836d296aa7c628c3199173a841e7a19798020\",\n \"268435456\": \"0234076b6e70f7fbf755d2227ecc8d8169d662518ee3a1401f729e2a12ccb2b276\",\n \"536870912\": \"03015bd88961e2a466a2163bd4248d1d2b42c7c58a157e594785e7eb34d880efc9\",\n \"1073741824\": \"02c9b076d08f9020ebee49ac8ba2610b404d4e553a4f800150ceb539e9421aaeee\",\n \"2147483648\": \"034d592f4c366afddc919a509600af81b489a03caf4f7517c2b3f4f2b558f9a41a\",\n \"4294967296\": \"037c09ecb66da082981e4cbdb1ac65c0eb631fc75d85bed13efb2c6364148879b5\",\n \"8589934592\": \"02b4ebb0dda3b9ad83b39e2e31024b777cc0ac205a96b9a6cfab3edea2912ed1b3\",\n \"17179869184\": \"026cc4dacdced45e63f6e4f62edbc5779ccd802e7fabb82d5123db879b636176e9\",\n \"34359738368\": \"02b2cee01b7d8e90180254459b8f09bbea9aad34c3a2fd98c85517ecfc9805af75\",\n \"68719476736\": \"037a0c0d564540fc574b8bfa0253cca987b75466e44b295ed59f6f8bd41aace754\",\n \"137438953472\": \"021df6585cae9b9ca431318a713fd73dbb76b3ef5667957e8633bca8aaa7214fb6\",\n \"274877906944\": \"02b8f53dde126f8c85fa5bb6061c0be5aca90984ce9b902966941caf963648d53a\",\n \"549755813888\": \"029cc8af2840d59f1d8761779b2496623c82c64be8e15f9ab577c657c6dd453785\",\n \"1099511627776\": \"03e446fdb84fad492ff3a25fc1046fb9a93a5b262ebcd0151caa442ea28959a38a\",\n \"2199023255552\": \"02d6b25bd4ab599dd0818c55f75702fde603c93f259222001246569018842d3258\",\n \"4398046511104\": \"03397b522bb4e156ec3952d3f048e5a986c20a00718e5e52cd5718466bf494156a\",\n \"8796093022208\": \"02d1fb9e78262b5d7d74028073075b80bb5ab281edcfc3191061962c1346340f1e\",\n \"17592186044416\": \"030d3f2ad7a4ca115712ff7f140434f802b19a4c9b2dd1c76f3e8e80c05c6a9310\",\n \"35184372088832\": \"03e325b691f292e1dfb151c3fb7cad440b225795583c32e24e10635a80e4221c06\",\n \"70368744177664\": \"03bee8f64d88de3dee21d61f89efa32933da51152ddbd67466bef815e9f93f8fd1\",\n \"140737488355328\": \"0327244c9019a4892e1f04ba3bf95fe43b327479e2d57c25979446cc508cd379ed\",\n \"281474976710656\": \"02fb58522cd662f2f8b042f8161caae6e45de98283f74d4e99f19b0ea85e08a56d\",\n \"562949953421312\": \"02adde4b466a9d7e59386b6a701a39717c53f30c4810613c1b55e6b6da43b7bc9a\",\n \"1125899906842624\": \"038eeda11f78ce05c774f30e393cda075192b890d68590813ff46362548528dca9\",\n \"2251799813685248\": \"02ec13e0058b196db80f7079d329333b330dc30c000dbdd7397cbbc5a37a664c4f\",\n \"4503599627370496\": \"02d2d162db63675bd04f7d56df04508840f41e2ad87312a3c93041b494efe80a73\",\n \"9007199254740992\": \"0356969d6aef2bb40121dbd07c68b6102339f4ea8e674a9008bb69506795998f49\",\n \"18014398509481984\": \"02f4e667567ebb9f4e6e180a4113bb071c48855f657766bb5e9c776a880335d1d6\",\n \"36028797018963968\": \"0385b4fe35e41703d7a657d957c67bb536629de57b7e6ee6fe2130728ef0fc90b0\",\n \"72057594037927936\": \"02b2bc1968a6fddbcc78fb9903940524824b5f5bed329c6ad48a19b56068c144fd\",\n \"144115188075855872\": \"02e0dbb24f1d288a693e8a49bc14264d1276be16972131520cf9e055ae92fba19a\",\n \"288230376151711744\": \"03efe75c106f931a525dc2d653ebedddc413a2c7d8cb9da410893ae7d2fa7d19cc\",\n \"576460752303423488\": \"02c7ec2bd9508a7fc03f73c7565dc600b30fd86f3d305f8f139c45c404a52d958a\",\n \"1152921504606846976\": \"035a6679c6b25e68ff4e29d1c7ef87f21e0a8fc574f6a08c1aa45ff352c1d59f06\",\n \"2305843009213693952\": \"033cdc225962c052d485f7cfbf55a5b2367d200fe1fe4373a347deb4cc99e9a099\",\n \"4611686018427387904\": \"024a4b806cf413d14b294719090a9da36ba75209c7657135ad09bc65328fba9e6f\",\n \"9223372036854775808\": \"0377a6fe114e291a8d8e991627c38001c8305b23b9e98b1c7b1893f5cd0dda6cad\"\n}" + ); private static KeysetId _testKeysetId = new KeysetId("000f01df73ea149a"); @@ -121,10 +127,7 @@ public void SumProofs_EmptyList_ReturnsZero() [Fact] public void SumProofs_SingleProof_ReturnsAmount() { - var proofs = new List - { - new Proof { Amount = 64 } - }; + var proofs = new List { new Proof { Amount = 64 } }; var sum = Utils.SumProofs(proofs); Assert.Equal(64UL, sum); } @@ -138,7 +141,7 @@ public void SumProofs_MultipleProofs_ReturnsCorrectSum() new Proof { Amount = 2 }, new Proof { Amount = 4 }, new Proof { Amount = 8 }, - new Proof { Amount = 16 } + new Proof { Amount = 16 }, }; var sum = Utils.SumProofs(proofs); Assert.Equal(31UL, sum); @@ -154,7 +157,10 @@ public void SumProofs_MultipleProofs_ReturnsCorrectSum() [InlineData(64UL, new ulong[] { 64 })] [InlineData(100UL, new ulong[] { 64, 32, 4 })] [InlineData(1337UL, new ulong[] { 1024, 256, 32, 16, 8, 1 })] - public void SplitToProofsAmounts_VariousAmounts_ReturnsCorrectSplit(ulong amount, ulong[] expected) + public void SplitToProofsAmounts_VariousAmounts_ReturnsCorrectSplit( + ulong amount, + ulong[] expected + ) { var result = Utils.SplitToProofsAmounts(amount, _testKeyset!); Assert.Equal(expected.ToList(), result); @@ -190,13 +196,13 @@ public void CreateOutputs_ValidAmounts_ReturnsCorrectOutputData() { var amounts = new List { 1, 2, 4 }; var outputs = Utils.CreateOutputs(amounts, _testKeysetId, _testKeyset!); - + Assert.Equal(3, outputs.Count); - + Assert.Equal(1UL, outputs[0].BlindedMessage.Amount); Assert.Equal(2UL, outputs[1].BlindedMessage.Amount); Assert.Equal(4UL, outputs[2].BlindedMessage.Amount); - + Assert.All(outputs, o => Assert.Equal(_testKeysetId, o.BlindedMessage.Id)); } @@ -204,23 +210,27 @@ public void CreateOutputs_ValidAmounts_ReturnsCorrectOutputData() public void CreateOutputs_InvalidAmount_ThrowsException() { var amounts = new List { 1, 3 }; // 3 is not a valid amount - Assert.Throws(() => Utils.CreateOutputs(amounts, _testKeysetId, _testKeyset!)); + Assert.Throws(() => + Utils.CreateOutputs(amounts, _testKeysetId, _testKeyset!) + ); } [Fact] public void CreateOutputs_DeterministicWithMnemonic() { - var mnemonic = new Mnemonic("abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about"); + var mnemonic = new Mnemonic( + "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about" + ); var amounts = new List { 1, 2, 4 }; - + var outputs1 = Utils.CreateOutputs(amounts, _testKeysetId, _testKeyset!, mnemonic, 0); var outputs2 = Utils.CreateOutputs(amounts, _testKeysetId, _testKeyset!, mnemonic, 0); - + // Same mnemonic and counter should produce same outputs for (int i = 0; i < outputs1.Count; i++) { Assert.Equal( - ((StringSecret)outputs1[i].Secret).Secret, + ((StringSecret)outputs1[i].Secret).Secret, ((StringSecret)outputs2[i].Secret).Secret ); } @@ -230,10 +240,10 @@ public void CreateOutputs_DeterministicWithMnemonic() public void CreateOutputs_RandomWithoutMnemonic() { var amounts = new List { 1 }; - + var outputs1 = Utils.CreateOutputs(amounts, _testKeysetId, _testKeyset!); var outputs2 = Utils.CreateOutputs(amounts, _testKeysetId, _testKeyset!); - + // without mnemonic, outputs should be random (different) Assert.NotEqual( ((StringSecret)outputs1[0].Secret).Secret, @@ -245,29 +255,49 @@ private static PubKey CreateTestPubKey(int seed) { var seedBytes = new byte[32]; BitConverter.GetBytes(seed).CopyTo(seedBytes, 0); - seedBytes[31] = 1; + seedBytes[31] = 1; var privKey = ECPrivKey.Create(seedBytes); ECPubKey ecPubKey = privKey.CreatePubKey(); - return ecPubKey; + return ecPubKey; } - + [Fact] public async Task ProofSelector_ExactMatch_SelectsCorrectProofs() { var keysetId = _testKeysetId; var fees = new Dictionary { { keysetId, 0 } }; var selector = new ProofSelector(fees); - + var proofs = new List { - new Proof { Amount = 1, Id = keysetId, C = CreateTestPubKey(1) }, - new Proof { Amount = 2, Id = keysetId, C = CreateTestPubKey(2) }, - new Proof { Amount = 4, Id = keysetId, C = CreateTestPubKey(3) }, - new Proof { Amount = 8, Id = keysetId, C = CreateTestPubKey(4) }, + new Proof + { + Amount = 1, + Id = keysetId, + C = CreateTestPubKey(1), + }, + new Proof + { + Amount = 2, + Id = keysetId, + C = CreateTestPubKey(2), + }, + new Proof + { + Amount = 4, + Id = keysetId, + C = CreateTestPubKey(3), + }, + new Proof + { + Amount = 8, + Id = keysetId, + C = CreateTestPubKey(4), + }, }; - + var result = await selector.SelectProofsToSend(proofs, 7, false); - + Assert.Equal(7UL, Utils.SumProofs(result.Send)); Assert.Equal(8UL, Utils.SumProofs(result.Keep)); } @@ -278,15 +308,25 @@ public async Task ProofSelector_InsufficientFunds_ReturnsEmptySend() var keysetId = _testKeysetId; var fees = new Dictionary { { keysetId, 0 } }; var selector = new ProofSelector(fees); - + var proofs = new List { - new Proof { Amount = 1, Id = keysetId, C = CreateTestPubKey(1) }, - new Proof { Amount = 2, Id = keysetId, C = CreateTestPubKey(2) }, + new Proof + { + Amount = 1, + Id = keysetId, + C = CreateTestPubKey(1), + }, + new Proof + { + Amount = 2, + Id = keysetId, + C = CreateTestPubKey(2), + }, }; - + var result = await selector.SelectProofsToSend(proofs, 100, false); - + Assert.Empty(result.Send); Assert.Equal(2, result.Keep.Count); } @@ -297,14 +337,19 @@ public async Task ProofSelector_ZeroAmount_ReturnsEmptySend() var keysetId = _testKeysetId; var fees = new Dictionary { { keysetId, 0 } }; var selector = new ProofSelector(fees); - + var proofs = new List { - new Proof { Amount = 8, Id = keysetId, C = CreateTestPubKey(1) }, + new Proof + { + Amount = 8, + Id = keysetId, + C = CreateTestPubKey(1), + }, }; - + var result = await selector.SelectProofsToSend(proofs, 0, false); - + Assert.Empty(result.Send); } @@ -314,18 +359,43 @@ public async Task ProofSelector_WithFees_AccountsForFees() var keysetId = _testKeysetId; var fees = new Dictionary { { keysetId, 1000 } }; // 1 sat per proof var selector = new ProofSelector(fees); - + var proofs = new List { - new Proof { Amount = 1, Id = keysetId, C = CreateTestPubKey(1) }, - new Proof { Amount = 2, Id = keysetId, C = CreateTestPubKey(2) }, - new Proof { Amount = 4, Id = keysetId, C = CreateTestPubKey(3) }, - new Proof { Amount = 8, Id = keysetId, C = CreateTestPubKey(4) }, - new Proof { Amount = 16, Id = keysetId, C = CreateTestPubKey(5) }, + new Proof + { + Amount = 1, + Id = keysetId, + C = CreateTestPubKey(1), + }, + new Proof + { + Amount = 2, + Id = keysetId, + C = CreateTestPubKey(2), + }, + new Proof + { + Amount = 4, + Id = keysetId, + C = CreateTestPubKey(3), + }, + new Proof + { + Amount = 8, + Id = keysetId, + C = CreateTestPubKey(4), + }, + new Proof + { + Amount = 16, + Id = keysetId, + C = CreateTestPubKey(5), + }, }; - + var result = await selector.SelectProofsToSend(proofs, 10, true); - + Assert.True(Utils.SumProofs(result.Send) >= 10); } @@ -335,14 +405,19 @@ public async Task ProofSelector_SingleLargeProof_SelectsIt() var keysetId = _testKeysetId; var fees = new Dictionary { { keysetId, 0 } }; var selector = new ProofSelector(fees); - + var proofs = new List { - new Proof { Amount = 100, Id = keysetId, C = CreateTestPubKey(1) }, + new Proof + { + Amount = 100, + Id = keysetId, + C = CreateTestPubKey(1), + }, }; - + var result = await selector.SelectProofsToSend(proofs, 50, false); - + Assert.Single(result.Send); Assert.Equal(100UL, result.Send[0].Amount); Assert.Empty(result.Keep); @@ -354,31 +429,29 @@ public void TokenEncode_V4_RoundTrip() var keysetId = new KeysetId("00ffd48b8f5ecf80"); var proofs = new List { - new Proof - { - Amount = 1, + new Proof + { + Amount = 1, Id = keysetId, - Secret = new StringSecret("acc12435e7b8484c3cf1850149218af90f716a52bf4a5ed347e48ecc13f77388"), - C = "0244538319de485d55bed3b29a642bee5879375ab9e7a620e11e48ba482421f3cf".ToPubKey() - } + Secret = new StringSecret( + "acc12435e7b8484c3cf1850149218af90f716a52bf4a5ed347e48ecc13f77388" + ), + C = "0244538319de485d55bed3b29a642bee5879375ab9e7a620e11e48ba482421f3cf".ToPubKey(), + }, }; - + var token = new CashuToken { Unit = "sat", Tokens = new List { - new CashuToken.Token - { - Mint = "http://localhost:3338", - Proofs = proofs - } - } + new CashuToken.Token { Mint = "http://localhost:3338", Proofs = proofs }, + }, }; - + var encoded = token.Encode("B", false); Assert.StartsWith("cashuB", encoded); - + var decoded = CashuTokenHelper.Decode(encoded, out var version); Assert.Equal("B", version); Assert.Equal("sat", decoded.Unit); @@ -401,7 +474,7 @@ public void KeysetId_Equality() var id1 = new KeysetId("009a1f293253e41e"); var id2 = new KeysetId("009a1f293253e41e"); var id3 = new KeysetId("000f01df73ea149a"); - + Assert.Equal(id1, id2); Assert.NotEqual(id1, id3); Assert.True(id1 == id2); @@ -412,8 +485,10 @@ public void KeysetId_Equality() public void KeysetId_GetVersion() { var v0Id = new KeysetId("009a1f293253e41e"); - var v1Id = new KeysetId("01adc013fa9d85171586660abab27579888611659d357bc86bc09cb26eee8bc035"); - + var v1Id = new KeysetId( + "01adc013fa9d85171586660abab27579888611659d357bc86bc09cb26eee8bc035" + ); + Assert.Equal(0x00, v0Id.GetVersion()); Assert.Equal(0x01, v1Id.GetVersion()); } @@ -423,13 +498,13 @@ public void ComputeFee_NoFees_ReturnsZero() { var keysetId = _testKeysetId; var fees = new Dictionary { { keysetId, 0 } }; - + var proofs = new List { new Proof { Amount = 1, Id = keysetId }, new Proof { Amount = 2, Id = keysetId }, }; - + var fee = proofs.ComputeFee(fees); Assert.Equal(0UL, fee); } @@ -439,14 +514,14 @@ public void ComputeFee_WithFees_ReturnsCorrectFee() { var keysetId = _testKeysetId; var fees = new Dictionary { { keysetId, 1000 } }; // 1 sat per proof (1000 ppk) - + var proofs = new List { new Proof { Amount = 1, Id = keysetId }, new Proof { Amount = 2, Id = keysetId }, new Proof { Amount = 4, Id = keysetId }, }; - + var fee = proofs.ComputeFee(fees); Assert.Equal(3UL, fee); // 3 proofs * 1 sat } @@ -464,10 +539,11 @@ public void SendResponse_DefaultsToEmptyLists() [Fact] public void Wallet_WithKeysetSyncThreshold_SetsCorrectly() { - var wallet = Wallet.Create() + var wallet = Wallet + .Create() .WithMint(MintUrl) .WithKeysetSync(true, TimeSpan.FromMinutes(30)); - + Assert.NotNull(wallet); } @@ -475,10 +551,8 @@ public void Wallet_WithKeysetSyncThreshold_SetsCorrectly() public void Wallet_ShouldBumpCounter_Default() { var counter = new InMemoryCounter(); - var wallet = Wallet.Create() - .WithMint(MintUrl) - .WithCounter(counter); - + var wallet = Wallet.Create().WithMint(MintUrl).WithCounter(counter); + Assert.NotNull(wallet); } @@ -486,11 +560,12 @@ public void Wallet_ShouldBumpCounter_Default() public void Wallet_ShouldBumpCounter_Disabled() { var counter = new InMemoryCounter(); - var wallet = Wallet.Create() + var wallet = Wallet + .Create() .WithMint(MintUrl) .WithCounter(counter) .ShouldBumpCounter(false); - + Assert.NotNull(wallet); } @@ -501,28 +576,29 @@ public void MintInfo_FromGetInfoResponse() { Version = "0.15.0", Name = "Test Mint", - Description = "A test mint" + Description = "A test mint", }; - + var info = new MintInfo(response); Assert.NotNull(info); } - [Fact] public void P2PkBuilder_Build_CreatesValidSecret() { - var privKey = new PrivKey("0000000000000000000000000000000000000000000000000000000000000001"); + var privKey = new PrivKey( + "0000000000000000000000000000000000000000000000000000000000000001" + ); var builder = new P2PkBuilder { Pubkeys = [privKey.Key.CreatePubKey()], SignatureThreshold = 1, - SigFlag = "SIG_INPUTS" + SigFlag = "SIG_INPUTS", }; - + var secret = builder.Build(); Assert.NotNull(secret); - + var allowedPubkeys = secret.GetAllowedPubkeys(out var threshold); Assert.Single(allowedPubkeys); Assert.Equal(1, threshold); @@ -531,19 +607,23 @@ public void P2PkBuilder_Build_CreatesValidSecret() [Fact] public void P2PkBuilder_WithMultisig_Build() { - var privKey1 = new PrivKey("0000000000000000000000000000000000000000000000000000000000000001"); - var privKey2 = new PrivKey("0000000000000000000000000000000000000000000000000000000000000002"); - + var privKey1 = new PrivKey( + "0000000000000000000000000000000000000000000000000000000000000001" + ); + var privKey2 = new PrivKey( + "0000000000000000000000000000000000000000000000000000000000000002" + ); + var builder = new P2PkBuilder { Pubkeys = [privKey1.Key.CreatePubKey(), privKey2.Key.CreatePubKey()], SignatureThreshold = 2, - SigFlag = "SIG_INPUTS" + SigFlag = "SIG_INPUTS", }; - + var secret = builder.Build(); var allowedPubkeys = secret.GetAllowedPubkeys(out var threshold); - + Assert.Equal(2, allowedPubkeys.Count()); Assert.Equal(2, threshold); } @@ -555,18 +635,20 @@ public void HTLCBuilder_Build_CreatesValidSecret() using var sha = System.Security.Cryptography.SHA256.Create(); var hashLockBytes = sha.ComputeHash(Convert.FromHexString(preimage)); var hashLock = Convert.ToHexString(hashLockBytes).ToLower(); - var privKey = new PrivKey("0000000000000000000000000000000000000000000000000000000000000001"); - + var privKey = new PrivKey( + "0000000000000000000000000000000000000000000000000000000000000001" + ); + var builder = new HTLCBuilder { HashLock = hashLock, Pubkeys = [privKey.Key.CreatePubKey()], - SignatureThreshold = 1 + SignatureThreshold = 1, }; - + var secret = builder.Build(); Assert.NotNull(secret); - + var allowedPubkeys = secret.GetAllowedPubkeys(out var threshold); Assert.Single(allowedPubkeys); } @@ -575,7 +657,7 @@ public void HTLCBuilder_Build_CreatesValidSecret() public async Task Wallet_ThrowsOnMissingMint_ForAllOperations() { var wallet = Wallet.Create(); - + await Assert.ThrowsAsync(() => wallet.GetInfo()); Assert.Throws(() => wallet.CreateMintQuote()); Assert.Throws(() => wallet.CreateMeltQuote()); @@ -588,7 +670,7 @@ public async Task Counter_ReturnsZeroForUnknownKeysetId() { var counter = new InMemoryCounter(); var unknownKeysetId = new KeysetId("00unknown1234567"); - + var value = await counter.GetCounterForId(unknownKeysetId); Assert.Equal((uint)0, value); } @@ -599,13 +681,11 @@ public async Task Counter_MultipleKeysets_IndependentCounters() var counter = new InMemoryCounter(); var keysetId1 = new KeysetId("00keyset11234567"); var keysetId2 = new KeysetId("00keyset21234567"); - + await counter.IncrementCounter(keysetId1, 10); await counter.IncrementCounter(keysetId2, 20); - + Assert.Equal((uint)10, await counter.GetCounterForId(keysetId1)); Assert.Equal((uint)20, await counter.GetCounterForId(keysetId2)); } } - - diff --git a/DotNut/Abstractions/Handlers/MeltHandlerBolt11.cs b/DotNut/Abstractions/Handlers/MeltHandlerBolt11.cs index 5e1ba3c..25146ae 100644 --- a/DotNut/Abstractions/Handlers/MeltHandlerBolt11.cs +++ b/DotNut/Abstractions/Handlers/MeltHandlerBolt11.cs @@ -7,34 +7,45 @@ public class MeltHandlerBolt11( PostMeltQuoteBolt11Response quote, List blankOutputs, List? privKeys = null, - string? htlcPreimage = null) - : IMeltHandler> + string? htlcPreimage = null +) : IMeltHandler> { public PostMeltQuoteBolt11Response GetQuote() => quote; + public async Task> Melt(List inputs, CancellationToken ct = default) { //we're operating on copy here since later the proof state is mutated in stripFingerprints var proofs = inputs.DeepCopyList(); - - Nut10Helper.MaybeProcessNut10(privKeys??[], proofs, blankOutputs, htlcPreimage, quote.Quote); + + Nut10Helper.MaybeProcessNut10( + privKeys ?? [], + proofs, + blankOutputs, + htlcPreimage, + quote.Quote + ); //since nut10 (with p2bk) is processed, now it's safe to strip P2PkE - proofs.ForEach(i=>i.StripFingerprints()); - + proofs.ForEach(i => i.StripFingerprints()); + var client = await wallet.GetMintApi(ct); var req = new PostMeltRequest { Quote = quote.Quote, Inputs = proofs.ToArray(), - Outputs = blankOutputs.Select(bo=> bo.BlindedMessage).ToArray(), + Outputs = blankOutputs.Select(bo => bo.BlindedMessage).ToArray(), }; - - var res = await client.Melt("bolt11", req, ct); - if (res.Change == null || res.Change.Length == 0) - { - return []; - } - var keyset = await wallet.GetKeys(res.Change.First().Id, true, false, ct); - return Utils.ConstructProofsFromPromises(res.Change.ToList(), blankOutputs, keyset.Keys); + var res = await client.Melt( + "bolt11", + req, + ct + ); + if (res.Change == null || res.Change.Length == 0) + { + return []; + } + + var keyset = await wallet.GetKeys(res.Change.First().Id, true, false, ct); + return Utils.ConstructProofsFromPromises(res.Change.ToList(), blankOutputs, keyset.Keys); } -} \ No newline at end of file +} diff --git a/DotNut/Abstractions/Handlers/MeltHandlerBolt12.cs b/DotNut/Abstractions/Handlers/MeltHandlerBolt12.cs index e9d7a57..e5e0cef 100644 --- a/DotNut/Abstractions/Handlers/MeltHandlerBolt12.cs +++ b/DotNut/Abstractions/Handlers/MeltHandlerBolt12.cs @@ -8,27 +8,38 @@ public class MeltHandlerBolt12( PostMeltQuoteBolt12Response quote, List blankOutputs, List? privKeys = null, - string? htlcPreimage = null) - : IMeltHandler> + string? htlcPreimage = null +) : IMeltHandler> { public PostMeltQuoteBolt12Response GetQuote() => quote; + public async Task> Melt(List inputs, CancellationToken ct = default) { //we're operating on copy here since later the proof state is mutated in stripFingerprints var proofs = inputs.DeepCopyList(); - - Nut10Helper.MaybeProcessNut10(privKeys??[], proofs, blankOutputs, htlcPreimage, quote.Quote); - proofs.ForEach(i=>i.StripFingerprints()); - + + Nut10Helper.MaybeProcessNut10( + privKeys ?? [], + proofs, + blankOutputs, + htlcPreimage, + quote.Quote + ); + proofs.ForEach(i => i.StripFingerprints()); + var client = await wallet.GetMintApi(ct); var req = new PostMeltRequest { Quote = quote.Quote, Inputs = proofs.ToArray(), - Outputs = blankOutputs.Select(bo=>bo.BlindedMessage).ToArray(), + Outputs = blankOutputs.Select(bo => bo.BlindedMessage).ToArray(), }; - - var res = await client.Melt("bolt12", req, ct); + + var res = await client.Melt( + "bolt12", + req, + ct + ); if (res.Change == null || res.Change.Length == 0) { return []; @@ -37,4 +48,4 @@ public async Task> Melt(List inputs, CancellationToken ct = d var keyset = await wallet.GetKeys(res.Change.First().Id, true, false, ct); return Utils.ConstructProofsFromPromises(res.Change.ToList(), blankOutputs, keyset.Keys); } -} \ No newline at end of file +} diff --git a/DotNut/Abstractions/Handlers/MintHandlerBolt11.cs b/DotNut/Abstractions/Handlers/MintHandlerBolt11.cs index 9f9a8fa..ab08895 100644 --- a/DotNut/Abstractions/Handlers/MintHandlerBolt11.cs +++ b/DotNut/Abstractions/Handlers/MintHandlerBolt11.cs @@ -6,8 +6,8 @@ public class MintHandlerBolt11( IWalletBuilder wallet, PostMintQuoteBolt11Response postMintQuoteBolt11Response, GetKeysResponse.KeysetItemResponse keyset, - List outputs) - : IMintHandler> + List outputs +) : IMintHandler> { private string? _signature; @@ -24,29 +24,38 @@ public IMintHandler> SignWithPrivkey(st public IMintHandler> SignWithPrivkey(PrivKey privkey) { - this._signature = privkey.SignMintQuote(postMintQuoteBolt11Response.Quote, outputs.Select(o=>o.BlindedMessage).ToList()); + this._signature = privkey.SignMintQuote( + postMintQuoteBolt11Response.Quote, + outputs.Select(o => o.BlindedMessage).ToList() + ); return this; } - + public PostMintQuoteBolt11Response GetQuote() => postMintQuoteBolt11Response; public async Task> Mint(CancellationToken ct = default) { if (postMintQuoteBolt11Response.PubKey is not null && this._signature is null) { - throw new ArgumentNullException(nameof(_signature),$"Signature for mint quote {postMintQuoteBolt11Response.Quote} is required!" ); + throw new ArgumentNullException( + nameof(_signature), + $"Signature for mint quote {postMintQuoteBolt11Response.Quote} is required!" + ); } var client = await wallet.GetMintApi(ct); var req = new PostMintRequest { - Outputs = outputs.Select(o=>o.BlindedMessage).ToArray(), + Outputs = outputs.Select(o => o.BlindedMessage).ToArray(), Quote = postMintQuoteBolt11Response.Quote, Signature = _signature, }; - - var promises= await client.Mint("bolt11", req, ct); - return Utils.ConstructProofsFromPromises(promises.Signatures.ToList(), outputs, keyset.Keys); - } -} \ No newline at end of file + var promises = await client.Mint("bolt11", req, ct); + return Utils.ConstructProofsFromPromises( + promises.Signatures.ToList(), + outputs, + keyset.Keys + ); + } +} diff --git a/DotNut/Abstractions/Handlers/MintHandlerBolt12.cs b/DotNut/Abstractions/Handlers/MintHandlerBolt12.cs index 2661f17..7e00381 100644 --- a/DotNut/Abstractions/Handlers/MintHandlerBolt12.cs +++ b/DotNut/Abstractions/Handlers/MintHandlerBolt12.cs @@ -1,15 +1,15 @@ using DotNut.ApiModels; using DotNut.ApiModels.Mint.bolt12; + namespace DotNut.Abstractions.Handlers; public class MintHandlerBolt12( IWalletBuilder wallet, PostMintQuoteBolt12Response quote, GetKeysResponse.KeysetItemResponse keyset, - List outputs) - : IMintHandler> + List outputs +) : IMintHandler> { - private string? _signature; public IMintHandler> WithSignature(string signature) @@ -25,28 +25,38 @@ public IMintHandler> SignWithPrivkey(st public IMintHandler> SignWithPrivkey(PrivKey privkey) { - this._signature = privkey.SignMintQuote(quote.Quote, outputs.Select(o=>o.BlindedMessage).ToList()); + this._signature = privkey.SignMintQuote( + quote.Quote, + outputs.Select(o => o.BlindedMessage).ToList() + ); return this; } - + public PostMintQuoteBolt12Response GetQuote() => quote; - + public async Task> Mint(CancellationToken ct = default) { if (this._signature is null) { - throw new ArgumentNullException(nameof(this._signature), $"Signature for mint quote {quote.Quote} is required!"); + throw new ArgumentNullException( + nameof(this._signature), + $"Signature for mint quote {quote.Quote} is required!" + ); } - + var client = await wallet.GetMintApi(ct); var req = new PostMintRequest { - Outputs = outputs.Select(o=>o.BlindedMessage).ToArray(), + Outputs = outputs.Select(o => o.BlindedMessage).ToArray(), Quote = quote.Quote, Signature = _signature, }; - - var promises= await client.Mint("bolt12", req, ct); - return Utils.ConstructProofsFromPromises(promises.Signatures.ToList(), outputs, keyset.Keys); + + var promises = await client.Mint("bolt12", req, ct); + return Utils.ConstructProofsFromPromises( + promises.Signatures.ToList(), + outputs, + keyset.Keys + ); } -} \ No newline at end of file +} diff --git a/DotNut/Abstractions/InMemoryCounter.cs b/DotNut/Abstractions/InMemoryCounter.cs index ee0e9fe..6b1983e 100644 --- a/DotNut/Abstractions/InMemoryCounter.cs +++ b/DotNut/Abstractions/InMemoryCounter.cs @@ -5,6 +5,7 @@ namespace DotNut.Abstractions; public class InMemoryCounter : ICounter { private readonly ConcurrentDictionary _counter; + public InMemoryCounter(IDictionary counter) { this._counter = new ConcurrentDictionary(counter); @@ -20,20 +21,24 @@ public Task GetCounterForId(KeysetId keysetId, CancellationToken ct = defa return Task.FromResult(_counter.GetOrAdd(keysetId, 0)); } - public Task IncrementCounter(KeysetId keysetId, uint bumpBy = 1, CancellationToken ct = default) + public Task IncrementCounter( + KeysetId keysetId, + uint bumpBy = 1, + CancellationToken ct = default + ) { var next = _counter.AddOrUpdate(keysetId, bumpBy, (_, current) => current + bumpBy); return Task.FromResult(next); } - public Task SetCounter(KeysetId keysetId, uint counter, CancellationToken ct = default) { + public Task SetCounter(KeysetId keysetId, uint counter, CancellationToken ct = default) + { _counter[keysetId] = counter; return Task.CompletedTask; } - + public async Task> Export() { return new Dictionary(_counter); } - -} \ No newline at end of file +} diff --git a/DotNut/Abstractions/Interfaces/ICounter.cs b/DotNut/Abstractions/Interfaces/ICounter.cs index a460331..81f4c07 100644 --- a/DotNut/Abstractions/Interfaces/ICounter.cs +++ b/DotNut/Abstractions/Interfaces/ICounter.cs @@ -10,7 +10,11 @@ public interface ICounter /// /// public Task GetCounterForId(KeysetId keysetId, CancellationToken ct = default); - public Task IncrementCounter(KeysetId keysetId, uint bumpBy = 1, CancellationToken ct = default); + public Task IncrementCounter( + KeysetId keysetId, + uint bumpBy = 1, + CancellationToken ct = default + ); public Task SetCounter(KeysetId keysetId, uint counter, CancellationToken ct = default); public Task> Export(); -} \ No newline at end of file +} diff --git a/DotNut/Abstractions/Interfaces/IMeltHandler.cs b/DotNut/Abstractions/Interfaces/IMeltHandler.cs index 98166be..5944a15 100644 --- a/DotNut/Abstractions/Interfaces/IMeltHandler.cs +++ b/DotNut/Abstractions/Interfaces/IMeltHandler.cs @@ -2,8 +2,8 @@ namespace DotNut.Abstractions; public interface IMeltHandler; -public interface IMeltHandler: IMeltHandler +public interface IMeltHandler : IMeltHandler { TQuote GetQuote(); Task Melt(List inputs, CancellationToken ct = default); -} \ No newline at end of file +} diff --git a/DotNut/Abstractions/Interfaces/IMeltQuoteBuilder.cs b/DotNut/Abstractions/Interfaces/IMeltQuoteBuilder.cs index ad133d0..06ef3b2 100644 --- a/DotNut/Abstractions/Interfaces/IMeltQuoteBuilder.cs +++ b/DotNut/Abstractions/Interfaces/IMeltQuoteBuilder.cs @@ -43,11 +43,14 @@ public interface IMeltQuoteBuilder /// /// Create a bolt11 melt handler. /// - Task>> ProcessAsyncBolt11(CancellationToken ct = default); + Task>> ProcessAsyncBolt11( + CancellationToken ct = default + ); /// /// Create a bolt12 melt handler. /// - Task>> ProcessAsyncBolt12(CancellationToken ct = default); - -} \ No newline at end of file + Task>> ProcessAsyncBolt12( + CancellationToken ct = default + ); +} diff --git a/DotNut/Abstractions/Interfaces/IMintHandler.cs b/DotNut/Abstractions/Interfaces/IMintHandler.cs index b5c0cc9..0dc5744 100644 --- a/DotNut/Abstractions/Interfaces/IMintHandler.cs +++ b/DotNut/Abstractions/Interfaces/IMintHandler.cs @@ -1,12 +1,13 @@ namespace DotNut.Abstractions; public interface IMintHandler; -public interface IMintHandler: IMintHandler + +public interface IMintHandler : IMintHandler { public IMintHandler WithSignature(string signature); public IMintHandler SignWithPrivkey(PrivKey privkey); public IMintHandler SignWithPrivkey(string privKeyHex); - + TQuote GetQuote(); Task Mint(CancellationToken ct = default); -} \ No newline at end of file +} diff --git a/DotNut/Abstractions/Interfaces/IMintQuoteBuilder.cs b/DotNut/Abstractions/Interfaces/IMintQuoteBuilder.cs index 04c47b5..a6fc50b 100644 --- a/DotNut/Abstractions/Interfaces/IMintQuoteBuilder.cs +++ b/DotNut/Abstractions/Interfaces/IMintQuoteBuilder.cs @@ -19,21 +19,22 @@ public interface IMintQuoteBuilder IMintQuoteBuilder WithAmount(ulong amount); /// - /// Optional for bolt11 and mandatory for bolt12. + /// Optional for bolt11 and mandatory for bolt12. /// /// /// IMintQuoteBuilder WithPubkey(string pubkey); + /// - /// Optional for bolt11 and mandatory for bolt12. + /// Optional for bolt11 and mandatory for bolt12. /// IMintQuoteBuilder WithPubkey(PubKey pubkey); - + /// /// Optional. Provide precomputed outputs so blinding factors and secrets are reused safely. /// IMintQuoteBuilder WithOutputs(List outputs); - + /// /// Optional. Provide description for the mint invoice. /// @@ -43,7 +44,7 @@ public interface IMintQuoteBuilder /// Optional. Allows providing a P2PK builder when a signature is required for minting. /// IMintQuoteBuilder WithP2PkLock(P2PkBuilder p2pkBuilder); - + /// /// Optional. When minting P2Pk / HTLC Proofs allows to blind the pubkeys. /// @@ -59,11 +60,14 @@ public interface IMintQuoteBuilder /// /// Creates a bolt11 mint quote and handler. /// - Task>> ProcessAsyncBolt11(CancellationToken ct = default); + Task>> ProcessAsyncBolt11( + CancellationToken ct = default + ); /// /// Creates a bolt12 mint quote and handler. /// - Task>> ProcessAsyncBolt12(CancellationToken ct = default); - -} \ No newline at end of file + Task>> ProcessAsyncBolt12( + CancellationToken ct = default + ); +} diff --git a/DotNut/Abstractions/Interfaces/IProofSelector.cs b/DotNut/Abstractions/Interfaces/IProofSelector.cs index 9b7f76d..61ca92c 100644 --- a/DotNut/Abstractions/Interfaces/IProofSelector.cs +++ b/DotNut/Abstractions/Interfaces/IProofSelector.cs @@ -1,6 +1,11 @@ namespace DotNut.Abstractions; public interface IProofSelector -{ - Task SelectProofsToSend(List proofs, ulong amountToSend, bool includeFees = false, CancellationToken ct = default); -} \ No newline at end of file +{ + Task SelectProofsToSend( + List proofs, + ulong amountToSend, + bool includeFees = false, + CancellationToken ct = default + ); +} diff --git a/DotNut/Abstractions/Interfaces/IRestoreBuilder.cs b/DotNut/Abstractions/Interfaces/IRestoreBuilder.cs index 27a48c9..71e4e2f 100644 --- a/DotNut/Abstractions/Interfaces/IRestoreBuilder.cs +++ b/DotNut/Abstractions/Interfaces/IRestoreBuilder.cs @@ -12,6 +12,6 @@ public interface IRestoreBuilder /// /// IRestoreBuilder FromKeysetIds(IEnumerable keysetIds); - + Task> ProcessAsync(CancellationToken ct = default); } diff --git a/DotNut/Abstractions/Interfaces/IWalletBuilder.cs b/DotNut/Abstractions/Interfaces/IWalletBuilder.cs index fba913f..61e2a3a 100644 --- a/DotNut/Abstractions/Interfaces/IWalletBuilder.cs +++ b/DotNut/Abstractions/Interfaces/IWalletBuilder.cs @@ -15,7 +15,7 @@ public interface IWalletBuilder : IDisposable /// /// Mint API object. IWalletBuilder WithMint(ICashuApi mintApi); - + /// /// Mandatory. Sets a mint in a wallet object (with default CashuHttpClient) /// @@ -27,43 +27,43 @@ public interface IWalletBuilder : IDisposable /// /// Mint URI. IWalletBuilder WithMint(Uri mintUri); - + /// /// Optional. Import Mint Info to CashuWallet. Otherwise, it will be fetched from /v1/info endpoint. /// /// MintInfo object IWalletBuilder WithInfo(MintInfo info); - + /// /// Optional. Import Mint Info to CashuWallet. Otherwise, it will be fetched from /v1/info endpoint. /// /// GetInfoResponse payload returned from mints API IWalletBuilder WithInfo(GetInfoResponse info); - + /// /// Optional. Import Keysets into CashuWallet class. Otherwise, they will be fetched from /v1/keysets endpoint. /// /// List of Keysets IWalletBuilder WithKeysets(IEnumerable keysets); - + /// /// Optional. Import Keysets into CashuWallet class. Otherwise, they will be fetched from /v1/keysets endpoint. /// /// GetKeysetsResponse payload returned from mints API IWalletBuilder WithKeysets(GetKeysetsResponse keysets); - + /// /// Optional. Import Keys into CashuWallet class. Otherwise, they will be fetched from /v1/keys endpoint. /// /// List of mints Keys IWalletBuilder WithKeys(IEnumerable keys); - + /// /// Optional. Import Keys into CashuWallet class. Otherwise, they will be fetched from /v1/keys endpoint. /// /// GetKeysResponse payload returned from mints API IWalletBuilder WithKeys(GetKeysResponse keys); - + /// /// Optional. Flag suggesting if CashuWallet should sync provided Keys and Keysets with actual mints state. /// Very useful if wallet stores keys in storage. @@ -80,25 +80,25 @@ public interface IWalletBuilder : IDisposable /// /// IWalletBuilder WithKeysetSync(bool syncKeyset, TimeSpan syncThreshold); - + /// /// Optional. Proof selecting algorithm. If not set, defaults to RGLI proof selector. /// /// IWalletBuilder WithSelector(IProofSelector selector); - + /// /// Optional. BIP39 seed for secret and blinding factors derivation. All proofs generated by CashuWallet will be recoverable. /// /// Mnemonic object - IWalletBuilder WithMnemonic(Mnemonic mnemonic); - + IWalletBuilder WithMnemonic(Mnemonic mnemonic); + /// /// Optional. BIP39 seed for secret and blinding factors derivation. All proofs generated by CashuWallet will be recoverable. /// /// Bip39 seed string separated by spaces. IWalletBuilder WithMnemonic(string mnemonic); - + /// /// Optional and mandatory if Mnemonic provided. Counter for each Keyset Id for derivation purposes. /// @@ -111,17 +111,17 @@ public interface IWalletBuilder : IDisposable /// Counter dictionary /// public IWalletBuilder WithCounter(IDictionary counter); - + /// /// Optional and if not set, always true. Controls automatic counter incrementation for secret generation. /// /// If true, counter increments automatically. If false, requires manual management. /// - /// WARNING: Disabling auto-increment is potentially dangerous. Manual counter management is required + /// WARNING: Disabling auto-increment is potentially dangerous. Manual counter management is required /// to prevent secret reuse, which will cause mint rejection and operation failures. /// IWalletBuilder ShouldBumpCounter(bool shouldBumpCounter = true); - + /// /// Optional. /// Adds websocket service. You should use single websocket service (singleton at best) for multiple wallets, in order to handle everything in nice manner. @@ -130,15 +130,15 @@ public interface IWalletBuilder : IDisposable /// /// IWalletBuilder WithWebsocketService(IWebsocketService websocketService); - + /// - /// Get Mints info, supported methods etc. + /// Get Mints info, supported methods etc. /// /// Refetch flag /// /// MintInfo object Task GetInfo(bool forceRefresh = false, CancellationToken ct = default); - + /// /// Create Outputs (BlindedMessags, Blinding Factors, Secrets), for given keysetId. /// Deterministic if Mnemonic and Counter set up. @@ -148,8 +148,12 @@ public interface IWalletBuilder : IDisposable /// /// Outputs /// If keys not set. If Mnemonic set, but no Counter. - Task> CreateOutputs(List amounts, KeysetId id, CancellationToken ct = default); - + Task> CreateOutputs( + List amounts, + KeysetId id, + CancellationToken ct = default + ); + /// /// Create Outputs for active KeysetId for given unit. Fetches a keyset for given unit automatically. /// @@ -158,13 +162,17 @@ public interface IWalletBuilder : IDisposable /// /// Outputs /// If no keysetID stored in wallet. - Task> CreateOutputs(List amounts, string unit, CancellationToken ct = default); + Task> CreateOutputs( + List amounts, + string unit, + CancellationToken ct = default + ); /// /// Set Last sync date to DateTime.MinValue - keysets will be synced before next operation /// public void InvalidateCache(); - + /// /// Get active keyset id for chosen unit. /// @@ -172,23 +180,28 @@ public interface IWalletBuilder : IDisposable /// /// Active keysetId Task GetActiveKeysetId(string unit, CancellationToken ct = default); - + /// /// Get active keyset ids for each supported unit /// /// Dictionary of (unit, KeysetId) - Task?> GetActiveKeysetIdsWithUnits(CancellationToken ct = default); + Task?> GetActiveKeysetIdsWithUnits( + CancellationToken ct = default + ); Task GetMintApi(CancellationToken ct = default); - + /// /// Get keys of current mint stored in wallet. /// /// Refetch flag /// /// Mints keys - Task> GetKeys(bool forceRefresh = false, CancellationToken ct = default); - + Task> GetKeys( + bool forceRefresh = false, + CancellationToken ct = default + ); + /// /// Get Keys for given KeysetID. At first it tries to find corresponding keys, if allowFetch is true, will try to /// fetch keys if not present in wallet. @@ -198,8 +211,12 @@ public interface IWalletBuilder : IDisposable /// Refetch flag /// /// Keys for given keyset - Task GetKeys(KeysetId id, bool allowFetch, bool forceRefresh = false, - CancellationToken ct = default); + Task GetKeys( + KeysetId id, + bool allowFetch, + bool forceRefresh = false, + CancellationToken ct = default + ); /// /// Get Keysets stored in wallet @@ -207,9 +224,11 @@ public interface IWalletBuilder : IDisposable /// Refetch flag /// /// List of Keysets - Task> GetKeysets(bool forceRefresh = false, - CancellationToken ct = default); - + Task> GetKeysets( + bool forceRefresh = false, + CancellationToken ct = default + ); + /// /// Select proofs for sending purposes. By default uses RGLI algorithm, unless another one provided. /// @@ -218,17 +237,20 @@ public interface IWalletBuilder : IDisposable /// /// /// + Task SelectProofsToSend( + List proofs, + ulong amount, + bool includeFees, + CancellationToken ct = default + ); - Task SelectProofsToSend(List proofs, ulong amount, bool includeFees, - CancellationToken ct = default); - /// /// Getter for proof selector. If not set, returns RGLI algorithm by default. /// /// /// Task GetSelector(CancellationToken ct = default); - + /// /// Returns websocket service, that can be shared between multiple wallets. /// @@ -241,25 +263,25 @@ Task SelectProofsToSend(List proofs, ulong amount, bool inc /// /// ICounter? GetCounter(); - + /// /// Create swap transaction builder. /// /// Swap transaction builder ISwapBuilder Swap(); - + /// /// Create melt quote builder. /// /// IMeltQuoteBuilder CreateMeltQuote(); - + /// /// Create Mint Quote /// /// Method-agnostic Mint Quote builder abstraction. IMintQuoteBuilder CreateMintQuote(); - + /// /// Can restore proofs if mnemonic provided. /// @@ -267,17 +289,17 @@ Task SelectProofsToSend(List proofs, ulong amount, bool inc IRestoreBuilder Restore(); /// - /// Check state of provided proofs. + /// Check state of provided proofs. /// /// - Task CheckState(IEnumerable proofs, CancellationToken ct = default); - + Task CheckState( + IEnumerable proofs, + CancellationToken ct = default + ); + /// - /// Check state of provided proofs. + /// Check state of provided proofs. /// /// Task CheckState(IEnumerable Ys, CancellationToken ct = default); - - } - diff --git a/DotNut/Abstractions/Interfaces/IWebsocketService.cs b/DotNut/Abstractions/Interfaces/IWebsocketService.cs index 1d6333b..77b55cb 100644 --- a/DotNut/Abstractions/Interfaces/IWebsocketService.cs +++ b/DotNut/Abstractions/Interfaces/IWebsocketService.cs @@ -11,10 +11,15 @@ public interface IWebsocketService : IAsyncDisposable event EventHandler? ConnectionStateChanged; Task LazyConnectAsync(string mintUrl, CancellationToken ct = default); - + Task DisconnectAsync(string connectionId, CancellationToken ct = default); - Task SubscribeAsync(string connectionId, SubscriptionKind kind, string[] filters, CancellationToken ct = default); + Task SubscribeAsync( + string connectionId, + SubscriptionKind kind, + string[] filters, + CancellationToken ct = default + ); Task UnsubscribeAsync(string subId, CancellationToken ct = default); diff --git a/DotNut/Abstractions/MeltQuoteBuilder.cs b/DotNut/Abstractions/MeltQuoteBuilder.cs index 3ab9628..8deae7a 100644 --- a/DotNut/Abstractions/MeltQuoteBuilder.cs +++ b/DotNut/Abstractions/MeltQuoteBuilder.cs @@ -13,10 +13,10 @@ class MeltQuoteBuilder : IMeltQuoteBuilder private string _unit = "sat"; private ulong? _amount; - + private List? _privKeys; private string? _htlcPreimage; - + public MeltQuoteBuilder(Wallet wallet) { _wallet = wallet; @@ -27,19 +27,18 @@ public IMeltQuoteBuilder WithInvoice(string invoice) this._invoice = invoice; return this; } - + public IMeltQuoteBuilder WithUnit(string unit) { this._unit = unit; return this; } - + public IMeltQuoteBuilder WithBlankOutputs(List blankOutputs) { this._blankOutputs = blankOutputs; return this; } - // when proofs were p2pk public IMeltQuoteBuilder WithPrivKeys(IEnumerable privKeys) @@ -60,33 +59,29 @@ public IMeltQuoteBuilder WithAmount(ulong msat) return this; } - public async Task>> ProcessAsyncBolt11(CancellationToken ct = default) + public async Task>> ProcessAsyncBolt11( + CancellationToken ct = default + ) { var mintApi = await _wallet.GetMintApi(ct); await _wallet._maybeSyncKeys(ct); ArgumentNullException.ThrowIfNull(this._invoice); - - var req = new PostMeltQuoteBolt11Request - { - Request = this._invoice, - Unit = this._unit, - }; + + var req = new PostMeltQuoteBolt11Request { Request = this._invoice, Unit = this._unit }; if (this._amount != null) { req.Options = new MeltQuoteRequestOptions { - Amountless = new AmountlessMeltQuoteOptions - { - AmountMsat = this._amount.Value, - } + Amountless = new AmountlessMeltQuoteOptions { AmountMsat = this._amount.Value }, }; } - var quote = - await mintApi.CreateMeltQuote("bolt11", req, ct); - - + var quote = await mintApi.CreateMeltQuote< + PostMeltQuoteBolt11Response, + PostMeltQuoteBolt11Request + >("bolt11", req, ct); + if (_blankOutputs == null) { var outputsAmount = Utils.CalculateNumberOfBlankOutputs((ulong)quote.FeeReserve); @@ -95,35 +90,30 @@ public async Task>> Proces } return new MeltHandlerBolt11(_wallet, quote, _blankOutputs, _privKeys, _htlcPreimage); } - + public async Task>> ProcessAsyncBolt12( - CancellationToken ct = default) + CancellationToken ct = default + ) { var mintApi = await _wallet.GetMintApi(ct); await _wallet._maybeSyncKeys(ct); ArgumentNullException.ThrowIfNull(this._invoice); - var req = new PostMeltQuoteBolt12Request() - { - Request = this._invoice, - Unit = this._unit, - }; - + var req = new PostMeltQuoteBolt12Request() { Request = this._invoice, Unit = this._unit }; + if (this._amount != null) { req.Options = new MeltQuoteRequestOptions { - Amountless = new AmountlessMeltQuoteOptions - { - AmountMsat = this._amount.Value, - } + Amountless = new AmountlessMeltQuoteOptions { AmountMsat = this._amount.Value }, }; } - - var quote = - await mintApi.CreateMeltQuote("bolt12", req, ct); - - + + var quote = await mintApi.CreateMeltQuote< + PostMeltQuoteBolt12Response, + PostMeltQuoteBolt12Request + >("bolt12", req, ct); + if (_blankOutputs == null) { var outputsAmount = Utils.CalculateNumberOfBlankOutputs((ulong)quote.FeeReserve); @@ -133,4 +123,3 @@ public async Task>> Proces return new MeltHandlerBolt12(_wallet, quote, _blankOutputs, _privKeys, _htlcPreimage); } } - diff --git a/DotNut/Abstractions/MintInfo.cs b/DotNut/Abstractions/MintInfo.cs index b897e7e..4029fed 100644 --- a/DotNut/Abstractions/MintInfo.cs +++ b/DotNut/Abstractions/MintInfo.cs @@ -15,7 +15,7 @@ public class MintInfo public MintInfo(GetInfoResponse info) { _mintInfo = info; - + if (info.Nuts?.TryGetValue(22, out var nut22Json) == true) { try @@ -26,14 +26,17 @@ public MintInfo(GetInfoResponse info) _protectedEndpoints = new ProtectedEndpoints { Cache = new ConcurrentDictionary(), - ApiReturn = nut22.ProtectedEndpoints.Select(o => new ProtectedEndpoint - { - Method = o.Method, - Regex = new Regex( - o.Path, - RegexOptions.None, - TimeSpan.FromMilliseconds(100)) - }).ToArray() + ApiReturn = nut22 + .ProtectedEndpoints.Select(o => new ProtectedEndpoint + { + Method = o.Method, + Regex = new Regex( + o.Path, + RegexOptions.None, + TimeSpan.FromMilliseconds(100) + ), + }) + .ToArray(), }; } } @@ -43,15 +46,18 @@ public MintInfo(GetInfoResponse info) } } } - + /// /// Checks support for NUTs 4 and 5 (mint/melt operations) /// public SwapInfo IsSupportedMintMelt(int nutNumber) { if (nutNumber != 4 && nutNumber != 5) - throw new ArgumentException("Only NUT 4 and 5 are supported by this method", nameof(nutNumber)); - + throw new ArgumentException( + "Only NUT 4 and 5 are supported by this method", + nameof(nutNumber) + ); + return CheckMintMelt(nutNumber); } @@ -62,8 +68,11 @@ public GenericNut IsSupportedGeneric(int nutNumber) { var supportedNuts = new[] { 7, 8, 9, 10, 11, 12, 14, 20 }; if (!supportedNuts.Contains(nutNumber)) - throw new ArgumentException($"NUT {nutNumber} is not supported by this method", nameof(nutNumber)); - + throw new ArgumentException( + $"NUT {nutNumber} is not supported by this method", + nameof(nutNumber) + ); + return CheckGenericNut(nutNumber); } @@ -94,9 +103,8 @@ public bool RequiresBlindAuthToken(string path) if (_protectedEndpoints.Cache.TryGetValue(path, out var cachedValue)) return cachedValue; - var isProtectedEndpoint = _protectedEndpoints.ApiReturn - .Any(e => e.Regex.IsMatch(path)); - + var isProtectedEndpoint = _protectedEndpoints.ApiReturn.Any(e => e.Regex.IsMatch(path)); + _protectedEndpoints.Cache[path] = isProtectedEndpoint; return isProtectedEndpoint; } @@ -127,32 +135,16 @@ private SwapInfo CheckMintMelt(int nutNumber) var nut = JsonSerializer.Deserialize(nutJson.RootElement.GetRawText()); if (nut?.Methods != null && nut.Methods.Length > 0 && nut.Disabled != true) { - return new SwapInfo - { - Disabled = false, - Methods = nut.Methods - }; + return new SwapInfo { Disabled = false, Methods = nut.Methods }; } - return new SwapInfo - { - Disabled = true, - Methods = nut?.Methods ?? [] - }; + return new SwapInfo { Disabled = true, Methods = nut?.Methods ?? [] }; } catch (JsonException) { - return new SwapInfo - { - Disabled = true, - Methods = [] - }; + return new SwapInfo { Disabled = true, Methods = [] }; } } - return new SwapInfo - { - Disabled = true, - Methods = [] - }; + return new SwapInfo { Disabled = true, Methods = [] }; } private WebSocketSupportResult CheckNut17() @@ -161,14 +153,12 @@ private WebSocketSupportResult CheckNut17() { try { - var nut = JsonSerializer.Deserialize(nutJson.RootElement.GetRawText()); + var nut = JsonSerializer.Deserialize( + nutJson.RootElement.GetRawText() + ); if (nut?.Supported != null && nut.Supported.Length > 0) { - return new WebSocketSupportResult - { - Supported = true, - Params = nut.Supported - }; + return new WebSocketSupportResult { Supported = true, Params = nut.Supported }; } } catch (JsonException) @@ -188,11 +178,7 @@ private MppSupport CheckNut15() var nut = JsonSerializer.Deserialize(nutJson.RootElement.GetRawText()); if (nut?.Methods != null && nut.Methods.Length > 0) { - return new MppSupport - { - Supported = true, - Methods = nut.Methods - }; + return new MppSupport { Supported = true, Methods = nut.Methods }; } } catch (JsonException) @@ -202,7 +188,7 @@ private MppSupport CheckNut15() } return new MppSupport() { Supported = false }; } - + public bool SupportsBolt12Description { get @@ -211,9 +197,12 @@ public bool SupportsBolt12Description { try { - var nut4 = JsonSerializer.Deserialize(nut4Json.RootElement.GetRawText()); - return nut4?.Methods?.Any(method => - method.Method == "bolt12" && method.Options?.Description == true) == true; + var nut4 = JsonSerializer.Deserialize( + nut4Json.RootElement.GetRawText() + ); + return nut4?.Methods?.Any(method => + method.Method == "bolt12" && method.Options?.Description == true + ) == true; } catch (JsonException) { @@ -224,7 +213,6 @@ public bool SupportsBolt12Description } } - public List? Contact => _mintInfo.Contact; public string? Description => _mintInfo.Description; public string? DescriptionLong => _mintInfo.DescriptionLong; @@ -246,7 +234,7 @@ public class MintMeltNut { [JsonPropertyName("methods")] public SwapInfo.SwapMethod[]? Methods { get; set; } - + [JsonPropertyName("disabled")] public bool? Disabled { get; set; } } @@ -267,7 +255,7 @@ public class ProtectedEndpointSpec { [JsonPropertyName("method")] public string Method { get; set; } = string.Empty; - + [JsonPropertyName("path")] public string Path { get; set; } = string.Empty; } diff --git a/DotNut/Abstractions/MintQuoteBuilder.cs b/DotNut/Abstractions/MintQuoteBuilder.cs index 05b88e6..9f22d0f 100644 --- a/DotNut/Abstractions/MintQuoteBuilder.cs +++ b/DotNut/Abstractions/MintQuoteBuilder.cs @@ -9,7 +9,7 @@ namespace DotNut.Abstractions; class MintQuoteBuilder : IMintQuoteBuilder { private readonly Wallet _wallet; - + private ulong? _amount; private List? _amounts; private string _unit = "sat"; @@ -20,7 +20,7 @@ class MintQuoteBuilder : IMintQuoteBuilder private KeysetId? _keysetId; private GetKeysResponse.KeysetItemResponse? _keyset; - + //for p2pk private P2PkBuilder? _builder; private bool _shouldBlind = false; @@ -60,7 +60,6 @@ public IMintQuoteBuilder WithKeyset(KeysetId keysetId) return this; } - public IMintQuoteBuilder WithOutputs(List outputs) { this._outputs = outputs; @@ -90,32 +89,40 @@ public IMintQuoteBuilder WithDescription(string description) this._description = description; return this; } - + public async Task>> ProcessAsyncBolt11( - CancellationToken ct = default) + CancellationToken ct = default + ) { - //todo implement info + //todo implement info await this._wallet._maybeSyncKeys(ct); if (_amount == null) { - throw new ArgumentNullException(nameof(_amount), "can't create mint quote without amount!"); + throw new ArgumentNullException( + nameof(_amount), + "can't create mint quote without amount!" + ); } var api = await this._wallet.GetMintApi(ct); if (api is null) { - throw new ArgumentNullException(nameof(ICashuApi), "Can't request mint quote without mint API"); + throw new ArgumentNullException( + nameof(ICashuApi), + "Can't request mint quote without mint API" + ); } - this._keysetId ??= await this._wallet.GetActiveKeysetId(this._unit, ct) ?? - throw new ArgumentException($"Can't get active keyset ID for unit: {_unit}"); + this._keysetId ??= + await this._wallet.GetActiveKeysetId(this._unit, ct) + ?? throw new ArgumentException($"Can't get active keyset ID for unit: {_unit}"); - this._keyset ??= await this._wallet.GetKeys(this._keysetId, true, false, ct) ?? - throw new ArgumentException($"Cant get keys for keysetId: {_keysetId}"); + this._keyset ??= + await this._wallet.GetKeys(this._keysetId, true, false, ct) + ?? throw new ArgumentException($"Cant get keys for keysetId: {_keysetId}"); var outputs = await this._createOutputs(); - var reqBolt11 = new PostMintQuoteBolt11Request() { @@ -124,44 +131,55 @@ public async Task>> Proces Description = this._description, Pubkey = this._pubkey, }; - var quoteBolt11 = - await api.CreateMintQuote("bolt11", reqBolt11, - ct); + var quoteBolt11 = await api.CreateMintQuote< + PostMintQuoteBolt11Response, + PostMintQuoteBolt11Request + >("bolt11", reqBolt11, ct); return new MintHandlerBolt11(this._wallet, quoteBolt11, this._keyset, outputs); } public async Task>> ProcessAsyncBolt12( - CancellationToken ct = default) + CancellationToken ct = default + ) { await this._wallet._maybeSyncKeys(ct); - + var api = await this._wallet.GetMintApi(); if (api is null) { - throw new ArgumentNullException(nameof(ICashuApi), "Can't request mint quote without mint API"); + throw new ArgumentNullException( + nameof(ICashuApi), + "Can't request mint quote without mint API" + ); } - + if (this._pubkey == null) { - throw new ArgumentNullException(nameof(_pubkey), "Can't request bolt12 mint quote without pubkey!"); + throw new ArgumentNullException( + nameof(_pubkey), + "Can't request bolt12 mint quote without pubkey!" + ); } if (this._amount == null) { - throw new ArgumentNullException(nameof(_amount), "Can't create bolt12 mint quote without amount!"); - + throw new ArgumentNullException( + nameof(_amount), + "Can't create bolt12 mint quote without amount!" + ); } - - this._keysetId ??= await this._wallet.GetActiveKeysetId(this._unit, ct) ?? - throw new ArgumentException($"Can't get active keyset ID for unit: {_unit}"); - + + this._keysetId ??= + await this._wallet.GetActiveKeysetId(this._unit, ct) + ?? throw new ArgumentException($"Can't get active keyset ID for unit: {_unit}"); + if (this._keyset == null) { - this._keyset = await this._wallet.GetKeys(this._keysetId, true, false, ct) ?? - throw new ArgumentException($"Cant fetch keys for keysetId: {_keysetId}"); + this._keyset = + await this._wallet.GetKeys(this._keysetId, true, false, ct) + ?? throw new ArgumentException($"Cant fetch keys for keysetId: {_keysetId}"); } - - var outputs = await this._createOutputs(); + var outputs = await this._createOutputs(); var req = new PostMintQuoteBolt12Request() { @@ -170,37 +188,43 @@ public async Task>> Proces Pubkey = this._pubkey, Description = this._description, }; - var mintQuote = await api.CreateMintQuote( - "bolt12", req, ct); + var mintQuote = await api.CreateMintQuote< + PostMintQuoteBolt12Response, + PostMintQuoteBolt12Request + >("bolt12", req, ct); return new MintHandlerBolt12(this._wallet, mintQuote, this._keyset, outputs); - } // skipped checks for keysetid and keys, since its validated before. make sure to remember about it. async Task> _createOutputs() { var outputs = new List(); - + if (this._outputs != null) { if (this._builder is not null) { - throw new ArgumentException("Can't create p2pk outputs if outputs provided. Remove either p2pk builder parameter or outputs."); + throw new ArgumentException( + "Can't create p2pk outputs if outputs provided. Remove either p2pk builder parameter or outputs." + ); } return this._outputs; } - + if (this._amount is null && this._amounts is null) { - throw new ArgumentNullException(nameof(_amount), "Amount can't be determined. Make sure to include amount, or amounts parameter!"); + throw new ArgumentNullException( + nameof(_amount), + "Amount can't be determined. Make sure to include amount, or amounts parameter!" + ); } - _amounts ??= Utils.SplitToProofsAmounts(_amount.Value, _keyset!.Keys); - + _amounts ??= Utils.SplitToProofsAmounts(_amount.Value, _keyset!.Keys); + if (this._builder is null) { return await _wallet.CreateOutputs(_amounts, this._keysetId!); } - + if (this._shouldBlind) { if (this._builder.SigFlag == "SIG_ALL") @@ -209,7 +233,12 @@ async Task> _createOutputs() foreach (var amount in _amounts) { var builder = _builder.Clone(); - var p2pkOutput = Utils.CreateNut10BlindedOutput(amount, this._keysetId!, builder, e); + var p2pkOutput = Utils.CreateNut10BlindedOutput( + amount, + this._keysetId!, + builder, + e + ); outputs.Add(p2pkOutput); } @@ -224,7 +253,7 @@ async Task> _createOutputs() } return outputs; } - + foreach (var amount in _amounts) { var builder = _builder.Clone(); @@ -233,4 +262,4 @@ async Task> _createOutputs() } return outputs; } -} \ No newline at end of file +} diff --git a/DotNut/Abstractions/Nut10Helper.cs b/DotNut/Abstractions/Nut10Helper.cs index 7a15755..dc6519a 100644 --- a/DotNut/Abstractions/Nut10Helper.cs +++ b/DotNut/Abstractions/Nut10Helper.cs @@ -11,29 +11,31 @@ public static void MaybeProcessNut10( List? outputs = null, string? htlcPreimage = null, string? meltQuoteId = null - ) + ) { if (privKeys.Count == 0 || proofs.Count == 0) { return; } - + outputs ??= []; var sigAllHandler = new SigAllHandler { Proofs = proofs, PrivKeys = privKeys, - BlindedMessages = outputs.Select(o=>o.BlindedMessage).ToList(), + BlindedMessages = outputs.Select(o => o.BlindedMessage).ToList(), HTLCPreimage = htlcPreimage, - MeltQuoteId = meltQuoteId + MeltQuoteId = meltQuoteId, }; - + if (sigAllHandler.TrySign(out string? witness)) { if (witness == null) { - throw new ArgumentNullException(nameof(witness), - "sig_all input was correct, but couldn't create a witness signature!"); + throw new ArgumentNullException( + nameof(witness), + "sig_all input was correct, but couldn't create a witness signature!" + ); } proofs[0].Witness = witness; @@ -52,8 +54,8 @@ private static void HandleWitnessCreation(Proof proof, ECPrivKey[] keys, string? { if (proof.Secret is Nut10Secret { ProofSecret: HTLCProofSecret htlc }) { - // preimage isn't verified after timelock - var preimage = htlcPreimage??""; + // preimage isn't verified after timelock + var preimage = htlcPreimage ?? ""; if (proof.P2PkE is { } E) { var blindwitness = htlc.GenerateBlindWitness(proof, keys, preimage); @@ -77,4 +79,4 @@ private static void HandleWitnessCreation(Proof proof, ECPrivKey[] keys, string? proof.Witness = JsonSerializer.Serialize(proofWitness); } } -} \ No newline at end of file +} diff --git a/DotNut/Abstractions/OutputData.cs b/DotNut/Abstractions/OutputData.cs index 75c3399..f7902a9 100644 --- a/DotNut/Abstractions/OutputData.cs +++ b/DotNut/Abstractions/OutputData.cs @@ -5,6 +5,6 @@ public class OutputData public BlindedMessage BlindedMessage { get; set; } public ISecret Secret { get; set; } public PrivKey BlindingFactor { get; set; } - - public PubKey? P2BkE {get; set;} -} \ No newline at end of file + + public PubKey? P2BkE { get; set; } +} diff --git a/DotNut/Abstractions/ProofSelector.cs b/DotNut/Abstractions/ProofSelector.cs index 63f7760..36e89e1 100644 --- a/DotNut/Abstractions/ProofSelector.cs +++ b/DotNut/Abstractions/ProofSelector.cs @@ -2,7 +2,7 @@ namespace DotNut.Abstractions; -// Borrowed from cashu-ts +// Borrowed from cashu-ts // see https://github.com/cashubtc/cashu-ts/pull/314 public class ProofSelector : IProofSelector { @@ -54,7 +54,12 @@ private ulong GetProofFeePPK(Proof proof) return _keysetFees.TryGetValue(proof.Id, out var fee) ? fee : 0; } - public async Task SelectProofsToSend(List proofs, ulong amountToSend, bool includeFees = false, CancellationToken ct = default) + public async Task SelectProofsToSend( + List proofs, + ulong amountToSend, + bool includeFees = false, + CancellationToken ct = default + ) { // Init vars const int MAX_TRIALS = 60; // 40-80 is optimal (per RGLI paper) @@ -63,7 +68,7 @@ public async Task SelectProofsToSend(List proofs, ulong amo const long MAX_TIMEMS = 1000; // Halt new trials if over time (in ms) const int MAX_P2SWAP = 5000; // Max number of Phase 2 improvement swaps const bool exactMatch = false; // Allows close match (> amountToSend + fee) - + var timer = new Timer(); // start the clock List? bestSubset = null; double bestDelta = double.PositiveInfinity; @@ -73,7 +78,7 @@ public async Task SelectProofsToSend(List proofs, ulong amo /* * Helper Functions. */ - + // Calculate net amount after fees double SumExFees(ulong amount, ulong feePPK) { @@ -97,27 +102,28 @@ List ShuffleArray(IEnumerable array) // If lessOrEqual=false, returns the leftmost index where exFee >= value int? BinarySearchIndex(List arr, double value, bool lessOrEqual) { - int left = 0, right = arr.Count - 1; + int left = 0, + right = arr.Count - 1; int? result = null; - + while (left <= right) { int mid = (left + right) / 2; double midValue = arr[mid].ExFee; - + if (lessOrEqual ? midValue <= value : midValue >= value) { result = mid; - if (lessOrEqual) + if (lessOrEqual) left = mid + 1; - else + else right = mid - 1; } else { - if (lessOrEqual) + if (lessOrEqual) right = mid - 1; - else + else left = mid + 1; } } @@ -128,14 +134,15 @@ List ShuffleArray(IEnumerable array) void InsertSorted(List arr, ProofWithFee obj) { double value = obj.ExFee; - int left = 0, right = arr.Count; - + int left = 0, + right = arr.Count; + while (left < right) { int mid = (left + right) / 2; - if (arr[mid].ExFee < value) + if (arr[mid].ExFee < value) left = mid + 1; - else + else right = mid; } arr.Insert(left, obj); @@ -147,7 +154,7 @@ void InsertSorted(List arr, ProofWithFee obj) double CalculateDelta(ulong amount, ulong feePPK) { double netSum = SumExFees(amount, feePPK); - if (netSum < amountToSend) + if (netSum < amountToSend) return double.PositiveInfinity; // no good return amount + feePPK / 1000.0 - amountToSend; } @@ -157,20 +164,22 @@ double CalculateDelta(ulong amount, ulong feePPK) */ ulong totalAmount = 0; ulong totalFeePPK = 0; - var proofWithFees = proofs.Select(p => - { - ulong ppkfee = GetProofFeePPK(p); - double exFee = includeFees ? p.Amount - ppkfee / 1000.0 : p.Amount; - var obj = new ProofWithFee(p, exFee, ppkfee); - - // Sum all economical proofs (filtered below) - if (!includeFees || exFee > 0) + var proofWithFees = proofs + .Select(p => { - totalAmount += p.Amount; - totalFeePPK += ppkfee; - } - return obj; - }).ToList(); + ulong ppkfee = GetProofFeePPK(p); + double exFee = includeFees ? p.Amount - ppkfee / 1000.0 : p.Amount; + var obj = new ProofWithFee(p, exFee, ppkfee); + + // Sum all economical proofs (filtered below) + if (!includeFees || exFee > 0) + { + totalAmount += p.Amount; + totalFeePPK += ppkfee; + } + return obj; + }) + .ToList(); // Filter uneconomical proofs (totals computed above) var spendableProofs = includeFees @@ -200,7 +209,9 @@ double CalculateDelta(ulong amount, ulong feePPK) var rightIndex = BinarySearchIndex(spendableProofs, nextBiggerExFee, true); if (rightIndex == null) { - throw new InvalidOperationException("Unexpected null rightIndex in binary search"); + throw new InvalidOperationException( + "Unexpected null rightIndex in binary search" + ); } endIndex = rightIndex.Value + 1; } @@ -230,7 +241,8 @@ double CalculateDelta(ulong amount, ulong feePPK) // Max acceptable amount for non-exact matches double maxOverAmount = Math.Min( Math.Ceiling(amountToSend * (1 + MAX_OVRPCT / 100)), - Math.Min(amountToSend + MAX_OVRAMT, totalNetSum)); + Math.Min(amountToSend + MAX_OVRAMT, totalNetSum) + ); /* * RGLI algorithm: Runs multiple trials (up to MAX_TRIALS) Each trial starts with randomized @@ -247,21 +259,21 @@ double CalculateDelta(ulong amount, ulong feePPK) var S = new List(); ulong amount = 0; ulong feePPK = 0; - + foreach (var obj in ShuffleArray(spendableProofs)) { ulong newAmount = amount + obj.Proof.Amount; ulong newFeePPK = feePPK + obj.PpkFee; double netSum = SumExFees(newAmount, newFeePPK); - - if (exactMatch && netSum > amountToSend) + + if (exactMatch && netSum > amountToSend) break; - + S.Add(obj); amount = newAmount; feePPK = newFeePPK; - - if (netSum >= amountToSend) + + if (netSum >= amountToSend) break; } @@ -275,16 +287,18 @@ double CalculateDelta(ulong amount, ulong feePPK) // Using array.Contains() would be way slower: O(n*m) var selectedCs = S.Select(pwf => pwf.Proof.C).ToHashSet(); var others = spendableProofs.Where(obj => !selectedCs.Contains(obj.Proof.C)).ToList(); - + // Generate a random order for accessing the trial subset ('S') var indices = ShuffleArray(Enumerable.Range(0, S.Count)).Take(MAX_P2SWAP).ToList(); - + foreach (int i in indices) { // Exact or acceptable close match solution found? double netSum = SumExFees(amount, feePPK); - if (Math.Abs(netSum - amountToSend) < 0.0001 || - (!exactMatch && netSum >= amountToSend && netSum <= maxOverAmount)) + if ( + Math.Abs(netSum - amountToSend) < 0.0001 + || (!exactMatch && netSum >= amountToSend && netSum <= maxOverAmount) + ) { break; } @@ -323,7 +337,6 @@ double CalculateDelta(ulong amount, ulong feePPK) double delta = CalculateDelta(amount, feePPK); if (delta < bestDelta) { - bestSubset = S.OrderByDescending(a => a.ExFee).ToList(); // copy & sort bestDelta = delta; bestAmount = amount; @@ -338,14 +351,14 @@ double CalculateDelta(ulong amount, ulong feePPK) { var objP = tempS.Last(); tempS.RemoveAt(tempS.Count - 1); - + ulong tempAmount2 = amount - objP.Proof.Amount; ulong tempFeePPK2 = feePPK - objP.PpkFee; double tempDelta = CalculateDelta(tempAmount2, tempFeePPK2); - - if (double.IsPositiveInfinity(tempDelta)) + + if (double.IsPositiveInfinity(tempDelta)) break; - + if (tempDelta < bestDelta) { bestSubset = tempS.ToList(); @@ -362,8 +375,10 @@ double CalculateDelta(ulong amount, ulong feePPK) if (bestSubset != null && !double.IsPositiveInfinity(bestDelta)) { double bestSum = SumExFees(bestAmount, bestFeePPK); - if (Math.Abs(bestSum - amountToSend) < 0.0001 || - (!exactMatch && bestSum >= amountToSend && bestSum <= maxOverAmount)) + if ( + Math.Abs(bestSum - amountToSend) < 0.0001 + || (!exactMatch && bestSum >= amountToSend && bestSum <= maxOverAmount) + ) { break; } @@ -374,7 +389,9 @@ double CalculateDelta(ulong amount, ulong feePPK) { if (exactMatch) { - throw new TimeoutException("Proof selection took too long. Try again with a smaller proof set."); + throw new TimeoutException( + "Proof selection took too long. Try again with a smaller proof set." + ); } else { @@ -389,7 +406,7 @@ double CalculateDelta(ulong amount, ulong feePPK) var bestProofs = bestSubset.Select(obj => obj.Proof).ToList(); var bestProofCs = bestProofs.Select(p => p.C).ToHashSet(); var keep = proofs.Where(p => !bestProofCs.Contains(p.C)).ToList(); - + return new SendResponse { Keep = keep, Send = bestProofs }; } diff --git a/DotNut/Abstractions/SendResponse.cs b/DotNut/Abstractions/SendResponse.cs index 78d4e65..ff92f68 100644 --- a/DotNut/Abstractions/SendResponse.cs +++ b/DotNut/Abstractions/SendResponse.cs @@ -4,4 +4,4 @@ public class SendResponse { public List Keep { get; set; } = new(); public List Send { get; set; } = new(); -} \ No newline at end of file +} diff --git a/DotNut/Abstractions/SwapBuilder.cs b/DotNut/Abstractions/SwapBuilder.cs index d361804..df05042 100644 --- a/DotNut/Abstractions/SwapBuilder.cs +++ b/DotNut/Abstractions/SwapBuilder.cs @@ -2,23 +2,23 @@ using DotNut.ApiModels; namespace DotNut.Abstractions; + /// /// Receive operation builder implementation /// class SwapBuilder : ISwapBuilder { private readonly Wallet _wallet; - - // input + + // input private readonly string? _tokenString; private readonly CashuToken? _token; private List? _proofsToSwap; - + private List? _outputs; private List? _amounts; private KeysetId? _targetKeysetId; - - + private string _unit = "sat"; private bool _verifyDleq = true; @@ -29,22 +29,24 @@ class SwapBuilder : ISwapBuilder private P2PkBuilder? _builder; private string? _htlcPreimage; private bool _shouldBlind = false; - + public SwapBuilder(Wallet wallet, string tokenString) { _wallet = wallet; _tokenString = tokenString; } + public SwapBuilder(Wallet wallet, CashuToken token) { _wallet = wallet; _token = token; } + public SwapBuilder(Wallet wallet) { _wallet = wallet; } - + public ISwapBuilder WithUnit(string unit) { this._unit = unit; @@ -80,7 +82,7 @@ public ISwapBuilder WithAmounts(IEnumerable amounts) _amounts = amounts.ToList(); return this; } - + public ISwapBuilder ForKeyset(KeysetId keysetId) { _targetKeysetId = keysetId; @@ -111,32 +113,32 @@ public ISwapBuilder ToHTLC(HTLCBuilder htlcBuilder) this._builder = htlcBuilder; return this; } - - // P2Bk should be compatible with both p2pk and HTLC. Not implemented in the second one + + // P2Bk should be compatible with both p2pk and HTLC. Not implemented in the second one public ISwapBuilder BlindPubkeys(bool withBlinding = true) { this._shouldBlind = withBlinding; return this; } - + public async Task> ProcessAsync(CancellationToken ct = default) { var mintApi = await _wallet.GetMintApi(ct); - + var swapInputs = _getSwapProofs(); if (swapInputs == null || swapInputs.Count == 0) { throw new ArgumentException("Nothing to swap!"); } - - // if there's no keysetId specified - let's choose it. + + // if there's no keysetId specified - let's choose it. if (_targetKeysetId == null) { - _targetKeysetId = await _wallet.GetActiveKeysetId(this._unit, ct) ?? - throw new InvalidOperationException("Could not fetch Keyset ID"); + _targetKeysetId = + await _wallet.GetActiveKeysetId(this._unit, ct) + ?? throw new InvalidOperationException("Could not fetch Keyset ID"); } - var keysForCurrentId = await - _wallet.GetKeys(_targetKeysetId, true, false, ct); + var keysForCurrentId = await _wallet.GetKeys(_targetKeysetId, true, false, ct); if (keysForCurrentId == null) { @@ -147,19 +149,25 @@ public async Task> ProcessAsync(CancellationToken ct = default) foreach (var proof in swapInputs) { // proof may be already inactive - make sure to fetch - var keyset = await _wallet.GetKeys(proof.Id, true, false, ct); - if (keyset == null) - { - throw new InvalidOperationException($"Can't find keys for keyset id ${proof.Id}"); - } - if (!keyset.Keys.TryGetValue(proof.Amount, out var key)) - { - throw new InvalidOperationException($"Can't find key for amount {proof.Amount} in keyset {keyset.Id}"); - } - var isValid = proof.Verify(key); + var keyset = await _wallet.GetKeys(proof.Id, true, false, ct); + if (keyset == null) + { + throw new InvalidOperationException( + $"Can't find keys for keyset id ${proof.Id}" + ); + } + if (!keyset.Keys.TryGetValue(proof.Amount, out var key)) + { + throw new InvalidOperationException( + $"Can't find key for amount {proof.Amount} in keyset {keyset.Id}" + ); + } + var isValid = proof.Verify(key); if (!isValid) { - throw new InvalidOperationException($"Invalid proof signature for amount {proof.Amount}"); + throw new InvalidOperationException( + $"Invalid proof signature for amount {proof.Amount}" + ); } } } @@ -168,50 +176,56 @@ public async Task> ProcessAsync(CancellationToken ct = default) if (_includeFees) { // returns also non-active keysets. - var keysetsFees = (await _wallet.GetKeysets(false, ct)).ToDictionary(k=>k.Id, k=>k.InputFee??0); + var keysetsFees = (await _wallet.GetKeysets(false, ct)).ToDictionary( + k => k.Id, + k => k.InputFee ?? 0 + ); fee = swapInputs.ComputeFee(keysetsFees); } - + var total = Utils.SumProofs(swapInputs); this._amounts ??= this._getAmounts(total, fee, keysForCurrentId.Keys); - + // Swap received proofs to our keyset var outputs = await this._getOutputs(keysForCurrentId.Keys, ct); - - Nut10Helper.MaybeProcessNut10(_privKeys??[], swapInputs, outputs, _htlcPreimage); - swapInputs.ForEach(i=>i.StripFingerprints()); + + Nut10Helper.MaybeProcessNut10(_privKeys ?? [], swapInputs, outputs, _htlcPreimage); + swapInputs.ForEach(i => i.StripFingerprints()); var request = new PostSwapRequest() { Inputs = swapInputs.ToArray(), - Outputs = outputs.Select(o=>o.BlindedMessage).ToArray(), + Outputs = outputs.Select(o => o.BlindedMessage).ToArray(), }; - + var swapResponse = await mintApi.Swap(request, ct); - var swappedProofs = - Utils.ConstructProofsFromPromises(swapResponse.Signatures.ToList(), outputs, keysForCurrentId.Keys); + var swappedProofs = Utils.ConstructProofsFromPromises( + swapResponse.Signatures.ToList(), + outputs, + keysForCurrentId.Keys + ); return swappedProofs; } - + private List _getSwapProofs() { _proofsToSwap ??= new(); - + if (_tokenString != null) - { + { var token = CashuTokenHelper.Decode(this._tokenString, out var v); ValidateSingleMint(token); - this._proofsToSwap.AddRange(token.Tokens.SelectMany(t=>t.Proofs)); + this._proofsToSwap.AddRange(token.Tokens.SelectMany(t => t.Proofs)); } if (_token != null) { ValidateSingleMint(_token); - this._proofsToSwap.AddRange(_token.Tokens.SelectMany(t=>t.Proofs)); + this._proofsToSwap.AddRange(_token.Tokens.SelectMany(t => t.Proofs)); } - + return _proofsToSwap; } @@ -221,16 +235,18 @@ private async Task> _getOutputs(Keyset keys, CancellationToken { if (this._builder is not null) { - throw new ArgumentException("Can't create nut10 outputs by builder if outputs provided. Remove either p2pk builder parameter or outputs."); + throw new ArgumentException( + "Can't create nut10 outputs by builder if outputs provided. Remove either p2pk builder parameter or outputs." + ); } return this._outputs; } - + if (this._amounts is null) { throw new ArgumentNullException(nameof(_amounts), "Amounts can't be null."); } - + var outputs = new List(); if (this._builder is not null) { @@ -242,14 +258,23 @@ private async Task> _getOutputs(Keyset keys, CancellationToken foreach (var amount in _amounts) { var builder = _builder.Clone(); - outputs.Add(Utils.CreateNut10BlindedOutput(amount, this._targetKeysetId!, builder, e)); + outputs.Add( + Utils.CreateNut10BlindedOutput( + amount, + this._targetKeysetId!, + builder, + e + ) + ); } return outputs; } foreach (var amount in _amounts) { var builder = _builder.Clone(); - outputs.Add(Utils.CreateNut10BlindedOutput(amount, this._targetKeysetId!, builder)); + outputs.Add( + Utils.CreateNut10BlindedOutput(amount, this._targetKeysetId!, builder) + ); } return outputs; } @@ -261,7 +286,7 @@ private async Task> _getOutputs(Keyset keys, CancellationToken } return outputs; } - + return await _wallet.CreateOutputs(_amounts, this._targetKeysetId!, ct); } @@ -270,7 +295,7 @@ private List _getAmounts(ulong total, ulong fee, Keyset keys) if (_amounts != null) { var sum = checked(_amounts.Aggregate(0UL, (acc, val) => acc + val)); - + if (checked(sum + fee) == total) { return _amounts; @@ -282,7 +307,9 @@ private List _getAmounts(ulong total, ulong fee, Keyset keys) return this._amounts; } - throw new ArgumentException($"Invalid amounts requested. Sum of amounts: {sum}, total input: {total}, fee:{fee}."); + throw new ArgumentException( + $"Invalid amounts requested. Sum of amounts: {sum}, total input: {total}, fee:{fee}." + ); } this._amounts = Utils.SplitToProofsAmounts(checked(total - fee), keys); @@ -297,5 +324,4 @@ private static void ValidateSingleMint(CashuToken token) throw new ArgumentException("Only swap from single mint is allowed"); } } - -} \ No newline at end of file +} diff --git a/DotNut/Abstractions/Utils.cs b/DotNut/Abstractions/Utils.cs index c5092a5..18f76a5 100644 --- a/DotNut/Abstractions/Utils.cs +++ b/DotNut/Abstractions/Utils.cs @@ -32,7 +32,7 @@ public static List SplitToProofsAmounts(ulong paymentAmount, Keyset keyse return outputAmounts; } - + /// /// Creates blank outputs (see nut-08) /// @@ -40,7 +40,13 @@ public static List SplitToProofsAmounts(ulong paymentAmount, Keyset keyse /// Active keyset id which will sign outputs /// Keys for given KeysetId /// Blank Outputs - public static List CreateBlankOutputs(ulong amount, KeysetId keysetId, Keyset keys, NBitcoin.BIP39.Mnemonic? mnemonic = null, uint? counter = null) + public static List CreateBlankOutputs( + ulong amount, + KeysetId keysetId, + Keyset keys, + NBitcoin.BIP39.Mnemonic? mnemonic = null, + uint? counter = null + ) { if (amount == 0) { @@ -53,7 +59,7 @@ public static List CreateBlankOutputs(ulong amount, KeysetId keysetI var amounts = Enumerable.Repeat((ulong)1, count).ToList(); return CreateOutputs(amounts, keysetId, keys, mnemonic, counter); } - + /// /// Calculates amount of blank outputs needed by mint to return overpaid fees /// @@ -66,17 +72,11 @@ public static int CalculateNumberOfBlankOutputs(ulong amountToCover) return 0; } - return Math.Max( - Convert.ToInt32( - Math.Ceiling( - Math.Log2(amountToCover) - ) - ), 1); + return Math.Max(Convert.ToInt32(Math.Ceiling(Math.Log2(amountToCover))), 1); } - - + /// - /// Creates outputs (secrets, proof messages and blinding factors). Outputs should have valid amounts. + /// Creates outputs (secrets, proof messages and blinding factors). Outputs should have valid amounts. /// /// Amounts for each output (e.g. [1,2,4,8] /// ID of keyset we want to receive the proofs @@ -88,13 +88,13 @@ public static List CreateOutputs( KeysetId keysetId, Keyset keys, NBitcoin.BIP39.Mnemonic? mnemonic = null, - uint? counter = null) + uint? counter = null + ) { if (amounts.Any(a => !keys.Keys.Contains(a))) throw new ArgumentException("Invalid amounts"); var outputs = new List(amounts.Count); - if (mnemonic is not null && counter is { } c) { @@ -105,15 +105,20 @@ public static List CreateOutputs( var B_ = Cashu.ComputeB_(secret.ToCurve(), r); var output = new OutputData { - BlindedMessage = new BlindedMessage { Amount = amounts[(int)i], B_ = B_, Id = keysetId }, + BlindedMessage = new BlindedMessage + { + Amount = amounts[(int)i], + B_ = B_, + Id = keysetId, + }, BlindingFactor = r, - Secret = secret + Secret = secret, }; outputs.Add(output); } return outputs; } - + foreach (var amount in amounts) { var secret = RandomSecret(); @@ -121,16 +126,20 @@ public static List CreateOutputs( var B_ = Cashu.ComputeB_(secret.ToCurve(), r); var output = new OutputData { - BlindedMessage = new BlindedMessage { Amount = amount, B_ = B_, Id = keysetId }, + BlindedMessage = new BlindedMessage + { + Amount = amount, + B_ = B_, + Id = keysetId, + }, BlindingFactor = r, - Secret = secret + Secret = secret, }; outputs.Add(output); } return outputs; } - /// /// Create P2Pk / HTLC outputs. /// @@ -138,11 +147,7 @@ public static List CreateOutputs( /// /// /// - public static OutputData CreateNut10Output( - ulong amount, - KeysetId keysetId, - P2PkBuilder builder - ) + public static OutputData CreateNut10Output(ulong amount, KeysetId keysetId, P2PkBuilder builder) { // ugliest hack ever Nut10Secret secret; @@ -159,11 +164,17 @@ P2PkBuilder builder var B_ = Cashu.ComputeB_(secret.ToCurve(), r); return new OutputData { - BlindedMessage = new BlindedMessage() { Amount = amount, B_ = B_, Id = keysetId }, + BlindedMessage = new BlindedMessage() + { + Amount = amount, + B_ = B_, + Id = keysetId, + }, BlindingFactor = r, - Secret = secret + Secret = secret, }; } + /// /// Creates P2Pk / HTLC Blinded Outputs /// @@ -171,7 +182,11 @@ P2PkBuilder builder /// /// /// - public static OutputData CreateNut10BlindedOutput(ulong amount, KeysetId keysetId, P2PkBuilder builder) + public static OutputData CreateNut10BlindedOutput( + ulong amount, + KeysetId keysetId, + P2PkBuilder builder + ) { // ugliest hack ever Nut10Secret secret; @@ -191,20 +206,31 @@ public static OutputData CreateNut10BlindedOutput(ulong amount, KeysetId keysetI var B_ = Cashu.ComputeB_(secret.ToCurve(), r); return new OutputData { - BlindedMessage = new BlindedMessage() { Amount = amount, B_ = B_, Id = keysetId }, + BlindedMessage = new BlindedMessage() + { + Amount = amount, + B_ = B_, + Id = keysetId, + }, BlindingFactor = r, Secret = secret, - P2BkE = E + P2BkE = E, }; } + /// - /// Creates P2Pk / HTLC Blinded Outputs with specified ephemeral sender keypair. + /// Creates P2Pk / HTLC Blinded Outputs with specified ephemeral sender keypair. /// /// /// /// /// - public static OutputData CreateNut10BlindedOutput(ulong amount, KeysetId keysetId, P2PkBuilder builder, PrivKey e) + public static OutputData CreateNut10BlindedOutput( + ulong amount, + KeysetId keysetId, + P2PkBuilder builder, + PrivKey e + ) { // ugliest hack ever Nut10Secret secret; @@ -221,13 +247,18 @@ public static OutputData CreateNut10BlindedOutput(ulong amount, KeysetId keysetI var B_ = Cashu.ComputeB_(secret.ToCurve(), r); return new OutputData { - BlindedMessage = new BlindedMessage() { Amount = amount, B_ = B_, Id = keysetId }, + BlindedMessage = new BlindedMessage() + { + Amount = amount, + B_ = B_, + Id = keysetId, + }, BlindingFactor = r, Secret = secret, - P2BkE = e.Key.CreatePubKey() + P2BkE = e.Key.CreatePubKey(), }; } - + /// /// Method creating proofs, from provided promises (blinded signatures) /// @@ -241,33 +272,33 @@ public static Proof ConstructProofFromPromise( PrivKey r, ISecret secret, PubKey amountPubkey, - PubKey? P2PkE = null) + PubKey? P2PkE = null + ) { - //unblind signature var C = Cashu.ComputeC(promise.C_, r, amountPubkey); DLEQProof? dleq = null; - + var proof = new Proof { Id = promise.Id, Amount = promise.Amount, Secret = secret, C = C, - P2PkE = P2PkE + P2PkE = P2PkE, }; if (promise.DLEQ is null) { return proof; } - + proof.DLEQ = new DLEQProof { E = promise.DLEQ.E, S = promise.DLEQ.S, - R = r.Key.Clone() + R = r.Key.Clone(), }; if (!proof.Verify(amountPubkey)) { @@ -280,14 +311,16 @@ public static List ConstructProofsFromPromises( List promises, List outputs, Keyset keys - ) + ) { List proofs = new List(); for (int i = 0; i < promises.Count; i++) { if (!keys.TryGetValue(promises[i].Amount, out var key)) { - throw new ArgumentException($"Provided keyset doesn't contain PubKey for amount {promises[i].Amount}" ); + throw new ArgumentException( + $"Provided keyset doesn't contain PubKey for amount {promises[i].Amount}" + ); } var proof = ConstructProofFromPromise( @@ -306,7 +339,7 @@ public static ulong SumProofs(List proofs) { return proofs.Aggregate(0UL, (current, proof) => current + proof.Amount); } - + public static ISecret RandomSecret() { var bytes = RandomNumberGenerator.GetBytes(32); @@ -332,28 +365,28 @@ public static void StripFingerprints(this Proof proof) } proof.P2PkE = null; } - + /// /// Create deep copy of the object, so original one won't get mutated by reference. /// /// Object to clone /// Object type /// Deep copy of the object - public static T DeepCopy(this T obj) where T : class + public static T DeepCopy(this T obj) + where T : class { - return JsonSerializer.Deserialize( - JsonSerializer.Serialize(obj) - )!; + return JsonSerializer.Deserialize(JsonSerializer.Serialize(obj))!; } - + /// /// Create deep copy of the list /// /// /// /// - public static List DeepCopyList(this IEnumerable list) where T : class + public static List DeepCopyList(this IEnumerable list) + where T : class { return list.Select(item => item.DeepCopy()).ToList(); } -} \ No newline at end of file +} diff --git a/DotNut/Abstractions/Wallet.cs b/DotNut/Abstractions/Wallet.cs index d3235bb..9033591 100644 --- a/DotNut/Abstractions/Wallet.cs +++ b/DotNut/Abstractions/Wallet.cs @@ -9,7 +9,6 @@ namespace DotNut.Abstractions; /// Main Cashu Wallet class implementing fluent builder pattern /// /// - public class Wallet : IWalletBuilder { private MintInfo? _info; @@ -17,32 +16,33 @@ public class Wallet : IWalletBuilder private ICashuApi? _mintApi; private List? _keysets; private List? _keys; - private Dictionary? _keysetFees => _keysets?.ToDictionary(k=>k.Id, k=>k.InputFee??0); + private Dictionary? _keysetFees => + _keysets?.ToDictionary(k => k.Id, k => k.InputFee ?? 0); private Mnemonic? _mnemonic; private ICounter? _counter; - + private IWebsocketService? _wsService; - - //flags + + //flags private bool _shouldSyncKeyset = true; private DateTime? _lastSync = DateTime.MinValue; - private TimeSpan? _syncThreshold; // if null sync only once + private TimeSpan? _syncThreshold; // if null sync only once private bool _shouldBumpCounter = true; - + /* - * Fluent Builder Methods + * Fluent Builder Methods */ public static IWalletBuilder Create() => new Wallet(); - + public IWalletBuilder WithMint(ICashuApi mintApi) { _mintApi = mintApi; return this; } - + public IWalletBuilder WithMint(string mintUrl) { - var httpClient = new HttpClient{ BaseAddress = new Uri(mintUrl)}; + var httpClient = new HttpClient { BaseAddress = new Uri(mintUrl) }; _mintApi = new CashuHttpClient(httpClient, true); return this; } @@ -53,31 +53,32 @@ public IWalletBuilder WithMint(Uri mintUri) _mintApi = new CashuHttpClient(httpClient, true); return this; } - + public IWalletBuilder WithInfo(MintInfo info) { this._info = info; return this; } - + public IWalletBuilder WithInfo(GetInfoResponse info) => this.WithInfo(new MintInfo(info)); - + public IWalletBuilder WithKeysets(IEnumerable keysets) { this._keysets = keysets.ToList(); return this; } - - public IWalletBuilder WithKeysets(GetKeysetsResponse keysets) => this.WithKeysets(keysets.Keysets.ToList()); - + + public IWalletBuilder WithKeysets(GetKeysetsResponse keysets) => + this.WithKeysets(keysets.Keysets.ToList()); + public IWalletBuilder WithKeys(IEnumerable keys) { this._keys = keys.ToList(); return this; } - + public IWalletBuilder WithKeys(GetKeysResponse keys) => this.WithKeys(keys.Keysets.ToList()); - + public IWalletBuilder WithKeysetSync(bool syncKeyset = true) { this._shouldSyncKeyset = syncKeyset; @@ -90,25 +91,25 @@ public IWalletBuilder WithKeysetSync(bool syncKeyset, TimeSpan syncThreshold) this._syncThreshold = syncThreshold; return this; } - + public IWalletBuilder WithSelector(IProofSelector selector) { _selector = selector; return this; } - + public IWalletBuilder WithMnemonic(Mnemonic mnemonic) { _mnemonic = mnemonic; return this; } - + public IWalletBuilder WithMnemonic(string mnemonic) { _mnemonic = new Mnemonic(mnemonic); return this; } - + public IWalletBuilder WithCounter(ICounter counter) { this._counter = counter; @@ -126,14 +127,13 @@ public IWalletBuilder ShouldBumpCounter(bool shouldBumpCounter = true) this._shouldBumpCounter = shouldBumpCounter; return this; } - + public IWalletBuilder WithWebsocketService(IWebsocketService websocketService) { this._wsService = websocketService; return this; } - /* * Main api methods */ @@ -142,7 +142,7 @@ public IMintQuoteBuilder CreateMintQuote() _ensureApiConnected(); return new MintQuoteBuilder(this); } - + public ISwapBuilder Swap() { _ensureApiConnected(); @@ -155,87 +155,96 @@ public IMeltQuoteBuilder CreateMeltQuote() return new MeltQuoteBuilder(this); } - public async Task CheckState(IEnumerable proofs, CancellationToken ct = default) + public async Task CheckState( + IEnumerable proofs, + CancellationToken ct = default + ) { // no need for striping DLEQ r, or p2pkE, since only Ys are being sent. - return await CheckState(proofs.Select(p => (PubKey) p.Secret.ToCurve()), ct); + return await CheckState(proofs.Select(p => (PubKey)p.Secret.ToCurve()), ct); } - public async Task CheckState(IEnumerable Ys, CancellationToken ct = default) + public async Task CheckState( + IEnumerable Ys, + CancellationToken ct = default + ) { _ensureApiConnected(); - var req = new PostCheckStateRequest() - { - Ys = Ys.Select(y=>y.ToString()).ToArray(), - }; + var req = new PostCheckStateRequest() { Ys = Ys.Select(y => y.ToString()).ToArray() }; return await _mintApi!.CheckState(req, ct); } - + public IRestoreBuilder Restore() { _ensureApiConnected(); return new RestoreBuilder(this); } - - - + /* * Public Mint utils */ - + public void InvalidateCache() { _lastSync = DateTime.MinValue; } - public async Task GetActiveKeysetId(string unit, CancellationToken ct = default) { await _maybeSyncKeys(ct); - return _keysets? - .OrderBy(k => k.InputFee) + return _keysets + ?.OrderBy(k => k.InputFee) .FirstOrDefault(k => k is { Active: true } && k.Unit == unit, null) ?.Id; } - public async Task>?> GetKeysetIdsWithUnits(CancellationToken ct = default) + + public async Task>?> GetKeysetIdsWithUnits( + CancellationToken ct = default + ) { await _maybeSyncKeys(ct); - return _keysets? - .GroupBy(k => k.Unit) - .ToDictionary( - g => g.Key, - g => g.OrderBy(k => k.InputFee).Select(k => k.Id).ToList() - ); + return _keysets + ?.GroupBy(k => k.Unit) + .ToDictionary(g => g.Key, g => g.OrderBy(k => k.InputFee).Select(k => k.Id).ToList()); } - public async Task?> GetActiveKeysetIdsWithUnits(CancellationToken ct = default) + + public async Task?> GetActiveKeysetIdsWithUnits( + CancellationToken ct = default + ) { await _maybeSyncKeys(ct); - return _keysets?.Where(k=>k.Active) + return _keysets + ?.Where(k => k.Active) .GroupBy(k => k.Unit) - .ToDictionary( - g => g.Key, - g => g.OrderBy(k => k.InputFee).First().Id - ); + .ToDictionary(g => g.Key, g => g.OrderBy(k => k.InputFee).First().Id); } - - public async Task> GetKeys(bool forceRefresh = false, CancellationToken ct = default) + + public async Task> GetKeys( + bool forceRefresh = false, + CancellationToken ct = default + ) { if (forceRefresh) { - this._keys = await _fetchKeys(ct); - return this._keys ?? []; + this._keys = await _fetchKeys(ct); + return this._keys ?? []; } await _maybeSyncKeys(ct); return this._keys ?? []; } - public async Task GetKeys(KeysetId id, bool allowFetch = true, bool forceRefresh = false, CancellationToken ct = default) + public async Task GetKeys( + KeysetId id, + bool allowFetch = true, + bool forceRefresh = false, + CancellationToken ct = default + ) { if (forceRefresh) { return await _fetchKeys(id, ct); } - + var localKeyset = this._keys?.SingleOrDefault(k => k.Id == id); if (localKeyset != null) { @@ -244,27 +253,29 @@ public void InvalidateCache() if (allowFetch) { - var keyset = await _fetchKeys(id, ct); + var keyset = await _fetchKeys(id, ct); if (keyset != null && _keys != null) { _keys.Add(keyset); } } - + throw new ArgumentException("No keys found for this keyset!"); } - - public async Task> GetKeysets(bool forceRefresh = false, CancellationToken ct = default) + + public async Task> GetKeysets( + bool forceRefresh = false, + CancellationToken ct = default + ) { if (forceRefresh) { - this._keysets = await _fetchKeysets(ct); - return _keysets ?? []; + this._keysets = await _fetchKeysets(ct); + return _keysets ?? []; } await _maybeSyncKeys(ct); return _keysets ?? []; } - public async Task GetInfo(bool forceRefresh = false, CancellationToken ct = default) { @@ -274,13 +285,20 @@ public async Task GetInfo(bool forceRefresh = false, CancellationToken } return await _lazyFetchMintInfo(ct); } - - public async Task> CreateOutputs(List amounts, KeysetId id, CancellationToken ct = default) + + public async Task> CreateOutputs( + List amounts, + KeysetId id, + CancellationToken ct = default + ) { await _maybeSyncKeys(ct); if (this._keys == null) { - throw new ArgumentNullException(nameof(this._keys), "No Keys found. Make sure to fetch them!"); + throw new ArgumentNullException( + nameof(this._keys), + "No Keys found. Make sure to fetch them!" + ); } var keyset = this._keys.Single(k => k.Id == id); if (this._mnemonic == null) @@ -290,7 +308,10 @@ public async Task> CreateOutputs(List amounts, KeysetId if (this._counter == null) { - throw new ArgumentNullException(nameof(ICounter), "Can't derive outputs without keyset counter"); + throw new ArgumentNullException( + nameof(ICounter), + "Can't derive outputs without keyset counter" + ); } var counterValue = await this._counter.GetCounterForId(id, ct); @@ -298,12 +319,16 @@ public async Task> CreateOutputs(List amounts, KeysetId { return Utils.CreateOutputs(amounts, id, keyset.Keys, this._mnemonic, counterValue); } - + await this._counter.IncrementCounter(id, (uint)amounts.Count, ct); return Utils.CreateOutputs(amounts, id, keyset.Keys, this._mnemonic, counterValue); } - - public async Task> CreateOutputs(List amounts, string unit, CancellationToken ct = default) + + public async Task> CreateOutputs( + List amounts, + string unit, + CancellationToken ct = default + ) { var keysetId = await this.GetActiveKeysetId(unit, ct); if (keysetId == null) @@ -312,8 +337,13 @@ public async Task> CreateOutputs(List amounts, string un } return await this.CreateOutputs(amounts, keysetId, ct); } - - public async Task SelectProofsToSend(List proofs, ulong amount, bool includeFees, CancellationToken ct = default) + + public async Task SelectProofsToSend( + List proofs, + ulong amount, + bool includeFees, + CancellationToken ct = default + ) { if (this._selector == null) { @@ -324,12 +354,13 @@ public async Task SelectProofsToSend(List proofs, ulong amo return await _selector.SelectProofsToSend(proofs, amount, includeFees, ct); } - + public async Task GetMintApi(CancellationToken ct = default) { _ensureApiConnected(); return _mintApi!; } + public async Task GetSelector(CancellationToken ct = default) { if (this._selector == null) @@ -342,18 +373,18 @@ public async Task GetSelector(CancellationToken ct = default) } public async Task GetWebsocketService(CancellationToken ct = default) - { + { return this._wsService ??= new WebsocketService(); } - + public Mnemonic? GetMnemonic() => _mnemonic; + public ICounter? GetCounter() => _counter; - - + /* * Private helpers */ - + /// /// Throws exception if api not connected /// @@ -373,48 +404,61 @@ internal void _ensureApiConnected(string? msg = null) throw new ArgumentNullException(nameof(this._mintApi)); } - + /// /// Wrapper for GetKeysets api endpoint. Formats Keysets to list. /// /// List of Keysets /// May be thrown if mint is not set. - private async Task> _fetchKeysets(CancellationToken ct = default) + private async Task> _fetchKeysets( + CancellationToken ct = default + ) { _ensureApiConnected("Can't fetch keysets without mint api!"); var keysetsRaw = await _mintApi!.GetKeysets(ct); return keysetsRaw.Keysets.ToList(); } - + /// /// Wrapper for GetKeys api endpoint. Validates returned KeysetIds and formats Keys to list. /// /// List of Keys (lists :)) /// May be thrown if mint is not set. /// May be thrown if mint returns invalid keysetId for at least one Keyset - private async Task> _fetchKeys(CancellationToken ct = default) + private async Task> _fetchKeys( + CancellationToken ct = default + ) { _ensureApiConnected("Can't fetch keys without mint api!"); var keysRaw = await _mintApi!.GetKeys(ct); foreach (var keysetItemResponse in keysRaw.Keysets) { - var isKeysetIdValid = keysetItemResponse.Keys.VerifyKeysetId(keysetItemResponse.Id, keysetItemResponse.Unit, keysetItemResponse.FinalExpiry); + var isKeysetIdValid = keysetItemResponse.Keys.VerifyKeysetId( + keysetItemResponse.Id, + keysetItemResponse.Unit, + keysetItemResponse.FinalExpiry + ); if (!isKeysetIdValid) { - throw new ArgumentException($"Mint provided invalid keysetId. Provided: {keysetItemResponse.Id}, derived: {keysetItemResponse.Keys.GetKeysetId()} "); + throw new ArgumentException( + $"Mint provided invalid keysetId. Provided: {keysetItemResponse.Id}, derived: {keysetItemResponse.Keys.GetKeysetId()} " + ); } } return keysRaw.Keysets.ToList(); } - + /// - /// Wrapper for GetKeys api endpoint. Validates KeysetId and fetches keys for single KeysetId Formats Keys to list. + /// Wrapper for GetKeys api endpoint. Validates KeysetId and fetches keys for single KeysetId Formats Keys to list. /// /// KeysetId we want fetch keys for. /// Keys /// May be thrown if mint returns invalid keysetId for at least one Keyset /// May be thrown if mint is not set. - private async Task _fetchKeys(KeysetId id, CancellationToken cts = default) + private async Task _fetchKeys( + KeysetId id, + CancellationToken cts = default + ) { _ensureApiConnected("Can't fetch keys without mint api!"); var keysRaw = (await _mintApi!.GetKeys(id, cts)).Keysets.SingleOrDefault(); @@ -422,14 +466,20 @@ internal void _ensureApiConnected(string? msg = null) { return null; } - var isKeysetIdValid = keysRaw.Keys.VerifyKeysetId(keysRaw.Id, keysRaw.Unit, keysRaw.FinalExpiry); + var isKeysetIdValid = keysRaw.Keys.VerifyKeysetId( + keysRaw.Id, + keysRaw.Unit, + keysRaw.FinalExpiry + ); if (!isKeysetIdValid) { - throw new ArgumentException($"Mint provided invalid keysetId. Provided: {keysRaw.Id}, derived: {keysRaw.Keys.GetKeysetId()} "); + throw new ArgumentException( + $"Mint provided invalid keysetId. Provided: {keysRaw.Id}, derived: {keysRaw.Keys.GetKeysetId()} " + ); } return keysRaw; } - + /// /// Wrapper for GetInfo api endpoint. Translates Payload to MintInfo. /// @@ -447,10 +497,11 @@ private async Task _fetchMintInfo(CancellationToken cts = default) /// private async Task _lazyFetchMintInfo(CancellationToken cts = default) { - if (this._info != null) return this._info; + if (this._info != null) + return this._info; return await this._fetchMintInfo(cts); } - + /// /// Local Keys sync. Will fetch _all_ keys if more than 2 unknown keysets are returned. /// Doesn't sync fetch non-active keys. If you want to fetch keys for inactive keyset, you will need to use GetKeys. @@ -465,10 +516,10 @@ internal async Task _maybeSyncKeys(CancellationToken cts = default) } // should sync keysets SINGLE time in the lifespan of object. If already synced - return; if (_syncThreshold == null && _lastSync != DateTime.MinValue) - { + { return; } - // should sync keysets in some timepsan + // should sync keysets in some timepsan if (_syncThreshold != null && _lastSync + _syncThreshold >= DateTime.UtcNow) { return; @@ -480,7 +531,7 @@ internal async Task _maybeSyncKeys(CancellationToken cts = default) this._keys = await _fetchKeys(cts); // we're fetching all keys here, so no need for additional check. return; } - + var knownIds = _keys.Select(key => key.Id).ToHashSet(); var unknownKeysets = _keysets.Where(k => !knownIds.Contains(k.Id) && k.Active).ToList(); if (unknownKeysets.Count > 2) // just make a single request. May override stored keys. @@ -488,17 +539,17 @@ internal async Task _maybeSyncKeys(CancellationToken cts = default) this._keys = await _fetchKeys(cts); return; } - + foreach (var unknownKeyset in unknownKeysets) { - var keyset = await this._fetchKeys(unknownKeyset.Id, cts); + var keyset = await this._fetchKeys(unknownKeyset.Id, cts); _lastSync = DateTime.UtcNow; if (keyset != null) { _keys.Add(keyset); } } - + _lastSync = DateTime.UtcNow; } @@ -507,4 +558,3 @@ public void Dispose() _mintApi?.Dispose(); } } - diff --git a/DotNut/Abstractions/Websockets/NotificationParser.cs b/DotNut/Abstractions/Websockets/NotificationParser.cs index 4e0969e..d9136b5 100644 --- a/DotNut/Abstractions/Websockets/NotificationParser.cs +++ b/DotNut/Abstractions/Websockets/NotificationParser.cs @@ -5,7 +5,10 @@ namespace DotNut.Abstractions.Websockets; public static class NotificationParser { - public static object? ParsePayload(WsNotification notification, SubscriptionKind subscriptionKind) + public static object? ParsePayload( + WsNotification notification, + SubscriptionKind subscriptionKind + ) { if (notification.Params.Payload == null) return null; @@ -14,14 +17,17 @@ public static class NotificationParser return subscriptionKind switch { - SubscriptionKind.bolt11_mint_quote => jsonElement.Deserialize(), - SubscriptionKind.bolt11_melt_quote => jsonElement.Deserialize(), + SubscriptionKind.bolt11_mint_quote => + jsonElement.Deserialize(), + SubscriptionKind.bolt11_melt_quote => + jsonElement.Deserialize(), SubscriptionKind.proof_state => jsonElement.Deserialize(), - _ => notification.Params.Payload + _ => notification.Params.Payload, }; } - public static T? ParsePayload(WsNotification notification) where T : class + public static T? ParsePayload(WsNotification notification) + where T : class { if (notification.Params.Payload == null) return null; @@ -30,7 +36,8 @@ public static class NotificationParser return jsonElement.Deserialize(); } - public static bool IsPayloadOfType(WsNotification notification) where T : class + public static bool IsPayloadOfType(WsNotification notification) + where T : class { try { diff --git a/DotNut/Abstractions/Websockets/Subscription.cs b/DotNut/Abstractions/Websockets/Subscription.cs index 3e6e46b..6e9e5a1 100644 --- a/DotNut/Abstractions/Websockets/Subscription.cs +++ b/DotNut/Abstractions/Websockets/Subscription.cs @@ -9,16 +9,19 @@ public class Subscription public SubscriptionKind Kind { get; set; } public string[] Filters { get; set; } = Array.Empty(); public DateTime CreatedAt { get; set; } = DateTime.UtcNow; - public Channel NotificationChannel { get; set; } = Channel.CreateUnbounded(); - + public Channel NotificationChannel { get; set; } = + Channel.CreateUnbounded(); + public EventHandler? OnError { get; set; } - + private readonly WeakReference? _serviceRef; - + public Subscription(IWebsocketService? websocketService = null) { - _serviceRef = websocketService != null ? - new WeakReference(websocketService) : null; + _serviceRef = + websocketService != null + ? new WeakReference(websocketService) + : null; } public async Task CloseAsync() diff --git a/DotNut/Abstractions/Websockets/WebsocketConnection.cs b/DotNut/Abstractions/Websockets/WebsocketConnection.cs index 9c7315f..e998382 100644 --- a/DotNut/Abstractions/Websockets/WebsocketConnection.cs +++ b/DotNut/Abstractions/Websockets/WebsocketConnection.cs @@ -9,11 +9,13 @@ public class WebsocketConnection public ClientWebSocket WebSocket { get; set; } = new(); public WebSocketState State { get; set; } public DateTime ConnectedAt { get; set; } = DateTime.UtcNow; - + public bool Equals(WebsocketConnection? other) { - if (other is null) return false; - if (ReferenceEquals(this, other)) return true; + if (other is null) + return false; + if (ReferenceEquals(this, other)) + return true; return string.Equals(MintUrl, other.MintUrl, StringComparison.OrdinalIgnoreCase); } @@ -37,4 +39,3 @@ public override int GetHashCode() return !object.Equals(left, right); } } - diff --git a/DotNut/Abstractions/Websockets/WebsocketEnums.cs b/DotNut/Abstractions/Websockets/WebsocketEnums.cs index c3e4b05..44cc448 100644 --- a/DotNut/Abstractions/Websockets/WebsocketEnums.cs +++ b/DotNut/Abstractions/Websockets/WebsocketEnums.cs @@ -9,12 +9,12 @@ public enum SubscriptionKind bolt11_mint_quote, bolt12_melt_quote, bolt12_mint_quote, - proof_state + proof_state, } [JsonConverter(typeof(JsonStringEnumConverter))] public enum WsRequestMethod { subscribe, - unsubscribe -} \ No newline at end of file + unsubscribe, +} diff --git a/DotNut/Abstractions/Websockets/WebsocketModels.cs b/DotNut/Abstractions/Websockets/WebsocketModels.cs index 8df82f6..e30afc0 100644 --- a/DotNut/Abstractions/Websockets/WebsocketModels.cs +++ b/DotNut/Abstractions/Websockets/WebsocketModels.cs @@ -4,7 +4,7 @@ namespace DotNut.Abstractions.Websockets; public class WsRequest { - [JsonPropertyName("jsonrpc")] + [JsonPropertyName("jsonrpc")] public string JsonRpc { get; set; } = "2.0"; [JsonPropertyName("method")] @@ -45,7 +45,7 @@ public class WsResponse public class WsResult { - [JsonPropertyName("status")] + [JsonPropertyName("status")] public string Status { get; } = "OK"; [JsonPropertyName("subId")] @@ -97,13 +97,16 @@ public class WsNotificationParams public abstract record WsMessage { public sealed record Response(WsResponse Value) : WsMessage; + public sealed record Error(WsError Value) : WsMessage; + public sealed record Notification(WsNotification Value) : WsMessage; } public abstract record RequestResult { public sealed record Success(string SubId, string Status) : RequestResult; + public sealed record Failure(int Code, string Message, int RequestId) : RequestResult; } @@ -111,4 +114,4 @@ internal class PendingRequest { public required TaskCompletionSource Tcs { get; set; } public required string SubscriptionId { get; set; } -} \ No newline at end of file +} diff --git a/DotNut/Abstractions/Websockets/WebsocketService.cs b/DotNut/Abstractions/Websockets/WebsocketService.cs index 5005b34..eb1df88 100644 --- a/DotNut/Abstractions/Websockets/WebsocketService.cs +++ b/DotNut/Abstractions/Websockets/WebsocketService.cs @@ -14,17 +14,19 @@ public class WebsocketService : IWebsocketService private readonly ConcurrentDictionary _subscriptions = new(); private readonly ConcurrentDictionary _pendingRequests = new(); private int _nextRequestId = 0; - + public event EventHandler? ConnectionStateChanged; - - public async Task ConnectAsync(string mintUrl, CancellationToken ct = default) + public async Task ConnectAsync( + string mintUrl, + CancellationToken ct = default + ) { var normalized = NormalizeMintUrl(mintUrl); var connectionId = Guid.NewGuid().ToString(); var wsUrl = GetWebSocketUrl(mintUrl); - + var clientWebSocket = new ClientWebSocket(); try { @@ -35,27 +37,30 @@ public async Task ConnectAsync(string mintUrl, Cancellation clientWebSocket.Dispose(); throw; } - + var connection = new WebsocketConnection { - Id = connectionId, - MintUrl = normalized, - WebSocket = clientWebSocket, - State = WebSocketState.Open + Id = connectionId, + MintUrl = normalized, + WebSocket = clientWebSocket, + State = WebSocketState.Open, }; - + _connections[normalized] = connection; OnConnectionStateChanged(connectionId, WebSocketState.Open); - + _ = Task.Run(async () => await ListenForMessages(connection, CancellationToken.None)); - + return connection; } - - public async Task LazyConnectAsync(string mintUrl, CancellationToken ct = default) + + public async Task LazyConnectAsync( + string mintUrl, + CancellationToken ct = default + ) { var normalized = NormalizeMintUrl(mintUrl); - + if (_connections.TryGetValue(normalized, out var existing)) { if (existing is { State: WebSocketState.Open, WebSocket.State: WebSocketState.Open }) @@ -70,20 +75,21 @@ public async Task LazyConnectAsync(string mintUrl, Cancella public async Task DisconnectAsync(string mintUrl, CancellationToken ct = default) { var normalized = NormalizeMintUrl(mintUrl); - + if (!_connections.TryGetValue(normalized, out var connection)) { return; } - + try { if (connection.State == WebSocketState.Open) { await connection.WebSocket.CloseAsync( - WebSocketCloseStatus.NormalClosure, - "Client disconnecting", - ct); + WebSocketCloseStatus.NormalClosure, + "Client disconnecting", + ct + ); } } catch (Exception _) @@ -95,12 +101,12 @@ await connection.WebSocket.CloseAsync( connection.State = WebSocketState.Closed; connection.WebSocket.Dispose(); _connections.TryRemove(normalized, out _); - + var subscriptionsToRemove = _subscriptions .Where(s => s.Value.ConnectionId == connection.Id) .Select(s => s.Key) .ToList(); - + foreach (var subId in subscriptionsToRemove) { if (_subscriptions.TryRemove(subId, out var removedSub)) @@ -108,30 +114,37 @@ await connection.WebSocket.CloseAsync( await removedSub.CloseAsync(); } } - + OnConnectionStateChanged(connection.Id, WebSocketState.Closed); } } - - public async Task SubscribeAsync(string mintUrl, SubscriptionKind kind, string[] filters, CancellationToken ct = default) + + public async Task SubscribeAsync( + string mintUrl, + SubscriptionKind kind, + string[] filters, + CancellationToken ct = default + ) { var normalized = NormalizeMintUrl(mintUrl); - + if (!_connections.TryGetValue(normalized, out var connection)) { throw new InvalidOperationException($"Connection for mint {mintUrl} not found"); } - + if (connection.State != WebSocketState.Open) { throw new InvalidOperationException($"Connection for mint {mintUrl} is not open"); } - + var subId = Guid.NewGuid().ToString(); var requestId = _getNextRequestId(); - - var channel = Channel.CreateUnbounded(new UnboundedChannelOptions { SingleReader = false }); - + + var channel = Channel.CreateUnbounded( + new UnboundedChannelOptions { SingleReader = false } + ); + var request = new WsRequest { JsonRpc = "2.0", @@ -140,11 +153,11 @@ public async Task SubscribeAsync(string mintUrl, SubscriptionKind { Kind = kind, SubId = subId, - Filters = filters + Filters = filters, }, - Id = requestId + Id = requestId, }; - + var subscription = new Subscription(this) { Id = subId, @@ -154,46 +167,42 @@ public async Task SubscribeAsync(string mintUrl, SubscriptionKind CreatedAt = DateTime.UtcNow, NotificationChannel = channel, }; - + _subscriptions[subId] = subscription; - + var tcs = new TaskCompletionSource(); - - _pendingRequests[requestId] = new PendingRequest - { - Tcs = tcs, - SubscriptionId = subId - }; - + + _pendingRequests[requestId] = new PendingRequest { Tcs = tcs, SubscriptionId = subId }; + try { await SendMessageAsync(connection, request, ct); - + using var cts = CancellationTokenSource.CreateLinkedTokenSource(ct); cts.CancelAfter(TimeSpan.FromSeconds(30)); - + var completedTask = await Task.WhenAny( - tcs.Task, + tcs.Task, Task.Delay(Timeout.Infinite, cts.Token) - ).ConfigureAwait(false); - + ) + .ConfigureAwait(false); + if (completedTask != tcs.Task) { _subscriptions.TryRemove(subId, out _); await subscription.CloseAsync(); throw new TimeoutException("Subscription request timed out"); } - + var result = await tcs.Task; - + if (result is RequestResult.Failure failure) { _subscriptions.TryRemove(subId, out _); await subscription.CloseAsync(); - throw new InvalidOperationException( - $"Subscription failed: {failure.Message}"); + throw new InvalidOperationException($"Subscription failed: {failure.Message}"); } - + return subscription; } finally @@ -201,7 +210,7 @@ public async Task SubscribeAsync(string mintUrl, SubscriptionKind _pendingRequests.TryRemove(requestId, out _); } } - + public async Task UnsubscribeAsync(string subId, CancellationToken ct = default) { if (!_subscriptions.TryGetValue(subId, out var subscription)) @@ -215,37 +224,28 @@ public async Task UnsubscribeAsync(string subId, CancellationToken ct = default) { throw new InvalidOperationException($"Connection is not open"); } - + var requestId = _getNextRequestId(); var tcs = new TaskCompletionSource(); - _pendingRequests[requestId] = new PendingRequest - { - Tcs = tcs, - SubscriptionId = subId - }; - + _pendingRequests[requestId] = new PendingRequest { Tcs = tcs, SubscriptionId = subId }; + try { var request = new WsRequest { JsonRpc = "2.0", Method = WsRequestMethod.unsubscribe, - Params = new WsRequestParams - { - SubId = subId - }, - Id = requestId + Params = new WsRequestParams { SubId = subId }, + Id = requestId, }; - + await SendMessageAsync(connection, request, ct); - + using var cts = CancellationTokenSource.CreateLinkedTokenSource(ct); cts.CancelAfter(TimeSpan.FromSeconds(30)); - - var completed = await Task.WhenAny( - tcs.Task, - Task.Delay(Timeout.Infinite, cts.Token) - ).ConfigureAwait(false); + + var completed = await Task.WhenAny(tcs.Task, Task.Delay(Timeout.Infinite, cts.Token)) + .ConfigureAwait(false); if (completed != tcs.Task) { @@ -260,16 +260,16 @@ public async Task UnsubscribeAsync(string subId, CancellationToken ct = default) _subscriptions.TryRemove(subId, out _); } } - + public async ValueTask DisposeAsync() { - foreach (var sub in _subscriptions.Values) { try { await sub.CloseAsync(); - } catch { } + } + catch { } } var mintUrls = _connections.Keys.ToList(); foreach (var mintUrl in mintUrls) @@ -280,15 +280,15 @@ public async ValueTask DisposeAsync() _connections.Clear(); _pendingRequests.Clear(); } - + public WebSocketState GetConnectionState(string mintUrl) { var normalized = NormalizeMintUrl(mintUrl); - return _connections.TryGetValue(normalized, out var connection) - ? connection.State + return _connections.TryGetValue(normalized, out var connection) + ? connection.State : WebSocketState.None; } - + public IEnumerable GetSubscriptions(string mintUrl) { var normalized = NormalizeMintUrl(mintUrl); @@ -298,22 +298,25 @@ public IEnumerable GetSubscriptions(string mintUrl) } return _subscriptions.Values.Where(s => s.ConnectionId == connection.Id); } - + public IEnumerable GetConnections() { return _connections.Values; } - + private async Task ListenForMessages(WebsocketConnection connection, CancellationToken ct) { var buffer = new byte[4096]; var messageBuffer = new MemoryStream(); - + try { while (connection.State == WebSocketState.Open && !ct.IsCancellationRequested) { - var result = await connection.WebSocket.ReceiveAsync(new ArraySegment(buffer), ct); + var result = await connection.WebSocket.ReceiveAsync( + new ArraySegment(buffer), + ct + ); if (result.MessageType == WebSocketMessageType.Close) { @@ -321,7 +324,7 @@ private async Task ListenForMessages(WebsocketConnection connection, Cancellatio OnConnectionStateChanged(connection.Id, WebSocketState.Closed); break; } - + if (result.MessageType == WebSocketMessageType.Text) { messageBuffer.Write(buffer, 0, result.Count); @@ -330,7 +333,7 @@ private async Task ListenForMessages(WebsocketConnection connection, Cancellatio var message = Encoding.UTF8.GetString(messageBuffer.ToArray()); messageBuffer.SetLength(0); _processMessage(connection, message); - } + } } } } @@ -346,29 +349,32 @@ private async Task ListenForMessages(WebsocketConnection connection, Cancellatio finally { // Close all subscriptions for this connection - var subscriptionsToClose = _subscriptions.Values - .Where(s => s.ConnectionId == connection.Id) + var subscriptionsToClose = _subscriptions + .Values.Where(s => s.ConnectionId == connection.Id) .ToList(); - + foreach (var sub in subscriptionsToClose) { try { await sub.CloseAsync(); - } catch {} + } + catch { } _subscriptions.TryRemove(sub.Id, out _); } } } - + private void _processMessage(WebsocketConnection connection, string message) { - try + try { var jsonElement = JsonSerializer.Deserialize(message); - - if (jsonElement.TryGetProperty("method", out var methodProp) && - methodProp.GetString() == "subscribe") + + if ( + jsonElement.TryGetProperty("method", out var methodProp) + && methodProp.GetString() == "subscribe" + ) { var notification = JsonSerializer.Deserialize(message); if (notification != null) @@ -379,7 +385,7 @@ private void _processMessage(WebsocketConnection connection, string message) else if (jsonElement.TryGetProperty("result", out _)) { var response = JsonSerializer.Deserialize(message); - if (response != null) + if (response != null) { HandleResponse(response); } @@ -398,27 +404,37 @@ private void _processMessage(WebsocketConnection connection, string message) // Could be logged if logging is added later } } - - private async Task SendMessageAsync(WebsocketConnection connection, T message, CancellationToken ct) + + private async Task SendMessageAsync( + WebsocketConnection connection, + T message, + CancellationToken ct + ) { - var json = JsonSerializer.Serialize(message, new JsonSerializerOptions - { - DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull - }); + var json = JsonSerializer.Serialize( + message, + new JsonSerializerOptions + { + DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, + } + ); var bytes = Encoding.UTF8.GetBytes(json); - + await connection.WebSocket.SendAsync( - new ArraySegment(bytes), - WebSocketMessageType.Text, - true, - ct); + new ArraySegment(bytes), + WebSocketMessageType.Text, + true, + ct + ); } - public bool IsConnected(string mintUrl) - { - var normalized = NormalizeMintUrl(mintUrl); - return _connections.TryGetValue(normalized, out var conn) && - conn.State == WebSocketState.Open; - } + + public bool IsConnected(string mintUrl) + { + var normalized = NormalizeMintUrl(mintUrl); + return _connections.TryGetValue(normalized, out var conn) + && conn.State == WebSocketState.Open; + } + private string GetWebSocketUrl(string mintUrl) { var uri = new Uri(NormalizeMintUrl(mintUrl)); @@ -427,21 +443,20 @@ private string GetWebSocketUrl(string mintUrl) var path = uri.AbsolutePath.TrimEnd('/'); return $"{scheme}://{hostPort}{path}/v1/ws"; } - + private int _getNextRequestId() { return Interlocked.Increment(ref _nextRequestId); } - + private void OnConnectionStateChanged(string connectionId, WebSocketState state) { - ConnectionStateChanged?.Invoke(this, new ConnectionStateChangedEventArgs - { - ConnectionId = connectionId, - State = state - }); + ConnectionStateChanged?.Invoke( + this, + new ConnectionStateChangedEventArgs { ConnectionId = connectionId, State = state } + ); } - + private static string NormalizeMintUrl(string mintUrl) { if (!Uri.TryCreate(mintUrl.TrimEnd('/'), UriKind.Absolute, out var uri)) @@ -461,7 +476,8 @@ private void HandleResponse(WsResponse response) } var result = new RequestResult.Success( SubId: response.Result.SubId, - Status: response.Result.Status); + Status: response.Result.Status + ); pr.Tcs.TrySetResult(result); if (!_subscriptions.TryGetValue(pr.SubscriptionId, out var sub)) @@ -470,16 +486,20 @@ private void HandleResponse(WsResponse response) } sub.NotificationChannel.Writer.TryWrite(new WsMessage.Response(response)); } - + private void HandleError(WsError error) { - if (!_pendingRequests.TryGetValue(error.Id, out var pr)){ return;} + if (!_pendingRequests.TryGetValue(error.Id, out var pr)) + { + return; + } var result = new RequestResult.Failure( Code: error.Error.Code, Message: error.Error.Message, - RequestId: error.Id); + RequestId: error.Id + ); pr.Tcs.TrySetResult(result); - + if (!_subscriptions.TryGetValue(pr.SubscriptionId, out var sub)) { return; @@ -487,6 +507,7 @@ private void HandleError(WsError error) sub.NotificationChannel.Writer.TryWrite(new WsMessage.Error(error)); } + private void _onNotificationReceived(WsNotification notification) { if (!_subscriptions.TryGetValue(notification.Params.SubId, out var sub)) @@ -495,4 +516,4 @@ private void _onNotificationReceived(WsNotification notification) } sub.NotificationChannel.Writer.TryWrite(new WsMessage.Notification(notification)); } -} \ No newline at end of file +} diff --git a/DotNut/Abstractions/Websockets/WebsocketServiceExtensions.cs b/DotNut/Abstractions/Websockets/WebsocketServiceExtensions.cs index cbad041..e2d3005 100644 --- a/DotNut/Abstractions/Websockets/WebsocketServiceExtensions.cs +++ b/DotNut/Abstractions/Websockets/WebsocketServiceExtensions.cs @@ -6,27 +6,40 @@ public static async Task SubscribeToMintQuoteAsync( this IWebsocketService service, string mintUrl, string[] quoteIds, - CancellationToken ct = default) + CancellationToken ct = default + ) { await service.LazyConnectAsync(mintUrl, ct); - return await service.SubscribeAsync(mintUrl, SubscriptionKind.bolt11_mint_quote, quoteIds, ct); + return await service.SubscribeAsync( + mintUrl, + SubscriptionKind.bolt11_mint_quote, + quoteIds, + ct + ); } public static async Task SubscribeToMeltQuoteAsync( this IWebsocketService service, string mintUrl, string[] quoteIds, - CancellationToken ct = default) + CancellationToken ct = default + ) { await service.LazyConnectAsync(mintUrl, ct); - return await service.SubscribeAsync(mintUrl, SubscriptionKind.bolt11_melt_quote, quoteIds, ct); + return await service.SubscribeAsync( + mintUrl, + SubscriptionKind.bolt11_melt_quote, + quoteIds, + ct + ); } public static async Task SubscribeToProofStateAsync( this IWebsocketService service, string mintUrl, string[] proofYs, - CancellationToken ct = default) + CancellationToken ct = default + ) { await service.LazyConnectAsync(mintUrl, ct); return await service.SubscribeAsync(mintUrl, SubscriptionKind.proof_state, proofYs, ct); @@ -36,7 +49,8 @@ public static async Task SubscribeToSingleProofStateAsync( this IWebsocketService service, string mintUrl, string proofY, - CancellationToken ct = default) + CancellationToken ct = default + ) { await service.LazyConnectAsync(mintUrl, ct); return await service.SubscribeToProofStateAsync(mintUrl, new[] { proofY }, ct); @@ -46,7 +60,8 @@ public static async Task SubscribeToSingleMintQuoteAsync( this IWebsocketService service, string mintUrl, string quoteId, - CancellationToken ct = default) + CancellationToken ct = default + ) { await service.LazyConnectAsync(mintUrl, ct); return await service.SubscribeToMintQuoteAsync(mintUrl, new[] { quoteId }, ct); @@ -56,7 +71,8 @@ public static async Task SubscribeToSingleMeltQuoteAsync( this IWebsocketService service, string mintUrl, string quoteId, - CancellationToken ct = default) + CancellationToken ct = default + ) { await service.LazyConnectAsync(mintUrl, ct); return await service.SubscribeToMeltQuoteAsync(mintUrl, new[] { quoteId }, ct); @@ -71,7 +87,8 @@ public static bool IsConnectionActive(this IWebsocketService service, string con public static IEnumerable GetSubscriptionsByKind( this IWebsocketService service, string connectionId, - SubscriptionKind kind) + SubscriptionKind kind + ) { return service.GetSubscriptions(connectionId).Where(s => s.Kind == kind); } @@ -79,7 +96,8 @@ public static IEnumerable GetSubscriptionsByKind( public static async Task UnsubscribeAllAsync( this IWebsocketService service, string connectionId, - CancellationToken ct = default) + CancellationToken ct = default + ) { var subscriptions = service.GetSubscriptions(connectionId).ToList(); foreach (var subscription in subscriptions) @@ -92,7 +110,8 @@ public static async Task UnsubscribeByKindAsync( this IWebsocketService service, string connectionId, SubscriptionKind kind, - CancellationToken ct = default) + CancellationToken ct = default + ) { var subscriptions = service.GetSubscriptionsByKind(connectionId, kind).ToList(); foreach (var subscription in subscriptions) diff --git a/DotNut/Api/CashuHttpClient.cs b/DotNut/Api/CashuHttpClient.cs index cf28069..9d98113 100644 --- a/DotNut/Api/CashuHttpClient.cs +++ b/DotNut/Api/CashuHttpClient.cs @@ -10,7 +10,7 @@ public class CashuHttpClient : ICashuApi { private readonly HttpClient _httpClient; private readonly bool _ownsHttpClient; - + public CashuHttpClient(HttpClient httpClient, bool ownsHttpClient = false) { ArgumentNullException.ThrowIfNull(httpClient); @@ -24,6 +24,7 @@ public string GetBaseUrl() ArgumentNullException.ThrowIfNull(_httpClient.BaseAddress); return _httpClient.BaseAddress.AbsoluteUri; } + public async Task GetKeys(CancellationToken cancellationToken = default) { var response = await _httpClient.GetAsync("v1/keys", cancellationToken); @@ -36,78 +37,133 @@ public async Task GetKeysets(CancellationToken cancellationT return await HandleResponse(response, cancellationToken); } - public async Task GetKeys(KeysetId keysetId, CancellationToken cancellationToken = default) + public async Task GetKeys( + KeysetId keysetId, + CancellationToken cancellationToken = default + ) { var response = await _httpClient.GetAsync($"v1/keys/{keysetId}", cancellationToken); return await HandleResponse(response, cancellationToken); } - public async Task Swap(PostSwapRequest request, CancellationToken cancellationToken = default) + public async Task Swap( + PostSwapRequest request, + CancellationToken cancellationToken = default + ) { - var response = await _httpClient.PostAsync($"v1/swap", - new StringContent(JsonSerializer.Serialize(request), Encoding.UTF8, "application/json"), cancellationToken); + var response = await _httpClient.PostAsync( + $"v1/swap", + new StringContent(JsonSerializer.Serialize(request), Encoding.UTF8, "application/json"), + cancellationToken + ); return await HandleResponse(response, cancellationToken); } - public async Task CreateMintQuote(string method, TRequest request, CancellationToken - cancellationToken = default) + public async Task CreateMintQuote( + string method, + TRequest request, + CancellationToken cancellationToken = default + ) { - var response = await _httpClient.PostAsync($"v1/mint/quote/{method}", - new StringContent(JsonSerializer.Serialize(request), Encoding.UTF8, "application/json"), cancellationToken); + var response = await _httpClient.PostAsync( + $"v1/mint/quote/{method}", + new StringContent(JsonSerializer.Serialize(request), Encoding.UTF8, "application/json"), + cancellationToken + ); return await HandleResponse(response, cancellationToken); } - public async Task CreateMeltQuote(string method, TRequest request, CancellationToken - cancellationToken = default) + public async Task CreateMeltQuote( + string method, + TRequest request, + CancellationToken cancellationToken = default + ) { - var response = await _httpClient.PostAsync($"v1/melt/quote/{method}", - new StringContent(JsonSerializer.Serialize(request), Encoding.UTF8, "application/json"), cancellationToken); + var response = await _httpClient.PostAsync( + $"v1/melt/quote/{method}", + new StringContent(JsonSerializer.Serialize(request), Encoding.UTF8, "application/json"), + cancellationToken + ); return await HandleResponse(response, cancellationToken); } - public async Task Melt(string method, TRequest request, CancellationToken - cancellationToken = default) + public async Task Melt( + string method, + TRequest request, + CancellationToken cancellationToken = default + ) { - var response = await _httpClient.PostAsync($"v1/melt/{method}", - new StringContent(JsonSerializer.Serialize(request), Encoding.UTF8, "application/json"), cancellationToken); + var response = await _httpClient.PostAsync( + $"v1/melt/{method}", + new StringContent(JsonSerializer.Serialize(request), Encoding.UTF8, "application/json"), + cancellationToken + ); return await HandleResponse(response, cancellationToken); } - public async Task CheckMeltQuote(string method, string quoteId, CancellationToken - cancellationToken = default) + public async Task CheckMeltQuote( + string method, + string quoteId, + CancellationToken cancellationToken = default + ) { - var response = await _httpClient.GetAsync($"v1/melt/quote/{method}/{quoteId}", cancellationToken); + var response = await _httpClient.GetAsync( + $"v1/melt/quote/{method}/{quoteId}", + cancellationToken + ); return await HandleResponse(response, cancellationToken); } - public async Task CheckMintQuote(string method, string quoteId, CancellationToken - cancellationToken = default) + public async Task CheckMintQuote( + string method, + string quoteId, + CancellationToken cancellationToken = default + ) { - var response = await _httpClient.GetAsync($"v1/mint/quote/{method}/{quoteId}", cancellationToken); + var response = await _httpClient.GetAsync( + $"v1/mint/quote/{method}/{quoteId}", + cancellationToken + ); return await HandleResponse(response, cancellationToken); } - public async Task Mint(string method, TRequest request, - CancellationToken cancellationToken = default) + public async Task Mint( + string method, + TRequest request, + CancellationToken cancellationToken = default + ) { - var response = await _httpClient.PostAsync($"v1/mint/{method}", - new StringContent(JsonSerializer.Serialize(request), Encoding.UTF8, "application/json"), cancellationToken); + var response = await _httpClient.PostAsync( + $"v1/mint/{method}", + new StringContent(JsonSerializer.Serialize(request), Encoding.UTF8, "application/json"), + cancellationToken + ); return await HandleResponse(response, cancellationToken); } - public async Task CheckState(PostCheckStateRequest request, - CancellationToken cancellationToken = default) + public async Task CheckState( + PostCheckStateRequest request, + CancellationToken cancellationToken = default + ) { - var response = await _httpClient.PostAsync($"v1/checkstate", - new StringContent(JsonSerializer.Serialize(request), Encoding.UTF8, "application/json"), cancellationToken); + var response = await _httpClient.PostAsync( + $"v1/checkstate", + new StringContent(JsonSerializer.Serialize(request), Encoding.UTF8, "application/json"), + cancellationToken + ); return await HandleResponse(response, cancellationToken); } - public async Task Restore(PostRestoreRequest request, - CancellationToken cancellationToken = default) + public async Task Restore( + PostRestoreRequest request, + CancellationToken cancellationToken = default + ) { - var response = await _httpClient.PostAsync($"v1/restore", - new StringContent(JsonSerializer.Serialize(request), Encoding.UTF8, "application/json"), cancellationToken); + var response = await _httpClient.PostAsync( + $"v1/restore", + new StringContent(JsonSerializer.Serialize(request), Encoding.UTF8, "application/json"), + cancellationToken + ); return await HandleResponse(response, cancellationToken); } @@ -117,12 +173,16 @@ public async Task GetInfo(CancellationToken cancellationToken = return await HandleResponse(response, cancellationToken); } - protected async Task HandleResponse(HttpResponseMessage response, CancellationToken cancellationToken) + protected async Task HandleResponse( + HttpResponseMessage response, + CancellationToken cancellationToken + ) { if (response.StatusCode == HttpStatusCode.BadRequest) { - var error = - await response.Content.ReadFromJsonAsync(cancellationToken: cancellationToken); + var error = await response.Content.ReadFromJsonAsync( + cancellationToken: cancellationToken + ); throw new CashuProtocolException(error); } @@ -148,4 +208,4 @@ public void Dispose() _httpClient.Dispose(); } } -} \ No newline at end of file +} diff --git a/DotNut/Api/CashuProtocolError.cs b/DotNut/Api/CashuProtocolError.cs index 3e4f7aa..edb98c8 100644 --- a/DotNut/Api/CashuProtocolError.cs +++ b/DotNut/Api/CashuProtocolError.cs @@ -4,6 +4,9 @@ namespace DotNut.Api; public class CashuProtocolError { - [JsonPropertyName("detail")] public string Detail { get; set; } - [JsonPropertyName("code")] public int Code { get; set; } -} \ No newline at end of file + [JsonPropertyName("detail")] + public string Detail { get; set; } + + [JsonPropertyName("code")] + public int Code { get; set; } +} diff --git a/DotNut/Api/CashuProtocolException.cs b/DotNut/Api/CashuProtocolException.cs index 86ad641..ab2d229 100644 --- a/DotNut/Api/CashuProtocolException.cs +++ b/DotNut/Api/CashuProtocolException.cs @@ -2,10 +2,11 @@ public class CashuProtocolException : Exception { - public CashuProtocolException(CashuProtocolError error) : base(error.Detail) + public CashuProtocolException(CashuProtocolError error) + : base(error.Detail) { Error = error; } public CashuProtocolError Error { get; } -} \ No newline at end of file +} diff --git a/DotNut/Api/ICashuApi.cs b/DotNut/Api/ICashuApi.cs index 3513d18..734ca4a 100644 --- a/DotNut/Api/ICashuApi.cs +++ b/DotNut/Api/ICashuApi.cs @@ -2,28 +2,53 @@ namespace DotNut.Api; -public interface ICashuApi: IDisposable +public interface ICashuApi : IDisposable { string GetBaseUrl(); Task GetKeys(CancellationToken cancellationToken = default); Task GetKeys(KeysetId keysetId, CancellationToken cancellationToken = default); Task GetKeysets(CancellationToken cancellationToken = default); - Task Swap(PostSwapRequest request, CancellationToken cancellationToken = default); + Task Swap( + PostSwapRequest request, + CancellationToken cancellationToken = default + ); - Task CreateMintQuote(string method, TRequest request, CancellationToken - cancellationToken = default); + Task CreateMintQuote( + string method, + TRequest request, + CancellationToken cancellationToken = default + ); - Task CreateMeltQuote(string method, TRequest request, CancellationToken - cancellationToken = default); + Task CreateMeltQuote( + string method, + TRequest request, + CancellationToken cancellationToken = default + ); - Task Melt(string method, TRequest request, CancellationToken - cancellationToken = default); + Task Melt( + string method, + TRequest request, + CancellationToken cancellationToken = default + ); - Task CheckMintQuote(string method, string quoteId, CancellationToken - cancellationToken = default); + Task CheckMintQuote( + string method, + string quoteId, + CancellationToken cancellationToken = default + ); - Task Mint(string method, TRequest request, CancellationToken cancellationToken = default); - Task CheckState(PostCheckStateRequest request, CancellationToken cancellationToken = default); - Task Restore(PostRestoreRequest request, CancellationToken cancellationToken = default); + Task Mint( + string method, + TRequest request, + CancellationToken cancellationToken = default + ); + Task CheckState( + PostCheckStateRequest request, + CancellationToken cancellationToken = default + ); + Task Restore( + PostRestoreRequest request, + CancellationToken cancellationToken = default + ); Task GetInfo(CancellationToken cancellationToken = default); -} \ No newline at end of file +} diff --git a/DotNut/ApiModels/GetKeysResponse.cs b/DotNut/ApiModels/GetKeysResponse.cs index db21c2b..abd9d8e 100644 --- a/DotNut/ApiModels/GetKeysResponse.cs +++ b/DotNut/ApiModels/GetKeysResponse.cs @@ -4,7 +4,8 @@ namespace DotNut.ApiModels; public class GetKeysResponse { - [JsonPropertyName("keysets")] public KeysetItemResponse[] Keysets { get; set; } + [JsonPropertyName("keysets")] + public KeysetItemResponse[] Keysets { get; set; } public class KeysetItemResponse { @@ -17,4 +18,4 @@ public class KeysetItemResponse [JsonPropertyName("final_expiry")] public ulong? FinalExpiry { get; set; } [JsonPropertyName("keys")] public Keyset Keys { get; set; } } -} \ No newline at end of file +} diff --git a/DotNut/ApiModels/GetKeysetsResponse.cs b/DotNut/ApiModels/GetKeysetsResponse.cs index 2760802..d388a3e 100644 --- a/DotNut/ApiModels/GetKeysetsResponse.cs +++ b/DotNut/ApiModels/GetKeysetsResponse.cs @@ -4,16 +4,26 @@ namespace DotNut.ApiModels; public class GetKeysetsResponse { - [JsonPropertyName("keysets")] public KeysetItemResponse[] Keysets { get; set; } + [JsonPropertyName("keysets")] + public KeysetItemResponse[] Keysets { get; set; } public class KeysetItemResponse { - [JsonPropertyName("id")] public KeysetId Id { get; set; } - [JsonPropertyName("unit")] public string Unit { get; set; } - [JsonPropertyName("active")] public bool Active { get; set; } + [JsonPropertyName("id")] + public KeysetId Id { get; set; } + + [JsonPropertyName("unit")] + public string Unit { get; set; } + + [JsonPropertyName("active")] + public bool Active { get; set; } + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - [JsonPropertyName("input_fee_ppk")] public ulong? InputFee { get; set; } + [JsonPropertyName("input_fee_ppk")] + public ulong? InputFee { get; set; } + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - [JsonPropertyName("final_expiry")] public ulong? FinalExpiry { get; set; } + [JsonPropertyName("final_expiry")] + public ulong? FinalExpiry { get; set; } } -} \ No newline at end of file +} diff --git a/DotNut/ApiModels/Info/ContactInfo.cs b/DotNut/ApiModels/Info/ContactInfo.cs index 46ea371..63573f3 100644 --- a/DotNut/ApiModels/Info/ContactInfo.cs +++ b/DotNut/ApiModels/Info/ContactInfo.cs @@ -4,6 +4,9 @@ namespace DotNut.ApiModels; public class ContactInfo { - [JsonPropertyName("method")] public string Method { get; set; } - [JsonPropertyName("info")] public string Info { get; set; } -} \ No newline at end of file + [JsonPropertyName("method")] + public string Method { get; set; } + + [JsonPropertyName("info")] + public string Info { get; set; } +} diff --git a/DotNut/ApiModels/Info/GetInfoResponse.cs b/DotNut/ApiModels/Info/GetInfoResponse.cs index 0ded0bd..73c3286 100644 --- a/DotNut/ApiModels/Info/GetInfoResponse.cs +++ b/DotNut/ApiModels/Info/GetInfoResponse.cs @@ -33,7 +33,7 @@ public class GetInfoResponse [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] [JsonPropertyName("motd")] public string? Motd { get; set; } - + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] [JsonPropertyName("icon_url")] public string? IconUrl { get; set; } @@ -41,12 +41,12 @@ public class GetInfoResponse [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] [JsonPropertyName("urls")] public string[]? Urls { get; set; } - + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] [JsonConverter(typeof(UnixDateTimeOffsetConverter))] [JsonPropertyName("time")] public DateTimeOffset? Time { get; set; } - + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] [JsonPropertyName("tos_url")] public string? TosUrl { get; set; } @@ -54,4 +54,4 @@ public class GetInfoResponse [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] [JsonPropertyName("nuts")] public Dictionary? Nuts { get; set; } -} \ No newline at end of file +} diff --git a/DotNut/ApiModels/Info/MPPInfo.cs b/DotNut/ApiModels/Info/MPPInfo.cs index 55aa887..7394f0c 100644 --- a/DotNut/ApiModels/Info/MPPInfo.cs +++ b/DotNut/ApiModels/Info/MPPInfo.cs @@ -6,12 +6,13 @@ public class MPPInfo { [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] [JsonPropertyName("methods")] - public MPPMethod[]? Methods {get; set;} + public MPPMethod[]? Methods { get; set; } + public class MPPMethod { [JsonPropertyName("method")] - public string Method {get; set;} - + public string Method { get; set; } + [JsonPropertyName("unit")] public string Unit { get; set; } } diff --git a/DotNut/ApiModels/Info/SwapInfo.cs b/DotNut/ApiModels/Info/SwapInfo.cs index 27ddb78..7c3707f 100644 --- a/DotNut/ApiModels/Info/SwapInfo.cs +++ b/DotNut/ApiModels/Info/SwapInfo.cs @@ -6,32 +6,32 @@ public class SwapInfo { [JsonPropertyName("methods")] public SwapMethod[] Methods { get; set; } - + [JsonPropertyName("disabled")] public bool Disabled { get; set; } - + public class SwapMethod { [JsonPropertyName("method")] - public string Method {get; set;} - + public string Method { get; set; } + [JsonPropertyName("unit")] - public string Unit {get; set;} - + public string Unit { get; set; } + [JsonPropertyName("min_amount")] public ulong MinAmount { get; set; } - + [JsonPropertyName("max_amount")] public ulong MaxAmount { get; set; } - + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] [JsonPropertyName("options")] - public SwapOptions? Options {get; set;} - + public SwapOptions? Options { get; set; } + public class SwapOptions { [JsonPropertyName("description")] - public bool? Description {get; set;} + public bool? Description { get; set; } } } } diff --git a/DotNut/ApiModels/Info/WebSocketSupport.cs b/DotNut/ApiModels/Info/WebSocketSupport.cs index be12e35..fed139a 100644 --- a/DotNut/ApiModels/Info/WebSocketSupport.cs +++ b/DotNut/ApiModels/Info/WebSocketSupport.cs @@ -6,8 +6,10 @@ public class WebSocketSupport { [JsonPropertyName("method")] public string Method { get; set; } + [JsonPropertyName("unit")] - public string Unit {get; set;} + public string Unit { get; set; } + [JsonPropertyName("commands")] public string[] Commands { get; set; } -} \ No newline at end of file +} diff --git a/DotNut/ApiModels/Melt/MeltQuoteRequestOptions.cs b/DotNut/ApiModels/Melt/MeltQuoteRequestOptions.cs index fd0b944..1895f2e 100644 --- a/DotNut/ApiModels/Melt/MeltQuoteRequestOptions.cs +++ b/DotNut/ApiModels/Melt/MeltQuoteRequestOptions.cs @@ -13,4 +13,4 @@ public class AmountlessMeltQuoteOptions { [JsonPropertyName("amount_msat")] public ulong AmountMsat { get; set; } -} \ No newline at end of file +} diff --git a/DotNut/ApiModels/Melt/PostMeltRequest.cs b/DotNut/ApiModels/Melt/PostMeltRequest.cs index a5cd262..706f5ca 100644 --- a/DotNut/ApiModels/Melt/PostMeltRequest.cs +++ b/DotNut/ApiModels/Melt/PostMeltRequest.cs @@ -4,14 +4,13 @@ namespace DotNut.ApiModels; public class PostMeltRequest { - [JsonPropertyName("quote")] public string Quote { get; set; } - + [JsonPropertyName("inputs")] public Proof[] Inputs { get; set; } - + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] [JsonPropertyName("outputs")] public BlindedMessage[]? Outputs { get; set; } -} \ No newline at end of file +} diff --git a/DotNut/ApiModels/Melt/bolt11/PostMeltQuoteBolt11Request.cs b/DotNut/ApiModels/Melt/bolt11/PostMeltQuoteBolt11Request.cs index d207c5d..a9358ca 100644 --- a/DotNut/ApiModels/Melt/bolt11/PostMeltQuoteBolt11Request.cs +++ b/DotNut/ApiModels/Melt/bolt11/PostMeltQuoteBolt11Request.cs @@ -5,14 +5,13 @@ namespace DotNut.ApiModels; public class PostMeltQuoteBolt11Request { - - [JsonPropertyName("request")] + [JsonPropertyName("request")] public string Request { get; set; } - [JsonPropertyName("unit")] + [JsonPropertyName("unit")] public string Unit { get; set; } - + [JsonPropertyName("options")] [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public MeltQuoteRequestOptions? Options { get; set; } -} \ No newline at end of file +} diff --git a/DotNut/ApiModels/Melt/bolt11/PostMeltQuoteBolt11Response.cs b/DotNut/ApiModels/Melt/bolt11/PostMeltQuoteBolt11Response.cs index 8beffbb..ecf0bd0 100644 --- a/DotNut/ApiModels/Melt/bolt11/PostMeltQuoteBolt11Response.cs +++ b/DotNut/ApiModels/Melt/bolt11/PostMeltQuoteBolt11Response.cs @@ -4,26 +4,26 @@ namespace DotNut.ApiModels; public class PostMeltQuoteBolt11Response { - [JsonPropertyName("quote")] + [JsonPropertyName("quote")] public string Quote { get; set; } - - [JsonPropertyName("amount")] + + [JsonPropertyName("amount")] public ulong Amount { get; set; } - - [JsonPropertyName("fee_reserve")] + + [JsonPropertyName("fee_reserve")] public int FeeReserve { get; set; } - + [JsonPropertyName("state")] - public string State {get; set;} - + public string State { get; set; } + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] [JsonPropertyName("expiry")] - public int? Expiry {get; set;} - + public int? Expiry { get; set; } + [JsonPropertyName("payment_preimage")] - public string? PaymentPreimage {get; set;} - + public string? PaymentPreimage { get; set; } + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] [JsonPropertyName("change")] public BlindSignature[]? Change { get; set; } -} \ No newline at end of file +} diff --git a/DotNut/ApiModels/Melt/bolt12/PostMeltQuoteBolt12Request.cs b/DotNut/ApiModels/Melt/bolt12/PostMeltQuoteBolt12Request.cs index 5d8a5ed..ed202f4 100644 --- a/DotNut/ApiModels/Melt/bolt12/PostMeltQuoteBolt12Request.cs +++ b/DotNut/ApiModels/Melt/bolt12/PostMeltQuoteBolt12Request.cs @@ -8,12 +8,11 @@ public class PostMeltQuoteBolt12Request { [JsonPropertyName("request")] public string Request { get; set; } - + [JsonPropertyName("unit")] public string Unit { get; set; } - + [JsonPropertyName("options")] [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public MeltQuoteRequestOptions? Options { get; set; } } - diff --git a/DotNut/ApiModels/Melt/bolt12/PostMeltQuoteBolt12Response.cs b/DotNut/ApiModels/Melt/bolt12/PostMeltQuoteBolt12Response.cs index 7a4eaf2..14a4a38 100644 --- a/DotNut/ApiModels/Melt/bolt12/PostMeltQuoteBolt12Response.cs +++ b/DotNut/ApiModels/Melt/bolt12/PostMeltQuoteBolt12Response.cs @@ -4,24 +4,32 @@ namespace DotNut.ApiModels.Melt.bolt12; public class PostMeltQuoteBolt12Response { - [JsonPropertyName("quote")] public string Quote { get; set; } + [JsonPropertyName("quote")] + public string Quote { get; set; } - [JsonPropertyName("request")] public string Request { get; set; } + [JsonPropertyName("request")] + public string Request { get; set; } - [JsonPropertyName("amount")] public ulong Amount { get; set; } - - [JsonPropertyName("unit")] public string Unit { get; set; } + [JsonPropertyName("amount")] + public ulong Amount { get; set; } - [JsonPropertyName("fee_reserve")] public ulong FeeReserve { get; set; } + [JsonPropertyName("unit")] + public string Unit { get; set; } + + [JsonPropertyName("fee_reserve")] + public ulong FeeReserve { get; set; } + + [JsonPropertyName("state")] + public string State { get; set; } + + [JsonPropertyName("expiry")] + public int Expiry { get; set; } - [JsonPropertyName("state")] public string State { get; set; } - - [JsonPropertyName("expiry")] public int Expiry { get; set; } - - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - [JsonPropertyName("payment_preimage")] public string? PaymentPreimage { get; set; } - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - [JsonPropertyName("change")] public BlindSignature[]? Change { get; set; } + [JsonPropertyName("payment_preimage")] + public string? PaymentPreimage { get; set; } -} \ No newline at end of file + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("change")] + public BlindSignature[]? Change { get; set; } +} diff --git a/DotNut/ApiModels/Mint/PostMintRequest.cs b/DotNut/ApiModels/Mint/PostMintRequest.cs index 4cfd4a2..0622927 100644 --- a/DotNut/ApiModels/Mint/PostMintRequest.cs +++ b/DotNut/ApiModels/Mint/PostMintRequest.cs @@ -9,8 +9,8 @@ public class PostMintRequest [JsonPropertyName("outputs")] public BlindedMessage[] Outputs { get; set; } - + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] [JsonPropertyName("signature")] public string? Signature { get; set; } -} \ No newline at end of file +} diff --git a/DotNut/ApiModels/Mint/PostMintResponse.cs b/DotNut/ApiModels/Mint/PostMintResponse.cs index 609fa6a..5aa0e8c 100644 --- a/DotNut/ApiModels/Mint/PostMintResponse.cs +++ b/DotNut/ApiModels/Mint/PostMintResponse.cs @@ -6,4 +6,4 @@ public class PostMintResponse { [JsonPropertyName("signatures")] public BlindSignature[] Signatures { get; set; } -} \ No newline at end of file +} diff --git a/DotNut/ApiModels/Mint/bolt11/PostMintQuoteBolt11Request.cs b/DotNut/ApiModels/Mint/bolt11/PostMintQuoteBolt11Request.cs index dc73c43..95021ad 100644 --- a/DotNut/ApiModels/Mint/bolt11/PostMintQuoteBolt11Request.cs +++ b/DotNut/ApiModels/Mint/bolt11/PostMintQuoteBolt11Request.cs @@ -4,18 +4,17 @@ namespace DotNut.ApiModels; public class PostMintQuoteBolt11Request { - - [JsonPropertyName("amount")] - public ulong Amount {get; set;} - - [JsonPropertyName("unit")] - public string Unit {get; set;} - + [JsonPropertyName("amount")] + public ulong Amount { get; set; } + + [JsonPropertyName("unit")] + public string Unit { get; set; } + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] [JsonPropertyName("pubkey")] - public string? Pubkey {get; set;} - + public string? Pubkey { get; set; } + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - [JsonPropertyName("description")] - public string? Description {get; set;} -} \ No newline at end of file + [JsonPropertyName("description")] + public string? Description { get; set; } +} diff --git a/DotNut/ApiModels/Mint/bolt11/PostMintQuoteBolt11Response.cs b/DotNut/ApiModels/Mint/bolt11/PostMintQuoteBolt11Response.cs index f5d42b2..c7f579d 100644 --- a/DotNut/ApiModels/Mint/bolt11/PostMintQuoteBolt11Response.cs +++ b/DotNut/ApiModels/Mint/bolt11/PostMintQuoteBolt11Response.cs @@ -4,29 +4,29 @@ namespace DotNut.ApiModels; public class PostMintQuoteBolt11Response { - [JsonPropertyName("quote")] + [JsonPropertyName("quote")] public string Quote { get; set; } - - [JsonPropertyName("request")] + + [JsonPropertyName("request")] public string Request { get; set; } - - [JsonPropertyName("state")] + + [JsonPropertyName("state")] public string State { get; set; } - + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - [JsonPropertyName("expiry")] + [JsonPropertyName("expiry")] public int? Expiry { get; set; } - + // 'amount' and 'unit' were recently added to the spec in PostMintQuoteBolt11Response, so they are optional for now [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] [JsonPropertyName("amount")] public ulong? Amount { get; set; } - + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] [JsonPropertyName("unit")] - public string? Unit {get; set;} - + public string? Unit { get; set; } + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] [JsonPropertyName("pubkey")] - public string? PubKey {get; set;} -} \ No newline at end of file + public string? PubKey { get; set; } +} diff --git a/DotNut/ApiModels/Mint/bolt12/PostMintQuoteBolt12Request.cs b/DotNut/ApiModels/Mint/bolt12/PostMintQuoteBolt12Request.cs index d00c3d1..40428e2 100644 --- a/DotNut/ApiModels/Mint/bolt12/PostMintQuoteBolt12Request.cs +++ b/DotNut/ApiModels/Mint/bolt12/PostMintQuoteBolt12Request.cs @@ -7,15 +7,14 @@ public class PostMintQuoteBolt12Request [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] [JsonPropertyName("amount")] public ulong? Amount { get; set; } - + [JsonPropertyName("unit")] - public string Unit {get; set;} - + public string Unit { get; set; } + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] [JsonPropertyName("description")] public string? Description { get; set; } - + [JsonPropertyName("pubkey")] public string Pubkey { get; set; } - -} \ No newline at end of file +} diff --git a/DotNut/ApiModels/Mint/bolt12/PostMintQuoteBolt12Response.cs b/DotNut/ApiModels/Mint/bolt12/PostMintQuoteBolt12Response.cs index cbb8a6b..ea1663b 100644 --- a/DotNut/ApiModels/Mint/bolt12/PostMintQuoteBolt12Response.cs +++ b/DotNut/ApiModels/Mint/bolt12/PostMintQuoteBolt12Response.cs @@ -6,27 +6,27 @@ public class PostMintQuoteBolt12Response { [JsonPropertyName("quote")] public string Quote { get; set; } - + [JsonPropertyName("request")] - public string Request {get; set;} - + public string Request { get; set; } + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] [JsonPropertyName("amount")] public ulong? Amount { get; set; } - + [JsonPropertyName("unit")] - public string Unit {get; set;} - + public string Unit { get; set; } + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] [JsonPropertyName("expiry")] - public int? Expiry {get; set;} - + public int? Expiry { get; set; } + [JsonPropertyName("pubkey")] - public string Pubkey {get; set;} - + public string Pubkey { get; set; } + [JsonPropertyName("amount_paid")] - public ulong AmountPaid {get; set;} - + public ulong AmountPaid { get; set; } + [JsonPropertyName("amount_issued")] - public ulong AmountIssued {get; set;} -} \ No newline at end of file + public ulong AmountIssued { get; set; } +} diff --git a/DotNut/ApiModels/PostCheckStateRequest.cs b/DotNut/ApiModels/PostCheckStateRequest.cs index 3421fd2..2860710 100644 --- a/DotNut/ApiModels/PostCheckStateRequest.cs +++ b/DotNut/ApiModels/PostCheckStateRequest.cs @@ -6,4 +6,4 @@ public class PostCheckStateRequest { [JsonPropertyName("Ys")] public string[] Ys { get; set; } -} \ No newline at end of file +} diff --git a/DotNut/ApiModels/PostCheckStateResponse.cs b/DotNut/ApiModels/PostCheckStateResponse.cs index 475881b..a197fdf 100644 --- a/DotNut/ApiModels/PostCheckStateResponse.cs +++ b/DotNut/ApiModels/PostCheckStateResponse.cs @@ -4,7 +4,6 @@ namespace DotNut.ApiModels; public class PostCheckStateResponse { - [JsonPropertyName("states")] public StateResponseItem[] States { get; set; } -} \ No newline at end of file +} diff --git a/DotNut/ApiModels/PostRestoreRequest.cs b/DotNut/ApiModels/PostRestoreRequest.cs index e46e872..d0d8c6b 100644 --- a/DotNut/ApiModels/PostRestoreRequest.cs +++ b/DotNut/ApiModels/PostRestoreRequest.cs @@ -6,4 +6,4 @@ public class PostRestoreRequest { [JsonPropertyName("outputs")] public BlindedMessage[] Outputs { get; set; } -} \ No newline at end of file +} diff --git a/DotNut/ApiModels/PostRestoreResponse.cs b/DotNut/ApiModels/PostRestoreResponse.cs index 7af6dfa..6b05a00 100644 --- a/DotNut/ApiModels/PostRestoreResponse.cs +++ b/DotNut/ApiModels/PostRestoreResponse.cs @@ -6,6 +6,7 @@ public class PostRestoreResponse { [JsonPropertyName("outputs")] public BlindedMessage[] Outputs { get; set; } + [JsonPropertyName("signatures")] public BlindSignature[] Signatures { get; set; } -} \ No newline at end of file +} diff --git a/DotNut/ApiModels/PostSwapRequest.cs b/DotNut/ApiModels/PostSwapRequest.cs index a018dce..cffafee 100644 --- a/DotNut/ApiModels/PostSwapRequest.cs +++ b/DotNut/ApiModels/PostSwapRequest.cs @@ -4,6 +4,9 @@ namespace DotNut.ApiModels; public class PostSwapRequest { - [JsonPropertyName("inputs")] public Proof[] Inputs { get; set; } - [JsonPropertyName("outputs")] public BlindedMessage[] Outputs { get; set; } -} \ No newline at end of file + [JsonPropertyName("inputs")] + public Proof[] Inputs { get; set; } + + [JsonPropertyName("outputs")] + public BlindedMessage[] Outputs { get; set; } +} diff --git a/DotNut/ApiModels/PostSwapResponse.cs b/DotNut/ApiModels/PostSwapResponse.cs index 5bb3caf..2103035 100644 --- a/DotNut/ApiModels/PostSwapResponse.cs +++ b/DotNut/ApiModels/PostSwapResponse.cs @@ -4,5 +4,6 @@ namespace DotNut.ApiModels; public class PostSwapResponse { - [JsonPropertyName("signatures")] public BlindSignature[] Signatures { get; set; } -} \ No newline at end of file + [JsonPropertyName("signatures")] + public BlindSignature[] Signatures { get; set; } +} diff --git a/DotNut/ApiModels/StateResponseItem.cs b/DotNut/ApiModels/StateResponseItem.cs index 5d0a05c..c670f97 100644 --- a/DotNut/ApiModels/StateResponseItem.cs +++ b/DotNut/ApiModels/StateResponseItem.cs @@ -4,8 +4,8 @@ namespace DotNut.ApiModels; public class StateResponseItem { - public string Y { get; set; } + [JsonConverter(typeof(JsonStringEnumConverter))] public TokenState State { get; set; } public string? Witness { get; set; } @@ -14,6 +14,6 @@ public enum TokenState { UNSPENT, PENDING, - SPENT + SPENT, } -} \ No newline at end of file +} diff --git a/DotNut/DotNut.csproj b/DotNut/DotNut.csproj index 2710efb..5c40026 100644 --- a/DotNut/DotNut.csproj +++ b/DotNut/DotNut.csproj @@ -1,27 +1,24 @@  + + net8.0 + enable + enable + true + DotNut + Kukks + A full C# native implementation of the Cashu protocol + MIT + https://github.com/Kukks/DotNut + 1.0.6 + https://github.com/Kukks/DotNut + git + bitcoin cashu ecash secp256k1 + https://github.com/Kukks/DotNut/blob/master/LICENSE + - - net8.0 - enable - enable - true - DotNut - Kukks - A full C# native implementation of the Cashu protocol - MIT - https://github.com/Kukks/DotNut - 1.0.6 - https://github.com/Kukks/DotNut - git - bitcoin cashu ecash secp256k1 - https://github.com/Kukks/DotNut/blob/master/LICENSE - - - - - - - - - + + + + + diff --git a/DotNut/Encoding/Base64UrlSafe.cs b/DotNut/Encoding/Base64UrlSafe.cs index dfa0e44..1d0e9fa 100644 --- a/DotNut/Encoding/Base64UrlSafe.cs +++ b/DotNut/Encoding/Base64UrlSafe.cs @@ -2,13 +2,17 @@ public static class Base64UrlSafe { - static readonly char[] padding = {'='}; + static readonly char[] padding = { '=' }; //(base64 encoding with / replaced by _ and + by -) public static string Encode(byte[] data) { - return System.Convert.ToBase64String(data) - .TrimEnd(padding).Replace('+', '-').Replace('/', '_').TrimEnd(padding); + return System + .Convert.ToBase64String(data) + .TrimEnd(padding) + .Replace('+', '-') + .Replace('/', '_') + .TrimEnd(padding); } public static byte[] Decode(string base64) @@ -26,4 +30,4 @@ public static byte[] Decode(string base64) return System.Convert.FromBase64String(incoming); } -} \ No newline at end of file +} diff --git a/DotNut/Encoding/CashuTokenHelper.cs b/DotNut/Encoding/CashuTokenHelper.cs index e7de7a5..e69b290 100644 --- a/DotNut/Encoding/CashuTokenHelper.cs +++ b/DotNut/Encoding/CashuTokenHelper.cs @@ -26,7 +26,7 @@ public static string Encode(this CashuToken token, string version = "B", bool ma foreach (var token1 in token.Tokens) { if (token1.Mint.EndsWith("/")) - { + { token1.Mint = token1.Mint.TrimEnd('/'); } foreach (var proof in token1.Proofs) @@ -34,7 +34,7 @@ public static string Encode(this CashuToken token, string version = "B", bool ma proof.Id = MaybeShortenId(proof.Id); } } - + var encoded = encoder.Encode(token); var result = $"{CashuPrefix}{version}{encoded}"; @@ -47,7 +47,11 @@ public static string Encode(this CashuToken token, string version = "B", bool ma return result; } - public static CashuToken Decode(string token, out string? version, List? keysetIds = null) + public static CashuToken Decode( + string token, + out string? version, + List? keysetIds = null + ) { version = null; if (Uri.IsWellFormedUriString(token, UriKind.Absolute)) @@ -70,7 +74,7 @@ public static CashuToken Decode(string token, out string? version, List MapShortKeysetIds(List proofs, List? keysetIds = null) + + private static List MapShortKeysetIds( + List proofs, + List? keysetIds = null + ) { - if (proofs.Count == 0 || proofs.All(p => p.Id.GetVersion() != 0x01 || p.Id.ToString().Length != 16)) + if ( + proofs.Count == 0 + || proofs.All(p => p.Id.GetVersion() != 0x01 || p.Id.ToString().Length != 16) + ) { return proofs; } if (keysetIds is null) { - throw new ArgumentNullException(nameof(keysetIds), - "Encountered short keyset IDs but no keysets were provided for mapping."); + throw new ArgumentNullException( + nameof(keysetIds), + "Encountered short keyset IDs but no keysets were provided for mapping." + ); } - - return proofs.Select(proof => - { - if (proof.Id.GetVersion() != 0x01) - return proof; - - var proofShortId = proof.Id.ToString(); - var match = keysetIds.FirstOrDefault(k=> k.ToString().StartsWith(proofShortId, StringComparison.OrdinalIgnoreCase)); - - if (match is null) - throw new Exception($"Couldn't map short keyset ID {proof.Id} to any known keysets of the current Mint"); - return new Proof + return proofs + .Select(proof => { - Amount = proof.Amount, - Secret = proof.Secret, - C = proof.C, - Witness = proof.Witness, - DLEQ = proof.DLEQ, - Id = match - }; - }).ToList(); + if (proof.Id.GetVersion() != 0x01) + return proof; + + var proofShortId = proof.Id.ToString(); + var match = keysetIds.FirstOrDefault(k => + k.ToString().StartsWith(proofShortId, StringComparison.OrdinalIgnoreCase) + ); + + if (match is null) + throw new Exception( + $"Couldn't map short keyset ID {proof.Id} to any known keysets of the current Mint" + ); + + return new Proof + { + Amount = proof.Amount, + Secret = proof.Secret, + C = proof.C, + Witness = proof.Witness, + DLEQ = proof.DLEQ, + Id = match, + }; + }) + .ToList(); } -} \ No newline at end of file +} diff --git a/DotNut/Encoding/CashuTokenV3Encoder.cs b/DotNut/Encoding/CashuTokenV3Encoder.cs index 8685ef4..2f94d89 100644 --- a/DotNut/Encoding/CashuTokenV3Encoder.cs +++ b/DotNut/Encoding/CashuTokenV3Encoder.cs @@ -16,4 +16,4 @@ public CashuToken Decode(string token) var json = Encoding.UTF8.GetString(Base64UrlSafe.Decode(token)); return JsonSerializer.Deserialize(json)!; } -} \ No newline at end of file +} diff --git a/DotNut/Encoding/CashuTokenV4Encoder.cs b/DotNut/Encoding/CashuTokenV4Encoder.cs index 1f39bc6..47b7b85 100644 --- a/DotNut/Encoding/CashuTokenV4Encoder.cs +++ b/DotNut/Encoding/CashuTokenV4Encoder.cs @@ -26,24 +26,32 @@ public CBORObject ToCBORObject(CashuToken token) if (mints.Distinct().Count() != 1) throw new FormatException("All proofs must have the same mint in v4 tokens"); var proofSets = CBORObject.NewArray(); - foreach (var proofSet in token.Tokens.SelectMany(token1 => token1.Proofs).GroupBy(proof => proof.Id)) + foreach ( + var proofSet in token + .Tokens.SelectMany(token1 => token1.Proofs) + .GroupBy(proof => proof.Id) + ) { var proofSetItem = CBORObject.NewOrderedMap(); proofSetItem.Add("i", Convert.FromHexString(proofSet.Key.ToString())); var proofSetItemArray = CBORObject.NewArray(); foreach (var proof in proofSet) { - var proofItem = CBORObject.NewOrderedMap() + var proofItem = CBORObject + .NewOrderedMap() .Add("a", proof.Amount) .Add("s", Encoding.UTF8.GetString(proof.Secret.GetBytes())) .Add("c", proof.C.Key.ToBytes()); if (proof.DLEQ is not null) { - proofItem.Add("d", CBORObject - .NewOrderedMap() - .Add("e", proof.DLEQ.E.Key.ToBytes()) - .Add("s", proof.DLEQ.S.Key.ToBytes()) - .Add("r", proof.DLEQ.R.Key.ToBytes())); + proofItem.Add( + "d", + CBORObject + .NewOrderedMap() + .Add("e", proof.DLEQ.E.Key.ToBytes()) + .Add("s", proof.DLEQ.S.Key.ToBytes()) + .Add("r", proof.DLEQ.R.Key.ToBytes()) + ); } if (proof.Witness is not null) @@ -64,13 +72,10 @@ public CBORObject ToCBORObject(CashuToken token) } var cbor = CBORObject.NewOrderedMap(); - if (token.Memo is not null) cbor.Add("d", token.Memo); - cbor.Add("t", proofSets) - .Add("m", mints.First()) - .Add("u", token.Unit!); + cbor.Add("t", proofSets).Add("m", mints.First()).Add("u", token.Unit!); return cbor; } @@ -85,34 +90,42 @@ public CashuToken FromCBORObject(CBORObject obj) new CashuToken.Token() { Mint = obj["m"].AsString(), - Proofs = obj["t"].Values.SelectMany(proofSet => - { - var id = new KeysetId(Convert.ToHexString(proofSet["i"].GetByteString()).ToLowerInvariant()); - - return proofSet["p"].Values.Select(proof => new Proof() + Proofs = obj["t"] + .Values.SelectMany(proofSet => { - Amount = proof["a"].ToObject(), - Secret = JsonSerializer.Deserialize(proof["s"].ToJSONString())!, - C = ECPubKey.Create(proof["c"].GetByteString()), - Witness = proof.GetOrDefault("w", null)?.AsString(), - DLEQ = proof.GetOrDefault("d", null) is { } cborDLEQ - ? new DLEQProof + var id = new KeysetId( + Convert + .ToHexString(proofSet["i"].GetByteString()) + .ToLowerInvariant() + ); + + return proofSet["p"] + .Values.Select(proof => new Proof() { - E = ECPrivKey.Create(cborDLEQ["e"].GetByteString()), - S = ECPrivKey.Create(cborDLEQ["s"].GetByteString()), - R = ECPrivKey.Create(cborDLEQ["r"].GetByteString()) - } - : null, - Id = id, - - P2PkE = proof.GetOrDefault("pe", null) is { } p2pkE - ? (PubKey?) ECPubKey.Create(p2pkE.GetByteString()) - : null - - }); - }).ToList() - } - ] + Amount = proof["a"].ToObject(), + Secret = JsonSerializer.Deserialize( + proof["s"].ToJSONString() + )!, + C = ECPubKey.Create(proof["c"].GetByteString()), + Witness = proof.GetOrDefault("w", null)?.AsString(), + DLEQ = proof.GetOrDefault("d", null) is { } cborDLEQ + ? new DLEQProof + { + E = ECPrivKey.Create(cborDLEQ["e"].GetByteString()), + S = ECPrivKey.Create(cborDLEQ["s"].GetByteString()), + R = ECPrivKey.Create(cborDLEQ["r"].GetByteString()), + } + : null, + Id = id, + + P2PkE = proof.GetOrDefault("pe", null) is { } p2pkE + ? (PubKey?)ECPubKey.Create(p2pkE.GetByteString()) + : null, + }); + }) + .ToList(), + }, + ], }; } -} \ No newline at end of file +} diff --git a/DotNut/Encoding/ConvertUtils.cs b/DotNut/Encoding/ConvertUtils.cs index de38e7e..cacc3a7 100644 --- a/DotNut/Encoding/ConvertUtils.cs +++ b/DotNut/Encoding/ConvertUtils.cs @@ -13,4 +13,4 @@ public static ECPrivKey ToPrivKey(this string hex) { return ECPrivKey.Create(global::System.Convert.FromHexString(hex)); } -} \ No newline at end of file +} diff --git a/DotNut/Encoding/ICashuTokenEncoder.cs b/DotNut/Encoding/ICashuTokenEncoder.cs index 6898820..93c7d61 100644 --- a/DotNut/Encoding/ICashuTokenEncoder.cs +++ b/DotNut/Encoding/ICashuTokenEncoder.cs @@ -4,5 +4,4 @@ public interface ICashuTokenEncoder { string Encode(CashuToken token); CashuToken Decode(string token); - -} \ No newline at end of file +} diff --git a/DotNut/Encoding/PaymentRequestEncoder.cs b/DotNut/Encoding/PaymentRequestEncoder.cs index 779cbd4..06aeb8e 100644 --- a/DotNut/Encoding/PaymentRequestEncoder.cs +++ b/DotNut/Encoding/PaymentRequestEncoder.cs @@ -24,7 +24,8 @@ public CBORObject ToCBORObject(PaymentRequest paymentRequest) var transports = CBORObject.NewArray(); foreach (var transport in paymentRequest.Transports) { - var transportItem = CBORObject.NewMap() + var transportItem = CBORObject + .NewMap() .Add("t", transport.Type) .Add("a", transport.Target); if (transport.Tags is not null) @@ -71,7 +72,7 @@ public CBORObject ToCBORObject(PaymentRequest paymentRequest) cbor.Add("nut10", nut10Obj); } - if (paymentRequest.Nut26 is {} nut26) + if (paymentRequest.Nut26 is { } nut26) { cbor.Add("nut26", nut26); } @@ -105,33 +106,37 @@ public PaymentRequest FromCBORObject(CBORObject obj) paymentRequest.Memo = value.AsString(); break; case "t": - paymentRequest.Transports = value.Values.Select(v => - { - var transport = new PaymentRequestTransport(); - foreach (var transportKey in v.Keys) + paymentRequest.Transports = value + .Values.Select(v => { - var transportValue = v[transportKey]; - switch (transportKey.AsString()) + var transport = new PaymentRequestTransport(); + foreach (var transportKey in v.Keys) { - case "t": - transport.Type = transportValue.AsString(); - break; - case "a": - transport.Target = transportValue.AsString(); - break; - case "g": - transport.Tags = transportValue.Values - .Where(tag => tag.Type == CBORType.Array) - .Select(tag => - new Tag(tag.Values.Select(cborObject => cborObject.AsString()).ToArray()) - ) - .ToArray(); - break; + var transportValue = v[transportKey]; + switch (transportKey.AsString()) + { + case "t": + transport.Type = transportValue.AsString(); + break; + case "a": + transport.Target = transportValue.AsString(); + break; + case "g": + transport.Tags = transportValue.Values + .Where(tag => tag.Type == CBORType.Array) + .Select(tag => + new Tag( + tag.Values.Select(cborObject => cborObject.AsString()).ToArray() + ) + ) + .ToArray(); + break; + } } - } - return transport; - }).ToArray(); + return transport; + }) + .ToArray(); break; case "nut10": var lockingCondition = new Nut10LockingCondition(); @@ -147,7 +152,7 @@ public PaymentRequest FromCBORObject(CBORObject obj) lockingCondition.Data = nut10Value.AsString(); break; case "t": - lockingCondition.Tags = nut10Value.Values + lockingCondition.Tags = nut10Value.Values .Where(tag => tag.Type == CBORType.Array) .Select(tag => new Tag(tag.Values.Select(cborObject => cborObject.AsString()).ToArray()) diff --git a/DotNut/JsonConverters/KeysetIdJsonConverter.cs b/DotNut/JsonConverters/KeysetIdJsonConverter.cs index 465e377..56dd0e8 100644 --- a/DotNut/JsonConverters/KeysetIdJsonConverter.cs +++ b/DotNut/JsonConverters/KeysetIdJsonConverter.cs @@ -5,16 +5,22 @@ namespace DotNut.JsonConverters; public class KeysetIdJsonConverter : JsonConverter { - public override KeysetId? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + public override KeysetId? Read( + ref Utf8JsonReader reader, + Type typeToConvert, + JsonSerializerOptions options + ) { if (reader.TokenType == JsonTokenType.Null) { return null; } - if (reader.TokenType != JsonTokenType.String || - reader.GetString() is not { } str || - string.IsNullOrEmpty(str)) + if ( + reader.TokenType != JsonTokenType.String + || reader.GetString() is not { } str + || string.IsNullOrEmpty(str) + ) { throw new JsonException("Expected string"); } @@ -22,7 +28,11 @@ public class KeysetIdJsonConverter : JsonConverter return new KeysetId(str); } - public override void Write(Utf8JsonWriter writer, KeysetId? value, JsonSerializerOptions options) + public override void Write( + Utf8JsonWriter writer, + KeysetId? value, + JsonSerializerOptions options + ) { if (value is null) { @@ -32,4 +42,4 @@ public override void Write(Utf8JsonWriter writer, KeysetId? value, JsonSerialize writer.WriteStringValue(value.ToString()); } -} \ No newline at end of file +} diff --git a/DotNut/JsonConverters/KeysetJsonConverter.cs b/DotNut/JsonConverters/KeysetJsonConverter.cs index d15c997..5b7b4eb 100644 --- a/DotNut/JsonConverters/KeysetJsonConverter.cs +++ b/DotNut/JsonConverters/KeysetJsonConverter.cs @@ -5,7 +5,11 @@ namespace DotNut.JsonConverters; public class KeysetJsonConverter : JsonConverter { - public override Keyset? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + public override Keyset? Read( + ref Utf8JsonReader reader, + Type typeToConvert, + JsonSerializerOptions options + ) { if (reader.TokenType == JsonTokenType.Null) { @@ -21,7 +25,6 @@ public class KeysetJsonConverter : JsonConverter { if (reader.TokenType == JsonTokenType.EndObject) { - return keyset; } @@ -30,7 +33,7 @@ public class KeysetJsonConverter : JsonConverter { amount = reader.GetUInt64(); } - else if (reader.TokenType is JsonTokenType.String or JsonTokenType.PropertyName) + else if (reader.TokenType is JsonTokenType.String or JsonTokenType.PropertyName) { var str = reader.GetString(); if (string.IsNullOrEmpty(str)) @@ -42,10 +45,9 @@ public class KeysetJsonConverter : JsonConverter throw new JsonException("Expected number or string"); } - reader.Read(); var pubkey = JsonSerializer.Deserialize(ref reader, options); - if(pubkey is null || pubkey.Key.ToBytes().Length != 33) + if (pubkey is null || pubkey.Key.ToBytes().Length != 33) throw new JsonException("Invalid public key (not compressed?)"); keyset.Add(amount, pubkey); } @@ -70,4 +72,4 @@ public override void Write(Utf8JsonWriter writer, Keyset? value, JsonSerializerO writer.WriteEndObject(); } -} \ No newline at end of file +} diff --git a/DotNut/JsonConverters/Nut10SecretJsonConverter.cs b/DotNut/JsonConverters/Nut10SecretJsonConverter.cs index e38a874..7dbc211 100644 --- a/DotNut/JsonConverters/Nut10SecretJsonConverter.cs +++ b/DotNut/JsonConverters/Nut10SecretJsonConverter.cs @@ -5,20 +5,24 @@ namespace DotNut.JsonConverters; public class Nut10SecretJsonConverter : JsonConverter { - public override Nut10Secret? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + public override Nut10Secret? Read( + ref Utf8JsonReader reader, + Type typeToConvert, + JsonSerializerOptions options + ) { - if(reader.TokenType == JsonTokenType.Null) + if (reader.TokenType == JsonTokenType.Null) return null; if (reader.TokenType != JsonTokenType.StartArray) { throw new JsonException("Expected array"); } reader.Read(); - if(reader.TokenType != JsonTokenType.String) + if (reader.TokenType != JsonTokenType.String) throw new JsonException("Expected string"); var key = reader.GetString(); reader.Read(); - + Nut10ProofSecret? proofSecret; switch (key) { @@ -32,7 +36,7 @@ public class Nut10SecretJsonConverter : JsonConverter default: throw new JsonException("Unknown secret type"); } - if(proofSecret is null) + if (proofSecret is null) throw new JsonException("Invalid proof secret"); reader.Read(); if (reader.TokenType != JsonTokenType.EndArray) @@ -40,22 +44,24 @@ public class Nut10SecretJsonConverter : JsonConverter throw new JsonException("Expected end array"); } - return new Nut10Secret(key, proofSecret); - - + return new Nut10Secret(key, proofSecret); } - public override void Write(Utf8JsonWriter writer, Nut10Secret? value, JsonSerializerOptions options) + public override void Write( + Utf8JsonWriter writer, + Nut10Secret? value, + JsonSerializerOptions options + ) { if (value is null) { writer.WriteNullValue(); return; } - + writer.WriteStartArray(); JsonSerializer.Serialize(writer, value.Key, options); JsonSerializer.Serialize(writer, value.ProofSecret, options); writer.WriteEndArray(); } -} \ No newline at end of file +} diff --git a/DotNut/JsonConverters/PrivKeyJsonConverter.cs b/DotNut/JsonConverters/PrivKeyJsonConverter.cs index 6e57bf1..062a524 100644 --- a/DotNut/JsonConverters/PrivKeyJsonConverter.cs +++ b/DotNut/JsonConverters/PrivKeyJsonConverter.cs @@ -5,16 +5,22 @@ namespace DotNut.JsonConverters; public class PrivKeyJsonConverter : JsonConverter { - public override PrivKey? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + public override PrivKey? Read( + ref Utf8JsonReader reader, + Type typeToConvert, + JsonSerializerOptions options + ) { if (reader.TokenType == JsonTokenType.Null) { return null; } - if (reader.TokenType != JsonTokenType.String || - reader.GetString() is not { } str || - string.IsNullOrEmpty(str)) + if ( + reader.TokenType != JsonTokenType.String + || reader.GetString() is not { } str + || string.IsNullOrEmpty(str) + ) { throw new JsonException("Expected string"); } @@ -32,4 +38,4 @@ public override void Write(Utf8JsonWriter writer, PrivKey? value, JsonSerializer writer.WriteStringValue(value.ToString()); } -} \ No newline at end of file +} diff --git a/DotNut/JsonConverters/PubKeyJsonConverter.cs b/DotNut/JsonConverters/PubKeyJsonConverter.cs index c3413ae..30e270d 100644 --- a/DotNut/JsonConverters/PubKeyJsonConverter.cs +++ b/DotNut/JsonConverters/PubKeyJsonConverter.cs @@ -5,16 +5,22 @@ namespace DotNut.JsonConverters; public class PubKeyJsonConverter : JsonConverter { - public override PubKey? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + public override PubKey? Read( + ref Utf8JsonReader reader, + Type typeToConvert, + JsonSerializerOptions options + ) { if (reader.TokenType == JsonTokenType.Null) { return null; } - if (reader.TokenType != JsonTokenType.String || - reader.GetString() is not { } str || - string.IsNullOrEmpty(str)) + if ( + reader.TokenType != JsonTokenType.String + || reader.GetString() is not { } str + || string.IsNullOrEmpty(str) + ) { throw new JsonException("Expected string"); } @@ -32,4 +38,4 @@ public override void Write(Utf8JsonWriter writer, PubKey? value, JsonSerializerO writer.WriteStringValue(value.ToString()); } -} \ No newline at end of file +} diff --git a/DotNut/JsonConverters/SecretJsonConverter.cs b/DotNut/JsonConverters/SecretJsonConverter.cs index 7576b38..6a99ec1 100644 --- a/DotNut/JsonConverters/SecretJsonConverter.cs +++ b/DotNut/JsonConverters/SecretJsonConverter.cs @@ -5,7 +5,11 @@ namespace DotNut.JsonConverters; public class SecretJsonConverter : JsonConverter { - public override ISecret? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + public override ISecret? Read( + ref Utf8JsonReader reader, + Type typeToConvert, + JsonSerializerOptions options + ) { if (reader.TokenType == JsonTokenType.Null) { @@ -14,7 +18,7 @@ public class SecretJsonConverter : JsonConverter if (reader.TokenType == JsonTokenType.StartArray && reader.CurrentDepth == 0) { - //we are converting a nut10 secret directly + //we are converting a nut10 secret directly return JsonSerializer.Deserialize(ref reader, options); } if (reader.TokenType != JsonTokenType.String) @@ -33,7 +37,6 @@ public class SecretJsonConverter : JsonConverter } catch (Exception e) { - return new StringSecret(str); } } @@ -53,4 +56,4 @@ public override void Write(Utf8JsonWriter writer, ISecret? value, JsonSerializer break; } } -} \ No newline at end of file +} diff --git a/DotNut/JsonConverters/UnixDateTimeOffsetConverter.cs b/DotNut/JsonConverters/UnixDateTimeOffsetConverter.cs index 0ecfbec..30460b8 100644 --- a/DotNut/JsonConverters/UnixDateTimeOffsetConverter.cs +++ b/DotNut/JsonConverters/UnixDateTimeOffsetConverter.cs @@ -5,16 +5,26 @@ namespace DotNut.JsonConverters; public class UnixDateTimeOffsetConverter : JsonConverter { - public override DateTimeOffset Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + public override DateTimeOffset Read( + ref Utf8JsonReader reader, + Type typeToConvert, + JsonSerializerOptions options + ) { - var val = reader.TokenType == JsonTokenType.Number? reader.GetInt64() : long.Parse(reader.GetString()!); - + var val = + reader.TokenType == JsonTokenType.Number + ? reader.GetInt64() + : long.Parse(reader.GetString()!); return DateTimeOffset.FromUnixTimeSeconds(val); } - public override void Write(Utf8JsonWriter writer, DateTimeOffset value, JsonSerializerOptions options) + public override void Write( + Utf8JsonWriter writer, + DateTimeOffset value, + JsonSerializerOptions options + ) { writer.WriteNumberValue(value.ToUnixTimeSeconds()); } -} \ No newline at end of file +} diff --git a/DotNut/NBitcoin/BIP39/HardcodedWordlistSource.cs b/DotNut/NBitcoin/BIP39/HardcodedWordlistSource.cs index 87b06dc..7110338 100644 --- a/DotNut/NBitcoin/BIP39/HardcodedWordlistSource.cs +++ b/DotNut/NBitcoin/BIP39/HardcodedWordlistSource.cs @@ -7,22 +7,38 @@ public class HardcodedWordlistSource : IWordlistSource static HardcodedWordlistSource() { var dico = new Dictionary(); - dico.Add("chinese_simplified", - "的\n一\n是\n在\n不\n了\n有\n和\n人\n这\n中\n大\n为\n上\n个\n国\n我\n以\n要\n他\n时\n来\n用\n们\n生\n到\n作\n地\n于\n出\n就\n分\n对\n成\n会\n可\n主\n发\n年\n动\n同\n工\n也\n能\n下\n过\n子\n说\n产\n种\n面\n而\n方\n后\n多\n定\n行\n学\n法\n所\n民\n得\n经\n十\n三\n之\n进\n着\n等\n部\n度\n家\n电\n力\n里\n如\n水\n化\n高\n自\n二\n理\n起\n小\n物\n现\n实\n加\n量\n都\n两\n体\n制\n机\n当\n使\n点\n从\n业\n本\n去\n把\n性\n好\n应\n开\n它\n合\n还\n因\n由\n其\n些\n然\n前\n外\n天\n政\n四\n日\n那\n社\n义\n事\n平\n形\n相\n全\n表\n间\n样\n与\n关\n各\n重\n新\n线\n内\n数\n正\n心\n反\n你\n明\n看\n原\n又\n么\n利\n比\n或\n但\n质\n气\n第\n向\n道\n命\n此\n变\n条\n只\n没\n结\n解\n问\n意\n建\n月\n公\n无\n系\n军\n很\n情\n者\n最\n立\n代\n想\n已\n通\n并\n提\n直\n题\n党\n程\n展\n五\n果\n料\n象\n员\n革\n位\n入\n常\n文\n总\n次\n品\n式\n活\n设\n及\n管\n特\n件\n长\n求\n老\n头\n基\n资\n边\n流\n路\n级\n少\n图\n山\n统\n接\n知\n较\n将\n组\n见\n计\n别\n她\n手\n角\n期\n根\n论\n运\n农\n指\n几\n九\n区\n强\n放\n决\n西\n被\n干\n做\n必\n战\n先\n回\n则\n任\n取\n据\n处\n队\n南\n给\n色\n光\n门\n即\n保\n治\n北\n造\n百\n规\n热\n领\n七\n海\n口\n东\n导\n器\n压\n志\n世\n金\n增\n争\n济\n阶\n油\n思\n术\n极\n交\n受\n联\n什\n认\n六\n共\n权\n收\n证\n改\n清\n美\n再\n采\n转\n更\n单\n风\n切\n打\n白\n教\n速\n花\n带\n安\n场\n身\n车\n例\n真\n务\n具\n万\n每\n目\n至\n达\n走\n积\n示\n议\n声\n报\n斗\n完\n类\n八\n离\n华\n名\n确\n才\n科\n张\n信\n马\n节\n话\n米\n整\n空\n元\n况\n今\n集\n温\n传\n土\n许\n步\n群\n广\n石\n记\n需\n段\n研\n界\n拉\n林\n律\n叫\n且\n究\n观\n越\n织\n装\n影\n算\n低\n持\n音\n众\n书\n布\n复\n容\n儿\n须\n际\n商\n非\n验\n连\n断\n深\n难\n近\n矿\n千\n周\n委\n素\n技\n备\n半\n办\n青\n省\n列\n习\n响\n约\n支\n般\n史\n感\n劳\n便\n团\n往\n酸\n历\n市\n克\n何\n除\n消\n构\n府\n称\n太\n准\n精\n值\n号\n率\n族\n维\n划\n选\n标\n写\n存\n候\n毛\n亲\n快\n效\n斯\n院\n查\n江\n型\n眼\n王\n按\n格\n养\n易\n置\n派\n层\n片\n始\n却\n专\n状\n育\n厂\n京\n识\n适\n属\n圆\n包\n火\n住\n调\n满\n县\n局\n照\n参\n红\n细\n引\n听\n该\n铁\n价\n严\n首\n底\n液\n官\n德\n随\n病\n苏\n失\n尔\n死\n讲\n配\n女\n黄\n推\n显\n谈\n罪\n神\n艺\n呢\n席\n含\n企\n望\n密\n批\n营\n项\n防\n举\n球\n英\n氧\n势\n告\n李\n台\n落\n木\n帮\n轮\n破\n亚\n师\n围\n注\n远\n字\n材\n排\n供\n河\n态\n封\n另\n施\n减\n树\n溶\n怎\n止\n案\n言\n士\n均\n武\n固\n叶\n鱼\n波\n视\n仅\n费\n紧\n爱\n左\n章\n早\n朝\n害\n续\n轻\n服\n试\n食\n充\n兵\n源\n判\n护\n司\n足\n某\n练\n差\n致\n板\n田\n降\n黑\n犯\n负\n击\n范\n继\n兴\n似\n余\n坚\n曲\n输\n修\n故\n城\n夫\n够\n送\n笔\n船\n占\n右\n财\n吃\n富\n春\n职\n觉\n汉\n画\n功\n巴\n跟\n虽\n杂\n飞\n检\n吸\n助\n升\n阳\n互\n初\n创\n抗\n考\n投\n坏\n策\n古\n径\n换\n未\n跑\n留\n钢\n曾\n端\n责\n站\n简\n述\n钱\n副\n尽\n帝\n射\n草\n冲\n承\n独\n令\n限\n阿\n宣\n环\n双\n请\n超\n微\n让\n控\n州\n良\n轴\n找\n否\n纪\n益\n依\n优\n顶\n础\n载\n倒\n房\n突\n坐\n粉\n敌\n略\n客\n袁\n冷\n胜\n绝\n析\n块\n剂\n测\n丝\n协\n诉\n念\n陈\n仍\n罗\n盐\n友\n洋\n错\n苦\n夜\n刑\n移\n频\n逐\n靠\n混\n母\n短\n皮\n终\n聚\n汽\n村\n云\n哪\n既\n距\n卫\n停\n烈\n央\n察\n烧\n迅\n境\n若\n印\n洲\n刻\n括\n激\n孔\n搞\n甚\n室\n待\n核\n校\n散\n侵\n吧\n甲\n游\n久\n菜\n味\n旧\n模\n湖\n货\n损\n预\n阻\n毫\n普\n稳\n乙\n妈\n植\n息\n扩\n银\n语\n挥\n酒\n守\n拿\n序\n纸\n医\n缺\n雨\n吗\n针\n刘\n啊\n急\n唱\n误\n训\n愿\n审\n附\n获\n茶\n鲜\n粮\n斤\n孩\n脱\n硫\n肥\n善\n龙\n演\n父\n渐\n血\n欢\n械\n掌\n歌\n沙\n刚\n攻\n谓\n盾\n讨\n晚\n粒\n乱\n燃\n矛\n乎\n杀\n药\n宁\n鲁\n贵\n钟\n煤\n读\n班\n伯\n香\n介\n迫\n句\n丰\n培\n握\n兰\n担\n弦\n蛋\n沉\n假\n穿\n执\n答\n乐\n谁\n顺\n烟\n缩\n征\n脸\n喜\n松\n脚\n困\n异\n免\n背\n星\n福\n买\n染\n井\n概\n慢\n怕\n磁\n倍\n祖\n皇\n促\n静\n补\n评\n翻\n肉\n践\n尼\n衣\n宽\n扬\n棉\n希\n伤\n操\n垂\n秋\n宜\n氢\n套\n督\n振\n架\n亮\n末\n宪\n庆\n编\n牛\n触\n映\n雷\n销\n诗\n座\n居\n抓\n裂\n胞\n呼\n娘\n景\n威\n绿\n晶\n厚\n盟\n衡\n鸡\n孙\n延\n危\n胶\n屋\n乡\n临\n陆\n顾\n掉\n呀\n灯\n岁\n措\n束\n耐\n剧\n玉\n赵\n跳\n哥\n季\n课\n凯\n胡\n额\n款\n绍\n卷\n齐\n伟\n蒸\n殖\n永\n宗\n苗\n川\n炉\n岩\n弱\n零\n杨\n奏\n沿\n露\n杆\n探\n滑\n镇\n饭\n浓\n航\n怀\n赶\n库\n夺\n伊\n灵\n税\n途\n灭\n赛\n归\n召\n鼓\n播\n盘\n裁\n险\n康\n唯\n录\n菌\n纯\n借\n糖\n盖\n横\n符\n私\n努\n堂\n域\n枪\n润\n幅\n哈\n竟\n熟\n虫\n泽\n脑\n壤\n碳\n欧\n遍\n侧\n寨\n敢\n彻\n虑\n斜\n薄\n庭\n纳\n弹\n饲\n伸\n折\n麦\n湿\n暗\n荷\n瓦\n塞\n床\n筑\n恶\n户\n访\n塔\n奇\n透\n梁\n刀\n旋\n迹\n卡\n氯\n遇\n份\n毒\n泥\n退\n洗\n摆\n灰\n彩\n卖\n耗\n夏\n择\n忙\n铜\n献\n硬\n予\n繁\n圈\n雪\n函\n亦\n抽\n篇\n阵\n阴\n丁\n尺\n追\n堆\n雄\n迎\n泛\n爸\n楼\n避\n谋\n吨\n野\n猪\n旗\n累\n偏\n典\n馆\n索\n秦\n脂\n潮\n爷\n豆\n忽\n托\n惊\n塑\n遗\n愈\n朱\n替\n纤\n粗\n倾\n尚\n痛\n楚\n谢\n奋\n购\n磨\n君\n池\n旁\n碎\n骨\n监\n捕\n弟\n暴\n割\n贯\n殊\n释\n词\n亡\n壁\n顿\n宝\n午\n尘\n闻\n揭\n炮\n残\n冬\n桥\n妇\n警\n综\n招\n吴\n付\n浮\n遭\n徐\n您\n摇\n谷\n赞\n箱\n隔\n订\n男\n吹\n园\n纷\n唐\n败\n宋\n玻\n巨\n耕\n坦\n荣\n闭\n湾\n键\n凡\n驻\n锅\n救\n恩\n剥\n凝\n碱\n齿\n截\n炼\n麻\n纺\n禁\n废\n盛\n版\n缓\n净\n睛\n昌\n婚\n涉\n筒\n嘴\n插\n岸\n朗\n庄\n街\n藏\n姑\n贸\n腐\n奴\n啦\n惯\n乘\n伙\n恢\n匀\n纱\n扎\n辩\n耳\n彪\n臣\n亿\n璃\n抵\n脉\n秀\n萨\n俄\n网\n舞\n店\n喷\n纵\n寸\n汗\n挂\n洪\n贺\n闪\n柬\n爆\n烯\n津\n稻\n墙\n软\n勇\n像\n滚\n厘\n蒙\n芳\n肯\n坡\n柱\n荡\n腿\n仪\n旅\n尾\n轧\n冰\n贡\n登\n黎\n削\n钻\n勒\n逃\n障\n氨\n郭\n峰\n币\n港\n伏\n轨\n亩\n毕\n擦\n莫\n刺\n浪\n秘\n援\n株\n健\n售\n股\n岛\n甘\n泡\n睡\n童\n铸\n汤\n阀\n休\n汇\n舍\n牧\n绕\n炸\n哲\n磷\n绩\n朋\n淡\n尖\n启\n陷\n柴\n呈\n徒\n颜\n泪\n稍\n忘\n泵\n蓝\n拖\n洞\n授\n镜\n辛\n壮\n锋\n贫\n虚\n弯\n摩\n泰\n幼\n廷\n尊\n窗\n纲\n弄\n隶\n疑\n氏\n宫\n姐\n震\n瑞\n怪\n尤\n琴\n循\n描\n膜\n违\n夹\n腰\n缘\n珠\n穷\n森\n枝\n竹\n沟\n催\n绳\n忆\n邦\n剩\n幸\n浆\n栏\n拥\n牙\n贮\n礼\n滤\n钠\n纹\n罢\n拍\n咱\n喊\n袖\n埃\n勤\n罚\n焦\n潜\n伍\n墨\n欲\n缝\n姓\n刊\n饱\n仿\n奖\n铝\n鬼\n丽\n跨\n默\n挖\n链\n扫\n喝\n袋\n炭\n污\n幕\n诸\n弧\n励\n梅\n奶\n洁\n灾\n舟\n鉴\n苯\n讼\n抱\n毁\n懂\n寒\n智\n埔\n寄\n届\n跃\n渡\n挑\n丹\n艰\n贝\n碰\n拔\n爹\n戴\n码\n梦\n芽\n熔\n赤\n渔\n哭\n敬\n颗\n奔\n铅\n仲\n虎\n稀\n妹\n乏\n珍\n申\n桌\n遵\n允\n隆\n螺\n仓\n魏\n锐\n晓\n氮\n兼\n隐\n碍\n赫\n拨\n忠\n肃\n缸\n牵\n抢\n博\n巧\n壳\n兄\n杜\n讯\n诚\n碧\n祥\n柯\n页\n巡\n矩\n悲\n灌\n龄\n伦\n票\n寻\n桂\n铺\n圣\n恐\n恰\n郑\n趣\n抬\n荒\n腾\n贴\n柔\n滴\n猛\n阔\n辆\n妻\n填\n撤\n储\n签\n闹\n扰\n紫\n砂\n递\n戏\n吊\n陶\n伐\n喂\n疗\n瓶\n婆\n抚\n臂\n摸\n忍\n虾\n蜡\n邻\n胸\n巩\n挤\n偶\n弃\n槽\n劲\n乳\n邓\n吉\n仁\n烂\n砖\n租\n乌\n舰\n伴\n瓜\n浅\n丙\n暂\n燥\n橡\n柳\n迷\n暖\n牌\n秧\n胆\n详\n簧\n踏\n瓷\n谱\n呆\n宾\n糊\n洛\n辉\n愤\n竞\n隙\n怒\n粘\n乃\n绪\n肩\n籍\n敏\n涂\n熙\n皆\n侦\n悬\n掘\n享\n纠\n醒\n狂\n锁\n淀\n恨\n牲\n霸\n爬\n赏\n逆\n玩\n陵\n祝\n秒\n浙\n貌\n役\n彼\n悉\n鸭\n趋\n凤\n晨\n畜\n辈\n秩\n卵\n署\n梯\n炎\n滩\n棋\n驱\n筛\n峡\n冒\n啥\n寿\n译\n浸\n泉\n帽\n迟\n硅\n疆\n贷\n漏\n稿\n冠\n嫩\n胁\n芯\n牢\n叛\n蚀\n奥\n鸣\n岭\n羊\n凭\n串\n塘\n绘\n酵\n融\n盆\n锡\n庙\n筹\n冻\n辅\n摄\n袭\n筋\n拒\n僚\n旱\n钾\n鸟\n漆\n沈\n眉\n疏\n添\n棒\n穗\n硝\n韩\n逼\n扭\n侨\n凉\n挺\n碗\n栽\n炒\n杯\n患\n馏\n劝\n豪\n辽\n勃\n鸿\n旦\n吏\n拜\n狗\n埋\n辊\n掩\n饮\n搬\n骂\n辞\n勾\n扣\n估\n蒋\n绒\n雾\n丈\n朵\n姆\n拟\n宇\n辑\n陕\n雕\n偿\n蓄\n崇\n剪\n倡\n厅\n咬\n驶\n薯\n刷\n斥\n番\n赋\n奉\n佛\n浇\n漫\n曼\n扇\n钙\n桃\n扶\n仔\n返\n俗\n亏\n腔\n鞋\n棱\n覆\n框\n悄\n叔\n撞\n骗\n勘\n旺\n沸\n孤\n吐\n孟\n渠\n屈\n疾\n妙\n惜\n仰\n狠\n胀\n谐\n抛\n霉\n桑\n岗\n嘛\n衰\n盗\n渗\n脏\n赖\n涌\n甜\n曹\n阅\n肌\n哩\n厉\n烃\n纬\n毅\n昨\n伪\n症\n煮\n叹\n钉\n搭\n茎\n笼\n酷\n偷\n弓\n锥\n恒\n杰\n坑\n鼻\n翼\n纶\n叙\n狱\n逮\n罐\n络\n棚\n抑\n膨\n蔬\n寺\n骤\n穆\n冶\n枯\n册\n尸\n凸\n绅\n坯\n牺\n焰\n轰\n欣\n晋\n瘦\n御\n锭\n锦\n丧\n旬\n锻\n垄\n搜\n扑\n邀\n亭\n酯\n迈\n舒\n脆\n酶\n闲\n忧\n酚\n顽\n羽\n涨\n卸\n仗\n陪\n辟\n惩\n杭\n姚\n肚\n捉\n飘\n漂\n昆\n欺\n吾\n郎\n烷\n汁\n呵\n饰\n萧\n雅\n邮\n迁\n燕\n撒\n姻\n赴\n宴\n烦\n债\n帐\n斑\n铃\n旨\n醇\n董\n饼\n雏\n姿\n拌\n傅\n腹\n妥\n揉\n贤\n拆\n歪\n葡\n胺\n丢\n浩\n徽\n昂\n垫\n挡\n览\n贪\n慰\n缴\n汪\n慌\n冯\n诺\n姜\n谊\n凶\n劣\n诬\n耀\n昏\n躺\n盈\n骑\n乔\n溪\n丛\n卢\n抹\n闷\n咨\n刮\n驾\n缆\n悟\n摘\n铒\n掷\n颇\n幻\n柄\n惠\n惨\n佳\n仇\n腊\n窝\n涤\n剑\n瞧\n堡\n泼\n葱\n罩\n霍\n捞\n胎\n苍\n滨\n俩\n捅\n湘\n砍\n霞\n邵\n萄\n疯\n淮\n遂\n熊\n粪\n烘\n宿\n档\n戈\n驳\n嫂\n裕\n徙\n箭\n捐\n肠\n撑\n晒\n辨\n殿\n莲\n摊\n搅\n酱\n屏\n疫\n哀\n蔡\n堵\n沫\n皱\n畅\n叠\n阁\n莱\n敲\n辖\n钩\n痕\n坝\n巷\n饿\n祸\n丘\n玄\n溜\n曰\n逻\n彭\n尝\n卿\n妨\n艇\n吞\n韦\n怨\n矮\n歇\n"); - dico.Add("chinese_traditional", - "的\n一\n是\n在\n不\n了\n有\n和\n人\n這\n中\n大\n為\n上\n個\n國\n我\n以\n要\n他\n時\n來\n用\n們\n生\n到\n作\n地\n於\n出\n就\n分\n對\n成\n會\n可\n主\n發\n年\n動\n同\n工\n也\n能\n下\n過\n子\n說\n產\n種\n面\n而\n方\n後\n多\n定\n行\n學\n法\n所\n民\n得\n經\n十\n三\n之\n進\n著\n等\n部\n度\n家\n電\n力\n裡\n如\n水\n化\n高\n自\n二\n理\n起\n小\n物\n現\n實\n加\n量\n都\n兩\n體\n制\n機\n當\n使\n點\n從\n業\n本\n去\n把\n性\n好\n應\n開\n它\n合\n還\n因\n由\n其\n些\n然\n前\n外\n天\n政\n四\n日\n那\n社\n義\n事\n平\n形\n相\n全\n表\n間\n樣\n與\n關\n各\n重\n新\n線\n內\n數\n正\n心\n反\n你\n明\n看\n原\n又\n麼\n利\n比\n或\n但\n質\n氣\n第\n向\n道\n命\n此\n變\n條\n只\n沒\n結\n解\n問\n意\n建\n月\n公\n無\n系\n軍\n很\n情\n者\n最\n立\n代\n想\n已\n通\n並\n提\n直\n題\n黨\n程\n展\n五\n果\n料\n象\n員\n革\n位\n入\n常\n文\n總\n次\n品\n式\n活\n設\n及\n管\n特\n件\n長\n求\n老\n頭\n基\n資\n邊\n流\n路\n級\n少\n圖\n山\n統\n接\n知\n較\n將\n組\n見\n計\n別\n她\n手\n角\n期\n根\n論\n運\n農\n指\n幾\n九\n區\n強\n放\n決\n西\n被\n幹\n做\n必\n戰\n先\n回\n則\n任\n取\n據\n處\n隊\n南\n給\n色\n光\n門\n即\n保\n治\n北\n造\n百\n規\n熱\n領\n七\n海\n口\n東\n導\n器\n壓\n志\n世\n金\n增\n爭\n濟\n階\n油\n思\n術\n極\n交\n受\n聯\n什\n認\n六\n共\n權\n收\n證\n改\n清\n美\n再\n採\n轉\n更\n單\n風\n切\n打\n白\n教\n速\n花\n帶\n安\n場\n身\n車\n例\n真\n務\n具\n萬\n每\n目\n至\n達\n走\n積\n示\n議\n聲\n報\n鬥\n完\n類\n八\n離\n華\n名\n確\n才\n科\n張\n信\n馬\n節\n話\n米\n整\n空\n元\n況\n今\n集\n溫\n傳\n土\n許\n步\n群\n廣\n石\n記\n需\n段\n研\n界\n拉\n林\n律\n叫\n且\n究\n觀\n越\n織\n裝\n影\n算\n低\n持\n音\n眾\n書\n布\n复\n容\n兒\n須\n際\n商\n非\n驗\n連\n斷\n深\n難\n近\n礦\n千\n週\n委\n素\n技\n備\n半\n辦\n青\n省\n列\n習\n響\n約\n支\n般\n史\n感\n勞\n便\n團\n往\n酸\n歷\n市\n克\n何\n除\n消\n構\n府\n稱\n太\n準\n精\n值\n號\n率\n族\n維\n劃\n選\n標\n寫\n存\n候\n毛\n親\n快\n效\n斯\n院\n查\n江\n型\n眼\n王\n按\n格\n養\n易\n置\n派\n層\n片\n始\n卻\n專\n狀\n育\n廠\n京\n識\n適\n屬\n圓\n包\n火\n住\n調\n滿\n縣\n局\n照\n參\n紅\n細\n引\n聽\n該\n鐵\n價\n嚴\n首\n底\n液\n官\n德\n隨\n病\n蘇\n失\n爾\n死\n講\n配\n女\n黃\n推\n顯\n談\n罪\n神\n藝\n呢\n席\n含\n企\n望\n密\n批\n營\n項\n防\n舉\n球\n英\n氧\n勢\n告\n李\n台\n落\n木\n幫\n輪\n破\n亞\n師\n圍\n注\n遠\n字\n材\n排\n供\n河\n態\n封\n另\n施\n減\n樹\n溶\n怎\n止\n案\n言\n士\n均\n武\n固\n葉\n魚\n波\n視\n僅\n費\n緊\n愛\n左\n章\n早\n朝\n害\n續\n輕\n服\n試\n食\n充\n兵\n源\n判\n護\n司\n足\n某\n練\n差\n致\n板\n田\n降\n黑\n犯\n負\n擊\n范\n繼\n興\n似\n餘\n堅\n曲\n輸\n修\n故\n城\n夫\n夠\n送\n筆\n船\n佔\n右\n財\n吃\n富\n春\n職\n覺\n漢\n畫\n功\n巴\n跟\n雖\n雜\n飛\n檢\n吸\n助\n昇\n陽\n互\n初\n創\n抗\n考\n投\n壞\n策\n古\n徑\n換\n未\n跑\n留\n鋼\n曾\n端\n責\n站\n簡\n述\n錢\n副\n盡\n帝\n射\n草\n衝\n承\n獨\n令\n限\n阿\n宣\n環\n雙\n請\n超\n微\n讓\n控\n州\n良\n軸\n找\n否\n紀\n益\n依\n優\n頂\n礎\n載\n倒\n房\n突\n坐\n粉\n敵\n略\n客\n袁\n冷\n勝\n絕\n析\n塊\n劑\n測\n絲\n協\n訴\n念\n陳\n仍\n羅\n鹽\n友\n洋\n錯\n苦\n夜\n刑\n移\n頻\n逐\n靠\n混\n母\n短\n皮\n終\n聚\n汽\n村\n雲\n哪\n既\n距\n衛\n停\n烈\n央\n察\n燒\n迅\n境\n若\n印\n洲\n刻\n括\n激\n孔\n搞\n甚\n室\n待\n核\n校\n散\n侵\n吧\n甲\n遊\n久\n菜\n味\n舊\n模\n湖\n貨\n損\n預\n阻\n毫\n普\n穩\n乙\n媽\n植\n息\n擴\n銀\n語\n揮\n酒\n守\n拿\n序\n紙\n醫\n缺\n雨\n嗎\n針\n劉\n啊\n急\n唱\n誤\n訓\n願\n審\n附\n獲\n茶\n鮮\n糧\n斤\n孩\n脫\n硫\n肥\n善\n龍\n演\n父\n漸\n血\n歡\n械\n掌\n歌\n沙\n剛\n攻\n謂\n盾\n討\n晚\n粒\n亂\n燃\n矛\n乎\n殺\n藥\n寧\n魯\n貴\n鐘\n煤\n讀\n班\n伯\n香\n介\n迫\n句\n豐\n培\n握\n蘭\n擔\n弦\n蛋\n沉\n假\n穿\n執\n答\n樂\n誰\n順\n煙\n縮\n徵\n臉\n喜\n松\n腳\n困\n異\n免\n背\n星\n福\n買\n染\n井\n概\n慢\n怕\n磁\n倍\n祖\n皇\n促\n靜\n補\n評\n翻\n肉\n踐\n尼\n衣\n寬\n揚\n棉\n希\n傷\n操\n垂\n秋\n宜\n氫\n套\n督\n振\n架\n亮\n末\n憲\n慶\n編\n牛\n觸\n映\n雷\n銷\n詩\n座\n居\n抓\n裂\n胞\n呼\n娘\n景\n威\n綠\n晶\n厚\n盟\n衡\n雞\n孫\n延\n危\n膠\n屋\n鄉\n臨\n陸\n顧\n掉\n呀\n燈\n歲\n措\n束\n耐\n劇\n玉\n趙\n跳\n哥\n季\n課\n凱\n胡\n額\n款\n紹\n卷\n齊\n偉\n蒸\n殖\n永\n宗\n苗\n川\n爐\n岩\n弱\n零\n楊\n奏\n沿\n露\n桿\n探\n滑\n鎮\n飯\n濃\n航\n懷\n趕\n庫\n奪\n伊\n靈\n稅\n途\n滅\n賽\n歸\n召\n鼓\n播\n盤\n裁\n險\n康\n唯\n錄\n菌\n純\n借\n糖\n蓋\n橫\n符\n私\n努\n堂\n域\n槍\n潤\n幅\n哈\n竟\n熟\n蟲\n澤\n腦\n壤\n碳\n歐\n遍\n側\n寨\n敢\n徹\n慮\n斜\n薄\n庭\n納\n彈\n飼\n伸\n折\n麥\n濕\n暗\n荷\n瓦\n塞\n床\n築\n惡\n戶\n訪\n塔\n奇\n透\n梁\n刀\n旋\n跡\n卡\n氯\n遇\n份\n毒\n泥\n退\n洗\n擺\n灰\n彩\n賣\n耗\n夏\n擇\n忙\n銅\n獻\n硬\n予\n繁\n圈\n雪\n函\n亦\n抽\n篇\n陣\n陰\n丁\n尺\n追\n堆\n雄\n迎\n泛\n爸\n樓\n避\n謀\n噸\n野\n豬\n旗\n累\n偏\n典\n館\n索\n秦\n脂\n潮\n爺\n豆\n忽\n托\n驚\n塑\n遺\n愈\n朱\n替\n纖\n粗\n傾\n尚\n痛\n楚\n謝\n奮\n購\n磨\n君\n池\n旁\n碎\n骨\n監\n捕\n弟\n暴\n割\n貫\n殊\n釋\n詞\n亡\n壁\n頓\n寶\n午\n塵\n聞\n揭\n炮\n殘\n冬\n橋\n婦\n警\n綜\n招\n吳\n付\n浮\n遭\n徐\n您\n搖\n谷\n贊\n箱\n隔\n訂\n男\n吹\n園\n紛\n唐\n敗\n宋\n玻\n巨\n耕\n坦\n榮\n閉\n灣\n鍵\n凡\n駐\n鍋\n救\n恩\n剝\n凝\n鹼\n齒\n截\n煉\n麻\n紡\n禁\n廢\n盛\n版\n緩\n淨\n睛\n昌\n婚\n涉\n筒\n嘴\n插\n岸\n朗\n莊\n街\n藏\n姑\n貿\n腐\n奴\n啦\n慣\n乘\n夥\n恢\n勻\n紗\n扎\n辯\n耳\n彪\n臣\n億\n璃\n抵\n脈\n秀\n薩\n俄\n網\n舞\n店\n噴\n縱\n寸\n汗\n掛\n洪\n賀\n閃\n柬\n爆\n烯\n津\n稻\n牆\n軟\n勇\n像\n滾\n厘\n蒙\n芳\n肯\n坡\n柱\n盪\n腿\n儀\n旅\n尾\n軋\n冰\n貢\n登\n黎\n削\n鑽\n勒\n逃\n障\n氨\n郭\n峰\n幣\n港\n伏\n軌\n畝\n畢\n擦\n莫\n刺\n浪\n秘\n援\n株\n健\n售\n股\n島\n甘\n泡\n睡\n童\n鑄\n湯\n閥\n休\n匯\n舍\n牧\n繞\n炸\n哲\n磷\n績\n朋\n淡\n尖\n啟\n陷\n柴\n呈\n徒\n顏\n淚\n稍\n忘\n泵\n藍\n拖\n洞\n授\n鏡\n辛\n壯\n鋒\n貧\n虛\n彎\n摩\n泰\n幼\n廷\n尊\n窗\n綱\n弄\n隸\n疑\n氏\n宮\n姐\n震\n瑞\n怪\n尤\n琴\n循\n描\n膜\n違\n夾\n腰\n緣\n珠\n窮\n森\n枝\n竹\n溝\n催\n繩\n憶\n邦\n剩\n幸\n漿\n欄\n擁\n牙\n貯\n禮\n濾\n鈉\n紋\n罷\n拍\n咱\n喊\n袖\n埃\n勤\n罰\n焦\n潛\n伍\n墨\n欲\n縫\n姓\n刊\n飽\n仿\n獎\n鋁\n鬼\n麗\n跨\n默\n挖\n鏈\n掃\n喝\n袋\n炭\n污\n幕\n諸\n弧\n勵\n梅\n奶\n潔\n災\n舟\n鑑\n苯\n訟\n抱\n毀\n懂\n寒\n智\n埔\n寄\n屆\n躍\n渡\n挑\n丹\n艱\n貝\n碰\n拔\n爹\n戴\n碼\n夢\n芽\n熔\n赤\n漁\n哭\n敬\n顆\n奔\n鉛\n仲\n虎\n稀\n妹\n乏\n珍\n申\n桌\n遵\n允\n隆\n螺\n倉\n魏\n銳\n曉\n氮\n兼\n隱\n礙\n赫\n撥\n忠\n肅\n缸\n牽\n搶\n博\n巧\n殼\n兄\n杜\n訊\n誠\n碧\n祥\n柯\n頁\n巡\n矩\n悲\n灌\n齡\n倫\n票\n尋\n桂\n鋪\n聖\n恐\n恰\n鄭\n趣\n抬\n荒\n騰\n貼\n柔\n滴\n猛\n闊\n輛\n妻\n填\n撤\n儲\n簽\n鬧\n擾\n紫\n砂\n遞\n戲\n吊\n陶\n伐\n餵\n療\n瓶\n婆\n撫\n臂\n摸\n忍\n蝦\n蠟\n鄰\n胸\n鞏\n擠\n偶\n棄\n槽\n勁\n乳\n鄧\n吉\n仁\n爛\n磚\n租\n烏\n艦\n伴\n瓜\n淺\n丙\n暫\n燥\n橡\n柳\n迷\n暖\n牌\n秧\n膽\n詳\n簧\n踏\n瓷\n譜\n呆\n賓\n糊\n洛\n輝\n憤\n競\n隙\n怒\n粘\n乃\n緒\n肩\n籍\n敏\n塗\n熙\n皆\n偵\n懸\n掘\n享\n糾\n醒\n狂\n鎖\n淀\n恨\n牲\n霸\n爬\n賞\n逆\n玩\n陵\n祝\n秒\n浙\n貌\n役\n彼\n悉\n鴨\n趨\n鳳\n晨\n畜\n輩\n秩\n卵\n署\n梯\n炎\n灘\n棋\n驅\n篩\n峽\n冒\n啥\n壽\n譯\n浸\n泉\n帽\n遲\n矽\n疆\n貸\n漏\n稿\n冠\n嫩\n脅\n芯\n牢\n叛\n蝕\n奧\n鳴\n嶺\n羊\n憑\n串\n塘\n繪\n酵\n融\n盆\n錫\n廟\n籌\n凍\n輔\n攝\n襲\n筋\n拒\n僚\n旱\n鉀\n鳥\n漆\n沈\n眉\n疏\n添\n棒\n穗\n硝\n韓\n逼\n扭\n僑\n涼\n挺\n碗\n栽\n炒\n杯\n患\n餾\n勸\n豪\n遼\n勃\n鴻\n旦\n吏\n拜\n狗\n埋\n輥\n掩\n飲\n搬\n罵\n辭\n勾\n扣\n估\n蔣\n絨\n霧\n丈\n朵\n姆\n擬\n宇\n輯\n陝\n雕\n償\n蓄\n崇\n剪\n倡\n廳\n咬\n駛\n薯\n刷\n斥\n番\n賦\n奉\n佛\n澆\n漫\n曼\n扇\n鈣\n桃\n扶\n仔\n返\n俗\n虧\n腔\n鞋\n棱\n覆\n框\n悄\n叔\n撞\n騙\n勘\n旺\n沸\n孤\n吐\n孟\n渠\n屈\n疾\n妙\n惜\n仰\n狠\n脹\n諧\n拋\n黴\n桑\n崗\n嘛\n衰\n盜\n滲\n臟\n賴\n湧\n甜\n曹\n閱\n肌\n哩\n厲\n烴\n緯\n毅\n昨\n偽\n症\n煮\n嘆\n釘\n搭\n莖\n籠\n酷\n偷\n弓\n錐\n恆\n傑\n坑\n鼻\n翼\n綸\n敘\n獄\n逮\n罐\n絡\n棚\n抑\n膨\n蔬\n寺\n驟\n穆\n冶\n枯\n冊\n屍\n凸\n紳\n坯\n犧\n焰\n轟\n欣\n晉\n瘦\n禦\n錠\n錦\n喪\n旬\n鍛\n壟\n搜\n撲\n邀\n亭\n酯\n邁\n舒\n脆\n酶\n閒\n憂\n酚\n頑\n羽\n漲\n卸\n仗\n陪\n闢\n懲\n杭\n姚\n肚\n捉\n飄\n漂\n昆\n欺\n吾\n郎\n烷\n汁\n呵\n飾\n蕭\n雅\n郵\n遷\n燕\n撒\n姻\n赴\n宴\n煩\n債\n帳\n斑\n鈴\n旨\n醇\n董\n餅\n雛\n姿\n拌\n傅\n腹\n妥\n揉\n賢\n拆\n歪\n葡\n胺\n丟\n浩\n徽\n昂\n墊\n擋\n覽\n貪\n慰\n繳\n汪\n慌\n馮\n諾\n姜\n誼\n兇\n劣\n誣\n耀\n昏\n躺\n盈\n騎\n喬\n溪\n叢\n盧\n抹\n悶\n諮\n刮\n駕\n纜\n悟\n摘\n鉺\n擲\n頗\n幻\n柄\n惠\n慘\n佳\n仇\n臘\n窩\n滌\n劍\n瞧\n堡\n潑\n蔥\n罩\n霍\n撈\n胎\n蒼\n濱\n倆\n捅\n湘\n砍\n霞\n邵\n萄\n瘋\n淮\n遂\n熊\n糞\n烘\n宿\n檔\n戈\n駁\n嫂\n裕\n徙\n箭\n捐\n腸\n撐\n曬\n辨\n殿\n蓮\n攤\n攪\n醬\n屏\n疫\n哀\n蔡\n堵\n沫\n皺\n暢\n疊\n閣\n萊\n敲\n轄\n鉤\n痕\n壩\n巷\n餓\n禍\n丘\n玄\n溜\n曰\n邏\n彭\n嘗\n卿\n妨\n艇\n吞\n韋\n怨\n矮\n歇\n"); - dico.Add("english", - "abandon\nability\nable\nabout\nabove\nabsent\nabsorb\nabstract\nabsurd\nabuse\naccess\naccident\naccount\naccuse\nachieve\nacid\nacoustic\nacquire\nacross\nact\naction\nactor\nactress\nactual\nadapt\nadd\naddict\naddress\nadjust\nadmit\nadult\nadvance\nadvice\naerobic\naffair\nafford\nafraid\nagain\nage\nagent\nagree\nahead\naim\nair\nairport\naisle\nalarm\nalbum\nalcohol\nalert\nalien\nall\nalley\nallow\nalmost\nalone\nalpha\nalready\nalso\nalter\nalways\namateur\namazing\namong\namount\namused\nanalyst\nanchor\nancient\nanger\nangle\nangry\nanimal\nankle\nannounce\nannual\nanother\nanswer\nantenna\nantique\nanxiety\nany\napart\napology\nappear\napple\napprove\napril\narch\narctic\narea\narena\nargue\narm\narmed\narmor\narmy\naround\narrange\narrest\narrive\narrow\nart\nartefact\nartist\nartwork\nask\naspect\nassault\nasset\nassist\nassume\nasthma\nathlete\natom\nattack\nattend\nattitude\nattract\nauction\naudit\naugust\naunt\nauthor\nauto\nautumn\naverage\navocado\navoid\nawake\naware\naway\nawesome\nawful\nawkward\naxis\nbaby\nbachelor\nbacon\nbadge\nbag\nbalance\nbalcony\nball\nbamboo\nbanana\nbanner\nbar\nbarely\nbargain\nbarrel\nbase\nbasic\nbasket\nbattle\nbeach\nbean\nbeauty\nbecause\nbecome\nbeef\nbefore\nbegin\nbehave\nbehind\nbelieve\nbelow\nbelt\nbench\nbenefit\nbest\nbetray\nbetter\nbetween\nbeyond\nbicycle\nbid\nbike\nbind\nbiology\nbird\nbirth\nbitter\nblack\nblade\nblame\nblanket\nblast\nbleak\nbless\nblind\nblood\nblossom\nblouse\nblue\nblur\nblush\nboard\nboat\nbody\nboil\nbomb\nbone\nbonus\nbook\nboost\nborder\nboring\nborrow\nboss\nbottom\nbounce\nbox\nboy\nbracket\nbrain\nbrand\nbrass\nbrave\nbread\nbreeze\nbrick\nbridge\nbrief\nbright\nbring\nbrisk\nbroccoli\nbroken\nbronze\nbroom\nbrother\nbrown\nbrush\nbubble\nbuddy\nbudget\nbuffalo\nbuild\nbulb\nbulk\nbullet\nbundle\nbunker\nburden\nburger\nburst\nbus\nbusiness\nbusy\nbutter\nbuyer\nbuzz\ncabbage\ncabin\ncable\ncactus\ncage\ncake\ncall\ncalm\ncamera\ncamp\ncan\ncanal\ncancel\ncandy\ncannon\ncanoe\ncanvas\ncanyon\ncapable\ncapital\ncaptain\ncar\ncarbon\ncard\ncargo\ncarpet\ncarry\ncart\ncase\ncash\ncasino\ncastle\ncasual\ncat\ncatalog\ncatch\ncategory\ncattle\ncaught\ncause\ncaution\ncave\nceiling\ncelery\ncement\ncensus\ncentury\ncereal\ncertain\nchair\nchalk\nchampion\nchange\nchaos\nchapter\ncharge\nchase\nchat\ncheap\ncheck\ncheese\nchef\ncherry\nchest\nchicken\nchief\nchild\nchimney\nchoice\nchoose\nchronic\nchuckle\nchunk\nchurn\ncigar\ncinnamon\ncircle\ncitizen\ncity\ncivil\nclaim\nclap\nclarify\nclaw\nclay\nclean\nclerk\nclever\nclick\nclient\ncliff\nclimb\nclinic\nclip\nclock\nclog\nclose\ncloth\ncloud\nclown\nclub\nclump\ncluster\nclutch\ncoach\ncoast\ncoconut\ncode\ncoffee\ncoil\ncoin\ncollect\ncolor\ncolumn\ncombine\ncome\ncomfort\ncomic\ncommon\ncompany\nconcert\nconduct\nconfirm\ncongress\nconnect\nconsider\ncontrol\nconvince\ncook\ncool\ncopper\ncopy\ncoral\ncore\ncorn\ncorrect\ncost\ncotton\ncouch\ncountry\ncouple\ncourse\ncousin\ncover\ncoyote\ncrack\ncradle\ncraft\ncram\ncrane\ncrash\ncrater\ncrawl\ncrazy\ncream\ncredit\ncreek\ncrew\ncricket\ncrime\ncrisp\ncritic\ncrop\ncross\ncrouch\ncrowd\ncrucial\ncruel\ncruise\ncrumble\ncrunch\ncrush\ncry\ncrystal\ncube\nculture\ncup\ncupboard\ncurious\ncurrent\ncurtain\ncurve\ncushion\ncustom\ncute\ncycle\ndad\ndamage\ndamp\ndance\ndanger\ndaring\ndash\ndaughter\ndawn\nday\ndeal\ndebate\ndebris\ndecade\ndecember\ndecide\ndecline\ndecorate\ndecrease\ndeer\ndefense\ndefine\ndefy\ndegree\ndelay\ndeliver\ndemand\ndemise\ndenial\ndentist\ndeny\ndepart\ndepend\ndeposit\ndepth\ndeputy\nderive\ndescribe\ndesert\ndesign\ndesk\ndespair\ndestroy\ndetail\ndetect\ndevelop\ndevice\ndevote\ndiagram\ndial\ndiamond\ndiary\ndice\ndiesel\ndiet\ndiffer\ndigital\ndignity\ndilemma\ndinner\ndinosaur\ndirect\ndirt\ndisagree\ndiscover\ndisease\ndish\ndismiss\ndisorder\ndisplay\ndistance\ndivert\ndivide\ndivorce\ndizzy\ndoctor\ndocument\ndog\ndoll\ndolphin\ndomain\ndonate\ndonkey\ndonor\ndoor\ndose\ndouble\ndove\ndraft\ndragon\ndrama\ndrastic\ndraw\ndream\ndress\ndrift\ndrill\ndrink\ndrip\ndrive\ndrop\ndrum\ndry\nduck\ndumb\ndune\nduring\ndust\ndutch\nduty\ndwarf\ndynamic\neager\neagle\nearly\nearn\nearth\neasily\neast\neasy\necho\necology\neconomy\nedge\nedit\neducate\neffort\negg\neight\neither\nelbow\nelder\nelectric\nelegant\nelement\nelephant\nelevator\nelite\nelse\nembark\nembody\nembrace\nemerge\nemotion\nemploy\nempower\nempty\nenable\nenact\nend\nendless\nendorse\nenemy\nenergy\nenforce\nengage\nengine\nenhance\nenjoy\nenlist\nenough\nenrich\nenroll\nensure\nenter\nentire\nentry\nenvelope\nepisode\nequal\nequip\nera\nerase\nerode\nerosion\nerror\nerupt\nescape\nessay\nessence\nestate\neternal\nethics\nevidence\nevil\nevoke\nevolve\nexact\nexample\nexcess\nexchange\nexcite\nexclude\nexcuse\nexecute\nexercise\nexhaust\nexhibit\nexile\nexist\nexit\nexotic\nexpand\nexpect\nexpire\nexplain\nexpose\nexpress\nextend\nextra\neye\neyebrow\nfabric\nface\nfaculty\nfade\nfaint\nfaith\nfall\nfalse\nfame\nfamily\nfamous\nfan\nfancy\nfantasy\nfarm\nfashion\nfat\nfatal\nfather\nfatigue\nfault\nfavorite\nfeature\nfebruary\nfederal\nfee\nfeed\nfeel\nfemale\nfence\nfestival\nfetch\nfever\nfew\nfiber\nfiction\nfield\nfigure\nfile\nfilm\nfilter\nfinal\nfind\nfine\nfinger\nfinish\nfire\nfirm\nfirst\nfiscal\nfish\nfit\nfitness\nfix\nflag\nflame\nflash\nflat\nflavor\nflee\nflight\nflip\nfloat\nflock\nfloor\nflower\nfluid\nflush\nfly\nfoam\nfocus\nfog\nfoil\nfold\nfollow\nfood\nfoot\nforce\nforest\nforget\nfork\nfortune\nforum\nforward\nfossil\nfoster\nfound\nfox\nfragile\nframe\nfrequent\nfresh\nfriend\nfringe\nfrog\nfront\nfrost\nfrown\nfrozen\nfruit\nfuel\nfun\nfunny\nfurnace\nfury\nfuture\ngadget\ngain\ngalaxy\ngallery\ngame\ngap\ngarage\ngarbage\ngarden\ngarlic\ngarment\ngas\ngasp\ngate\ngather\ngauge\ngaze\ngeneral\ngenius\ngenre\ngentle\ngenuine\ngesture\nghost\ngiant\ngift\ngiggle\nginger\ngiraffe\ngirl\ngive\nglad\nglance\nglare\nglass\nglide\nglimpse\nglobe\ngloom\nglory\nglove\nglow\nglue\ngoat\ngoddess\ngold\ngood\ngoose\ngorilla\ngospel\ngossip\ngovern\ngown\ngrab\ngrace\ngrain\ngrant\ngrape\ngrass\ngravity\ngreat\ngreen\ngrid\ngrief\ngrit\ngrocery\ngroup\ngrow\ngrunt\nguard\nguess\nguide\nguilt\nguitar\ngun\ngym\nhabit\nhair\nhalf\nhammer\nhamster\nhand\nhappy\nharbor\nhard\nharsh\nharvest\nhat\nhave\nhawk\nhazard\nhead\nhealth\nheart\nheavy\nhedgehog\nheight\nhello\nhelmet\nhelp\nhen\nhero\nhidden\nhigh\nhill\nhint\nhip\nhire\nhistory\nhobby\nhockey\nhold\nhole\nholiday\nhollow\nhome\nhoney\nhood\nhope\nhorn\nhorror\nhorse\nhospital\nhost\nhotel\nhour\nhover\nhub\nhuge\nhuman\nhumble\nhumor\nhundred\nhungry\nhunt\nhurdle\nhurry\nhurt\nhusband\nhybrid\nice\nicon\nidea\nidentify\nidle\nignore\nill\nillegal\nillness\nimage\nimitate\nimmense\nimmune\nimpact\nimpose\nimprove\nimpulse\ninch\ninclude\nincome\nincrease\nindex\nindicate\nindoor\nindustry\ninfant\ninflict\ninform\ninhale\ninherit\ninitial\ninject\ninjury\ninmate\ninner\ninnocent\ninput\ninquiry\ninsane\ninsect\ninside\ninspire\ninstall\nintact\ninterest\ninto\ninvest\ninvite\ninvolve\niron\nisland\nisolate\nissue\nitem\nivory\njacket\njaguar\njar\njazz\njealous\njeans\njelly\njewel\njob\njoin\njoke\njourney\njoy\njudge\njuice\njump\njungle\njunior\njunk\njust\nkangaroo\nkeen\nkeep\nketchup\nkey\nkick\nkid\nkidney\nkind\nkingdom\nkiss\nkit\nkitchen\nkite\nkitten\nkiwi\nknee\nknife\nknock\nknow\nlab\nlabel\nlabor\nladder\nlady\nlake\nlamp\nlanguage\nlaptop\nlarge\nlater\nlatin\nlaugh\nlaundry\nlava\nlaw\nlawn\nlawsuit\nlayer\nlazy\nleader\nleaf\nlearn\nleave\nlecture\nleft\nleg\nlegal\nlegend\nleisure\nlemon\nlend\nlength\nlens\nleopard\nlesson\nletter\nlevel\nliar\nliberty\nlibrary\nlicense\nlife\nlift\nlight\nlike\nlimb\nlimit\nlink\nlion\nliquid\nlist\nlittle\nlive\nlizard\nload\nloan\nlobster\nlocal\nlock\nlogic\nlonely\nlong\nloop\nlottery\nloud\nlounge\nlove\nloyal\nlucky\nluggage\nlumber\nlunar\nlunch\nluxury\nlyrics\nmachine\nmad\nmagic\nmagnet\nmaid\nmail\nmain\nmajor\nmake\nmammal\nman\nmanage\nmandate\nmango\nmansion\nmanual\nmaple\nmarble\nmarch\nmargin\nmarine\nmarket\nmarriage\nmask\nmass\nmaster\nmatch\nmaterial\nmath\nmatrix\nmatter\nmaximum\nmaze\nmeadow\nmean\nmeasure\nmeat\nmechanic\nmedal\nmedia\nmelody\nmelt\nmember\nmemory\nmention\nmenu\nmercy\nmerge\nmerit\nmerry\nmesh\nmessage\nmetal\nmethod\nmiddle\nmidnight\nmilk\nmillion\nmimic\nmind\nminimum\nminor\nminute\nmiracle\nmirror\nmisery\nmiss\nmistake\nmix\nmixed\nmixture\nmobile\nmodel\nmodify\nmom\nmoment\nmonitor\nmonkey\nmonster\nmonth\nmoon\nmoral\nmore\nmorning\nmosquito\nmother\nmotion\nmotor\nmountain\nmouse\nmove\nmovie\nmuch\nmuffin\nmule\nmultiply\nmuscle\nmuseum\nmushroom\nmusic\nmust\nmutual\nmyself\nmystery\nmyth\nnaive\nname\nnapkin\nnarrow\nnasty\nnation\nnature\nnear\nneck\nneed\nnegative\nneglect\nneither\nnephew\nnerve\nnest\nnet\nnetwork\nneutral\nnever\nnews\nnext\nnice\nnight\nnoble\nnoise\nnominee\nnoodle\nnormal\nnorth\nnose\nnotable\nnote\nnothing\nnotice\nnovel\nnow\nnuclear\nnumber\nnurse\nnut\noak\nobey\nobject\noblige\nobscure\nobserve\nobtain\nobvious\noccur\nocean\noctober\nodor\noff\noffer\noffice\noften\noil\nokay\nold\nolive\nolympic\nomit\nonce\none\nonion\nonline\nonly\nopen\nopera\nopinion\noppose\noption\norange\norbit\norchard\norder\nordinary\norgan\norient\noriginal\norphan\nostrich\nother\noutdoor\nouter\noutput\noutside\noval\noven\nover\nown\nowner\noxygen\noyster\nozone\npact\npaddle\npage\npair\npalace\npalm\npanda\npanel\npanic\npanther\npaper\nparade\nparent\npark\nparrot\nparty\npass\npatch\npath\npatient\npatrol\npattern\npause\npave\npayment\npeace\npeanut\npear\npeasant\npelican\npen\npenalty\npencil\npeople\npepper\nperfect\npermit\nperson\npet\nphone\nphoto\nphrase\nphysical\npiano\npicnic\npicture\npiece\npig\npigeon\npill\npilot\npink\npioneer\npipe\npistol\npitch\npizza\nplace\nplanet\nplastic\nplate\nplay\nplease\npledge\npluck\nplug\nplunge\npoem\npoet\npoint\npolar\npole\npolice\npond\npony\npool\npopular\nportion\nposition\npossible\npost\npotato\npottery\npoverty\npowder\npower\npractice\npraise\npredict\nprefer\nprepare\npresent\npretty\nprevent\nprice\npride\nprimary\nprint\npriority\nprison\nprivate\nprize\nproblem\nprocess\nproduce\nprofit\nprogram\nproject\npromote\nproof\nproperty\nprosper\nprotect\nproud\nprovide\npublic\npudding\npull\npulp\npulse\npumpkin\npunch\npupil\npuppy\npurchase\npurity\npurpose\npurse\npush\nput\npuzzle\npyramid\nquality\nquantum\nquarter\nquestion\nquick\nquit\nquiz\nquote\nrabbit\nraccoon\nrace\nrack\nradar\nradio\nrail\nrain\nraise\nrally\nramp\nranch\nrandom\nrange\nrapid\nrare\nrate\nrather\nraven\nraw\nrazor\nready\nreal\nreason\nrebel\nrebuild\nrecall\nreceive\nrecipe\nrecord\nrecycle\nreduce\nreflect\nreform\nrefuse\nregion\nregret\nregular\nreject\nrelax\nrelease\nrelief\nrely\nremain\nremember\nremind\nremove\nrender\nrenew\nrent\nreopen\nrepair\nrepeat\nreplace\nreport\nrequire\nrescue\nresemble\nresist\nresource\nresponse\nresult\nretire\nretreat\nreturn\nreunion\nreveal\nreview\nreward\nrhythm\nrib\nribbon\nrice\nrich\nride\nridge\nrifle\nright\nrigid\nring\nriot\nripple\nrisk\nritual\nrival\nriver\nroad\nroast\nrobot\nrobust\nrocket\nromance\nroof\nrookie\nroom\nrose\nrotate\nrough\nround\nroute\nroyal\nrubber\nrude\nrug\nrule\nrun\nrunway\nrural\nsad\nsaddle\nsadness\nsafe\nsail\nsalad\nsalmon\nsalon\nsalt\nsalute\nsame\nsample\nsand\nsatisfy\nsatoshi\nsauce\nsausage\nsave\nsay\nscale\nscan\nscare\nscatter\nscene\nscheme\nschool\nscience\nscissors\nscorpion\nscout\nscrap\nscreen\nscript\nscrub\nsea\nsearch\nseason\nseat\nsecond\nsecret\nsection\nsecurity\nseed\nseek\nsegment\nselect\nsell\nseminar\nsenior\nsense\nsentence\nseries\nservice\nsession\nsettle\nsetup\nseven\nshadow\nshaft\nshallow\nshare\nshed\nshell\nsheriff\nshield\nshift\nshine\nship\nshiver\nshock\nshoe\nshoot\nshop\nshort\nshoulder\nshove\nshrimp\nshrug\nshuffle\nshy\nsibling\nsick\nside\nsiege\nsight\nsign\nsilent\nsilk\nsilly\nsilver\nsimilar\nsimple\nsince\nsing\nsiren\nsister\nsituate\nsix\nsize\nskate\nsketch\nski\nskill\nskin\nskirt\nskull\nslab\nslam\nsleep\nslender\nslice\nslide\nslight\nslim\nslogan\nslot\nslow\nslush\nsmall\nsmart\nsmile\nsmoke\nsmooth\nsnack\nsnake\nsnap\nsniff\nsnow\nsoap\nsoccer\nsocial\nsock\nsoda\nsoft\nsolar\nsoldier\nsolid\nsolution\nsolve\nsomeone\nsong\nsoon\nsorry\nsort\nsoul\nsound\nsoup\nsource\nsouth\nspace\nspare\nspatial\nspawn\nspeak\nspecial\nspeed\nspell\nspend\nsphere\nspice\nspider\nspike\nspin\nspirit\nsplit\nspoil\nsponsor\nspoon\nsport\nspot\nspray\nspread\nspring\nspy\nsquare\nsqueeze\nsquirrel\nstable\nstadium\nstaff\nstage\nstairs\nstamp\nstand\nstart\nstate\nstay\nsteak\nsteel\nstem\nstep\nstereo\nstick\nstill\nsting\nstock\nstomach\nstone\nstool\nstory\nstove\nstrategy\nstreet\nstrike\nstrong\nstruggle\nstudent\nstuff\nstumble\nstyle\nsubject\nsubmit\nsubway\nsuccess\nsuch\nsudden\nsuffer\nsugar\nsuggest\nsuit\nsummer\nsun\nsunny\nsunset\nsuper\nsupply\nsupreme\nsure\nsurface\nsurge\nsurprise\nsurround\nsurvey\nsuspect\nsustain\nswallow\nswamp\nswap\nswarm\nswear\nsweet\nswift\nswim\nswing\nswitch\nsword\nsymbol\nsymptom\nsyrup\nsystem\ntable\ntackle\ntag\ntail\ntalent\ntalk\ntank\ntape\ntarget\ntask\ntaste\ntattoo\ntaxi\nteach\nteam\ntell\nten\ntenant\ntennis\ntent\nterm\ntest\ntext\nthank\nthat\ntheme\nthen\ntheory\nthere\nthey\nthing\nthis\nthought\nthree\nthrive\nthrow\nthumb\nthunder\nticket\ntide\ntiger\ntilt\ntimber\ntime\ntiny\ntip\ntired\ntissue\ntitle\ntoast\ntobacco\ntoday\ntoddler\ntoe\ntogether\ntoilet\ntoken\ntomato\ntomorrow\ntone\ntongue\ntonight\ntool\ntooth\ntop\ntopic\ntopple\ntorch\ntornado\ntortoise\ntoss\ntotal\ntourist\ntoward\ntower\ntown\ntoy\ntrack\ntrade\ntraffic\ntragic\ntrain\ntransfer\ntrap\ntrash\ntravel\ntray\ntreat\ntree\ntrend\ntrial\ntribe\ntrick\ntrigger\ntrim\ntrip\ntrophy\ntrouble\ntruck\ntrue\ntruly\ntrumpet\ntrust\ntruth\ntry\ntube\ntuition\ntumble\ntuna\ntunnel\nturkey\nturn\nturtle\ntwelve\ntwenty\ntwice\ntwin\ntwist\ntwo\ntype\ntypical\nugly\numbrella\nunable\nunaware\nuncle\nuncover\nunder\nundo\nunfair\nunfold\nunhappy\nuniform\nunique\nunit\nuniverse\nunknown\nunlock\nuntil\nunusual\nunveil\nupdate\nupgrade\nuphold\nupon\nupper\nupset\nurban\nurge\nusage\nuse\nused\nuseful\nuseless\nusual\nutility\nvacant\nvacuum\nvague\nvalid\nvalley\nvalve\nvan\nvanish\nvapor\nvarious\nvast\nvault\nvehicle\nvelvet\nvendor\nventure\nvenue\nverb\nverify\nversion\nvery\nvessel\nveteran\nviable\nvibrant\nvicious\nvictory\nvideo\nview\nvillage\nvintage\nviolin\nvirtual\nvirus\nvisa\nvisit\nvisual\nvital\nvivid\nvocal\nvoice\nvoid\nvolcano\nvolume\nvote\nvoyage\nwage\nwagon\nwait\nwalk\nwall\nwalnut\nwant\nwarfare\nwarm\nwarrior\nwash\nwasp\nwaste\nwater\nwave\nway\nwealth\nweapon\nwear\nweasel\nweather\nweb\nwedding\nweekend\nweird\nwelcome\nwest\nwet\nwhale\nwhat\nwheat\nwheel\nwhen\nwhere\nwhip\nwhisper\nwide\nwidth\nwife\nwild\nwill\nwin\nwindow\nwine\nwing\nwink\nwinner\nwinter\nwire\nwisdom\nwise\nwish\nwitness\nwolf\nwoman\nwonder\nwood\nwool\nword\nwork\nworld\nworry\nworth\nwrap\nwreck\nwrestle\nwrist\nwrite\nwrong\nyard\nyear\nyellow\nyou\nyoung\nyouth\nzebra\nzero\nzone\nzoo\n"); - dico.Add("japanese", - "あいこくしん\nあいさつ\nあいだ\nあおぞら\nあかちゃん\nあきる\nあけがた\nあける\nあこがれる\nあさい\nあさひ\nあしあと\nあじわう\nあずかる\nあずき\nあそぶ\nあたえる\nあたためる\nあたりまえ\nあたる\nあつい\nあつかう\nあっしゅく\nあつまり\nあつめる\nあてな\nあてはまる\nあひる\nあぶら\nあぶる\nあふれる\nあまい\nあまど\nあまやかす\nあまり\nあみもの\nあめりか\nあやまる\nあゆむ\nあらいぐま\nあらし\nあらすじ\nあらためる\nあらゆる\nあらわす\nありがとう\nあわせる\nあわてる\nあんい\nあんがい\nあんこ\nあんぜん\nあんてい\nあんない\nあんまり\nいいだす\nいおん\nいがい\nいがく\nいきおい\nいきなり\nいきもの\nいきる\nいくじ\nいくぶん\nいけばな\nいけん\nいこう\nいこく\nいこつ\nいさましい\nいさん\nいしき\nいじゅう\nいじょう\nいじわる\nいずみ\nいずれ\nいせい\nいせえび\nいせかい\nいせき\nいぜん\nいそうろう\nいそがしい\nいだい\nいだく\nいたずら\nいたみ\nいたりあ\nいちおう\nいちじ\nいちど\nいちば\nいちぶ\nいちりゅう\nいつか\nいっしゅん\nいっせい\nいっそう\nいったん\nいっち\nいってい\nいっぽう\nいてざ\nいてん\nいどう\nいとこ\nいない\nいなか\nいねむり\nいのち\nいのる\nいはつ\nいばる\nいはん\nいびき\nいひん\nいふく\nいへん\nいほう\nいみん\nいもうと\nいもたれ\nいもり\nいやがる\nいやす\nいよかん\nいよく\nいらい\nいらすと\nいりぐち\nいりょう\nいれい\nいれもの\nいれる\nいろえんぴつ\nいわい\nいわう\nいわかん\nいわば\nいわゆる\nいんげんまめ\nいんさつ\nいんしょう\nいんよう\nうえき\nうえる\nうおざ\nうがい\nうかぶ\nうかべる\nうきわ\nうくらいな\nうくれれ\nうけたまわる\nうけつけ\nうけとる\nうけもつ\nうける\nうごかす\nうごく\nうこん\nうさぎ\nうしなう\nうしろがみ\nうすい\nうすぎ\nうすぐらい\nうすめる\nうせつ\nうちあわせ\nうちがわ\nうちき\nうちゅう\nうっかり\nうつくしい\nうったえる\nうつる\nうどん\nうなぎ\nうなじ\nうなずく\nうなる\nうねる\nうのう\nうぶげ\nうぶごえ\nうまれる\nうめる\nうもう\nうやまう\nうよく\nうらがえす\nうらぐち\nうらない\nうりあげ\nうりきれ\nうるさい\nうれしい\nうれゆき\nうれる\nうろこ\nうわき\nうわさ\nうんこう\nうんちん\nうんてん\nうんどう\nえいえん\nえいが\nえいきょう\nえいご\nえいせい\nえいぶん\nえいよう\nえいわ\nえおり\nえがお\nえがく\nえきたい\nえくせる\nえしゃく\nえすて\nえつらん\nえのぐ\nえほうまき\nえほん\nえまき\nえもじ\nえもの\nえらい\nえらぶ\nえりあ\nえんえん\nえんかい\nえんぎ\nえんげき\nえんしゅう\nえんぜつ\nえんそく\nえんちょう\nえんとつ\nおいかける\nおいこす\nおいしい\nおいつく\nおうえん\nおうさま\nおうじ\nおうせつ\nおうたい\nおうふく\nおうべい\nおうよう\nおえる\nおおい\nおおう\nおおどおり\nおおや\nおおよそ\nおかえり\nおかず\nおがむ\nおかわり\nおぎなう\nおきる\nおくさま\nおくじょう\nおくりがな\nおくる\nおくれる\nおこす\nおこなう\nおこる\nおさえる\nおさない\nおさめる\nおしいれ\nおしえる\nおじぎ\nおじさん\nおしゃれ\nおそらく\nおそわる\nおたがい\nおたく\nおだやか\nおちつく\nおっと\nおつり\nおでかけ\nおとしもの\nおとなしい\nおどり\nおどろかす\nおばさん\nおまいり\nおめでとう\nおもいで\nおもう\nおもたい\nおもちゃ\nおやつ\nおやゆび\nおよぼす\nおらんだ\nおろす\nおんがく\nおんけい\nおんしゃ\nおんせん\nおんだん\nおんちゅう\nおんどけい\nかあつ\nかいが\nがいき\nがいけん\nがいこう\nかいさつ\nかいしゃ\nかいすいよく\nかいぜん\nかいぞうど\nかいつう\nかいてん\nかいとう\nかいふく\nがいへき\nかいほう\nかいよう\nがいらい\nかいわ\nかえる\nかおり\nかかえる\nかがく\nかがし\nかがみ\nかくご\nかくとく\nかざる\nがぞう\nかたい\nかたち\nがちょう\nがっきゅう\nがっこう\nがっさん\nがっしょう\nかなざわし\nかのう\nがはく\nかぶか\nかほう\nかほご\nかまう\nかまぼこ\nかめれおん\nかゆい\nかようび\nからい\nかるい\nかろう\nかわく\nかわら\nがんか\nかんけい\nかんこう\nかんしゃ\nかんそう\nかんたん\nかんち\nがんばる\nきあい\nきあつ\nきいろ\nぎいん\nきうい\nきうん\nきえる\nきおう\nきおく\nきおち\nきおん\nきかい\nきかく\nきかんしゃ\nききて\nきくばり\nきくらげ\nきけんせい\nきこう\nきこえる\nきこく\nきさい\nきさく\nきさま\nきさらぎ\nぎじかがく\nぎしき\nぎじたいけん\nぎじにってい\nぎじゅつしゃ\nきすう\nきせい\nきせき\nきせつ\nきそう\nきぞく\nきぞん\nきたえる\nきちょう\nきつえん\nぎっちり\nきつつき\nきつね\nきてい\nきどう\nきどく\nきない\nきなが\nきなこ\nきぬごし\nきねん\nきのう\nきのした\nきはく\nきびしい\nきひん\nきふく\nきぶん\nきぼう\nきほん\nきまる\nきみつ\nきむずかしい\nきめる\nきもだめし\nきもち\nきもの\nきゃく\nきやく\nぎゅうにく\nきよう\nきょうりゅう\nきらい\nきらく\nきりん\nきれい\nきれつ\nきろく\nぎろん\nきわめる\nぎんいろ\nきんかくじ\nきんじょ\nきんようび\nぐあい\nくいず\nくうかん\nくうき\nくうぐん\nくうこう\nぐうせい\nくうそう\nぐうたら\nくうふく\nくうぼ\nくかん\nくきょう\nくげん\nぐこう\nくさい\nくさき\nくさばな\nくさる\nくしゃみ\nくしょう\nくすのき\nくすりゆび\nくせげ\nくせん\nぐたいてき\nくださる\nくたびれる\nくちこみ\nくちさき\nくつした\nぐっすり\nくつろぐ\nくとうてん\nくどく\nくなん\nくねくね\nくのう\nくふう\nくみあわせ\nくみたてる\nくめる\nくやくしょ\nくらす\nくらべる\nくるま\nくれる\nくろう\nくわしい\nぐんかん\nぐんしょく\nぐんたい\nぐんて\nけあな\nけいかく\nけいけん\nけいこ\nけいさつ\nげいじゅつ\nけいたい\nげいのうじん\nけいれき\nけいろ\nけおとす\nけおりもの\nげきか\nげきげん\nげきだん\nげきちん\nげきとつ\nげきは\nげきやく\nげこう\nげこくじょう\nげざい\nけさき\nげざん\nけしき\nけしごむ\nけしょう\nげすと\nけたば\nけちゃっぷ\nけちらす\nけつあつ\nけつい\nけつえき\nけっこん\nけつじょ\nけっせき\nけってい\nけつまつ\nげつようび\nげつれい\nけつろん\nげどく\nけとばす\nけとる\nけなげ\nけなす\nけなみ\nけぬき\nげねつ\nけねん\nけはい\nげひん\nけぶかい\nげぼく\nけまり\nけみかる\nけむし\nけむり\nけもの\nけらい\nけろけろ\nけわしい\nけんい\nけんえつ\nけんお\nけんか\nげんき\nけんげん\nけんこう\nけんさく\nけんしゅう\nけんすう\nげんそう\nけんちく\nけんてい\nけんとう\nけんない\nけんにん\nげんぶつ\nけんま\nけんみん\nけんめい\nけんらん\nけんり\nこあくま\nこいぬ\nこいびと\nごうい\nこうえん\nこうおん\nこうかん\nごうきゅう\nごうけい\nこうこう\nこうさい\nこうじ\nこうすい\nごうせい\nこうそく\nこうたい\nこうちゃ\nこうつう\nこうてい\nこうどう\nこうない\nこうはい\nごうほう\nごうまん\nこうもく\nこうりつ\nこえる\nこおり\nごかい\nごがつ\nごかん\nこくご\nこくさい\nこくとう\nこくない\nこくはく\nこぐま\nこけい\nこける\nここのか\nこころ\nこさめ\nこしつ\nこすう\nこせい\nこせき\nこぜん\nこそだて\nこたい\nこたえる\nこたつ\nこちょう\nこっか\nこつこつ\nこつばん\nこつぶ\nこてい\nこてん\nことがら\nことし\nことば\nことり\nこなごな\nこねこね\nこのまま\nこのみ\nこのよ\nごはん\nこひつじ\nこふう\nこふん\nこぼれる\nごまあぶら\nこまかい\nごますり\nこまつな\nこまる\nこむぎこ\nこもじ\nこもち\nこもの\nこもん\nこやく\nこやま\nこゆう\nこゆび\nこよい\nこよう\nこりる\nこれくしょん\nころっけ\nこわもて\nこわれる\nこんいん\nこんかい\nこんき\nこんしゅう\nこんすい\nこんだて\nこんとん\nこんなん\nこんびに\nこんぽん\nこんまけ\nこんや\nこんれい\nこんわく\nざいえき\nさいかい\nさいきん\nざいげん\nざいこ\nさいしょ\nさいせい\nざいたく\nざいちゅう\nさいてき\nざいりょう\nさうな\nさかいし\nさがす\nさかな\nさかみち\nさがる\nさぎょう\nさくし\nさくひん\nさくら\nさこく\nさこつ\nさずかる\nざせき\nさたん\nさつえい\nざつおん\nざっか\nざつがく\nさっきょく\nざっし\nさつじん\nざっそう\nさつたば\nさつまいも\nさてい\nさといも\nさとう\nさとおや\nさとし\nさとる\nさのう\nさばく\nさびしい\nさべつ\nさほう\nさほど\nさます\nさみしい\nさみだれ\nさむけ\nさめる\nさやえんどう\nさゆう\nさよう\nさよく\nさらだ\nざるそば\nさわやか\nさわる\nさんいん\nさんか\nさんきゃく\nさんこう\nさんさい\nざんしょ\nさんすう\nさんせい\nさんそ\nさんち\nさんま\nさんみ\nさんらん\nしあい\nしあげ\nしあさって\nしあわせ\nしいく\nしいん\nしうち\nしえい\nしおけ\nしかい\nしかく\nじかん\nしごと\nしすう\nじだい\nしたうけ\nしたぎ\nしたて\nしたみ\nしちょう\nしちりん\nしっかり\nしつじ\nしつもん\nしてい\nしてき\nしてつ\nじてん\nじどう\nしなぎれ\nしなもの\nしなん\nしねま\nしねん\nしのぐ\nしのぶ\nしはい\nしばかり\nしはつ\nしはらい\nしはん\nしひょう\nしふく\nじぶん\nしへい\nしほう\nしほん\nしまう\nしまる\nしみん\nしむける\nじむしょ\nしめい\nしめる\nしもん\nしゃいん\nしゃうん\nしゃおん\nじゃがいも\nしやくしょ\nしゃくほう\nしゃけん\nしゃこ\nしゃざい\nしゃしん\nしゃせん\nしゃそう\nしゃたい\nしゃちょう\nしゃっきん\nじゃま\nしゃりん\nしゃれい\nじゆう\nじゅうしょ\nしゅくはく\nじゅしん\nしゅっせき\nしゅみ\nしゅらば\nじゅんばん\nしょうかい\nしょくたく\nしょっけん\nしょどう\nしょもつ\nしらせる\nしらべる\nしんか\nしんこう\nじんじゃ\nしんせいじ\nしんちく\nしんりん\nすあげ\nすあし\nすあな\nずあん\nすいえい\nすいか\nすいとう\nずいぶん\nすいようび\nすうがく\nすうじつ\nすうせん\nすおどり\nすきま\nすくう\nすくない\nすける\nすごい\nすこし\nずさん\nすずしい\nすすむ\nすすめる\nすっかり\nずっしり\nずっと\nすてき\nすてる\nすねる\nすのこ\nすはだ\nすばらしい\nずひょう\nずぶぬれ\nすぶり\nすふれ\nすべて\nすべる\nずほう\nすぼん\nすまい\nすめし\nすもう\nすやき\nすらすら\nするめ\nすれちがう\nすろっと\nすわる\nすんぜん\nすんぽう\nせあぶら\nせいかつ\nせいげん\nせいじ\nせいよう\nせおう\nせかいかん\nせきにん\nせきむ\nせきゆ\nせきらんうん\nせけん\nせこう\nせすじ\nせたい\nせたけ\nせっかく\nせっきゃく\nぜっく\nせっけん\nせっこつ\nせっさたくま\nせつぞく\nせつだん\nせつでん\nせっぱん\nせつび\nせつぶん\nせつめい\nせつりつ\nせなか\nせのび\nせはば\nせびろ\nせぼね\nせまい\nせまる\nせめる\nせもたれ\nせりふ\nぜんあく\nせんい\nせんえい\nせんか\nせんきょ\nせんく\nせんげん\nぜんご\nせんさい\nせんしゅ\nせんすい\nせんせい\nせんぞ\nせんたく\nせんちょう\nせんてい\nせんとう\nせんぬき\nせんねん\nせんぱい\nぜんぶ\nぜんぽう\nせんむ\nせんめんじょ\nせんもん\nせんやく\nせんゆう\nせんよう\nぜんら\nぜんりゃく\nせんれい\nせんろ\nそあく\nそいとげる\nそいね\nそうがんきょう\nそうき\nそうご\nそうしん\nそうだん\nそうなん\nそうび\nそうめん\nそうり\nそえもの\nそえん\nそがい\nそげき\nそこう\nそこそこ\nそざい\nそしな\nそせい\nそせん\nそそぐ\nそだてる\nそつう\nそつえん\nそっかん\nそつぎょう\nそっけつ\nそっこう\nそっせん\nそっと\nそとがわ\nそとづら\nそなえる\nそなた\nそふぼ\nそぼく\nそぼろ\nそまつ\nそまる\nそむく\nそむりえ\nそめる\nそもそも\nそよかぜ\nそらまめ\nそろう\nそんかい\nそんけい\nそんざい\nそんしつ\nそんぞく\nそんちょう\nぞんび\nぞんぶん\nそんみん\nたあい\nたいいん\nたいうん\nたいえき\nたいおう\nだいがく\nたいき\nたいぐう\nたいけん\nたいこ\nたいざい\nだいじょうぶ\nだいすき\nたいせつ\nたいそう\nだいたい\nたいちょう\nたいてい\nだいどころ\nたいない\nたいねつ\nたいのう\nたいはん\nだいひょう\nたいふう\nたいへん\nたいほ\nたいまつばな\nたいみんぐ\nたいむ\nたいめん\nたいやき\nたいよう\nたいら\nたいりょく\nたいる\nたいわん\nたうえ\nたえる\nたおす\nたおる\nたおれる\nたかい\nたかね\nたきび\nたくさん\nたこく\nたこやき\nたさい\nたしざん\nだじゃれ\nたすける\nたずさわる\nたそがれ\nたたかう\nたたく\nただしい\nたたみ\nたちばな\nだっかい\nだっきゃく\nだっこ\nだっしゅつ\nだったい\nたてる\nたとえる\nたなばた\nたにん\nたぬき\nたのしみ\nたはつ\nたぶん\nたべる\nたぼう\nたまご\nたまる\nだむる\nためいき\nためす\nためる\nたもつ\nたやすい\nたよる\nたらす\nたりきほんがん\nたりょう\nたりる\nたると\nたれる\nたれんと\nたろっと\nたわむれる\nだんあつ\nたんい\nたんおん\nたんか\nたんき\nたんけん\nたんご\nたんさん\nたんじょうび\nだんせい\nたんそく\nたんたい\nだんち\nたんてい\nたんとう\nだんな\nたんにん\nだんねつ\nたんのう\nたんぴん\nだんぼう\nたんまつ\nたんめい\nだんれつ\nだんろ\nだんわ\nちあい\nちあん\nちいき\nちいさい\nちえん\nちかい\nちから\nちきゅう\nちきん\nちけいず\nちけん\nちこく\nちさい\nちしき\nちしりょう\nちせい\nちそう\nちたい\nちたん\nちちおや\nちつじょ\nちてき\nちてん\nちぬき\nちぬり\nちのう\nちひょう\nちへいせん\nちほう\nちまた\nちみつ\nちみどろ\nちめいど\nちゃんこなべ\nちゅうい\nちゆりょく\nちょうし\nちょさくけん\nちらし\nちらみ\nちりがみ\nちりょう\nちるど\nちわわ\nちんたい\nちんもく\nついか\nついたち\nつうか\nつうじょう\nつうはん\nつうわ\nつかう\nつかれる\nつくね\nつくる\nつけね\nつける\nつごう\nつたえる\nつづく\nつつじ\nつつむ\nつとめる\nつながる\nつなみ\nつねづね\nつのる\nつぶす\nつまらない\nつまる\nつみき\nつめたい\nつもり\nつもる\nつよい\nつるぼ\nつるみく\nつわもの\nつわり\nてあし\nてあて\nてあみ\nていおん\nていか\nていき\nていけい\nていこく\nていさつ\nていし\nていせい\nていたい\nていど\nていねい\nていひょう\nていへん\nていぼう\nてうち\nておくれ\nてきとう\nてくび\nでこぼこ\nてさぎょう\nてさげ\nてすり\nてそう\nてちがい\nてちょう\nてつがく\nてつづき\nでっぱ\nてつぼう\nてつや\nでぬかえ\nてぬき\nてぬぐい\nてのひら\nてはい\nてぶくろ\nてふだ\nてほどき\nてほん\nてまえ\nてまきずし\nてみじか\nてみやげ\nてらす\nてれび\nてわけ\nてわたし\nでんあつ\nてんいん\nてんかい\nてんき\nてんぐ\nてんけん\nてんごく\nてんさい\nてんし\nてんすう\nでんち\nてんてき\nてんとう\nてんない\nてんぷら\nてんぼうだい\nてんめつ\nてんらんかい\nでんりょく\nでんわ\nどあい\nといれ\nどうかん\nとうきゅう\nどうぐ\nとうし\nとうむぎ\nとおい\nとおか\nとおく\nとおす\nとおる\nとかい\nとかす\nときおり\nときどき\nとくい\nとくしゅう\nとくてん\nとくに\nとくべつ\nとけい\nとける\nとこや\nとさか\nとしょかん\nとそう\nとたん\nとちゅう\nとっきゅう\nとっくん\nとつぜん\nとつにゅう\nとどける\nととのえる\nとない\nとなえる\nとなり\nとのさま\nとばす\nどぶがわ\nとほう\nとまる\nとめる\nともだち\nともる\nどようび\nとらえる\nとんかつ\nどんぶり\nないかく\nないこう\nないしょ\nないす\nないせん\nないそう\nなおす\nながい\nなくす\nなげる\nなこうど\nなさけ\nなたでここ\nなっとう\nなつやすみ\nななおし\nなにごと\nなにもの\nなにわ\nなのか\nなふだ\nなまいき\nなまえ\nなまみ\nなみだ\nなめらか\nなめる\nなやむ\nならう\nならび\nならぶ\nなれる\nなわとび\nなわばり\nにあう\nにいがた\nにうけ\nにおい\nにかい\nにがて\nにきび\nにくしみ\nにくまん\nにげる\nにさんかたんそ\nにしき\nにせもの\nにちじょう\nにちようび\nにっか\nにっき\nにっけい\nにっこう\nにっさん\nにっしょく\nにっすう\nにっせき\nにってい\nになう\nにほん\nにまめ\nにもつ\nにやり\nにゅういん\nにりんしゃ\nにわとり\nにんい\nにんか\nにんき\nにんげん\nにんしき\nにんずう\nにんそう\nにんたい\nにんち\nにんてい\nにんにく\nにんぷ\nにんまり\nにんむ\nにんめい\nにんよう\nぬいくぎ\nぬかす\nぬぐいとる\nぬぐう\nぬくもり\nぬすむ\nぬまえび\nぬめり\nぬらす\nぬんちゃく\nねあげ\nねいき\nねいる\nねいろ\nねぐせ\nねくたい\nねくら\nねこぜ\nねこむ\nねさげ\nねすごす\nねそべる\nねだん\nねつい\nねっしん\nねつぞう\nねったいぎょ\nねぶそく\nねふだ\nねぼう\nねほりはほり\nねまき\nねまわし\nねみみ\nねむい\nねむたい\nねもと\nねらう\nねわざ\nねんいり\nねんおし\nねんかん\nねんきん\nねんぐ\nねんざ\nねんし\nねんちゃく\nねんど\nねんぴ\nねんぶつ\nねんまつ\nねんりょう\nねんれい\nのいず\nのおづま\nのがす\nのきなみ\nのこぎり\nのこす\nのこる\nのせる\nのぞく\nのぞむ\nのたまう\nのちほど\nのっく\nのばす\nのはら\nのべる\nのぼる\nのみもの\nのやま\nのらいぬ\nのらねこ\nのりもの\nのりゆき\nのれん\nのんき\nばあい\nはあく\nばあさん\nばいか\nばいく\nはいけん\nはいご\nはいしん\nはいすい\nはいせん\nはいそう\nはいち\nばいばい\nはいれつ\nはえる\nはおる\nはかい\nばかり\nはかる\nはくしゅ\nはけん\nはこぶ\nはさみ\nはさん\nはしご\nばしょ\nはしる\nはせる\nぱそこん\nはそん\nはたん\nはちみつ\nはつおん\nはっかく\nはづき\nはっきり\nはっくつ\nはっけん\nはっこう\nはっさん\nはっしん\nはったつ\nはっちゅう\nはってん\nはっぴょう\nはっぽう\nはなす\nはなび\nはにかむ\nはぶらし\nはみがき\nはむかう\nはめつ\nはやい\nはやし\nはらう\nはろうぃん\nはわい\nはんい\nはんえい\nはんおん\nはんかく\nはんきょう\nばんぐみ\nはんこ\nはんしゃ\nはんすう\nはんだん\nぱんち\nぱんつ\nはんてい\nはんとし\nはんのう\nはんぱ\nはんぶん\nはんぺん\nはんぼうき\nはんめい\nはんらん\nはんろん\nひいき\nひうん\nひえる\nひかく\nひかり\nひかる\nひかん\nひくい\nひけつ\nひこうき\nひこく\nひさい\nひさしぶり\nひさん\nびじゅつかん\nひしょ\nひそか\nひそむ\nひたむき\nひだり\nひたる\nひつぎ\nひっこし\nひっし\nひつじゅひん\nひっす\nひつぜん\nぴったり\nぴっちり\nひつよう\nひてい\nひとごみ\nひなまつり\nひなん\nひねる\nひはん\nひびく\nひひょう\nひほう\nひまわり\nひまん\nひみつ\nひめい\nひめじし\nひやけ\nひやす\nひよう\nびょうき\nひらがな\nひらく\nひりつ\nひりょう\nひるま\nひるやすみ\nひれい\nひろい\nひろう\nひろき\nひろゆき\nひんかく\nひんけつ\nひんこん\nひんしゅ\nひんそう\nぴんち\nひんぱん\nびんぼう\nふあん\nふいうち\nふうけい\nふうせん\nぷうたろう\nふうとう\nふうふ\nふえる\nふおん\nふかい\nふきん\nふくざつ\nふくぶくろ\nふこう\nふさい\nふしぎ\nふじみ\nふすま\nふせい\nふせぐ\nふそく\nぶたにく\nふたん\nふちょう\nふつう\nふつか\nふっかつ\nふっき\nふっこく\nぶどう\nふとる\nふとん\nふのう\nふはい\nふひょう\nふへん\nふまん\nふみん\nふめつ\nふめん\nふよう\nふりこ\nふりる\nふるい\nふんいき\nぶんがく\nぶんぐ\nふんしつ\nぶんせき\nふんそう\nぶんぽう\nへいあん\nへいおん\nへいがい\nへいき\nへいげん\nへいこう\nへいさ\nへいしゃ\nへいせつ\nへいそ\nへいたく\nへいてん\nへいねつ\nへいわ\nへきが\nへこむ\nべにいろ\nべにしょうが\nへらす\nへんかん\nべんきょう\nべんごし\nへんさい\nへんたい\nべんり\nほあん\nほいく\nぼうぎょ\nほうこく\nほうそう\nほうほう\nほうもん\nほうりつ\nほえる\nほおん\nほかん\nほきょう\nぼきん\nほくろ\nほけつ\nほけん\nほこう\nほこる\nほしい\nほしつ\nほしゅ\nほしょう\nほせい\nほそい\nほそく\nほたて\nほたる\nぽちぶくろ\nほっきょく\nほっさ\nほったん\nほとんど\nほめる\nほんい\nほんき\nほんけ\nほんしつ\nほんやく\nまいにち\nまかい\nまかせる\nまがる\nまける\nまこと\nまさつ\nまじめ\nますく\nまぜる\nまつり\nまとめ\nまなぶ\nまぬけ\nまねく\nまほう\nまもる\nまゆげ\nまよう\nまろやか\nまわす\nまわり\nまわる\nまんが\nまんきつ\nまんぞく\nまんなか\nみいら\nみうち\nみえる\nみがく\nみかた\nみかん\nみけん\nみこん\nみじかい\nみすい\nみすえる\nみせる\nみっか\nみつかる\nみつける\nみてい\nみとめる\nみなと\nみなみかさい\nみねらる\nみのう\nみのがす\nみほん\nみもと\nみやげ\nみらい\nみりょく\nみわく\nみんか\nみんぞく\nむいか\nむえき\nむえん\nむかい\nむかう\nむかえ\nむかし\nむぎちゃ\nむける\nむげん\nむさぼる\nむしあつい\nむしば\nむじゅん\nむしろ\nむすう\nむすこ\nむすぶ\nむすめ\nむせる\nむせん\nむちゅう\nむなしい\nむのう\nむやみ\nむよう\nむらさき\nむりょう\nむろん\nめいあん\nめいうん\nめいえん\nめいかく\nめいきょく\nめいさい\nめいし\nめいそう\nめいぶつ\nめいれい\nめいわく\nめぐまれる\nめざす\nめした\nめずらしい\nめだつ\nめまい\nめやす\nめんきょ\nめんせき\nめんどう\nもうしあげる\nもうどうけん\nもえる\nもくし\nもくてき\nもくようび\nもちろん\nもどる\nもらう\nもんく\nもんだい\nやおや\nやける\nやさい\nやさしい\nやすい\nやすたろう\nやすみ\nやせる\nやそう\nやたい\nやちん\nやっと\nやっぱり\nやぶる\nやめる\nややこしい\nやよい\nやわらかい\nゆうき\nゆうびんきょく\nゆうべ\nゆうめい\nゆけつ\nゆしゅつ\nゆせん\nゆそう\nゆたか\nゆちゃく\nゆでる\nゆにゅう\nゆびわ\nゆらい\nゆれる\nようい\nようか\nようきゅう\nようじ\nようす\nようちえん\nよかぜ\nよかん\nよきん\nよくせい\nよくぼう\nよけい\nよごれる\nよさん\nよしゅう\nよそう\nよそく\nよっか\nよてい\nよどがわく\nよねつ\nよやく\nよゆう\nよろこぶ\nよろしい\nらいう\nらくがき\nらくご\nらくさつ\nらくだ\nらしんばん\nらせん\nらぞく\nらたい\nらっか\nられつ\nりえき\nりかい\nりきさく\nりきせつ\nりくぐん\nりくつ\nりけん\nりこう\nりせい\nりそう\nりそく\nりてん\nりねん\nりゆう\nりゅうがく\nりよう\nりょうり\nりょかん\nりょくちゃ\nりょこう\nりりく\nりれき\nりろん\nりんご\nるいけい\nるいさい\nるいじ\nるいせき\nるすばん\nるりがわら\nれいかん\nれいぎ\nれいせい\nれいぞうこ\nれいとう\nれいぼう\nれきし\nれきだい\nれんあい\nれんけい\nれんこん\nれんさい\nれんしゅう\nれんぞく\nれんらく\nろうか\nろうご\nろうじん\nろうそく\nろくが\nろこつ\nろじうら\nろしゅつ\nろせん\nろてん\nろめん\nろれつ\nろんぎ\nろんぱ\nろんぶん\nろんり\nわかす\nわかめ\nわかやま\nわかれる\nわしつ\nわじまし\nわすれもの\nわらう\nわれる\n"); - dico.Add("spanish", - "ábaco\nabdomen\nabeja\nabierto\nabogado\nabono\naborto\nabrazo\nabrir\nabuelo\nabuso\nacabar\nacademia\nacceso\nacción\naceite\nacelga\nacento\naceptar\nácido\naclarar\nacné\nacoger\nacoso\nactivo\nacto\nactriz\nactuar\nacudir\nacuerdo\nacusar\nadicto\nadmitir\nadoptar\nadorno\naduana\nadulto\naéreo\nafectar\nafición\nafinar\nafirmar\nágil\nagitar\nagonía\nagosto\nagotar\nagregar\nagrio\nagua\nagudo\náguila\naguja\nahogo\nahorro\naire\naislar\najedrez\najeno\najuste\nalacrán\nalambre\nalarma\nalba\nálbum\nalcalde\naldea\nalegre\nalejar\nalerta\naleta\nalfiler\nalga\nalgodón\naliado\naliento\nalivio\nalma\nalmeja\nalmíbar\naltar\nalteza\naltivo\nalto\naltura\nalumno\nalzar\namable\namante\namapola\namargo\namasar\námbar\námbito\nameno\namigo\namistad\namor\namparo\namplio\nancho\nanciano\nancla\nandar\nandén\nanemia\nángulo\nanillo\nánimo\nanís\nanotar\nantena\nantiguo\nantojo\nanual\nanular\nanuncio\nañadir\nañejo\naño\napagar\naparato\napetito\napio\naplicar\napodo\naporte\napoyo\naprender\naprobar\napuesta\napuro\narado\naraña\narar\nárbitro\nárbol\narbusto\narchivo\narco\narder\nardilla\narduo\nárea\nárido\naries\narmonía\narnés\naroma\narpa\narpón\narreglo\narroz\narruga\narte\nartista\nasa\nasado\nasalto\nascenso\nasegurar\naseo\nasesor\nasiento\nasilo\nasistir\nasno\nasombro\náspero\nastilla\nastro\nastuto\nasumir\nasunto\natajo\nataque\natar\natento\nateo\nático\natleta\nátomo\natraer\natroz\natún\naudaz\naudio\nauge\naula\naumento\nausente\nautor\naval\navance\navaro\nave\navellana\navena\navestruz\navión\naviso\nayer\nayuda\nayuno\nazafrán\nazar\nazote\nazúcar\nazufre\nazul\nbaba\nbabor\nbache\nbahía\nbaile\nbajar\nbalanza\nbalcón\nbalde\nbambú\nbanco\nbanda\nbaño\nbarba\nbarco\nbarniz\nbarro\nbáscula\nbastón\nbasura\nbatalla\nbatería\nbatir\nbatuta\nbaúl\nbazar\nbebé\nbebida\nbello\nbesar\nbeso\nbestia\nbicho\nbien\nbingo\nblanco\nbloque\nblusa\nboa\nbobina\nbobo\nboca\nbocina\nboda\nbodega\nboina\nbola\nbolero\nbolsa\nbomba\nbondad\nbonito\nbono\nbonsái\nborde\nborrar\nbosque\nbote\nbotín\nbóveda\nbozal\nbravo\nbrazo\nbrecha\nbreve\nbrillo\nbrinco\nbrisa\nbroca\nbroma\nbronce\nbrote\nbruja\nbrusco\nbruto\nbuceo\nbucle\nbueno\nbuey\nbufanda\nbufón\nbúho\nbuitre\nbulto\nburbuja\nburla\nburro\nbuscar\nbutaca\nbuzón\ncaballo\ncabeza\ncabina\ncabra\ncacao\ncadáver\ncadena\ncaer\ncafé\ncaída\ncaimán\ncaja\ncajón\ncal\ncalamar\ncalcio\ncaldo\ncalidad\ncalle\ncalma\ncalor\ncalvo\ncama\ncambio\ncamello\ncamino\ncampo\ncáncer\ncandil\ncanela\ncanguro\ncanica\ncanto\ncaña\ncañón\ncaoba\ncaos\ncapaz\ncapitán\ncapote\ncaptar\ncapucha\ncara\ncarbón\ncárcel\ncareta\ncarga\ncariño\ncarne\ncarpeta\ncarro\ncarta\ncasa\ncasco\ncasero\ncaspa\ncastor\ncatorce\ncatre\ncaudal\ncausa\ncazo\ncebolla\nceder\ncedro\ncelda\ncélebre\nceloso\ncélula\ncemento\nceniza\ncentro\ncerca\ncerdo\ncereza\ncero\ncerrar\ncerteza\ncésped\ncetro\nchacal\nchaleco\nchampú\nchancla\nchapa\ncharla\nchico\nchiste\nchivo\nchoque\nchoza\nchuleta\nchupar\nciclón\nciego\ncielo\ncien\ncierto\ncifra\ncigarro\ncima\ncinco\ncine\ncinta\nciprés\ncirco\nciruela\ncisne\ncita\nciudad\nclamor\nclan\nclaro\nclase\nclave\ncliente\nclima\nclínica\ncobre\ncocción\ncochino\ncocina\ncoco\ncódigo\ncodo\ncofre\ncoger\ncohete\ncojín\ncojo\ncola\ncolcha\ncolegio\ncolgar\ncolina\ncollar\ncolmo\ncolumna\ncombate\ncomer\ncomida\ncómodo\ncompra\nconde\nconejo\nconga\nconocer\nconsejo\ncontar\ncopa\ncopia\ncorazón\ncorbata\ncorcho\ncordón\ncorona\ncorrer\ncoser\ncosmos\ncosta\ncráneo\ncráter\ncrear\ncrecer\ncreído\ncrema\ncría\ncrimen\ncripta\ncrisis\ncromo\ncrónica\ncroqueta\ncrudo\ncruz\ncuadro\ncuarto\ncuatro\ncubo\ncubrir\ncuchara\ncuello\ncuento\ncuerda\ncuesta\ncueva\ncuidar\nculebra\nculpa\nculto\ncumbre\ncumplir\ncuna\ncuneta\ncuota\ncupón\ncúpula\ncurar\ncurioso\ncurso\ncurva\ncutis\ndama\ndanza\ndar\ndardo\ndátil\ndeber\ndébil\ndécada\ndecir\ndedo\ndefensa\ndefinir\ndejar\ndelfín\ndelgado\ndelito\ndemora\ndenso\ndental\ndeporte\nderecho\nderrota\ndesayuno\ndeseo\ndesfile\ndesnudo\ndestino\ndesvío\ndetalle\ndetener\ndeuda\ndía\ndiablo\ndiadema\ndiamante\ndiana\ndiario\ndibujo\ndictar\ndiente\ndieta\ndiez\ndifícil\ndigno\ndilema\ndiluir\ndinero\ndirecto\ndirigir\ndisco\ndiseño\ndisfraz\ndiva\ndivino\ndoble\ndoce\ndolor\ndomingo\ndon\ndonar\ndorado\ndormir\ndorso\ndos\ndosis\ndragón\ndroga\nducha\nduda\nduelo\ndueño\ndulce\ndúo\nduque\ndurar\ndureza\nduro\nébano\nebrio\nechar\neco\necuador\nedad\nedición\nedificio\neditor\neducar\nefecto\neficaz\neje\nejemplo\nelefante\nelegir\nelemento\nelevar\nelipse\nélite\nelixir\nelogio\neludir\nembudo\nemitir\nemoción\nempate\nempeño\nempleo\nempresa\nenano\nencargo\nenchufe\nencía\nenemigo\nenero\nenfado\nenfermo\nengaño\nenigma\nenlace\nenorme\nenredo\nensayo\nenseñar\nentero\nentrar\nenvase\nenvío\népoca\nequipo\nerizo\nescala\nescena\nescolar\nescribir\nescudo\nesencia\nesfera\nesfuerzo\nespada\nespejo\nespía\nesposa\nespuma\nesquí\nestar\neste\nestilo\nestufa\netapa\neterno\nética\netnia\nevadir\nevaluar\nevento\nevitar\nexacto\nexamen\nexceso\nexcusa\nexento\nexigir\nexilio\nexistir\néxito\nexperto\nexplicar\nexponer\nextremo\nfábrica\nfábula\nfachada\nfácil\nfactor\nfaena\nfaja\nfalda\nfallo\nfalso\nfaltar\nfama\nfamilia\nfamoso\nfaraón\nfarmacia\nfarol\nfarsa\nfase\nfatiga\nfauna\nfavor\nfax\nfebrero\nfecha\nfeliz\nfeo\nferia\nferoz\nfértil\nfervor\nfestín\nfiable\nfianza\nfiar\nfibra\nficción\nficha\nfideo\nfiebre\nfiel\nfiera\nfiesta\nfigura\nfijar\nfijo\nfila\nfilete\nfilial\nfiltro\nfin\nfinca\nfingir\nfinito\nfirma\nflaco\nflauta\nflecha\nflor\nflota\nfluir\nflujo\nflúor\nfobia\nfoca\nfogata\nfogón\nfolio\nfolleto\nfondo\nforma\nforro\nfortuna\nforzar\nfosa\nfoto\nfracaso\nfrágil\nfranja\nfrase\nfraude\nfreír\nfreno\nfresa\nfrío\nfrito\nfruta\nfuego\nfuente\nfuerza\nfuga\nfumar\nfunción\nfunda\nfurgón\nfuria\nfusil\nfútbol\nfuturo\ngacela\ngafas\ngaita\ngajo\ngala\ngalería\ngallo\ngamba\nganar\ngancho\nganga\nganso\ngaraje\ngarza\ngasolina\ngastar\ngato\ngavilán\ngemelo\ngemir\ngen\ngénero\ngenio\ngente\ngeranio\ngerente\ngermen\ngesto\ngigante\ngimnasio\ngirar\ngiro\nglaciar\nglobo\ngloria\ngol\ngolfo\ngoloso\ngolpe\ngoma\ngordo\ngorila\ngorra\ngota\ngoteo\ngozar\ngrada\ngráfico\ngrano\ngrasa\ngratis\ngrave\ngrieta\ngrillo\ngripe\ngris\ngrito\ngrosor\ngrúa\ngrueso\ngrumo\ngrupo\nguante\nguapo\nguardia\nguerra\nguía\nguiño\nguion\nguiso\nguitarra\ngusano\ngustar\nhaber\nhábil\nhablar\nhacer\nhacha\nhada\nhallar\nhamaca\nharina\nhaz\nhazaña\nhebilla\nhebra\nhecho\nhelado\nhelio\nhembra\nherir\nhermano\nhéroe\nhervir\nhielo\nhierro\nhígado\nhigiene\nhijo\nhimno\nhistoria\nhocico\nhogar\nhoguera\nhoja\nhombre\nhongo\nhonor\nhonra\nhora\nhormiga\nhorno\nhostil\nhoyo\nhueco\nhuelga\nhuerta\nhueso\nhuevo\nhuida\nhuir\nhumano\nhúmedo\nhumilde\nhumo\nhundir\nhuracán\nhurto\nicono\nideal\nidioma\nídolo\niglesia\niglú\nigual\nilegal\nilusión\nimagen\nimán\nimitar\nimpar\nimperio\nimponer\nimpulso\nincapaz\níndice\ninerte\ninfiel\ninforme\ningenio\ninicio\ninmenso\ninmune\ninnato\ninsecto\ninstante\ninterés\níntimo\nintuir\ninútil\ninvierno\nira\niris\nironía\nisla\nislote\njabalí\njabón\njamón\njarabe\njardín\njarra\njaula\njazmín\njefe\njeringa\njinete\njornada\njoroba\njoven\njoya\njuerga\njueves\njuez\njugador\njugo\njuguete\njuicio\njunco\njungla\njunio\njuntar\njúpiter\njurar\njusto\njuvenil\njuzgar\nkilo\nkoala\nlabio\nlacio\nlacra\nlado\nladrón\nlagarto\nlágrima\nlaguna\nlaico\nlamer\nlámina\nlámpara\nlana\nlancha\nlangosta\nlanza\nlápiz\nlargo\nlarva\nlástima\nlata\nlátex\nlatir\nlaurel\nlavar\nlazo\nleal\nlección\nleche\nlector\nleer\nlegión\nlegumbre\nlejano\nlengua\nlento\nleña\nleón\nleopardo\nlesión\nletal\nletra\nleve\nleyenda\nlibertad\nlibro\nlicor\nlíder\nlidiar\nlienzo\nliga\nligero\nlima\nlímite\nlimón\nlimpio\nlince\nlindo\nlínea\nlingote\nlino\nlinterna\nlíquido\nliso\nlista\nlitera\nlitio\nlitro\nllaga\nllama\nllanto\nllave\nllegar\nllenar\nllevar\nllorar\nllover\nlluvia\nlobo\nloción\nloco\nlocura\nlógica\nlogro\nlombriz\nlomo\nlonja\nlote\nlucha\nlucir\nlugar\nlujo\nluna\nlunes\nlupa\nlustro\nluto\nluz\nmaceta\nmacho\nmadera\nmadre\nmaduro\nmaestro\nmafia\nmagia\nmago\nmaíz\nmaldad\nmaleta\nmalla\nmalo\nmamá\nmambo\nmamut\nmanco\nmando\nmanejar\nmanga\nmaniquí\nmanjar\nmano\nmanso\nmanta\nmañana\nmapa\nmáquina\nmar\nmarco\nmarea\nmarfil\nmargen\nmarido\nmármol\nmarrón\nmartes\nmarzo\nmasa\nmáscara\nmasivo\nmatar\nmateria\nmatiz\nmatriz\nmáximo\nmayor\nmazorca\nmecha\nmedalla\nmedio\nmédula\nmejilla\nmejor\nmelena\nmelón\nmemoria\nmenor\nmensaje\nmente\nmenú\nmercado\nmerengue\nmérito\nmes\nmesón\nmeta\nmeter\nmétodo\nmetro\nmezcla\nmiedo\nmiel\nmiembro\nmiga\nmil\nmilagro\nmilitar\nmillón\nmimo\nmina\nminero\nmínimo\nminuto\nmiope\nmirar\nmisa\nmiseria\nmisil\nmismo\nmitad\nmito\nmochila\nmoción\nmoda\nmodelo\nmoho\nmojar\nmolde\nmoler\nmolino\nmomento\nmomia\nmonarca\nmoneda\nmonja\nmonto\nmoño\nmorada\nmorder\nmoreno\nmorir\nmorro\nmorsa\nmortal\nmosca\nmostrar\nmotivo\nmover\nmóvil\nmozo\nmucho\nmudar\nmueble\nmuela\nmuerte\nmuestra\nmugre\nmujer\nmula\nmuleta\nmulta\nmundo\nmuñeca\nmural\nmuro\nmúsculo\nmuseo\nmusgo\nmúsica\nmuslo\nnácar\nnación\nnadar\nnaipe\nnaranja\nnariz\nnarrar\nnasal\nnatal\nnativo\nnatural\nnáusea\nnaval\nnave\nnavidad\nnecio\nnéctar\nnegar\nnegocio\nnegro\nneón\nnervio\nneto\nneutro\nnevar\nnevera\nnicho\nnido\nniebla\nnieto\nniñez\nniño\nnítido\nnivel\nnobleza\nnoche\nnómina\nnoria\nnorma\nnorte\nnota\nnoticia\nnovato\nnovela\nnovio\nnube\nnuca\nnúcleo\nnudillo\nnudo\nnuera\nnueve\nnuez\nnulo\nnúmero\nnutria\noasis\nobeso\nobispo\nobjeto\nobra\nobrero\nobservar\nobtener\nobvio\noca\nocaso\nocéano\nochenta\nocho\nocio\nocre\noctavo\noctubre\noculto\nocupar\nocurrir\nodiar\nodio\nodisea\noeste\nofensa\noferta\noficio\nofrecer\nogro\noído\noír\nojo\nola\noleada\nolfato\nolivo\nolla\nolmo\nolor\nolvido\nombligo\nonda\nonza\nopaco\nopción\nópera\nopinar\noponer\noptar\nóptica\nopuesto\noración\norador\noral\nórbita\norca\norden\noreja\nórgano\norgía\norgullo\noriente\norigen\norilla\noro\norquesta\noruga\nosadía\noscuro\nosezno\noso\nostra\notoño\notro\noveja\nóvulo\nóxido\noxígeno\noyente\nozono\npacto\npadre\npaella\npágina\npago\npaís\npájaro\npalabra\npalco\npaleta\npálido\npalma\npaloma\npalpar\npan\npanal\npánico\npantera\npañuelo\npapá\npapel\npapilla\npaquete\nparar\nparcela\npared\nparir\nparo\npárpado\nparque\npárrafo\nparte\npasar\npaseo\npasión\npaso\npasta\npata\npatio\npatria\npausa\npauta\npavo\npayaso\npeatón\npecado\npecera\npecho\npedal\npedir\npegar\npeine\npelar\npeldaño\npelea\npeligro\npellejo\npelo\npeluca\npena\npensar\npeñón\npeón\npeor\npepino\npequeño\npera\npercha\nperder\npereza\nperfil\nperico\nperla\npermiso\nperro\npersona\npesa\npesca\npésimo\npestaña\npétalo\npetróleo\npez\npezuña\npicar\npichón\npie\npiedra\npierna\npieza\npijama\npilar\npiloto\npimienta\npino\npintor\npinza\npiña\npiojo\npipa\npirata\npisar\npiscina\npiso\npista\npitón\npizca\nplaca\nplan\nplata\nplaya\nplaza\npleito\npleno\nplomo\npluma\nplural\npobre\npoco\npoder\npodio\npoema\npoesía\npoeta\npolen\npolicía\npollo\npolvo\npomada\npomelo\npomo\npompa\nponer\nporción\nportal\nposada\nposeer\nposible\nposte\npotencia\npotro\npozo\nprado\nprecoz\npregunta\npremio\nprensa\npreso\nprevio\nprimo\npríncipe\nprisión\nprivar\nproa\nprobar\nproceso\nproducto\nproeza\nprofesor\nprograma\nprole\npromesa\npronto\npropio\npróximo\nprueba\npúblico\npuchero\npudor\npueblo\npuerta\npuesto\npulga\npulir\npulmón\npulpo\npulso\npuma\npunto\npuñal\npuño\npupa\npupila\npuré\nquedar\nqueja\nquemar\nquerer\nqueso\nquieto\nquímica\nquince\nquitar\nrábano\nrabia\nrabo\nración\nradical\nraíz\nrama\nrampa\nrancho\nrango\nrapaz\nrápido\nrapto\nrasgo\nraspa\nrato\nrayo\nraza\nrazón\nreacción\nrealidad\nrebaño\nrebote\nrecaer\nreceta\nrechazo\nrecoger\nrecreo\nrecto\nrecurso\nred\nredondo\nreducir\nreflejo\nreforma\nrefrán\nrefugio\nregalo\nregir\nregla\nregreso\nrehén\nreino\nreír\nreja\nrelato\nrelevo\nrelieve\nrelleno\nreloj\nremar\nremedio\nremo\nrencor\nrendir\nrenta\nreparto\nrepetir\nreposo\nreptil\nres\nrescate\nresina\nrespeto\nresto\nresumen\nretiro\nretorno\nretrato\nreunir\nrevés\nrevista\nrey\nrezar\nrico\nriego\nrienda\nriesgo\nrifa\nrígido\nrigor\nrincón\nriñón\nrío\nriqueza\nrisa\nritmo\nrito\nrizo\nroble\nroce\nrociar\nrodar\nrodeo\nrodilla\nroer\nrojizo\nrojo\nromero\nromper\nron\nronco\nronda\nropa\nropero\nrosa\nrosca\nrostro\nrotar\nrubí\nrubor\nrudo\nrueda\nrugir\nruido\nruina\nruleta\nrulo\nrumbo\nrumor\nruptura\nruta\nrutina\nsábado\nsaber\nsabio\nsable\nsacar\nsagaz\nsagrado\nsala\nsaldo\nsalero\nsalir\nsalmón\nsalón\nsalsa\nsalto\nsalud\nsalvar\nsamba\nsanción\nsandía\nsanear\nsangre\nsanidad\nsano\nsanto\nsapo\nsaque\nsardina\nsartén\nsastre\nsatán\nsauna\nsaxofón\nsección\nseco\nsecreto\nsecta\nsed\nseguir\nseis\nsello\nselva\nsemana\nsemilla\nsenda\nsensor\nseñal\nseñor\nseparar\nsepia\nsequía\nser\nserie\nsermón\nservir\nsesenta\nsesión\nseta\nsetenta\nsevero\nsexo\nsexto\nsidra\nsiesta\nsiete\nsiglo\nsigno\nsílaba\nsilbar\nsilencio\nsilla\nsímbolo\nsimio\nsirena\nsistema\nsitio\nsituar\nsobre\nsocio\nsodio\nsol\nsolapa\nsoldado\nsoledad\nsólido\nsoltar\nsolución\nsombra\nsondeo\nsonido\nsonoro\nsonrisa\nsopa\nsoplar\nsoporte\nsordo\nsorpresa\nsorteo\nsostén\nsótano\nsuave\nsubir\nsuceso\nsudor\nsuegra\nsuelo\nsueño\nsuerte\nsufrir\nsujeto\nsultán\nsumar\nsuperar\nsuplir\nsuponer\nsupremo\nsur\nsurco\nsureño\nsurgir\nsusto\nsutil\ntabaco\ntabique\ntabla\ntabú\ntaco\ntacto\ntajo\ntalar\ntalco\ntalento\ntalla\ntalón\ntamaño\ntambor\ntango\ntanque\ntapa\ntapete\ntapia\ntapón\ntaquilla\ntarde\ntarea\ntarifa\ntarjeta\ntarot\ntarro\ntarta\ntatuaje\ntauro\ntaza\ntazón\nteatro\ntecho\ntecla\ntécnica\ntejado\ntejer\ntejido\ntela\nteléfono\ntema\ntemor\ntemplo\ntenaz\ntender\ntener\ntenis\ntenso\nteoría\nterapia\nterco\ntérmino\nternura\nterror\ntesis\ntesoro\ntestigo\ntetera\ntexto\ntez\ntibio\ntiburón\ntiempo\ntienda\ntierra\ntieso\ntigre\ntijera\ntilde\ntimbre\ntímido\ntimo\ntinta\ntío\ntípico\ntipo\ntira\ntirón\ntitán\ntítere\ntítulo\ntiza\ntoalla\ntobillo\ntocar\ntocino\ntodo\ntoga\ntoldo\ntomar\ntono\ntonto\ntopar\ntope\ntoque\ntórax\ntorero\ntormenta\ntorneo\ntoro\ntorpedo\ntorre\ntorso\ntortuga\ntos\ntosco\ntoser\ntóxico\ntrabajo\ntractor\ntraer\ntráfico\ntrago\ntraje\ntramo\ntrance\ntrato\ntrauma\ntrazar\ntrébol\ntregua\ntreinta\ntren\ntrepar\ntres\ntribu\ntrigo\ntripa\ntriste\ntriunfo\ntrofeo\ntrompa\ntronco\ntropa\ntrote\ntrozo\ntruco\ntrueno\ntrufa\ntubería\ntubo\ntuerto\ntumba\ntumor\ntúnel\ntúnica\nturbina\nturismo\nturno\ntutor\nubicar\núlcera\numbral\nunidad\nunir\nuniverso\nuno\nuntar\nuña\nurbano\nurbe\nurgente\nurna\nusar\nusuario\nútil\nutopía\nuva\nvaca\nvacío\nvacuna\nvagar\nvago\nvaina\nvajilla\nvale\nválido\nvalle\nvalor\nválvula\nvampiro\nvara\nvariar\nvarón\nvaso\nvecino\nvector\nvehículo\nveinte\nvejez\nvela\nvelero\nveloz\nvena\nvencer\nvenda\nveneno\nvengar\nvenir\nventa\nvenus\nver\nverano\nverbo\nverde\nvereda\nverja\nverso\nverter\nvía\nviaje\nvibrar\nvicio\nvíctima\nvida\nvídeo\nvidrio\nviejo\nviernes\nvigor\nvil\nvilla\nvinagre\nvino\nviñedo\nviolín\nviral\nvirgo\nvirtud\nvisor\nvíspera\nvista\nvitamina\nviudo\nvivaz\nvivero\nvivir\nvivo\nvolcán\nvolumen\nvolver\nvoraz\nvotar\nvoto\nvoz\nvuelo\nvulgar\nyacer\nyate\nyegua\nyema\nyerno\nyeso\nyodo\nyoga\nyogur\nzafiro\nzanja\nzapato\nzarza\nzona\nzorro\nzumo\nzurdo\n"); - dico.Add("french", - "abaisser\nabandon\nabdiquer\nabeille\nabolir\naborder\naboutir\naboyer\nabrasif\nabreuver\nabriter\nabroger\nabrupt\nabsence\nabsolu\nabsurde\nabusif\nabyssal\nacadémie\nacajou\nacarien\naccabler\naccepter\nacclamer\naccolade\naccroche\naccuser\nacerbe\nachat\nacheter\naciduler\nacier\nacompte\nacquérir\nacronyme\nacteur\nactif\nactuel\nadepte\nadéquat\nadhésif\nadjectif\nadjuger\nadmettre\nadmirer\nadopter\nadorer\nadoucir\nadresse\nadroit\nadulte\nadverbe\naérer\naéronef\naffaire\naffecter\naffiche\naffreux\naffubler\nagacer\nagencer\nagile\nagiter\nagrafer\nagréable\nagrume\naider\naiguille\nailier\naimable\naisance\najouter\najuster\nalarmer\nalchimie\nalerte\nalgèbre\nalgue\naliéner\naliment\nalléger\nalliage\nallouer\nallumer\nalourdir\nalpaga\naltesse\nalvéole\namateur\nambigu\nambre\naménager\namertume\namidon\namiral\namorcer\namour\namovible\namphibie\nampleur\namusant\nanalyse\nanaphore\nanarchie\nanatomie\nancien\nanéantir\nangle\nangoisse\nanguleux\nanimal\nannexer\nannonce\nannuel\nanodin\nanomalie\nanonyme\nanormal\nantenne\nantidote\nanxieux\napaiser\napéritif\naplanir\napologie\nappareil\nappeler\napporter\nappuyer\naquarium\naqueduc\narbitre\narbuste\nardeur\nardoise\nargent\narlequin\narmature\narmement\narmoire\narmure\narpenter\narracher\narriver\narroser\narsenic\nartériel\narticle\naspect\nasphalte\naspirer\nassaut\nasservir\nassiette\nassocier\nassurer\nasticot\nastre\nastuce\natelier\natome\natrium\natroce\nattaque\nattentif\nattirer\nattraper\naubaine\nauberge\naudace\naudible\naugurer\naurore\nautomne\nautruche\navaler\navancer\navarice\navenir\naverse\naveugle\naviateur\navide\navion\naviser\navoine\navouer\navril\naxial\naxiome\nbadge\nbafouer\nbagage\nbaguette\nbaignade\nbalancer\nbalcon\nbaleine\nbalisage\nbambin\nbancaire\nbandage\nbanlieue\nbannière\nbanquier\nbarbier\nbaril\nbaron\nbarque\nbarrage\nbassin\nbastion\nbataille\nbateau\nbatterie\nbaudrier\nbavarder\nbelette\nbélier\nbelote\nbénéfice\nberceau\nberger\nberline\nbermuda\nbesace\nbesogne\nbétail\nbeurre\nbiberon\nbicycle\nbidule\nbijou\nbilan\nbilingue\nbillard\nbinaire\nbiologie\nbiopsie\nbiotype\nbiscuit\nbison\nbistouri\nbitume\nbizarre\nblafard\nblague\nblanchir\nblessant\nblinder\nblond\nbloquer\nblouson\nbobard\nbobine\nboire\nboiser\nbolide\nbonbon\nbondir\nbonheur\nbonifier\nbonus\nbordure\nborne\nbotte\nboucle\nboueux\nbougie\nboulon\nbouquin\nbourse\nboussole\nboutique\nboxeur\nbranche\nbrasier\nbrave\nbrebis\nbrèche\nbreuvage\nbricoler\nbrigade\nbrillant\nbrioche\nbrique\nbrochure\nbroder\nbronzer\nbrousse\nbroyeur\nbrume\nbrusque\nbrutal\nbruyant\nbuffle\nbuisson\nbulletin\nbureau\nburin\nbustier\nbutiner\nbutoir\nbuvable\nbuvette\ncabanon\ncabine\ncachette\ncadeau\ncadre\ncaféine\ncaillou\ncaisson\ncalculer\ncalepin\ncalibre\ncalmer\ncalomnie\ncalvaire\ncamarade\ncaméra\ncamion\ncampagne\ncanal\ncaneton\ncanon\ncantine\ncanular\ncapable\ncaporal\ncaprice\ncapsule\ncapter\ncapuche\ncarabine\ncarbone\ncaresser\ncaribou\ncarnage\ncarotte\ncarreau\ncarton\ncascade\ncasier\ncasque\ncassure\ncauser\ncaution\ncavalier\ncaverne\ncaviar\ncédille\nceinture\ncéleste\ncellule\ncendrier\ncensurer\ncentral\ncercle\ncérébral\ncerise\ncerner\ncerveau\ncesser\nchagrin\nchaise\nchaleur\nchambre\nchance\nchapitre\ncharbon\nchasseur\nchaton\nchausson\nchavirer\nchemise\nchenille\nchéquier\nchercher\ncheval\nchien\nchiffre\nchignon\nchimère\nchiot\nchlorure\nchocolat\nchoisir\nchose\nchouette\nchrome\nchute\ncigare\ncigogne\ncimenter\ncinéma\ncintrer\ncirculer\ncirer\ncirque\nciterne\ncitoyen\ncitron\ncivil\nclairon\nclameur\nclaquer\nclasse\nclavier\nclient\ncligner\nclimat\nclivage\ncloche\nclonage\ncloporte\ncobalt\ncobra\ncocasse\ncocotier\ncoder\ncodifier\ncoffre\ncogner\ncohésion\ncoiffer\ncoincer\ncolère\ncolibri\ncolline\ncolmater\ncolonel\ncombat\ncomédie\ncommande\ncompact\nconcert\nconduire\nconfier\ncongeler\nconnoter\nconsonne\ncontact\nconvexe\ncopain\ncopie\ncorail\ncorbeau\ncordage\ncorniche\ncorpus\ncorrect\ncortège\ncosmique\ncostume\ncoton\ncoude\ncoupure\ncourage\ncouteau\ncouvrir\ncoyote\ncrabe\ncrainte\ncravate\ncrayon\ncréature\ncréditer\ncrémeux\ncreuser\ncrevette\ncribler\ncrier\ncristal\ncritère\ncroire\ncroquer\ncrotale\ncrucial\ncruel\ncrypter\ncubique\ncueillir\ncuillère\ncuisine\ncuivre\nculminer\ncultiver\ncumuler\ncupide\ncuratif\ncurseur\ncyanure\ncycle\ncylindre\ncynique\ndaigner\ndamier\ndanger\ndanseur\ndauphin\ndébattre\ndébiter\ndéborder\ndébrider\ndébutant\ndécaler\ndécembre\ndéchirer\ndécider\ndéclarer\ndécorer\ndécrire\ndécupler\ndédale\ndéductif\ndéesse\ndéfensif\ndéfiler\ndéfrayer\ndégager\ndégivrer\ndéglutir\ndégrafer\ndéjeuner\ndélice\ndéloger\ndemander\ndemeurer\ndémolir\ndénicher\ndénouer\ndentelle\ndénuder\ndépart\ndépenser\ndéphaser\ndéplacer\ndéposer\ndéranger\ndérober\ndésastre\ndescente\ndésert\ndésigner\ndésobéir\ndessiner\ndestrier\ndétacher\ndétester\ndétourer\ndétresse\ndevancer\ndevenir\ndeviner\ndevoir\ndiable\ndialogue\ndiamant\ndicter\ndifférer\ndigérer\ndigital\ndigne\ndiluer\ndimanche\ndiminuer\ndioxyde\ndirectif\ndiriger\ndiscuter\ndisposer\ndissiper\ndistance\ndivertir\ndiviser\ndocile\ndocteur\ndogme\ndoigt\ndomaine\ndomicile\ndompter\ndonateur\ndonjon\ndonner\ndopamine\ndortoir\ndorure\ndosage\ndoseur\ndossier\ndotation\ndouanier\ndouble\ndouceur\ndouter\ndoyen\ndragon\ndraper\ndresser\ndribbler\ndroiture\nduperie\nduplexe\ndurable\ndurcir\ndynastie\néblouir\nécarter\nécharpe\néchelle\néclairer\néclipse\néclore\nécluse\nécole\néconomie\nécorce\nécouter\nécraser\nécrémer\nécrivain\nécrou\nécume\nécureuil\nédifier\néduquer\neffacer\neffectif\neffigie\neffort\neffrayer\neffusion\négaliser\négarer\néjecter\nélaborer\nélargir\nélectron\nélégant\néléphant\nélève\néligible\nélitisme\néloge\nélucider\néluder\nemballer\nembellir\nembryon\némeraude\némission\nemmener\némotion\némouvoir\nempereur\nemployer\nemporter\nemprise\némulsion\nencadrer\nenchère\nenclave\nencoche\nendiguer\nendosser\nendroit\nenduire\nénergie\nenfance\nenfermer\nenfouir\nengager\nengin\nenglober\nénigme\nenjamber\nenjeu\nenlever\nennemi\nennuyeux\nenrichir\nenrobage\nenseigne\nentasser\nentendre\nentier\nentourer\nentraver\nénumérer\nenvahir\nenviable\nenvoyer\nenzyme\néolien\népaissir\népargne\népatant\népaule\népicerie\népidémie\népier\népilogue\népine\népisode\népitaphe\népoque\népreuve\néprouver\népuisant\néquerre\néquipe\nériger\nérosion\nerreur\néruption\nescalier\nespadon\nespèce\nespiègle\nespoir\nesprit\nesquiver\nessayer\nessence\nessieu\nessorer\nestime\nestomac\nestrade\nétagère\nétaler\nétanche\nétatique\néteindre\nétendoir\néternel\néthanol\néthique\nethnie\nétirer\nétoffer\nétoile\nétonnant\nétourdir\nétrange\nétroit\nétude\neuphorie\névaluer\névasion\néventail\névidence\néviter\névolutif\névoquer\nexact\nexagérer\nexaucer\nexceller\nexcitant\nexclusif\nexcuse\nexécuter\nexemple\nexercer\nexhaler\nexhorter\nexigence\nexiler\nexister\nexotique\nexpédier\nexplorer\nexposer\nexprimer\nexquis\nextensif\nextraire\nexulter\nfable\nfabuleux\nfacette\nfacile\nfacture\nfaiblir\nfalaise\nfameux\nfamille\nfarceur\nfarfelu\nfarine\nfarouche\nfasciner\nfatal\nfatigue\nfaucon\nfautif\nfaveur\nfavori\nfébrile\nféconder\nfédérer\nfélin\nfemme\nfémur\nfendoir\nféodal\nfermer\nféroce\nferveur\nfestival\nfeuille\nfeutre\nfévrier\nfiasco\nficeler\nfictif\nfidèle\nfigure\nfilature\nfiletage\nfilière\nfilleul\nfilmer\nfilou\nfiltrer\nfinancer\nfinir\nfiole\nfirme\nfissure\nfixer\nflairer\nflamme\nflasque\nflatteur\nfléau\nflèche\nfleur\nflexion\nflocon\nflore\nfluctuer\nfluide\nfluvial\nfolie\nfonderie\nfongible\nfontaine\nforcer\nforgeron\nformuler\nfortune\nfossile\nfoudre\nfougère\nfouiller\nfoulure\nfourmi\nfragile\nfraise\nfranchir\nfrapper\nfrayeur\nfrégate\nfreiner\nfrelon\nfrémir\nfrénésie\nfrère\nfriable\nfriction\nfrisson\nfrivole\nfroid\nfromage\nfrontal\nfrotter\nfruit\nfugitif\nfuite\nfureur\nfurieux\nfurtif\nfusion\nfutur\ngagner\ngalaxie\ngalerie\ngambader\ngarantir\ngardien\ngarnir\ngarrigue\ngazelle\ngazon\ngéant\ngélatine\ngélule\ngendarme\ngénéral\ngénie\ngenou\ngentil\ngéologie\ngéomètre\ngéranium\ngerme\ngestuel\ngeyser\ngibier\ngicler\ngirafe\ngivre\nglace\nglaive\nglisser\nglobe\ngloire\nglorieux\ngolfeur\ngomme\ngonfler\ngorge\ngorille\ngoudron\ngouffre\ngoulot\ngoupille\ngourmand\ngoutte\ngraduel\ngraffiti\ngraine\ngrand\ngrappin\ngratuit\ngravir\ngrenat\ngriffure\ngriller\ngrimper\ngrogner\ngronder\ngrotte\ngroupe\ngruger\ngrutier\ngruyère\nguépard\nguerrier\nguide\nguimauve\nguitare\ngustatif\ngymnaste\ngyrostat\nhabitude\nhachoir\nhalte\nhameau\nhangar\nhanneton\nharicot\nharmonie\nharpon\nhasard\nhélium\nhématome\nherbe\nhérisson\nhermine\nhéron\nhésiter\nheureux\nhiberner\nhibou\nhilarant\nhistoire\nhiver\nhomard\nhommage\nhomogène\nhonneur\nhonorer\nhonteux\nhorde\nhorizon\nhorloge\nhormone\nhorrible\nhouleux\nhousse\nhublot\nhuileux\nhumain\nhumble\nhumide\nhumour\nhurler\nhydromel\nhygiène\nhymne\nhypnose\nidylle\nignorer\niguane\nillicite\nillusion\nimage\nimbiber\nimiter\nimmense\nimmobile\nimmuable\nimpact\nimpérial\nimplorer\nimposer\nimprimer\nimputer\nincarner\nincendie\nincident\nincliner\nincolore\nindexer\nindice\ninductif\ninédit\nineptie\ninexact\ninfini\ninfliger\ninformer\ninfusion\ningérer\ninhaler\ninhiber\ninjecter\ninjure\ninnocent\ninoculer\ninonder\ninscrire\ninsecte\ninsigne\ninsolite\ninspirer\ninstinct\ninsulter\nintact\nintense\nintime\nintrigue\nintuitif\ninutile\ninvasion\ninventer\ninviter\ninvoquer\nironique\nirradier\nirréel\nirriter\nisoler\nivoire\nivresse\njaguar\njaillir\njambe\njanvier\njardin\njauger\njaune\njavelot\njetable\njeton\njeudi\njeunesse\njoindre\njoncher\njongler\njoueur\njouissif\njournal\njovial\njoyau\njoyeux\njubiler\njugement\njunior\njupon\njuriste\njustice\njuteux\njuvénile\nkayak\nkimono\nkiosque\nlabel\nlabial\nlabourer\nlacérer\nlactose\nlagune\nlaine\nlaisser\nlaitier\nlambeau\nlamelle\nlampe\nlanceur\nlangage\nlanterne\nlapin\nlargeur\nlarme\nlaurier\nlavabo\nlavoir\nlecture\nlégal\nléger\nlégume\nlessive\nlettre\nlevier\nlexique\nlézard\nliasse\nlibérer\nlibre\nlicence\nlicorne\nliège\nlièvre\nligature\nligoter\nligue\nlimer\nlimite\nlimonade\nlimpide\nlinéaire\nlingot\nlionceau\nliquide\nlisière\nlister\nlithium\nlitige\nlittoral\nlivreur\nlogique\nlointain\nloisir\nlombric\nloterie\nlouer\nlourd\nloutre\nlouve\nloyal\nlubie\nlucide\nlucratif\nlueur\nlugubre\nluisant\nlumière\nlunaire\nlundi\nluron\nlutter\nluxueux\nmachine\nmagasin\nmagenta\nmagique\nmaigre\nmaillon\nmaintien\nmairie\nmaison\nmajorer\nmalaxer\nmaléfice\nmalheur\nmalice\nmallette\nmammouth\nmandater\nmaniable\nmanquant\nmanteau\nmanuel\nmarathon\nmarbre\nmarchand\nmardi\nmaritime\nmarqueur\nmarron\nmarteler\nmascotte\nmassif\nmatériel\nmatière\nmatraque\nmaudire\nmaussade\nmauve\nmaximal\nméchant\nméconnu\nmédaille\nmédecin\nméditer\nméduse\nmeilleur\nmélange\nmélodie\nmembre\nmémoire\nmenacer\nmener\nmenhir\nmensonge\nmentor\nmercredi\nmérite\nmerle\nmessager\nmesure\nmétal\nmétéore\nméthode\nmétier\nmeuble\nmiauler\nmicrobe\nmiette\nmignon\nmigrer\nmilieu\nmillion\nmimique\nmince\nminéral\nminimal\nminorer\nminute\nmiracle\nmiroiter\nmissile\nmixte\nmobile\nmoderne\nmoelleux\nmondial\nmoniteur\nmonnaie\nmonotone\nmonstre\nmontagne\nmonument\nmoqueur\nmorceau\nmorsure\nmortier\nmoteur\nmotif\nmouche\nmoufle\nmoulin\nmousson\nmouton\nmouvant\nmultiple\nmunition\nmuraille\nmurène\nmurmure\nmuscle\nmuséum\nmusicien\nmutation\nmuter\nmutuel\nmyriade\nmyrtille\nmystère\nmythique\nnageur\nnappe\nnarquois\nnarrer\nnatation\nnation\nnature\nnaufrage\nnautique\nnavire\nnébuleux\nnectar\nnéfaste\nnégation\nnégliger\nnégocier\nneige\nnerveux\nnettoyer\nneurone\nneutron\nneveu\nniche\nnickel\nnitrate\nniveau\nnoble\nnocif\nnocturne\nnoirceur\nnoisette\nnomade\nnombreux\nnommer\nnormatif\nnotable\nnotifier\nnotoire\nnourrir\nnouveau\nnovateur\nnovembre\nnovice\nnuage\nnuancer\nnuire\nnuisible\nnuméro\nnuptial\nnuque\nnutritif\nobéir\nobjectif\nobliger\nobscur\nobserver\nobstacle\nobtenir\nobturer\noccasion\noccuper\nocéan\noctobre\noctroyer\noctupler\noculaire\nodeur\nodorant\noffenser\nofficier\noffrir\nogive\noiseau\noisillon\nolfactif\nolivier\nombrage\nomettre\nonctueux\nonduler\nonéreux\nonirique\nopale\nopaque\nopérer\nopinion\nopportun\nopprimer\nopter\noptique\norageux\norange\norbite\nordonner\noreille\norgane\norgueil\norifice\nornement\norque\nortie\nosciller\nosmose\nossature\notarie\nouragan\nourson\noutil\noutrager\nouvrage\novation\noxyde\noxygène\nozone\npaisible\npalace\npalmarès\npalourde\npalper\npanache\npanda\npangolin\npaniquer\npanneau\npanorama\npantalon\npapaye\npapier\npapoter\npapyrus\nparadoxe\nparcelle\nparesse\nparfumer\nparler\nparole\nparrain\nparsemer\npartager\nparure\nparvenir\npassion\npastèque\npaternel\npatience\npatron\npavillon\npavoiser\npayer\npaysage\npeigne\npeintre\npelage\npélican\npelle\npelouse\npeluche\npendule\npénétrer\npénible\npensif\npénurie\npépite\npéplum\nperdrix\nperforer\npériode\npermuter\nperplexe\npersil\nperte\npeser\npétale\npetit\npétrir\npeuple\npharaon\nphobie\nphoque\nphoton\nphrase\nphysique\npiano\npictural\npièce\npierre\npieuvre\npilote\npinceau\npipette\npiquer\npirogue\npiscine\npiston\npivoter\npixel\npizza\nplacard\nplafond\nplaisir\nplaner\nplaque\nplastron\nplateau\npleurer\nplexus\npliage\nplomb\nplonger\npluie\nplumage\npochette\npoésie\npoète\npointe\npoirier\npoisson\npoivre\npolaire\npolicier\npollen\npolygone\npommade\npompier\nponctuel\npondérer\nponey\nportique\nposition\nposséder\nposture\npotager\npoteau\npotion\npouce\npoulain\npoumon\npourpre\npoussin\npouvoir\nprairie\npratique\nprécieux\nprédire\npréfixe\nprélude\nprénom\nprésence\nprétexte\nprévoir\nprimitif\nprince\nprison\npriver\nproblème\nprocéder\nprodige\nprofond\nprogrès\nproie\nprojeter\nprologue\npromener\npropre\nprospère\nprotéger\nprouesse\nproverbe\nprudence\npruneau\npsychose\npublic\npuceron\npuiser\npulpe\npulsar\npunaise\npunitif\npupitre\npurifier\npuzzle\npyramide\nquasar\nquerelle\nquestion\nquiétude\nquitter\nquotient\nracine\nraconter\nradieux\nragondin\nraideur\nraisin\nralentir\nrallonge\nramasser\nrapide\nrasage\nratisser\nravager\nravin\nrayonner\nréactif\nréagir\nréaliser\nréanimer\nrecevoir\nréciter\nréclamer\nrécolter\nrecruter\nreculer\nrecycler\nrédiger\nredouter\nrefaire\nréflexe\nréformer\nrefrain\nrefuge\nrégalien\nrégion\nréglage\nrégulier\nréitérer\nrejeter\nrejouer\nrelatif\nrelever\nrelief\nremarque\nremède\nremise\nremonter\nremplir\nremuer\nrenard\nrenfort\nrenifler\nrenoncer\nrentrer\nrenvoi\nreplier\nreporter\nreprise\nreptile\nrequin\nréserve\nrésineux\nrésoudre\nrespect\nrester\nrésultat\nrétablir\nretenir\nréticule\nretomber\nretracer\nréunion\nréussir\nrevanche\nrevivre\nrévolte\nrévulsif\nrichesse\nrideau\nrieur\nrigide\nrigoler\nrincer\nriposter\nrisible\nrisque\nrituel\nrival\nrivière\nrocheux\nromance\nrompre\nronce\nrondin\nroseau\nrosier\nrotatif\nrotor\nrotule\nrouge\nrouille\nrouleau\nroutine\nroyaume\nruban\nrubis\nruche\nruelle\nrugueux\nruiner\nruisseau\nruser\nrustique\nrythme\nsabler\nsaboter\nsabre\nsacoche\nsafari\nsagesse\nsaisir\nsalade\nsalive\nsalon\nsaluer\nsamedi\nsanction\nsanglier\nsarcasme\nsardine\nsaturer\nsaugrenu\nsaumon\nsauter\nsauvage\nsavant\nsavonner\nscalpel\nscandale\nscélérat\nscénario\nsceptre\nschéma\nscience\nscinder\nscore\nscrutin\nsculpter\nséance\nsécable\nsécher\nsecouer\nsécréter\nsédatif\nséduire\nseigneur\nséjour\nsélectif\nsemaine\nsembler\nsemence\nséminal\nsénateur\nsensible\nsentence\nséparer\nséquence\nserein\nsergent\nsérieux\nserrure\nsérum\nservice\nsésame\nsévir\nsevrage\nsextuple\nsidéral\nsiècle\nsiéger\nsiffler\nsigle\nsignal\nsilence\nsilicium\nsimple\nsincère\nsinistre\nsiphon\nsirop\nsismique\nsituer\nskier\nsocial\nsocle\nsodium\nsoigneux\nsoldat\nsoleil\nsolitude\nsoluble\nsombre\nsommeil\nsomnoler\nsonde\nsongeur\nsonnette\nsonore\nsorcier\nsortir\nsosie\nsottise\nsoucieux\nsoudure\nsouffle\nsoulever\nsoupape\nsource\nsoutirer\nsouvenir\nspacieux\nspatial\nspécial\nsphère\nspiral\nstable\nstation\nsternum\nstimulus\nstipuler\nstrict\nstudieux\nstupeur\nstyliste\nsublime\nsubstrat\nsubtil\nsubvenir\nsuccès\nsucre\nsuffixe\nsuggérer\nsuiveur\nsulfate\nsuperbe\nsupplier\nsurface\nsuricate\nsurmener\nsurprise\nsursaut\nsurvie\nsuspect\nsyllabe\nsymbole\nsymétrie\nsynapse\nsyntaxe\nsystème\ntabac\ntablier\ntactile\ntailler\ntalent\ntalisman\ntalonner\ntambour\ntamiser\ntangible\ntapis\ntaquiner\ntarder\ntarif\ntartine\ntasse\ntatami\ntatouage\ntaupe\ntaureau\ntaxer\ntémoin\ntemporel\ntenaille\ntendre\nteneur\ntenir\ntension\nterminer\nterne\nterrible\ntétine\ntexte\nthème\nthéorie\nthérapie\nthorax\ntibia\ntiède\ntimide\ntirelire\ntiroir\ntissu\ntitane\ntitre\ntituber\ntoboggan\ntolérant\ntomate\ntonique\ntonneau\ntoponyme\ntorche\ntordre\ntornade\ntorpille\ntorrent\ntorse\ntortue\ntotem\ntoucher\ntournage\ntousser\ntoxine\ntraction\ntrafic\ntragique\ntrahir\ntrain\ntrancher\ntravail\ntrèfle\ntremper\ntrésor\ntreuil\ntriage\ntribunal\ntricoter\ntrilogie\ntriomphe\ntripler\ntriturer\ntrivial\ntrombone\ntronc\ntropical\ntroupeau\ntuile\ntulipe\ntumulte\ntunnel\nturbine\ntuteur\ntutoyer\ntuyau\ntympan\ntyphon\ntypique\ntyran\nubuesque\nultime\nultrason\nunanime\nunifier\nunion\nunique\nunitaire\nunivers\nuranium\nurbain\nurticant\nusage\nusine\nusuel\nusure\nutile\nutopie\nvacarme\nvaccin\nvagabond\nvague\nvaillant\nvaincre\nvaisseau\nvalable\nvalise\nvallon\nvalve\nvampire\nvanille\nvapeur\nvarier\nvaseux\nvassal\nvaste\nvecteur\nvedette\nvégétal\nvéhicule\nveinard\nvéloce\nvendredi\nvénérer\nvenger\nvenimeux\nventouse\nverdure\nvérin\nvernir\nverrou\nverser\nvertu\nveston\nvétéran\nvétuste\nvexant\nvexer\nviaduc\nviande\nvictoire\nvidange\nvidéo\nvignette\nvigueur\nvilain\nvillage\nvinaigre\nviolon\nvipère\nvirement\nvirtuose\nvirus\nvisage\nviseur\nvision\nvisqueux\nvisuel\nvital\nvitesse\nviticole\nvitrine\nvivace\nvivipare\nvocation\nvoguer\nvoile\nvoisin\nvoiture\nvolaille\nvolcan\nvoltiger\nvolume\nvorace\nvortex\nvoter\nvouloir\nvoyage\nvoyelle\nwagon\nxénon\nyacht\nzèbre\nzénith\nzeste\nzoologie"); - dico.Add("portuguese_brazil", - "abacate\nabaixo\nabalar\nabater\nabduzir\nabelha\naberto\nabismo\nabotoar\nabranger\nabreviar\nabrigar\nabrupto\nabsinto\nabsoluto\nabsurdo\nabutre\nacabado\nacalmar\nacampar\nacanhar\nacaso\naceitar\nacelerar\nacenar\nacervo\nacessar\nacetona\nachatar\nacidez\nacima\nacionado\nacirrar\naclamar\naclive\nacolhida\nacomodar\nacoplar\nacordar\nacumular\nacusador\nadaptar\nadega\nadentro\nadepto\nadequar\naderente\nadesivo\nadeus\nadiante\naditivo\nadjetivo\nadjunto\nadmirar\nadorar\nadquirir\nadubo\nadverso\nadvogado\naeronave\nafastar\naferir\nafetivo\nafinador\nafivelar\naflito\nafluente\nafrontar\nagachar\nagarrar\nagasalho\nagenciar\nagilizar\nagiota\nagitado\nagora\nagradar\nagreste\nagrupar\naguardar\nagulha\najoelhar\najudar\najustar\nalameda\nalarme\nalastrar\nalavanca\nalbergue\nalbino\nalcatra\naldeia\nalecrim\nalegria\nalertar\nalface\nalfinete\nalgum\nalheio\naliar\nalicate\nalienar\nalinhar\naliviar\nalmofada\nalocar\nalpiste\nalterar\naltitude\nalucinar\nalugar\naluno\nalusivo\nalvo\namaciar\namador\namarelo\namassar\nambas\nambiente\nameixa\namenizar\namido\namistoso\namizade\namolador\namontoar\namoroso\namostra\namparar\nampliar\nampola\nanagrama\nanalisar\nanarquia\nanatomia\nandaime\nanel\nanexo\nangular\nanimar\nanjo\nanomalia\nanotado\nansioso\nanterior\nanuidade\nanunciar\nanzol\napagador\napalpar\napanhado\napego\napelido\napertada\napesar\napetite\napito\naplauso\naplicada\napoio\napontar\naposta\naprendiz\naprovar\naquecer\narame\naranha\narara\narcada\nardente\nareia\narejar\narenito\naresta\nargiloso\nargola\narma\narquivo\narraial\narrebate\narriscar\narroba\narrumar\narsenal\narterial\nartigo\narvoredo\nasfaltar\nasilado\naspirar\nassador\nassinar\nassoalho\nassunto\nastral\natacado\natadura\natalho\natarefar\natear\natender\naterro\nateu\natingir\natirador\nativo\natoleiro\natracar\natrevido\natriz\natual\natum\nauditor\naumentar\naura\naurora\nautismo\nautoria\nautuar\navaliar\navante\navaria\navental\navesso\naviador\navisar\navulso\naxila\nazarar\nazedo\nazeite\nazulejo\nbabar\nbabosa\nbacalhau\nbacharel\nbacia\nbagagem\nbaiano\nbailar\nbaioneta\nbairro\nbaixista\nbajular\nbaleia\nbaliza\nbalsa\nbanal\nbandeira\nbanho\nbanir\nbanquete\nbarato\nbarbado\nbaronesa\nbarraca\nbarulho\nbaseado\nbastante\nbatata\nbatedor\nbatida\nbatom\nbatucar\nbaunilha\nbeber\nbeijo\nbeirada\nbeisebol\nbeldade\nbeleza\nbelga\nbeliscar\nbendito\nbengala\nbenzer\nberimbau\nberlinda\nberro\nbesouro\nbexiga\nbezerro\nbico\nbicudo\nbienal\nbifocal\nbifurcar\nbigorna\nbilhete\nbimestre\nbimotor\nbiologia\nbiombo\nbiosfera\nbipolar\nbirrento\nbiscoito\nbisneto\nbispo\nbissexto\nbitola\nbizarro\nblindado\nbloco\nbloquear\nboato\nbobagem\nbocado\nbocejo\nbochecha\nboicotar\nbolada\nboletim\nbolha\nbolo\nbombeiro\nbonde\nboneco\nbonita\nborbulha\nborda\nboreal\nborracha\nbovino\nboxeador\nbranco\nbrasa\nbraveza\nbreu\nbriga\nbrilho\nbrincar\nbroa\nbrochura\nbronzear\nbroto\nbruxo\nbucha\nbudismo\nbufar\nbule\nburaco\nbusca\nbusto\nbuzina\ncabana\ncabelo\ncabide\ncabo\ncabrito\ncacau\ncacetada\ncachorro\ncacique\ncadastro\ncadeado\ncafezal\ncaiaque\ncaipira\ncaixote\ncajado\ncaju\ncalafrio\ncalcular\ncaldeira\ncalibrar\ncalmante\ncalota\ncamada\ncambista\ncamisa\ncamomila\ncampanha\ncamuflar\ncanavial\ncancelar\ncaneta\ncanguru\ncanhoto\ncanivete\ncanoa\ncansado\ncantar\ncanudo\ncapacho\ncapela\ncapinar\ncapotar\ncapricho\ncaptador\ncapuz\ncaracol\ncarbono\ncardeal\ncareca\ncarimbar\ncarneiro\ncarpete\ncarreira\ncartaz\ncarvalho\ncasaco\ncasca\ncasebre\ncastelo\ncasulo\ncatarata\ncativar\ncaule\ncausador\ncautelar\ncavalo\ncaverna\ncebola\ncedilha\ncegonha\ncelebrar\ncelular\ncenoura\ncenso\ncenteio\ncercar\ncerrado\ncerteiro\ncerveja\ncetim\ncevada\nchacota\nchaleira\nchamado\nchapada\ncharme\nchatice\nchave\nchefe\nchegada\ncheiro\ncheque\nchicote\nchifre\nchinelo\nchocalho\nchover\nchumbo\nchutar\nchuva\ncicatriz\nciclone\ncidade\ncidreira\nciente\ncigana\ncimento\ncinto\ncinza\nciranda\ncircuito\ncirurgia\ncitar\nclareza\nclero\nclicar\nclone\nclube\ncoado\ncoagir\ncobaia\ncobertor\ncobrar\ncocada\ncoelho\ncoentro\ncoeso\ncogumelo\ncoibir\ncoifa\ncoiote\ncolar\ncoleira\ncolher\ncolidir\ncolmeia\ncolono\ncoluna\ncomando\ncombinar\ncomentar\ncomitiva\ncomover\ncomplexo\ncomum\nconcha\ncondor\nconectar\nconfuso\ncongelar\nconhecer\nconjugar\nconsumir\ncontrato\nconvite\ncooperar\ncopeiro\ncopiador\ncopo\ncoquetel\ncoragem\ncordial\ncorneta\ncoronha\ncorporal\ncorreio\ncortejo\ncoruja\ncorvo\ncosseno\ncostela\ncotonete\ncouro\ncouve\ncovil\ncozinha\ncratera\ncravo\ncreche\ncredor\ncreme\ncrer\ncrespo\ncriada\ncriminal\ncrioulo\ncrise\ncriticar\ncrosta\ncrua\ncruzeiro\ncubano\ncueca\ncuidado\ncujo\nculatra\nculminar\nculpar\ncultura\ncumprir\ncunhado\ncupido\ncurativo\ncurral\ncursar\ncurto\ncuspir\ncustear\ncutelo\ndamasco\ndatar\ndebater\ndebitar\ndeboche\ndebulhar\ndecalque\ndecimal\ndeclive\ndecote\ndecretar\ndedal\ndedicado\ndeduzir\ndefesa\ndefumar\ndegelo\ndegrau\ndegustar\ndeitado\ndeixar\ndelator\ndelegado\ndelinear\ndelonga\ndemanda\ndemitir\ndemolido\ndentista\ndepenado\ndepilar\ndepois\ndepressa\ndepurar\nderiva\nderramar\ndesafio\ndesbotar\ndescanso\ndesenho\ndesfiado\ndesgaste\ndesigual\ndeslize\ndesmamar\ndesova\ndespesa\ndestaque\ndesviar\ndetalhar\ndetentor\ndetonar\ndetrito\ndeusa\ndever\ndevido\ndevotado\ndezena\ndiagrama\ndialeto\ndidata\ndifuso\ndigitar\ndilatado\ndiluente\ndiminuir\ndinastia\ndinheiro\ndiocese\ndireto\ndiscreta\ndisfarce\ndisparo\ndisquete\ndissipar\ndistante\nditador\ndiurno\ndiverso\ndivisor\ndivulgar\ndizer\ndobrador\ndolorido\ndomador\ndominado\ndonativo\ndonzela\ndormente\ndorsal\ndosagem\ndourado\ndoutor\ndrenagem\ndrible\ndrogaria\nduelar\nduende\ndueto\nduplo\nduquesa\ndurante\nduvidoso\neclodir\necoar\necologia\nedificar\nedital\neducado\nefeito\nefetivar\nejetar\nelaborar\neleger\neleitor\nelenco\nelevador\neliminar\nelogiar\nembargo\nembolado\nembrulho\nembutido\nemenda\nemergir\nemissor\nempatia\nempenho\nempinado\nempolgar\nemprego\nempurrar\nemulador\nencaixe\nencenado\nenchente\nencontro\nendeusar\nendossar\nenfaixar\nenfeite\nenfim\nengajado\nengenho\nenglobar\nengomado\nengraxar\nenguia\nenjoar\nenlatar\nenquanto\nenraizar\nenrolado\nenrugar\nensaio\nenseada\nensino\nensopado\nentanto\nenteado\nentidade\nentortar\nentrada\nentulho\nenvergar\nenviado\nenvolver\nenxame\nenxerto\nenxofre\nenxuto\nepiderme\nequipar\nereto\nerguido\nerrata\nerva\nervilha\nesbanjar\nesbelto\nescama\nescola\nescrita\nescuta\nesfinge\nesfolar\nesfregar\nesfumado\nesgrima\nesmalte\nespanto\nespelho\nespiga\nesponja\nespreita\nespumar\nesquerda\nestaca\nesteira\nesticar\nestofado\nestrela\nestudo\nesvaziar\netanol\netiqueta\neuforia\neuropeu\nevacuar\nevaporar\nevasivo\neventual\nevidente\nevoluir\nexagero\nexalar\nexaminar\nexato\nexausto\nexcesso\nexcitar\nexclamar\nexecutar\nexemplo\nexibir\nexigente\nexonerar\nexpandir\nexpelir\nexpirar\nexplanar\nexposto\nexpresso\nexpulsar\nexterno\nextinto\nextrato\nfabricar\nfabuloso\nfaceta\nfacial\nfada\nfadiga\nfaixa\nfalar\nfalta\nfamiliar\nfandango\nfanfarra\nfantoche\nfardado\nfarelo\nfarinha\nfarofa\nfarpa\nfartura\nfatia\nfator\nfavorita\nfaxina\nfazenda\nfechado\nfeijoada\nfeirante\nfelino\nfeminino\nfenda\nfeno\nfera\nferiado\nferrugem\nferver\nfestejar\nfetal\nfeudal\nfiapo\nfibrose\nficar\nficheiro\nfigurado\nfileira\nfilho\nfilme\nfiltrar\nfirmeza\nfisgada\nfissura\nfita\nfivela\nfixador\nfixo\nflacidez\nflamingo\nflanela\nflechada\nflora\nflutuar\nfluxo\nfocal\nfocinho\nfofocar\nfogo\nfoguete\nfoice\nfolgado\nfolheto\nforjar\nformiga\nforno\nforte\nfosco\nfossa\nfragata\nfralda\nfrango\nfrasco\nfraterno\nfreira\nfrente\nfretar\nfrieza\nfriso\nfritura\nfronha\nfrustrar\nfruteira\nfugir\nfulano\nfuligem\nfundar\nfungo\nfunil\nfurador\nfurioso\nfutebol\ngabarito\ngabinete\ngado\ngaiato\ngaiola\ngaivota\ngalega\ngalho\ngalinha\ngalocha\nganhar\ngaragem\ngarfo\ngargalo\ngarimpo\ngaroupa\ngarrafa\ngasoduto\ngasto\ngata\ngatilho\ngaveta\ngazela\ngelado\ngeleia\ngelo\ngemada\ngemer\ngemido\ngeneroso\ngengiva\ngenial\ngenoma\ngenro\ngeologia\ngerador\ngerminar\ngesso\ngestor\nginasta\ngincana\ngingado\ngirafa\ngirino\nglacial\nglicose\nglobal\nglorioso\ngoela\ngoiaba\ngolfe\ngolpear\ngordura\ngorjeta\ngorro\ngostoso\ngoteira\ngovernar\ngracejo\ngradual\ngrafite\ngralha\ngrampo\ngranada\ngratuito\ngraveto\ngraxa\ngrego\ngrelhar\ngreve\ngrilo\ngrisalho\ngritaria\ngrosso\ngrotesco\ngrudado\ngrunhido\ngruta\nguache\nguarani\nguaxinim\nguerrear\nguiar\nguincho\nguisado\ngula\nguloso\nguru\nhabitar\nharmonia\nhaste\nhaver\nhectare\nherdar\nheresia\nhesitar\nhiato\nhibernar\nhidratar\nhiena\nhino\nhipismo\nhipnose\nhipoteca\nhoje\nholofote\nhomem\nhonesto\nhonrado\nhormonal\nhospedar\nhumorado\niate\nideia\nidoso\nignorado\nigreja\niguana\nileso\nilha\niludido\niluminar\nilustrar\nimagem\nimediato\nimenso\nimersivo\niminente\nimitador\nimortal\nimpacto\nimpedir\nimplante\nimpor\nimprensa\nimpune\nimunizar\ninalador\ninapto\ninativo\nincenso\ninchar\nincidir\nincluir\nincolor\nindeciso\nindireto\nindutor\nineficaz\ninerente\ninfantil\ninfestar\ninfinito\ninflamar\ninformal\ninfrator\ningerir\ninibido\ninicial\ninimigo\ninjetar\ninocente\ninodoro\ninovador\ninox\ninquieto\ninscrito\ninseto\ninsistir\ninspetor\ninstalar\ninsulto\nintacto\nintegral\nintimar\nintocado\nintriga\ninvasor\ninverno\ninvicto\ninvocar\niogurte\niraniano\nironizar\nirreal\nirritado\nisca\nisento\nisolado\nisqueiro\nitaliano\njaneiro\njangada\njanta\njararaca\njardim\njarro\njasmim\njato\njavali\njazida\njejum\njoaninha\njoelhada\njogador\njoia\njornal\njorrar\njovem\njuba\njudeu\njudoca\njuiz\njulgador\njulho\njurado\njurista\njuro\njusta\nlabareda\nlaboral\nlacre\nlactante\nladrilho\nlagarta\nlagoa\nlaje\nlamber\nlamentar\nlaminar\nlampejo\nlanche\nlapidar\nlapso\nlaranja\nlareira\nlargura\nlasanha\nlastro\nlateral\nlatido\nlavanda\nlavoura\nlavrador\nlaxante\nlazer\nlealdade\nlebre\nlegado\nlegendar\nlegista\nleigo\nleiloar\nleitura\nlembrete\nleme\nlenhador\nlentilha\nleoa\nlesma\nleste\nletivo\nletreiro\nlevar\nleveza\nlevitar\nliberal\nlibido\nliderar\nligar\nligeiro\nlimitar\nlimoeiro\nlimpador\nlinda\nlinear\nlinhagem\nliquidez\nlistagem\nlisura\nlitoral\nlivro\nlixa\nlixeira\nlocador\nlocutor\nlojista\nlombo\nlona\nlonge\nlontra\nlorde\nlotado\nloteria\nloucura\nlousa\nlouvar\nluar\nlucidez\nlucro\nluneta\nlustre\nlutador\nluva\nmacaco\nmacete\nmachado\nmacio\nmadeira\nmadrinha\nmagnata\nmagreza\nmaior\nmais\nmalandro\nmalha\nmalote\nmaluco\nmamilo\nmamoeiro\nmamute\nmanada\nmancha\nmandato\nmanequim\nmanhoso\nmanivela\nmanobrar\nmansa\nmanter\nmanusear\nmapeado\nmaquinar\nmarcador\nmaresia\nmarfim\nmargem\nmarinho\nmarmita\nmaroto\nmarquise\nmarreco\nmartelo\nmarujo\nmascote\nmasmorra\nmassagem\nmastigar\nmatagal\nmaterno\nmatinal\nmatutar\nmaxilar\nmedalha\nmedida\nmedusa\nmegafone\nmeiga\nmelancia\nmelhor\nmembro\nmemorial\nmenino\nmenos\nmensagem\nmental\nmerecer\nmergulho\nmesada\nmesclar\nmesmo\nmesquita\nmestre\nmetade\nmeteoro\nmetragem\nmexer\nmexicano\nmicro\nmigalha\nmigrar\nmilagre\nmilenar\nmilhar\nmimado\nminerar\nminhoca\nministro\nminoria\nmiolo\nmirante\nmirtilo\nmisturar\nmocidade\nmoderno\nmodular\nmoeda\nmoer\nmoinho\nmoita\nmoldura\nmoleza\nmolho\nmolinete\nmolusco\nmontanha\nmoqueca\nmorango\nmorcego\nmordomo\nmorena\nmosaico\nmosquete\nmostarda\nmotel\nmotim\nmoto\nmotriz\nmuda\nmuito\nmulata\nmulher\nmultar\nmundial\nmunido\nmuralha\nmurcho\nmuscular\nmuseu\nmusical\nnacional\nnadador\nnaja\nnamoro\nnarina\nnarrado\nnascer\nnativa\nnatureza\nnavalha\nnavegar\nnavio\nneblina\nnebuloso\nnegativa\nnegociar\nnegrito\nnervoso\nneta\nneural\nnevasca\nnevoeiro\nninar\nninho\nnitidez\nnivelar\nnobreza\nnoite\nnoiva\nnomear\nnominal\nnordeste\nnortear\nnotar\nnoticiar\nnoturno\nnovelo\nnovilho\nnovo\nnublado\nnudez\nnumeral\nnupcial\nnutrir\nnuvem\nobcecado\nobedecer\nobjetivo\nobrigado\nobscuro\nobstetra\nobter\nobturar\nocidente\nocioso\nocorrer\noculista\nocupado\nofegante\nofensiva\noferenda\noficina\nofuscado\nogiva\nolaria\noleoso\nolhar\noliveira\nombro\nomelete\nomisso\nomitir\nondulado\noneroso\nontem\nopcional\noperador\noponente\noportuno\noposto\norar\norbitar\nordem\nordinal\norfanato\norgasmo\norgulho\noriental\norigem\noriundo\norla\nortodoxo\norvalho\noscilar\nossada\nosso\nostentar\notimismo\nousadia\noutono\noutubro\nouvido\novelha\novular\noxidar\noxigenar\npacato\npaciente\npacote\npactuar\npadaria\npadrinho\npagar\npagode\npainel\npairar\npaisagem\npalavra\npalestra\npalheta\npalito\npalmada\npalpitar\npancada\npanela\npanfleto\npanqueca\npantanal\npapagaio\npapelada\npapiro\nparafina\nparcial\npardal\nparede\npartida\npasmo\npassado\npastel\npatamar\npatente\npatinar\npatrono\npaulada\npausar\npeculiar\npedalar\npedestre\npediatra\npedra\npegada\npeitoral\npeixe\npele\npelicano\npenca\npendurar\npeneira\npenhasco\npensador\npente\nperceber\nperfeito\npergunta\nperito\npermitir\nperna\nperplexo\npersiana\npertence\nperuca\npescado\npesquisa\npessoa\npetiscar\npiada\npicado\npiedade\npigmento\npilastra\npilhado\npilotar\npimenta\npincel\npinguim\npinha\npinote\npintar\npioneiro\npipoca\npiquete\npiranha\npires\npirueta\npiscar\npistola\npitanga\npivete\nplanta\nplaqueta\nplatina\nplebeu\nplumagem\npluvial\npneu\npoda\npoeira\npoetisa\npolegada\npoliciar\npoluente\npolvilho\npomar\npomba\nponderar\npontaria\npopuloso\nporta\npossuir\npostal\npote\npoupar\npouso\npovoar\npraia\nprancha\nprato\npraxe\nprece\npredador\nprefeito\npremiar\nprensar\npreparar\npresilha\npretexto\nprevenir\nprezar\nprimata\nprincesa\nprisma\nprivado\nprocesso\nproduto\nprofeta\nproibido\nprojeto\nprometer\npropagar\nprosa\nprotetor\nprovador\npublicar\npudim\npular\npulmonar\npulseira\npunhal\npunir\npupilo\npureza\npuxador\nquadra\nquantia\nquarto\nquase\nquebrar\nqueda\nqueijo\nquente\nquerido\nquimono\nquina\nquiosque\nrabanada\nrabisco\nrachar\nracionar\nradial\nraiar\nrainha\nraio\nraiva\nrajada\nralado\nramal\nranger\nranhura\nrapadura\nrapel\nrapidez\nraposa\nraquete\nraridade\nrasante\nrascunho\nrasgar\nraspador\nrasteira\nrasurar\nratazana\nratoeira\nrealeza\nreanimar\nreaver\nrebaixar\nrebelde\nrebolar\nrecado\nrecente\nrecheio\nrecibo\nrecordar\nrecrutar\nrecuar\nrede\nredimir\nredonda\nreduzida\nreenvio\nrefinar\nrefletir\nrefogar\nrefresco\nrefugiar\nregalia\nregime\nregra\nreinado\nreitor\nrejeitar\nrelativo\nremador\nremendo\nremorso\nrenovado\nreparo\nrepelir\nrepleto\nrepolho\nrepresa\nrepudiar\nrequerer\nresenha\nresfriar\nresgatar\nresidir\nresolver\nrespeito\nressaca\nrestante\nresumir\nretalho\nreter\nretirar\nretomada\nretratar\nrevelar\nrevisor\nrevolta\nriacho\nrica\nrigidez\nrigoroso\nrimar\nringue\nrisada\nrisco\nrisonho\nrobalo\nrochedo\nrodada\nrodeio\nrodovia\nroedor\nroleta\nromano\nroncar\nrosado\nroseira\nrosto\nrota\nroteiro\nrotina\nrotular\nrouco\nroupa\nroxo\nrubro\nrugido\nrugoso\nruivo\nrumo\nrupestre\nrusso\nsabor\nsaciar\nsacola\nsacudir\nsadio\nsafira\nsaga\nsagrada\nsaibro\nsalada\nsaleiro\nsalgado\nsaliva\nsalpicar\nsalsicha\nsaltar\nsalvador\nsambar\nsamurai\nsanar\nsanfona\nsangue\nsanidade\nsapato\nsarda\nsargento\nsarjeta\nsaturar\nsaudade\nsaxofone\nsazonal\nsecar\nsecular\nseda\nsedento\nsediado\nsedoso\nsedutor\nsegmento\nsegredo\nsegundo\nseiva\nseleto\nselvagem\nsemanal\nsemente\nsenador\nsenhor\nsensual\nsentado\nseparado\nsereia\nseringa\nserra\nservo\nsetembro\nsetor\nsigilo\nsilhueta\nsilicone\nsimetria\nsimpatia\nsimular\nsinal\nsincero\nsingular\nsinopse\nsintonia\nsirene\nsiri\nsituado\nsoberano\nsobra\nsocorro\nsogro\nsoja\nsolda\nsoletrar\nsolteiro\nsombrio\nsonata\nsondar\nsonegar\nsonhador\nsono\nsoprano\nsoquete\nsorrir\nsorteio\nsossego\nsotaque\nsoterrar\nsovado\nsozinho\nsuavizar\nsubida\nsubmerso\nsubsolo\nsubtrair\nsucata\nsucesso\nsuco\nsudeste\nsufixo\nsugador\nsugerir\nsujeito\nsulfato\nsumir\nsuor\nsuperior\nsuplicar\nsuposto\nsuprimir\nsurdina\nsurfista\nsurpresa\nsurreal\nsurtir\nsuspiro\nsustento\ntabela\ntablete\ntabuada\ntacho\ntagarela\ntalher\ntalo\ntalvez\ntamanho\ntamborim\ntampa\ntangente\ntanto\ntapar\ntapioca\ntardio\ntarefa\ntarja\ntarraxa\ntatuagem\ntaurino\ntaxativo\ntaxista\nteatral\ntecer\ntecido\nteclado\ntedioso\nteia\nteimar\ntelefone\ntelhado\ntempero\ntenente\ntensor\ntentar\ntermal\nterno\nterreno\ntese\ntesoura\ntestado\nteto\ntextura\ntexugo\ntiara\ntigela\ntijolo\ntimbrar\ntimidez\ntingido\ntinteiro\ntiragem\ntitular\ntoalha\ntocha\ntolerar\ntolice\ntomada\ntomilho\ntonel\ntontura\ntopete\ntora\ntorcido\ntorneio\ntorque\ntorrada\ntorto\ntostar\ntouca\ntoupeira\ntoxina\ntrabalho\ntracejar\ntradutor\ntrafegar\ntrajeto\ntrama\ntrancar\ntrapo\ntraseiro\ntratador\ntravar\ntreino\ntremer\ntrepidar\ntrevo\ntriagem\ntribo\ntriciclo\ntridente\ntrilogia\ntrindade\ntriplo\ntriturar\ntriunfal\ntrocar\ntrombeta\ntrova\ntrunfo\ntruque\ntubular\ntucano\ntudo\ntulipa\ntupi\nturbo\nturma\nturquesa\ntutelar\ntutorial\nuivar\numbigo\nunha\nunidade\nuniforme\nurologia\nurso\nurtiga\nurubu\nusado\nusina\nusufruir\nvacina\nvadiar\nvagaroso\nvaidoso\nvala\nvalente\nvalidade\nvalores\nvantagem\nvaqueiro\nvaranda\nvareta\nvarrer\nvascular\nvasilha\nvassoura\nvazar\nvazio\nveado\nvedar\nvegetar\nveicular\nveleiro\nvelhice\nveludo\nvencedor\nvendaval\nvenerar\nventre\nverbal\nverdade\nvereador\nvergonha\nvermelho\nverniz\nversar\nvertente\nvespa\nvestido\nvetorial\nviaduto\nviagem\nviajar\nviatura\nvibrador\nvideira\nvidraria\nviela\nviga\nvigente\nvigiar\nvigorar\nvilarejo\nvinco\nvinheta\nvinil\nvioleta\nvirada\nvirtude\nvisitar\nvisto\nvitral\nviveiro\nvizinho\nvoador\nvoar\nvogal\nvolante\nvoleibol\nvoltagem\nvolumoso\nvontade\nvulto\nvuvuzela\nxadrez\nxarope\nxeque\nxeretar\nxerife\nxingar\nzangado\nzarpar\nzebu\nzelador\nzombar\nzoologia\nzumbido"); - dico.Add("czech", - "abdikace\nabeceda\nadresa\nagrese\nakce\naktovka\nalej\nalkohol\namputace\nananas\nandulka\nanekdota\nanketa\nantika\nanulovat\narcha\narogance\nasfalt\nasistent\naspirace\nastma\nastronom\natlas\natletika\natol\nautobus\nazyl\nbabka\nbachor\nbacil\nbaculka\nbadatel\nbageta\nbagr\nbahno\nbakterie\nbalada\nbaletka\nbalkon\nbalonek\nbalvan\nbalza\nbambus\nbankomat\nbarbar\nbaret\nbarman\nbaroko\nbarva\nbaterka\nbatoh\nbavlna\nbazalka\nbazilika\nbazuka\nbedna\nberan\nbeseda\nbestie\nbeton\nbezinka\nbezmoc\nbeztak\nbicykl\nbidlo\nbiftek\nbikiny\nbilance\nbiograf\nbiolog\nbitva\nbizon\nblahobyt\nblatouch\nblecha\nbledule\nblesk\nblikat\nblizna\nblokovat\nbloudit\nblud\nbobek\nbobr\nbodlina\nbodnout\nbohatost\nbojkot\nbojovat\nbokorys\nbolest\nborec\nborovice\nbota\nboubel\nbouchat\nbouda\nboule\nbourat\nboxer\nbradavka\nbrambora\nbranka\nbratr\nbrepta\nbriketa\nbrko\nbrloh\nbronz\nbroskev\nbrunetka\nbrusinka\nbrzda\nbrzy\nbublina\nbubnovat\nbuchta\nbuditel\nbudka\nbudova\nbufet\nbujarost\nbukvice\nbuldok\nbulva\nbunda\nbunkr\nburza\nbutik\nbuvol\nbuzola\nbydlet\nbylina\nbytovka\nbzukot\ncapart\ncarevna\ncedr\ncedule\ncejch\ncejn\ncela\nceler\ncelkem\ncelnice\ncenina\ncennost\ncenovka\ncentrum\ncenzor\ncestopis\ncetka\nchalupa\nchapadlo\ncharita\nchata\nchechtat\nchemie\nchichot\nchirurg\nchlad\nchleba\nchlubit\nchmel\nchmura\nchobot\nchochol\nchodba\ncholera\nchomout\nchopit\nchoroba\nchov\nchrapot\nchrlit\nchrt\nchrup\nchtivost\nchudina\nchutnat\nchvat\nchvilka\nchvost\nchyba\nchystat\nchytit\ncibule\ncigareta\ncihelna\ncihla\ncinkot\ncirkus\ncisterna\ncitace\ncitrus\ncizinec\ncizost\nclona\ncokoliv\ncouvat\nctitel\nctnost\ncudnost\ncuketa\ncukr\ncupot\ncvaknout\ncval\ncvik\ncvrkot\ncyklista\ndaleko\ndareba\ndatel\ndatum\ndcera\ndebata\ndechovka\ndecibel\ndeficit\ndeflace\ndekl\ndekret\ndemokrat\ndeprese\nderby\ndeska\ndetektiv\ndikobraz\ndiktovat\ndioda\ndiplom\ndisk\ndisplej\ndivadlo\ndivoch\ndlaha\ndlouho\ndluhopis\ndnes\ndobro\ndobytek\ndocent\ndochutit\ndodnes\ndohled\ndohoda\ndohra\ndojem\ndojnice\ndoklad\ndokola\ndoktor\ndokument\ndolar\ndoleva\ndolina\ndoma\ndominant\ndomluvit\ndomov\ndonutit\ndopad\ndopis\ndoplnit\ndoposud\ndoprovod\ndopustit\ndorazit\ndorost\ndort\ndosah\ndoslov\ndostatek\ndosud\ndosyta\ndotaz\ndotek\ndotknout\ndoufat\ndoutnat\ndovozce\ndozadu\ndoznat\ndozorce\ndrahota\ndrak\ndramatik\ndravec\ndraze\ndrdol\ndrobnost\ndrogerie\ndrozd\ndrsnost\ndrtit\ndrzost\nduben\nduchovno\ndudek\nduha\nduhovka\ndusit\ndusno\ndutost\ndvojice\ndvorec\ndynamit\nekolog\nekonomie\nelektron\nelipsa\nemail\nemise\nemoce\nempatie\nepizoda\nepocha\nepopej\nepos\nesej\nesence\neskorta\neskymo\netiketa\neuforie\nevoluce\nexekuce\nexkurze\nexpedice\nexploze\nexport\nextrakt\nfacka\nfajfka\nfakulta\nfanatik\nfantazie\nfarmacie\nfavorit\nfazole\nfederace\nfejeton\nfenka\nfialka\nfigurant\nfilozof\nfiltr\nfinance\nfinta\nfixace\nfjord\nflanel\nflirt\nflotila\nfond\nfosfor\nfotbal\nfotka\nfoton\nfrakce\nfreska\nfronta\nfukar\nfunkce\nfyzika\ngaleje\ngarant\ngenetika\ngeolog\ngilotina\nglazura\nglejt\ngolem\ngolfista\ngotika\ngraf\ngramofon\ngranule\ngrep\ngril\ngrog\ngroteska\nguma\nhadice\nhadr\nhala\nhalenka\nhanba\nhanopis\nharfa\nharpuna\nhavran\nhebkost\nhejkal\nhejno\nhejtman\nhektar\nhelma\nhematom\nherec\nherna\nheslo\nhezky\nhistorik\nhladovka\nhlasivky\nhlava\nhledat\nhlen\nhlodavec\nhloh\nhloupost\nhltat\nhlubina\nhluchota\nhmat\nhmota\nhmyz\nhnis\nhnojivo\nhnout\nhoblina\nhoboj\nhoch\nhodiny\nhodlat\nhodnota\nhodovat\nhojnost\nhokej\nholinka\nholka\nholub\nhomole\nhonitba\nhonorace\nhoral\nhorda\nhorizont\nhorko\nhorlivec\nhormon\nhornina\nhoroskop\nhorstvo\nhospoda\nhostina\nhotovost\nhouba\nhouf\nhoupat\nhouska\nhovor\nhradba\nhranice\nhravost\nhrazda\nhrbolek\nhrdina\nhrdlo\nhrdost\nhrnek\nhrobka\nhromada\nhrot\nhrouda\nhrozen\nhrstka\nhrubost\nhryzat\nhubenost\nhubnout\nhudba\nhukot\nhumr\nhusita\nhustota\nhvozd\nhybnost\nhydrant\nhygiena\nhymna\nhysterik\nidylka\nihned\nikona\niluze\nimunita\ninfekce\ninflace\ninkaso\ninovace\ninspekce\ninternet\ninvalida\ninvestor\ninzerce\nironie\njablko\njachta\njahoda\njakmile\njakost\njalovec\njantar\njarmark\njaro\njasan\njasno\njatka\njavor\njazyk\njedinec\njedle\njednatel\njehlan\njekot\njelen\njelito\njemnost\njenom\njepice\njeseter\njevit\njezdec\njezero\njinak\njindy\njinoch\njiskra\njistota\njitrnice\njizva\njmenovat\njogurt\njurta\nkabaret\nkabel\nkabinet\nkachna\nkadet\nkadidlo\nkahan\nkajak\nkajuta\nkakao\nkaktus\nkalamita\nkalhoty\nkalibr\nkalnost\nkamera\nkamkoliv\nkamna\nkanibal\nkanoe\nkantor\nkapalina\nkapela\nkapitola\nkapka\nkaple\nkapota\nkapr\nkapusta\nkapybara\nkaramel\nkarotka\nkarton\nkasa\nkatalog\nkatedra\nkauce\nkauza\nkavalec\nkazajka\nkazeta\nkazivost\nkdekoliv\nkdesi\nkedluben\nkemp\nkeramika\nkino\nklacek\nkladivo\nklam\nklapot\nklasika\nklaun\nklec\nklenba\nklepat\nklesnout\nklid\nklima\nklisna\nklobouk\nklokan\nklopa\nkloub\nklubovna\nklusat\nkluzkost\nkmen\nkmitat\nkmotr\nkniha\nknot\nkoalice\nkoberec\nkobka\nkobliha\nkobyla\nkocour\nkohout\nkojenec\nkokos\nkoktejl\nkolaps\nkoleda\nkolize\nkolo\nkomando\nkometa\nkomik\nkomnata\nkomora\nkompas\nkomunita\nkonat\nkoncept\nkondice\nkonec\nkonfese\nkongres\nkonina\nkonkurs\nkontakt\nkonzerva\nkopanec\nkopie\nkopnout\nkoprovka\nkorbel\nkorektor\nkormidlo\nkoroptev\nkorpus\nkoruna\nkoryto\nkorzet\nkosatec\nkostka\nkotel\nkotleta\nkotoul\nkoukat\nkoupelna\nkousek\nkouzlo\nkovboj\nkoza\nkozoroh\nkrabice\nkrach\nkrajina\nkralovat\nkrasopis\nkravata\nkredit\nkrejcar\nkresba\nkreveta\nkriket\nkritik\nkrize\nkrkavec\nkrmelec\nkrmivo\nkrocan\nkrok\nkronika\nkropit\nkroupa\nkrovka\nkrtek\nkruhadlo\nkrupice\nkrutost\nkrvinka\nkrychle\nkrypta\nkrystal\nkryt\nkudlanka\nkufr\nkujnost\nkukla\nkulajda\nkulich\nkulka\nkulomet\nkultura\nkuna\nkupodivu\nkurt\nkurzor\nkutil\nkvalita\nkvasinka\nkvestor\nkynolog\nkyselina\nkytara\nkytice\nkytka\nkytovec\nkyvadlo\nlabrador\nlachtan\nladnost\nlaik\nlakomec\nlamela\nlampa\nlanovka\nlasice\nlaso\nlastura\nlatinka\nlavina\nlebka\nleckdy\nleden\nlednice\nledovka\nledvina\nlegenda\nlegie\nlegrace\nlehce\nlehkost\nlehnout\nlektvar\nlenochod\nlentilka\nlepenka\nlepidlo\nletadlo\nletec\nletmo\nletokruh\nlevhart\nlevitace\nlevobok\nlibra\nlichotka\nlidojed\nlidskost\nlihovina\nlijavec\nlilek\nlimetka\nlinie\nlinka\nlinoleum\nlistopad\nlitina\nlitovat\nlobista\nlodivod\nlogika\nlogoped\nlokalita\nloket\nlomcovat\nlopata\nlopuch\nlord\nlosos\nlotr\nloudal\nlouh\nlouka\nlouskat\nlovec\nlstivost\nlucerna\nlucifer\nlump\nlusk\nlustrace\nlvice\nlyra\nlyrika\nlysina\nmadam\nmadlo\nmagistr\nmahagon\nmajetek\nmajitel\nmajorita\nmakak\nmakovice\nmakrela\nmalba\nmalina\nmalovat\nmalvice\nmaminka\nmandle\nmanko\nmarnost\nmasakr\nmaskot\nmasopust\nmatice\nmatrika\nmaturita\nmazanec\nmazivo\nmazlit\nmazurka\nmdloba\nmechanik\nmeditace\nmedovina\nmelasa\nmeloun\nmentolka\nmetla\nmetoda\nmetr\nmezera\nmigrace\nmihnout\nmihule\nmikina\nmikrofon\nmilenec\nmilimetr\nmilost\nmimika\nmincovna\nminibar\nminomet\nminulost\nmiska\nmistr\nmixovat\nmladost\nmlha\nmlhovina\nmlok\nmlsat\nmluvit\nmnich\nmnohem\nmobil\nmocnost\nmodelka\nmodlitba\nmohyla\nmokro\nmolekula\nmomentka\nmonarcha\nmonokl\nmonstrum\nmontovat\nmonzun\nmosaz\nmoskyt\nmost\nmotivace\nmotorka\nmotyka\nmoucha\nmoudrost\nmozaika\nmozek\nmozol\nmramor\nmravenec\nmrkev\nmrtvola\nmrzet\nmrzutost\nmstitel\nmudrc\nmuflon\nmulat\nmumie\nmunice\nmuset\nmutace\nmuzeum\nmuzikant\nmyslivec\nmzda\nnabourat\nnachytat\nnadace\nnadbytek\nnadhoz\nnadobro\nnadpis\nnahlas\nnahnat\nnahodile\nnahradit\nnaivita\nnajednou\nnajisto\nnajmout\nnaklonit\nnakonec\nnakrmit\nnalevo\nnamazat\nnamluvit\nnanometr\nnaoko\nnaopak\nnaostro\nnapadat\nnapevno\nnaplnit\nnapnout\nnaposled\nnaprosto\nnarodit\nnaruby\nnarychlo\nnasadit\nnasekat\nnaslepo\nnastat\nnatolik\nnavenek\nnavrch\nnavzdory\nnazvat\nnebe\nnechat\nnecky\nnedaleko\nnedbat\nneduh\nnegace\nnehet\nnehoda\nnejen\nnejprve\nneklid\nnelibost\nnemilost\nnemoc\nneochota\nneonka\nnepokoj\nnerost\nnerv\nnesmysl\nnesoulad\nnetvor\nneuron\nnevina\nnezvykle\nnicota\nnijak\nnikam\nnikdy\nnikl\nnikterak\nnitro\nnocleh\nnohavice\nnominace\nnora\nnorek\nnositel\nnosnost\nnouze\nnoviny\nnovota\nnozdra\nnuda\nnudle\nnuget\nnutit\nnutnost\nnutrie\nnymfa\nobal\nobarvit\nobava\nobdiv\nobec\nobehnat\nobejmout\nobezita\nobhajoba\nobilnice\nobjasnit\nobjekt\nobklopit\noblast\noblek\nobliba\nobloha\nobluda\nobnos\nobohatit\nobojek\nobout\nobrazec\nobrna\nobruba\nobrys\nobsah\nobsluha\nobstarat\nobuv\nobvaz\nobvinit\nobvod\nobvykle\nobyvatel\nobzor\nocas\nocel\nocenit\nochladit\nochota\nochrana\nocitnout\nodboj\nodbyt\nodchod\nodcizit\nodebrat\nodeslat\nodevzdat\nodezva\nodhadce\nodhodit\nodjet\nodjinud\nodkaz\nodkoupit\nodliv\nodluka\nodmlka\nodolnost\nodpad\nodpis\nodplout\nodpor\nodpustit\nodpykat\nodrazka\nodsoudit\nodstup\nodsun\nodtok\nodtud\nodvaha\nodveta\nodvolat\nodvracet\nodznak\nofina\nofsajd\nohlas\nohnisko\nohrada\nohrozit\nohryzek\nokap\nokenice\noklika\nokno\nokouzlit\nokovy\nokrasa\nokres\nokrsek\nokruh\nokupant\nokurka\nokusit\nolejnina\nolizovat\nomak\nomeleta\nomezit\nomladina\nomlouvat\nomluva\nomyl\nonehdy\nopakovat\nopasek\noperace\nopice\nopilost\nopisovat\nopora\nopozice\nopravdu\noproti\norbital\norchestr\norgie\norlice\norloj\nortel\nosada\noschnout\nosika\nosivo\noslava\noslepit\noslnit\noslovit\nosnova\nosoba\nosolit\nospalec\nosten\nostraha\nostuda\nostych\nosvojit\noteplit\notisk\notop\notrhat\notrlost\notrok\notruby\notvor\novanout\novar\noves\novlivnit\novoce\noxid\nozdoba\npachatel\npacient\npadouch\npahorek\npakt\npalanda\npalec\npalivo\npaluba\npamflet\npamlsek\npanenka\npanika\npanna\npanovat\npanstvo\npantofle\npaprika\nparketa\nparodie\nparta\nparuka\nparyba\npaseka\npasivita\npastelka\npatent\npatrona\npavouk\npazneht\npazourek\npecka\npedagog\npejsek\npeklo\npeloton\npenalta\npendrek\npenze\nperiskop\npero\npestrost\npetarda\npetice\npetrolej\npevnina\npexeso\npianista\npiha\npijavice\npikle\npiknik\npilina\npilnost\npilulka\npinzeta\npipeta\npisatel\npistole\npitevna\npivnice\npivovar\nplacenta\nplakat\nplamen\nplaneta\nplastika\nplatit\nplavidlo\nplaz\nplech\nplemeno\nplenta\nples\npletivo\nplevel\nplivat\nplnit\nplno\nplocha\nplodina\nplomba\nplout\npluk\nplyn\npobavit\npobyt\npochod\npocit\npoctivec\npodat\npodcenit\npodepsat\npodhled\npodivit\npodklad\npodmanit\npodnik\npodoba\npodpora\npodraz\npodstata\npodvod\npodzim\npoezie\npohanka\npohnutka\npohovor\npohroma\npohyb\npointa\npojistka\npojmout\npokazit\npokles\npokoj\npokrok\npokuta\npokyn\npoledne\npolibek\npolknout\npoloha\npolynom\npomalu\npominout\npomlka\npomoc\npomsta\npomyslet\nponechat\nponorka\nponurost\npopadat\npopel\npopisek\npoplach\npoprosit\npopsat\npopud\nporadce\nporce\nporod\nporucha\nporyv\nposadit\nposed\nposila\nposkok\nposlanec\nposoudit\npospolu\npostava\nposudek\nposyp\npotah\npotkan\npotlesk\npotomek\npotrava\npotupa\npotvora\npoukaz\npouto\npouzdro\npovaha\npovidla\npovlak\npovoz\npovrch\npovstat\npovyk\npovzdech\npozdrav\npozemek\npoznatek\npozor\npozvat\npracovat\nprahory\npraktika\nprales\npraotec\npraporek\nprase\npravda\nprincip\nprkno\nprobudit\nprocento\nprodej\nprofese\nprohra\nprojekt\nprolomit\npromile\npronikat\npropad\nprorok\nprosba\nproton\nproutek\nprovaz\nprskavka\nprsten\nprudkost\nprut\nprvek\nprvohory\npsanec\npsovod\npstruh\nptactvo\npuberta\npuch\npudl\npukavec\npuklina\npukrle\npult\npumpa\npunc\npupen\npusa\npusinka\npustina\nputovat\nputyka\npyramida\npysk\npytel\nracek\nrachot\nradiace\nradnice\nradon\nraft\nragby\nraketa\nrakovina\nrameno\nrampouch\nrande\nrarach\nrarita\nrasovna\nrastr\nratolest\nrazance\nrazidlo\nreagovat\nreakce\nrecept\nredaktor\nreferent\nreflex\nrejnok\nreklama\nrekord\nrekrut\nrektor\nreputace\nrevize\nrevma\nrevolver\nrezerva\nriskovat\nriziko\nrobotika\nrodokmen\nrohovka\nrokle\nrokoko\nromaneto\nropovod\nropucha\nrorejs\nrosol\nrostlina\nrotmistr\nrotoped\nrotunda\nroubenka\nroucho\nroup\nroura\nrovina\nrovnice\nrozbor\nrozchod\nrozdat\nrozeznat\nrozhodce\nrozinka\nrozjezd\nrozkaz\nrozloha\nrozmar\nrozpad\nrozruch\nrozsah\nroztok\nrozum\nrozvod\nrubrika\nruchadlo\nrukavice\nrukopis\nryba\nrybolov\nrychlost\nrydlo\nrypadlo\nrytina\nryzost\nsadista\nsahat\nsako\nsamec\nsamizdat\nsamota\nsanitka\nsardinka\nsasanka\nsatelit\nsazba\nsazenice\nsbor\nschovat\nsebranka\nsecese\nsedadlo\nsediment\nsedlo\nsehnat\nsejmout\nsekera\nsekta\nsekunda\nsekvoje\nsemeno\nseno\nservis\nsesadit\nseshora\nseskok\nseslat\nsestra\nsesuv\nsesypat\nsetba\nsetina\nsetkat\nsetnout\nsetrvat\nsever\nseznam\nshoda\nshrnout\nsifon\nsilnice\nsirka\nsirotek\nsirup\nsituace\nskafandr\nskalisko\nskanzen\nskaut\nskeptik\nskica\nskladba\nsklenice\nsklo\nskluz\nskoba\nskokan\nskoro\nskripta\nskrz\nskupina\nskvost\nskvrna\nslabika\nsladidlo\nslanina\nslast\nslavnost\nsledovat\nslepec\nsleva\nslezina\nslib\nslina\nsliznice\nslon\nsloupek\nslovo\nsluch\nsluha\nslunce\nslupka\nslza\nsmaragd\nsmetana\nsmilstvo\nsmlouva\nsmog\nsmrad\nsmrk\nsmrtka\nsmutek\nsmysl\nsnad\nsnaha\nsnob\nsobota\nsocha\nsodovka\nsokol\nsopka\nsotva\nsouboj\nsoucit\nsoudce\nsouhlas\nsoulad\nsoumrak\nsouprava\nsoused\nsoutok\nsouviset\nspalovna\nspasitel\nspis\nsplav\nspodek\nspojenec\nspolu\nsponzor\nspornost\nspousta\nsprcha\nspustit\nsranda\nsraz\nsrdce\nsrna\nsrnec\nsrovnat\nsrpen\nsrst\nsrub\nstanice\nstarosta\nstatika\nstavba\nstehno\nstezka\nstodola\nstolek\nstopa\nstorno\nstoupat\nstrach\nstres\nstrhnout\nstrom\nstruna\nstudna\nstupnice\nstvol\nstyk\nsubjekt\nsubtropy\nsuchar\nsudost\nsukno\nsundat\nsunout\nsurikata\nsurovina\nsvah\nsvalstvo\nsvetr\nsvatba\nsvazek\nsvisle\nsvitek\nsvoboda\nsvodidlo\nsvorka\nsvrab\nsykavka\nsykot\nsynek\nsynovec\nsypat\nsypkost\nsyrovost\nsysel\nsytost\ntabletka\ntabule\ntahoun\ntajemno\ntajfun\ntajga\ntajit\ntajnost\ntaktika\ntamhle\ntampon\ntancovat\ntanec\ntanker\ntapeta\ntavenina\ntazatel\ntechnika\ntehdy\ntekutina\ntelefon\ntemnota\ntendence\ntenista\ntenor\nteplota\ntepna\nteprve\nterapie\ntermoska\ntextil\nticho\ntiskopis\ntitulek\ntkadlec\ntkanina\ntlapka\ntleskat\ntlukot\ntlupa\ntmel\ntoaleta\ntopinka\ntopol\ntorzo\ntouha\ntoulec\ntradice\ntraktor\ntramp\ntrasa\ntraverza\ntrefit\ntrest\ntrezor\ntrhavina\ntrhlina\ntrochu\ntrojice\ntroska\ntrouba\ntrpce\ntrpitel\ntrpkost\ntrubec\ntruchlit\ntruhlice\ntrus\ntrvat\ntudy\ntuhnout\ntuhost\ntundra\nturista\nturnaj\ntuzemsko\ntvaroh\ntvorba\ntvrdost\ntvrz\ntygr\ntykev\nubohost\nuboze\nubrat\nubrousek\nubrus\nubytovna\nucho\nuctivost\nudivit\nuhradit\nujednat\nujistit\nujmout\nukazatel\nuklidnit\nuklonit\nukotvit\nukrojit\nulice\nulita\nulovit\numyvadlo\nunavit\nuniforma\nuniknout\nupadnout\nuplatnit\nuplynout\nupoutat\nupravit\nuran\nurazit\nusednout\nusilovat\nusmrtit\nusnadnit\nusnout\nusoudit\nustlat\nustrnout\nutahovat\nutkat\nutlumit\nutonout\nutopenec\nutrousit\nuvalit\nuvolnit\nuvozovka\nuzdravit\nuzel\nuzenina\nuzlina\nuznat\nvagon\nvalcha\nvaloun\nvana\nvandal\nvanilka\nvaran\nvarhany\nvarovat\nvcelku\nvchod\nvdova\nvedro\nvegetace\nvejce\nvelbloud\nveletrh\nvelitel\nvelmoc\nvelryba\nvenkov\nveranda\nverze\nveselka\nveskrze\nvesnice\nvespodu\nvesta\nveterina\nveverka\nvibrace\nvichr\nvideohra\nvidina\nvidle\nvila\nvinice\nviset\nvitalita\nvize\nvizitka\nvjezd\nvklad\nvkus\nvlajka\nvlak\nvlasec\nvlevo\nvlhkost\nvliv\nvlnovka\nvloupat\nvnucovat\nvnuk\nvoda\nvodivost\nvodoznak\nvodstvo\nvojensky\nvojna\nvojsko\nvolant\nvolba\nvolit\nvolno\nvoskovka\nvozidlo\nvozovna\nvpravo\nvrabec\nvracet\nvrah\nvrata\nvrba\nvrcholek\nvrhat\nvrstva\nvrtule\nvsadit\nvstoupit\nvstup\nvtip\nvybavit\nvybrat\nvychovat\nvydat\nvydra\nvyfotit\nvyhledat\nvyhnout\nvyhodit\nvyhradit\nvyhubit\nvyjasnit\nvyjet\nvyjmout\nvyklopit\nvykonat\nvylekat\nvymazat\nvymezit\nvymizet\nvymyslet\nvynechat\nvynikat\nvynutit\nvypadat\nvyplatit\nvypravit\nvypustit\nvyrazit\nvyrovnat\nvyrvat\nvyslovit\nvysoko\nvystavit\nvysunout\nvysypat\nvytasit\nvytesat\nvytratit\nvyvinout\nvyvolat\nvyvrhel\nvyzdobit\nvyznat\nvzadu\nvzbudit\nvzchopit\nvzdor\nvzduch\nvzdychat\nvzestup\nvzhledem\nvzkaz\nvzlykat\nvznik\nvzorek\nvzpoura\nvztah\nvztek\nxylofon\nzabrat\nzabydlet\nzachovat\nzadarmo\nzadusit\nzafoukat\nzahltit\nzahodit\nzahrada\nzahynout\nzajatec\nzajet\nzajistit\nzaklepat\nzakoupit\nzalepit\nzamezit\nzamotat\nzamyslet\nzanechat\nzanikat\nzaplatit\nzapojit\nzapsat\nzarazit\nzastavit\nzasunout\nzatajit\nzatemnit\nzatknout\nzaujmout\nzavalit\nzavelet\nzavinit\nzavolat\nzavrtat\nzazvonit\nzbavit\nzbrusu\nzbudovat\nzbytek\nzdaleka\nzdarma\nzdatnost\nzdivo\nzdobit\nzdroj\nzdvih\nzdymadlo\nzelenina\nzeman\nzemina\nzeptat\nzezadu\nzezdola\nzhatit\nzhltnout\nzhluboka\nzhotovit\nzhruba\nzima\nzimnice\nzjemnit\nzklamat\nzkoumat\nzkratka\nzkumavka\nzlato\nzlehka\nzloba\nzlom\nzlost\nzlozvyk\nzmapovat\nzmar\nzmatek\nzmije\nzmizet\nzmocnit\nzmodrat\nzmrzlina\nzmutovat\nznak\nznalost\nznamenat\nznovu\nzobrazit\nzotavit\nzoubek\nzoufale\nzplodit\nzpomalit\nzprava\nzprostit\nzprudka\nzprvu\nzrada\nzranit\nzrcadlo\nzrnitost\nzrno\nzrovna\nzrychlit\nzrzavost\nzticha\nztratit\nzubovina\nzubr\nzvednout\nzvenku\nzvesela\nzvon\nzvrat\nzvukovod\nzvyk"); + dico.Add( + "chinese_simplified", + "的\n一\n是\n在\n不\n了\n有\n和\n人\n这\n中\n大\n为\n上\n个\n国\n我\n以\n要\n他\n时\n来\n用\n们\n生\n到\n作\n地\n于\n出\n就\n分\n对\n成\n会\n可\n主\n发\n年\n动\n同\n工\n也\n能\n下\n过\n子\n说\n产\n种\n面\n而\n方\n后\n多\n定\n行\n学\n法\n所\n民\n得\n经\n十\n三\n之\n进\n着\n等\n部\n度\n家\n电\n力\n里\n如\n水\n化\n高\n自\n二\n理\n起\n小\n物\n现\n实\n加\n量\n都\n两\n体\n制\n机\n当\n使\n点\n从\n业\n本\n去\n把\n性\n好\n应\n开\n它\n合\n还\n因\n由\n其\n些\n然\n前\n外\n天\n政\n四\n日\n那\n社\n义\n事\n平\n形\n相\n全\n表\n间\n样\n与\n关\n各\n重\n新\n线\n内\n数\n正\n心\n反\n你\n明\n看\n原\n又\n么\n利\n比\n或\n但\n质\n气\n第\n向\n道\n命\n此\n变\n条\n只\n没\n结\n解\n问\n意\n建\n月\n公\n无\n系\n军\n很\n情\n者\n最\n立\n代\n想\n已\n通\n并\n提\n直\n题\n党\n程\n展\n五\n果\n料\n象\n员\n革\n位\n入\n常\n文\n总\n次\n品\n式\n活\n设\n及\n管\n特\n件\n长\n求\n老\n头\n基\n资\n边\n流\n路\n级\n少\n图\n山\n统\n接\n知\n较\n将\n组\n见\n计\n别\n她\n手\n角\n期\n根\n论\n运\n农\n指\n几\n九\n区\n强\n放\n决\n西\n被\n干\n做\n必\n战\n先\n回\n则\n任\n取\n据\n处\n队\n南\n给\n色\n光\n门\n即\n保\n治\n北\n造\n百\n规\n热\n领\n七\n海\n口\n东\n导\n器\n压\n志\n世\n金\n增\n争\n济\n阶\n油\n思\n术\n极\n交\n受\n联\n什\n认\n六\n共\n权\n收\n证\n改\n清\n美\n再\n采\n转\n更\n单\n风\n切\n打\n白\n教\n速\n花\n带\n安\n场\n身\n车\n例\n真\n务\n具\n万\n每\n目\n至\n达\n走\n积\n示\n议\n声\n报\n斗\n完\n类\n八\n离\n华\n名\n确\n才\n科\n张\n信\n马\n节\n话\n米\n整\n空\n元\n况\n今\n集\n温\n传\n土\n许\n步\n群\n广\n石\n记\n需\n段\n研\n界\n拉\n林\n律\n叫\n且\n究\n观\n越\n织\n装\n影\n算\n低\n持\n音\n众\n书\n布\n复\n容\n儿\n须\n际\n商\n非\n验\n连\n断\n深\n难\n近\n矿\n千\n周\n委\n素\n技\n备\n半\n办\n青\n省\n列\n习\n响\n约\n支\n般\n史\n感\n劳\n便\n团\n往\n酸\n历\n市\n克\n何\n除\n消\n构\n府\n称\n太\n准\n精\n值\n号\n率\n族\n维\n划\n选\n标\n写\n存\n候\n毛\n亲\n快\n效\n斯\n院\n查\n江\n型\n眼\n王\n按\n格\n养\n易\n置\n派\n层\n片\n始\n却\n专\n状\n育\n厂\n京\n识\n适\n属\n圆\n包\n火\n住\n调\n满\n县\n局\n照\n参\n红\n细\n引\n听\n该\n铁\n价\n严\n首\n底\n液\n官\n德\n随\n病\n苏\n失\n尔\n死\n讲\n配\n女\n黄\n推\n显\n谈\n罪\n神\n艺\n呢\n席\n含\n企\n望\n密\n批\n营\n项\n防\n举\n球\n英\n氧\n势\n告\n李\n台\n落\n木\n帮\n轮\n破\n亚\n师\n围\n注\n远\n字\n材\n排\n供\n河\n态\n封\n另\n施\n减\n树\n溶\n怎\n止\n案\n言\n士\n均\n武\n固\n叶\n鱼\n波\n视\n仅\n费\n紧\n爱\n左\n章\n早\n朝\n害\n续\n轻\n服\n试\n食\n充\n兵\n源\n判\n护\n司\n足\n某\n练\n差\n致\n板\n田\n降\n黑\n犯\n负\n击\n范\n继\n兴\n似\n余\n坚\n曲\n输\n修\n故\n城\n夫\n够\n送\n笔\n船\n占\n右\n财\n吃\n富\n春\n职\n觉\n汉\n画\n功\n巴\n跟\n虽\n杂\n飞\n检\n吸\n助\n升\n阳\n互\n初\n创\n抗\n考\n投\n坏\n策\n古\n径\n换\n未\n跑\n留\n钢\n曾\n端\n责\n站\n简\n述\n钱\n副\n尽\n帝\n射\n草\n冲\n承\n独\n令\n限\n阿\n宣\n环\n双\n请\n超\n微\n让\n控\n州\n良\n轴\n找\n否\n纪\n益\n依\n优\n顶\n础\n载\n倒\n房\n突\n坐\n粉\n敌\n略\n客\n袁\n冷\n胜\n绝\n析\n块\n剂\n测\n丝\n协\n诉\n念\n陈\n仍\n罗\n盐\n友\n洋\n错\n苦\n夜\n刑\n移\n频\n逐\n靠\n混\n母\n短\n皮\n终\n聚\n汽\n村\n云\n哪\n既\n距\n卫\n停\n烈\n央\n察\n烧\n迅\n境\n若\n印\n洲\n刻\n括\n激\n孔\n搞\n甚\n室\n待\n核\n校\n散\n侵\n吧\n甲\n游\n久\n菜\n味\n旧\n模\n湖\n货\n损\n预\n阻\n毫\n普\n稳\n乙\n妈\n植\n息\n扩\n银\n语\n挥\n酒\n守\n拿\n序\n纸\n医\n缺\n雨\n吗\n针\n刘\n啊\n急\n唱\n误\n训\n愿\n审\n附\n获\n茶\n鲜\n粮\n斤\n孩\n脱\n硫\n肥\n善\n龙\n演\n父\n渐\n血\n欢\n械\n掌\n歌\n沙\n刚\n攻\n谓\n盾\n讨\n晚\n粒\n乱\n燃\n矛\n乎\n杀\n药\n宁\n鲁\n贵\n钟\n煤\n读\n班\n伯\n香\n介\n迫\n句\n丰\n培\n握\n兰\n担\n弦\n蛋\n沉\n假\n穿\n执\n答\n乐\n谁\n顺\n烟\n缩\n征\n脸\n喜\n松\n脚\n困\n异\n免\n背\n星\n福\n买\n染\n井\n概\n慢\n怕\n磁\n倍\n祖\n皇\n促\n静\n补\n评\n翻\n肉\n践\n尼\n衣\n宽\n扬\n棉\n希\n伤\n操\n垂\n秋\n宜\n氢\n套\n督\n振\n架\n亮\n末\n宪\n庆\n编\n牛\n触\n映\n雷\n销\n诗\n座\n居\n抓\n裂\n胞\n呼\n娘\n景\n威\n绿\n晶\n厚\n盟\n衡\n鸡\n孙\n延\n危\n胶\n屋\n乡\n临\n陆\n顾\n掉\n呀\n灯\n岁\n措\n束\n耐\n剧\n玉\n赵\n跳\n哥\n季\n课\n凯\n胡\n额\n款\n绍\n卷\n齐\n伟\n蒸\n殖\n永\n宗\n苗\n川\n炉\n岩\n弱\n零\n杨\n奏\n沿\n露\n杆\n探\n滑\n镇\n饭\n浓\n航\n怀\n赶\n库\n夺\n伊\n灵\n税\n途\n灭\n赛\n归\n召\n鼓\n播\n盘\n裁\n险\n康\n唯\n录\n菌\n纯\n借\n糖\n盖\n横\n符\n私\n努\n堂\n域\n枪\n润\n幅\n哈\n竟\n熟\n虫\n泽\n脑\n壤\n碳\n欧\n遍\n侧\n寨\n敢\n彻\n虑\n斜\n薄\n庭\n纳\n弹\n饲\n伸\n折\n麦\n湿\n暗\n荷\n瓦\n塞\n床\n筑\n恶\n户\n访\n塔\n奇\n透\n梁\n刀\n旋\n迹\n卡\n氯\n遇\n份\n毒\n泥\n退\n洗\n摆\n灰\n彩\n卖\n耗\n夏\n择\n忙\n铜\n献\n硬\n予\n繁\n圈\n雪\n函\n亦\n抽\n篇\n阵\n阴\n丁\n尺\n追\n堆\n雄\n迎\n泛\n爸\n楼\n避\n谋\n吨\n野\n猪\n旗\n累\n偏\n典\n馆\n索\n秦\n脂\n潮\n爷\n豆\n忽\n托\n惊\n塑\n遗\n愈\n朱\n替\n纤\n粗\n倾\n尚\n痛\n楚\n谢\n奋\n购\n磨\n君\n池\n旁\n碎\n骨\n监\n捕\n弟\n暴\n割\n贯\n殊\n释\n词\n亡\n壁\n顿\n宝\n午\n尘\n闻\n揭\n炮\n残\n冬\n桥\n妇\n警\n综\n招\n吴\n付\n浮\n遭\n徐\n您\n摇\n谷\n赞\n箱\n隔\n订\n男\n吹\n园\n纷\n唐\n败\n宋\n玻\n巨\n耕\n坦\n荣\n闭\n湾\n键\n凡\n驻\n锅\n救\n恩\n剥\n凝\n碱\n齿\n截\n炼\n麻\n纺\n禁\n废\n盛\n版\n缓\n净\n睛\n昌\n婚\n涉\n筒\n嘴\n插\n岸\n朗\n庄\n街\n藏\n姑\n贸\n腐\n奴\n啦\n惯\n乘\n伙\n恢\n匀\n纱\n扎\n辩\n耳\n彪\n臣\n亿\n璃\n抵\n脉\n秀\n萨\n俄\n网\n舞\n店\n喷\n纵\n寸\n汗\n挂\n洪\n贺\n闪\n柬\n爆\n烯\n津\n稻\n墙\n软\n勇\n像\n滚\n厘\n蒙\n芳\n肯\n坡\n柱\n荡\n腿\n仪\n旅\n尾\n轧\n冰\n贡\n登\n黎\n削\n钻\n勒\n逃\n障\n氨\n郭\n峰\n币\n港\n伏\n轨\n亩\n毕\n擦\n莫\n刺\n浪\n秘\n援\n株\n健\n售\n股\n岛\n甘\n泡\n睡\n童\n铸\n汤\n阀\n休\n汇\n舍\n牧\n绕\n炸\n哲\n磷\n绩\n朋\n淡\n尖\n启\n陷\n柴\n呈\n徒\n颜\n泪\n稍\n忘\n泵\n蓝\n拖\n洞\n授\n镜\n辛\n壮\n锋\n贫\n虚\n弯\n摩\n泰\n幼\n廷\n尊\n窗\n纲\n弄\n隶\n疑\n氏\n宫\n姐\n震\n瑞\n怪\n尤\n琴\n循\n描\n膜\n违\n夹\n腰\n缘\n珠\n穷\n森\n枝\n竹\n沟\n催\n绳\n忆\n邦\n剩\n幸\n浆\n栏\n拥\n牙\n贮\n礼\n滤\n钠\n纹\n罢\n拍\n咱\n喊\n袖\n埃\n勤\n罚\n焦\n潜\n伍\n墨\n欲\n缝\n姓\n刊\n饱\n仿\n奖\n铝\n鬼\n丽\n跨\n默\n挖\n链\n扫\n喝\n袋\n炭\n污\n幕\n诸\n弧\n励\n梅\n奶\n洁\n灾\n舟\n鉴\n苯\n讼\n抱\n毁\n懂\n寒\n智\n埔\n寄\n届\n跃\n渡\n挑\n丹\n艰\n贝\n碰\n拔\n爹\n戴\n码\n梦\n芽\n熔\n赤\n渔\n哭\n敬\n颗\n奔\n铅\n仲\n虎\n稀\n妹\n乏\n珍\n申\n桌\n遵\n允\n隆\n螺\n仓\n魏\n锐\n晓\n氮\n兼\n隐\n碍\n赫\n拨\n忠\n肃\n缸\n牵\n抢\n博\n巧\n壳\n兄\n杜\n讯\n诚\n碧\n祥\n柯\n页\n巡\n矩\n悲\n灌\n龄\n伦\n票\n寻\n桂\n铺\n圣\n恐\n恰\n郑\n趣\n抬\n荒\n腾\n贴\n柔\n滴\n猛\n阔\n辆\n妻\n填\n撤\n储\n签\n闹\n扰\n紫\n砂\n递\n戏\n吊\n陶\n伐\n喂\n疗\n瓶\n婆\n抚\n臂\n摸\n忍\n虾\n蜡\n邻\n胸\n巩\n挤\n偶\n弃\n槽\n劲\n乳\n邓\n吉\n仁\n烂\n砖\n租\n乌\n舰\n伴\n瓜\n浅\n丙\n暂\n燥\n橡\n柳\n迷\n暖\n牌\n秧\n胆\n详\n簧\n踏\n瓷\n谱\n呆\n宾\n糊\n洛\n辉\n愤\n竞\n隙\n怒\n粘\n乃\n绪\n肩\n籍\n敏\n涂\n熙\n皆\n侦\n悬\n掘\n享\n纠\n醒\n狂\n锁\n淀\n恨\n牲\n霸\n爬\n赏\n逆\n玩\n陵\n祝\n秒\n浙\n貌\n役\n彼\n悉\n鸭\n趋\n凤\n晨\n畜\n辈\n秩\n卵\n署\n梯\n炎\n滩\n棋\n驱\n筛\n峡\n冒\n啥\n寿\n译\n浸\n泉\n帽\n迟\n硅\n疆\n贷\n漏\n稿\n冠\n嫩\n胁\n芯\n牢\n叛\n蚀\n奥\n鸣\n岭\n羊\n凭\n串\n塘\n绘\n酵\n融\n盆\n锡\n庙\n筹\n冻\n辅\n摄\n袭\n筋\n拒\n僚\n旱\n钾\n鸟\n漆\n沈\n眉\n疏\n添\n棒\n穗\n硝\n韩\n逼\n扭\n侨\n凉\n挺\n碗\n栽\n炒\n杯\n患\n馏\n劝\n豪\n辽\n勃\n鸿\n旦\n吏\n拜\n狗\n埋\n辊\n掩\n饮\n搬\n骂\n辞\n勾\n扣\n估\n蒋\n绒\n雾\n丈\n朵\n姆\n拟\n宇\n辑\n陕\n雕\n偿\n蓄\n崇\n剪\n倡\n厅\n咬\n驶\n薯\n刷\n斥\n番\n赋\n奉\n佛\n浇\n漫\n曼\n扇\n钙\n桃\n扶\n仔\n返\n俗\n亏\n腔\n鞋\n棱\n覆\n框\n悄\n叔\n撞\n骗\n勘\n旺\n沸\n孤\n吐\n孟\n渠\n屈\n疾\n妙\n惜\n仰\n狠\n胀\n谐\n抛\n霉\n桑\n岗\n嘛\n衰\n盗\n渗\n脏\n赖\n涌\n甜\n曹\n阅\n肌\n哩\n厉\n烃\n纬\n毅\n昨\n伪\n症\n煮\n叹\n钉\n搭\n茎\n笼\n酷\n偷\n弓\n锥\n恒\n杰\n坑\n鼻\n翼\n纶\n叙\n狱\n逮\n罐\n络\n棚\n抑\n膨\n蔬\n寺\n骤\n穆\n冶\n枯\n册\n尸\n凸\n绅\n坯\n牺\n焰\n轰\n欣\n晋\n瘦\n御\n锭\n锦\n丧\n旬\n锻\n垄\n搜\n扑\n邀\n亭\n酯\n迈\n舒\n脆\n酶\n闲\n忧\n酚\n顽\n羽\n涨\n卸\n仗\n陪\n辟\n惩\n杭\n姚\n肚\n捉\n飘\n漂\n昆\n欺\n吾\n郎\n烷\n汁\n呵\n饰\n萧\n雅\n邮\n迁\n燕\n撒\n姻\n赴\n宴\n烦\n债\n帐\n斑\n铃\n旨\n醇\n董\n饼\n雏\n姿\n拌\n傅\n腹\n妥\n揉\n贤\n拆\n歪\n葡\n胺\n丢\n浩\n徽\n昂\n垫\n挡\n览\n贪\n慰\n缴\n汪\n慌\n冯\n诺\n姜\n谊\n凶\n劣\n诬\n耀\n昏\n躺\n盈\n骑\n乔\n溪\n丛\n卢\n抹\n闷\n咨\n刮\n驾\n缆\n悟\n摘\n铒\n掷\n颇\n幻\n柄\n惠\n惨\n佳\n仇\n腊\n窝\n涤\n剑\n瞧\n堡\n泼\n葱\n罩\n霍\n捞\n胎\n苍\n滨\n俩\n捅\n湘\n砍\n霞\n邵\n萄\n疯\n淮\n遂\n熊\n粪\n烘\n宿\n档\n戈\n驳\n嫂\n裕\n徙\n箭\n捐\n肠\n撑\n晒\n辨\n殿\n莲\n摊\n搅\n酱\n屏\n疫\n哀\n蔡\n堵\n沫\n皱\n畅\n叠\n阁\n莱\n敲\n辖\n钩\n痕\n坝\n巷\n饿\n祸\n丘\n玄\n溜\n曰\n逻\n彭\n尝\n卿\n妨\n艇\n吞\n韦\n怨\n矮\n歇\n" + ); + dico.Add( + "chinese_traditional", + "的\n一\n是\n在\n不\n了\n有\n和\n人\n這\n中\n大\n為\n上\n個\n國\n我\n以\n要\n他\n時\n來\n用\n們\n生\n到\n作\n地\n於\n出\n就\n分\n對\n成\n會\n可\n主\n發\n年\n動\n同\n工\n也\n能\n下\n過\n子\n說\n產\n種\n面\n而\n方\n後\n多\n定\n行\n學\n法\n所\n民\n得\n經\n十\n三\n之\n進\n著\n等\n部\n度\n家\n電\n力\n裡\n如\n水\n化\n高\n自\n二\n理\n起\n小\n物\n現\n實\n加\n量\n都\n兩\n體\n制\n機\n當\n使\n點\n從\n業\n本\n去\n把\n性\n好\n應\n開\n它\n合\n還\n因\n由\n其\n些\n然\n前\n外\n天\n政\n四\n日\n那\n社\n義\n事\n平\n形\n相\n全\n表\n間\n樣\n與\n關\n各\n重\n新\n線\n內\n數\n正\n心\n反\n你\n明\n看\n原\n又\n麼\n利\n比\n或\n但\n質\n氣\n第\n向\n道\n命\n此\n變\n條\n只\n沒\n結\n解\n問\n意\n建\n月\n公\n無\n系\n軍\n很\n情\n者\n最\n立\n代\n想\n已\n通\n並\n提\n直\n題\n黨\n程\n展\n五\n果\n料\n象\n員\n革\n位\n入\n常\n文\n總\n次\n品\n式\n活\n設\n及\n管\n特\n件\n長\n求\n老\n頭\n基\n資\n邊\n流\n路\n級\n少\n圖\n山\n統\n接\n知\n較\n將\n組\n見\n計\n別\n她\n手\n角\n期\n根\n論\n運\n農\n指\n幾\n九\n區\n強\n放\n決\n西\n被\n幹\n做\n必\n戰\n先\n回\n則\n任\n取\n據\n處\n隊\n南\n給\n色\n光\n門\n即\n保\n治\n北\n造\n百\n規\n熱\n領\n七\n海\n口\n東\n導\n器\n壓\n志\n世\n金\n增\n爭\n濟\n階\n油\n思\n術\n極\n交\n受\n聯\n什\n認\n六\n共\n權\n收\n證\n改\n清\n美\n再\n採\n轉\n更\n單\n風\n切\n打\n白\n教\n速\n花\n帶\n安\n場\n身\n車\n例\n真\n務\n具\n萬\n每\n目\n至\n達\n走\n積\n示\n議\n聲\n報\n鬥\n完\n類\n八\n離\n華\n名\n確\n才\n科\n張\n信\n馬\n節\n話\n米\n整\n空\n元\n況\n今\n集\n溫\n傳\n土\n許\n步\n群\n廣\n石\n記\n需\n段\n研\n界\n拉\n林\n律\n叫\n且\n究\n觀\n越\n織\n裝\n影\n算\n低\n持\n音\n眾\n書\n布\n复\n容\n兒\n須\n際\n商\n非\n驗\n連\n斷\n深\n難\n近\n礦\n千\n週\n委\n素\n技\n備\n半\n辦\n青\n省\n列\n習\n響\n約\n支\n般\n史\n感\n勞\n便\n團\n往\n酸\n歷\n市\n克\n何\n除\n消\n構\n府\n稱\n太\n準\n精\n值\n號\n率\n族\n維\n劃\n選\n標\n寫\n存\n候\n毛\n親\n快\n效\n斯\n院\n查\n江\n型\n眼\n王\n按\n格\n養\n易\n置\n派\n層\n片\n始\n卻\n專\n狀\n育\n廠\n京\n識\n適\n屬\n圓\n包\n火\n住\n調\n滿\n縣\n局\n照\n參\n紅\n細\n引\n聽\n該\n鐵\n價\n嚴\n首\n底\n液\n官\n德\n隨\n病\n蘇\n失\n爾\n死\n講\n配\n女\n黃\n推\n顯\n談\n罪\n神\n藝\n呢\n席\n含\n企\n望\n密\n批\n營\n項\n防\n舉\n球\n英\n氧\n勢\n告\n李\n台\n落\n木\n幫\n輪\n破\n亞\n師\n圍\n注\n遠\n字\n材\n排\n供\n河\n態\n封\n另\n施\n減\n樹\n溶\n怎\n止\n案\n言\n士\n均\n武\n固\n葉\n魚\n波\n視\n僅\n費\n緊\n愛\n左\n章\n早\n朝\n害\n續\n輕\n服\n試\n食\n充\n兵\n源\n判\n護\n司\n足\n某\n練\n差\n致\n板\n田\n降\n黑\n犯\n負\n擊\n范\n繼\n興\n似\n餘\n堅\n曲\n輸\n修\n故\n城\n夫\n夠\n送\n筆\n船\n佔\n右\n財\n吃\n富\n春\n職\n覺\n漢\n畫\n功\n巴\n跟\n雖\n雜\n飛\n檢\n吸\n助\n昇\n陽\n互\n初\n創\n抗\n考\n投\n壞\n策\n古\n徑\n換\n未\n跑\n留\n鋼\n曾\n端\n責\n站\n簡\n述\n錢\n副\n盡\n帝\n射\n草\n衝\n承\n獨\n令\n限\n阿\n宣\n環\n雙\n請\n超\n微\n讓\n控\n州\n良\n軸\n找\n否\n紀\n益\n依\n優\n頂\n礎\n載\n倒\n房\n突\n坐\n粉\n敵\n略\n客\n袁\n冷\n勝\n絕\n析\n塊\n劑\n測\n絲\n協\n訴\n念\n陳\n仍\n羅\n鹽\n友\n洋\n錯\n苦\n夜\n刑\n移\n頻\n逐\n靠\n混\n母\n短\n皮\n終\n聚\n汽\n村\n雲\n哪\n既\n距\n衛\n停\n烈\n央\n察\n燒\n迅\n境\n若\n印\n洲\n刻\n括\n激\n孔\n搞\n甚\n室\n待\n核\n校\n散\n侵\n吧\n甲\n遊\n久\n菜\n味\n舊\n模\n湖\n貨\n損\n預\n阻\n毫\n普\n穩\n乙\n媽\n植\n息\n擴\n銀\n語\n揮\n酒\n守\n拿\n序\n紙\n醫\n缺\n雨\n嗎\n針\n劉\n啊\n急\n唱\n誤\n訓\n願\n審\n附\n獲\n茶\n鮮\n糧\n斤\n孩\n脫\n硫\n肥\n善\n龍\n演\n父\n漸\n血\n歡\n械\n掌\n歌\n沙\n剛\n攻\n謂\n盾\n討\n晚\n粒\n亂\n燃\n矛\n乎\n殺\n藥\n寧\n魯\n貴\n鐘\n煤\n讀\n班\n伯\n香\n介\n迫\n句\n豐\n培\n握\n蘭\n擔\n弦\n蛋\n沉\n假\n穿\n執\n答\n樂\n誰\n順\n煙\n縮\n徵\n臉\n喜\n松\n腳\n困\n異\n免\n背\n星\n福\n買\n染\n井\n概\n慢\n怕\n磁\n倍\n祖\n皇\n促\n靜\n補\n評\n翻\n肉\n踐\n尼\n衣\n寬\n揚\n棉\n希\n傷\n操\n垂\n秋\n宜\n氫\n套\n督\n振\n架\n亮\n末\n憲\n慶\n編\n牛\n觸\n映\n雷\n銷\n詩\n座\n居\n抓\n裂\n胞\n呼\n娘\n景\n威\n綠\n晶\n厚\n盟\n衡\n雞\n孫\n延\n危\n膠\n屋\n鄉\n臨\n陸\n顧\n掉\n呀\n燈\n歲\n措\n束\n耐\n劇\n玉\n趙\n跳\n哥\n季\n課\n凱\n胡\n額\n款\n紹\n卷\n齊\n偉\n蒸\n殖\n永\n宗\n苗\n川\n爐\n岩\n弱\n零\n楊\n奏\n沿\n露\n桿\n探\n滑\n鎮\n飯\n濃\n航\n懷\n趕\n庫\n奪\n伊\n靈\n稅\n途\n滅\n賽\n歸\n召\n鼓\n播\n盤\n裁\n險\n康\n唯\n錄\n菌\n純\n借\n糖\n蓋\n橫\n符\n私\n努\n堂\n域\n槍\n潤\n幅\n哈\n竟\n熟\n蟲\n澤\n腦\n壤\n碳\n歐\n遍\n側\n寨\n敢\n徹\n慮\n斜\n薄\n庭\n納\n彈\n飼\n伸\n折\n麥\n濕\n暗\n荷\n瓦\n塞\n床\n築\n惡\n戶\n訪\n塔\n奇\n透\n梁\n刀\n旋\n跡\n卡\n氯\n遇\n份\n毒\n泥\n退\n洗\n擺\n灰\n彩\n賣\n耗\n夏\n擇\n忙\n銅\n獻\n硬\n予\n繁\n圈\n雪\n函\n亦\n抽\n篇\n陣\n陰\n丁\n尺\n追\n堆\n雄\n迎\n泛\n爸\n樓\n避\n謀\n噸\n野\n豬\n旗\n累\n偏\n典\n館\n索\n秦\n脂\n潮\n爺\n豆\n忽\n托\n驚\n塑\n遺\n愈\n朱\n替\n纖\n粗\n傾\n尚\n痛\n楚\n謝\n奮\n購\n磨\n君\n池\n旁\n碎\n骨\n監\n捕\n弟\n暴\n割\n貫\n殊\n釋\n詞\n亡\n壁\n頓\n寶\n午\n塵\n聞\n揭\n炮\n殘\n冬\n橋\n婦\n警\n綜\n招\n吳\n付\n浮\n遭\n徐\n您\n搖\n谷\n贊\n箱\n隔\n訂\n男\n吹\n園\n紛\n唐\n敗\n宋\n玻\n巨\n耕\n坦\n榮\n閉\n灣\n鍵\n凡\n駐\n鍋\n救\n恩\n剝\n凝\n鹼\n齒\n截\n煉\n麻\n紡\n禁\n廢\n盛\n版\n緩\n淨\n睛\n昌\n婚\n涉\n筒\n嘴\n插\n岸\n朗\n莊\n街\n藏\n姑\n貿\n腐\n奴\n啦\n慣\n乘\n夥\n恢\n勻\n紗\n扎\n辯\n耳\n彪\n臣\n億\n璃\n抵\n脈\n秀\n薩\n俄\n網\n舞\n店\n噴\n縱\n寸\n汗\n掛\n洪\n賀\n閃\n柬\n爆\n烯\n津\n稻\n牆\n軟\n勇\n像\n滾\n厘\n蒙\n芳\n肯\n坡\n柱\n盪\n腿\n儀\n旅\n尾\n軋\n冰\n貢\n登\n黎\n削\n鑽\n勒\n逃\n障\n氨\n郭\n峰\n幣\n港\n伏\n軌\n畝\n畢\n擦\n莫\n刺\n浪\n秘\n援\n株\n健\n售\n股\n島\n甘\n泡\n睡\n童\n鑄\n湯\n閥\n休\n匯\n舍\n牧\n繞\n炸\n哲\n磷\n績\n朋\n淡\n尖\n啟\n陷\n柴\n呈\n徒\n顏\n淚\n稍\n忘\n泵\n藍\n拖\n洞\n授\n鏡\n辛\n壯\n鋒\n貧\n虛\n彎\n摩\n泰\n幼\n廷\n尊\n窗\n綱\n弄\n隸\n疑\n氏\n宮\n姐\n震\n瑞\n怪\n尤\n琴\n循\n描\n膜\n違\n夾\n腰\n緣\n珠\n窮\n森\n枝\n竹\n溝\n催\n繩\n憶\n邦\n剩\n幸\n漿\n欄\n擁\n牙\n貯\n禮\n濾\n鈉\n紋\n罷\n拍\n咱\n喊\n袖\n埃\n勤\n罰\n焦\n潛\n伍\n墨\n欲\n縫\n姓\n刊\n飽\n仿\n獎\n鋁\n鬼\n麗\n跨\n默\n挖\n鏈\n掃\n喝\n袋\n炭\n污\n幕\n諸\n弧\n勵\n梅\n奶\n潔\n災\n舟\n鑑\n苯\n訟\n抱\n毀\n懂\n寒\n智\n埔\n寄\n屆\n躍\n渡\n挑\n丹\n艱\n貝\n碰\n拔\n爹\n戴\n碼\n夢\n芽\n熔\n赤\n漁\n哭\n敬\n顆\n奔\n鉛\n仲\n虎\n稀\n妹\n乏\n珍\n申\n桌\n遵\n允\n隆\n螺\n倉\n魏\n銳\n曉\n氮\n兼\n隱\n礙\n赫\n撥\n忠\n肅\n缸\n牽\n搶\n博\n巧\n殼\n兄\n杜\n訊\n誠\n碧\n祥\n柯\n頁\n巡\n矩\n悲\n灌\n齡\n倫\n票\n尋\n桂\n鋪\n聖\n恐\n恰\n鄭\n趣\n抬\n荒\n騰\n貼\n柔\n滴\n猛\n闊\n輛\n妻\n填\n撤\n儲\n簽\n鬧\n擾\n紫\n砂\n遞\n戲\n吊\n陶\n伐\n餵\n療\n瓶\n婆\n撫\n臂\n摸\n忍\n蝦\n蠟\n鄰\n胸\n鞏\n擠\n偶\n棄\n槽\n勁\n乳\n鄧\n吉\n仁\n爛\n磚\n租\n烏\n艦\n伴\n瓜\n淺\n丙\n暫\n燥\n橡\n柳\n迷\n暖\n牌\n秧\n膽\n詳\n簧\n踏\n瓷\n譜\n呆\n賓\n糊\n洛\n輝\n憤\n競\n隙\n怒\n粘\n乃\n緒\n肩\n籍\n敏\n塗\n熙\n皆\n偵\n懸\n掘\n享\n糾\n醒\n狂\n鎖\n淀\n恨\n牲\n霸\n爬\n賞\n逆\n玩\n陵\n祝\n秒\n浙\n貌\n役\n彼\n悉\n鴨\n趨\n鳳\n晨\n畜\n輩\n秩\n卵\n署\n梯\n炎\n灘\n棋\n驅\n篩\n峽\n冒\n啥\n壽\n譯\n浸\n泉\n帽\n遲\n矽\n疆\n貸\n漏\n稿\n冠\n嫩\n脅\n芯\n牢\n叛\n蝕\n奧\n鳴\n嶺\n羊\n憑\n串\n塘\n繪\n酵\n融\n盆\n錫\n廟\n籌\n凍\n輔\n攝\n襲\n筋\n拒\n僚\n旱\n鉀\n鳥\n漆\n沈\n眉\n疏\n添\n棒\n穗\n硝\n韓\n逼\n扭\n僑\n涼\n挺\n碗\n栽\n炒\n杯\n患\n餾\n勸\n豪\n遼\n勃\n鴻\n旦\n吏\n拜\n狗\n埋\n輥\n掩\n飲\n搬\n罵\n辭\n勾\n扣\n估\n蔣\n絨\n霧\n丈\n朵\n姆\n擬\n宇\n輯\n陝\n雕\n償\n蓄\n崇\n剪\n倡\n廳\n咬\n駛\n薯\n刷\n斥\n番\n賦\n奉\n佛\n澆\n漫\n曼\n扇\n鈣\n桃\n扶\n仔\n返\n俗\n虧\n腔\n鞋\n棱\n覆\n框\n悄\n叔\n撞\n騙\n勘\n旺\n沸\n孤\n吐\n孟\n渠\n屈\n疾\n妙\n惜\n仰\n狠\n脹\n諧\n拋\n黴\n桑\n崗\n嘛\n衰\n盜\n滲\n臟\n賴\n湧\n甜\n曹\n閱\n肌\n哩\n厲\n烴\n緯\n毅\n昨\n偽\n症\n煮\n嘆\n釘\n搭\n莖\n籠\n酷\n偷\n弓\n錐\n恆\n傑\n坑\n鼻\n翼\n綸\n敘\n獄\n逮\n罐\n絡\n棚\n抑\n膨\n蔬\n寺\n驟\n穆\n冶\n枯\n冊\n屍\n凸\n紳\n坯\n犧\n焰\n轟\n欣\n晉\n瘦\n禦\n錠\n錦\n喪\n旬\n鍛\n壟\n搜\n撲\n邀\n亭\n酯\n邁\n舒\n脆\n酶\n閒\n憂\n酚\n頑\n羽\n漲\n卸\n仗\n陪\n闢\n懲\n杭\n姚\n肚\n捉\n飄\n漂\n昆\n欺\n吾\n郎\n烷\n汁\n呵\n飾\n蕭\n雅\n郵\n遷\n燕\n撒\n姻\n赴\n宴\n煩\n債\n帳\n斑\n鈴\n旨\n醇\n董\n餅\n雛\n姿\n拌\n傅\n腹\n妥\n揉\n賢\n拆\n歪\n葡\n胺\n丟\n浩\n徽\n昂\n墊\n擋\n覽\n貪\n慰\n繳\n汪\n慌\n馮\n諾\n姜\n誼\n兇\n劣\n誣\n耀\n昏\n躺\n盈\n騎\n喬\n溪\n叢\n盧\n抹\n悶\n諮\n刮\n駕\n纜\n悟\n摘\n鉺\n擲\n頗\n幻\n柄\n惠\n慘\n佳\n仇\n臘\n窩\n滌\n劍\n瞧\n堡\n潑\n蔥\n罩\n霍\n撈\n胎\n蒼\n濱\n倆\n捅\n湘\n砍\n霞\n邵\n萄\n瘋\n淮\n遂\n熊\n糞\n烘\n宿\n檔\n戈\n駁\n嫂\n裕\n徙\n箭\n捐\n腸\n撐\n曬\n辨\n殿\n蓮\n攤\n攪\n醬\n屏\n疫\n哀\n蔡\n堵\n沫\n皺\n暢\n疊\n閣\n萊\n敲\n轄\n鉤\n痕\n壩\n巷\n餓\n禍\n丘\n玄\n溜\n曰\n邏\n彭\n嘗\n卿\n妨\n艇\n吞\n韋\n怨\n矮\n歇\n" + ); + dico.Add( + "english", + "abandon\nability\nable\nabout\nabove\nabsent\nabsorb\nabstract\nabsurd\nabuse\naccess\naccident\naccount\naccuse\nachieve\nacid\nacoustic\nacquire\nacross\nact\naction\nactor\nactress\nactual\nadapt\nadd\naddict\naddress\nadjust\nadmit\nadult\nadvance\nadvice\naerobic\naffair\nafford\nafraid\nagain\nage\nagent\nagree\nahead\naim\nair\nairport\naisle\nalarm\nalbum\nalcohol\nalert\nalien\nall\nalley\nallow\nalmost\nalone\nalpha\nalready\nalso\nalter\nalways\namateur\namazing\namong\namount\namused\nanalyst\nanchor\nancient\nanger\nangle\nangry\nanimal\nankle\nannounce\nannual\nanother\nanswer\nantenna\nantique\nanxiety\nany\napart\napology\nappear\napple\napprove\napril\narch\narctic\narea\narena\nargue\narm\narmed\narmor\narmy\naround\narrange\narrest\narrive\narrow\nart\nartefact\nartist\nartwork\nask\naspect\nassault\nasset\nassist\nassume\nasthma\nathlete\natom\nattack\nattend\nattitude\nattract\nauction\naudit\naugust\naunt\nauthor\nauto\nautumn\naverage\navocado\navoid\nawake\naware\naway\nawesome\nawful\nawkward\naxis\nbaby\nbachelor\nbacon\nbadge\nbag\nbalance\nbalcony\nball\nbamboo\nbanana\nbanner\nbar\nbarely\nbargain\nbarrel\nbase\nbasic\nbasket\nbattle\nbeach\nbean\nbeauty\nbecause\nbecome\nbeef\nbefore\nbegin\nbehave\nbehind\nbelieve\nbelow\nbelt\nbench\nbenefit\nbest\nbetray\nbetter\nbetween\nbeyond\nbicycle\nbid\nbike\nbind\nbiology\nbird\nbirth\nbitter\nblack\nblade\nblame\nblanket\nblast\nbleak\nbless\nblind\nblood\nblossom\nblouse\nblue\nblur\nblush\nboard\nboat\nbody\nboil\nbomb\nbone\nbonus\nbook\nboost\nborder\nboring\nborrow\nboss\nbottom\nbounce\nbox\nboy\nbracket\nbrain\nbrand\nbrass\nbrave\nbread\nbreeze\nbrick\nbridge\nbrief\nbright\nbring\nbrisk\nbroccoli\nbroken\nbronze\nbroom\nbrother\nbrown\nbrush\nbubble\nbuddy\nbudget\nbuffalo\nbuild\nbulb\nbulk\nbullet\nbundle\nbunker\nburden\nburger\nburst\nbus\nbusiness\nbusy\nbutter\nbuyer\nbuzz\ncabbage\ncabin\ncable\ncactus\ncage\ncake\ncall\ncalm\ncamera\ncamp\ncan\ncanal\ncancel\ncandy\ncannon\ncanoe\ncanvas\ncanyon\ncapable\ncapital\ncaptain\ncar\ncarbon\ncard\ncargo\ncarpet\ncarry\ncart\ncase\ncash\ncasino\ncastle\ncasual\ncat\ncatalog\ncatch\ncategory\ncattle\ncaught\ncause\ncaution\ncave\nceiling\ncelery\ncement\ncensus\ncentury\ncereal\ncertain\nchair\nchalk\nchampion\nchange\nchaos\nchapter\ncharge\nchase\nchat\ncheap\ncheck\ncheese\nchef\ncherry\nchest\nchicken\nchief\nchild\nchimney\nchoice\nchoose\nchronic\nchuckle\nchunk\nchurn\ncigar\ncinnamon\ncircle\ncitizen\ncity\ncivil\nclaim\nclap\nclarify\nclaw\nclay\nclean\nclerk\nclever\nclick\nclient\ncliff\nclimb\nclinic\nclip\nclock\nclog\nclose\ncloth\ncloud\nclown\nclub\nclump\ncluster\nclutch\ncoach\ncoast\ncoconut\ncode\ncoffee\ncoil\ncoin\ncollect\ncolor\ncolumn\ncombine\ncome\ncomfort\ncomic\ncommon\ncompany\nconcert\nconduct\nconfirm\ncongress\nconnect\nconsider\ncontrol\nconvince\ncook\ncool\ncopper\ncopy\ncoral\ncore\ncorn\ncorrect\ncost\ncotton\ncouch\ncountry\ncouple\ncourse\ncousin\ncover\ncoyote\ncrack\ncradle\ncraft\ncram\ncrane\ncrash\ncrater\ncrawl\ncrazy\ncream\ncredit\ncreek\ncrew\ncricket\ncrime\ncrisp\ncritic\ncrop\ncross\ncrouch\ncrowd\ncrucial\ncruel\ncruise\ncrumble\ncrunch\ncrush\ncry\ncrystal\ncube\nculture\ncup\ncupboard\ncurious\ncurrent\ncurtain\ncurve\ncushion\ncustom\ncute\ncycle\ndad\ndamage\ndamp\ndance\ndanger\ndaring\ndash\ndaughter\ndawn\nday\ndeal\ndebate\ndebris\ndecade\ndecember\ndecide\ndecline\ndecorate\ndecrease\ndeer\ndefense\ndefine\ndefy\ndegree\ndelay\ndeliver\ndemand\ndemise\ndenial\ndentist\ndeny\ndepart\ndepend\ndeposit\ndepth\ndeputy\nderive\ndescribe\ndesert\ndesign\ndesk\ndespair\ndestroy\ndetail\ndetect\ndevelop\ndevice\ndevote\ndiagram\ndial\ndiamond\ndiary\ndice\ndiesel\ndiet\ndiffer\ndigital\ndignity\ndilemma\ndinner\ndinosaur\ndirect\ndirt\ndisagree\ndiscover\ndisease\ndish\ndismiss\ndisorder\ndisplay\ndistance\ndivert\ndivide\ndivorce\ndizzy\ndoctor\ndocument\ndog\ndoll\ndolphin\ndomain\ndonate\ndonkey\ndonor\ndoor\ndose\ndouble\ndove\ndraft\ndragon\ndrama\ndrastic\ndraw\ndream\ndress\ndrift\ndrill\ndrink\ndrip\ndrive\ndrop\ndrum\ndry\nduck\ndumb\ndune\nduring\ndust\ndutch\nduty\ndwarf\ndynamic\neager\neagle\nearly\nearn\nearth\neasily\neast\neasy\necho\necology\neconomy\nedge\nedit\neducate\neffort\negg\neight\neither\nelbow\nelder\nelectric\nelegant\nelement\nelephant\nelevator\nelite\nelse\nembark\nembody\nembrace\nemerge\nemotion\nemploy\nempower\nempty\nenable\nenact\nend\nendless\nendorse\nenemy\nenergy\nenforce\nengage\nengine\nenhance\nenjoy\nenlist\nenough\nenrich\nenroll\nensure\nenter\nentire\nentry\nenvelope\nepisode\nequal\nequip\nera\nerase\nerode\nerosion\nerror\nerupt\nescape\nessay\nessence\nestate\neternal\nethics\nevidence\nevil\nevoke\nevolve\nexact\nexample\nexcess\nexchange\nexcite\nexclude\nexcuse\nexecute\nexercise\nexhaust\nexhibit\nexile\nexist\nexit\nexotic\nexpand\nexpect\nexpire\nexplain\nexpose\nexpress\nextend\nextra\neye\neyebrow\nfabric\nface\nfaculty\nfade\nfaint\nfaith\nfall\nfalse\nfame\nfamily\nfamous\nfan\nfancy\nfantasy\nfarm\nfashion\nfat\nfatal\nfather\nfatigue\nfault\nfavorite\nfeature\nfebruary\nfederal\nfee\nfeed\nfeel\nfemale\nfence\nfestival\nfetch\nfever\nfew\nfiber\nfiction\nfield\nfigure\nfile\nfilm\nfilter\nfinal\nfind\nfine\nfinger\nfinish\nfire\nfirm\nfirst\nfiscal\nfish\nfit\nfitness\nfix\nflag\nflame\nflash\nflat\nflavor\nflee\nflight\nflip\nfloat\nflock\nfloor\nflower\nfluid\nflush\nfly\nfoam\nfocus\nfog\nfoil\nfold\nfollow\nfood\nfoot\nforce\nforest\nforget\nfork\nfortune\nforum\nforward\nfossil\nfoster\nfound\nfox\nfragile\nframe\nfrequent\nfresh\nfriend\nfringe\nfrog\nfront\nfrost\nfrown\nfrozen\nfruit\nfuel\nfun\nfunny\nfurnace\nfury\nfuture\ngadget\ngain\ngalaxy\ngallery\ngame\ngap\ngarage\ngarbage\ngarden\ngarlic\ngarment\ngas\ngasp\ngate\ngather\ngauge\ngaze\ngeneral\ngenius\ngenre\ngentle\ngenuine\ngesture\nghost\ngiant\ngift\ngiggle\nginger\ngiraffe\ngirl\ngive\nglad\nglance\nglare\nglass\nglide\nglimpse\nglobe\ngloom\nglory\nglove\nglow\nglue\ngoat\ngoddess\ngold\ngood\ngoose\ngorilla\ngospel\ngossip\ngovern\ngown\ngrab\ngrace\ngrain\ngrant\ngrape\ngrass\ngravity\ngreat\ngreen\ngrid\ngrief\ngrit\ngrocery\ngroup\ngrow\ngrunt\nguard\nguess\nguide\nguilt\nguitar\ngun\ngym\nhabit\nhair\nhalf\nhammer\nhamster\nhand\nhappy\nharbor\nhard\nharsh\nharvest\nhat\nhave\nhawk\nhazard\nhead\nhealth\nheart\nheavy\nhedgehog\nheight\nhello\nhelmet\nhelp\nhen\nhero\nhidden\nhigh\nhill\nhint\nhip\nhire\nhistory\nhobby\nhockey\nhold\nhole\nholiday\nhollow\nhome\nhoney\nhood\nhope\nhorn\nhorror\nhorse\nhospital\nhost\nhotel\nhour\nhover\nhub\nhuge\nhuman\nhumble\nhumor\nhundred\nhungry\nhunt\nhurdle\nhurry\nhurt\nhusband\nhybrid\nice\nicon\nidea\nidentify\nidle\nignore\nill\nillegal\nillness\nimage\nimitate\nimmense\nimmune\nimpact\nimpose\nimprove\nimpulse\ninch\ninclude\nincome\nincrease\nindex\nindicate\nindoor\nindustry\ninfant\ninflict\ninform\ninhale\ninherit\ninitial\ninject\ninjury\ninmate\ninner\ninnocent\ninput\ninquiry\ninsane\ninsect\ninside\ninspire\ninstall\nintact\ninterest\ninto\ninvest\ninvite\ninvolve\niron\nisland\nisolate\nissue\nitem\nivory\njacket\njaguar\njar\njazz\njealous\njeans\njelly\njewel\njob\njoin\njoke\njourney\njoy\njudge\njuice\njump\njungle\njunior\njunk\njust\nkangaroo\nkeen\nkeep\nketchup\nkey\nkick\nkid\nkidney\nkind\nkingdom\nkiss\nkit\nkitchen\nkite\nkitten\nkiwi\nknee\nknife\nknock\nknow\nlab\nlabel\nlabor\nladder\nlady\nlake\nlamp\nlanguage\nlaptop\nlarge\nlater\nlatin\nlaugh\nlaundry\nlava\nlaw\nlawn\nlawsuit\nlayer\nlazy\nleader\nleaf\nlearn\nleave\nlecture\nleft\nleg\nlegal\nlegend\nleisure\nlemon\nlend\nlength\nlens\nleopard\nlesson\nletter\nlevel\nliar\nliberty\nlibrary\nlicense\nlife\nlift\nlight\nlike\nlimb\nlimit\nlink\nlion\nliquid\nlist\nlittle\nlive\nlizard\nload\nloan\nlobster\nlocal\nlock\nlogic\nlonely\nlong\nloop\nlottery\nloud\nlounge\nlove\nloyal\nlucky\nluggage\nlumber\nlunar\nlunch\nluxury\nlyrics\nmachine\nmad\nmagic\nmagnet\nmaid\nmail\nmain\nmajor\nmake\nmammal\nman\nmanage\nmandate\nmango\nmansion\nmanual\nmaple\nmarble\nmarch\nmargin\nmarine\nmarket\nmarriage\nmask\nmass\nmaster\nmatch\nmaterial\nmath\nmatrix\nmatter\nmaximum\nmaze\nmeadow\nmean\nmeasure\nmeat\nmechanic\nmedal\nmedia\nmelody\nmelt\nmember\nmemory\nmention\nmenu\nmercy\nmerge\nmerit\nmerry\nmesh\nmessage\nmetal\nmethod\nmiddle\nmidnight\nmilk\nmillion\nmimic\nmind\nminimum\nminor\nminute\nmiracle\nmirror\nmisery\nmiss\nmistake\nmix\nmixed\nmixture\nmobile\nmodel\nmodify\nmom\nmoment\nmonitor\nmonkey\nmonster\nmonth\nmoon\nmoral\nmore\nmorning\nmosquito\nmother\nmotion\nmotor\nmountain\nmouse\nmove\nmovie\nmuch\nmuffin\nmule\nmultiply\nmuscle\nmuseum\nmushroom\nmusic\nmust\nmutual\nmyself\nmystery\nmyth\nnaive\nname\nnapkin\nnarrow\nnasty\nnation\nnature\nnear\nneck\nneed\nnegative\nneglect\nneither\nnephew\nnerve\nnest\nnet\nnetwork\nneutral\nnever\nnews\nnext\nnice\nnight\nnoble\nnoise\nnominee\nnoodle\nnormal\nnorth\nnose\nnotable\nnote\nnothing\nnotice\nnovel\nnow\nnuclear\nnumber\nnurse\nnut\noak\nobey\nobject\noblige\nobscure\nobserve\nobtain\nobvious\noccur\nocean\noctober\nodor\noff\noffer\noffice\noften\noil\nokay\nold\nolive\nolympic\nomit\nonce\none\nonion\nonline\nonly\nopen\nopera\nopinion\noppose\noption\norange\norbit\norchard\norder\nordinary\norgan\norient\noriginal\norphan\nostrich\nother\noutdoor\nouter\noutput\noutside\noval\noven\nover\nown\nowner\noxygen\noyster\nozone\npact\npaddle\npage\npair\npalace\npalm\npanda\npanel\npanic\npanther\npaper\nparade\nparent\npark\nparrot\nparty\npass\npatch\npath\npatient\npatrol\npattern\npause\npave\npayment\npeace\npeanut\npear\npeasant\npelican\npen\npenalty\npencil\npeople\npepper\nperfect\npermit\nperson\npet\nphone\nphoto\nphrase\nphysical\npiano\npicnic\npicture\npiece\npig\npigeon\npill\npilot\npink\npioneer\npipe\npistol\npitch\npizza\nplace\nplanet\nplastic\nplate\nplay\nplease\npledge\npluck\nplug\nplunge\npoem\npoet\npoint\npolar\npole\npolice\npond\npony\npool\npopular\nportion\nposition\npossible\npost\npotato\npottery\npoverty\npowder\npower\npractice\npraise\npredict\nprefer\nprepare\npresent\npretty\nprevent\nprice\npride\nprimary\nprint\npriority\nprison\nprivate\nprize\nproblem\nprocess\nproduce\nprofit\nprogram\nproject\npromote\nproof\nproperty\nprosper\nprotect\nproud\nprovide\npublic\npudding\npull\npulp\npulse\npumpkin\npunch\npupil\npuppy\npurchase\npurity\npurpose\npurse\npush\nput\npuzzle\npyramid\nquality\nquantum\nquarter\nquestion\nquick\nquit\nquiz\nquote\nrabbit\nraccoon\nrace\nrack\nradar\nradio\nrail\nrain\nraise\nrally\nramp\nranch\nrandom\nrange\nrapid\nrare\nrate\nrather\nraven\nraw\nrazor\nready\nreal\nreason\nrebel\nrebuild\nrecall\nreceive\nrecipe\nrecord\nrecycle\nreduce\nreflect\nreform\nrefuse\nregion\nregret\nregular\nreject\nrelax\nrelease\nrelief\nrely\nremain\nremember\nremind\nremove\nrender\nrenew\nrent\nreopen\nrepair\nrepeat\nreplace\nreport\nrequire\nrescue\nresemble\nresist\nresource\nresponse\nresult\nretire\nretreat\nreturn\nreunion\nreveal\nreview\nreward\nrhythm\nrib\nribbon\nrice\nrich\nride\nridge\nrifle\nright\nrigid\nring\nriot\nripple\nrisk\nritual\nrival\nriver\nroad\nroast\nrobot\nrobust\nrocket\nromance\nroof\nrookie\nroom\nrose\nrotate\nrough\nround\nroute\nroyal\nrubber\nrude\nrug\nrule\nrun\nrunway\nrural\nsad\nsaddle\nsadness\nsafe\nsail\nsalad\nsalmon\nsalon\nsalt\nsalute\nsame\nsample\nsand\nsatisfy\nsatoshi\nsauce\nsausage\nsave\nsay\nscale\nscan\nscare\nscatter\nscene\nscheme\nschool\nscience\nscissors\nscorpion\nscout\nscrap\nscreen\nscript\nscrub\nsea\nsearch\nseason\nseat\nsecond\nsecret\nsection\nsecurity\nseed\nseek\nsegment\nselect\nsell\nseminar\nsenior\nsense\nsentence\nseries\nservice\nsession\nsettle\nsetup\nseven\nshadow\nshaft\nshallow\nshare\nshed\nshell\nsheriff\nshield\nshift\nshine\nship\nshiver\nshock\nshoe\nshoot\nshop\nshort\nshoulder\nshove\nshrimp\nshrug\nshuffle\nshy\nsibling\nsick\nside\nsiege\nsight\nsign\nsilent\nsilk\nsilly\nsilver\nsimilar\nsimple\nsince\nsing\nsiren\nsister\nsituate\nsix\nsize\nskate\nsketch\nski\nskill\nskin\nskirt\nskull\nslab\nslam\nsleep\nslender\nslice\nslide\nslight\nslim\nslogan\nslot\nslow\nslush\nsmall\nsmart\nsmile\nsmoke\nsmooth\nsnack\nsnake\nsnap\nsniff\nsnow\nsoap\nsoccer\nsocial\nsock\nsoda\nsoft\nsolar\nsoldier\nsolid\nsolution\nsolve\nsomeone\nsong\nsoon\nsorry\nsort\nsoul\nsound\nsoup\nsource\nsouth\nspace\nspare\nspatial\nspawn\nspeak\nspecial\nspeed\nspell\nspend\nsphere\nspice\nspider\nspike\nspin\nspirit\nsplit\nspoil\nsponsor\nspoon\nsport\nspot\nspray\nspread\nspring\nspy\nsquare\nsqueeze\nsquirrel\nstable\nstadium\nstaff\nstage\nstairs\nstamp\nstand\nstart\nstate\nstay\nsteak\nsteel\nstem\nstep\nstereo\nstick\nstill\nsting\nstock\nstomach\nstone\nstool\nstory\nstove\nstrategy\nstreet\nstrike\nstrong\nstruggle\nstudent\nstuff\nstumble\nstyle\nsubject\nsubmit\nsubway\nsuccess\nsuch\nsudden\nsuffer\nsugar\nsuggest\nsuit\nsummer\nsun\nsunny\nsunset\nsuper\nsupply\nsupreme\nsure\nsurface\nsurge\nsurprise\nsurround\nsurvey\nsuspect\nsustain\nswallow\nswamp\nswap\nswarm\nswear\nsweet\nswift\nswim\nswing\nswitch\nsword\nsymbol\nsymptom\nsyrup\nsystem\ntable\ntackle\ntag\ntail\ntalent\ntalk\ntank\ntape\ntarget\ntask\ntaste\ntattoo\ntaxi\nteach\nteam\ntell\nten\ntenant\ntennis\ntent\nterm\ntest\ntext\nthank\nthat\ntheme\nthen\ntheory\nthere\nthey\nthing\nthis\nthought\nthree\nthrive\nthrow\nthumb\nthunder\nticket\ntide\ntiger\ntilt\ntimber\ntime\ntiny\ntip\ntired\ntissue\ntitle\ntoast\ntobacco\ntoday\ntoddler\ntoe\ntogether\ntoilet\ntoken\ntomato\ntomorrow\ntone\ntongue\ntonight\ntool\ntooth\ntop\ntopic\ntopple\ntorch\ntornado\ntortoise\ntoss\ntotal\ntourist\ntoward\ntower\ntown\ntoy\ntrack\ntrade\ntraffic\ntragic\ntrain\ntransfer\ntrap\ntrash\ntravel\ntray\ntreat\ntree\ntrend\ntrial\ntribe\ntrick\ntrigger\ntrim\ntrip\ntrophy\ntrouble\ntruck\ntrue\ntruly\ntrumpet\ntrust\ntruth\ntry\ntube\ntuition\ntumble\ntuna\ntunnel\nturkey\nturn\nturtle\ntwelve\ntwenty\ntwice\ntwin\ntwist\ntwo\ntype\ntypical\nugly\numbrella\nunable\nunaware\nuncle\nuncover\nunder\nundo\nunfair\nunfold\nunhappy\nuniform\nunique\nunit\nuniverse\nunknown\nunlock\nuntil\nunusual\nunveil\nupdate\nupgrade\nuphold\nupon\nupper\nupset\nurban\nurge\nusage\nuse\nused\nuseful\nuseless\nusual\nutility\nvacant\nvacuum\nvague\nvalid\nvalley\nvalve\nvan\nvanish\nvapor\nvarious\nvast\nvault\nvehicle\nvelvet\nvendor\nventure\nvenue\nverb\nverify\nversion\nvery\nvessel\nveteran\nviable\nvibrant\nvicious\nvictory\nvideo\nview\nvillage\nvintage\nviolin\nvirtual\nvirus\nvisa\nvisit\nvisual\nvital\nvivid\nvocal\nvoice\nvoid\nvolcano\nvolume\nvote\nvoyage\nwage\nwagon\nwait\nwalk\nwall\nwalnut\nwant\nwarfare\nwarm\nwarrior\nwash\nwasp\nwaste\nwater\nwave\nway\nwealth\nweapon\nwear\nweasel\nweather\nweb\nwedding\nweekend\nweird\nwelcome\nwest\nwet\nwhale\nwhat\nwheat\nwheel\nwhen\nwhere\nwhip\nwhisper\nwide\nwidth\nwife\nwild\nwill\nwin\nwindow\nwine\nwing\nwink\nwinner\nwinter\nwire\nwisdom\nwise\nwish\nwitness\nwolf\nwoman\nwonder\nwood\nwool\nword\nwork\nworld\nworry\nworth\nwrap\nwreck\nwrestle\nwrist\nwrite\nwrong\nyard\nyear\nyellow\nyou\nyoung\nyouth\nzebra\nzero\nzone\nzoo\n" + ); + dico.Add( + "japanese", + "あいこくしん\nあいさつ\nあいだ\nあおぞら\nあかちゃん\nあきる\nあけがた\nあける\nあこがれる\nあさい\nあさひ\nあしあと\nあじわう\nあずかる\nあずき\nあそぶ\nあたえる\nあたためる\nあたりまえ\nあたる\nあつい\nあつかう\nあっしゅく\nあつまり\nあつめる\nあてな\nあてはまる\nあひる\nあぶら\nあぶる\nあふれる\nあまい\nあまど\nあまやかす\nあまり\nあみもの\nあめりか\nあやまる\nあゆむ\nあらいぐま\nあらし\nあらすじ\nあらためる\nあらゆる\nあらわす\nありがとう\nあわせる\nあわてる\nあんい\nあんがい\nあんこ\nあんぜん\nあんてい\nあんない\nあんまり\nいいだす\nいおん\nいがい\nいがく\nいきおい\nいきなり\nいきもの\nいきる\nいくじ\nいくぶん\nいけばな\nいけん\nいこう\nいこく\nいこつ\nいさましい\nいさん\nいしき\nいじゅう\nいじょう\nいじわる\nいずみ\nいずれ\nいせい\nいせえび\nいせかい\nいせき\nいぜん\nいそうろう\nいそがしい\nいだい\nいだく\nいたずら\nいたみ\nいたりあ\nいちおう\nいちじ\nいちど\nいちば\nいちぶ\nいちりゅう\nいつか\nいっしゅん\nいっせい\nいっそう\nいったん\nいっち\nいってい\nいっぽう\nいてざ\nいてん\nいどう\nいとこ\nいない\nいなか\nいねむり\nいのち\nいのる\nいはつ\nいばる\nいはん\nいびき\nいひん\nいふく\nいへん\nいほう\nいみん\nいもうと\nいもたれ\nいもり\nいやがる\nいやす\nいよかん\nいよく\nいらい\nいらすと\nいりぐち\nいりょう\nいれい\nいれもの\nいれる\nいろえんぴつ\nいわい\nいわう\nいわかん\nいわば\nいわゆる\nいんげんまめ\nいんさつ\nいんしょう\nいんよう\nうえき\nうえる\nうおざ\nうがい\nうかぶ\nうかべる\nうきわ\nうくらいな\nうくれれ\nうけたまわる\nうけつけ\nうけとる\nうけもつ\nうける\nうごかす\nうごく\nうこん\nうさぎ\nうしなう\nうしろがみ\nうすい\nうすぎ\nうすぐらい\nうすめる\nうせつ\nうちあわせ\nうちがわ\nうちき\nうちゅう\nうっかり\nうつくしい\nうったえる\nうつる\nうどん\nうなぎ\nうなじ\nうなずく\nうなる\nうねる\nうのう\nうぶげ\nうぶごえ\nうまれる\nうめる\nうもう\nうやまう\nうよく\nうらがえす\nうらぐち\nうらない\nうりあげ\nうりきれ\nうるさい\nうれしい\nうれゆき\nうれる\nうろこ\nうわき\nうわさ\nうんこう\nうんちん\nうんてん\nうんどう\nえいえん\nえいが\nえいきょう\nえいご\nえいせい\nえいぶん\nえいよう\nえいわ\nえおり\nえがお\nえがく\nえきたい\nえくせる\nえしゃく\nえすて\nえつらん\nえのぐ\nえほうまき\nえほん\nえまき\nえもじ\nえもの\nえらい\nえらぶ\nえりあ\nえんえん\nえんかい\nえんぎ\nえんげき\nえんしゅう\nえんぜつ\nえんそく\nえんちょう\nえんとつ\nおいかける\nおいこす\nおいしい\nおいつく\nおうえん\nおうさま\nおうじ\nおうせつ\nおうたい\nおうふく\nおうべい\nおうよう\nおえる\nおおい\nおおう\nおおどおり\nおおや\nおおよそ\nおかえり\nおかず\nおがむ\nおかわり\nおぎなう\nおきる\nおくさま\nおくじょう\nおくりがな\nおくる\nおくれる\nおこす\nおこなう\nおこる\nおさえる\nおさない\nおさめる\nおしいれ\nおしえる\nおじぎ\nおじさん\nおしゃれ\nおそらく\nおそわる\nおたがい\nおたく\nおだやか\nおちつく\nおっと\nおつり\nおでかけ\nおとしもの\nおとなしい\nおどり\nおどろかす\nおばさん\nおまいり\nおめでとう\nおもいで\nおもう\nおもたい\nおもちゃ\nおやつ\nおやゆび\nおよぼす\nおらんだ\nおろす\nおんがく\nおんけい\nおんしゃ\nおんせん\nおんだん\nおんちゅう\nおんどけい\nかあつ\nかいが\nがいき\nがいけん\nがいこう\nかいさつ\nかいしゃ\nかいすいよく\nかいぜん\nかいぞうど\nかいつう\nかいてん\nかいとう\nかいふく\nがいへき\nかいほう\nかいよう\nがいらい\nかいわ\nかえる\nかおり\nかかえる\nかがく\nかがし\nかがみ\nかくご\nかくとく\nかざる\nがぞう\nかたい\nかたち\nがちょう\nがっきゅう\nがっこう\nがっさん\nがっしょう\nかなざわし\nかのう\nがはく\nかぶか\nかほう\nかほご\nかまう\nかまぼこ\nかめれおん\nかゆい\nかようび\nからい\nかるい\nかろう\nかわく\nかわら\nがんか\nかんけい\nかんこう\nかんしゃ\nかんそう\nかんたん\nかんち\nがんばる\nきあい\nきあつ\nきいろ\nぎいん\nきうい\nきうん\nきえる\nきおう\nきおく\nきおち\nきおん\nきかい\nきかく\nきかんしゃ\nききて\nきくばり\nきくらげ\nきけんせい\nきこう\nきこえる\nきこく\nきさい\nきさく\nきさま\nきさらぎ\nぎじかがく\nぎしき\nぎじたいけん\nぎじにってい\nぎじゅつしゃ\nきすう\nきせい\nきせき\nきせつ\nきそう\nきぞく\nきぞん\nきたえる\nきちょう\nきつえん\nぎっちり\nきつつき\nきつね\nきてい\nきどう\nきどく\nきない\nきなが\nきなこ\nきぬごし\nきねん\nきのう\nきのした\nきはく\nきびしい\nきひん\nきふく\nきぶん\nきぼう\nきほん\nきまる\nきみつ\nきむずかしい\nきめる\nきもだめし\nきもち\nきもの\nきゃく\nきやく\nぎゅうにく\nきよう\nきょうりゅう\nきらい\nきらく\nきりん\nきれい\nきれつ\nきろく\nぎろん\nきわめる\nぎんいろ\nきんかくじ\nきんじょ\nきんようび\nぐあい\nくいず\nくうかん\nくうき\nくうぐん\nくうこう\nぐうせい\nくうそう\nぐうたら\nくうふく\nくうぼ\nくかん\nくきょう\nくげん\nぐこう\nくさい\nくさき\nくさばな\nくさる\nくしゃみ\nくしょう\nくすのき\nくすりゆび\nくせげ\nくせん\nぐたいてき\nくださる\nくたびれる\nくちこみ\nくちさき\nくつした\nぐっすり\nくつろぐ\nくとうてん\nくどく\nくなん\nくねくね\nくのう\nくふう\nくみあわせ\nくみたてる\nくめる\nくやくしょ\nくらす\nくらべる\nくるま\nくれる\nくろう\nくわしい\nぐんかん\nぐんしょく\nぐんたい\nぐんて\nけあな\nけいかく\nけいけん\nけいこ\nけいさつ\nげいじゅつ\nけいたい\nげいのうじん\nけいれき\nけいろ\nけおとす\nけおりもの\nげきか\nげきげん\nげきだん\nげきちん\nげきとつ\nげきは\nげきやく\nげこう\nげこくじょう\nげざい\nけさき\nげざん\nけしき\nけしごむ\nけしょう\nげすと\nけたば\nけちゃっぷ\nけちらす\nけつあつ\nけつい\nけつえき\nけっこん\nけつじょ\nけっせき\nけってい\nけつまつ\nげつようび\nげつれい\nけつろん\nげどく\nけとばす\nけとる\nけなげ\nけなす\nけなみ\nけぬき\nげねつ\nけねん\nけはい\nげひん\nけぶかい\nげぼく\nけまり\nけみかる\nけむし\nけむり\nけもの\nけらい\nけろけろ\nけわしい\nけんい\nけんえつ\nけんお\nけんか\nげんき\nけんげん\nけんこう\nけんさく\nけんしゅう\nけんすう\nげんそう\nけんちく\nけんてい\nけんとう\nけんない\nけんにん\nげんぶつ\nけんま\nけんみん\nけんめい\nけんらん\nけんり\nこあくま\nこいぬ\nこいびと\nごうい\nこうえん\nこうおん\nこうかん\nごうきゅう\nごうけい\nこうこう\nこうさい\nこうじ\nこうすい\nごうせい\nこうそく\nこうたい\nこうちゃ\nこうつう\nこうてい\nこうどう\nこうない\nこうはい\nごうほう\nごうまん\nこうもく\nこうりつ\nこえる\nこおり\nごかい\nごがつ\nごかん\nこくご\nこくさい\nこくとう\nこくない\nこくはく\nこぐま\nこけい\nこける\nここのか\nこころ\nこさめ\nこしつ\nこすう\nこせい\nこせき\nこぜん\nこそだて\nこたい\nこたえる\nこたつ\nこちょう\nこっか\nこつこつ\nこつばん\nこつぶ\nこてい\nこてん\nことがら\nことし\nことば\nことり\nこなごな\nこねこね\nこのまま\nこのみ\nこのよ\nごはん\nこひつじ\nこふう\nこふん\nこぼれる\nごまあぶら\nこまかい\nごますり\nこまつな\nこまる\nこむぎこ\nこもじ\nこもち\nこもの\nこもん\nこやく\nこやま\nこゆう\nこゆび\nこよい\nこよう\nこりる\nこれくしょん\nころっけ\nこわもて\nこわれる\nこんいん\nこんかい\nこんき\nこんしゅう\nこんすい\nこんだて\nこんとん\nこんなん\nこんびに\nこんぽん\nこんまけ\nこんや\nこんれい\nこんわく\nざいえき\nさいかい\nさいきん\nざいげん\nざいこ\nさいしょ\nさいせい\nざいたく\nざいちゅう\nさいてき\nざいりょう\nさうな\nさかいし\nさがす\nさかな\nさかみち\nさがる\nさぎょう\nさくし\nさくひん\nさくら\nさこく\nさこつ\nさずかる\nざせき\nさたん\nさつえい\nざつおん\nざっか\nざつがく\nさっきょく\nざっし\nさつじん\nざっそう\nさつたば\nさつまいも\nさてい\nさといも\nさとう\nさとおや\nさとし\nさとる\nさのう\nさばく\nさびしい\nさべつ\nさほう\nさほど\nさます\nさみしい\nさみだれ\nさむけ\nさめる\nさやえんどう\nさゆう\nさよう\nさよく\nさらだ\nざるそば\nさわやか\nさわる\nさんいん\nさんか\nさんきゃく\nさんこう\nさんさい\nざんしょ\nさんすう\nさんせい\nさんそ\nさんち\nさんま\nさんみ\nさんらん\nしあい\nしあげ\nしあさって\nしあわせ\nしいく\nしいん\nしうち\nしえい\nしおけ\nしかい\nしかく\nじかん\nしごと\nしすう\nじだい\nしたうけ\nしたぎ\nしたて\nしたみ\nしちょう\nしちりん\nしっかり\nしつじ\nしつもん\nしてい\nしてき\nしてつ\nじてん\nじどう\nしなぎれ\nしなもの\nしなん\nしねま\nしねん\nしのぐ\nしのぶ\nしはい\nしばかり\nしはつ\nしはらい\nしはん\nしひょう\nしふく\nじぶん\nしへい\nしほう\nしほん\nしまう\nしまる\nしみん\nしむける\nじむしょ\nしめい\nしめる\nしもん\nしゃいん\nしゃうん\nしゃおん\nじゃがいも\nしやくしょ\nしゃくほう\nしゃけん\nしゃこ\nしゃざい\nしゃしん\nしゃせん\nしゃそう\nしゃたい\nしゃちょう\nしゃっきん\nじゃま\nしゃりん\nしゃれい\nじゆう\nじゅうしょ\nしゅくはく\nじゅしん\nしゅっせき\nしゅみ\nしゅらば\nじゅんばん\nしょうかい\nしょくたく\nしょっけん\nしょどう\nしょもつ\nしらせる\nしらべる\nしんか\nしんこう\nじんじゃ\nしんせいじ\nしんちく\nしんりん\nすあげ\nすあし\nすあな\nずあん\nすいえい\nすいか\nすいとう\nずいぶん\nすいようび\nすうがく\nすうじつ\nすうせん\nすおどり\nすきま\nすくう\nすくない\nすける\nすごい\nすこし\nずさん\nすずしい\nすすむ\nすすめる\nすっかり\nずっしり\nずっと\nすてき\nすてる\nすねる\nすのこ\nすはだ\nすばらしい\nずひょう\nずぶぬれ\nすぶり\nすふれ\nすべて\nすべる\nずほう\nすぼん\nすまい\nすめし\nすもう\nすやき\nすらすら\nするめ\nすれちがう\nすろっと\nすわる\nすんぜん\nすんぽう\nせあぶら\nせいかつ\nせいげん\nせいじ\nせいよう\nせおう\nせかいかん\nせきにん\nせきむ\nせきゆ\nせきらんうん\nせけん\nせこう\nせすじ\nせたい\nせたけ\nせっかく\nせっきゃく\nぜっく\nせっけん\nせっこつ\nせっさたくま\nせつぞく\nせつだん\nせつでん\nせっぱん\nせつび\nせつぶん\nせつめい\nせつりつ\nせなか\nせのび\nせはば\nせびろ\nせぼね\nせまい\nせまる\nせめる\nせもたれ\nせりふ\nぜんあく\nせんい\nせんえい\nせんか\nせんきょ\nせんく\nせんげん\nぜんご\nせんさい\nせんしゅ\nせんすい\nせんせい\nせんぞ\nせんたく\nせんちょう\nせんてい\nせんとう\nせんぬき\nせんねん\nせんぱい\nぜんぶ\nぜんぽう\nせんむ\nせんめんじょ\nせんもん\nせんやく\nせんゆう\nせんよう\nぜんら\nぜんりゃく\nせんれい\nせんろ\nそあく\nそいとげる\nそいね\nそうがんきょう\nそうき\nそうご\nそうしん\nそうだん\nそうなん\nそうび\nそうめん\nそうり\nそえもの\nそえん\nそがい\nそげき\nそこう\nそこそこ\nそざい\nそしな\nそせい\nそせん\nそそぐ\nそだてる\nそつう\nそつえん\nそっかん\nそつぎょう\nそっけつ\nそっこう\nそっせん\nそっと\nそとがわ\nそとづら\nそなえる\nそなた\nそふぼ\nそぼく\nそぼろ\nそまつ\nそまる\nそむく\nそむりえ\nそめる\nそもそも\nそよかぜ\nそらまめ\nそろう\nそんかい\nそんけい\nそんざい\nそんしつ\nそんぞく\nそんちょう\nぞんび\nぞんぶん\nそんみん\nたあい\nたいいん\nたいうん\nたいえき\nたいおう\nだいがく\nたいき\nたいぐう\nたいけん\nたいこ\nたいざい\nだいじょうぶ\nだいすき\nたいせつ\nたいそう\nだいたい\nたいちょう\nたいてい\nだいどころ\nたいない\nたいねつ\nたいのう\nたいはん\nだいひょう\nたいふう\nたいへん\nたいほ\nたいまつばな\nたいみんぐ\nたいむ\nたいめん\nたいやき\nたいよう\nたいら\nたいりょく\nたいる\nたいわん\nたうえ\nたえる\nたおす\nたおる\nたおれる\nたかい\nたかね\nたきび\nたくさん\nたこく\nたこやき\nたさい\nたしざん\nだじゃれ\nたすける\nたずさわる\nたそがれ\nたたかう\nたたく\nただしい\nたたみ\nたちばな\nだっかい\nだっきゃく\nだっこ\nだっしゅつ\nだったい\nたてる\nたとえる\nたなばた\nたにん\nたぬき\nたのしみ\nたはつ\nたぶん\nたべる\nたぼう\nたまご\nたまる\nだむる\nためいき\nためす\nためる\nたもつ\nたやすい\nたよる\nたらす\nたりきほんがん\nたりょう\nたりる\nたると\nたれる\nたれんと\nたろっと\nたわむれる\nだんあつ\nたんい\nたんおん\nたんか\nたんき\nたんけん\nたんご\nたんさん\nたんじょうび\nだんせい\nたんそく\nたんたい\nだんち\nたんてい\nたんとう\nだんな\nたんにん\nだんねつ\nたんのう\nたんぴん\nだんぼう\nたんまつ\nたんめい\nだんれつ\nだんろ\nだんわ\nちあい\nちあん\nちいき\nちいさい\nちえん\nちかい\nちから\nちきゅう\nちきん\nちけいず\nちけん\nちこく\nちさい\nちしき\nちしりょう\nちせい\nちそう\nちたい\nちたん\nちちおや\nちつじょ\nちてき\nちてん\nちぬき\nちぬり\nちのう\nちひょう\nちへいせん\nちほう\nちまた\nちみつ\nちみどろ\nちめいど\nちゃんこなべ\nちゅうい\nちゆりょく\nちょうし\nちょさくけん\nちらし\nちらみ\nちりがみ\nちりょう\nちるど\nちわわ\nちんたい\nちんもく\nついか\nついたち\nつうか\nつうじょう\nつうはん\nつうわ\nつかう\nつかれる\nつくね\nつくる\nつけね\nつける\nつごう\nつたえる\nつづく\nつつじ\nつつむ\nつとめる\nつながる\nつなみ\nつねづね\nつのる\nつぶす\nつまらない\nつまる\nつみき\nつめたい\nつもり\nつもる\nつよい\nつるぼ\nつるみく\nつわもの\nつわり\nてあし\nてあて\nてあみ\nていおん\nていか\nていき\nていけい\nていこく\nていさつ\nていし\nていせい\nていたい\nていど\nていねい\nていひょう\nていへん\nていぼう\nてうち\nておくれ\nてきとう\nてくび\nでこぼこ\nてさぎょう\nてさげ\nてすり\nてそう\nてちがい\nてちょう\nてつがく\nてつづき\nでっぱ\nてつぼう\nてつや\nでぬかえ\nてぬき\nてぬぐい\nてのひら\nてはい\nてぶくろ\nてふだ\nてほどき\nてほん\nてまえ\nてまきずし\nてみじか\nてみやげ\nてらす\nてれび\nてわけ\nてわたし\nでんあつ\nてんいん\nてんかい\nてんき\nてんぐ\nてんけん\nてんごく\nてんさい\nてんし\nてんすう\nでんち\nてんてき\nてんとう\nてんない\nてんぷら\nてんぼうだい\nてんめつ\nてんらんかい\nでんりょく\nでんわ\nどあい\nといれ\nどうかん\nとうきゅう\nどうぐ\nとうし\nとうむぎ\nとおい\nとおか\nとおく\nとおす\nとおる\nとかい\nとかす\nときおり\nときどき\nとくい\nとくしゅう\nとくてん\nとくに\nとくべつ\nとけい\nとける\nとこや\nとさか\nとしょかん\nとそう\nとたん\nとちゅう\nとっきゅう\nとっくん\nとつぜん\nとつにゅう\nとどける\nととのえる\nとない\nとなえる\nとなり\nとのさま\nとばす\nどぶがわ\nとほう\nとまる\nとめる\nともだち\nともる\nどようび\nとらえる\nとんかつ\nどんぶり\nないかく\nないこう\nないしょ\nないす\nないせん\nないそう\nなおす\nながい\nなくす\nなげる\nなこうど\nなさけ\nなたでここ\nなっとう\nなつやすみ\nななおし\nなにごと\nなにもの\nなにわ\nなのか\nなふだ\nなまいき\nなまえ\nなまみ\nなみだ\nなめらか\nなめる\nなやむ\nならう\nならび\nならぶ\nなれる\nなわとび\nなわばり\nにあう\nにいがた\nにうけ\nにおい\nにかい\nにがて\nにきび\nにくしみ\nにくまん\nにげる\nにさんかたんそ\nにしき\nにせもの\nにちじょう\nにちようび\nにっか\nにっき\nにっけい\nにっこう\nにっさん\nにっしょく\nにっすう\nにっせき\nにってい\nになう\nにほん\nにまめ\nにもつ\nにやり\nにゅういん\nにりんしゃ\nにわとり\nにんい\nにんか\nにんき\nにんげん\nにんしき\nにんずう\nにんそう\nにんたい\nにんち\nにんてい\nにんにく\nにんぷ\nにんまり\nにんむ\nにんめい\nにんよう\nぬいくぎ\nぬかす\nぬぐいとる\nぬぐう\nぬくもり\nぬすむ\nぬまえび\nぬめり\nぬらす\nぬんちゃく\nねあげ\nねいき\nねいる\nねいろ\nねぐせ\nねくたい\nねくら\nねこぜ\nねこむ\nねさげ\nねすごす\nねそべる\nねだん\nねつい\nねっしん\nねつぞう\nねったいぎょ\nねぶそく\nねふだ\nねぼう\nねほりはほり\nねまき\nねまわし\nねみみ\nねむい\nねむたい\nねもと\nねらう\nねわざ\nねんいり\nねんおし\nねんかん\nねんきん\nねんぐ\nねんざ\nねんし\nねんちゃく\nねんど\nねんぴ\nねんぶつ\nねんまつ\nねんりょう\nねんれい\nのいず\nのおづま\nのがす\nのきなみ\nのこぎり\nのこす\nのこる\nのせる\nのぞく\nのぞむ\nのたまう\nのちほど\nのっく\nのばす\nのはら\nのべる\nのぼる\nのみもの\nのやま\nのらいぬ\nのらねこ\nのりもの\nのりゆき\nのれん\nのんき\nばあい\nはあく\nばあさん\nばいか\nばいく\nはいけん\nはいご\nはいしん\nはいすい\nはいせん\nはいそう\nはいち\nばいばい\nはいれつ\nはえる\nはおる\nはかい\nばかり\nはかる\nはくしゅ\nはけん\nはこぶ\nはさみ\nはさん\nはしご\nばしょ\nはしる\nはせる\nぱそこん\nはそん\nはたん\nはちみつ\nはつおん\nはっかく\nはづき\nはっきり\nはっくつ\nはっけん\nはっこう\nはっさん\nはっしん\nはったつ\nはっちゅう\nはってん\nはっぴょう\nはっぽう\nはなす\nはなび\nはにかむ\nはぶらし\nはみがき\nはむかう\nはめつ\nはやい\nはやし\nはらう\nはろうぃん\nはわい\nはんい\nはんえい\nはんおん\nはんかく\nはんきょう\nばんぐみ\nはんこ\nはんしゃ\nはんすう\nはんだん\nぱんち\nぱんつ\nはんてい\nはんとし\nはんのう\nはんぱ\nはんぶん\nはんぺん\nはんぼうき\nはんめい\nはんらん\nはんろん\nひいき\nひうん\nひえる\nひかく\nひかり\nひかる\nひかん\nひくい\nひけつ\nひこうき\nひこく\nひさい\nひさしぶり\nひさん\nびじゅつかん\nひしょ\nひそか\nひそむ\nひたむき\nひだり\nひたる\nひつぎ\nひっこし\nひっし\nひつじゅひん\nひっす\nひつぜん\nぴったり\nぴっちり\nひつよう\nひてい\nひとごみ\nひなまつり\nひなん\nひねる\nひはん\nひびく\nひひょう\nひほう\nひまわり\nひまん\nひみつ\nひめい\nひめじし\nひやけ\nひやす\nひよう\nびょうき\nひらがな\nひらく\nひりつ\nひりょう\nひるま\nひるやすみ\nひれい\nひろい\nひろう\nひろき\nひろゆき\nひんかく\nひんけつ\nひんこん\nひんしゅ\nひんそう\nぴんち\nひんぱん\nびんぼう\nふあん\nふいうち\nふうけい\nふうせん\nぷうたろう\nふうとう\nふうふ\nふえる\nふおん\nふかい\nふきん\nふくざつ\nふくぶくろ\nふこう\nふさい\nふしぎ\nふじみ\nふすま\nふせい\nふせぐ\nふそく\nぶたにく\nふたん\nふちょう\nふつう\nふつか\nふっかつ\nふっき\nふっこく\nぶどう\nふとる\nふとん\nふのう\nふはい\nふひょう\nふへん\nふまん\nふみん\nふめつ\nふめん\nふよう\nふりこ\nふりる\nふるい\nふんいき\nぶんがく\nぶんぐ\nふんしつ\nぶんせき\nふんそう\nぶんぽう\nへいあん\nへいおん\nへいがい\nへいき\nへいげん\nへいこう\nへいさ\nへいしゃ\nへいせつ\nへいそ\nへいたく\nへいてん\nへいねつ\nへいわ\nへきが\nへこむ\nべにいろ\nべにしょうが\nへらす\nへんかん\nべんきょう\nべんごし\nへんさい\nへんたい\nべんり\nほあん\nほいく\nぼうぎょ\nほうこく\nほうそう\nほうほう\nほうもん\nほうりつ\nほえる\nほおん\nほかん\nほきょう\nぼきん\nほくろ\nほけつ\nほけん\nほこう\nほこる\nほしい\nほしつ\nほしゅ\nほしょう\nほせい\nほそい\nほそく\nほたて\nほたる\nぽちぶくろ\nほっきょく\nほっさ\nほったん\nほとんど\nほめる\nほんい\nほんき\nほんけ\nほんしつ\nほんやく\nまいにち\nまかい\nまかせる\nまがる\nまける\nまこと\nまさつ\nまじめ\nますく\nまぜる\nまつり\nまとめ\nまなぶ\nまぬけ\nまねく\nまほう\nまもる\nまゆげ\nまよう\nまろやか\nまわす\nまわり\nまわる\nまんが\nまんきつ\nまんぞく\nまんなか\nみいら\nみうち\nみえる\nみがく\nみかた\nみかん\nみけん\nみこん\nみじかい\nみすい\nみすえる\nみせる\nみっか\nみつかる\nみつける\nみてい\nみとめる\nみなと\nみなみかさい\nみねらる\nみのう\nみのがす\nみほん\nみもと\nみやげ\nみらい\nみりょく\nみわく\nみんか\nみんぞく\nむいか\nむえき\nむえん\nむかい\nむかう\nむかえ\nむかし\nむぎちゃ\nむける\nむげん\nむさぼる\nむしあつい\nむしば\nむじゅん\nむしろ\nむすう\nむすこ\nむすぶ\nむすめ\nむせる\nむせん\nむちゅう\nむなしい\nむのう\nむやみ\nむよう\nむらさき\nむりょう\nむろん\nめいあん\nめいうん\nめいえん\nめいかく\nめいきょく\nめいさい\nめいし\nめいそう\nめいぶつ\nめいれい\nめいわく\nめぐまれる\nめざす\nめした\nめずらしい\nめだつ\nめまい\nめやす\nめんきょ\nめんせき\nめんどう\nもうしあげる\nもうどうけん\nもえる\nもくし\nもくてき\nもくようび\nもちろん\nもどる\nもらう\nもんく\nもんだい\nやおや\nやける\nやさい\nやさしい\nやすい\nやすたろう\nやすみ\nやせる\nやそう\nやたい\nやちん\nやっと\nやっぱり\nやぶる\nやめる\nややこしい\nやよい\nやわらかい\nゆうき\nゆうびんきょく\nゆうべ\nゆうめい\nゆけつ\nゆしゅつ\nゆせん\nゆそう\nゆたか\nゆちゃく\nゆでる\nゆにゅう\nゆびわ\nゆらい\nゆれる\nようい\nようか\nようきゅう\nようじ\nようす\nようちえん\nよかぜ\nよかん\nよきん\nよくせい\nよくぼう\nよけい\nよごれる\nよさん\nよしゅう\nよそう\nよそく\nよっか\nよてい\nよどがわく\nよねつ\nよやく\nよゆう\nよろこぶ\nよろしい\nらいう\nらくがき\nらくご\nらくさつ\nらくだ\nらしんばん\nらせん\nらぞく\nらたい\nらっか\nられつ\nりえき\nりかい\nりきさく\nりきせつ\nりくぐん\nりくつ\nりけん\nりこう\nりせい\nりそう\nりそく\nりてん\nりねん\nりゆう\nりゅうがく\nりよう\nりょうり\nりょかん\nりょくちゃ\nりょこう\nりりく\nりれき\nりろん\nりんご\nるいけい\nるいさい\nるいじ\nるいせき\nるすばん\nるりがわら\nれいかん\nれいぎ\nれいせい\nれいぞうこ\nれいとう\nれいぼう\nれきし\nれきだい\nれんあい\nれんけい\nれんこん\nれんさい\nれんしゅう\nれんぞく\nれんらく\nろうか\nろうご\nろうじん\nろうそく\nろくが\nろこつ\nろじうら\nろしゅつ\nろせん\nろてん\nろめん\nろれつ\nろんぎ\nろんぱ\nろんぶん\nろんり\nわかす\nわかめ\nわかやま\nわかれる\nわしつ\nわじまし\nわすれもの\nわらう\nわれる\n" + ); + dico.Add( + "spanish", + "ábaco\nabdomen\nabeja\nabierto\nabogado\nabono\naborto\nabrazo\nabrir\nabuelo\nabuso\nacabar\nacademia\nacceso\nacción\naceite\nacelga\nacento\naceptar\nácido\naclarar\nacné\nacoger\nacoso\nactivo\nacto\nactriz\nactuar\nacudir\nacuerdo\nacusar\nadicto\nadmitir\nadoptar\nadorno\naduana\nadulto\naéreo\nafectar\nafición\nafinar\nafirmar\nágil\nagitar\nagonía\nagosto\nagotar\nagregar\nagrio\nagua\nagudo\náguila\naguja\nahogo\nahorro\naire\naislar\najedrez\najeno\najuste\nalacrán\nalambre\nalarma\nalba\nálbum\nalcalde\naldea\nalegre\nalejar\nalerta\naleta\nalfiler\nalga\nalgodón\naliado\naliento\nalivio\nalma\nalmeja\nalmíbar\naltar\nalteza\naltivo\nalto\naltura\nalumno\nalzar\namable\namante\namapola\namargo\namasar\námbar\námbito\nameno\namigo\namistad\namor\namparo\namplio\nancho\nanciano\nancla\nandar\nandén\nanemia\nángulo\nanillo\nánimo\nanís\nanotar\nantena\nantiguo\nantojo\nanual\nanular\nanuncio\nañadir\nañejo\naño\napagar\naparato\napetito\napio\naplicar\napodo\naporte\napoyo\naprender\naprobar\napuesta\napuro\narado\naraña\narar\nárbitro\nárbol\narbusto\narchivo\narco\narder\nardilla\narduo\nárea\nárido\naries\narmonía\narnés\naroma\narpa\narpón\narreglo\narroz\narruga\narte\nartista\nasa\nasado\nasalto\nascenso\nasegurar\naseo\nasesor\nasiento\nasilo\nasistir\nasno\nasombro\náspero\nastilla\nastro\nastuto\nasumir\nasunto\natajo\nataque\natar\natento\nateo\nático\natleta\nátomo\natraer\natroz\natún\naudaz\naudio\nauge\naula\naumento\nausente\nautor\naval\navance\navaro\nave\navellana\navena\navestruz\navión\naviso\nayer\nayuda\nayuno\nazafrán\nazar\nazote\nazúcar\nazufre\nazul\nbaba\nbabor\nbache\nbahía\nbaile\nbajar\nbalanza\nbalcón\nbalde\nbambú\nbanco\nbanda\nbaño\nbarba\nbarco\nbarniz\nbarro\nbáscula\nbastón\nbasura\nbatalla\nbatería\nbatir\nbatuta\nbaúl\nbazar\nbebé\nbebida\nbello\nbesar\nbeso\nbestia\nbicho\nbien\nbingo\nblanco\nbloque\nblusa\nboa\nbobina\nbobo\nboca\nbocina\nboda\nbodega\nboina\nbola\nbolero\nbolsa\nbomba\nbondad\nbonito\nbono\nbonsái\nborde\nborrar\nbosque\nbote\nbotín\nbóveda\nbozal\nbravo\nbrazo\nbrecha\nbreve\nbrillo\nbrinco\nbrisa\nbroca\nbroma\nbronce\nbrote\nbruja\nbrusco\nbruto\nbuceo\nbucle\nbueno\nbuey\nbufanda\nbufón\nbúho\nbuitre\nbulto\nburbuja\nburla\nburro\nbuscar\nbutaca\nbuzón\ncaballo\ncabeza\ncabina\ncabra\ncacao\ncadáver\ncadena\ncaer\ncafé\ncaída\ncaimán\ncaja\ncajón\ncal\ncalamar\ncalcio\ncaldo\ncalidad\ncalle\ncalma\ncalor\ncalvo\ncama\ncambio\ncamello\ncamino\ncampo\ncáncer\ncandil\ncanela\ncanguro\ncanica\ncanto\ncaña\ncañón\ncaoba\ncaos\ncapaz\ncapitán\ncapote\ncaptar\ncapucha\ncara\ncarbón\ncárcel\ncareta\ncarga\ncariño\ncarne\ncarpeta\ncarro\ncarta\ncasa\ncasco\ncasero\ncaspa\ncastor\ncatorce\ncatre\ncaudal\ncausa\ncazo\ncebolla\nceder\ncedro\ncelda\ncélebre\nceloso\ncélula\ncemento\nceniza\ncentro\ncerca\ncerdo\ncereza\ncero\ncerrar\ncerteza\ncésped\ncetro\nchacal\nchaleco\nchampú\nchancla\nchapa\ncharla\nchico\nchiste\nchivo\nchoque\nchoza\nchuleta\nchupar\nciclón\nciego\ncielo\ncien\ncierto\ncifra\ncigarro\ncima\ncinco\ncine\ncinta\nciprés\ncirco\nciruela\ncisne\ncita\nciudad\nclamor\nclan\nclaro\nclase\nclave\ncliente\nclima\nclínica\ncobre\ncocción\ncochino\ncocina\ncoco\ncódigo\ncodo\ncofre\ncoger\ncohete\ncojín\ncojo\ncola\ncolcha\ncolegio\ncolgar\ncolina\ncollar\ncolmo\ncolumna\ncombate\ncomer\ncomida\ncómodo\ncompra\nconde\nconejo\nconga\nconocer\nconsejo\ncontar\ncopa\ncopia\ncorazón\ncorbata\ncorcho\ncordón\ncorona\ncorrer\ncoser\ncosmos\ncosta\ncráneo\ncráter\ncrear\ncrecer\ncreído\ncrema\ncría\ncrimen\ncripta\ncrisis\ncromo\ncrónica\ncroqueta\ncrudo\ncruz\ncuadro\ncuarto\ncuatro\ncubo\ncubrir\ncuchara\ncuello\ncuento\ncuerda\ncuesta\ncueva\ncuidar\nculebra\nculpa\nculto\ncumbre\ncumplir\ncuna\ncuneta\ncuota\ncupón\ncúpula\ncurar\ncurioso\ncurso\ncurva\ncutis\ndama\ndanza\ndar\ndardo\ndátil\ndeber\ndébil\ndécada\ndecir\ndedo\ndefensa\ndefinir\ndejar\ndelfín\ndelgado\ndelito\ndemora\ndenso\ndental\ndeporte\nderecho\nderrota\ndesayuno\ndeseo\ndesfile\ndesnudo\ndestino\ndesvío\ndetalle\ndetener\ndeuda\ndía\ndiablo\ndiadema\ndiamante\ndiana\ndiario\ndibujo\ndictar\ndiente\ndieta\ndiez\ndifícil\ndigno\ndilema\ndiluir\ndinero\ndirecto\ndirigir\ndisco\ndiseño\ndisfraz\ndiva\ndivino\ndoble\ndoce\ndolor\ndomingo\ndon\ndonar\ndorado\ndormir\ndorso\ndos\ndosis\ndragón\ndroga\nducha\nduda\nduelo\ndueño\ndulce\ndúo\nduque\ndurar\ndureza\nduro\nébano\nebrio\nechar\neco\necuador\nedad\nedición\nedificio\neditor\neducar\nefecto\neficaz\neje\nejemplo\nelefante\nelegir\nelemento\nelevar\nelipse\nélite\nelixir\nelogio\neludir\nembudo\nemitir\nemoción\nempate\nempeño\nempleo\nempresa\nenano\nencargo\nenchufe\nencía\nenemigo\nenero\nenfado\nenfermo\nengaño\nenigma\nenlace\nenorme\nenredo\nensayo\nenseñar\nentero\nentrar\nenvase\nenvío\népoca\nequipo\nerizo\nescala\nescena\nescolar\nescribir\nescudo\nesencia\nesfera\nesfuerzo\nespada\nespejo\nespía\nesposa\nespuma\nesquí\nestar\neste\nestilo\nestufa\netapa\neterno\nética\netnia\nevadir\nevaluar\nevento\nevitar\nexacto\nexamen\nexceso\nexcusa\nexento\nexigir\nexilio\nexistir\néxito\nexperto\nexplicar\nexponer\nextremo\nfábrica\nfábula\nfachada\nfácil\nfactor\nfaena\nfaja\nfalda\nfallo\nfalso\nfaltar\nfama\nfamilia\nfamoso\nfaraón\nfarmacia\nfarol\nfarsa\nfase\nfatiga\nfauna\nfavor\nfax\nfebrero\nfecha\nfeliz\nfeo\nferia\nferoz\nfértil\nfervor\nfestín\nfiable\nfianza\nfiar\nfibra\nficción\nficha\nfideo\nfiebre\nfiel\nfiera\nfiesta\nfigura\nfijar\nfijo\nfila\nfilete\nfilial\nfiltro\nfin\nfinca\nfingir\nfinito\nfirma\nflaco\nflauta\nflecha\nflor\nflota\nfluir\nflujo\nflúor\nfobia\nfoca\nfogata\nfogón\nfolio\nfolleto\nfondo\nforma\nforro\nfortuna\nforzar\nfosa\nfoto\nfracaso\nfrágil\nfranja\nfrase\nfraude\nfreír\nfreno\nfresa\nfrío\nfrito\nfruta\nfuego\nfuente\nfuerza\nfuga\nfumar\nfunción\nfunda\nfurgón\nfuria\nfusil\nfútbol\nfuturo\ngacela\ngafas\ngaita\ngajo\ngala\ngalería\ngallo\ngamba\nganar\ngancho\nganga\nganso\ngaraje\ngarza\ngasolina\ngastar\ngato\ngavilán\ngemelo\ngemir\ngen\ngénero\ngenio\ngente\ngeranio\ngerente\ngermen\ngesto\ngigante\ngimnasio\ngirar\ngiro\nglaciar\nglobo\ngloria\ngol\ngolfo\ngoloso\ngolpe\ngoma\ngordo\ngorila\ngorra\ngota\ngoteo\ngozar\ngrada\ngráfico\ngrano\ngrasa\ngratis\ngrave\ngrieta\ngrillo\ngripe\ngris\ngrito\ngrosor\ngrúa\ngrueso\ngrumo\ngrupo\nguante\nguapo\nguardia\nguerra\nguía\nguiño\nguion\nguiso\nguitarra\ngusano\ngustar\nhaber\nhábil\nhablar\nhacer\nhacha\nhada\nhallar\nhamaca\nharina\nhaz\nhazaña\nhebilla\nhebra\nhecho\nhelado\nhelio\nhembra\nherir\nhermano\nhéroe\nhervir\nhielo\nhierro\nhígado\nhigiene\nhijo\nhimno\nhistoria\nhocico\nhogar\nhoguera\nhoja\nhombre\nhongo\nhonor\nhonra\nhora\nhormiga\nhorno\nhostil\nhoyo\nhueco\nhuelga\nhuerta\nhueso\nhuevo\nhuida\nhuir\nhumano\nhúmedo\nhumilde\nhumo\nhundir\nhuracán\nhurto\nicono\nideal\nidioma\nídolo\niglesia\niglú\nigual\nilegal\nilusión\nimagen\nimán\nimitar\nimpar\nimperio\nimponer\nimpulso\nincapaz\níndice\ninerte\ninfiel\ninforme\ningenio\ninicio\ninmenso\ninmune\ninnato\ninsecto\ninstante\ninterés\níntimo\nintuir\ninútil\ninvierno\nira\niris\nironía\nisla\nislote\njabalí\njabón\njamón\njarabe\njardín\njarra\njaula\njazmín\njefe\njeringa\njinete\njornada\njoroba\njoven\njoya\njuerga\njueves\njuez\njugador\njugo\njuguete\njuicio\njunco\njungla\njunio\njuntar\njúpiter\njurar\njusto\njuvenil\njuzgar\nkilo\nkoala\nlabio\nlacio\nlacra\nlado\nladrón\nlagarto\nlágrima\nlaguna\nlaico\nlamer\nlámina\nlámpara\nlana\nlancha\nlangosta\nlanza\nlápiz\nlargo\nlarva\nlástima\nlata\nlátex\nlatir\nlaurel\nlavar\nlazo\nleal\nlección\nleche\nlector\nleer\nlegión\nlegumbre\nlejano\nlengua\nlento\nleña\nleón\nleopardo\nlesión\nletal\nletra\nleve\nleyenda\nlibertad\nlibro\nlicor\nlíder\nlidiar\nlienzo\nliga\nligero\nlima\nlímite\nlimón\nlimpio\nlince\nlindo\nlínea\nlingote\nlino\nlinterna\nlíquido\nliso\nlista\nlitera\nlitio\nlitro\nllaga\nllama\nllanto\nllave\nllegar\nllenar\nllevar\nllorar\nllover\nlluvia\nlobo\nloción\nloco\nlocura\nlógica\nlogro\nlombriz\nlomo\nlonja\nlote\nlucha\nlucir\nlugar\nlujo\nluna\nlunes\nlupa\nlustro\nluto\nluz\nmaceta\nmacho\nmadera\nmadre\nmaduro\nmaestro\nmafia\nmagia\nmago\nmaíz\nmaldad\nmaleta\nmalla\nmalo\nmamá\nmambo\nmamut\nmanco\nmando\nmanejar\nmanga\nmaniquí\nmanjar\nmano\nmanso\nmanta\nmañana\nmapa\nmáquina\nmar\nmarco\nmarea\nmarfil\nmargen\nmarido\nmármol\nmarrón\nmartes\nmarzo\nmasa\nmáscara\nmasivo\nmatar\nmateria\nmatiz\nmatriz\nmáximo\nmayor\nmazorca\nmecha\nmedalla\nmedio\nmédula\nmejilla\nmejor\nmelena\nmelón\nmemoria\nmenor\nmensaje\nmente\nmenú\nmercado\nmerengue\nmérito\nmes\nmesón\nmeta\nmeter\nmétodo\nmetro\nmezcla\nmiedo\nmiel\nmiembro\nmiga\nmil\nmilagro\nmilitar\nmillón\nmimo\nmina\nminero\nmínimo\nminuto\nmiope\nmirar\nmisa\nmiseria\nmisil\nmismo\nmitad\nmito\nmochila\nmoción\nmoda\nmodelo\nmoho\nmojar\nmolde\nmoler\nmolino\nmomento\nmomia\nmonarca\nmoneda\nmonja\nmonto\nmoño\nmorada\nmorder\nmoreno\nmorir\nmorro\nmorsa\nmortal\nmosca\nmostrar\nmotivo\nmover\nmóvil\nmozo\nmucho\nmudar\nmueble\nmuela\nmuerte\nmuestra\nmugre\nmujer\nmula\nmuleta\nmulta\nmundo\nmuñeca\nmural\nmuro\nmúsculo\nmuseo\nmusgo\nmúsica\nmuslo\nnácar\nnación\nnadar\nnaipe\nnaranja\nnariz\nnarrar\nnasal\nnatal\nnativo\nnatural\nnáusea\nnaval\nnave\nnavidad\nnecio\nnéctar\nnegar\nnegocio\nnegro\nneón\nnervio\nneto\nneutro\nnevar\nnevera\nnicho\nnido\nniebla\nnieto\nniñez\nniño\nnítido\nnivel\nnobleza\nnoche\nnómina\nnoria\nnorma\nnorte\nnota\nnoticia\nnovato\nnovela\nnovio\nnube\nnuca\nnúcleo\nnudillo\nnudo\nnuera\nnueve\nnuez\nnulo\nnúmero\nnutria\noasis\nobeso\nobispo\nobjeto\nobra\nobrero\nobservar\nobtener\nobvio\noca\nocaso\nocéano\nochenta\nocho\nocio\nocre\noctavo\noctubre\noculto\nocupar\nocurrir\nodiar\nodio\nodisea\noeste\nofensa\noferta\noficio\nofrecer\nogro\noído\noír\nojo\nola\noleada\nolfato\nolivo\nolla\nolmo\nolor\nolvido\nombligo\nonda\nonza\nopaco\nopción\nópera\nopinar\noponer\noptar\nóptica\nopuesto\noración\norador\noral\nórbita\norca\norden\noreja\nórgano\norgía\norgullo\noriente\norigen\norilla\noro\norquesta\noruga\nosadía\noscuro\nosezno\noso\nostra\notoño\notro\noveja\nóvulo\nóxido\noxígeno\noyente\nozono\npacto\npadre\npaella\npágina\npago\npaís\npájaro\npalabra\npalco\npaleta\npálido\npalma\npaloma\npalpar\npan\npanal\npánico\npantera\npañuelo\npapá\npapel\npapilla\npaquete\nparar\nparcela\npared\nparir\nparo\npárpado\nparque\npárrafo\nparte\npasar\npaseo\npasión\npaso\npasta\npata\npatio\npatria\npausa\npauta\npavo\npayaso\npeatón\npecado\npecera\npecho\npedal\npedir\npegar\npeine\npelar\npeldaño\npelea\npeligro\npellejo\npelo\npeluca\npena\npensar\npeñón\npeón\npeor\npepino\npequeño\npera\npercha\nperder\npereza\nperfil\nperico\nperla\npermiso\nperro\npersona\npesa\npesca\npésimo\npestaña\npétalo\npetróleo\npez\npezuña\npicar\npichón\npie\npiedra\npierna\npieza\npijama\npilar\npiloto\npimienta\npino\npintor\npinza\npiña\npiojo\npipa\npirata\npisar\npiscina\npiso\npista\npitón\npizca\nplaca\nplan\nplata\nplaya\nplaza\npleito\npleno\nplomo\npluma\nplural\npobre\npoco\npoder\npodio\npoema\npoesía\npoeta\npolen\npolicía\npollo\npolvo\npomada\npomelo\npomo\npompa\nponer\nporción\nportal\nposada\nposeer\nposible\nposte\npotencia\npotro\npozo\nprado\nprecoz\npregunta\npremio\nprensa\npreso\nprevio\nprimo\npríncipe\nprisión\nprivar\nproa\nprobar\nproceso\nproducto\nproeza\nprofesor\nprograma\nprole\npromesa\npronto\npropio\npróximo\nprueba\npúblico\npuchero\npudor\npueblo\npuerta\npuesto\npulga\npulir\npulmón\npulpo\npulso\npuma\npunto\npuñal\npuño\npupa\npupila\npuré\nquedar\nqueja\nquemar\nquerer\nqueso\nquieto\nquímica\nquince\nquitar\nrábano\nrabia\nrabo\nración\nradical\nraíz\nrama\nrampa\nrancho\nrango\nrapaz\nrápido\nrapto\nrasgo\nraspa\nrato\nrayo\nraza\nrazón\nreacción\nrealidad\nrebaño\nrebote\nrecaer\nreceta\nrechazo\nrecoger\nrecreo\nrecto\nrecurso\nred\nredondo\nreducir\nreflejo\nreforma\nrefrán\nrefugio\nregalo\nregir\nregla\nregreso\nrehén\nreino\nreír\nreja\nrelato\nrelevo\nrelieve\nrelleno\nreloj\nremar\nremedio\nremo\nrencor\nrendir\nrenta\nreparto\nrepetir\nreposo\nreptil\nres\nrescate\nresina\nrespeto\nresto\nresumen\nretiro\nretorno\nretrato\nreunir\nrevés\nrevista\nrey\nrezar\nrico\nriego\nrienda\nriesgo\nrifa\nrígido\nrigor\nrincón\nriñón\nrío\nriqueza\nrisa\nritmo\nrito\nrizo\nroble\nroce\nrociar\nrodar\nrodeo\nrodilla\nroer\nrojizo\nrojo\nromero\nromper\nron\nronco\nronda\nropa\nropero\nrosa\nrosca\nrostro\nrotar\nrubí\nrubor\nrudo\nrueda\nrugir\nruido\nruina\nruleta\nrulo\nrumbo\nrumor\nruptura\nruta\nrutina\nsábado\nsaber\nsabio\nsable\nsacar\nsagaz\nsagrado\nsala\nsaldo\nsalero\nsalir\nsalmón\nsalón\nsalsa\nsalto\nsalud\nsalvar\nsamba\nsanción\nsandía\nsanear\nsangre\nsanidad\nsano\nsanto\nsapo\nsaque\nsardina\nsartén\nsastre\nsatán\nsauna\nsaxofón\nsección\nseco\nsecreto\nsecta\nsed\nseguir\nseis\nsello\nselva\nsemana\nsemilla\nsenda\nsensor\nseñal\nseñor\nseparar\nsepia\nsequía\nser\nserie\nsermón\nservir\nsesenta\nsesión\nseta\nsetenta\nsevero\nsexo\nsexto\nsidra\nsiesta\nsiete\nsiglo\nsigno\nsílaba\nsilbar\nsilencio\nsilla\nsímbolo\nsimio\nsirena\nsistema\nsitio\nsituar\nsobre\nsocio\nsodio\nsol\nsolapa\nsoldado\nsoledad\nsólido\nsoltar\nsolución\nsombra\nsondeo\nsonido\nsonoro\nsonrisa\nsopa\nsoplar\nsoporte\nsordo\nsorpresa\nsorteo\nsostén\nsótano\nsuave\nsubir\nsuceso\nsudor\nsuegra\nsuelo\nsueño\nsuerte\nsufrir\nsujeto\nsultán\nsumar\nsuperar\nsuplir\nsuponer\nsupremo\nsur\nsurco\nsureño\nsurgir\nsusto\nsutil\ntabaco\ntabique\ntabla\ntabú\ntaco\ntacto\ntajo\ntalar\ntalco\ntalento\ntalla\ntalón\ntamaño\ntambor\ntango\ntanque\ntapa\ntapete\ntapia\ntapón\ntaquilla\ntarde\ntarea\ntarifa\ntarjeta\ntarot\ntarro\ntarta\ntatuaje\ntauro\ntaza\ntazón\nteatro\ntecho\ntecla\ntécnica\ntejado\ntejer\ntejido\ntela\nteléfono\ntema\ntemor\ntemplo\ntenaz\ntender\ntener\ntenis\ntenso\nteoría\nterapia\nterco\ntérmino\nternura\nterror\ntesis\ntesoro\ntestigo\ntetera\ntexto\ntez\ntibio\ntiburón\ntiempo\ntienda\ntierra\ntieso\ntigre\ntijera\ntilde\ntimbre\ntímido\ntimo\ntinta\ntío\ntípico\ntipo\ntira\ntirón\ntitán\ntítere\ntítulo\ntiza\ntoalla\ntobillo\ntocar\ntocino\ntodo\ntoga\ntoldo\ntomar\ntono\ntonto\ntopar\ntope\ntoque\ntórax\ntorero\ntormenta\ntorneo\ntoro\ntorpedo\ntorre\ntorso\ntortuga\ntos\ntosco\ntoser\ntóxico\ntrabajo\ntractor\ntraer\ntráfico\ntrago\ntraje\ntramo\ntrance\ntrato\ntrauma\ntrazar\ntrébol\ntregua\ntreinta\ntren\ntrepar\ntres\ntribu\ntrigo\ntripa\ntriste\ntriunfo\ntrofeo\ntrompa\ntronco\ntropa\ntrote\ntrozo\ntruco\ntrueno\ntrufa\ntubería\ntubo\ntuerto\ntumba\ntumor\ntúnel\ntúnica\nturbina\nturismo\nturno\ntutor\nubicar\núlcera\numbral\nunidad\nunir\nuniverso\nuno\nuntar\nuña\nurbano\nurbe\nurgente\nurna\nusar\nusuario\nútil\nutopía\nuva\nvaca\nvacío\nvacuna\nvagar\nvago\nvaina\nvajilla\nvale\nválido\nvalle\nvalor\nválvula\nvampiro\nvara\nvariar\nvarón\nvaso\nvecino\nvector\nvehículo\nveinte\nvejez\nvela\nvelero\nveloz\nvena\nvencer\nvenda\nveneno\nvengar\nvenir\nventa\nvenus\nver\nverano\nverbo\nverde\nvereda\nverja\nverso\nverter\nvía\nviaje\nvibrar\nvicio\nvíctima\nvida\nvídeo\nvidrio\nviejo\nviernes\nvigor\nvil\nvilla\nvinagre\nvino\nviñedo\nviolín\nviral\nvirgo\nvirtud\nvisor\nvíspera\nvista\nvitamina\nviudo\nvivaz\nvivero\nvivir\nvivo\nvolcán\nvolumen\nvolver\nvoraz\nvotar\nvoto\nvoz\nvuelo\nvulgar\nyacer\nyate\nyegua\nyema\nyerno\nyeso\nyodo\nyoga\nyogur\nzafiro\nzanja\nzapato\nzarza\nzona\nzorro\nzumo\nzurdo\n" + ); + dico.Add( + "french", + "abaisser\nabandon\nabdiquer\nabeille\nabolir\naborder\naboutir\naboyer\nabrasif\nabreuver\nabriter\nabroger\nabrupt\nabsence\nabsolu\nabsurde\nabusif\nabyssal\nacadémie\nacajou\nacarien\naccabler\naccepter\nacclamer\naccolade\naccroche\naccuser\nacerbe\nachat\nacheter\naciduler\nacier\nacompte\nacquérir\nacronyme\nacteur\nactif\nactuel\nadepte\nadéquat\nadhésif\nadjectif\nadjuger\nadmettre\nadmirer\nadopter\nadorer\nadoucir\nadresse\nadroit\nadulte\nadverbe\naérer\naéronef\naffaire\naffecter\naffiche\naffreux\naffubler\nagacer\nagencer\nagile\nagiter\nagrafer\nagréable\nagrume\naider\naiguille\nailier\naimable\naisance\najouter\najuster\nalarmer\nalchimie\nalerte\nalgèbre\nalgue\naliéner\naliment\nalléger\nalliage\nallouer\nallumer\nalourdir\nalpaga\naltesse\nalvéole\namateur\nambigu\nambre\naménager\namertume\namidon\namiral\namorcer\namour\namovible\namphibie\nampleur\namusant\nanalyse\nanaphore\nanarchie\nanatomie\nancien\nanéantir\nangle\nangoisse\nanguleux\nanimal\nannexer\nannonce\nannuel\nanodin\nanomalie\nanonyme\nanormal\nantenne\nantidote\nanxieux\napaiser\napéritif\naplanir\napologie\nappareil\nappeler\napporter\nappuyer\naquarium\naqueduc\narbitre\narbuste\nardeur\nardoise\nargent\narlequin\narmature\narmement\narmoire\narmure\narpenter\narracher\narriver\narroser\narsenic\nartériel\narticle\naspect\nasphalte\naspirer\nassaut\nasservir\nassiette\nassocier\nassurer\nasticot\nastre\nastuce\natelier\natome\natrium\natroce\nattaque\nattentif\nattirer\nattraper\naubaine\nauberge\naudace\naudible\naugurer\naurore\nautomne\nautruche\navaler\navancer\navarice\navenir\naverse\naveugle\naviateur\navide\navion\naviser\navoine\navouer\navril\naxial\naxiome\nbadge\nbafouer\nbagage\nbaguette\nbaignade\nbalancer\nbalcon\nbaleine\nbalisage\nbambin\nbancaire\nbandage\nbanlieue\nbannière\nbanquier\nbarbier\nbaril\nbaron\nbarque\nbarrage\nbassin\nbastion\nbataille\nbateau\nbatterie\nbaudrier\nbavarder\nbelette\nbélier\nbelote\nbénéfice\nberceau\nberger\nberline\nbermuda\nbesace\nbesogne\nbétail\nbeurre\nbiberon\nbicycle\nbidule\nbijou\nbilan\nbilingue\nbillard\nbinaire\nbiologie\nbiopsie\nbiotype\nbiscuit\nbison\nbistouri\nbitume\nbizarre\nblafard\nblague\nblanchir\nblessant\nblinder\nblond\nbloquer\nblouson\nbobard\nbobine\nboire\nboiser\nbolide\nbonbon\nbondir\nbonheur\nbonifier\nbonus\nbordure\nborne\nbotte\nboucle\nboueux\nbougie\nboulon\nbouquin\nbourse\nboussole\nboutique\nboxeur\nbranche\nbrasier\nbrave\nbrebis\nbrèche\nbreuvage\nbricoler\nbrigade\nbrillant\nbrioche\nbrique\nbrochure\nbroder\nbronzer\nbrousse\nbroyeur\nbrume\nbrusque\nbrutal\nbruyant\nbuffle\nbuisson\nbulletin\nbureau\nburin\nbustier\nbutiner\nbutoir\nbuvable\nbuvette\ncabanon\ncabine\ncachette\ncadeau\ncadre\ncaféine\ncaillou\ncaisson\ncalculer\ncalepin\ncalibre\ncalmer\ncalomnie\ncalvaire\ncamarade\ncaméra\ncamion\ncampagne\ncanal\ncaneton\ncanon\ncantine\ncanular\ncapable\ncaporal\ncaprice\ncapsule\ncapter\ncapuche\ncarabine\ncarbone\ncaresser\ncaribou\ncarnage\ncarotte\ncarreau\ncarton\ncascade\ncasier\ncasque\ncassure\ncauser\ncaution\ncavalier\ncaverne\ncaviar\ncédille\nceinture\ncéleste\ncellule\ncendrier\ncensurer\ncentral\ncercle\ncérébral\ncerise\ncerner\ncerveau\ncesser\nchagrin\nchaise\nchaleur\nchambre\nchance\nchapitre\ncharbon\nchasseur\nchaton\nchausson\nchavirer\nchemise\nchenille\nchéquier\nchercher\ncheval\nchien\nchiffre\nchignon\nchimère\nchiot\nchlorure\nchocolat\nchoisir\nchose\nchouette\nchrome\nchute\ncigare\ncigogne\ncimenter\ncinéma\ncintrer\ncirculer\ncirer\ncirque\nciterne\ncitoyen\ncitron\ncivil\nclairon\nclameur\nclaquer\nclasse\nclavier\nclient\ncligner\nclimat\nclivage\ncloche\nclonage\ncloporte\ncobalt\ncobra\ncocasse\ncocotier\ncoder\ncodifier\ncoffre\ncogner\ncohésion\ncoiffer\ncoincer\ncolère\ncolibri\ncolline\ncolmater\ncolonel\ncombat\ncomédie\ncommande\ncompact\nconcert\nconduire\nconfier\ncongeler\nconnoter\nconsonne\ncontact\nconvexe\ncopain\ncopie\ncorail\ncorbeau\ncordage\ncorniche\ncorpus\ncorrect\ncortège\ncosmique\ncostume\ncoton\ncoude\ncoupure\ncourage\ncouteau\ncouvrir\ncoyote\ncrabe\ncrainte\ncravate\ncrayon\ncréature\ncréditer\ncrémeux\ncreuser\ncrevette\ncribler\ncrier\ncristal\ncritère\ncroire\ncroquer\ncrotale\ncrucial\ncruel\ncrypter\ncubique\ncueillir\ncuillère\ncuisine\ncuivre\nculminer\ncultiver\ncumuler\ncupide\ncuratif\ncurseur\ncyanure\ncycle\ncylindre\ncynique\ndaigner\ndamier\ndanger\ndanseur\ndauphin\ndébattre\ndébiter\ndéborder\ndébrider\ndébutant\ndécaler\ndécembre\ndéchirer\ndécider\ndéclarer\ndécorer\ndécrire\ndécupler\ndédale\ndéductif\ndéesse\ndéfensif\ndéfiler\ndéfrayer\ndégager\ndégivrer\ndéglutir\ndégrafer\ndéjeuner\ndélice\ndéloger\ndemander\ndemeurer\ndémolir\ndénicher\ndénouer\ndentelle\ndénuder\ndépart\ndépenser\ndéphaser\ndéplacer\ndéposer\ndéranger\ndérober\ndésastre\ndescente\ndésert\ndésigner\ndésobéir\ndessiner\ndestrier\ndétacher\ndétester\ndétourer\ndétresse\ndevancer\ndevenir\ndeviner\ndevoir\ndiable\ndialogue\ndiamant\ndicter\ndifférer\ndigérer\ndigital\ndigne\ndiluer\ndimanche\ndiminuer\ndioxyde\ndirectif\ndiriger\ndiscuter\ndisposer\ndissiper\ndistance\ndivertir\ndiviser\ndocile\ndocteur\ndogme\ndoigt\ndomaine\ndomicile\ndompter\ndonateur\ndonjon\ndonner\ndopamine\ndortoir\ndorure\ndosage\ndoseur\ndossier\ndotation\ndouanier\ndouble\ndouceur\ndouter\ndoyen\ndragon\ndraper\ndresser\ndribbler\ndroiture\nduperie\nduplexe\ndurable\ndurcir\ndynastie\néblouir\nécarter\nécharpe\néchelle\néclairer\néclipse\néclore\nécluse\nécole\néconomie\nécorce\nécouter\nécraser\nécrémer\nécrivain\nécrou\nécume\nécureuil\nédifier\néduquer\neffacer\neffectif\neffigie\neffort\neffrayer\neffusion\négaliser\négarer\néjecter\nélaborer\nélargir\nélectron\nélégant\néléphant\nélève\néligible\nélitisme\néloge\nélucider\néluder\nemballer\nembellir\nembryon\némeraude\némission\nemmener\némotion\némouvoir\nempereur\nemployer\nemporter\nemprise\némulsion\nencadrer\nenchère\nenclave\nencoche\nendiguer\nendosser\nendroit\nenduire\nénergie\nenfance\nenfermer\nenfouir\nengager\nengin\nenglober\nénigme\nenjamber\nenjeu\nenlever\nennemi\nennuyeux\nenrichir\nenrobage\nenseigne\nentasser\nentendre\nentier\nentourer\nentraver\nénumérer\nenvahir\nenviable\nenvoyer\nenzyme\néolien\népaissir\népargne\népatant\népaule\népicerie\népidémie\népier\népilogue\népine\népisode\népitaphe\népoque\népreuve\néprouver\népuisant\néquerre\néquipe\nériger\nérosion\nerreur\néruption\nescalier\nespadon\nespèce\nespiègle\nespoir\nesprit\nesquiver\nessayer\nessence\nessieu\nessorer\nestime\nestomac\nestrade\nétagère\nétaler\nétanche\nétatique\néteindre\nétendoir\néternel\néthanol\néthique\nethnie\nétirer\nétoffer\nétoile\nétonnant\nétourdir\nétrange\nétroit\nétude\neuphorie\névaluer\névasion\néventail\névidence\néviter\névolutif\névoquer\nexact\nexagérer\nexaucer\nexceller\nexcitant\nexclusif\nexcuse\nexécuter\nexemple\nexercer\nexhaler\nexhorter\nexigence\nexiler\nexister\nexotique\nexpédier\nexplorer\nexposer\nexprimer\nexquis\nextensif\nextraire\nexulter\nfable\nfabuleux\nfacette\nfacile\nfacture\nfaiblir\nfalaise\nfameux\nfamille\nfarceur\nfarfelu\nfarine\nfarouche\nfasciner\nfatal\nfatigue\nfaucon\nfautif\nfaveur\nfavori\nfébrile\nféconder\nfédérer\nfélin\nfemme\nfémur\nfendoir\nféodal\nfermer\nféroce\nferveur\nfestival\nfeuille\nfeutre\nfévrier\nfiasco\nficeler\nfictif\nfidèle\nfigure\nfilature\nfiletage\nfilière\nfilleul\nfilmer\nfilou\nfiltrer\nfinancer\nfinir\nfiole\nfirme\nfissure\nfixer\nflairer\nflamme\nflasque\nflatteur\nfléau\nflèche\nfleur\nflexion\nflocon\nflore\nfluctuer\nfluide\nfluvial\nfolie\nfonderie\nfongible\nfontaine\nforcer\nforgeron\nformuler\nfortune\nfossile\nfoudre\nfougère\nfouiller\nfoulure\nfourmi\nfragile\nfraise\nfranchir\nfrapper\nfrayeur\nfrégate\nfreiner\nfrelon\nfrémir\nfrénésie\nfrère\nfriable\nfriction\nfrisson\nfrivole\nfroid\nfromage\nfrontal\nfrotter\nfruit\nfugitif\nfuite\nfureur\nfurieux\nfurtif\nfusion\nfutur\ngagner\ngalaxie\ngalerie\ngambader\ngarantir\ngardien\ngarnir\ngarrigue\ngazelle\ngazon\ngéant\ngélatine\ngélule\ngendarme\ngénéral\ngénie\ngenou\ngentil\ngéologie\ngéomètre\ngéranium\ngerme\ngestuel\ngeyser\ngibier\ngicler\ngirafe\ngivre\nglace\nglaive\nglisser\nglobe\ngloire\nglorieux\ngolfeur\ngomme\ngonfler\ngorge\ngorille\ngoudron\ngouffre\ngoulot\ngoupille\ngourmand\ngoutte\ngraduel\ngraffiti\ngraine\ngrand\ngrappin\ngratuit\ngravir\ngrenat\ngriffure\ngriller\ngrimper\ngrogner\ngronder\ngrotte\ngroupe\ngruger\ngrutier\ngruyère\nguépard\nguerrier\nguide\nguimauve\nguitare\ngustatif\ngymnaste\ngyrostat\nhabitude\nhachoir\nhalte\nhameau\nhangar\nhanneton\nharicot\nharmonie\nharpon\nhasard\nhélium\nhématome\nherbe\nhérisson\nhermine\nhéron\nhésiter\nheureux\nhiberner\nhibou\nhilarant\nhistoire\nhiver\nhomard\nhommage\nhomogène\nhonneur\nhonorer\nhonteux\nhorde\nhorizon\nhorloge\nhormone\nhorrible\nhouleux\nhousse\nhublot\nhuileux\nhumain\nhumble\nhumide\nhumour\nhurler\nhydromel\nhygiène\nhymne\nhypnose\nidylle\nignorer\niguane\nillicite\nillusion\nimage\nimbiber\nimiter\nimmense\nimmobile\nimmuable\nimpact\nimpérial\nimplorer\nimposer\nimprimer\nimputer\nincarner\nincendie\nincident\nincliner\nincolore\nindexer\nindice\ninductif\ninédit\nineptie\ninexact\ninfini\ninfliger\ninformer\ninfusion\ningérer\ninhaler\ninhiber\ninjecter\ninjure\ninnocent\ninoculer\ninonder\ninscrire\ninsecte\ninsigne\ninsolite\ninspirer\ninstinct\ninsulter\nintact\nintense\nintime\nintrigue\nintuitif\ninutile\ninvasion\ninventer\ninviter\ninvoquer\nironique\nirradier\nirréel\nirriter\nisoler\nivoire\nivresse\njaguar\njaillir\njambe\njanvier\njardin\njauger\njaune\njavelot\njetable\njeton\njeudi\njeunesse\njoindre\njoncher\njongler\njoueur\njouissif\njournal\njovial\njoyau\njoyeux\njubiler\njugement\njunior\njupon\njuriste\njustice\njuteux\njuvénile\nkayak\nkimono\nkiosque\nlabel\nlabial\nlabourer\nlacérer\nlactose\nlagune\nlaine\nlaisser\nlaitier\nlambeau\nlamelle\nlampe\nlanceur\nlangage\nlanterne\nlapin\nlargeur\nlarme\nlaurier\nlavabo\nlavoir\nlecture\nlégal\nléger\nlégume\nlessive\nlettre\nlevier\nlexique\nlézard\nliasse\nlibérer\nlibre\nlicence\nlicorne\nliège\nlièvre\nligature\nligoter\nligue\nlimer\nlimite\nlimonade\nlimpide\nlinéaire\nlingot\nlionceau\nliquide\nlisière\nlister\nlithium\nlitige\nlittoral\nlivreur\nlogique\nlointain\nloisir\nlombric\nloterie\nlouer\nlourd\nloutre\nlouve\nloyal\nlubie\nlucide\nlucratif\nlueur\nlugubre\nluisant\nlumière\nlunaire\nlundi\nluron\nlutter\nluxueux\nmachine\nmagasin\nmagenta\nmagique\nmaigre\nmaillon\nmaintien\nmairie\nmaison\nmajorer\nmalaxer\nmaléfice\nmalheur\nmalice\nmallette\nmammouth\nmandater\nmaniable\nmanquant\nmanteau\nmanuel\nmarathon\nmarbre\nmarchand\nmardi\nmaritime\nmarqueur\nmarron\nmarteler\nmascotte\nmassif\nmatériel\nmatière\nmatraque\nmaudire\nmaussade\nmauve\nmaximal\nméchant\nméconnu\nmédaille\nmédecin\nméditer\nméduse\nmeilleur\nmélange\nmélodie\nmembre\nmémoire\nmenacer\nmener\nmenhir\nmensonge\nmentor\nmercredi\nmérite\nmerle\nmessager\nmesure\nmétal\nmétéore\nméthode\nmétier\nmeuble\nmiauler\nmicrobe\nmiette\nmignon\nmigrer\nmilieu\nmillion\nmimique\nmince\nminéral\nminimal\nminorer\nminute\nmiracle\nmiroiter\nmissile\nmixte\nmobile\nmoderne\nmoelleux\nmondial\nmoniteur\nmonnaie\nmonotone\nmonstre\nmontagne\nmonument\nmoqueur\nmorceau\nmorsure\nmortier\nmoteur\nmotif\nmouche\nmoufle\nmoulin\nmousson\nmouton\nmouvant\nmultiple\nmunition\nmuraille\nmurène\nmurmure\nmuscle\nmuséum\nmusicien\nmutation\nmuter\nmutuel\nmyriade\nmyrtille\nmystère\nmythique\nnageur\nnappe\nnarquois\nnarrer\nnatation\nnation\nnature\nnaufrage\nnautique\nnavire\nnébuleux\nnectar\nnéfaste\nnégation\nnégliger\nnégocier\nneige\nnerveux\nnettoyer\nneurone\nneutron\nneveu\nniche\nnickel\nnitrate\nniveau\nnoble\nnocif\nnocturne\nnoirceur\nnoisette\nnomade\nnombreux\nnommer\nnormatif\nnotable\nnotifier\nnotoire\nnourrir\nnouveau\nnovateur\nnovembre\nnovice\nnuage\nnuancer\nnuire\nnuisible\nnuméro\nnuptial\nnuque\nnutritif\nobéir\nobjectif\nobliger\nobscur\nobserver\nobstacle\nobtenir\nobturer\noccasion\noccuper\nocéan\noctobre\noctroyer\noctupler\noculaire\nodeur\nodorant\noffenser\nofficier\noffrir\nogive\noiseau\noisillon\nolfactif\nolivier\nombrage\nomettre\nonctueux\nonduler\nonéreux\nonirique\nopale\nopaque\nopérer\nopinion\nopportun\nopprimer\nopter\noptique\norageux\norange\norbite\nordonner\noreille\norgane\norgueil\norifice\nornement\norque\nortie\nosciller\nosmose\nossature\notarie\nouragan\nourson\noutil\noutrager\nouvrage\novation\noxyde\noxygène\nozone\npaisible\npalace\npalmarès\npalourde\npalper\npanache\npanda\npangolin\npaniquer\npanneau\npanorama\npantalon\npapaye\npapier\npapoter\npapyrus\nparadoxe\nparcelle\nparesse\nparfumer\nparler\nparole\nparrain\nparsemer\npartager\nparure\nparvenir\npassion\npastèque\npaternel\npatience\npatron\npavillon\npavoiser\npayer\npaysage\npeigne\npeintre\npelage\npélican\npelle\npelouse\npeluche\npendule\npénétrer\npénible\npensif\npénurie\npépite\npéplum\nperdrix\nperforer\npériode\npermuter\nperplexe\npersil\nperte\npeser\npétale\npetit\npétrir\npeuple\npharaon\nphobie\nphoque\nphoton\nphrase\nphysique\npiano\npictural\npièce\npierre\npieuvre\npilote\npinceau\npipette\npiquer\npirogue\npiscine\npiston\npivoter\npixel\npizza\nplacard\nplafond\nplaisir\nplaner\nplaque\nplastron\nplateau\npleurer\nplexus\npliage\nplomb\nplonger\npluie\nplumage\npochette\npoésie\npoète\npointe\npoirier\npoisson\npoivre\npolaire\npolicier\npollen\npolygone\npommade\npompier\nponctuel\npondérer\nponey\nportique\nposition\nposséder\nposture\npotager\npoteau\npotion\npouce\npoulain\npoumon\npourpre\npoussin\npouvoir\nprairie\npratique\nprécieux\nprédire\npréfixe\nprélude\nprénom\nprésence\nprétexte\nprévoir\nprimitif\nprince\nprison\npriver\nproblème\nprocéder\nprodige\nprofond\nprogrès\nproie\nprojeter\nprologue\npromener\npropre\nprospère\nprotéger\nprouesse\nproverbe\nprudence\npruneau\npsychose\npublic\npuceron\npuiser\npulpe\npulsar\npunaise\npunitif\npupitre\npurifier\npuzzle\npyramide\nquasar\nquerelle\nquestion\nquiétude\nquitter\nquotient\nracine\nraconter\nradieux\nragondin\nraideur\nraisin\nralentir\nrallonge\nramasser\nrapide\nrasage\nratisser\nravager\nravin\nrayonner\nréactif\nréagir\nréaliser\nréanimer\nrecevoir\nréciter\nréclamer\nrécolter\nrecruter\nreculer\nrecycler\nrédiger\nredouter\nrefaire\nréflexe\nréformer\nrefrain\nrefuge\nrégalien\nrégion\nréglage\nrégulier\nréitérer\nrejeter\nrejouer\nrelatif\nrelever\nrelief\nremarque\nremède\nremise\nremonter\nremplir\nremuer\nrenard\nrenfort\nrenifler\nrenoncer\nrentrer\nrenvoi\nreplier\nreporter\nreprise\nreptile\nrequin\nréserve\nrésineux\nrésoudre\nrespect\nrester\nrésultat\nrétablir\nretenir\nréticule\nretomber\nretracer\nréunion\nréussir\nrevanche\nrevivre\nrévolte\nrévulsif\nrichesse\nrideau\nrieur\nrigide\nrigoler\nrincer\nriposter\nrisible\nrisque\nrituel\nrival\nrivière\nrocheux\nromance\nrompre\nronce\nrondin\nroseau\nrosier\nrotatif\nrotor\nrotule\nrouge\nrouille\nrouleau\nroutine\nroyaume\nruban\nrubis\nruche\nruelle\nrugueux\nruiner\nruisseau\nruser\nrustique\nrythme\nsabler\nsaboter\nsabre\nsacoche\nsafari\nsagesse\nsaisir\nsalade\nsalive\nsalon\nsaluer\nsamedi\nsanction\nsanglier\nsarcasme\nsardine\nsaturer\nsaugrenu\nsaumon\nsauter\nsauvage\nsavant\nsavonner\nscalpel\nscandale\nscélérat\nscénario\nsceptre\nschéma\nscience\nscinder\nscore\nscrutin\nsculpter\nséance\nsécable\nsécher\nsecouer\nsécréter\nsédatif\nséduire\nseigneur\nséjour\nsélectif\nsemaine\nsembler\nsemence\nséminal\nsénateur\nsensible\nsentence\nséparer\nséquence\nserein\nsergent\nsérieux\nserrure\nsérum\nservice\nsésame\nsévir\nsevrage\nsextuple\nsidéral\nsiècle\nsiéger\nsiffler\nsigle\nsignal\nsilence\nsilicium\nsimple\nsincère\nsinistre\nsiphon\nsirop\nsismique\nsituer\nskier\nsocial\nsocle\nsodium\nsoigneux\nsoldat\nsoleil\nsolitude\nsoluble\nsombre\nsommeil\nsomnoler\nsonde\nsongeur\nsonnette\nsonore\nsorcier\nsortir\nsosie\nsottise\nsoucieux\nsoudure\nsouffle\nsoulever\nsoupape\nsource\nsoutirer\nsouvenir\nspacieux\nspatial\nspécial\nsphère\nspiral\nstable\nstation\nsternum\nstimulus\nstipuler\nstrict\nstudieux\nstupeur\nstyliste\nsublime\nsubstrat\nsubtil\nsubvenir\nsuccès\nsucre\nsuffixe\nsuggérer\nsuiveur\nsulfate\nsuperbe\nsupplier\nsurface\nsuricate\nsurmener\nsurprise\nsursaut\nsurvie\nsuspect\nsyllabe\nsymbole\nsymétrie\nsynapse\nsyntaxe\nsystème\ntabac\ntablier\ntactile\ntailler\ntalent\ntalisman\ntalonner\ntambour\ntamiser\ntangible\ntapis\ntaquiner\ntarder\ntarif\ntartine\ntasse\ntatami\ntatouage\ntaupe\ntaureau\ntaxer\ntémoin\ntemporel\ntenaille\ntendre\nteneur\ntenir\ntension\nterminer\nterne\nterrible\ntétine\ntexte\nthème\nthéorie\nthérapie\nthorax\ntibia\ntiède\ntimide\ntirelire\ntiroir\ntissu\ntitane\ntitre\ntituber\ntoboggan\ntolérant\ntomate\ntonique\ntonneau\ntoponyme\ntorche\ntordre\ntornade\ntorpille\ntorrent\ntorse\ntortue\ntotem\ntoucher\ntournage\ntousser\ntoxine\ntraction\ntrafic\ntragique\ntrahir\ntrain\ntrancher\ntravail\ntrèfle\ntremper\ntrésor\ntreuil\ntriage\ntribunal\ntricoter\ntrilogie\ntriomphe\ntripler\ntriturer\ntrivial\ntrombone\ntronc\ntropical\ntroupeau\ntuile\ntulipe\ntumulte\ntunnel\nturbine\ntuteur\ntutoyer\ntuyau\ntympan\ntyphon\ntypique\ntyran\nubuesque\nultime\nultrason\nunanime\nunifier\nunion\nunique\nunitaire\nunivers\nuranium\nurbain\nurticant\nusage\nusine\nusuel\nusure\nutile\nutopie\nvacarme\nvaccin\nvagabond\nvague\nvaillant\nvaincre\nvaisseau\nvalable\nvalise\nvallon\nvalve\nvampire\nvanille\nvapeur\nvarier\nvaseux\nvassal\nvaste\nvecteur\nvedette\nvégétal\nvéhicule\nveinard\nvéloce\nvendredi\nvénérer\nvenger\nvenimeux\nventouse\nverdure\nvérin\nvernir\nverrou\nverser\nvertu\nveston\nvétéran\nvétuste\nvexant\nvexer\nviaduc\nviande\nvictoire\nvidange\nvidéo\nvignette\nvigueur\nvilain\nvillage\nvinaigre\nviolon\nvipère\nvirement\nvirtuose\nvirus\nvisage\nviseur\nvision\nvisqueux\nvisuel\nvital\nvitesse\nviticole\nvitrine\nvivace\nvivipare\nvocation\nvoguer\nvoile\nvoisin\nvoiture\nvolaille\nvolcan\nvoltiger\nvolume\nvorace\nvortex\nvoter\nvouloir\nvoyage\nvoyelle\nwagon\nxénon\nyacht\nzèbre\nzénith\nzeste\nzoologie" + ); + dico.Add( + "portuguese_brazil", + "abacate\nabaixo\nabalar\nabater\nabduzir\nabelha\naberto\nabismo\nabotoar\nabranger\nabreviar\nabrigar\nabrupto\nabsinto\nabsoluto\nabsurdo\nabutre\nacabado\nacalmar\nacampar\nacanhar\nacaso\naceitar\nacelerar\nacenar\nacervo\nacessar\nacetona\nachatar\nacidez\nacima\nacionado\nacirrar\naclamar\naclive\nacolhida\nacomodar\nacoplar\nacordar\nacumular\nacusador\nadaptar\nadega\nadentro\nadepto\nadequar\naderente\nadesivo\nadeus\nadiante\naditivo\nadjetivo\nadjunto\nadmirar\nadorar\nadquirir\nadubo\nadverso\nadvogado\naeronave\nafastar\naferir\nafetivo\nafinador\nafivelar\naflito\nafluente\nafrontar\nagachar\nagarrar\nagasalho\nagenciar\nagilizar\nagiota\nagitado\nagora\nagradar\nagreste\nagrupar\naguardar\nagulha\najoelhar\najudar\najustar\nalameda\nalarme\nalastrar\nalavanca\nalbergue\nalbino\nalcatra\naldeia\nalecrim\nalegria\nalertar\nalface\nalfinete\nalgum\nalheio\naliar\nalicate\nalienar\nalinhar\naliviar\nalmofada\nalocar\nalpiste\nalterar\naltitude\nalucinar\nalugar\naluno\nalusivo\nalvo\namaciar\namador\namarelo\namassar\nambas\nambiente\nameixa\namenizar\namido\namistoso\namizade\namolador\namontoar\namoroso\namostra\namparar\nampliar\nampola\nanagrama\nanalisar\nanarquia\nanatomia\nandaime\nanel\nanexo\nangular\nanimar\nanjo\nanomalia\nanotado\nansioso\nanterior\nanuidade\nanunciar\nanzol\napagador\napalpar\napanhado\napego\napelido\napertada\napesar\napetite\napito\naplauso\naplicada\napoio\napontar\naposta\naprendiz\naprovar\naquecer\narame\naranha\narara\narcada\nardente\nareia\narejar\narenito\naresta\nargiloso\nargola\narma\narquivo\narraial\narrebate\narriscar\narroba\narrumar\narsenal\narterial\nartigo\narvoredo\nasfaltar\nasilado\naspirar\nassador\nassinar\nassoalho\nassunto\nastral\natacado\natadura\natalho\natarefar\natear\natender\naterro\nateu\natingir\natirador\nativo\natoleiro\natracar\natrevido\natriz\natual\natum\nauditor\naumentar\naura\naurora\nautismo\nautoria\nautuar\navaliar\navante\navaria\navental\navesso\naviador\navisar\navulso\naxila\nazarar\nazedo\nazeite\nazulejo\nbabar\nbabosa\nbacalhau\nbacharel\nbacia\nbagagem\nbaiano\nbailar\nbaioneta\nbairro\nbaixista\nbajular\nbaleia\nbaliza\nbalsa\nbanal\nbandeira\nbanho\nbanir\nbanquete\nbarato\nbarbado\nbaronesa\nbarraca\nbarulho\nbaseado\nbastante\nbatata\nbatedor\nbatida\nbatom\nbatucar\nbaunilha\nbeber\nbeijo\nbeirada\nbeisebol\nbeldade\nbeleza\nbelga\nbeliscar\nbendito\nbengala\nbenzer\nberimbau\nberlinda\nberro\nbesouro\nbexiga\nbezerro\nbico\nbicudo\nbienal\nbifocal\nbifurcar\nbigorna\nbilhete\nbimestre\nbimotor\nbiologia\nbiombo\nbiosfera\nbipolar\nbirrento\nbiscoito\nbisneto\nbispo\nbissexto\nbitola\nbizarro\nblindado\nbloco\nbloquear\nboato\nbobagem\nbocado\nbocejo\nbochecha\nboicotar\nbolada\nboletim\nbolha\nbolo\nbombeiro\nbonde\nboneco\nbonita\nborbulha\nborda\nboreal\nborracha\nbovino\nboxeador\nbranco\nbrasa\nbraveza\nbreu\nbriga\nbrilho\nbrincar\nbroa\nbrochura\nbronzear\nbroto\nbruxo\nbucha\nbudismo\nbufar\nbule\nburaco\nbusca\nbusto\nbuzina\ncabana\ncabelo\ncabide\ncabo\ncabrito\ncacau\ncacetada\ncachorro\ncacique\ncadastro\ncadeado\ncafezal\ncaiaque\ncaipira\ncaixote\ncajado\ncaju\ncalafrio\ncalcular\ncaldeira\ncalibrar\ncalmante\ncalota\ncamada\ncambista\ncamisa\ncamomila\ncampanha\ncamuflar\ncanavial\ncancelar\ncaneta\ncanguru\ncanhoto\ncanivete\ncanoa\ncansado\ncantar\ncanudo\ncapacho\ncapela\ncapinar\ncapotar\ncapricho\ncaptador\ncapuz\ncaracol\ncarbono\ncardeal\ncareca\ncarimbar\ncarneiro\ncarpete\ncarreira\ncartaz\ncarvalho\ncasaco\ncasca\ncasebre\ncastelo\ncasulo\ncatarata\ncativar\ncaule\ncausador\ncautelar\ncavalo\ncaverna\ncebola\ncedilha\ncegonha\ncelebrar\ncelular\ncenoura\ncenso\ncenteio\ncercar\ncerrado\ncerteiro\ncerveja\ncetim\ncevada\nchacota\nchaleira\nchamado\nchapada\ncharme\nchatice\nchave\nchefe\nchegada\ncheiro\ncheque\nchicote\nchifre\nchinelo\nchocalho\nchover\nchumbo\nchutar\nchuva\ncicatriz\nciclone\ncidade\ncidreira\nciente\ncigana\ncimento\ncinto\ncinza\nciranda\ncircuito\ncirurgia\ncitar\nclareza\nclero\nclicar\nclone\nclube\ncoado\ncoagir\ncobaia\ncobertor\ncobrar\ncocada\ncoelho\ncoentro\ncoeso\ncogumelo\ncoibir\ncoifa\ncoiote\ncolar\ncoleira\ncolher\ncolidir\ncolmeia\ncolono\ncoluna\ncomando\ncombinar\ncomentar\ncomitiva\ncomover\ncomplexo\ncomum\nconcha\ncondor\nconectar\nconfuso\ncongelar\nconhecer\nconjugar\nconsumir\ncontrato\nconvite\ncooperar\ncopeiro\ncopiador\ncopo\ncoquetel\ncoragem\ncordial\ncorneta\ncoronha\ncorporal\ncorreio\ncortejo\ncoruja\ncorvo\ncosseno\ncostela\ncotonete\ncouro\ncouve\ncovil\ncozinha\ncratera\ncravo\ncreche\ncredor\ncreme\ncrer\ncrespo\ncriada\ncriminal\ncrioulo\ncrise\ncriticar\ncrosta\ncrua\ncruzeiro\ncubano\ncueca\ncuidado\ncujo\nculatra\nculminar\nculpar\ncultura\ncumprir\ncunhado\ncupido\ncurativo\ncurral\ncursar\ncurto\ncuspir\ncustear\ncutelo\ndamasco\ndatar\ndebater\ndebitar\ndeboche\ndebulhar\ndecalque\ndecimal\ndeclive\ndecote\ndecretar\ndedal\ndedicado\ndeduzir\ndefesa\ndefumar\ndegelo\ndegrau\ndegustar\ndeitado\ndeixar\ndelator\ndelegado\ndelinear\ndelonga\ndemanda\ndemitir\ndemolido\ndentista\ndepenado\ndepilar\ndepois\ndepressa\ndepurar\nderiva\nderramar\ndesafio\ndesbotar\ndescanso\ndesenho\ndesfiado\ndesgaste\ndesigual\ndeslize\ndesmamar\ndesova\ndespesa\ndestaque\ndesviar\ndetalhar\ndetentor\ndetonar\ndetrito\ndeusa\ndever\ndevido\ndevotado\ndezena\ndiagrama\ndialeto\ndidata\ndifuso\ndigitar\ndilatado\ndiluente\ndiminuir\ndinastia\ndinheiro\ndiocese\ndireto\ndiscreta\ndisfarce\ndisparo\ndisquete\ndissipar\ndistante\nditador\ndiurno\ndiverso\ndivisor\ndivulgar\ndizer\ndobrador\ndolorido\ndomador\ndominado\ndonativo\ndonzela\ndormente\ndorsal\ndosagem\ndourado\ndoutor\ndrenagem\ndrible\ndrogaria\nduelar\nduende\ndueto\nduplo\nduquesa\ndurante\nduvidoso\neclodir\necoar\necologia\nedificar\nedital\neducado\nefeito\nefetivar\nejetar\nelaborar\neleger\neleitor\nelenco\nelevador\neliminar\nelogiar\nembargo\nembolado\nembrulho\nembutido\nemenda\nemergir\nemissor\nempatia\nempenho\nempinado\nempolgar\nemprego\nempurrar\nemulador\nencaixe\nencenado\nenchente\nencontro\nendeusar\nendossar\nenfaixar\nenfeite\nenfim\nengajado\nengenho\nenglobar\nengomado\nengraxar\nenguia\nenjoar\nenlatar\nenquanto\nenraizar\nenrolado\nenrugar\nensaio\nenseada\nensino\nensopado\nentanto\nenteado\nentidade\nentortar\nentrada\nentulho\nenvergar\nenviado\nenvolver\nenxame\nenxerto\nenxofre\nenxuto\nepiderme\nequipar\nereto\nerguido\nerrata\nerva\nervilha\nesbanjar\nesbelto\nescama\nescola\nescrita\nescuta\nesfinge\nesfolar\nesfregar\nesfumado\nesgrima\nesmalte\nespanto\nespelho\nespiga\nesponja\nespreita\nespumar\nesquerda\nestaca\nesteira\nesticar\nestofado\nestrela\nestudo\nesvaziar\netanol\netiqueta\neuforia\neuropeu\nevacuar\nevaporar\nevasivo\neventual\nevidente\nevoluir\nexagero\nexalar\nexaminar\nexato\nexausto\nexcesso\nexcitar\nexclamar\nexecutar\nexemplo\nexibir\nexigente\nexonerar\nexpandir\nexpelir\nexpirar\nexplanar\nexposto\nexpresso\nexpulsar\nexterno\nextinto\nextrato\nfabricar\nfabuloso\nfaceta\nfacial\nfada\nfadiga\nfaixa\nfalar\nfalta\nfamiliar\nfandango\nfanfarra\nfantoche\nfardado\nfarelo\nfarinha\nfarofa\nfarpa\nfartura\nfatia\nfator\nfavorita\nfaxina\nfazenda\nfechado\nfeijoada\nfeirante\nfelino\nfeminino\nfenda\nfeno\nfera\nferiado\nferrugem\nferver\nfestejar\nfetal\nfeudal\nfiapo\nfibrose\nficar\nficheiro\nfigurado\nfileira\nfilho\nfilme\nfiltrar\nfirmeza\nfisgada\nfissura\nfita\nfivela\nfixador\nfixo\nflacidez\nflamingo\nflanela\nflechada\nflora\nflutuar\nfluxo\nfocal\nfocinho\nfofocar\nfogo\nfoguete\nfoice\nfolgado\nfolheto\nforjar\nformiga\nforno\nforte\nfosco\nfossa\nfragata\nfralda\nfrango\nfrasco\nfraterno\nfreira\nfrente\nfretar\nfrieza\nfriso\nfritura\nfronha\nfrustrar\nfruteira\nfugir\nfulano\nfuligem\nfundar\nfungo\nfunil\nfurador\nfurioso\nfutebol\ngabarito\ngabinete\ngado\ngaiato\ngaiola\ngaivota\ngalega\ngalho\ngalinha\ngalocha\nganhar\ngaragem\ngarfo\ngargalo\ngarimpo\ngaroupa\ngarrafa\ngasoduto\ngasto\ngata\ngatilho\ngaveta\ngazela\ngelado\ngeleia\ngelo\ngemada\ngemer\ngemido\ngeneroso\ngengiva\ngenial\ngenoma\ngenro\ngeologia\ngerador\ngerminar\ngesso\ngestor\nginasta\ngincana\ngingado\ngirafa\ngirino\nglacial\nglicose\nglobal\nglorioso\ngoela\ngoiaba\ngolfe\ngolpear\ngordura\ngorjeta\ngorro\ngostoso\ngoteira\ngovernar\ngracejo\ngradual\ngrafite\ngralha\ngrampo\ngranada\ngratuito\ngraveto\ngraxa\ngrego\ngrelhar\ngreve\ngrilo\ngrisalho\ngritaria\ngrosso\ngrotesco\ngrudado\ngrunhido\ngruta\nguache\nguarani\nguaxinim\nguerrear\nguiar\nguincho\nguisado\ngula\nguloso\nguru\nhabitar\nharmonia\nhaste\nhaver\nhectare\nherdar\nheresia\nhesitar\nhiato\nhibernar\nhidratar\nhiena\nhino\nhipismo\nhipnose\nhipoteca\nhoje\nholofote\nhomem\nhonesto\nhonrado\nhormonal\nhospedar\nhumorado\niate\nideia\nidoso\nignorado\nigreja\niguana\nileso\nilha\niludido\niluminar\nilustrar\nimagem\nimediato\nimenso\nimersivo\niminente\nimitador\nimortal\nimpacto\nimpedir\nimplante\nimpor\nimprensa\nimpune\nimunizar\ninalador\ninapto\ninativo\nincenso\ninchar\nincidir\nincluir\nincolor\nindeciso\nindireto\nindutor\nineficaz\ninerente\ninfantil\ninfestar\ninfinito\ninflamar\ninformal\ninfrator\ningerir\ninibido\ninicial\ninimigo\ninjetar\ninocente\ninodoro\ninovador\ninox\ninquieto\ninscrito\ninseto\ninsistir\ninspetor\ninstalar\ninsulto\nintacto\nintegral\nintimar\nintocado\nintriga\ninvasor\ninverno\ninvicto\ninvocar\niogurte\niraniano\nironizar\nirreal\nirritado\nisca\nisento\nisolado\nisqueiro\nitaliano\njaneiro\njangada\njanta\njararaca\njardim\njarro\njasmim\njato\njavali\njazida\njejum\njoaninha\njoelhada\njogador\njoia\njornal\njorrar\njovem\njuba\njudeu\njudoca\njuiz\njulgador\njulho\njurado\njurista\njuro\njusta\nlabareda\nlaboral\nlacre\nlactante\nladrilho\nlagarta\nlagoa\nlaje\nlamber\nlamentar\nlaminar\nlampejo\nlanche\nlapidar\nlapso\nlaranja\nlareira\nlargura\nlasanha\nlastro\nlateral\nlatido\nlavanda\nlavoura\nlavrador\nlaxante\nlazer\nlealdade\nlebre\nlegado\nlegendar\nlegista\nleigo\nleiloar\nleitura\nlembrete\nleme\nlenhador\nlentilha\nleoa\nlesma\nleste\nletivo\nletreiro\nlevar\nleveza\nlevitar\nliberal\nlibido\nliderar\nligar\nligeiro\nlimitar\nlimoeiro\nlimpador\nlinda\nlinear\nlinhagem\nliquidez\nlistagem\nlisura\nlitoral\nlivro\nlixa\nlixeira\nlocador\nlocutor\nlojista\nlombo\nlona\nlonge\nlontra\nlorde\nlotado\nloteria\nloucura\nlousa\nlouvar\nluar\nlucidez\nlucro\nluneta\nlustre\nlutador\nluva\nmacaco\nmacete\nmachado\nmacio\nmadeira\nmadrinha\nmagnata\nmagreza\nmaior\nmais\nmalandro\nmalha\nmalote\nmaluco\nmamilo\nmamoeiro\nmamute\nmanada\nmancha\nmandato\nmanequim\nmanhoso\nmanivela\nmanobrar\nmansa\nmanter\nmanusear\nmapeado\nmaquinar\nmarcador\nmaresia\nmarfim\nmargem\nmarinho\nmarmita\nmaroto\nmarquise\nmarreco\nmartelo\nmarujo\nmascote\nmasmorra\nmassagem\nmastigar\nmatagal\nmaterno\nmatinal\nmatutar\nmaxilar\nmedalha\nmedida\nmedusa\nmegafone\nmeiga\nmelancia\nmelhor\nmembro\nmemorial\nmenino\nmenos\nmensagem\nmental\nmerecer\nmergulho\nmesada\nmesclar\nmesmo\nmesquita\nmestre\nmetade\nmeteoro\nmetragem\nmexer\nmexicano\nmicro\nmigalha\nmigrar\nmilagre\nmilenar\nmilhar\nmimado\nminerar\nminhoca\nministro\nminoria\nmiolo\nmirante\nmirtilo\nmisturar\nmocidade\nmoderno\nmodular\nmoeda\nmoer\nmoinho\nmoita\nmoldura\nmoleza\nmolho\nmolinete\nmolusco\nmontanha\nmoqueca\nmorango\nmorcego\nmordomo\nmorena\nmosaico\nmosquete\nmostarda\nmotel\nmotim\nmoto\nmotriz\nmuda\nmuito\nmulata\nmulher\nmultar\nmundial\nmunido\nmuralha\nmurcho\nmuscular\nmuseu\nmusical\nnacional\nnadador\nnaja\nnamoro\nnarina\nnarrado\nnascer\nnativa\nnatureza\nnavalha\nnavegar\nnavio\nneblina\nnebuloso\nnegativa\nnegociar\nnegrito\nnervoso\nneta\nneural\nnevasca\nnevoeiro\nninar\nninho\nnitidez\nnivelar\nnobreza\nnoite\nnoiva\nnomear\nnominal\nnordeste\nnortear\nnotar\nnoticiar\nnoturno\nnovelo\nnovilho\nnovo\nnublado\nnudez\nnumeral\nnupcial\nnutrir\nnuvem\nobcecado\nobedecer\nobjetivo\nobrigado\nobscuro\nobstetra\nobter\nobturar\nocidente\nocioso\nocorrer\noculista\nocupado\nofegante\nofensiva\noferenda\noficina\nofuscado\nogiva\nolaria\noleoso\nolhar\noliveira\nombro\nomelete\nomisso\nomitir\nondulado\noneroso\nontem\nopcional\noperador\noponente\noportuno\noposto\norar\norbitar\nordem\nordinal\norfanato\norgasmo\norgulho\noriental\norigem\noriundo\norla\nortodoxo\norvalho\noscilar\nossada\nosso\nostentar\notimismo\nousadia\noutono\noutubro\nouvido\novelha\novular\noxidar\noxigenar\npacato\npaciente\npacote\npactuar\npadaria\npadrinho\npagar\npagode\npainel\npairar\npaisagem\npalavra\npalestra\npalheta\npalito\npalmada\npalpitar\npancada\npanela\npanfleto\npanqueca\npantanal\npapagaio\npapelada\npapiro\nparafina\nparcial\npardal\nparede\npartida\npasmo\npassado\npastel\npatamar\npatente\npatinar\npatrono\npaulada\npausar\npeculiar\npedalar\npedestre\npediatra\npedra\npegada\npeitoral\npeixe\npele\npelicano\npenca\npendurar\npeneira\npenhasco\npensador\npente\nperceber\nperfeito\npergunta\nperito\npermitir\nperna\nperplexo\npersiana\npertence\nperuca\npescado\npesquisa\npessoa\npetiscar\npiada\npicado\npiedade\npigmento\npilastra\npilhado\npilotar\npimenta\npincel\npinguim\npinha\npinote\npintar\npioneiro\npipoca\npiquete\npiranha\npires\npirueta\npiscar\npistola\npitanga\npivete\nplanta\nplaqueta\nplatina\nplebeu\nplumagem\npluvial\npneu\npoda\npoeira\npoetisa\npolegada\npoliciar\npoluente\npolvilho\npomar\npomba\nponderar\npontaria\npopuloso\nporta\npossuir\npostal\npote\npoupar\npouso\npovoar\npraia\nprancha\nprato\npraxe\nprece\npredador\nprefeito\npremiar\nprensar\npreparar\npresilha\npretexto\nprevenir\nprezar\nprimata\nprincesa\nprisma\nprivado\nprocesso\nproduto\nprofeta\nproibido\nprojeto\nprometer\npropagar\nprosa\nprotetor\nprovador\npublicar\npudim\npular\npulmonar\npulseira\npunhal\npunir\npupilo\npureza\npuxador\nquadra\nquantia\nquarto\nquase\nquebrar\nqueda\nqueijo\nquente\nquerido\nquimono\nquina\nquiosque\nrabanada\nrabisco\nrachar\nracionar\nradial\nraiar\nrainha\nraio\nraiva\nrajada\nralado\nramal\nranger\nranhura\nrapadura\nrapel\nrapidez\nraposa\nraquete\nraridade\nrasante\nrascunho\nrasgar\nraspador\nrasteira\nrasurar\nratazana\nratoeira\nrealeza\nreanimar\nreaver\nrebaixar\nrebelde\nrebolar\nrecado\nrecente\nrecheio\nrecibo\nrecordar\nrecrutar\nrecuar\nrede\nredimir\nredonda\nreduzida\nreenvio\nrefinar\nrefletir\nrefogar\nrefresco\nrefugiar\nregalia\nregime\nregra\nreinado\nreitor\nrejeitar\nrelativo\nremador\nremendo\nremorso\nrenovado\nreparo\nrepelir\nrepleto\nrepolho\nrepresa\nrepudiar\nrequerer\nresenha\nresfriar\nresgatar\nresidir\nresolver\nrespeito\nressaca\nrestante\nresumir\nretalho\nreter\nretirar\nretomada\nretratar\nrevelar\nrevisor\nrevolta\nriacho\nrica\nrigidez\nrigoroso\nrimar\nringue\nrisada\nrisco\nrisonho\nrobalo\nrochedo\nrodada\nrodeio\nrodovia\nroedor\nroleta\nromano\nroncar\nrosado\nroseira\nrosto\nrota\nroteiro\nrotina\nrotular\nrouco\nroupa\nroxo\nrubro\nrugido\nrugoso\nruivo\nrumo\nrupestre\nrusso\nsabor\nsaciar\nsacola\nsacudir\nsadio\nsafira\nsaga\nsagrada\nsaibro\nsalada\nsaleiro\nsalgado\nsaliva\nsalpicar\nsalsicha\nsaltar\nsalvador\nsambar\nsamurai\nsanar\nsanfona\nsangue\nsanidade\nsapato\nsarda\nsargento\nsarjeta\nsaturar\nsaudade\nsaxofone\nsazonal\nsecar\nsecular\nseda\nsedento\nsediado\nsedoso\nsedutor\nsegmento\nsegredo\nsegundo\nseiva\nseleto\nselvagem\nsemanal\nsemente\nsenador\nsenhor\nsensual\nsentado\nseparado\nsereia\nseringa\nserra\nservo\nsetembro\nsetor\nsigilo\nsilhueta\nsilicone\nsimetria\nsimpatia\nsimular\nsinal\nsincero\nsingular\nsinopse\nsintonia\nsirene\nsiri\nsituado\nsoberano\nsobra\nsocorro\nsogro\nsoja\nsolda\nsoletrar\nsolteiro\nsombrio\nsonata\nsondar\nsonegar\nsonhador\nsono\nsoprano\nsoquete\nsorrir\nsorteio\nsossego\nsotaque\nsoterrar\nsovado\nsozinho\nsuavizar\nsubida\nsubmerso\nsubsolo\nsubtrair\nsucata\nsucesso\nsuco\nsudeste\nsufixo\nsugador\nsugerir\nsujeito\nsulfato\nsumir\nsuor\nsuperior\nsuplicar\nsuposto\nsuprimir\nsurdina\nsurfista\nsurpresa\nsurreal\nsurtir\nsuspiro\nsustento\ntabela\ntablete\ntabuada\ntacho\ntagarela\ntalher\ntalo\ntalvez\ntamanho\ntamborim\ntampa\ntangente\ntanto\ntapar\ntapioca\ntardio\ntarefa\ntarja\ntarraxa\ntatuagem\ntaurino\ntaxativo\ntaxista\nteatral\ntecer\ntecido\nteclado\ntedioso\nteia\nteimar\ntelefone\ntelhado\ntempero\ntenente\ntensor\ntentar\ntermal\nterno\nterreno\ntese\ntesoura\ntestado\nteto\ntextura\ntexugo\ntiara\ntigela\ntijolo\ntimbrar\ntimidez\ntingido\ntinteiro\ntiragem\ntitular\ntoalha\ntocha\ntolerar\ntolice\ntomada\ntomilho\ntonel\ntontura\ntopete\ntora\ntorcido\ntorneio\ntorque\ntorrada\ntorto\ntostar\ntouca\ntoupeira\ntoxina\ntrabalho\ntracejar\ntradutor\ntrafegar\ntrajeto\ntrama\ntrancar\ntrapo\ntraseiro\ntratador\ntravar\ntreino\ntremer\ntrepidar\ntrevo\ntriagem\ntribo\ntriciclo\ntridente\ntrilogia\ntrindade\ntriplo\ntriturar\ntriunfal\ntrocar\ntrombeta\ntrova\ntrunfo\ntruque\ntubular\ntucano\ntudo\ntulipa\ntupi\nturbo\nturma\nturquesa\ntutelar\ntutorial\nuivar\numbigo\nunha\nunidade\nuniforme\nurologia\nurso\nurtiga\nurubu\nusado\nusina\nusufruir\nvacina\nvadiar\nvagaroso\nvaidoso\nvala\nvalente\nvalidade\nvalores\nvantagem\nvaqueiro\nvaranda\nvareta\nvarrer\nvascular\nvasilha\nvassoura\nvazar\nvazio\nveado\nvedar\nvegetar\nveicular\nveleiro\nvelhice\nveludo\nvencedor\nvendaval\nvenerar\nventre\nverbal\nverdade\nvereador\nvergonha\nvermelho\nverniz\nversar\nvertente\nvespa\nvestido\nvetorial\nviaduto\nviagem\nviajar\nviatura\nvibrador\nvideira\nvidraria\nviela\nviga\nvigente\nvigiar\nvigorar\nvilarejo\nvinco\nvinheta\nvinil\nvioleta\nvirada\nvirtude\nvisitar\nvisto\nvitral\nviveiro\nvizinho\nvoador\nvoar\nvogal\nvolante\nvoleibol\nvoltagem\nvolumoso\nvontade\nvulto\nvuvuzela\nxadrez\nxarope\nxeque\nxeretar\nxerife\nxingar\nzangado\nzarpar\nzebu\nzelador\nzombar\nzoologia\nzumbido" + ); + dico.Add( + "czech", + "abdikace\nabeceda\nadresa\nagrese\nakce\naktovka\nalej\nalkohol\namputace\nananas\nandulka\nanekdota\nanketa\nantika\nanulovat\narcha\narogance\nasfalt\nasistent\naspirace\nastma\nastronom\natlas\natletika\natol\nautobus\nazyl\nbabka\nbachor\nbacil\nbaculka\nbadatel\nbageta\nbagr\nbahno\nbakterie\nbalada\nbaletka\nbalkon\nbalonek\nbalvan\nbalza\nbambus\nbankomat\nbarbar\nbaret\nbarman\nbaroko\nbarva\nbaterka\nbatoh\nbavlna\nbazalka\nbazilika\nbazuka\nbedna\nberan\nbeseda\nbestie\nbeton\nbezinka\nbezmoc\nbeztak\nbicykl\nbidlo\nbiftek\nbikiny\nbilance\nbiograf\nbiolog\nbitva\nbizon\nblahobyt\nblatouch\nblecha\nbledule\nblesk\nblikat\nblizna\nblokovat\nbloudit\nblud\nbobek\nbobr\nbodlina\nbodnout\nbohatost\nbojkot\nbojovat\nbokorys\nbolest\nborec\nborovice\nbota\nboubel\nbouchat\nbouda\nboule\nbourat\nboxer\nbradavka\nbrambora\nbranka\nbratr\nbrepta\nbriketa\nbrko\nbrloh\nbronz\nbroskev\nbrunetka\nbrusinka\nbrzda\nbrzy\nbublina\nbubnovat\nbuchta\nbuditel\nbudka\nbudova\nbufet\nbujarost\nbukvice\nbuldok\nbulva\nbunda\nbunkr\nburza\nbutik\nbuvol\nbuzola\nbydlet\nbylina\nbytovka\nbzukot\ncapart\ncarevna\ncedr\ncedule\ncejch\ncejn\ncela\nceler\ncelkem\ncelnice\ncenina\ncennost\ncenovka\ncentrum\ncenzor\ncestopis\ncetka\nchalupa\nchapadlo\ncharita\nchata\nchechtat\nchemie\nchichot\nchirurg\nchlad\nchleba\nchlubit\nchmel\nchmura\nchobot\nchochol\nchodba\ncholera\nchomout\nchopit\nchoroba\nchov\nchrapot\nchrlit\nchrt\nchrup\nchtivost\nchudina\nchutnat\nchvat\nchvilka\nchvost\nchyba\nchystat\nchytit\ncibule\ncigareta\ncihelna\ncihla\ncinkot\ncirkus\ncisterna\ncitace\ncitrus\ncizinec\ncizost\nclona\ncokoliv\ncouvat\nctitel\nctnost\ncudnost\ncuketa\ncukr\ncupot\ncvaknout\ncval\ncvik\ncvrkot\ncyklista\ndaleko\ndareba\ndatel\ndatum\ndcera\ndebata\ndechovka\ndecibel\ndeficit\ndeflace\ndekl\ndekret\ndemokrat\ndeprese\nderby\ndeska\ndetektiv\ndikobraz\ndiktovat\ndioda\ndiplom\ndisk\ndisplej\ndivadlo\ndivoch\ndlaha\ndlouho\ndluhopis\ndnes\ndobro\ndobytek\ndocent\ndochutit\ndodnes\ndohled\ndohoda\ndohra\ndojem\ndojnice\ndoklad\ndokola\ndoktor\ndokument\ndolar\ndoleva\ndolina\ndoma\ndominant\ndomluvit\ndomov\ndonutit\ndopad\ndopis\ndoplnit\ndoposud\ndoprovod\ndopustit\ndorazit\ndorost\ndort\ndosah\ndoslov\ndostatek\ndosud\ndosyta\ndotaz\ndotek\ndotknout\ndoufat\ndoutnat\ndovozce\ndozadu\ndoznat\ndozorce\ndrahota\ndrak\ndramatik\ndravec\ndraze\ndrdol\ndrobnost\ndrogerie\ndrozd\ndrsnost\ndrtit\ndrzost\nduben\nduchovno\ndudek\nduha\nduhovka\ndusit\ndusno\ndutost\ndvojice\ndvorec\ndynamit\nekolog\nekonomie\nelektron\nelipsa\nemail\nemise\nemoce\nempatie\nepizoda\nepocha\nepopej\nepos\nesej\nesence\neskorta\neskymo\netiketa\neuforie\nevoluce\nexekuce\nexkurze\nexpedice\nexploze\nexport\nextrakt\nfacka\nfajfka\nfakulta\nfanatik\nfantazie\nfarmacie\nfavorit\nfazole\nfederace\nfejeton\nfenka\nfialka\nfigurant\nfilozof\nfiltr\nfinance\nfinta\nfixace\nfjord\nflanel\nflirt\nflotila\nfond\nfosfor\nfotbal\nfotka\nfoton\nfrakce\nfreska\nfronta\nfukar\nfunkce\nfyzika\ngaleje\ngarant\ngenetika\ngeolog\ngilotina\nglazura\nglejt\ngolem\ngolfista\ngotika\ngraf\ngramofon\ngranule\ngrep\ngril\ngrog\ngroteska\nguma\nhadice\nhadr\nhala\nhalenka\nhanba\nhanopis\nharfa\nharpuna\nhavran\nhebkost\nhejkal\nhejno\nhejtman\nhektar\nhelma\nhematom\nherec\nherna\nheslo\nhezky\nhistorik\nhladovka\nhlasivky\nhlava\nhledat\nhlen\nhlodavec\nhloh\nhloupost\nhltat\nhlubina\nhluchota\nhmat\nhmota\nhmyz\nhnis\nhnojivo\nhnout\nhoblina\nhoboj\nhoch\nhodiny\nhodlat\nhodnota\nhodovat\nhojnost\nhokej\nholinka\nholka\nholub\nhomole\nhonitba\nhonorace\nhoral\nhorda\nhorizont\nhorko\nhorlivec\nhormon\nhornina\nhoroskop\nhorstvo\nhospoda\nhostina\nhotovost\nhouba\nhouf\nhoupat\nhouska\nhovor\nhradba\nhranice\nhravost\nhrazda\nhrbolek\nhrdina\nhrdlo\nhrdost\nhrnek\nhrobka\nhromada\nhrot\nhrouda\nhrozen\nhrstka\nhrubost\nhryzat\nhubenost\nhubnout\nhudba\nhukot\nhumr\nhusita\nhustota\nhvozd\nhybnost\nhydrant\nhygiena\nhymna\nhysterik\nidylka\nihned\nikona\niluze\nimunita\ninfekce\ninflace\ninkaso\ninovace\ninspekce\ninternet\ninvalida\ninvestor\ninzerce\nironie\njablko\njachta\njahoda\njakmile\njakost\njalovec\njantar\njarmark\njaro\njasan\njasno\njatka\njavor\njazyk\njedinec\njedle\njednatel\njehlan\njekot\njelen\njelito\njemnost\njenom\njepice\njeseter\njevit\njezdec\njezero\njinak\njindy\njinoch\njiskra\njistota\njitrnice\njizva\njmenovat\njogurt\njurta\nkabaret\nkabel\nkabinet\nkachna\nkadet\nkadidlo\nkahan\nkajak\nkajuta\nkakao\nkaktus\nkalamita\nkalhoty\nkalibr\nkalnost\nkamera\nkamkoliv\nkamna\nkanibal\nkanoe\nkantor\nkapalina\nkapela\nkapitola\nkapka\nkaple\nkapota\nkapr\nkapusta\nkapybara\nkaramel\nkarotka\nkarton\nkasa\nkatalog\nkatedra\nkauce\nkauza\nkavalec\nkazajka\nkazeta\nkazivost\nkdekoliv\nkdesi\nkedluben\nkemp\nkeramika\nkino\nklacek\nkladivo\nklam\nklapot\nklasika\nklaun\nklec\nklenba\nklepat\nklesnout\nklid\nklima\nklisna\nklobouk\nklokan\nklopa\nkloub\nklubovna\nklusat\nkluzkost\nkmen\nkmitat\nkmotr\nkniha\nknot\nkoalice\nkoberec\nkobka\nkobliha\nkobyla\nkocour\nkohout\nkojenec\nkokos\nkoktejl\nkolaps\nkoleda\nkolize\nkolo\nkomando\nkometa\nkomik\nkomnata\nkomora\nkompas\nkomunita\nkonat\nkoncept\nkondice\nkonec\nkonfese\nkongres\nkonina\nkonkurs\nkontakt\nkonzerva\nkopanec\nkopie\nkopnout\nkoprovka\nkorbel\nkorektor\nkormidlo\nkoroptev\nkorpus\nkoruna\nkoryto\nkorzet\nkosatec\nkostka\nkotel\nkotleta\nkotoul\nkoukat\nkoupelna\nkousek\nkouzlo\nkovboj\nkoza\nkozoroh\nkrabice\nkrach\nkrajina\nkralovat\nkrasopis\nkravata\nkredit\nkrejcar\nkresba\nkreveta\nkriket\nkritik\nkrize\nkrkavec\nkrmelec\nkrmivo\nkrocan\nkrok\nkronika\nkropit\nkroupa\nkrovka\nkrtek\nkruhadlo\nkrupice\nkrutost\nkrvinka\nkrychle\nkrypta\nkrystal\nkryt\nkudlanka\nkufr\nkujnost\nkukla\nkulajda\nkulich\nkulka\nkulomet\nkultura\nkuna\nkupodivu\nkurt\nkurzor\nkutil\nkvalita\nkvasinka\nkvestor\nkynolog\nkyselina\nkytara\nkytice\nkytka\nkytovec\nkyvadlo\nlabrador\nlachtan\nladnost\nlaik\nlakomec\nlamela\nlampa\nlanovka\nlasice\nlaso\nlastura\nlatinka\nlavina\nlebka\nleckdy\nleden\nlednice\nledovka\nledvina\nlegenda\nlegie\nlegrace\nlehce\nlehkost\nlehnout\nlektvar\nlenochod\nlentilka\nlepenka\nlepidlo\nletadlo\nletec\nletmo\nletokruh\nlevhart\nlevitace\nlevobok\nlibra\nlichotka\nlidojed\nlidskost\nlihovina\nlijavec\nlilek\nlimetka\nlinie\nlinka\nlinoleum\nlistopad\nlitina\nlitovat\nlobista\nlodivod\nlogika\nlogoped\nlokalita\nloket\nlomcovat\nlopata\nlopuch\nlord\nlosos\nlotr\nloudal\nlouh\nlouka\nlouskat\nlovec\nlstivost\nlucerna\nlucifer\nlump\nlusk\nlustrace\nlvice\nlyra\nlyrika\nlysina\nmadam\nmadlo\nmagistr\nmahagon\nmajetek\nmajitel\nmajorita\nmakak\nmakovice\nmakrela\nmalba\nmalina\nmalovat\nmalvice\nmaminka\nmandle\nmanko\nmarnost\nmasakr\nmaskot\nmasopust\nmatice\nmatrika\nmaturita\nmazanec\nmazivo\nmazlit\nmazurka\nmdloba\nmechanik\nmeditace\nmedovina\nmelasa\nmeloun\nmentolka\nmetla\nmetoda\nmetr\nmezera\nmigrace\nmihnout\nmihule\nmikina\nmikrofon\nmilenec\nmilimetr\nmilost\nmimika\nmincovna\nminibar\nminomet\nminulost\nmiska\nmistr\nmixovat\nmladost\nmlha\nmlhovina\nmlok\nmlsat\nmluvit\nmnich\nmnohem\nmobil\nmocnost\nmodelka\nmodlitba\nmohyla\nmokro\nmolekula\nmomentka\nmonarcha\nmonokl\nmonstrum\nmontovat\nmonzun\nmosaz\nmoskyt\nmost\nmotivace\nmotorka\nmotyka\nmoucha\nmoudrost\nmozaika\nmozek\nmozol\nmramor\nmravenec\nmrkev\nmrtvola\nmrzet\nmrzutost\nmstitel\nmudrc\nmuflon\nmulat\nmumie\nmunice\nmuset\nmutace\nmuzeum\nmuzikant\nmyslivec\nmzda\nnabourat\nnachytat\nnadace\nnadbytek\nnadhoz\nnadobro\nnadpis\nnahlas\nnahnat\nnahodile\nnahradit\nnaivita\nnajednou\nnajisto\nnajmout\nnaklonit\nnakonec\nnakrmit\nnalevo\nnamazat\nnamluvit\nnanometr\nnaoko\nnaopak\nnaostro\nnapadat\nnapevno\nnaplnit\nnapnout\nnaposled\nnaprosto\nnarodit\nnaruby\nnarychlo\nnasadit\nnasekat\nnaslepo\nnastat\nnatolik\nnavenek\nnavrch\nnavzdory\nnazvat\nnebe\nnechat\nnecky\nnedaleko\nnedbat\nneduh\nnegace\nnehet\nnehoda\nnejen\nnejprve\nneklid\nnelibost\nnemilost\nnemoc\nneochota\nneonka\nnepokoj\nnerost\nnerv\nnesmysl\nnesoulad\nnetvor\nneuron\nnevina\nnezvykle\nnicota\nnijak\nnikam\nnikdy\nnikl\nnikterak\nnitro\nnocleh\nnohavice\nnominace\nnora\nnorek\nnositel\nnosnost\nnouze\nnoviny\nnovota\nnozdra\nnuda\nnudle\nnuget\nnutit\nnutnost\nnutrie\nnymfa\nobal\nobarvit\nobava\nobdiv\nobec\nobehnat\nobejmout\nobezita\nobhajoba\nobilnice\nobjasnit\nobjekt\nobklopit\noblast\noblek\nobliba\nobloha\nobluda\nobnos\nobohatit\nobojek\nobout\nobrazec\nobrna\nobruba\nobrys\nobsah\nobsluha\nobstarat\nobuv\nobvaz\nobvinit\nobvod\nobvykle\nobyvatel\nobzor\nocas\nocel\nocenit\nochladit\nochota\nochrana\nocitnout\nodboj\nodbyt\nodchod\nodcizit\nodebrat\nodeslat\nodevzdat\nodezva\nodhadce\nodhodit\nodjet\nodjinud\nodkaz\nodkoupit\nodliv\nodluka\nodmlka\nodolnost\nodpad\nodpis\nodplout\nodpor\nodpustit\nodpykat\nodrazka\nodsoudit\nodstup\nodsun\nodtok\nodtud\nodvaha\nodveta\nodvolat\nodvracet\nodznak\nofina\nofsajd\nohlas\nohnisko\nohrada\nohrozit\nohryzek\nokap\nokenice\noklika\nokno\nokouzlit\nokovy\nokrasa\nokres\nokrsek\nokruh\nokupant\nokurka\nokusit\nolejnina\nolizovat\nomak\nomeleta\nomezit\nomladina\nomlouvat\nomluva\nomyl\nonehdy\nopakovat\nopasek\noperace\nopice\nopilost\nopisovat\nopora\nopozice\nopravdu\noproti\norbital\norchestr\norgie\norlice\norloj\nortel\nosada\noschnout\nosika\nosivo\noslava\noslepit\noslnit\noslovit\nosnova\nosoba\nosolit\nospalec\nosten\nostraha\nostuda\nostych\nosvojit\noteplit\notisk\notop\notrhat\notrlost\notrok\notruby\notvor\novanout\novar\noves\novlivnit\novoce\noxid\nozdoba\npachatel\npacient\npadouch\npahorek\npakt\npalanda\npalec\npalivo\npaluba\npamflet\npamlsek\npanenka\npanika\npanna\npanovat\npanstvo\npantofle\npaprika\nparketa\nparodie\nparta\nparuka\nparyba\npaseka\npasivita\npastelka\npatent\npatrona\npavouk\npazneht\npazourek\npecka\npedagog\npejsek\npeklo\npeloton\npenalta\npendrek\npenze\nperiskop\npero\npestrost\npetarda\npetice\npetrolej\npevnina\npexeso\npianista\npiha\npijavice\npikle\npiknik\npilina\npilnost\npilulka\npinzeta\npipeta\npisatel\npistole\npitevna\npivnice\npivovar\nplacenta\nplakat\nplamen\nplaneta\nplastika\nplatit\nplavidlo\nplaz\nplech\nplemeno\nplenta\nples\npletivo\nplevel\nplivat\nplnit\nplno\nplocha\nplodina\nplomba\nplout\npluk\nplyn\npobavit\npobyt\npochod\npocit\npoctivec\npodat\npodcenit\npodepsat\npodhled\npodivit\npodklad\npodmanit\npodnik\npodoba\npodpora\npodraz\npodstata\npodvod\npodzim\npoezie\npohanka\npohnutka\npohovor\npohroma\npohyb\npointa\npojistka\npojmout\npokazit\npokles\npokoj\npokrok\npokuta\npokyn\npoledne\npolibek\npolknout\npoloha\npolynom\npomalu\npominout\npomlka\npomoc\npomsta\npomyslet\nponechat\nponorka\nponurost\npopadat\npopel\npopisek\npoplach\npoprosit\npopsat\npopud\nporadce\nporce\nporod\nporucha\nporyv\nposadit\nposed\nposila\nposkok\nposlanec\nposoudit\npospolu\npostava\nposudek\nposyp\npotah\npotkan\npotlesk\npotomek\npotrava\npotupa\npotvora\npoukaz\npouto\npouzdro\npovaha\npovidla\npovlak\npovoz\npovrch\npovstat\npovyk\npovzdech\npozdrav\npozemek\npoznatek\npozor\npozvat\npracovat\nprahory\npraktika\nprales\npraotec\npraporek\nprase\npravda\nprincip\nprkno\nprobudit\nprocento\nprodej\nprofese\nprohra\nprojekt\nprolomit\npromile\npronikat\npropad\nprorok\nprosba\nproton\nproutek\nprovaz\nprskavka\nprsten\nprudkost\nprut\nprvek\nprvohory\npsanec\npsovod\npstruh\nptactvo\npuberta\npuch\npudl\npukavec\npuklina\npukrle\npult\npumpa\npunc\npupen\npusa\npusinka\npustina\nputovat\nputyka\npyramida\npysk\npytel\nracek\nrachot\nradiace\nradnice\nradon\nraft\nragby\nraketa\nrakovina\nrameno\nrampouch\nrande\nrarach\nrarita\nrasovna\nrastr\nratolest\nrazance\nrazidlo\nreagovat\nreakce\nrecept\nredaktor\nreferent\nreflex\nrejnok\nreklama\nrekord\nrekrut\nrektor\nreputace\nrevize\nrevma\nrevolver\nrezerva\nriskovat\nriziko\nrobotika\nrodokmen\nrohovka\nrokle\nrokoko\nromaneto\nropovod\nropucha\nrorejs\nrosol\nrostlina\nrotmistr\nrotoped\nrotunda\nroubenka\nroucho\nroup\nroura\nrovina\nrovnice\nrozbor\nrozchod\nrozdat\nrozeznat\nrozhodce\nrozinka\nrozjezd\nrozkaz\nrozloha\nrozmar\nrozpad\nrozruch\nrozsah\nroztok\nrozum\nrozvod\nrubrika\nruchadlo\nrukavice\nrukopis\nryba\nrybolov\nrychlost\nrydlo\nrypadlo\nrytina\nryzost\nsadista\nsahat\nsako\nsamec\nsamizdat\nsamota\nsanitka\nsardinka\nsasanka\nsatelit\nsazba\nsazenice\nsbor\nschovat\nsebranka\nsecese\nsedadlo\nsediment\nsedlo\nsehnat\nsejmout\nsekera\nsekta\nsekunda\nsekvoje\nsemeno\nseno\nservis\nsesadit\nseshora\nseskok\nseslat\nsestra\nsesuv\nsesypat\nsetba\nsetina\nsetkat\nsetnout\nsetrvat\nsever\nseznam\nshoda\nshrnout\nsifon\nsilnice\nsirka\nsirotek\nsirup\nsituace\nskafandr\nskalisko\nskanzen\nskaut\nskeptik\nskica\nskladba\nsklenice\nsklo\nskluz\nskoba\nskokan\nskoro\nskripta\nskrz\nskupina\nskvost\nskvrna\nslabika\nsladidlo\nslanina\nslast\nslavnost\nsledovat\nslepec\nsleva\nslezina\nslib\nslina\nsliznice\nslon\nsloupek\nslovo\nsluch\nsluha\nslunce\nslupka\nslza\nsmaragd\nsmetana\nsmilstvo\nsmlouva\nsmog\nsmrad\nsmrk\nsmrtka\nsmutek\nsmysl\nsnad\nsnaha\nsnob\nsobota\nsocha\nsodovka\nsokol\nsopka\nsotva\nsouboj\nsoucit\nsoudce\nsouhlas\nsoulad\nsoumrak\nsouprava\nsoused\nsoutok\nsouviset\nspalovna\nspasitel\nspis\nsplav\nspodek\nspojenec\nspolu\nsponzor\nspornost\nspousta\nsprcha\nspustit\nsranda\nsraz\nsrdce\nsrna\nsrnec\nsrovnat\nsrpen\nsrst\nsrub\nstanice\nstarosta\nstatika\nstavba\nstehno\nstezka\nstodola\nstolek\nstopa\nstorno\nstoupat\nstrach\nstres\nstrhnout\nstrom\nstruna\nstudna\nstupnice\nstvol\nstyk\nsubjekt\nsubtropy\nsuchar\nsudost\nsukno\nsundat\nsunout\nsurikata\nsurovina\nsvah\nsvalstvo\nsvetr\nsvatba\nsvazek\nsvisle\nsvitek\nsvoboda\nsvodidlo\nsvorka\nsvrab\nsykavka\nsykot\nsynek\nsynovec\nsypat\nsypkost\nsyrovost\nsysel\nsytost\ntabletka\ntabule\ntahoun\ntajemno\ntajfun\ntajga\ntajit\ntajnost\ntaktika\ntamhle\ntampon\ntancovat\ntanec\ntanker\ntapeta\ntavenina\ntazatel\ntechnika\ntehdy\ntekutina\ntelefon\ntemnota\ntendence\ntenista\ntenor\nteplota\ntepna\nteprve\nterapie\ntermoska\ntextil\nticho\ntiskopis\ntitulek\ntkadlec\ntkanina\ntlapka\ntleskat\ntlukot\ntlupa\ntmel\ntoaleta\ntopinka\ntopol\ntorzo\ntouha\ntoulec\ntradice\ntraktor\ntramp\ntrasa\ntraverza\ntrefit\ntrest\ntrezor\ntrhavina\ntrhlina\ntrochu\ntrojice\ntroska\ntrouba\ntrpce\ntrpitel\ntrpkost\ntrubec\ntruchlit\ntruhlice\ntrus\ntrvat\ntudy\ntuhnout\ntuhost\ntundra\nturista\nturnaj\ntuzemsko\ntvaroh\ntvorba\ntvrdost\ntvrz\ntygr\ntykev\nubohost\nuboze\nubrat\nubrousek\nubrus\nubytovna\nucho\nuctivost\nudivit\nuhradit\nujednat\nujistit\nujmout\nukazatel\nuklidnit\nuklonit\nukotvit\nukrojit\nulice\nulita\nulovit\numyvadlo\nunavit\nuniforma\nuniknout\nupadnout\nuplatnit\nuplynout\nupoutat\nupravit\nuran\nurazit\nusednout\nusilovat\nusmrtit\nusnadnit\nusnout\nusoudit\nustlat\nustrnout\nutahovat\nutkat\nutlumit\nutonout\nutopenec\nutrousit\nuvalit\nuvolnit\nuvozovka\nuzdravit\nuzel\nuzenina\nuzlina\nuznat\nvagon\nvalcha\nvaloun\nvana\nvandal\nvanilka\nvaran\nvarhany\nvarovat\nvcelku\nvchod\nvdova\nvedro\nvegetace\nvejce\nvelbloud\nveletrh\nvelitel\nvelmoc\nvelryba\nvenkov\nveranda\nverze\nveselka\nveskrze\nvesnice\nvespodu\nvesta\nveterina\nveverka\nvibrace\nvichr\nvideohra\nvidina\nvidle\nvila\nvinice\nviset\nvitalita\nvize\nvizitka\nvjezd\nvklad\nvkus\nvlajka\nvlak\nvlasec\nvlevo\nvlhkost\nvliv\nvlnovka\nvloupat\nvnucovat\nvnuk\nvoda\nvodivost\nvodoznak\nvodstvo\nvojensky\nvojna\nvojsko\nvolant\nvolba\nvolit\nvolno\nvoskovka\nvozidlo\nvozovna\nvpravo\nvrabec\nvracet\nvrah\nvrata\nvrba\nvrcholek\nvrhat\nvrstva\nvrtule\nvsadit\nvstoupit\nvstup\nvtip\nvybavit\nvybrat\nvychovat\nvydat\nvydra\nvyfotit\nvyhledat\nvyhnout\nvyhodit\nvyhradit\nvyhubit\nvyjasnit\nvyjet\nvyjmout\nvyklopit\nvykonat\nvylekat\nvymazat\nvymezit\nvymizet\nvymyslet\nvynechat\nvynikat\nvynutit\nvypadat\nvyplatit\nvypravit\nvypustit\nvyrazit\nvyrovnat\nvyrvat\nvyslovit\nvysoko\nvystavit\nvysunout\nvysypat\nvytasit\nvytesat\nvytratit\nvyvinout\nvyvolat\nvyvrhel\nvyzdobit\nvyznat\nvzadu\nvzbudit\nvzchopit\nvzdor\nvzduch\nvzdychat\nvzestup\nvzhledem\nvzkaz\nvzlykat\nvznik\nvzorek\nvzpoura\nvztah\nvztek\nxylofon\nzabrat\nzabydlet\nzachovat\nzadarmo\nzadusit\nzafoukat\nzahltit\nzahodit\nzahrada\nzahynout\nzajatec\nzajet\nzajistit\nzaklepat\nzakoupit\nzalepit\nzamezit\nzamotat\nzamyslet\nzanechat\nzanikat\nzaplatit\nzapojit\nzapsat\nzarazit\nzastavit\nzasunout\nzatajit\nzatemnit\nzatknout\nzaujmout\nzavalit\nzavelet\nzavinit\nzavolat\nzavrtat\nzazvonit\nzbavit\nzbrusu\nzbudovat\nzbytek\nzdaleka\nzdarma\nzdatnost\nzdivo\nzdobit\nzdroj\nzdvih\nzdymadlo\nzelenina\nzeman\nzemina\nzeptat\nzezadu\nzezdola\nzhatit\nzhltnout\nzhluboka\nzhotovit\nzhruba\nzima\nzimnice\nzjemnit\nzklamat\nzkoumat\nzkratka\nzkumavka\nzlato\nzlehka\nzloba\nzlom\nzlost\nzlozvyk\nzmapovat\nzmar\nzmatek\nzmije\nzmizet\nzmocnit\nzmodrat\nzmrzlina\nzmutovat\nznak\nznalost\nznamenat\nznovu\nzobrazit\nzotavit\nzoubek\nzoufale\nzplodit\nzpomalit\nzprava\nzprostit\nzprudka\nzprvu\nzrada\nzranit\nzrcadlo\nzrnitost\nzrno\nzrovna\nzrychlit\nzrzavost\nzticha\nztratit\nzubovina\nzubr\nzvednout\nzvenku\nzvesela\nzvon\nzvrat\nzvukovod\nzvyk" + ); _WordLists = dico; } @@ -33,10 +49,14 @@ static HardcodedWordlistSource() { if (!_WordLists.TryGetValue(name, out var list)) return null; - return Task.FromResult(new Wordlist(list.Split(["\n"], StringSplitOptions.RemoveEmptyEntries), - name == "japanese" ? ' ' : ' ', name - )); + return Task.FromResult( + new Wordlist( + list.Split(["\n"], StringSplitOptions.RemoveEmptyEntries), + name == "japanese" ? ' ' : ' ', + name + ) + ); } #endregion -} \ No newline at end of file +} diff --git a/DotNut/NBitcoin/BIP39/IWordlistSource.cs b/DotNut/NBitcoin/BIP39/IWordlistSource.cs index a40557c..6b51336 100644 --- a/DotNut/NBitcoin/BIP39/IWordlistSource.cs +++ b/DotNut/NBitcoin/BIP39/IWordlistSource.cs @@ -4,4 +4,4 @@ public interface IWordlistSource { Task? Load(string name); } -} \ No newline at end of file +} diff --git a/DotNut/NBitcoin/BIP39/KDTable.cs b/DotNut/NBitcoin/BIP39/KDTable.cs index 85926ea..05d3f4b 100644 --- a/DotNut/NBitcoin/BIP39/KDTable.cs +++ b/DotNut/NBitcoin/BIP39/KDTable.cs @@ -2,71 +2,74 @@ namespace DotNut.NBitcoin.BIP39 { - class KDTable - { - public static string NormalizeKD(string str) - { - StringBuilder builder = new StringBuilder(str.Length); - foreach (char c in str.ToCharArray()) - { - if (!Supported(c)) - { - throw new PlatformNotSupportedException("the input string can't be normalized on this platform"); - } - Substitute(c, builder); - } - return builder.ToString(); - } + class KDTable + { + public static string NormalizeKD(string str) + { + StringBuilder builder = new StringBuilder(str.Length); + foreach (char c in str.ToCharArray()) + { + if (!Supported(c)) + { + throw new PlatformNotSupportedException( + "the input string can't be normalized on this platform" + ); + } + Substitute(c, builder); + } + return builder.ToString(); + } - private static void Substitute(char c, StringBuilder builder) - { - for (int i = 0; i < _SubstitutionTable.Length; i++) - { - var substituedChar = _SubstitutionTable[i]; - if (substituedChar == c) - { - Substitute(i, builder); - return; - } - if (substituedChar > c) - break; - while (_SubstitutionTable[i] != '\n') - i++; - } - builder.Append(c); - } + private static void Substitute(char c, StringBuilder builder) + { + for (int i = 0; i < _SubstitutionTable.Length; i++) + { + var substituedChar = _SubstitutionTable[i]; + if (substituedChar == c) + { + Substitute(i, builder); + return; + } + if (substituedChar > c) + break; + while (_SubstitutionTable[i] != '\n') + i++; + } + builder.Append(c); + } - private static void Substitute(int pos, StringBuilder builder) - { - for (int i = pos + 1; i < _SubstitutionTable.Length; i++) - { - if (_SubstitutionTable[i] == '\n') - break; - builder.Append(_SubstitutionTable[i]); - } - } + private static void Substitute(int pos, StringBuilder builder) + { + for (int i = pos + 1; i < _SubstitutionTable.Length; i++) + { + if (_SubstitutionTable[i] == '\n') + break; + builder.Append(_SubstitutionTable[i]); + } + } - private static bool Supported(char c) - { - return _SupportedChars.Any(r => r[0] <= c && c <= r[1]); - } + private static bool Supported(char c) + { + return _SupportedChars.Any(r => r[0] <= c && c <= r[1]); + } - - static int[][] _SupportedChars = new int[][]{ -new[]{0,1000}, -new[]{12352,12447}, -new[]{12448,12543}, -new[]{19968,40959}, -new[]{13312,19967}, -new[]{131072,173791}, -new[]{63744,64255}, -new[]{194560,195103}, -new[]{13056,13311}, -new[]{12288,12351}, -new[]{65280,65535}, -new[]{8192,8303}, -new[]{8352,8399}, -}; - const string _SubstitutionTable = "  \n¨ ̈\nªa\n¯ ̄\n²2\n³3\n´ ́\nµμ\n¸ ̧\n¹1\nºo\n¼1⁄4\n½1⁄2\n¾3⁄4\nÀÀ\nÁÁ\nÂÂ\nÃÃ\nÄÄ\nÅÅ\nÇÇ\nÈÈ\nÉÉ\nÊÊ\nËË\nÌÌ\nÍÍ\nÎÎ\nÏÏ\nÑÑ\nÒÒ\nÓÓ\nÔÔ\nÕÕ\nÖÖ\nÙÙ\nÚÚ\nÛÛ\nÜÜ\nÝÝ\nàà\náá\nââ\nãã\nää\nåå\nçç\nèè\néé\nêê\nëë\nìì\níí\nîî\nïï\nññ\nòò\nóó\nôô\nõõ\nöö\nùù\núú\nûû\nüü\nýý\nÿÿ\nĀĀ\nāā\nĂĂ\năă\nĄĄ\nąą\nĆĆ\nćć\nĈĈ\nĉĉ\nĊĊ\nċċ\nČČ\nčč\nĎĎ\nďď\nĒĒ\nēē\nĔĔ\nĕĕ\nĖĖ\nėė\nĘĘ\nęę\nĚĚ\něě\nĜĜ\nĝĝ\nĞĞ\nğğ\nĠĠ\nġġ\nĢĢ\nģģ\nĤĤ\nĥĥ\nĨĨ\nĩĩ\nĪĪ\nīī\nĬĬ\nĭĭ\nĮĮ\nįį\nİİ\nIJIJ\nijij\nĴĴ\nĵĵ\nĶĶ\nķķ\nĹĹ\nĺĺ\nĻĻ\nļļ\nĽĽ\nľľ\nĿL·\nŀl·\nŃŃ\nńń\nŅŅ\nņņ\nŇŇ\nňň\nʼnʼn\nŌŌ\nōō\nŎŎ\nŏŏ\nŐŐ\nőő\nŔŔ\nŕŕ\nŖŖ\nŗŗ\nŘŘ\nřř\nŚŚ\nśś\nŜŜ\nŝŝ\nŞŞ\nşş\nŠŠ\nšš\nŢŢ\nţţ\nŤŤ\nťť\nŨŨ\nũũ\nŪŪ\nūū\nŬŬ\nŭŭ\nŮŮ\nůů\nŰŰ\nűű\nŲŲ\nųų\nŴŴ\nŵŵ\nŶŶ\nŷŷ\nŸŸ\nŹŹ\nźź\nŻŻ\nżż\nŽŽ\nžž\nſs\nƠƠ\nơơ\nƯƯ\nưư\nDŽDŽ\nDžDž\ndždž\nLJLJ\nLjLj\nljlj\nNJNJ\nNjNj\nnjnj\nǍǍ\nǎǎ\nǏǏ\nǐǐ\nǑǑ\nǒǒ\nǓǓ\nǔǔ\nǕǕ\nǖǖ\nǗǗ\nǘǘ\nǙǙ\nǚǚ\nǛǛ\nǜǜ\nǞǞ\nǟǟ\nǠǠ\nǡǡ\nǢǢ\nǣǣ\nǦǦ\nǧǧ\nǨǨ\nǩǩ\nǪǪ\nǫǫ\nǬǬ\nǭǭ\nǮǮ\nǯǯ\nǰǰ\nDZDZ\nDzDz\ndzdz\nǴǴ\nǵǵ\nǸǸ\nǹǹ\nǺǺ\nǻǻ\nǼǼ\nǽǽ\nǾǾ\nǿǿ\nȀȀ\nȁȁ\nȂȂ\nȃȃ\nȄȄ\nȅȅ\nȆȆ\nȇȇ\nȈȈ\nȉȉ\nȊȊ\nȋȋ\nȌȌ\nȍȍ\nȎȎ\nȏȏ\nȐȐ\nȑȑ\nȒȒ\nȓȓ\nȔȔ\nȕȕ\nȖȖ\nȗȗ\nȘȘ\nșș\nȚȚ\nțț\nȞȞ\nȟȟ\nȦȦ\nȧȧ\nȨȨ\nȩȩ\nȪȪ\nȫȫ\nȬȬ\nȭȭ\nȮȮ\nȯȯ\nȰȰ\nȱȱ\nȲȲ\nȳȳ\nʰh\nʱɦ\nʲj\nʳr\nʴɹ\nʵɻ\nʶʁ\nʷw\nʸy\n˘ ̆\n˙ ̇\n˚ ̊\n˛ ̨\n˜ ̃\n˝ ̋\nˠɣ\nˡl\nˢs\nˣx\nˤʕ\ǹ̀\ń́\n̓̓\n̈́̈́\nʹʹ\nͺ ͅ\n;;\n΄ ́\n΅ ̈́\nΆΆ\n··\nΈΈ\nΉΉ\nΊΊ\nΌΌ\nΎΎ\nΏΏ\nΐΐ\nΪΪ\nΫΫ\nάά\nέέ\nήή\nίί\nΰΰ\nϊϊ\nϋϋ\nόό\nύύ\nώώ\nϐβ\nϑθ\nϒΥ\nϓΎ\nϔΫ\nϕφ\nϖπ\nϰκ\nϱρ\nϲς\nϴΘ\nϵε\nϹΣ\nЀЀ\nЁЁ\nЃЃ\nЇЇ\nЌЌ\nЍЍ\nЎЎ\nЙЙ\nйй\nѐѐ\nёё\nѓѓ\nїї\nќќ\nѝѝ\nўў\nѶѶ\nѷѷ\nӁӁ\nӂӂ\nӐӐ\nӑӑ\nӒӒ\nӓӓ\nӖӖ\nӗӗ\nӚӚ\nӛӛ\nӜӜ\nӝӝ\nӞӞ\nӟӟ\nӢӢ\nӣӣ\nӤӤ\nӥӥ\nӦӦ\nӧӧ\nӪӪ\nӫӫ\nӬӬ\nӭӭ\nӮӮ\nӯӯ\nӰӰ\nӱӱ\nӲӲ\nӳӳ\nӴӴ\nӵӵ\nӸӸ\nӹӹ\nևեւ\nآآ\nأأ\nؤؤ\nإإ\nئئ\nٵاٴ\nٶوٴ\nٷۇٴ\nٸيٴ\nۀۀ\nۂۂ\nۓۓ\nऩऩ\nऱऱ\nऴऴ\nक़क़\nख़ख़\nग़ग़\nज़ज़\nड़ड़\nढ़ढ़\nफ़फ़\nय़य़\nোো\nৌৌ\nড়ড়\nঢ়ঢ়\nয়য়\nਲ਼ਲ਼\nਸ਼ਸ਼\nਖ਼ਖ਼\nਗ਼ਗ਼\nਜ਼ਜ਼\nਫ਼ਫ਼\nୈୈ\nୋୋ\nୌୌ\nଡ଼ଡ଼\nଢ଼ଢ଼\nஔஔ\nொொ\nோோ\nௌௌ\nైై\nೀೀ\nೇೇ\nೈೈ\nೊೊ\nೋೋ\nൊൊ\nോോ\nൌൌ\nේේ\nොො\nෝෝ\nෞෞ\nำํา\nຳໍາ\nໜຫນ\nໝຫມ\n༌་\nགྷགྷ\nཌྷཌྷ\nདྷདྷ\nབྷབྷ\nཛྷཛྷ\nཀྵཀྵ\nཱཱིི\nཱཱུུ\nྲྀྲྀ\nཷྲཱྀ\nླྀླྀ\nཹླཱྀ\nཱཱྀྀ\nྒྷྒྷ\nྜྷྜྷ\nྡྷྡྷ\nྦྷྦྷ\nྫྷྫྷ\nྐྵྐྵ\nဦဦ\nჼნ\nᬆᬆ\nᬈᬈ\nᬊᬊ\nᬌᬌ\nᬎᬎ\nᬒᬒ\nᬻᬻ\nᬽᬽ\nᭀᭀ\nᭁᭁ\nᭃᭃ\nᴬA\nᴭÆ\nᴮB\nᴰD\nᴱE\nᴲƎ\nᴳG\nᴴH\nᴵI\nᴶJ\nᴷK\nᴸL\nᴹM\nᴺN\nᴼO\nᴽȢ\nᴾP\nᴿR\nᵀT\nᵁU\nᵂW\nᵃa\nᵄɐ\nᵅɑ\nᵆᴂ\nᵇb\nᵈd\nᵉe\nᵊə\nᵋɛ\nᵌɜ\nᵍg\nᵏk\nᵐm\nᵑŋ\nᵒo\nᵓɔ\nᵔᴖ\nᵕᴗ\nᵖp\nᵗt\nᵘu\nᵙᴝ\nᵚɯ\nᵛv\nᵜᴥ\nᵝβ\nᵞγ\nᵟδ\nᵠφ\nᵡχ\nᵢi\nᵣr\nᵤu\nᵥv\nᵦβ\nᵧγ\nᵨρ\nᵩφ\nᵪχ\nᵸн\nᶛɒ\nᶜc\nᶝɕ\nᶞð\nᶟɜ\nᶠf\nᶡɟ\nᶢɡ\nᶣɥ\nᶤɨ\nᶥɩ\nᶦɪ\nᶧᵻ\nᶨʝ\nᶩɭ\nᶪᶅ\nᶫʟ\nᶬɱ\nᶭɰ\nᶮɲ\nᶯɳ\nᶰɴ\nᶱɵ\nᶲɸ\nᶳʂ\nᶴʃ\nᶵƫ\nᶶʉ\nᶷʊ\nᶸᴜ\nᶹʋ\nᶺʌ\nᶻz\nᶼʐ\nᶽʑ\nᶾʒ\nᶿθ\nḀḀ\nḁḁ\nḂḂ\nḃḃ\nḄḄ\nḅḅ\nḆḆ\nḇḇ\nḈḈ\nḉḉ\nḊḊ\nḋḋ\nḌḌ\nḍḍ\nḎḎ\nḏḏ\nḐḐ\nḑḑ\nḒḒ\nḓḓ\nḔḔ\nḕḕ\nḖḖ\nḗḗ\nḘḘ\nḙḙ\nḚḚ\nḛḛ\nḜḜ\nḝḝ\nḞḞ\nḟḟ\nḠḠ\nḡḡ\nḢḢ\nḣḣ\nḤḤ\nḥḥ\nḦḦ\nḧḧ\nḨḨ\nḩḩ\nḪḪ\nḫḫ\nḬḬ\nḭḭ\nḮḮ\nḯḯ\nḰḰ\nḱḱ\nḲḲ\nḳḳ\nḴḴ\nḵḵ\nḶḶ\nḷḷ\nḸḸ\nḹḹ\nḺḺ\nḻḻ\nḼḼ\nḽḽ\nḾḾ\nḿḿ\nṀṀ\nṁṁ\nṂṂ\nṃṃ\nṄṄ\nṅṅ\nṆṆ\nṇṇ\nṈṈ\nṉṉ\nṊṊ\nṋṋ\nṌṌ\nṍṍ\nṎṎ\nṏṏ\nṐṐ\nṑṑ\nṒṒ\nṓṓ\nṔṔ\nṕṕ\nṖṖ\nṗṗ\nṘṘ\nṙṙ\nṚṚ\nṛṛ\nṜṜ\nṝṝ\nṞṞ\nṟṟ\nṠṠ\nṡṡ\nṢṢ\nṣṣ\nṤṤ\nṥṥ\nṦṦ\nṧṧ\nṨṨ\nṩṩ\nṪṪ\nṫṫ\nṬṬ\nṭṭ\nṮṮ\nṯṯ\nṰṰ\nṱṱ\nṲṲ\nṳṳ\nṴṴ\nṵṵ\nṶṶ\nṷṷ\nṸṸ\nṹṹ\nṺṺ\nṻṻ\nṼṼ\nṽṽ\nṾṾ\nṿṿ\nẀẀ\nẁẁ\nẂẂ\nẃẃ\nẄẄ\nẅẅ\nẆẆ\nẇẇ\nẈẈ\nẉẉ\nẊẊ\nẋẋ\nẌẌ\nẍẍ\nẎẎ\nẏẏ\nẐẐ\nẑẑ\nẒẒ\nẓẓ\nẔẔ\nẕẕ\nẖẖ\nẗẗ\nẘẘ\nẙẙ\nẚaʾ\nẛṡ\nẠẠ\nạạ\nẢẢ\nảả\nẤẤ\nấấ\nẦẦ\nầầ\nẨẨ\nẩẩ\nẪẪ\nẫẫ\nẬẬ\nậậ\nẮẮ\nắắ\nẰẰ\nằằ\nẲẲ\nẳẳ\nẴẴ\nẵẵ\nẶẶ\nặặ\nẸẸ\nẹẹ\nẺẺ\nẻẻ\nẼẼ\nẽẽ\nẾẾ\nếế\nỀỀ\nềề\nỂỂ\nểể\nỄỄ\nễễ\nỆỆ\nệệ\nỈỈ\nỉỉ\nỊỊ\nịị\nỌỌ\nọọ\nỎỎ\nỏỏ\nỐỐ\nốố\nỒỒ\nồồ\nỔỔ\nổổ\nỖỖ\nỗỗ\nỘỘ\nộộ\nỚỚ\nớớ\nỜỜ\nờờ\nỞỞ\nởở\nỠỠ\nỡỡ\nỢỢ\nợợ\nỤỤ\nụụ\nỦỦ\nủủ\nỨỨ\nứứ\nỪỪ\nừừ\nỬỬ\nửử\nỮỮ\nữữ\nỰỰ\nựự\nỲỲ\nỳỳ\nỴỴ\nỵỵ\nỶỶ\nỷỷ\nỸỸ\nỹỹ\nἀἀ\nἁἁ\nἂἂ\nἃἃ\nἄἄ\nἅἅ\nἆἆ\nἇἇ\nἈἈ\nἉἉ\nἊἊ\nἋἋ\nἌἌ\nἍἍ\nἎἎ\nἏἏ\nἐἐ\nἑἑ\nἒἒ\nἓἓ\nἔἔ\nἕἕ\nἘἘ\nἙἙ\nἚἚ\nἛἛ\nἜἜ\nἝἝ\nἠἠ\nἡἡ\nἢἢ\nἣἣ\nἤἤ\nἥἥ\nἦἦ\nἧἧ\nἨἨ\nἩἩ\nἪἪ\nἫἫ\nἬἬ\nἭἭ\nἮἮ\nἯἯ\nἰἰ\nἱἱ\nἲἲ\nἳἳ\nἴἴ\nἵἵ\nἶἶ\nἷἷ\nἸἸ\nἹἹ\nἺἺ\nἻἻ\nἼἼ\nἽἽ\nἾἾ\nἿἿ\nὀὀ\nὁὁ\nὂὂ\nὃὃ\nὄὄ\nὅὅ\nὈὈ\nὉὉ\nὊὊ\nὋὋ\nὌὌ\nὍὍ\nὐὐ\nὑὑ\nὒὒ\nὓὓ\nὔὔ\nὕὕ\nὖὖ\nὗὗ\nὙὙ\nὛὛ\nὝὝ\nὟὟ\nὠὠ\nὡὡ\nὢὢ\nὣὣ\nὤὤ\nὥὥ\nὦὦ\nὧὧ\nὨὨ\nὩὩ\nὪὪ\nὫὫ\nὬὬ\nὭὭ\nὮὮ\nὯὯ\nὰὰ\nάά\nὲὲ\nέέ\nὴὴ\nήή\nὶὶ\nίί\nὸὸ\nόό\nὺὺ\nύύ\nὼὼ\nώώ\nᾀᾀ\nᾁᾁ\nᾂᾂ\nᾃᾃ\nᾄᾄ\nᾅᾅ\nᾆᾆ\nᾇᾇ\nᾈᾈ\nᾉᾉ\nᾊᾊ\nᾋᾋ\nᾌᾌ\nᾍᾍ\nᾎᾎ\nᾏᾏ\nᾐᾐ\nᾑᾑ\nᾒᾒ\nᾓᾓ\nᾔᾔ\nᾕᾕ\nᾖᾖ\nᾗᾗ\nᾘᾘ\nᾙᾙ\nᾚᾚ\nᾛᾛ\nᾜᾜ\nᾝᾝ\nᾞᾞ\nᾟᾟ\nᾠᾠ\nᾡᾡ\nᾢᾢ\nᾣᾣ\nᾤᾤ\nᾥᾥ\nᾦᾦ\nᾧᾧ\nᾨᾨ\nᾩᾩ\nᾪᾪ\nᾫᾫ\nᾬᾬ\nᾭᾭ\nᾮᾮ\nᾯᾯ\nᾰᾰ\nᾱᾱ\nᾲᾲ\nᾳᾳ\nᾴᾴ\nᾶᾶ\nᾷᾷ\nᾸᾸ\nᾹᾹ\nᾺᾺ\nΆΆ\nᾼᾼ\n᾽ ̓\nιι\n᾿ ̓\n῀ ͂\n῁ ̈͂\nῂῂ\nῃῃ\nῄῄ\nῆῆ\nῇῇ\nῈῈ\nΈΈ\nῊῊ\nΉΉ\nῌῌ\n῍ ̓̀\n῎ ̓́\n῏ ̓͂\nῐῐ\nῑῑ\nῒῒ\nΐΐ\nῖῖ\nῗῗ\nῘῘ\nῙῙ\nῚῚ\nΊΊ\n῝ ̔̀\n῞ ̔́\n῟ ̔͂\nῠῠ\nῡῡ\nῢῢ\nΰΰ\nῤῤ\nῥῥ\nῦῦ\nῧῧ\nῨῨ\nῩῩ\nῪῪ\nΎΎ\nῬῬ\n῭ ̈̀\n΅ ̈́\n``\nῲῲ\nῳῳ\nῴῴ\nῶῶ\nῷῷ\nῸῸ\nΌΌ\nῺῺ\nΏΏ\nῼῼ\n´ ́\n῾ ̔\n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n‑‐\n‗ ̳\n․.\n‥..\n…...\n  \n″′′\n‴′′′\n‶‵‵\n‷‵‵‵\n‼!!\n‾ ̅\n⁇??\n⁈?!\n⁉!?\n⁗′′′′\n  \n⁰0\nⁱi\n⁴4\n⁵5\n⁶6\n⁷7\n⁸8\n⁹9\n⁺+\n⁻−\n⁼=\n⁽(\n⁾)\nⁿn\n₀0\n₁1\n₂2\n₃3\n₄4\n₅5\n₆6\n₇7\n₈8\n₉9\n₊+\n₋−\n₌=\n₍(\n₎)\nₐa\nₑe\nₒo\nₓx\nₔə\n₨Rs\n℀a/c\n℁a/s\nℂC\n℃°C\n℅c/o\n℆c/u\nℇƐ\n℉°F\nℊg\nℋH\nℌH\nℍH\nℎh\nℏħ\nℐI\nℑI\nℒL\nℓl\nℕN\n№No\nℙP\nℚQ\nℛR\nℜR\nℝR\n℠SM\n℡TEL\n™TM\nℤZ\nΩΩ\nℨZ\nKK\nÅÅ\nℬB\nℭC\nℯe\nℰE\nℱF\nℳM\nℴo\nℵא\nℶב\nℷג\nℸד\nℹi\n℻FAX\nℼπ\nℽγ\nℾΓ\nℿΠ\n⅀∑\nⅅD\nⅆd\nⅇe\nⅈi\nⅉj\n⅓1⁄3\n⅔2⁄3\n⅕1⁄5\n⅖2⁄5\n⅗3⁄5\n⅘4⁄5\n⅙1⁄6\n⅚5⁄6\n⅛1⁄8\n⅜3⁄8\n⅝5⁄8\n⅞7⁄8\n⅟1⁄\nⅠI\nⅡII\nⅢIII\nⅣIV\nⅤV\nⅥVI\nⅦVII\nⅧVIII\nⅨIX\nⅩX\nⅪXI\nⅫXII\nⅬL\nⅭC\nⅮD\nⅯM\nⅰi\nⅱii\nⅲiii\nⅳiv\nⅴv\nⅵvi\nⅶvii\nⅷviii\nⅸix\nⅹx\nⅺxi\nⅻxii\nⅼl\nⅽc\nⅾd\nⅿm\n↚↚\n↛↛\n↮↮\n⇍⇍\n⇎⇎\n⇏⇏\n∄∄\n∉∉\n∌∌\n∤∤\n∦∦\n∬∫∫\n∭∫∫∫\n∯∮∮\n∰∮∮∮\n≁≁\n≄≄\n≇≇\n≉≉\n≠≠\n≢≢\n≭≭\n≮≮\n≯≯\n≰≰\n≱≱\n≴≴\n≵≵\n≸≸\n≹≹\n⊀⊀\n⊁⊁\n⊄⊄\n⊅⊅\n⊈⊈\n⊉⊉\n⊬⊬\n⊭⊭\n⊮⊮\n⊯⊯\n⋠⋠\n⋡⋡\n⋢⋢\n⋣⋣\n⋪⋪\n⋫⋫\n⋬⋬\n⋭⋭\n〈〈\n〉〉\n①1\n②2\n③3\n④4\n⑤5\n⑥6\n⑦7\n⑧8\n⑨9\n⑩10\n⑪11\n⑫12\n⑬13\n⑭14\n⑮15\n⑯16\n⑰17\n⑱18\n⑲19\n⑳20\n⑴(1)\n⑵(2)\n⑶(3)\n⑷(4)\n⑸(5)\n⑹(6)\n⑺(7)\n⑻(8)\n⑼(9)\n⑽(10)\n⑾(11)\n⑿(12)\n⒀(13)\n⒁(14)\n⒂(15)\n⒃(16)\n⒄(17)\n⒅(18)\n⒆(19)\n⒇(20)\n⒈1.\n⒉2.\n⒊3.\n⒋4.\n⒌5.\n⒍6.\n⒎7.\n⒏8.\n⒐9.\n⒑10.\n⒒11.\n⒓12.\n⒔13.\n⒕14.\n⒖15.\n⒗16.\n⒘17.\n⒙18.\n⒚19.\n⒛20.\n⒜(a)\n⒝(b)\n⒞(c)\n⒟(d)\n⒠(e)\n⒡(f)\n⒢(g)\n⒣(h)\n⒤(i)\n⒥(j)\n⒦(k)\n⒧(l)\n⒨(m)\n⒩(n)\n⒪(o)\n⒫(p)\n⒬(q)\n⒭(r)\n⒮(s)\n⒯(t)\n⒰(u)\n⒱(v)\n⒲(w)\n⒳(x)\n⒴(y)\n⒵(z)\nⒶA\nⒷB\nⒸC\nⒹD\nⒺE\nⒻF\nⒼG\nⒽH\nⒾI\nⒿJ\nⓀK\nⓁL\nⓂM\nⓃN\nⓄO\nⓅP\nⓆQ\nⓇR\nⓈS\nⓉT\nⓊU\nⓋV\nⓌW\nⓍX\nⓎY\nⓏZ\nⓐa\nⓑb\nⓒc\nⓓd\nⓔe\nⓕf\nⓖg\nⓗh\nⓘi\nⓙj\nⓚk\nⓛl\nⓜm\nⓝn\nⓞo\nⓟp\nⓠq\nⓡr\nⓢs\nⓣt\nⓤu\nⓥv\nⓦw\nⓧx\nⓨy\nⓩz\n⓪0\n⨌∫∫∫∫\n⩴::=\n⩵==\n⩶===\n⫝̸⫝̸\nⱼj\nⱽV\nⵯⵡ\n⺟母\n⻳龟\n⼀一\n⼁丨\n⼂丶\n⼃丿\n⼄乙\n⼅亅\n⼆二\n⼇亠\n⼈人\n⼉儿\n⼊入\n⼋八\n⼌冂\n⼍冖\n⼎冫\n⼏几\n⼐凵\n⼑刀\n⼒力\n⼓勹\n⼔匕\n⼕匚\n⼖匸\n⼗十\n⼘卜\n⼙卩\n⼚厂\n⼛厶\n⼜又\n⼝口\n⼞囗\n⼟土\n⼠士\n⼡夂\n⼢夊\n⼣夕\n⼤大\n⼥女\n⼦子\n⼧宀\n⼨寸\n⼩小\n⼪尢\n⼫尸\n⼬屮\n⼭山\n⼮巛\n⼯工\n⼰己\n⼱巾\n⼲干\n⼳幺\n⼴广\n⼵廴\n⼶廾\n⼷弋\n⼸弓\n⼹彐\n⼺彡\n⼻彳\n⼼心\n⼽戈\n⼾戶\n⼿手\n⽀支\n⽁攴\n⽂文\n⽃斗\n⽄斤\n⽅方\n⽆无\n⽇日\n⽈曰\n⽉月\n⽊木\n⽋欠\n⽌止\n⽍歹\n⽎殳\n⽏毋\n⽐比\n⽑毛\n⽒氏\n⽓气\n⽔水\n⽕火\n⽖爪\n⽗父\n⽘爻\n⽙爿\n⽚片\n⽛牙\n⽜牛\n⽝犬\n⽞玄\n⽟玉\n⽠瓜\n⽡瓦\n⽢甘\n⽣生\n⽤用\n⽥田\n⽦疋\n⽧疒\n⽨癶\n⽩白\n⽪皮\n⽫皿\n⽬目\n⽭矛\n⽮矢\n⽯石\n⽰示\n⽱禸\n⽲禾\n⽳穴\n⽴立\n⽵竹\n⽶米\n⽷糸\n⽸缶\n⽹网\n⽺羊\n⽻羽\n⽼老\n⽽而\n⽾耒\n⽿耳\n⾀聿\n⾁肉\n⾂臣\n⾃自\n⾄至\n⾅臼\n⾆舌\n⾇舛\n⾈舟\n⾉艮\n⾊色\n⾋艸\n⾌虍\n⾍虫\n⾎血\n⾏行\n⾐衣\n⾑襾\n⾒見\n⾓角\n⾔言\n⾕谷\n⾖豆\n⾗豕\n⾘豸\n⾙貝\n⾚赤\n⾛走\n⾜足\n⾝身\n⾞車\n⾟辛\n⾠辰\n⾡辵\n⾢邑\n⾣酉\n⾤釆\n⾥里\n⾦金\n⾧長\n⾨門\n⾩阜\n⾪隶\n⾫隹\n⾬雨\n⾭靑\n⾮非\n⾯面\n⾰革\n⾱韋\n⾲韭\n⾳音\n⾴頁\n⾵風\n⾶飛\n⾷食\n⾸首\n⾹香\n⾺馬\n⾻骨\n⾼高\n⾽髟\n⾾鬥\n⾿鬯\n⿀鬲\n⿁鬼\n⿂魚\n⿃鳥\n⿄鹵\n⿅鹿\n⿆麥\n⿇麻\n⿈黃\n⿉黍\n⿊黑\n⿋黹\n⿌黽\n⿍鼎\n⿎鼓\n⿏鼠\n⿐鼻\n⿑齊\n⿒齒\n⿓龍\n⿔龜\n⿕龠\n  \n〶〒\n〸十\n〹卄\n〺卅\nがが\nぎぎ\nぐぐ\nげげ\nごご\nざざ\nじじ\nずず\nぜぜ\nぞぞ\nだだ\nぢぢ\nづづ\nでで\nどど\nばば\nぱぱ\nびび\nぴぴ\nぶぶ\nぷぷ\nべべ\nぺぺ\nぼぼ\nぽぽ\nゔゔ\n゛ ゙\n゜ ゚\nゞゞ\nゟより\nガガ\nギギ\nググ\nゲゲ\nゴゴ\nザザ\nジジ\nズズ\nゼゼ\nゾゾ\nダダ\nヂヂ\nヅヅ\nデデ\nドド\nババ\nパパ\nビビ\nピピ\nブブ\nププ\nベベ\nペペ\nボボ\nポポ\nヴヴ\nヷヷ\nヸヸ\nヹヹ\nヺヺ\nヾヾ\nヿコト\nㄱᄀ\nㄲᄁ\nㄳᆪ\nㄴᄂ\nㄵᆬ\nㄶᆭ\nㄷᄃ\nㄸᄄ\nㄹᄅ\nㄺᆰ\nㄻᆱ\nㄼᆲ\nㄽᆳ\nㄾᆴ\nㄿᆵ\nㅀᄚ\nㅁᄆ\nㅂᄇ\nㅃᄈ\nㅄᄡ\nㅅᄉ\nㅆᄊ\nㅇᄋ\nㅈᄌ\nㅉᄍ\nㅊᄎ\nㅋᄏ\nㅌᄐ\nㅍᄑ\nㅎᄒ\nㅏᅡ\nㅐᅢ\nㅑᅣ\nㅒᅤ\nㅓᅥ\nㅔᅦ\nㅕᅧ\nㅖᅨ\nㅗᅩ\nㅘᅪ\nㅙᅫ\nㅚᅬ\nㅛᅭ\nㅜᅮ\nㅝᅯ\nㅞᅰ\nㅟᅱ\nㅠᅲ\nㅡᅳ\nㅢᅴ\nㅣᅵ\nㅤᅠ\nㅥᄔ\nㅦᄕ\nㅧᇇ\nㅨᇈ\nㅩᇌ\nㅪᇎ\nㅫᇓ\nㅬᇗ\nㅭᇙ\nㅮᄜ\nㅯᇝ\nㅰᇟ\nㅱᄝ\nㅲᄞ\nㅳᄠ\nㅴᄢ\nㅵᄣ\nㅶᄧ\nㅷᄩ\nㅸᄫ\nㅹᄬ\nㅺᄭ\nㅻᄮ\nㅼᄯ\nㅽᄲ\nㅾᄶ\nㅿᅀ\nㆀᅇ\nㆁᅌ\nㆂᇱ\nㆃᇲ\nㆄᅗ\nㆅᅘ\nㆆᅙ\nㆇᆄ\nㆈᆅ\nㆉᆈ\nㆊᆑ\nㆋᆒ\nㆌᆔ\nㆍᆞ\nㆎᆡ\n㆒一\n㆓二\n㆔三\n㆕四\n㆖上\n㆗中\n㆘下\n㆙甲\n㆚乙\n㆛丙\n㆜丁\n㆝天\n㆞地\n㆟人\n㈀(ᄀ)\n㈁(ᄂ)\n㈂(ᄃ)\n㈃(ᄅ)\n㈄(ᄆ)\n㈅(ᄇ)\n㈆(ᄉ)\n㈇(ᄋ)\n㈈(ᄌ)\n㈉(ᄎ)\n㈊(ᄏ)\n㈋(ᄐ)\n㈌(ᄑ)\n㈍(ᄒ)\n㈎(가)\n㈏(나)\n㈐(다)\n㈑(라)\n㈒(마)\n㈓(바)\n㈔(사)\n㈕(아)\n㈖(자)\n㈗(차)\n㈘(카)\n㈙(타)\n㈚(파)\n㈛(하)\n㈜(주)\n㈝(오전)\n㈞(오후)\n㈠(一)\n㈡(二)\n㈢(三)\n㈣(四)\n㈤(五)\n㈥(六)\n㈦(七)\n㈧(八)\n㈨(九)\n㈩(十)\n㈪(月)\n㈫(火)\n㈬(水)\n㈭(木)\n㈮(金)\n㈯(土)\n㈰(日)\n㈱(株)\n㈲(有)\n㈳(社)\n㈴(名)\n㈵(特)\n㈶(財)\n㈷(祝)\n㈸(労)\n㈹(代)\n㈺(呼)\n㈻(学)\n㈼(監)\n㈽(企)\n㈾(資)\n㈿(協)\n㉀(祭)\n㉁(休)\n㉂(自)\n㉃(至)\n㉐PTE\n㉑21\n㉒22\n㉓23\n㉔24\n㉕25\n㉖26\n㉗27\n㉘28\n㉙29\n㉚30\n㉛31\n㉜32\n㉝33\n㉞34\n㉟35\n㉠ᄀ\n㉡ᄂ\n㉢ᄃ\n㉣ᄅ\n㉤ᄆ\n㉥ᄇ\n㉦ᄉ\n㉧ᄋ\n㉨ᄌ\n㉩ᄎ\n㉪ᄏ\n㉫ᄐ\n㉬ᄑ\n㉭ᄒ\n㉮가\n㉯나\n㉰다\n㉱라\n㉲마\n㉳바\n㉴사\n㉵아\n㉶자\n㉷차\n㉸카\n㉹타\n㉺파\n㉻하\n㉼참고\n㉽주의\n㉾우\n㊀一\n㊁二\n㊂三\n㊃四\n㊄五\n㊅六\n㊆七\n㊇八\n㊈九\n㊉十\n㊊月\n㊋火\n㊌水\n㊍木\n㊎金\n㊏土\n㊐日\n㊑株\n㊒有\n㊓社\n㊔名\n㊕特\n㊖財\n㊗祝\n㊘労\n㊙秘\n㊚男\n㊛女\n㊜適\n㊝優\n㊞印\n㊟注\n㊠項\n㊡休\n㊢写\n㊣正\n㊤上\n㊥中\n㊦下\n㊧左\n㊨右\n㊩医\n㊪宗\n㊫学\n㊬監\n㊭企\n㊮資\n㊯協\n㊰夜\n㊱36\n㊲37\n㊳38\n㊴39\n㊵40\n㊶41\n㊷42\n㊸43\n㊹44\n㊺45\n㊻46\n㊼47\n㊽48\n㊾49\n㊿50\n㋀1月\n㋁2月\n㋂3月\n㋃4月\n㋄5月\n㋅6月\n㋆7月\n㋇8月\n㋈9月\n㋉10月\n㋊11月\n㋋12月\n㋌Hg\n㋍erg\n㋎eV\n㋏LTD\n㋐ア\n㋑イ\n㋒ウ\n㋓エ\n㋔オ\n㋕カ\n㋖キ\n㋗ク\n㋘ケ\n㋙コ\n㋚サ\n㋛シ\n㋜ス\n㋝セ\n㋞ソ\n㋟タ\n㋠チ\n㋡ツ\n㋢テ\n㋣ト\n㋤ナ\n㋥ニ\n㋦ヌ\n㋧ネ\n㋨ノ\n㋩ハ\n㋪ヒ\n㋫フ\n㋬ヘ\n㋭ホ\n㋮マ\n㋯ミ\n㋰ム\n㋱メ\n㋲モ\n㋳ヤ\n㋴ユ\n㋵ヨ\n㋶ラ\n㋷リ\n㋸ル\n㋹レ\n㋺ロ\n㋻ワ\n㋼ヰ\n㋽ヱ\n㋾ヲ\n㌀アパート\n㌁アルファ\n㌂アンペア\n㌃アール\n㌄イニング\n㌅インチ\n㌆ウォン\n㌇エスクード\n㌈エーカー\n㌉オンス\n㌊オーム\n㌋カイリ\n㌌カラット\n㌍カロリー\n㌎ガロン\n㌏ガンマ\n㌐ギガ\n㌑ギニー\n㌒キュリー\n㌓ギルダー\n㌔キロ\n㌕キログラム\n㌖キロメートル\n㌗キロワット\n㌘グラム\n㌙グラムトン\n㌚クルゼイロ\n㌛クローネ\n㌜ケース\n㌝コルナ\n㌞コーポ\n㌟サイクル\n㌠サンチーム\n㌡シリング\n㌢センチ\n㌣セント\n㌤ダース\n㌥デシ\n㌦ドル\n㌧トン\n㌨ナノ\n㌩ノット\n㌪ハイツ\n㌫パーセント\n㌬パーツ\n㌭バーレル\n㌮ピアストル\n㌯ピクル\n㌰ピコ\n㌱ビル\n㌲ファラッド\n㌳フィート\n㌴ブッシェル\n㌵フラン\n㌶ヘクタール\n㌷ペソ\n㌸ペニヒ\n㌹ヘルツ\n㌺ペンス\n㌻ページ\n㌼ベータ\n㌽ポイント\n㌾ボルト\n㌿ホン\n㍀ポンド\n㍁ホール\n㍂ホーン\n㍃マイクロ\n㍄マイル\n㍅マッハ\n㍆マルク\n㍇マンション\n㍈ミクロン\n㍉ミリ\n㍊ミリバール\n㍋メガ\n㍌メガトン\n㍍メートル\n㍎ヤード\n㍏ヤール\n㍐ユアン\n㍑リットル\n㍒リラ\n㍓ルピー\n㍔ルーブル\n㍕レム\n㍖レントゲン\n㍗ワット\n㍘0点\n㍙1点\n㍚2点\n㍛3点\n㍜4点\n㍝5点\n㍞6点\n㍟7点\n㍠8点\n㍡9点\n㍢10点\n㍣11点\n㍤12点\n㍥13点\n㍦14点\n㍧15点\n㍨16点\n㍩17点\n㍪18点\n㍫19点\n㍬20点\n㍭21点\n㍮22点\n㍯23点\n㍰24点\n㍱hPa\n㍲da\n㍳AU\n㍴bar\n㍵oV\n㍶pc\n㍷dm\n㍸dm2\n㍹dm3\n㍺IU\n㍻平成\n㍼昭和\n㍽大正\n㍾明治\n㍿株式会社\n㎀pA\n㎁nA\n㎂μA\n㎃mA\n㎄kA\n㎅KB\n㎆MB\n㎇GB\n㎈cal\n㎉kcal\n㎊pF\n㎋nF\n㎌μF\n㎍μg\n㎎mg\n㎏kg\n㎐Hz\n㎑kHz\n㎒MHz\n㎓GHz\n㎔THz\n㎕μl\n㎖ml\n㎗dl\n㎘kl\n㎙fm\n㎚nm\n㎛μm\n㎜mm\n㎝cm\n㎞km\n㎟mm2\n㎠cm2\n㎡m2\n㎢km2\n㎣mm3\n㎤cm3\n㎥m3\n㎦km3\n㎧m∕s\n㎨m∕s2\n㎩Pa\n㎪kPa\n㎫MPa\n㎬GPa\n㎭rad\n㎮rad∕s\n㎯rad∕s2\n㎰ps\n㎱ns\n㎲μs\n㎳ms\n㎴pV\n㎵nV\n㎶μV\n㎷mV\n㎸kV\n㎹MV\n㎺pW\n㎻nW\n㎼μW\n㎽mW\n㎾kW\n㎿MW\n㏀kΩ\n㏁MΩ\n㏂a.m.\n㏃Bq\n㏄cc\n㏅cd\n㏆C∕kg\n㏇Co.\n㏈dB\n㏉Gy\n㏊ha\n㏋HP\n㏌in\n㏍KK\n㏎KM\n㏏kt\n㏐lm\n㏑ln\n㏒log\n㏓lx\n㏔mb\n㏕mil\n㏖mol\n㏗PH\n㏘p.m.\n㏙PPM\n㏚PR\n㏛sr\n㏜Sv\n㏝Wb\n㏞V∕m\n㏟A∕m\n㏠1日\n㏡2日\n㏢3日\n㏣4日\n㏤5日\n㏥6日\n㏦7日\n㏧8日\n㏨9日\n㏩10日\n㏪11日\n㏫12日\n㏬13日\n㏭14日\n㏮15日\n㏯16日\n㏰17日\n㏱18日\n㏲19日\n㏳20日\n㏴21日\n㏵22日\n㏶23日\n㏷24日\n㏸25日\n㏹26日\n㏺27日\n㏻28日\n㏼29日\n㏽30日\n㏾31日\n㏿gal\n豈豈\n更更\n車車\n賈賈\n滑滑\n串串\n句句\n龜龜\n龜龜\n契契\n金金\n喇喇\n奈奈\n懶懶\n癩癩\n羅羅\n蘿蘿\n螺螺\n裸裸\n邏邏\n樂樂\n洛洛\n烙烙\n珞珞\n落落\n酪酪\n駱駱\n亂亂\n卵卵\n欄欄\n爛爛\n蘭蘭\n鸞鸞\n嵐嵐\n濫濫\n藍藍\n襤襤\n拉拉\n臘臘\n蠟蠟\n廊廊\n朗朗\n浪浪\n狼狼\n郎郎\n來來\n冷冷\n勞勞\n擄擄\n櫓櫓\n爐爐\n盧盧\n老老\n蘆蘆\n虜虜\n路路\n露露\n魯魯\n鷺鷺\n碌碌\n祿祿\n綠綠\n菉菉\n錄錄\n鹿鹿\n論論\n壟壟\n弄弄\n籠籠\n聾聾\n牢牢\n磊磊\n賂賂\n雷雷\n壘壘\n屢屢\n樓樓\n淚淚\n漏漏\n累累\n縷縷\n陋陋\n勒勒\n肋肋\n凜凜\n凌凌\n稜稜\n綾綾\n菱菱\n陵陵\n讀讀\n拏拏\n樂樂\n諾諾\n丹丹\n寧寧\n怒怒\n率率\n異異\n北北\n磻磻\n便便\n復復\n不不\n泌泌\n數數\n索索\n參參\n塞塞\n省省\n葉葉\n說說\n殺殺\n辰辰\n沈沈\n拾拾\n若若\n掠掠\n略略\n亮亮\n兩兩\n凉凉\n梁梁\n糧糧\n良良\n諒諒\n量量\n勵勵\n呂呂\n女女\n廬廬\n旅旅\n濾濾\n礪礪\n閭閭\n驪驪\n麗麗\n黎黎\n力力\n曆曆\n歷歷\n轢轢\n年年\n憐憐\n戀戀\n撚撚\n漣漣\n煉煉\n璉璉\n秊秊\n練練\n聯聯\n輦輦\n蓮蓮\n連連\n鍊鍊\n列列\n劣劣\n咽咽\n烈烈\n裂裂\n說說\n廉廉\n念念\n捻捻\n殮殮\n簾簾\n獵獵\n令令\n囹囹\n寧寧\n嶺嶺\n怜怜\n玲玲\n瑩瑩\n羚羚\n聆聆\n鈴鈴\n零零\n靈靈\n領領\n例例\n禮禮\n醴醴\n隸隸\n惡惡\n了了\n僚僚\n寮寮\n尿尿\n料料\n樂樂\n燎燎\n療療\n蓼蓼\n遼遼\n龍龍\n暈暈\n阮阮\n劉劉\n杻杻\n柳柳\n流流\n溜溜\n琉琉\n留留\n硫硫\n紐紐\n類類\n六六\n戮戮\n陸陸\n倫倫\n崙崙\n淪淪\n輪輪\n律律\n慄慄\n栗栗\n率率\n隆隆\n利利\n吏吏\n履履\n易易\n李李\n梨梨\n泥泥\n理理\n痢痢\n罹罹\n裏裏\n裡裡\n里里\n離離\n匿匿\n溺溺\n吝吝\n燐燐\n璘璘\n藺藺\n隣隣\n鱗鱗\n麟麟\n林林\n淋淋\n臨臨\n立立\n笠笠\n粒粒\n狀狀\n炙炙\n識識\n什什\n茶茶\n刺刺\n切切\n度度\n拓拓\n糖糖\n宅宅\n洞洞\n暴暴\n輻輻\n行行\n降降\n見見\n廓廓\n兀兀\n嗀嗀\n塚塚\n晴晴\n凞凞\n猪猪\n益益\n礼礼\n神神\n祥祥\n福福\n靖靖\n精精\n羽羽\n蘒蘒\n諸諸\n逸逸\n都都\n飯飯\n飼飼\n館館\n鶴鶴\n侮侮\n僧僧\n免免\n勉勉\n勤勤\n卑卑\n喝喝\n嘆嘆\n器器\n塀塀\n墨墨\n層層\n屮屮\n悔悔\n慨慨\n憎憎\n懲懲\n敏敏\n既既\n暑暑\n梅梅\n海海\n渚渚\n漢漢\n煮煮\n爫爫\n琢琢\n碑碑\n社社\n祉祉\n祈祈\n祐祐\n祖祖\n祝祝\n禍禍\n禎禎\n穀穀\n突突\n節節\n練練\n縉縉\n繁繁\n署署\n者者\n臭臭\n艹艹\n艹艹\n著著\n褐褐\n視視\n謁謁\n謹謹\n賓賓\n贈贈\n辶辶\n逸逸\n難難\n響響\n頻頻\n並並\n况况\n全全\n侀侀\n充充\n冀冀\n勇勇\n勺勺\n喝喝\n啕啕\n喙喙\n嗢嗢\n塚塚\n墳墳\n奄奄\n奔奔\n婢婢\n嬨嬨\n廒廒\n廙廙\n彩彩\n徭徭\n惘惘\n慎慎\n愈愈\n憎憎\n慠慠\n懲懲\n戴戴\n揄揄\n搜搜\n摒摒\n敖敖\n晴晴\n朗朗\n望望\n杖杖\n歹歹\n殺殺\n流流\n滛滛\n滋滋\n漢漢\n瀞瀞\n煮煮\n瞧瞧\n爵爵\n犯犯\n猪猪\n瑱瑱\n甆甆\n画画\n瘝瘝\n瘟瘟\n益益\n盛盛\n直直\n睊睊\n着着\n磌磌\n窱窱\n節節\n类类\n絛絛\n練練\n缾缾\n者者\n荒荒\n華華\n蝹蝹\n襁襁\n覆覆\n視視\n調調\n諸諸\n請請\n謁謁\n諾諾\n諭諭\n謹謹\n變變\n贈贈\n輸輸\n遲遲\n醙醙\n鉶鉶\n陼陼\n難難\n靖靖\n韛韛\n響響\n頋頋\n頻頻\n鬒鬒\n龜龜\n𢡊𢡊\n𢡄𢡄\n𣏕𣏕\n㮝㮝\n䀘䀘\n䀹䀹\n𥉉𥉉\n𥳐𥳐\n𧻓𧻓\n齃齃\n龎龎\n!!\n"\"\n##\n$$\n%%\n&&\n''\n((\n))\n**\n++\n,,\n--\n..\n//\n00\n11\n22\n33\n44\n55\n66\n77\n88\n99\n::\n;;\n<<\n==\n>>\n??\n@@\nAA\nBB\nCC\nDD\nEE\nFF\nGG\nHH\nII\nJJ\nKK\nLL\nMM\nNN\nOO\nPP\nQQ\nRR\nSS\nTT\nUU\nVV\nWW\nXX\nYY\nZZ\n[[\n\\\n]]\n^^\n__\n``\naa\nbb\ncc\ndd\nee\nff\ngg\nhh\nii\njj\nkk\nll\nmm\nnn\noo\npp\nqq\nrr\nss\ntt\nuu\nvv\nww\nxx\nyy\nzz\n{{\n||\n}}\n~~\n⦅⦅\n⦆⦆\n。。\n「「\n」」\n、、\n・・\nヲヲ\nァァ\nィィ\nゥゥ\nェェ\nォォ\nャャ\nュュ\nョョ\nッッ\nーー\nアア\nイイ\nウウ\nエエ\nオオ\nカカ\nキキ\nクク\nケケ\nココ\nササ\nシシ\nスス\nセセ\nソソ\nタタ\nチチ\nツツ\nテテ\nトト\nナナ\nニニ\nヌヌ\nネネ\nノノ\nハハ\nヒヒ\nフフ\nヘヘ\nホホ\nママ\nミミ\nムム\nメメ\nモモ\nヤヤ\nユユ\nヨヨ\nララ\nリリ\nルル\nレレ\nロロ\nワワ\nンン\n゙゙\n゚゚\nᅠᅠ\nᄀᄀ\nᄁᄁ\nᆪᆪ\nᄂᄂ\nᆬᆬ\nᆭᆭ\nᄃᄃ\nᄄᄄ\nᄅᄅ\nᆰᆰ\nᆱᆱ\nᆲᆲ\nᆳᆳ\nᆴᆴ\nᆵᆵ\nᄚᄚ\nᄆᄆ\nᄇᄇ\nᄈᄈ\nᄡᄡ\nᄉᄉ\nᄊᄊ\nᄋᄋ\nᄌᄌ\nᄍᄍ\nᄎᄎ\nᄏᄏ\nᄐᄐ\nᄑᄑ\nᄒᄒ\nᅡᅡ\nᅢᅢ\nᅣᅣ\nᅤᅤ\nᅥᅥ\nᅦᅦ\nᅧᅧ\nᅨᅨ\nᅩᅩ\nᅪᅪ\nᅫᅫ\nᅬᅬ\nᅭᅭ\nᅮᅮ\nᅯᅯ\nᅰᅰ\nᅱᅱ\nᅲᅲ\nᅳᅳ\nᅴᅴ\nᅵᅵ\n¢¢\n££\n¬¬\n ̄ ̄\n¦¦\n¥¥\n₩₩\n││\n←←\n↑↑\n→→\n↓↓\n■■\n○○\n"; - } + static int[][] _SupportedChars = new int[][] + { + new[] { 0, 1000 }, + new[] { 12352, 12447 }, + new[] { 12448, 12543 }, + new[] { 19968, 40959 }, + new[] { 13312, 19967 }, + new[] { 131072, 173791 }, + new[] { 63744, 64255 }, + new[] { 194560, 195103 }, + new[] { 13056, 13311 }, + new[] { 12288, 12351 }, + new[] { 65280, 65535 }, + new[] { 8192, 8303 }, + new[] { 8352, 8399 }, + }; + const string _SubstitutionTable = + "  \n¨ ̈\nªa\n¯ ̄\n²2\n³3\n´ ́\nµμ\n¸ ̧\n¹1\nºo\n¼1⁄4\n½1⁄2\n¾3⁄4\nÀÀ\nÁÁ\nÂÂ\nÃÃ\nÄÄ\nÅÅ\nÇÇ\nÈÈ\nÉÉ\nÊÊ\nËË\nÌÌ\nÍÍ\nÎÎ\nÏÏ\nÑÑ\nÒÒ\nÓÓ\nÔÔ\nÕÕ\nÖÖ\nÙÙ\nÚÚ\nÛÛ\nÜÜ\nÝÝ\nàà\náá\nââ\nãã\nää\nåå\nçç\nèè\néé\nêê\nëë\nìì\níí\nîî\nïï\nññ\nòò\nóó\nôô\nõõ\nöö\nùù\núú\nûû\nüü\nýý\nÿÿ\nĀĀ\nāā\nĂĂ\năă\nĄĄ\nąą\nĆĆ\nćć\nĈĈ\nĉĉ\nĊĊ\nċċ\nČČ\nčč\nĎĎ\nďď\nĒĒ\nēē\nĔĔ\nĕĕ\nĖĖ\nėė\nĘĘ\nęę\nĚĚ\něě\nĜĜ\nĝĝ\nĞĞ\nğğ\nĠĠ\nġġ\nĢĢ\nģģ\nĤĤ\nĥĥ\nĨĨ\nĩĩ\nĪĪ\nīī\nĬĬ\nĭĭ\nĮĮ\nįį\nİİ\nIJIJ\nijij\nĴĴ\nĵĵ\nĶĶ\nķķ\nĹĹ\nĺĺ\nĻĻ\nļļ\nĽĽ\nľľ\nĿL·\nŀl·\nŃŃ\nńń\nŅŅ\nņņ\nŇŇ\nňň\nʼnʼn\nŌŌ\nōō\nŎŎ\nŏŏ\nŐŐ\nőő\nŔŔ\nŕŕ\nŖŖ\nŗŗ\nŘŘ\nřř\nŚŚ\nśś\nŜŜ\nŝŝ\nŞŞ\nşş\nŠŠ\nšš\nŢŢ\nţţ\nŤŤ\nťť\nŨŨ\nũũ\nŪŪ\nūū\nŬŬ\nŭŭ\nŮŮ\nůů\nŰŰ\nűű\nŲŲ\nųų\nŴŴ\nŵŵ\nŶŶ\nŷŷ\nŸŸ\nŹŹ\nźź\nŻŻ\nżż\nŽŽ\nžž\nſs\nƠƠ\nơơ\nƯƯ\nưư\nDŽDŽ\nDžDž\ndždž\nLJLJ\nLjLj\nljlj\nNJNJ\nNjNj\nnjnj\nǍǍ\nǎǎ\nǏǏ\nǐǐ\nǑǑ\nǒǒ\nǓǓ\nǔǔ\nǕǕ\nǖǖ\nǗǗ\nǘǘ\nǙǙ\nǚǚ\nǛǛ\nǜǜ\nǞǞ\nǟǟ\nǠǠ\nǡǡ\nǢǢ\nǣǣ\nǦǦ\nǧǧ\nǨǨ\nǩǩ\nǪǪ\nǫǫ\nǬǬ\nǭǭ\nǮǮ\nǯǯ\nǰǰ\nDZDZ\nDzDz\ndzdz\nǴǴ\nǵǵ\nǸǸ\nǹǹ\nǺǺ\nǻǻ\nǼǼ\nǽǽ\nǾǾ\nǿǿ\nȀȀ\nȁȁ\nȂȂ\nȃȃ\nȄȄ\nȅȅ\nȆȆ\nȇȇ\nȈȈ\nȉȉ\nȊȊ\nȋȋ\nȌȌ\nȍȍ\nȎȎ\nȏȏ\nȐȐ\nȑȑ\nȒȒ\nȓȓ\nȔȔ\nȕȕ\nȖȖ\nȗȗ\nȘȘ\nșș\nȚȚ\nțț\nȞȞ\nȟȟ\nȦȦ\nȧȧ\nȨȨ\nȩȩ\nȪȪ\nȫȫ\nȬȬ\nȭȭ\nȮȮ\nȯȯ\nȰȰ\nȱȱ\nȲȲ\nȳȳ\nʰh\nʱɦ\nʲj\nʳr\nʴɹ\nʵɻ\nʶʁ\nʷw\nʸy\n˘ ̆\n˙ ̇\n˚ ̊\n˛ ̨\n˜ ̃\n˝ ̋\nˠɣ\nˡl\nˢs\nˣx\nˤʕ\ǹ̀\ń́\n̓̓\n̈́̈́\nʹʹ\nͺ ͅ\n;;\n΄ ́\n΅ ̈́\nΆΆ\n··\nΈΈ\nΉΉ\nΊΊ\nΌΌ\nΎΎ\nΏΏ\nΐΐ\nΪΪ\nΫΫ\nάά\nέέ\nήή\nίί\nΰΰ\nϊϊ\nϋϋ\nόό\nύύ\nώώ\nϐβ\nϑθ\nϒΥ\nϓΎ\nϔΫ\nϕφ\nϖπ\nϰκ\nϱρ\nϲς\nϴΘ\nϵε\nϹΣ\nЀЀ\nЁЁ\nЃЃ\nЇЇ\nЌЌ\nЍЍ\nЎЎ\nЙЙ\nйй\nѐѐ\nёё\nѓѓ\nїї\nќќ\nѝѝ\nўў\nѶѶ\nѷѷ\nӁӁ\nӂӂ\nӐӐ\nӑӑ\nӒӒ\nӓӓ\nӖӖ\nӗӗ\nӚӚ\nӛӛ\nӜӜ\nӝӝ\nӞӞ\nӟӟ\nӢӢ\nӣӣ\nӤӤ\nӥӥ\nӦӦ\nӧӧ\nӪӪ\nӫӫ\nӬӬ\nӭӭ\nӮӮ\nӯӯ\nӰӰ\nӱӱ\nӲӲ\nӳӳ\nӴӴ\nӵӵ\nӸӸ\nӹӹ\nևեւ\nآآ\nأأ\nؤؤ\nإإ\nئئ\nٵاٴ\nٶوٴ\nٷۇٴ\nٸيٴ\nۀۀ\nۂۂ\nۓۓ\nऩऩ\nऱऱ\nऴऴ\nक़क़\nख़ख़\nग़ग़\nज़ज़\nड़ड़\nढ़ढ़\nफ़फ़\nय़य़\nোো\nৌৌ\nড়ড়\nঢ়ঢ়\nয়য়\nਲ਼ਲ਼\nਸ਼ਸ਼\nਖ਼ਖ਼\nਗ਼ਗ਼\nਜ਼ਜ਼\nਫ਼ਫ਼\nୈୈ\nୋୋ\nୌୌ\nଡ଼ଡ଼\nଢ଼ଢ଼\nஔஔ\nொொ\nோோ\nௌௌ\nైై\nೀೀ\nೇೇ\nೈೈ\nೊೊ\nೋೋ\nൊൊ\nോോ\nൌൌ\nේේ\nොො\nෝෝ\nෞෞ\nำํา\nຳໍາ\nໜຫນ\nໝຫມ\n༌་\nགྷགྷ\nཌྷཌྷ\nདྷདྷ\nབྷབྷ\nཛྷཛྷ\nཀྵཀྵ\nཱཱིི\nཱཱུུ\nྲྀྲྀ\nཷྲཱྀ\nླྀླྀ\nཹླཱྀ\nཱཱྀྀ\nྒྷྒྷ\nྜྷྜྷ\nྡྷྡྷ\nྦྷྦྷ\nྫྷྫྷ\nྐྵྐྵ\nဦဦ\nჼნ\nᬆᬆ\nᬈᬈ\nᬊᬊ\nᬌᬌ\nᬎᬎ\nᬒᬒ\nᬻᬻ\nᬽᬽ\nᭀᭀ\nᭁᭁ\nᭃᭃ\nᴬA\nᴭÆ\nᴮB\nᴰD\nᴱE\nᴲƎ\nᴳG\nᴴH\nᴵI\nᴶJ\nᴷK\nᴸL\nᴹM\nᴺN\nᴼO\nᴽȢ\nᴾP\nᴿR\nᵀT\nᵁU\nᵂW\nᵃa\nᵄɐ\nᵅɑ\nᵆᴂ\nᵇb\nᵈd\nᵉe\nᵊə\nᵋɛ\nᵌɜ\nᵍg\nᵏk\nᵐm\nᵑŋ\nᵒo\nᵓɔ\nᵔᴖ\nᵕᴗ\nᵖp\nᵗt\nᵘu\nᵙᴝ\nᵚɯ\nᵛv\nᵜᴥ\nᵝβ\nᵞγ\nᵟδ\nᵠφ\nᵡχ\nᵢi\nᵣr\nᵤu\nᵥv\nᵦβ\nᵧγ\nᵨρ\nᵩφ\nᵪχ\nᵸн\nᶛɒ\nᶜc\nᶝɕ\nᶞð\nᶟɜ\nᶠf\nᶡɟ\nᶢɡ\nᶣɥ\nᶤɨ\nᶥɩ\nᶦɪ\nᶧᵻ\nᶨʝ\nᶩɭ\nᶪᶅ\nᶫʟ\nᶬɱ\nᶭɰ\nᶮɲ\nᶯɳ\nᶰɴ\nᶱɵ\nᶲɸ\nᶳʂ\nᶴʃ\nᶵƫ\nᶶʉ\nᶷʊ\nᶸᴜ\nᶹʋ\nᶺʌ\nᶻz\nᶼʐ\nᶽʑ\nᶾʒ\nᶿθ\nḀḀ\nḁḁ\nḂḂ\nḃḃ\nḄḄ\nḅḅ\nḆḆ\nḇḇ\nḈḈ\nḉḉ\nḊḊ\nḋḋ\nḌḌ\nḍḍ\nḎḎ\nḏḏ\nḐḐ\nḑḑ\nḒḒ\nḓḓ\nḔḔ\nḕḕ\nḖḖ\nḗḗ\nḘḘ\nḙḙ\nḚḚ\nḛḛ\nḜḜ\nḝḝ\nḞḞ\nḟḟ\nḠḠ\nḡḡ\nḢḢ\nḣḣ\nḤḤ\nḥḥ\nḦḦ\nḧḧ\nḨḨ\nḩḩ\nḪḪ\nḫḫ\nḬḬ\nḭḭ\nḮḮ\nḯḯ\nḰḰ\nḱḱ\nḲḲ\nḳḳ\nḴḴ\nḵḵ\nḶḶ\nḷḷ\nḸḸ\nḹḹ\nḺḺ\nḻḻ\nḼḼ\nḽḽ\nḾḾ\nḿḿ\nṀṀ\nṁṁ\nṂṂ\nṃṃ\nṄṄ\nṅṅ\nṆṆ\nṇṇ\nṈṈ\nṉṉ\nṊṊ\nṋṋ\nṌṌ\nṍṍ\nṎṎ\nṏṏ\nṐṐ\nṑṑ\nṒṒ\nṓṓ\nṔṔ\nṕṕ\nṖṖ\nṗṗ\nṘṘ\nṙṙ\nṚṚ\nṛṛ\nṜṜ\nṝṝ\nṞṞ\nṟṟ\nṠṠ\nṡṡ\nṢṢ\nṣṣ\nṤṤ\nṥṥ\nṦṦ\nṧṧ\nṨṨ\nṩṩ\nṪṪ\nṫṫ\nṬṬ\nṭṭ\nṮṮ\nṯṯ\nṰṰ\nṱṱ\nṲṲ\nṳṳ\nṴṴ\nṵṵ\nṶṶ\nṷṷ\nṸṸ\nṹṹ\nṺṺ\nṻṻ\nṼṼ\nṽṽ\nṾṾ\nṿṿ\nẀẀ\nẁẁ\nẂẂ\nẃẃ\nẄẄ\nẅẅ\nẆẆ\nẇẇ\nẈẈ\nẉẉ\nẊẊ\nẋẋ\nẌẌ\nẍẍ\nẎẎ\nẏẏ\nẐẐ\nẑẑ\nẒẒ\nẓẓ\nẔẔ\nẕẕ\nẖẖ\nẗẗ\nẘẘ\nẙẙ\nẚaʾ\nẛṡ\nẠẠ\nạạ\nẢẢ\nảả\nẤẤ\nấấ\nẦẦ\nầầ\nẨẨ\nẩẩ\nẪẪ\nẫẫ\nẬẬ\nậậ\nẮẮ\nắắ\nẰẰ\nằằ\nẲẲ\nẳẳ\nẴẴ\nẵẵ\nẶẶ\nặặ\nẸẸ\nẹẹ\nẺẺ\nẻẻ\nẼẼ\nẽẽ\nẾẾ\nếế\nỀỀ\nềề\nỂỂ\nểể\nỄỄ\nễễ\nỆỆ\nệệ\nỈỈ\nỉỉ\nỊỊ\nịị\nỌỌ\nọọ\nỎỎ\nỏỏ\nỐỐ\nốố\nỒỒ\nồồ\nỔỔ\nổổ\nỖỖ\nỗỗ\nỘỘ\nộộ\nỚỚ\nớớ\nỜỜ\nờờ\nỞỞ\nởở\nỠỠ\nỡỡ\nỢỢ\nợợ\nỤỤ\nụụ\nỦỦ\nủủ\nỨỨ\nứứ\nỪỪ\nừừ\nỬỬ\nửử\nỮỮ\nữữ\nỰỰ\nựự\nỲỲ\nỳỳ\nỴỴ\nỵỵ\nỶỶ\nỷỷ\nỸỸ\nỹỹ\nἀἀ\nἁἁ\nἂἂ\nἃἃ\nἄἄ\nἅἅ\nἆἆ\nἇἇ\nἈἈ\nἉἉ\nἊἊ\nἋἋ\nἌἌ\nἍἍ\nἎἎ\nἏἏ\nἐἐ\nἑἑ\nἒἒ\nἓἓ\nἔἔ\nἕἕ\nἘἘ\nἙἙ\nἚἚ\nἛἛ\nἜἜ\nἝἝ\nἠἠ\nἡἡ\nἢἢ\nἣἣ\nἤἤ\nἥἥ\nἦἦ\nἧἧ\nἨἨ\nἩἩ\nἪἪ\nἫἫ\nἬἬ\nἭἭ\nἮἮ\nἯἯ\nἰἰ\nἱἱ\nἲἲ\nἳἳ\nἴἴ\nἵἵ\nἶἶ\nἷἷ\nἸἸ\nἹἹ\nἺἺ\nἻἻ\nἼἼ\nἽἽ\nἾἾ\nἿἿ\nὀὀ\nὁὁ\nὂὂ\nὃὃ\nὄὄ\nὅὅ\nὈὈ\nὉὉ\nὊὊ\nὋὋ\nὌὌ\nὍὍ\nὐὐ\nὑὑ\nὒὒ\nὓὓ\nὔὔ\nὕὕ\nὖὖ\nὗὗ\nὙὙ\nὛὛ\nὝὝ\nὟὟ\nὠὠ\nὡὡ\nὢὢ\nὣὣ\nὤὤ\nὥὥ\nὦὦ\nὧὧ\nὨὨ\nὩὩ\nὪὪ\nὫὫ\nὬὬ\nὭὭ\nὮὮ\nὯὯ\nὰὰ\nάά\nὲὲ\nέέ\nὴὴ\nήή\nὶὶ\nίί\nὸὸ\nόό\nὺὺ\nύύ\nὼὼ\nώώ\nᾀᾀ\nᾁᾁ\nᾂᾂ\nᾃᾃ\nᾄᾄ\nᾅᾅ\nᾆᾆ\nᾇᾇ\nᾈᾈ\nᾉᾉ\nᾊᾊ\nᾋᾋ\nᾌᾌ\nᾍᾍ\nᾎᾎ\nᾏᾏ\nᾐᾐ\nᾑᾑ\nᾒᾒ\nᾓᾓ\nᾔᾔ\nᾕᾕ\nᾖᾖ\nᾗᾗ\nᾘᾘ\nᾙᾙ\nᾚᾚ\nᾛᾛ\nᾜᾜ\nᾝᾝ\nᾞᾞ\nᾟᾟ\nᾠᾠ\nᾡᾡ\nᾢᾢ\nᾣᾣ\nᾤᾤ\nᾥᾥ\nᾦᾦ\nᾧᾧ\nᾨᾨ\nᾩᾩ\nᾪᾪ\nᾫᾫ\nᾬᾬ\nᾭᾭ\nᾮᾮ\nᾯᾯ\nᾰᾰ\nᾱᾱ\nᾲᾲ\nᾳᾳ\nᾴᾴ\nᾶᾶ\nᾷᾷ\nᾸᾸ\nᾹᾹ\nᾺᾺ\nΆΆ\nᾼᾼ\n᾽ ̓\nιι\n᾿ ̓\n῀ ͂\n῁ ̈͂\nῂῂ\nῃῃ\nῄῄ\nῆῆ\nῇῇ\nῈῈ\nΈΈ\nῊῊ\nΉΉ\nῌῌ\n῍ ̓̀\n῎ ̓́\n῏ ̓͂\nῐῐ\nῑῑ\nῒῒ\nΐΐ\nῖῖ\nῗῗ\nῘῘ\nῙῙ\nῚῚ\nΊΊ\n῝ ̔̀\n῞ ̔́\n῟ ̔͂\nῠῠ\nῡῡ\nῢῢ\nΰΰ\nῤῤ\nῥῥ\nῦῦ\nῧῧ\nῨῨ\nῩῩ\nῪῪ\nΎΎ\nῬῬ\n῭ ̈̀\n΅ ̈́\n``\nῲῲ\nῳῳ\nῴῴ\nῶῶ\nῷῷ\nῸῸ\nΌΌ\nῺῺ\nΏΏ\nῼῼ\n´ ́\n῾ ̔\n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n‑‐\n‗ ̳\n․.\n‥..\n…...\n  \n″′′\n‴′′′\n‶‵‵\n‷‵‵‵\n‼!!\n‾ ̅\n⁇??\n⁈?!\n⁉!?\n⁗′′′′\n  \n⁰0\nⁱi\n⁴4\n⁵5\n⁶6\n⁷7\n⁸8\n⁹9\n⁺+\n⁻−\n⁼=\n⁽(\n⁾)\nⁿn\n₀0\n₁1\n₂2\n₃3\n₄4\n₅5\n₆6\n₇7\n₈8\n₉9\n₊+\n₋−\n₌=\n₍(\n₎)\nₐa\nₑe\nₒo\nₓx\nₔə\n₨Rs\n℀a/c\n℁a/s\nℂC\n℃°C\n℅c/o\n℆c/u\nℇƐ\n℉°F\nℊg\nℋH\nℌH\nℍH\nℎh\nℏħ\nℐI\nℑI\nℒL\nℓl\nℕN\n№No\nℙP\nℚQ\nℛR\nℜR\nℝR\n℠SM\n℡TEL\n™TM\nℤZ\nΩΩ\nℨZ\nKK\nÅÅ\nℬB\nℭC\nℯe\nℰE\nℱF\nℳM\nℴo\nℵא\nℶב\nℷג\nℸד\nℹi\n℻FAX\nℼπ\nℽγ\nℾΓ\nℿΠ\n⅀∑\nⅅD\nⅆd\nⅇe\nⅈi\nⅉj\n⅓1⁄3\n⅔2⁄3\n⅕1⁄5\n⅖2⁄5\n⅗3⁄5\n⅘4⁄5\n⅙1⁄6\n⅚5⁄6\n⅛1⁄8\n⅜3⁄8\n⅝5⁄8\n⅞7⁄8\n⅟1⁄\nⅠI\nⅡII\nⅢIII\nⅣIV\nⅤV\nⅥVI\nⅦVII\nⅧVIII\nⅨIX\nⅩX\nⅪXI\nⅫXII\nⅬL\nⅭC\nⅮD\nⅯM\nⅰi\nⅱii\nⅲiii\nⅳiv\nⅴv\nⅵvi\nⅶvii\nⅷviii\nⅸix\nⅹx\nⅺxi\nⅻxii\nⅼl\nⅽc\nⅾd\nⅿm\n↚↚\n↛↛\n↮↮\n⇍⇍\n⇎⇎\n⇏⇏\n∄∄\n∉∉\n∌∌\n∤∤\n∦∦\n∬∫∫\n∭∫∫∫\n∯∮∮\n∰∮∮∮\n≁≁\n≄≄\n≇≇\n≉≉\n≠≠\n≢≢\n≭≭\n≮≮\n≯≯\n≰≰\n≱≱\n≴≴\n≵≵\n≸≸\n≹≹\n⊀⊀\n⊁⊁\n⊄⊄\n⊅⊅\n⊈⊈\n⊉⊉\n⊬⊬\n⊭⊭\n⊮⊮\n⊯⊯\n⋠⋠\n⋡⋡\n⋢⋢\n⋣⋣\n⋪⋪\n⋫⋫\n⋬⋬\n⋭⋭\n〈〈\n〉〉\n①1\n②2\n③3\n④4\n⑤5\n⑥6\n⑦7\n⑧8\n⑨9\n⑩10\n⑪11\n⑫12\n⑬13\n⑭14\n⑮15\n⑯16\n⑰17\n⑱18\n⑲19\n⑳20\n⑴(1)\n⑵(2)\n⑶(3)\n⑷(4)\n⑸(5)\n⑹(6)\n⑺(7)\n⑻(8)\n⑼(9)\n⑽(10)\n⑾(11)\n⑿(12)\n⒀(13)\n⒁(14)\n⒂(15)\n⒃(16)\n⒄(17)\n⒅(18)\n⒆(19)\n⒇(20)\n⒈1.\n⒉2.\n⒊3.\n⒋4.\n⒌5.\n⒍6.\n⒎7.\n⒏8.\n⒐9.\n⒑10.\n⒒11.\n⒓12.\n⒔13.\n⒕14.\n⒖15.\n⒗16.\n⒘17.\n⒙18.\n⒚19.\n⒛20.\n⒜(a)\n⒝(b)\n⒞(c)\n⒟(d)\n⒠(e)\n⒡(f)\n⒢(g)\n⒣(h)\n⒤(i)\n⒥(j)\n⒦(k)\n⒧(l)\n⒨(m)\n⒩(n)\n⒪(o)\n⒫(p)\n⒬(q)\n⒭(r)\n⒮(s)\n⒯(t)\n⒰(u)\n⒱(v)\n⒲(w)\n⒳(x)\n⒴(y)\n⒵(z)\nⒶA\nⒷB\nⒸC\nⒹD\nⒺE\nⒻF\nⒼG\nⒽH\nⒾI\nⒿJ\nⓀK\nⓁL\nⓂM\nⓃN\nⓄO\nⓅP\nⓆQ\nⓇR\nⓈS\nⓉT\nⓊU\nⓋV\nⓌW\nⓍX\nⓎY\nⓏZ\nⓐa\nⓑb\nⓒc\nⓓd\nⓔe\nⓕf\nⓖg\nⓗh\nⓘi\nⓙj\nⓚk\nⓛl\nⓜm\nⓝn\nⓞo\nⓟp\nⓠq\nⓡr\nⓢs\nⓣt\nⓤu\nⓥv\nⓦw\nⓧx\nⓨy\nⓩz\n⓪0\n⨌∫∫∫∫\n⩴::=\n⩵==\n⩶===\n⫝̸⫝̸\nⱼj\nⱽV\nⵯⵡ\n⺟母\n⻳龟\n⼀一\n⼁丨\n⼂丶\n⼃丿\n⼄乙\n⼅亅\n⼆二\n⼇亠\n⼈人\n⼉儿\n⼊入\n⼋八\n⼌冂\n⼍冖\n⼎冫\n⼏几\n⼐凵\n⼑刀\n⼒力\n⼓勹\n⼔匕\n⼕匚\n⼖匸\n⼗十\n⼘卜\n⼙卩\n⼚厂\n⼛厶\n⼜又\n⼝口\n⼞囗\n⼟土\n⼠士\n⼡夂\n⼢夊\n⼣夕\n⼤大\n⼥女\n⼦子\n⼧宀\n⼨寸\n⼩小\n⼪尢\n⼫尸\n⼬屮\n⼭山\n⼮巛\n⼯工\n⼰己\n⼱巾\n⼲干\n⼳幺\n⼴广\n⼵廴\n⼶廾\n⼷弋\n⼸弓\n⼹彐\n⼺彡\n⼻彳\n⼼心\n⼽戈\n⼾戶\n⼿手\n⽀支\n⽁攴\n⽂文\n⽃斗\n⽄斤\n⽅方\n⽆无\n⽇日\n⽈曰\n⽉月\n⽊木\n⽋欠\n⽌止\n⽍歹\n⽎殳\n⽏毋\n⽐比\n⽑毛\n⽒氏\n⽓气\n⽔水\n⽕火\n⽖爪\n⽗父\n⽘爻\n⽙爿\n⽚片\n⽛牙\n⽜牛\n⽝犬\n⽞玄\n⽟玉\n⽠瓜\n⽡瓦\n⽢甘\n⽣生\n⽤用\n⽥田\n⽦疋\n⽧疒\n⽨癶\n⽩白\n⽪皮\n⽫皿\n⽬目\n⽭矛\n⽮矢\n⽯石\n⽰示\n⽱禸\n⽲禾\n⽳穴\n⽴立\n⽵竹\n⽶米\n⽷糸\n⽸缶\n⽹网\n⽺羊\n⽻羽\n⽼老\n⽽而\n⽾耒\n⽿耳\n⾀聿\n⾁肉\n⾂臣\n⾃自\n⾄至\n⾅臼\n⾆舌\n⾇舛\n⾈舟\n⾉艮\n⾊色\n⾋艸\n⾌虍\n⾍虫\n⾎血\n⾏行\n⾐衣\n⾑襾\n⾒見\n⾓角\n⾔言\n⾕谷\n⾖豆\n⾗豕\n⾘豸\n⾙貝\n⾚赤\n⾛走\n⾜足\n⾝身\n⾞車\n⾟辛\n⾠辰\n⾡辵\n⾢邑\n⾣酉\n⾤釆\n⾥里\n⾦金\n⾧長\n⾨門\n⾩阜\n⾪隶\n⾫隹\n⾬雨\n⾭靑\n⾮非\n⾯面\n⾰革\n⾱韋\n⾲韭\n⾳音\n⾴頁\n⾵風\n⾶飛\n⾷食\n⾸首\n⾹香\n⾺馬\n⾻骨\n⾼高\n⾽髟\n⾾鬥\n⾿鬯\n⿀鬲\n⿁鬼\n⿂魚\n⿃鳥\n⿄鹵\n⿅鹿\n⿆麥\n⿇麻\n⿈黃\n⿉黍\n⿊黑\n⿋黹\n⿌黽\n⿍鼎\n⿎鼓\n⿏鼠\n⿐鼻\n⿑齊\n⿒齒\n⿓龍\n⿔龜\n⿕龠\n  \n〶〒\n〸十\n〹卄\n〺卅\nがが\nぎぎ\nぐぐ\nげげ\nごご\nざざ\nじじ\nずず\nぜぜ\nぞぞ\nだだ\nぢぢ\nづづ\nでで\nどど\nばば\nぱぱ\nびび\nぴぴ\nぶぶ\nぷぷ\nべべ\nぺぺ\nぼぼ\nぽぽ\nゔゔ\n゛ ゙\n゜ ゚\nゞゞ\nゟより\nガガ\nギギ\nググ\nゲゲ\nゴゴ\nザザ\nジジ\nズズ\nゼゼ\nゾゾ\nダダ\nヂヂ\nヅヅ\nデデ\nドド\nババ\nパパ\nビビ\nピピ\nブブ\nププ\nベベ\nペペ\nボボ\nポポ\nヴヴ\nヷヷ\nヸヸ\nヹヹ\nヺヺ\nヾヾ\nヿコト\nㄱᄀ\nㄲᄁ\nㄳᆪ\nㄴᄂ\nㄵᆬ\nㄶᆭ\nㄷᄃ\nㄸᄄ\nㄹᄅ\nㄺᆰ\nㄻᆱ\nㄼᆲ\nㄽᆳ\nㄾᆴ\nㄿᆵ\nㅀᄚ\nㅁᄆ\nㅂᄇ\nㅃᄈ\nㅄᄡ\nㅅᄉ\nㅆᄊ\nㅇᄋ\nㅈᄌ\nㅉᄍ\nㅊᄎ\nㅋᄏ\nㅌᄐ\nㅍᄑ\nㅎᄒ\nㅏᅡ\nㅐᅢ\nㅑᅣ\nㅒᅤ\nㅓᅥ\nㅔᅦ\nㅕᅧ\nㅖᅨ\nㅗᅩ\nㅘᅪ\nㅙᅫ\nㅚᅬ\nㅛᅭ\nㅜᅮ\nㅝᅯ\nㅞᅰ\nㅟᅱ\nㅠᅲ\nㅡᅳ\nㅢᅴ\nㅣᅵ\nㅤᅠ\nㅥᄔ\nㅦᄕ\nㅧᇇ\nㅨᇈ\nㅩᇌ\nㅪᇎ\nㅫᇓ\nㅬᇗ\nㅭᇙ\nㅮᄜ\nㅯᇝ\nㅰᇟ\nㅱᄝ\nㅲᄞ\nㅳᄠ\nㅴᄢ\nㅵᄣ\nㅶᄧ\nㅷᄩ\nㅸᄫ\nㅹᄬ\nㅺᄭ\nㅻᄮ\nㅼᄯ\nㅽᄲ\nㅾᄶ\nㅿᅀ\nㆀᅇ\nㆁᅌ\nㆂᇱ\nㆃᇲ\nㆄᅗ\nㆅᅘ\nㆆᅙ\nㆇᆄ\nㆈᆅ\nㆉᆈ\nㆊᆑ\nㆋᆒ\nㆌᆔ\nㆍᆞ\nㆎᆡ\n㆒一\n㆓二\n㆔三\n㆕四\n㆖上\n㆗中\n㆘下\n㆙甲\n㆚乙\n㆛丙\n㆜丁\n㆝天\n㆞地\n㆟人\n㈀(ᄀ)\n㈁(ᄂ)\n㈂(ᄃ)\n㈃(ᄅ)\n㈄(ᄆ)\n㈅(ᄇ)\n㈆(ᄉ)\n㈇(ᄋ)\n㈈(ᄌ)\n㈉(ᄎ)\n㈊(ᄏ)\n㈋(ᄐ)\n㈌(ᄑ)\n㈍(ᄒ)\n㈎(가)\n㈏(나)\n㈐(다)\n㈑(라)\n㈒(마)\n㈓(바)\n㈔(사)\n㈕(아)\n㈖(자)\n㈗(차)\n㈘(카)\n㈙(타)\n㈚(파)\n㈛(하)\n㈜(주)\n㈝(오전)\n㈞(오후)\n㈠(一)\n㈡(二)\n㈢(三)\n㈣(四)\n㈤(五)\n㈥(六)\n㈦(七)\n㈧(八)\n㈨(九)\n㈩(十)\n㈪(月)\n㈫(火)\n㈬(水)\n㈭(木)\n㈮(金)\n㈯(土)\n㈰(日)\n㈱(株)\n㈲(有)\n㈳(社)\n㈴(名)\n㈵(特)\n㈶(財)\n㈷(祝)\n㈸(労)\n㈹(代)\n㈺(呼)\n㈻(学)\n㈼(監)\n㈽(企)\n㈾(資)\n㈿(協)\n㉀(祭)\n㉁(休)\n㉂(自)\n㉃(至)\n㉐PTE\n㉑21\n㉒22\n㉓23\n㉔24\n㉕25\n㉖26\n㉗27\n㉘28\n㉙29\n㉚30\n㉛31\n㉜32\n㉝33\n㉞34\n㉟35\n㉠ᄀ\n㉡ᄂ\n㉢ᄃ\n㉣ᄅ\n㉤ᄆ\n㉥ᄇ\n㉦ᄉ\n㉧ᄋ\n㉨ᄌ\n㉩ᄎ\n㉪ᄏ\n㉫ᄐ\n㉬ᄑ\n㉭ᄒ\n㉮가\n㉯나\n㉰다\n㉱라\n㉲마\n㉳바\n㉴사\n㉵아\n㉶자\n㉷차\n㉸카\n㉹타\n㉺파\n㉻하\n㉼참고\n㉽주의\n㉾우\n㊀一\n㊁二\n㊂三\n㊃四\n㊄五\n㊅六\n㊆七\n㊇八\n㊈九\n㊉十\n㊊月\n㊋火\n㊌水\n㊍木\n㊎金\n㊏土\n㊐日\n㊑株\n㊒有\n㊓社\n㊔名\n㊕特\n㊖財\n㊗祝\n㊘労\n㊙秘\n㊚男\n㊛女\n㊜適\n㊝優\n㊞印\n㊟注\n㊠項\n㊡休\n㊢写\n㊣正\n㊤上\n㊥中\n㊦下\n㊧左\n㊨右\n㊩医\n㊪宗\n㊫学\n㊬監\n㊭企\n㊮資\n㊯協\n㊰夜\n㊱36\n㊲37\n㊳38\n㊴39\n㊵40\n㊶41\n㊷42\n㊸43\n㊹44\n㊺45\n㊻46\n㊼47\n㊽48\n㊾49\n㊿50\n㋀1月\n㋁2月\n㋂3月\n㋃4月\n㋄5月\n㋅6月\n㋆7月\n㋇8月\n㋈9月\n㋉10月\n㋊11月\n㋋12月\n㋌Hg\n㋍erg\n㋎eV\n㋏LTD\n㋐ア\n㋑イ\n㋒ウ\n㋓エ\n㋔オ\n㋕カ\n㋖キ\n㋗ク\n㋘ケ\n㋙コ\n㋚サ\n㋛シ\n㋜ス\n㋝セ\n㋞ソ\n㋟タ\n㋠チ\n㋡ツ\n㋢テ\n㋣ト\n㋤ナ\n㋥ニ\n㋦ヌ\n㋧ネ\n㋨ノ\n㋩ハ\n㋪ヒ\n㋫フ\n㋬ヘ\n㋭ホ\n㋮マ\n㋯ミ\n㋰ム\n㋱メ\n㋲モ\n㋳ヤ\n㋴ユ\n㋵ヨ\n㋶ラ\n㋷リ\n㋸ル\n㋹レ\n㋺ロ\n㋻ワ\n㋼ヰ\n㋽ヱ\n㋾ヲ\n㌀アパート\n㌁アルファ\n㌂アンペア\n㌃アール\n㌄イニング\n㌅インチ\n㌆ウォン\n㌇エスクード\n㌈エーカー\n㌉オンス\n㌊オーム\n㌋カイリ\n㌌カラット\n㌍カロリー\n㌎ガロン\n㌏ガンマ\n㌐ギガ\n㌑ギニー\n㌒キュリー\n㌓ギルダー\n㌔キロ\n㌕キログラム\n㌖キロメートル\n㌗キロワット\n㌘グラム\n㌙グラムトン\n㌚クルゼイロ\n㌛クローネ\n㌜ケース\n㌝コルナ\n㌞コーポ\n㌟サイクル\n㌠サンチーム\n㌡シリング\n㌢センチ\n㌣セント\n㌤ダース\n㌥デシ\n㌦ドル\n㌧トン\n㌨ナノ\n㌩ノット\n㌪ハイツ\n㌫パーセント\n㌬パーツ\n㌭バーレル\n㌮ピアストル\n㌯ピクル\n㌰ピコ\n㌱ビル\n㌲ファラッド\n㌳フィート\n㌴ブッシェル\n㌵フラン\n㌶ヘクタール\n㌷ペソ\n㌸ペニヒ\n㌹ヘルツ\n㌺ペンス\n㌻ページ\n㌼ベータ\n㌽ポイント\n㌾ボルト\n㌿ホン\n㍀ポンド\n㍁ホール\n㍂ホーン\n㍃マイクロ\n㍄マイル\n㍅マッハ\n㍆マルク\n㍇マンション\n㍈ミクロン\n㍉ミリ\n㍊ミリバール\n㍋メガ\n㍌メガトン\n㍍メートル\n㍎ヤード\n㍏ヤール\n㍐ユアン\n㍑リットル\n㍒リラ\n㍓ルピー\n㍔ルーブル\n㍕レム\n㍖レントゲン\n㍗ワット\n㍘0点\n㍙1点\n㍚2点\n㍛3点\n㍜4点\n㍝5点\n㍞6点\n㍟7点\n㍠8点\n㍡9点\n㍢10点\n㍣11点\n㍤12点\n㍥13点\n㍦14点\n㍧15点\n㍨16点\n㍩17点\n㍪18点\n㍫19点\n㍬20点\n㍭21点\n㍮22点\n㍯23点\n㍰24点\n㍱hPa\n㍲da\n㍳AU\n㍴bar\n㍵oV\n㍶pc\n㍷dm\n㍸dm2\n㍹dm3\n㍺IU\n㍻平成\n㍼昭和\n㍽大正\n㍾明治\n㍿株式会社\n㎀pA\n㎁nA\n㎂μA\n㎃mA\n㎄kA\n㎅KB\n㎆MB\n㎇GB\n㎈cal\n㎉kcal\n㎊pF\n㎋nF\n㎌μF\n㎍μg\n㎎mg\n㎏kg\n㎐Hz\n㎑kHz\n㎒MHz\n㎓GHz\n㎔THz\n㎕μl\n㎖ml\n㎗dl\n㎘kl\n㎙fm\n㎚nm\n㎛μm\n㎜mm\n㎝cm\n㎞km\n㎟mm2\n㎠cm2\n㎡m2\n㎢km2\n㎣mm3\n㎤cm3\n㎥m3\n㎦km3\n㎧m∕s\n㎨m∕s2\n㎩Pa\n㎪kPa\n㎫MPa\n㎬GPa\n㎭rad\n㎮rad∕s\n㎯rad∕s2\n㎰ps\n㎱ns\n㎲μs\n㎳ms\n㎴pV\n㎵nV\n㎶μV\n㎷mV\n㎸kV\n㎹MV\n㎺pW\n㎻nW\n㎼μW\n㎽mW\n㎾kW\n㎿MW\n㏀kΩ\n㏁MΩ\n㏂a.m.\n㏃Bq\n㏄cc\n㏅cd\n㏆C∕kg\n㏇Co.\n㏈dB\n㏉Gy\n㏊ha\n㏋HP\n㏌in\n㏍KK\n㏎KM\n㏏kt\n㏐lm\n㏑ln\n㏒log\n㏓lx\n㏔mb\n㏕mil\n㏖mol\n㏗PH\n㏘p.m.\n㏙PPM\n㏚PR\n㏛sr\n㏜Sv\n㏝Wb\n㏞V∕m\n㏟A∕m\n㏠1日\n㏡2日\n㏢3日\n㏣4日\n㏤5日\n㏥6日\n㏦7日\n㏧8日\n㏨9日\n㏩10日\n㏪11日\n㏫12日\n㏬13日\n㏭14日\n㏮15日\n㏯16日\n㏰17日\n㏱18日\n㏲19日\n㏳20日\n㏴21日\n㏵22日\n㏶23日\n㏷24日\n㏸25日\n㏹26日\n㏺27日\n㏻28日\n㏼29日\n㏽30日\n㏾31日\n㏿gal\n豈豈\n更更\n車車\n賈賈\n滑滑\n串串\n句句\n龜龜\n龜龜\n契契\n金金\n喇喇\n奈奈\n懶懶\n癩癩\n羅羅\n蘿蘿\n螺螺\n裸裸\n邏邏\n樂樂\n洛洛\n烙烙\n珞珞\n落落\n酪酪\n駱駱\n亂亂\n卵卵\n欄欄\n爛爛\n蘭蘭\n鸞鸞\n嵐嵐\n濫濫\n藍藍\n襤襤\n拉拉\n臘臘\n蠟蠟\n廊廊\n朗朗\n浪浪\n狼狼\n郎郎\n來來\n冷冷\n勞勞\n擄擄\n櫓櫓\n爐爐\n盧盧\n老老\n蘆蘆\n虜虜\n路路\n露露\n魯魯\n鷺鷺\n碌碌\n祿祿\n綠綠\n菉菉\n錄錄\n鹿鹿\n論論\n壟壟\n弄弄\n籠籠\n聾聾\n牢牢\n磊磊\n賂賂\n雷雷\n壘壘\n屢屢\n樓樓\n淚淚\n漏漏\n累累\n縷縷\n陋陋\n勒勒\n肋肋\n凜凜\n凌凌\n稜稜\n綾綾\n菱菱\n陵陵\n讀讀\n拏拏\n樂樂\n諾諾\n丹丹\n寧寧\n怒怒\n率率\n異異\n北北\n磻磻\n便便\n復復\n不不\n泌泌\n數數\n索索\n參參\n塞塞\n省省\n葉葉\n說說\n殺殺\n辰辰\n沈沈\n拾拾\n若若\n掠掠\n略略\n亮亮\n兩兩\n凉凉\n梁梁\n糧糧\n良良\n諒諒\n量量\n勵勵\n呂呂\n女女\n廬廬\n旅旅\n濾濾\n礪礪\n閭閭\n驪驪\n麗麗\n黎黎\n力力\n曆曆\n歷歷\n轢轢\n年年\n憐憐\n戀戀\n撚撚\n漣漣\n煉煉\n璉璉\n秊秊\n練練\n聯聯\n輦輦\n蓮蓮\n連連\n鍊鍊\n列列\n劣劣\n咽咽\n烈烈\n裂裂\n說說\n廉廉\n念念\n捻捻\n殮殮\n簾簾\n獵獵\n令令\n囹囹\n寧寧\n嶺嶺\n怜怜\n玲玲\n瑩瑩\n羚羚\n聆聆\n鈴鈴\n零零\n靈靈\n領領\n例例\n禮禮\n醴醴\n隸隸\n惡惡\n了了\n僚僚\n寮寮\n尿尿\n料料\n樂樂\n燎燎\n療療\n蓼蓼\n遼遼\n龍龍\n暈暈\n阮阮\n劉劉\n杻杻\n柳柳\n流流\n溜溜\n琉琉\n留留\n硫硫\n紐紐\n類類\n六六\n戮戮\n陸陸\n倫倫\n崙崙\n淪淪\n輪輪\n律律\n慄慄\n栗栗\n率率\n隆隆\n利利\n吏吏\n履履\n易易\n李李\n梨梨\n泥泥\n理理\n痢痢\n罹罹\n裏裏\n裡裡\n里里\n離離\n匿匿\n溺溺\n吝吝\n燐燐\n璘璘\n藺藺\n隣隣\n鱗鱗\n麟麟\n林林\n淋淋\n臨臨\n立立\n笠笠\n粒粒\n狀狀\n炙炙\n識識\n什什\n茶茶\n刺刺\n切切\n度度\n拓拓\n糖糖\n宅宅\n洞洞\n暴暴\n輻輻\n行行\n降降\n見見\n廓廓\n兀兀\n嗀嗀\n塚塚\n晴晴\n凞凞\n猪猪\n益益\n礼礼\n神神\n祥祥\n福福\n靖靖\n精精\n羽羽\n蘒蘒\n諸諸\n逸逸\n都都\n飯飯\n飼飼\n館館\n鶴鶴\n侮侮\n僧僧\n免免\n勉勉\n勤勤\n卑卑\n喝喝\n嘆嘆\n器器\n塀塀\n墨墨\n層層\n屮屮\n悔悔\n慨慨\n憎憎\n懲懲\n敏敏\n既既\n暑暑\n梅梅\n海海\n渚渚\n漢漢\n煮煮\n爫爫\n琢琢\n碑碑\n社社\n祉祉\n祈祈\n祐祐\n祖祖\n祝祝\n禍禍\n禎禎\n穀穀\n突突\n節節\n練練\n縉縉\n繁繁\n署署\n者者\n臭臭\n艹艹\n艹艹\n著著\n褐褐\n視視\n謁謁\n謹謹\n賓賓\n贈贈\n辶辶\n逸逸\n難難\n響響\n頻頻\n並並\n况况\n全全\n侀侀\n充充\n冀冀\n勇勇\n勺勺\n喝喝\n啕啕\n喙喙\n嗢嗢\n塚塚\n墳墳\n奄奄\n奔奔\n婢婢\n嬨嬨\n廒廒\n廙廙\n彩彩\n徭徭\n惘惘\n慎慎\n愈愈\n憎憎\n慠慠\n懲懲\n戴戴\n揄揄\n搜搜\n摒摒\n敖敖\n晴晴\n朗朗\n望望\n杖杖\n歹歹\n殺殺\n流流\n滛滛\n滋滋\n漢漢\n瀞瀞\n煮煮\n瞧瞧\n爵爵\n犯犯\n猪猪\n瑱瑱\n甆甆\n画画\n瘝瘝\n瘟瘟\n益益\n盛盛\n直直\n睊睊\n着着\n磌磌\n窱窱\n節節\n类类\n絛絛\n練練\n缾缾\n者者\n荒荒\n華華\n蝹蝹\n襁襁\n覆覆\n視視\n調調\n諸諸\n請請\n謁謁\n諾諾\n諭諭\n謹謹\n變變\n贈贈\n輸輸\n遲遲\n醙醙\n鉶鉶\n陼陼\n難難\n靖靖\n韛韛\n響響\n頋頋\n頻頻\n鬒鬒\n龜龜\n𢡊𢡊\n𢡄𢡄\n𣏕𣏕\n㮝㮝\n䀘䀘\n䀹䀹\n𥉉𥉉\n𥳐𥳐\n𧻓𧻓\n齃齃\n龎龎\n!!\n"\"\n##\n$$\n%%\n&&\n''\n((\n))\n**\n++\n,,\n--\n..\n//\n00\n11\n22\n33\n44\n55\n66\n77\n88\n99\n::\n;;\n<<\n==\n>>\n??\n@@\nAA\nBB\nCC\nDD\nEE\nFF\nGG\nHH\nII\nJJ\nKK\nLL\nMM\nNN\nOO\nPP\nQQ\nRR\nSS\nTT\nUU\nVV\nWW\nXX\nYY\nZZ\n[[\n\\\n]]\n^^\n__\n``\naa\nbb\ncc\ndd\nee\nff\ngg\nhh\nii\njj\nkk\nll\nmm\nnn\noo\npp\nqq\nrr\nss\ntt\nuu\nvv\nww\nxx\nyy\nzz\n{{\n||\n}}\n~~\n⦅⦅\n⦆⦆\n。。\n「「\n」」\n、、\n・・\nヲヲ\nァァ\nィィ\nゥゥ\nェェ\nォォ\nャャ\nュュ\nョョ\nッッ\nーー\nアア\nイイ\nウウ\nエエ\nオオ\nカカ\nキキ\nクク\nケケ\nココ\nササ\nシシ\nスス\nセセ\nソソ\nタタ\nチチ\nツツ\nテテ\nトト\nナナ\nニニ\nヌヌ\nネネ\nノノ\nハハ\nヒヒ\nフフ\nヘヘ\nホホ\nママ\nミミ\nムム\nメメ\nモモ\nヤヤ\nユユ\nヨヨ\nララ\nリリ\nルル\nレレ\nロロ\nワワ\nンン\n゙゙\n゚゚\nᅠᅠ\nᄀᄀ\nᄁᄁ\nᆪᆪ\nᄂᄂ\nᆬᆬ\nᆭᆭ\nᄃᄃ\nᄄᄄ\nᄅᄅ\nᆰᆰ\nᆱᆱ\nᆲᆲ\nᆳᆳ\nᆴᆴ\nᆵᆵ\nᄚᄚ\nᄆᄆ\nᄇᄇ\nᄈᄈ\nᄡᄡ\nᄉᄉ\nᄊᄊ\nᄋᄋ\nᄌᄌ\nᄍᄍ\nᄎᄎ\nᄏᄏ\nᄐᄐ\nᄑᄑ\nᄒᄒ\nᅡᅡ\nᅢᅢ\nᅣᅣ\nᅤᅤ\nᅥᅥ\nᅦᅦ\nᅧᅧ\nᅨᅨ\nᅩᅩ\nᅪᅪ\nᅫᅫ\nᅬᅬ\nᅭᅭ\nᅮᅮ\nᅯᅯ\nᅰᅰ\nᅱᅱ\nᅲᅲ\nᅳᅳ\nᅴᅴ\nᅵᅵ\n¢¢\n££\n¬¬\n ̄ ̄\n¦¦\n¥¥\n₩₩\n││\n←←\n↑↑\n→→\n↓↓\n■■\n○○\n"; + } } diff --git a/DotNut/NBitcoin/BIP39/Language.cs b/DotNut/NBitcoin/BIP39/Language.cs index 6a9a762..a5b90ed 100644 --- a/DotNut/NBitcoin/BIP39/Language.cs +++ b/DotNut/NBitcoin/BIP39/Language.cs @@ -1,15 +1,15 @@ namespace DotNut.NBitcoin.BIP39 { - public enum Language - { - English, - Japanese, - Spanish, - ChineseSimplified, - ChineseTraditional, - French, - PortugueseBrazil, - Czech, - Unknown - }; + public enum Language + { + English, + Japanese, + Spanish, + ChineseSimplified, + ChineseTraditional, + French, + PortugueseBrazil, + Czech, + Unknown, + }; } diff --git a/DotNut/NBitcoin/BIP39/Mnemonic.cs b/DotNut/NBitcoin/BIP39/Mnemonic.cs index aa8493f..f1944e4 100644 --- a/DotNut/NBitcoin/BIP39/Mnemonic.cs +++ b/DotNut/NBitcoin/BIP39/Mnemonic.cs @@ -4,208 +4,207 @@ namespace DotNut.NBitcoin.BIP39 { - /// - /// A .NET implementation of the Bitcoin Improvement Proposal - 39 (BIP39) - /// BIP39 specification used as reference located here: https://github.com/bitcoin/bips/blob/master/bip-0039.mediawiki - /// Made by thashiznets@yahoo.com.au - /// v1.0.1.1 - /// I ♥ Bitcoin :) - /// Bitcoin:1ETQjMkR1NNh4jwLuN5LxY7bMsHC9PUPSV - /// - public class Mnemonic - { - public Mnemonic(string mnemonic, Wordlist wordlist = null) - { - if (mnemonic == null) - throw new ArgumentNullException(nameof(mnemonic)); - _Mnemonic = mnemonic.Trim(); - if (wordlist == null) - wordlist = Wordlist.AutoDetect(mnemonic) ?? Wordlist.English; - var words = mnemonic.Split((char[])null, StringSplitOptions.RemoveEmptyEntries); - _Mnemonic = string.Join(wordlist.Space.ToString(), words); - //if the sentence is not at least 12 characters or cleanly divisible by 3, it is bad! - if (!CorrectWordCount(words.Length)) - { - throw new FormatException("Word count should be 12,15,18,21 or 24"); - } - _Words = words; - _WordList = wordlist; - _Indices = wordlist.ToIndices(words); - } - - /// - /// Generate a mnemonic - /// - /// - /// - public Mnemonic(Wordlist wordList, byte[] entropy = null) - { - wordList = wordList ?? Wordlist.English; - _WordList = wordList; - if (entropy == null) - entropy = RandomNumberGenerator.GetBytes(32); - - var i = Array.IndexOf(entArray, entropy.Length * 8); - if (i == -1) - throw new ArgumentException("The length for entropy should be " + String.Join(",", entArray) + " bits", "entropy"); - - int cs = csArray[i]; - byte[] checksum = SHA256.HashData(entropy); - BitWriter entcsResult = new BitWriter(); - - entcsResult.Write(entropy); - entcsResult.Write(checksum, cs); - _Indices = entcsResult.ToIntegers(); - _Words = _WordList.GetWords(_Indices); - _Mnemonic = _WordList.GetSentence(_Indices); - } - - public Mnemonic(Wordlist wordList, WordCount wordCount) - : this(wordList, GenerateEntropy(wordCount)) - { - - } - - private static byte[] GenerateEntropy(WordCount wordCount) - { - var ms = (int)wordCount; - if (!CorrectWordCount(ms)) - throw new ArgumentException("Word count should be 12,15,18,21 or 24", "wordCount"); - int i = Array.IndexOf(msArray, (int)wordCount); - return RandomNumberGenerator.GetBytes(entArray[i] / 8); - } - - static readonly int[] msArray = new[] { 12, 15, 18, 21, 24 }; - static readonly int[] csArray = new[] { 4, 5, 6, 7, 8 }; - static readonly int[] entArray = new[] { 128, 160, 192, 224, 256 }; - - bool? _IsValidChecksum; - public bool IsValidChecksum - { - get - { - if (_IsValidChecksum == null) - { - int i = Array.IndexOf(msArray, _Indices.Length); - int cs = csArray[i]; - int ent = entArray[i]; - - BitWriter writer = new BitWriter(); - var bits = Wordlist.ToBits(_Indices); - writer.Write(bits, ent); - var entropy = writer.ToBytes(); - var checksum = SHA256.HashData(entropy as byte[]); - - writer.Write(checksum, cs); - var expectedIndices = writer.ToIntegers(); - _IsValidChecksum = expectedIndices.SequenceEqual(_Indices); - } - return _IsValidChecksum.Value; - } - } - - private static bool CorrectWordCount(int ms) - { - return msArray.Any(_ => _ == ms); - } - - private readonly Wordlist _WordList; - public Wordlist WordList - { - get - { - return _WordList; - } - } - - private readonly int[] _Indices; - public int[] Indices - { - get - { - return _Indices; - } - } - private readonly string[] _Words; - public string[] Words - { - get - { - return _Words; - } - } - - static Encoding NoBOMUTF8 = new UTF8Encoding(false); - public byte[] DeriveSeed(string passphrase = null) - { - passphrase = passphrase ?? ""; - var salt = Concat(NoBOMUTF8.GetBytes("mnemonic"), Normalize(passphrase)); - var bytes = Normalize(_Mnemonic); - - using Rfc2898DeriveBytes derive = new Rfc2898DeriveBytes(bytes, salt, 2048, HashAlgorithmName.SHA512); - return derive.GetBytes(64); - } - - internal static byte[] Normalize(string str) - { - return NoBOMUTF8.GetBytes(NormalizeString(str)); - } - - internal static string NormalizeString(string word) - { - if (!SupportOsNormalization()) - { - return KDTable.NormalizeKD(word); - } - else - { - return word.Normalize(NormalizationForm.FormKD); - } - - } - - static bool? _SupportOSNormalization; - internal static bool SupportOsNormalization() - { - if (_SupportOSNormalization == null) - { - var notNormalized = "あおぞら"; - var normalized = "あおぞら"; - if (notNormalized.Equals(normalized, StringComparison.Ordinal)) - { - _SupportOSNormalization = false; - } - else - { - try - { - _SupportOSNormalization = notNormalized.Normalize(NormalizationForm.FormKD).Equals(normalized, StringComparison.Ordinal); - } - catch { _SupportOSNormalization = false; } - } - } - return _SupportOSNormalization.Value; - } - - - static Byte[] Concat(Byte[] source1, Byte[] source2) - { - //Most efficient way to merge two arrays this according to http://stackoverflow.com/questions/415291/best-way-to-combine-two-or-more-byte-arrays-in-c-sharp - Byte[] buffer = new Byte[source1.Length + source2.Length]; - System.Buffer.BlockCopy(source1, 0, buffer, 0, source1.Length); - System.Buffer.BlockCopy(source2, 0, buffer, source1.Length, source2.Length); - - return buffer; - } - - - string _Mnemonic; - public override string ToString() - { - return _Mnemonic; - } - - - } + /// + /// A .NET implementation of the Bitcoin Improvement Proposal - 39 (BIP39) + /// BIP39 specification used as reference located here: https://github.com/bitcoin/bips/blob/master/bip-0039.mediawiki + /// Made by thashiznets@yahoo.com.au + /// v1.0.1.1 + /// I ♥ Bitcoin :) + /// Bitcoin:1ETQjMkR1NNh4jwLuN5LxY7bMsHC9PUPSV + /// + public class Mnemonic + { + public Mnemonic(string mnemonic, Wordlist wordlist = null) + { + if (mnemonic == null) + throw new ArgumentNullException(nameof(mnemonic)); + _Mnemonic = mnemonic.Trim(); + if (wordlist == null) + wordlist = Wordlist.AutoDetect(mnemonic) ?? Wordlist.English; + var words = mnemonic.Split((char[])null, StringSplitOptions.RemoveEmptyEntries); + _Mnemonic = string.Join(wordlist.Space.ToString(), words); + //if the sentence is not at least 12 characters or cleanly divisible by 3, it is bad! + if (!CorrectWordCount(words.Length)) + { + throw new FormatException("Word count should be 12,15,18,21 or 24"); + } + _Words = words; + _WordList = wordlist; + _Indices = wordlist.ToIndices(words); + } + + /// + /// Generate a mnemonic + /// + /// + /// + public Mnemonic(Wordlist wordList, byte[] entropy = null) + { + wordList = wordList ?? Wordlist.English; + _WordList = wordList; + if (entropy == null) + entropy = RandomNumberGenerator.GetBytes(32); + + var i = Array.IndexOf(entArray, entropy.Length * 8); + if (i == -1) + throw new ArgumentException( + "The length for entropy should be " + String.Join(",", entArray) + " bits", + "entropy" + ); + + int cs = csArray[i]; + byte[] checksum = SHA256.HashData(entropy); + BitWriter entcsResult = new BitWriter(); + + entcsResult.Write(entropy); + entcsResult.Write(checksum, cs); + _Indices = entcsResult.ToIntegers(); + _Words = _WordList.GetWords(_Indices); + _Mnemonic = _WordList.GetSentence(_Indices); + } + + public Mnemonic(Wordlist wordList, WordCount wordCount) + : this(wordList, GenerateEntropy(wordCount)) { } + + private static byte[] GenerateEntropy(WordCount wordCount) + { + var ms = (int)wordCount; + if (!CorrectWordCount(ms)) + throw new ArgumentException("Word count should be 12,15,18,21 or 24", "wordCount"); + int i = Array.IndexOf(msArray, (int)wordCount); + return RandomNumberGenerator.GetBytes(entArray[i] / 8); + } + + static readonly int[] msArray = new[] { 12, 15, 18, 21, 24 }; + static readonly int[] csArray = new[] { 4, 5, 6, 7, 8 }; + static readonly int[] entArray = new[] { 128, 160, 192, 224, 256 }; + + bool? _IsValidChecksum; + public bool IsValidChecksum + { + get + { + if (_IsValidChecksum == null) + { + int i = Array.IndexOf(msArray, _Indices.Length); + int cs = csArray[i]; + int ent = entArray[i]; + + BitWriter writer = new BitWriter(); + var bits = Wordlist.ToBits(_Indices); + writer.Write(bits, ent); + var entropy = writer.ToBytes(); + var checksum = SHA256.HashData(entropy as byte[]); + + writer.Write(checksum, cs); + var expectedIndices = writer.ToIntegers(); + _IsValidChecksum = expectedIndices.SequenceEqual(_Indices); + } + return _IsValidChecksum.Value; + } + } + + private static bool CorrectWordCount(int ms) + { + return msArray.Any(_ => _ == ms); + } + + private readonly Wordlist _WordList; + public Wordlist WordList + { + get { return _WordList; } + } + + private readonly int[] _Indices; + public int[] Indices + { + get { return _Indices; } + } + private readonly string[] _Words; + public string[] Words + { + get { return _Words; } + } + + static Encoding NoBOMUTF8 = new UTF8Encoding(false); + + public byte[] DeriveSeed(string passphrase = null) + { + passphrase = passphrase ?? ""; + var salt = Concat(NoBOMUTF8.GetBytes("mnemonic"), Normalize(passphrase)); + var bytes = Normalize(_Mnemonic); + + using Rfc2898DeriveBytes derive = new Rfc2898DeriveBytes( + bytes, + salt, + 2048, + HashAlgorithmName.SHA512 + ); + return derive.GetBytes(64); + } + + internal static byte[] Normalize(string str) + { + return NoBOMUTF8.GetBytes(NormalizeString(str)); + } + + internal static string NormalizeString(string word) + { + if (!SupportOsNormalization()) + { + return KDTable.NormalizeKD(word); + } + else + { + return word.Normalize(NormalizationForm.FormKD); + } + } + + static bool? _SupportOSNormalization; + + internal static bool SupportOsNormalization() + { + if (_SupportOSNormalization == null) + { + var notNormalized = "あおぞら"; + var normalized = "あおぞら"; + if (notNormalized.Equals(normalized, StringComparison.Ordinal)) + { + _SupportOSNormalization = false; + } + else + { + try + { + _SupportOSNormalization = notNormalized + .Normalize(NormalizationForm.FormKD) + .Equals(normalized, StringComparison.Ordinal); + } + catch + { + _SupportOSNormalization = false; + } + } + } + return _SupportOSNormalization.Value; + } + + static Byte[] Concat(Byte[] source1, Byte[] source2) + { + //Most efficient way to merge two arrays this according to http://stackoverflow.com/questions/415291/best-way-to-combine-two-or-more-byte-arrays-in-c-sharp + Byte[] buffer = new Byte[source1.Length + source2.Length]; + System.Buffer.BlockCopy(source1, 0, buffer, 0, source1.Length); + System.Buffer.BlockCopy(source2, 0, buffer, source1.Length, source2.Length); + + return buffer; + } + + string _Mnemonic; + + public override string ToString() + { + return _Mnemonic; + } + } } #pragma warning restore CS0618 // Type or member is obsolete diff --git a/DotNut/NBitcoin/BIP39/WordCount.cs b/DotNut/NBitcoin/BIP39/WordCount.cs index 5f03dae..aa9a044 100644 --- a/DotNut/NBitcoin/BIP39/WordCount.cs +++ b/DotNut/NBitcoin/BIP39/WordCount.cs @@ -6,5 +6,5 @@ public enum WordCount : int Fifteen = 15, Eighteen = 18, TwentyOne = 21, - TwentyFour = 24 -} \ No newline at end of file + TwentyFour = 24, +} diff --git a/DotNut/NBitcoin/BIP39/Wordlist.cs b/DotNut/NBitcoin/BIP39/Wordlist.cs index 61b4258..3dc4b6e 100644 --- a/DotNut/NBitcoin/BIP39/Wordlist.cs +++ b/DotNut/NBitcoin/BIP39/Wordlist.cs @@ -3,451 +3,439 @@ namespace DotNut.NBitcoin.BIP39 { - public class Wordlist - { - static Wordlist() - { - WordlistSource = new HardcodedWordlistSource(); - } - private static Wordlist _Japanese; - public static Wordlist Japanese - { - get - { - if (_Japanese == null) - _Japanese = LoadWordList(Language.Japanese).Result; - return _Japanese; - } - } - - private static Wordlist _ChineseSimplified; - public static Wordlist ChineseSimplified - { - get - { - if (_ChineseSimplified == null) - _ChineseSimplified = LoadWordList(Language.ChineseSimplified).Result; - return _ChineseSimplified; - } - } - - private static Wordlist _ChineseTraditional; - public static Wordlist ChineseTraditional - { - get - { - if (_ChineseTraditional == null) - _ChineseTraditional = LoadWordList(Language.ChineseTraditional).Result; - return _ChineseTraditional; - } - } - - private static Wordlist _Spanish; - public static Wordlist Spanish - { - get - { - if (_Spanish == null) - _Spanish = LoadWordList(Language.Spanish).Result; - return _Spanish; - } - } - - private static Wordlist _English; - public static Wordlist English - { - get - { - if (_English == null) - _English = LoadWordList(Language.English).Result; - return _English; - } - } - - private static Wordlist _French; - public static Wordlist French - { - get - { - if (_French == null) - _French = LoadWordList(Language.French).Result; - return _French; - } - } - - private static Wordlist _PortugueseBrazil; - public static Wordlist PortugueseBrazil - { - get - { - if (_PortugueseBrazil == null) - _PortugueseBrazil = LoadWordList(Language.PortugueseBrazil).Result; - return _PortugueseBrazil; - } - } - - private static Wordlist _Czech; - public static Wordlist Czech - { - get - { - if (_Czech == null) - _Czech = LoadWordList(Language.Czech).Result; - return _Czech; - } - } - - public static Task LoadWordList(Language language) - { - string name = GetLanguageFileName(language); - return LoadWordList(name); - } - - internal static string GetLanguageFileName(Language language) - { - string name = null; - switch (language) - { - case Language.ChineseTraditional: - name = "chinese_traditional"; - break; - case Language.ChineseSimplified: - name = "chinese_simplified"; - break; - case Language.English: - name = "english"; - break; - case Language.Japanese: - name = "japanese"; - break; - case Language.Spanish: - name = "spanish"; - break; - case Language.French: - name = "french"; - break; - case Language.PortugueseBrazil: - name = "portuguese_brazil"; - break; - case Language.Czech: - name = "czech"; - break; - default: - throw new NotSupportedException(language.ToString()); - } - return name; - } - - static Dictionary _LoadedLists = new Dictionary(); - public static async Task LoadWordList(string name) - { - if (name == null) - throw new ArgumentNullException(nameof(name)); - Wordlist result = null; - lock (_LoadedLists) - { - _LoadedLists.TryGetValue(name, out result); - } - if (result != null) - return await Task.FromResult(result).ConfigureAwait(false); - - - if (WordlistSource == null) - throw new InvalidOperationException("Wordlist.WordlistSource is not set, impossible to fetch word list."); - result = await WordlistSource.Load(name).ConfigureAwait(false); - if (result != null) - lock (_LoadedLists) - { - _LoadedLists.Remove(name); - _LoadedLists.Add(name, result); - } - return result; - } - - public static IWordlistSource WordlistSource - { - get; - set; - } - - private String[] _words; - - /// - /// Constructor used by inheritence only - /// - /// The words to be used in the wordlist - public Wordlist(String[] words, char space, string name) - { - _words = words - .Select(w => Mnemonic.NormalizeString(w)) - .ToArray(); - _Space = space; - _Name = name; - } - - private readonly string _Name; - public string Name - { - get - { - return _Name; - } - } - private readonly char _Space; - public char Space - { - get - { - return _Space; - } - } - - /// - /// Method to determine if word exists in word list, great for auto language detection - /// - /// The word to check for existence - /// Exists (true/false) - public bool WordExists(string word, out int index) - { - word = Mnemonic.NormalizeString(word); - if (_words.Contains(word)) - { - index = Array.IndexOf(_words, word); - return true; - } - - //index -1 means word is not in wordlist - index = -1; - return false; - } - - /// - /// Returns a string containing the word at the specified index of the wordlist - /// - /// Index of word to return - /// Word - public string GetWordAtIndex(int index) - { - return _words[index]; - } - - /// - /// The number of all the words in the wordlist - /// - public int WordCount - { - get - { - return _words.Length; - } - } - - - public static Task AutoDetectAsync(string sentence) - { - return LoadWordList(AutoDetectLanguage(sentence)); - } - public static Wordlist AutoDetect(string sentence) - { - return LoadWordList(AutoDetectLanguage(sentence)).Result; - } - public static Language AutoDetectLanguage(string[] words) - { - List languageCount = new List(new int[] { 0, 0, 0, 0, 0, 0, 0, 0 }); - int index; - - foreach (string s in words) - { - if (Wordlist.English.WordExists(s, out index)) - { - //english is at 0 - languageCount[0]++; - } - - if (Wordlist.Japanese.WordExists(s, out index)) - { - //japanese is at 1 - languageCount[1]++; - } - - if (Wordlist.Spanish.WordExists(s, out index)) - { - //spanish is at 2 - languageCount[2]++; - } - - if (Wordlist.ChineseSimplified.WordExists(s, out index)) - { - //chinese simplified is at 3 - languageCount[3]++; - } - - if (Wordlist.ChineseTraditional.WordExists(s, out index) && !Wordlist.ChineseSimplified.WordExists(s, out index)) - { - //chinese traditional is at 4 - languageCount[4]++; - } - if (Wordlist.French.WordExists(s, out index)) - { - languageCount[5]++; - } - - if (Wordlist.PortugueseBrazil.WordExists(s, out index)) - { - //portuguese_brazil is at 6 - languageCount[6]++; - } - - if (Wordlist.Czech.WordExists(s, out index)) - { - //czech is at 7 - languageCount[7]++; - } - } - - //no hits found for any language unknown - if (languageCount.Max() == 0) - { - return Language.Unknown; - } - - if (languageCount.IndexOf(languageCount.Max()) == 0) - { - return Language.English; - } - else if (languageCount.IndexOf(languageCount.Max()) == 1) - { - return Language.Japanese; - } - else if (languageCount.IndexOf(languageCount.Max()) == 2) - { - return Language.Spanish; - } - else if (languageCount.IndexOf(languageCount.Max()) == 3) - { - if (languageCount[4] > 0) - { - //has traditional characters so not simplified but instead traditional - return Language.ChineseTraditional; - } - - return Language.ChineseSimplified; - } - else if (languageCount.IndexOf(languageCount.Max()) == 4) - { - return Language.ChineseTraditional; - } - else if (languageCount.IndexOf(languageCount.Max()) == 5) - { - return Language.French; - } - else if (languageCount.IndexOf(languageCount.Max()) == 6) - { - return Language.PortugueseBrazil; - } - else if (languageCount.IndexOf(languageCount.Max()) == 7) - { - return Language.Czech; - } - return Language.Unknown; - } - public static Language AutoDetectLanguage(string sentence) - { - string[] words = sentence.Split(new char[] { ' ', ' ' }); //normal space and JP space - - return AutoDetectLanguage(words); - } - - public string[] Split(string mnemonic) - { - return mnemonic.Split(new char[] { Space }, StringSplitOptions.RemoveEmptyEntries); - } - - public override string ToString() - { - return _Name; - } - - public ReadOnlyCollection GetWords() - { - return new ReadOnlyCollection(_words); - } - - public string[] GetWords(int[] indices) - { - return - indices - .Select(i => GetWordAtIndex(i)) - .ToArray(); - } - - public string GetSentence(int[] indices) - { - return String.Join(Space.ToString(), GetWords(indices)); - - } - - public int[] ToIndices(string[] words) - { - var indices = new int[words.Length]; - for (int i = 0; i < words.Length; i++) - { - int idx = -1; - - if (!WordExists(words[i], out idx)) - { - throw new FormatException("Word " + words[i] + " is not in the wordlist for this language, cannot continue to rebuild entropy from wordlist"); - } - indices[i] = idx; - } - return indices; - } - - public int[] ToIndices(string sentence) - { - return ToIndices(Split(sentence)); - } - - public static BitArray ToBits(int[] values) - { - if (values.Any(v => v >= 2048)) - throw new ArgumentException("values should be between 0 and 2048", "values"); - BitArray result = new BitArray(values.Length * 11); - int i = 0; - foreach (var val in values) - { - for (int p = 0; p < 11; p++) - { - var v = (val & (1 << (10 - p))) != 0; - result.Set(i, v); - i++; - } - } - return result; - } - public static int[] ToIntegers(BitArray bits) - { - return - bits - .OfType() - .Select((v, i) => new - { - Group = i / 11, - Value = v ? 1 << (10 - (i % 11)) : 0 - }) - .GroupBy(_ => _.Group, _ => _.Value) - .Select(g => g.Sum()) - .ToArray(); - } - - public BitArray ToBits(string sentence) - { - return ToBits(ToIndices(sentence)); - } - - public string[] GetWords(string sentence) - { - return ToIndices(sentence).Select(i => GetWordAtIndex(i)).ToArray(); - } - } + public class Wordlist + { + static Wordlist() + { + WordlistSource = new HardcodedWordlistSource(); + } + + private static Wordlist _Japanese; + public static Wordlist Japanese + { + get + { + if (_Japanese == null) + _Japanese = LoadWordList(Language.Japanese).Result; + return _Japanese; + } + } + + private static Wordlist _ChineseSimplified; + public static Wordlist ChineseSimplified + { + get + { + if (_ChineseSimplified == null) + _ChineseSimplified = LoadWordList(Language.ChineseSimplified).Result; + return _ChineseSimplified; + } + } + + private static Wordlist _ChineseTraditional; + public static Wordlist ChineseTraditional + { + get + { + if (_ChineseTraditional == null) + _ChineseTraditional = LoadWordList(Language.ChineseTraditional).Result; + return _ChineseTraditional; + } + } + + private static Wordlist _Spanish; + public static Wordlist Spanish + { + get + { + if (_Spanish == null) + _Spanish = LoadWordList(Language.Spanish).Result; + return _Spanish; + } + } + + private static Wordlist _English; + public static Wordlist English + { + get + { + if (_English == null) + _English = LoadWordList(Language.English).Result; + return _English; + } + } + + private static Wordlist _French; + public static Wordlist French + { + get + { + if (_French == null) + _French = LoadWordList(Language.French).Result; + return _French; + } + } + + private static Wordlist _PortugueseBrazil; + public static Wordlist PortugueseBrazil + { + get + { + if (_PortugueseBrazil == null) + _PortugueseBrazil = LoadWordList(Language.PortugueseBrazil).Result; + return _PortugueseBrazil; + } + } + + private static Wordlist _Czech; + public static Wordlist Czech + { + get + { + if (_Czech == null) + _Czech = LoadWordList(Language.Czech).Result; + return _Czech; + } + } + + public static Task LoadWordList(Language language) + { + string name = GetLanguageFileName(language); + return LoadWordList(name); + } + + internal static string GetLanguageFileName(Language language) + { + string name = null; + switch (language) + { + case Language.ChineseTraditional: + name = "chinese_traditional"; + break; + case Language.ChineseSimplified: + name = "chinese_simplified"; + break; + case Language.English: + name = "english"; + break; + case Language.Japanese: + name = "japanese"; + break; + case Language.Spanish: + name = "spanish"; + break; + case Language.French: + name = "french"; + break; + case Language.PortugueseBrazil: + name = "portuguese_brazil"; + break; + case Language.Czech: + name = "czech"; + break; + default: + throw new NotSupportedException(language.ToString()); + } + return name; + } + + static Dictionary _LoadedLists = new Dictionary(); + + public static async Task LoadWordList(string name) + { + if (name == null) + throw new ArgumentNullException(nameof(name)); + Wordlist result = null; + lock (_LoadedLists) + { + _LoadedLists.TryGetValue(name, out result); + } + if (result != null) + return await Task.FromResult(result).ConfigureAwait(false); + + if (WordlistSource == null) + throw new InvalidOperationException( + "Wordlist.WordlistSource is not set, impossible to fetch word list." + ); + result = await WordlistSource.Load(name).ConfigureAwait(false); + if (result != null) + lock (_LoadedLists) + { + _LoadedLists.Remove(name); + _LoadedLists.Add(name, result); + } + return result; + } + + public static IWordlistSource WordlistSource { get; set; } + + private String[] _words; + + /// + /// Constructor used by inheritence only + /// + /// The words to be used in the wordlist + public Wordlist(String[] words, char space, string name) + { + _words = words.Select(w => Mnemonic.NormalizeString(w)).ToArray(); + _Space = space; + _Name = name; + } + + private readonly string _Name; + public string Name + { + get { return _Name; } + } + private readonly char _Space; + public char Space + { + get { return _Space; } + } + + /// + /// Method to determine if word exists in word list, great for auto language detection + /// + /// The word to check for existence + /// Exists (true/false) + public bool WordExists(string word, out int index) + { + word = Mnemonic.NormalizeString(word); + if (_words.Contains(word)) + { + index = Array.IndexOf(_words, word); + return true; + } + + //index -1 means word is not in wordlist + index = -1; + return false; + } + + /// + /// Returns a string containing the word at the specified index of the wordlist + /// + /// Index of word to return + /// Word + public string GetWordAtIndex(int index) + { + return _words[index]; + } + + /// + /// The number of all the words in the wordlist + /// + public int WordCount + { + get { return _words.Length; } + } + + public static Task AutoDetectAsync(string sentence) + { + return LoadWordList(AutoDetectLanguage(sentence)); + } + + public static Wordlist AutoDetect(string sentence) + { + return LoadWordList(AutoDetectLanguage(sentence)).Result; + } + + public static Language AutoDetectLanguage(string[] words) + { + List languageCount = new List(new int[] { 0, 0, 0, 0, 0, 0, 0, 0 }); + int index; + + foreach (string s in words) + { + if (Wordlist.English.WordExists(s, out index)) + { + //english is at 0 + languageCount[0]++; + } + + if (Wordlist.Japanese.WordExists(s, out index)) + { + //japanese is at 1 + languageCount[1]++; + } + + if (Wordlist.Spanish.WordExists(s, out index)) + { + //spanish is at 2 + languageCount[2]++; + } + + if (Wordlist.ChineseSimplified.WordExists(s, out index)) + { + //chinese simplified is at 3 + languageCount[3]++; + } + + if ( + Wordlist.ChineseTraditional.WordExists(s, out index) + && !Wordlist.ChineseSimplified.WordExists(s, out index) + ) + { + //chinese traditional is at 4 + languageCount[4]++; + } + if (Wordlist.French.WordExists(s, out index)) + { + languageCount[5]++; + } + + if (Wordlist.PortugueseBrazil.WordExists(s, out index)) + { + //portuguese_brazil is at 6 + languageCount[6]++; + } + + if (Wordlist.Czech.WordExists(s, out index)) + { + //czech is at 7 + languageCount[7]++; + } + } + + //no hits found for any language unknown + if (languageCount.Max() == 0) + { + return Language.Unknown; + } + + if (languageCount.IndexOf(languageCount.Max()) == 0) + { + return Language.English; + } + else if (languageCount.IndexOf(languageCount.Max()) == 1) + { + return Language.Japanese; + } + else if (languageCount.IndexOf(languageCount.Max()) == 2) + { + return Language.Spanish; + } + else if (languageCount.IndexOf(languageCount.Max()) == 3) + { + if (languageCount[4] > 0) + { + //has traditional characters so not simplified but instead traditional + return Language.ChineseTraditional; + } + + return Language.ChineseSimplified; + } + else if (languageCount.IndexOf(languageCount.Max()) == 4) + { + return Language.ChineseTraditional; + } + else if (languageCount.IndexOf(languageCount.Max()) == 5) + { + return Language.French; + } + else if (languageCount.IndexOf(languageCount.Max()) == 6) + { + return Language.PortugueseBrazil; + } + else if (languageCount.IndexOf(languageCount.Max()) == 7) + { + return Language.Czech; + } + return Language.Unknown; + } + + public static Language AutoDetectLanguage(string sentence) + { + string[] words = sentence.Split(new char[] { ' ', ' ' }); //normal space and JP space + + return AutoDetectLanguage(words); + } + + public string[] Split(string mnemonic) + { + return mnemonic.Split(new char[] { Space }, StringSplitOptions.RemoveEmptyEntries); + } + + public override string ToString() + { + return _Name; + } + + public ReadOnlyCollection GetWords() + { + return new ReadOnlyCollection(_words); + } + + public string[] GetWords(int[] indices) + { + return indices.Select(i => GetWordAtIndex(i)).ToArray(); + } + + public string GetSentence(int[] indices) + { + return String.Join(Space.ToString(), GetWords(indices)); + } + + public int[] ToIndices(string[] words) + { + var indices = new int[words.Length]; + for (int i = 0; i < words.Length; i++) + { + int idx = -1; + + if (!WordExists(words[i], out idx)) + { + throw new FormatException( + "Word " + + words[i] + + " is not in the wordlist for this language, cannot continue to rebuild entropy from wordlist" + ); + } + indices[i] = idx; + } + return indices; + } + + public int[] ToIndices(string sentence) + { + return ToIndices(Split(sentence)); + } + + public static BitArray ToBits(int[] values) + { + if (values.Any(v => v >= 2048)) + throw new ArgumentException("values should be between 0 and 2048", "values"); + BitArray result = new BitArray(values.Length * 11); + int i = 0; + foreach (var val in values) + { + for (int p = 0; p < 11; p++) + { + var v = (val & (1 << (10 - p))) != 0; + result.Set(i, v); + i++; + } + } + return result; + } + + public static int[] ToIntegers(BitArray bits) + { + return bits.OfType() + .Select((v, i) => new { Group = i / 11, Value = v ? 1 << (10 - (i % 11)) : 0 }) + .GroupBy(_ => _.Group, _ => _.Value) + .Select(g => g.Sum()) + .ToArray(); + } + + public BitArray ToBits(string sentence) + { + return ToBits(ToIndices(sentence)); + } + + public string[] GetWords(string sentence) + { + return ToIndices(sentence).Select(i => GetWordAtIndex(i)).ToArray(); + } + } } diff --git a/DotNut/NBitcoin/BitWriter.cs b/DotNut/NBitcoin/BitWriter.cs index c37fefd..2f6196e 100644 --- a/DotNut/NBitcoin/BitWriter.cs +++ b/DotNut/NBitcoin/BitWriter.cs @@ -3,96 +3,93 @@ namespace DotNut.NBitcoin { + class BitWriter + { + List values = new List(); - class BitWriter - { - List values = new List(); - public void Write(bool value) - { - values.Insert(Position, value); - _Position++; - } + public void Write(bool value) + { + values.Insert(Position, value); + _Position++; + } - internal void Write(byte[] bytes) - { - Write(bytes, bytes.Length * 8); - } + internal void Write(byte[] bytes) + { + Write(bytes, bytes.Length * 8); + } - public void Write(byte[] bytes, int bitCount) - { - bytes = SwapEndianBytes(bytes); - BitArray array = new BitArray(bytes); - values.InsertRange(Position, array.OfType().Take(bitCount)); - _Position += bitCount; - } + public void Write(byte[] bytes, int bitCount) + { + bytes = SwapEndianBytes(bytes); + BitArray array = new BitArray(bytes); + values.InsertRange(Position, array.OfType().Take(bitCount)); + _Position += bitCount; + } - public byte[] ToBytes() - { - var array = ToBitArray(); - var bytes = ToByteArray(array); - bytes = SwapEndianBytes(bytes); - return bytes; - } + public byte[] ToBytes() + { + var array = ToBitArray(); + var bytes = ToByteArray(array); + bytes = SwapEndianBytes(bytes); + return bytes; + } - //BitArray.CopyTo do not exist in portable lib - static byte[] ToByteArray(BitArray bits) - { - int arrayLength = bits.Length / 8; - if (bits.Length % 8 != 0) - arrayLength++; - byte[] array = new byte[arrayLength]; + //BitArray.CopyTo do not exist in portable lib + static byte[] ToByteArray(BitArray bits) + { + int arrayLength = bits.Length / 8; + if (bits.Length % 8 != 0) + arrayLength++; + byte[] array = new byte[arrayLength]; - for (int i = 0; i < bits.Length; i++) - { - int b = i / 8; - int offset = i % 8; - array[b] |= bits.Get(i) ? (byte)(1 << offset) : (byte)0; - } - return array; - } + for (int i = 0; i < bits.Length; i++) + { + int b = i / 8; + int offset = i % 8; + array[b] |= bits.Get(i) ? (byte)(1 << offset) : (byte)0; + } + return array; + } + public BitArray ToBitArray() + { + return new BitArray(values.ToArray()); + } - public BitArray ToBitArray() - { - return new BitArray(values.ToArray()); - } + public int[] ToIntegers() + { + var array = new BitArray(values.ToArray()); + return Wordlist.ToIntegers(array); + } - public int[] ToIntegers() - { - var array = new BitArray(values.ToArray()); - return Wordlist.ToIntegers(array); - } + static byte[] SwapEndianBytes(byte[] bytes) + { + byte[] output = new byte[bytes.Length]; + for (int i = 0; i < output.Length; i++) + { + byte newByte = 0; + for (int ib = 0; ib < 8; ib++) + { + newByte += (byte)(((bytes[i] >> ib) & 1) << (7 - ib)); + } + output[i] = newByte; + } + return output; + } + int _Position; + public int Position + { + get => _Position; + set => _Position = value; + } - static byte[] SwapEndianBytes(byte[] bytes) - { - byte[] output = new byte[bytes.Length]; - for (int i = 0; i < output.Length; i++) - { - byte newByte = 0; - for (int ib = 0; ib < 8; ib++) - { - newByte += (byte)(((bytes[i] >> ib) & 1) << (7 - ib)); - } - output[i] = newByte; - } - return output; - } - - - int _Position; - public int Position - { - get => _Position; - set => _Position = value; - } - public void Write(BitArray bitArray, int bitCount) - { - for (int i = 0; i < bitCount; i++) - { - Write(bitArray.Get(i)); - } - } - } - + public void Write(BitArray bitArray, int bitCount) + { + for (int i = 0; i < bitCount; i++) + { + Write(bitArray.Get(i)); + } + } + } } diff --git a/DotNut/NUT00/BlindSignature.cs b/DotNut/NUT00/BlindSignature.cs index 8f99401..d373b79 100644 --- a/DotNut/NUT00/BlindSignature.cs +++ b/DotNut/NUT00/BlindSignature.cs @@ -5,16 +5,17 @@ namespace DotNut; public class BlindSignature { - [JsonPropertyName("amount")] public ulong Amount { get; set; } + [JsonPropertyName("amount")] + public ulong Amount { get; set; } [JsonConverter(typeof(KeysetIdJsonConverter))] [JsonPropertyName("id")] public KeysetId Id { get; set; } - [JsonPropertyName("C_")] public PubKey C_ { get; set; } - - + [JsonPropertyName("C_")] + public PubKey C_ { get; set; } + [JsonPropertyName("dleq")] [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public DLEQProof? DLEQ { get; set; } -} \ No newline at end of file +} diff --git a/DotNut/NUT00/BlindedMessage.cs b/DotNut/NUT00/BlindedMessage.cs index b6fbb45..8f066a7 100644 --- a/DotNut/NUT00/BlindedMessage.cs +++ b/DotNut/NUT00/BlindedMessage.cs @@ -4,8 +4,16 @@ namespace DotNut; public class BlindedMessage { - [JsonPropertyName("amount")] public ulong Amount { get; set; } - [JsonPropertyName("id")] public KeysetId Id { get; set; } - [JsonPropertyName("B_")] public PubKey B_ { get; set; } - [JsonPropertyName("witness")][JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public string? Witness { get; set; } -} \ No newline at end of file + [JsonPropertyName("amount")] + public ulong Amount { get; set; } + + [JsonPropertyName("id")] + public KeysetId Id { get; set; } + + [JsonPropertyName("B_")] + public PubKey B_ { get; set; } + + [JsonPropertyName("witness")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public string? Witness { get; set; } +} diff --git a/DotNut/NUT00/Cashu.cs b/DotNut/NUT00/Cashu.cs index 3f25e18..fc781a1 100644 --- a/DotNut/NUT00/Cashu.cs +++ b/DotNut/NUT00/Cashu.cs @@ -10,25 +10,27 @@ public static class Cashu private static readonly byte[] DOMAIN_SEPARATOR = "Secp256k1_HashToCurve_Cashu_"u8.ToArray(); private static readonly byte[] P2BK_PREFIX = "Cashu_P2BK_v1"u8.ToArray(); - - internal static readonly BigInteger N = - BigInteger.Parse("115792089237316195423570985008687907852837564279074904382605163141518161494337"); + + internal static readonly BigInteger N = BigInteger.Parse( + "115792089237316195423570985008687907852837564279074904382605163141518161494337" + ); + public static ECPubKey MessageToCurve(string message) { var hash = Encoding.UTF8.GetBytes(message); return HashToCurve(hash); - } - + } + public static ECPubKey HexToCurve(string hex) { var bytes = Convert.FromHexString(hex); return HashToCurve(bytes); } - + public static ECPubKey HashToCurve(byte[] x) { var msgHash = SHA256.HashData(Concat(DOMAIN_SEPARATOR, x)); - for (uint counter = 0;; counter++) + for (uint counter = 0; ; counter++) { var counterBytes = BitConverter.GetBytes(counter); var publicKeyBytes = Concat([0x02], SHA256.HashData(Concat(msgHash, counterBytes))); @@ -36,12 +38,10 @@ public static ECPubKey HashToCurve(byte[] x) { return ECPubKey.Create(publicKeyBytes); } - catch (FormatException) - { - } + catch (FormatException) { } } } - + /// /// Blinding /// @@ -65,7 +65,7 @@ public static ECPubKey ComputeC_(ECPubKey B_, ECPrivKey k) //C_ = kB_ return (B_.Q * k.sec).ToPubkey(); } - + /// /// Unblinding /// @@ -76,9 +76,12 @@ public static ECPubKey ComputeC_(ECPubKey B_, ECPrivKey k) public static ECPubKey ComputeC(ECPubKey C_, ECPrivKey r, ECPubKey A) { //C_ - rA = C - return C_.Q.ToGroupElementJacobian().Add((A.Q * r.sec).ToGroupElement().Negate()).ToPubkey(); + return C_ + .Q.ToGroupElementJacobian() + .Add((A.Q * r.sec).ToGroupElement().Negate()) + .ToPubkey(); } - + /// /// Creates DLEQ Proof. /// @@ -98,7 +101,7 @@ public static (ECPrivKey e, ECPrivKey s) ComputeProof(ECPubKey B_, ECPrivKey a, var s = p.TweakAdd(a.TweakMul(e.ToBytes()).ToBytes()); return (e.ToPrivateKey(), s); } - + /// /// Computes the challenge scalar 'e' for the DLEQ proof. /// @@ -109,7 +112,9 @@ public static (ECPrivKey e, ECPrivKey s) ComputeProof(ECPubKey B_, ECPrivKey a, /// The challenge scalar e derived as a SHA256 hash over the concatenation of the uncompressed points. public static Scalar ComputeE(ECPubKey R1, ECPubKey R2, ECPubKey K, ECPubKey C_) { - byte[] eBytes = Encoding.UTF8.GetBytes(string.Concat(new[] {R1, R2, K, C_}.Select(pk => pk.ToHex(false)))); + byte[] eBytes = Encoding.UTF8.GetBytes( + string.Concat(new[] { R1, R2, K, C_ }.Select(pk => pk.ToHex(false))) + ); return new Scalar(SHA256.HashData(eBytes)); } @@ -121,9 +126,16 @@ public static Scalar ComputeE(ECPubKey R1, ECPubKey R2, ECPubKey K, ECPubKey C_) /// public static bool Verify(this Proof proof, ECPubKey A) { - return VerifyProof(proof.Secret.ToCurve(),proof.DLEQ.R, proof.C, proof.DLEQ.E, proof.DLEQ.S, A); + return VerifyProof( + proof.Secret.ToCurve(), + proof.DLEQ.R, + proof.C, + proof.DLEQ.E, + proof.DLEQ.S, + A + ); } - + /// /// Verify DLEQ proof of Blinded signature /// @@ -133,9 +145,9 @@ public static bool Verify(this Proof proof, ECPubKey A) /// public static bool Verify(this BlindSignature blindSig, ECPubKey A, ECPubKey B_) { - return Cashu.VerifyProof(B_, blindSig.C_, blindSig.DLEQ.E, blindSig.DLEQ.S, A); + return Cashu.VerifyProof(B_, blindSig.C_, blindSig.DLEQ.E, blindSig.DLEQ.S, A); } - + /// /// Verify DLEQ proof /// @@ -147,11 +159,15 @@ public static bool Verify(this BlindSignature blindSig, ECPubKey A, ECPubKey B_) /// public static bool VerifyProof(ECPubKey B_, ECPubKey C_, ECPrivKey e, ECPrivKey s, ECPubKey A) { - var r1 = s.CreatePubKey().Q.ToGroupElementJacobian().Add((A.Q * e.sec.Negate()).ToGroupElement()).ToPubkey(); + var r1 = s.CreatePubKey() + .Q.ToGroupElementJacobian() + .Add((A.Q * e.sec.Negate()).ToGroupElement()) + .ToPubkey(); var r2 = (B_.Q * s.sec).Add((C_.Q * e.sec.Negate()).ToGroupElement()).ToPubkey(); var e_ = ComputeE(r1, r2, A, C_); return e.sec.Equals(e_); } + /// /// Verify DLEQ proof /// @@ -162,14 +178,19 @@ public static bool VerifyProof(ECPubKey B_, ECPubKey C_, ECPrivKey e, ECPrivKey /// Dleq.S returned by mint /// Amount pubkey /// - - public static bool VerifyProof(ECPubKey Y, ECPrivKey r, ECPubKey C, ECPrivKey e, ECPrivKey s, ECPubKey A) + public static bool VerifyProof( + ECPubKey Y, + ECPrivKey r, + ECPubKey C, + ECPrivKey e, + ECPrivKey s, + ECPubKey A + ) { var C_ = C.Q.ToGroupElementJacobian().Add((A.Q * r.sec).ToGroupElement()).ToPubkey(); var B_ = Y.Q.ToGroupElementJacobian().Add(r.CreatePubKey().Q).ToPubkey(); return VerifyProof(B_, C_, e, s, A); } - public static GE ToGE(this Scalar scalar) { @@ -185,7 +206,9 @@ public static ECPubKey ToPubkey(this Scalar scalar) public static ECPrivKey ToPrivateKey(this Scalar scalar) { - return ECPrivKey.TryCreate(scalar, out var key) ? key : throw new InvalidOperationException(); + return ECPrivKey.TryCreate(scalar, out var key) + ? key + : throw new InvalidOperationException(); } public static ECPubKey ToPubkey(this GEJ gej) @@ -212,11 +235,11 @@ public static byte[] ComputeZx(ECPrivKey e, ECPubKey P) ? xOnly.ToBytes() : throw new InvalidOperationException("Could not create xOnly pubkey"); } - + public static ECPrivKey ComputeRi(byte[] Zx, int i) { byte[] hash; - + hash = SHA256.HashData(Concat(P2BK_PREFIX, Zx, [(byte)(i & 0xFF)])); var hashValue = new BigInteger(hash); if (hashValue == 0 || hashValue.CompareTo(N) != -1) @@ -225,8 +248,7 @@ public static ECPrivKey ComputeRi(byte[] Zx, int i) } return ECPrivKey.Create(hash); } - - + private static byte[] Concat(params byte[][] arrays) { int totalLength = arrays.Sum(a => a?.Length ?? 0); @@ -235,43 +257,48 @@ private static byte[] Concat(params byte[][] arrays) foreach (var arr in arrays) { - if (arr == null || arr.Length == 0) continue; + if (arr == null || arr.Length == 0) + continue; Buffer.BlockCopy(arr, 0, result, offset, arr.Length); offset += arr.Length; } return result; } - + public static string ToHex(this ECPrivKey key) { return Convert.ToHexString(key.ToBytes()).ToLower(); } - + public static byte[] ToBytes(this ECPrivKey key) { Span output = stackalloc byte[32]; key.WriteToSpan(output); return output.ToArray(); } - - + public static byte[] ToUncompressedBytes(this ECPubKey key) { Span output = stackalloc byte[65]; - key.WriteToSpan(false, output, out _); + key.WriteToSpan(false, output, out _); return output.ToArray(); } + public static string ToHex(this ECPubKey key, bool compressed = true) { - return compressed ? Convert.ToHexString(key.ToBytes(true)).ToLower() : Convert.ToHexString(key.ToUncompressedBytes()).ToLower(); + return compressed + ? Convert.ToHexString(key.ToBytes(true)).ToLower() + : Convert.ToHexString(key.ToUncompressedBytes()).ToLower(); } + public static string ToHex(this Scalar scalar) { return Convert.ToHexString(scalar.ToBytes()).ToLower(); } + public static string ToHex(this SecpSchnorrSignature sig) { return Convert.ToHexString(sig.ToBytes()).ToLower(); } -} \ No newline at end of file +} diff --git a/DotNut/NUT00/CashuToken.cs b/DotNut/NUT00/CashuToken.cs index 7327747..0c962bd 100644 --- a/DotNut/NUT00/CashuToken.cs +++ b/DotNut/NUT00/CashuToken.cs @@ -6,9 +6,7 @@ public class CashuToken { public class Token { - public Token() - { - } + public Token() { } public Token(string mint, List proofs) { @@ -16,11 +14,15 @@ public Token(string mint, List proofs) Proofs = proofs; } - [JsonPropertyName("mint")] public string Mint { get; set; } - [JsonPropertyName("proofs")] public List Proofs { get; set; } + [JsonPropertyName("mint")] + public string Mint { get; set; } + + [JsonPropertyName("proofs")] + public List Proofs { get; set; } } - [JsonPropertyName("token")] public List Tokens { get; set; } + [JsonPropertyName("token")] + public List Tokens { get; set; } [JsonPropertyName("unit")] [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] @@ -29,4 +31,4 @@ public Token(string mint, List proofs) [JsonPropertyName("memo")] [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public string? Memo { get; set; } -} \ No newline at end of file +} diff --git a/DotNut/NUT00/ISecret.cs b/DotNut/NUT00/ISecret.cs index 5e860f8..8996bc3 100644 --- a/DotNut/NUT00/ISecret.cs +++ b/DotNut/NUT00/ISecret.cs @@ -9,4 +9,4 @@ public interface ISecret { byte[] GetBytes(); ECPubKey ToCurve(); -} \ No newline at end of file +} diff --git a/DotNut/NUT00/Proof.cs b/DotNut/NUT00/Proof.cs index e332d02..c2148de 100644 --- a/DotNut/NUT00/Proof.cs +++ b/DotNut/NUT00/Proof.cs @@ -1,30 +1,31 @@ using System.Text.Json.Serialization; using DotNut.JsonConverters; - namespace DotNut; public class Proof { - [JsonPropertyName("amount")] public ulong Amount { get; set; } + [JsonPropertyName("amount")] + public ulong Amount { get; set; } [JsonPropertyName("id")] public KeysetId Id { get; set; } - [JsonPropertyName("secret")] public ISecret Secret { get; set; } + [JsonPropertyName("secret")] + public ISecret Secret { get; set; } - [JsonPropertyName("C")] public PubKey C { get; set; } + [JsonPropertyName("C")] + public PubKey C { get; set; } [JsonPropertyName("witness")] [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public string? Witness { get; set; } - + [JsonPropertyName("dleq")] [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public DLEQProof? DLEQ { get; set; } - + [JsonPropertyName("p2pk_e")] [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public PubKey? P2PkE { get; set; } // must not be exposed to mint - -} \ No newline at end of file +} diff --git a/DotNut/NUT00/StringSecret.cs b/DotNut/NUT00/StringSecret.cs index 5181db0..1e530d1 100644 --- a/DotNut/NUT00/StringSecret.cs +++ b/DotNut/NUT00/StringSecret.cs @@ -10,6 +10,7 @@ public StringSecret(string secret) } public string Secret { get; init; } + public byte[] GetBytes() { return System.Text.Encoding.UTF8.GetBytes(Secret); @@ -19,4 +20,4 @@ public ECPubKey ToCurve() { return Cashu.HashToCurve(GetBytes()); } -} \ No newline at end of file +} diff --git a/DotNut/NUT01/Keyset.cs b/DotNut/NUT01/Keyset.cs index 20e4329..760dfc9 100644 --- a/DotNut/NUT01/Keyset.cs +++ b/DotNut/NUT01/Keyset.cs @@ -10,15 +10,20 @@ namespace DotNut; [JsonConverter(typeof(KeysetJsonConverter))] public class Keyset : Dictionary { - public KeysetId GetKeysetId(byte version = 0x00, string? unit = null, ulong? inputFeePpk = null, string? finalExpiration = null) + public KeysetId GetKeysetId( + byte version = 0x00, + string? unit = null, + ulong? inputFeePpk = null, + string? finalExpiration = null + ) { // 1 - sort public keys by their amount in ascending order - if (Count == 0) throw new InvalidOperationException("Keyset cannot be empty."); - var sortedKeys = this - .OrderBy(x => x.Key); - + if (Count == 0) + throw new InvalidOperationException("Keyset cannot be empty."); + var sortedKeys = this.OrderBy(x => x.Key); + using SHA256 sha256 = SHA256.Create(); - + switch (version) { case 0x00: @@ -32,30 +37,36 @@ public KeysetId GetKeysetId(byte version = 0x00, string? unit = null, ulong? inp var hash = sha256.ComputeHash(sortedBytes); // 4 - take the first 14 characters of the hex-encoded hash // 5 - prefix it with a keyset ID version byte - return new KeysetId(Convert.ToHexString(new []{version}) + Convert.ToHexString(hash).Substring(0, 14).ToLower()); + return new KeysetId( + Convert.ToHexString(new[] { version }) + + Convert.ToHexString(hash).Substring(0, 14).ToLower() + ); } - + case 0x01: { MemoryStream stream = new MemoryStream(); - + // 2 - concatenate each amount and its corresponding public key hex string (as "amount:publickey_hex") // to a single byte array, separating each pair with a comma (",") var sortedBytes = Encoding.UTF8.GetBytes( string.Join( ",", - sortedKeys - .Select(pair => $"{pair.Key}:{pair.Value.ToString().ToLowerInvariant()}") + sortedKeys.Select(pair => + $"{pair.Key}:{pair.Value.ToString().ToLowerInvariant()}" + ) ) ); - + stream.Write(sortedBytes, 0, sortedBytes.Length); - + // 3 - add the lowercase UTF8-encoded unit string prefixed with "|unit:" to the byte array (e.g. "|unit:sat") if (String.IsNullOrWhiteSpace(unit)) { - throw new ArgumentNullException(nameof(unit), - $"Unit parameter is required with version: {version}"); + throw new ArgumentNullException( + nameof(unit), + $"Unit parameter is required with version: {version}" + ); } var unitBytes = Encoding.UTF8.GetBytes($"|unit:{unit.Trim().ToLowerInvariant()}"); @@ -66,37 +77,45 @@ public KeysetId GetKeysetId(byte version = 0x00, string? unit = null, ulong? inp // If input_fee_ppk is omitted, null, or 0, it MUST be omitted from the preimage. if (inputFeePpk.HasValue && inputFeePpk.Value != 0) { - var feeBytes = Encoding.UTF8.GetBytes($"|input_fee_ppk:{inputFeePpk.Value}"); - stream.Write(feeBytes, 0, feeBytes.Length); + var feeBytes = Encoding.UTF8.GetBytes($"|input_fee_ppk:{inputFeePpk.Value}"); + stream.Write(feeBytes, 0, feeBytes.Length); } - + // 5 - If a final expiration is specified, add the UTF8-encoded string prefixed with "|final_expiry:" (e.g. "|final_expiry:1896187313") if (!string.IsNullOrWhiteSpace(finalExpiration)) { - var expiryBytes = Encoding.UTF8.GetBytes($"|final_expiry:{finalExpiration.Trim()}"); + var expiryBytes = Encoding.UTF8.GetBytes( + $"|final_expiry:{finalExpiration.Trim()}" + ); stream.Write(expiryBytes, 0, expiryBytes.Length); } - + // 6 - HASH_SHA256 the concatenated byte array var hash = sha256.ComputeHash(stream.ToArray()); - + // 7 - prefix it with a keyset ID version byte "01" - return new KeysetId(Convert.ToHexString(new[] { version }) + - Convert.ToHexString(hash).ToLower()); + return new KeysetId( + Convert.ToHexString(new[] { version }) + Convert.ToHexString(hash).ToLower() + ); } default: throw new ArgumentException($"Unsupported keyset version: {version}"); } - } - public bool VerifyKeysetId(KeysetId keysetId, string? unit = null, ulong? inputFeePpk = null, string? finalExpiration = null) + public bool VerifyKeysetId( + KeysetId keysetId, + string? unit = null, + ulong? inputFeePpk = null, + string? finalExpiration = null + ) { byte version = keysetId.GetVersion(); var derived = GetKeysetId(version, unit, inputFeePpk, finalExpiration).ToString(); var presented = keysetId.ToString(); - if (presented.Length > derived.Length) return false; - return string.Equals(derived, presented, StringComparison.InvariantCultureIgnoreCase) || - derived.StartsWith(presented, StringComparison.InvariantCultureIgnoreCase); + if (presented.Length > derived.Length) + return false; + return string.Equals(derived, presented, StringComparison.InvariantCultureIgnoreCase) + || derived.StartsWith(presented, StringComparison.InvariantCultureIgnoreCase); } -} \ No newline at end of file +} diff --git a/DotNut/NUT02/FeeHelper.cs b/DotNut/NUT02/FeeHelper.cs index 23802d0..adea066 100644 --- a/DotNut/NUT02/FeeHelper.cs +++ b/DotNut/NUT02/FeeHelper.cs @@ -4,8 +4,10 @@ namespace DotNut; public static class FeeHelper { - - public static ulong ComputeFee(this IEnumerable proofsToSpend, Dictionary keysetFees) + public static ulong ComputeFee( + this IEnumerable proofsToSpend, + Dictionary keysetFees + ) { ulong sum = 0; foreach (var proof in proofsToSpend) @@ -24,5 +26,4 @@ public static ulong Sum(this IEnumerable values) ArgumentNullException.ThrowIfNull(values); return values.Aggregate(0, (current, v) => current + v); } - -} \ No newline at end of file +} diff --git a/DotNut/NUT02/KeysetId.cs b/DotNut/NUT02/KeysetId.cs index 7249d84..1c3ba7a 100644 --- a/DotNut/NUT02/KeysetId.cs +++ b/DotNut/NUT02/KeysetId.cs @@ -4,14 +4,18 @@ namespace DotNut; [JsonConverter(typeof(KeysetIdJsonConverter))] -public class KeysetId : IEquatable,IEqualityComparer +public class KeysetId : IEquatable, IEqualityComparer { public bool Equals(KeysetId? x, KeysetId? y) { - if (ReferenceEquals(x, y)) return true; - if (ReferenceEquals(x, null)) return false; - if (ReferenceEquals(y, null)) return false; - if (x.GetType() != y.GetType()) return false; + if (ReferenceEquals(x, y)) + return true; + if (ReferenceEquals(x, null)) + return false; + if (ReferenceEquals(y, null)) + return false; + if (x.GetType() != y.GetType()) + return false; return string.Equals(x._id, y._id, StringComparison.InvariantCultureIgnoreCase); } @@ -27,7 +31,6 @@ public bool Equals(KeysetId? other) public override bool Equals(object? obj) { - return Equals(this, obj as KeysetId); } @@ -38,14 +41,15 @@ public override int GetHashCode() public static bool operator ==(KeysetId? left, KeysetId? right) { - return (left is null && right is null) || left?.Equals(right) is true || right?.Equals(left) is true; + return (left is null && right is null) + || left?.Equals(right) is true + || right?.Equals(left) is true; } public static bool operator !=(KeysetId? left, KeysetId? right) { return !(left == right); } - private readonly string _id; @@ -54,7 +58,8 @@ public KeysetId(string Id) if ( Id.Length != 66 // full length keysetId v2 && Id.Length != 16 // keysetId v1 or keysetId v2 short - && Id.Length != 12) // old pre-v1 base64 keysetId + && Id.Length != 12 + ) // old pre-v1 base64 keysetId { throw new ArgumentException("KeysetId must be 66, 16 or 12 (legacy) characters long"); } @@ -82,4 +87,4 @@ public byte[] GetBytes() { return Convert.FromHexString(_id); } -} \ No newline at end of file +} diff --git a/DotNut/NUT04/MintMethodSetting.cs b/DotNut/NUT04/MintMethodSetting.cs index 291c5a2..a2786ca 100644 --- a/DotNut/NUT04/MintMethodSetting.cs +++ b/DotNut/NUT04/MintMethodSetting.cs @@ -5,10 +5,18 @@ namespace DotNut; public class MintMethodSetting { - [JsonPropertyName("method")] public string Method { get; set; } - [JsonPropertyName("unit")] public string Unit { get; set; } - [JsonPropertyName("min_amount")] public ulong? Min { get; set; } - [JsonPropertyName("max_amount")] public ulong? Max { get; set; } - [JsonPropertyName("options")] public JsonDocument? Options { get; set; } -} + [JsonPropertyName("method")] + public string Method { get; set; } + + [JsonPropertyName("unit")] + public string Unit { get; set; } + + [JsonPropertyName("min_amount")] + public ulong? Min { get; set; } + [JsonPropertyName("max_amount")] + public ulong? Max { get; set; } + + [JsonPropertyName("options")] + public JsonDocument? Options { get; set; } +} diff --git a/DotNut/NUT05/MeltMethodSetting.cs b/DotNut/NUT05/MeltMethodSetting.cs index a89630b..5a17597 100644 --- a/DotNut/NUT05/MeltMethodSetting.cs +++ b/DotNut/NUT05/MeltMethodSetting.cs @@ -4,8 +4,15 @@ namespace DotNut; public class MeltMethodSetting { - [JsonPropertyName("method")] public string Method { get; set; } - [JsonPropertyName("unit")] public string Unit { get; set; } - [JsonPropertyName("min_amount")] public ulong? Min { get; set; } - [JsonPropertyName("max_amount")] public ulong? Max { get; set; } -} \ No newline at end of file + [JsonPropertyName("method")] + public string Method { get; set; } + + [JsonPropertyName("unit")] + public string Unit { get; set; } + + [JsonPropertyName("min_amount")] + public ulong? Min { get; set; } + + [JsonPropertyName("max_amount")] + public ulong? Max { get; set; } +} diff --git a/DotNut/NUT10/Nut10ProofSecret.cs b/DotNut/NUT10/Nut10ProofSecret.cs index 3389ba1..47c0b2a 100644 --- a/DotNut/NUT10/Nut10ProofSecret.cs +++ b/DotNut/NUT10/Nut10ProofSecret.cs @@ -6,11 +6,11 @@ public class Nut10ProofSecret { [JsonPropertyName("nonce")] public string Nonce { get; set; } - + [JsonPropertyName("data")] [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public string Data { get; set; } - + [JsonPropertyName("tags")] [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public string[][]? Tags { get; set; } @@ -27,21 +27,20 @@ public bool Equals(Nut10ProofSecret s) { return true; } - + if (this.GetType() != s.GetType()) { return false; } - - - return + + return this.Nonce == s.Nonce && - this.Data == s.Data && - ((this.Tags == null && s.Tags == null) || - (this.Tags != null && s.Tags != null && this.Tags.Length == s.Tags.Length && + this.Data == s.Data && + ((this.Tags == null && s.Tags == null) || + (this.Tags != null && s.Tags != null && this.Tags.Length == s.Tags.Length && this.Tags.Zip(s.Tags).All(pair => pair.First.SequenceEqual(pair.Second)))); } - + public override int GetHashCode() { var hash = new HashCode(); @@ -72,5 +71,4 @@ public override int GetHashCode() } public static bool operator !=(Nut10ProofSecret first, Nut10ProofSecret second) => !(first == second); - -} \ No newline at end of file +} diff --git a/DotNut/NUT10/Nut10Secret.cs b/DotNut/NUT10/Nut10Secret.cs index 05538b6..0799e43 100644 --- a/DotNut/NUT10/Nut10Secret.cs +++ b/DotNut/NUT10/Nut10Secret.cs @@ -24,7 +24,6 @@ public Nut10Secret(string originalString) public string Key { get; set; } public Nut10ProofSecret ProofSecret { get; set; } - public byte[] GetBytes() { return _originalString != null @@ -36,4 +35,4 @@ public ECPubKey ToCurve() { return Cashu.HashToCurve(GetBytes()); } -} \ No newline at end of file +} diff --git a/DotNut/NUT11/P2PKProofSecret.cs b/DotNut/NUT11/P2PKProofSecret.cs index 6627a97..a722369 100644 --- a/DotNut/NUT11/P2PKProofSecret.cs +++ b/DotNut/NUT11/P2PKProofSecret.cs @@ -1,4 +1,4 @@ -using System.Text; +using System.Text; using System.Text.Json; using System.Text.Json.Serialization; using NBitcoin.Secp256k1; @@ -10,7 +10,8 @@ public class P2PKProofSecret : Nut10ProofSecret { public const string Key = "P2PK"; - [JsonIgnore] public virtual P2PkBuilder Builder => P2PkBuilder.Load(this); + [JsonIgnore] + public virtual P2PkBuilder Builder => P2PkBuilder.Load(this); public virtual ECPubKey[] GetAllowedPubkeys(out int requiredSignatures) { @@ -18,16 +19,19 @@ public virtual ECPubKey[] GetAllowedPubkeys(out int requiredSignatures) requiredSignatures = builder.SignatureThreshold; return builder.Pubkeys; } - + public virtual ECPubKey[] GetAllowedRefundPubkeys(out int? requiredSignatures) { var builder = Builder; - if (!builder.Lock.HasValue || builder.Lock.Value.ToUnixTimeSeconds() >= DateTimeOffset.Now.ToUnixTimeSeconds()) + if ( + !builder.Lock.HasValue + || builder.Lock.Value.ToUnixTimeSeconds() >= DateTimeOffset.Now.ToUnixTimeSeconds() + ) { requiredSignatures = null; // there's no refund condition, or timelock didn't expire yet :/ return []; } - + if (builder.RefundPubkeys == null) { requiredSignatures = 0; // proof is spendable without any signature @@ -36,63 +40,72 @@ public virtual ECPubKey[] GetAllowedRefundPubkeys(out int? requiredSignatures) requiredSignatures = builder.RefundSignatureThreshold ?? 1; return builder.RefundPubkeys ?? []; } - - /* + + /* * ====================================================================== * * If any of these returns null witness - well, witness is not necessary * * ====================================================================== * */ - + public virtual P2PKWitness? GenerateWitness(Proof proof, ECPrivKey[] keys) { return GenerateWitness(proof.Secret.GetBytes(), keys); - } - + } + public virtual P2PKWitness? GenerateWitness(BlindedMessage message, ECPrivKey[] keys) { return GenerateWitness(message.B_.Key.ToBytes(), keys); } - + public virtual P2PKWitness? GenerateWitness(byte[] msg, ECPrivKey[] keys) { var hash = SHA256.HashData(msg); return GenerateWitness(ECPrivKey.Create(hash), keys); } - + public virtual P2PKWitness? GenerateWitness(ECPrivKey hash, ECPrivKey[] keys) { var msg = hash.ToBytes(); - + var allowedKeys = GetAllowedPubkeys(out var requiredSignatures); var allowedRefundKeys = GetAllowedRefundPubkeys(out var requiredRefundSignatures); - + if (requiredRefundSignatures == 0) { return null; } - + // try normal path var (isValid, result) = TrySignPath(allowedKeys.ToArray(), requiredSignatures, keys, msg); if (isValid) - { + { return result; } - + // if it's after locktime - try refund path if (requiredRefundSignatures.HasValue && allowedRefundKeys.Any()) { - (isValid, result) = TrySignPath(allowedRefundKeys.ToArray(), requiredRefundSignatures.Value, keys, msg); + (isValid, result) = TrySignPath( + allowedRefundKeys.ToArray(), + requiredRefundSignatures.Value, + keys, + msg + ); if (isValid) { return result; } } - + throw new InvalidOperationException("Not enough valid keys to sign!"); } - - private (bool IsValid, P2PKWitness Witness) TrySignPath(ECPubKey[] allowedKeys, int requiredSignatures, - ECPrivKey[] availableKeys, byte[] msg) + + private (bool IsValid, P2PKWitness Witness) TrySignPath( + ECPubKey[] allowedKeys, + int requiredSignatures, + ECPrivKey[] availableKeys, + byte[] msg + ) { var allowedKeysSet = new HashSet(allowedKeys); var result = new P2PKWitness(); @@ -112,10 +125,6 @@ public virtual ECPubKey[] GetAllowedRefundPubkeys(out int? requiredSignatures) return (result.Signatures.Length >= requiredSignatures, result); } - - - - public virtual bool VerifyWitness(Proof proof) { @@ -126,13 +135,13 @@ public virtual bool VerifyWitness(Proof proof) var witness = JsonSerializer.Deserialize(proof.Witness) ?? new P2PKWitness(); return VerifyWitness(proof.Secret, witness); } - + /* * ========================= * NUT-XX Pay to blinded key * ========================= */ - + public virtual P2PKWitness GenerateBlindWitness(Proof proof, ECPrivKey[] keys) { ArgumentNullException.ThrowIfNull(proof.P2PkE); @@ -144,28 +153,48 @@ public virtual P2PKWitness GenerateBlindWitness(Proof proof, ECPrivKey[] keys, E return GenerateBlindWitness(proof.Secret.GetBytes(), keys, proof.Id, P2PkE); } - public virtual P2PKWitness GenerateBlindWitness(BlindedMessage message, ECPrivKey[] keys, ECPubKey P2PkE) + public virtual P2PKWitness GenerateBlindWitness( + BlindedMessage message, + ECPrivKey[] keys, + ECPubKey P2PkE + ) { return GenerateBlindWitness(message.B_.Key.ToBytes(), keys, message.Id, P2PkE); } - - public virtual P2PKWitness? GenerateBlindWitness(byte[] msg, ECPrivKey[] keys, KeysetId keysetId, ECPubKey P2PkE) + + public virtual P2PKWitness? GenerateBlindWitness( + byte[] msg, + ECPrivKey[] keys, + KeysetId keysetId, + ECPubKey P2PkE + ) { var hash = SHA256.HashData(msg); return GenerateBlindWitness(ECPrivKey.Create(hash), keys, keysetId, P2PkE); } - - public virtual P2PKWitness? GenerateBlindWitness(ECPrivKey hash, ECPrivKey[] keys, KeysetId keysetId, ECPubKey P2PkE) + + public virtual P2PKWitness? GenerateBlindWitness( + ECPrivKey hash, + ECPrivKey[] keys, + KeysetId keysetId, + ECPubKey P2PkE + ) { var msg = hash.ToBytes(); - + var allowedKeys = GetAllowedPubkeys(out var requiredSignatures); var allowedRefundKeys = GetAllowedRefundPubkeys(out var requiredRefundSignatures); if (requiredRefundSignatures == 0) return null; - var (isValid, result) = TrySignBlindPath(allowedKeys.ToArray(), requiredSignatures, keys, P2PkE, msg); + var (isValid, result) = TrySignBlindPath( + allowedKeys.ToArray(), + requiredSignatures, + keys, + P2PkE, + msg + ); if (isValid) { return result; @@ -173,7 +202,13 @@ public virtual P2PKWitness GenerateBlindWitness(BlindedMessage message, ECPrivKe if (requiredRefundSignatures.HasValue && allowedRefundKeys.Any()) { - (isValid, result) = TrySignBlindPath(allowedRefundKeys.ToArray(), requiredRefundSignatures.Value, keys, P2PkE, msg); + (isValid, result) = TrySignBlindPath( + allowedRefundKeys.ToArray(), + requiredRefundSignatures.Value, + keys, + P2PkE, + msg + ); if (isValid) { return result; @@ -183,9 +218,13 @@ public virtual P2PKWitness GenerateBlindWitness(BlindedMessage message, ECPrivKe throw new InvalidOperationException("Not enough valid keys to sign any blind path"); } - - private (bool IsValid, P2PKWitness Witness) TrySignBlindPath(ECPubKey[] allowedKeys, int requiredSignatures, - ECPrivKey[] availableKeys, ECPubKey P2PkE, byte[] msg) + private (bool IsValid, P2PKWitness Witness) TrySignBlindPath( + ECPubKey[] allowedKeys, + int requiredSignatures, + ECPrivKey[] availableKeys, + ECPubKey P2PkE, + byte[] msg + ) { var allowedKeysSet = new HashSet(allowedKeys); var result = new P2PKWitness(); @@ -196,9 +235,10 @@ public virtual P2PKWitness GenerateBlindWitness(BlindedMessage message, ECPrivKe if (result.Signatures.Length >= requiredSignatures) break; - for (int i = 0; i < allowedKeys.Length; i++) + for (int i = 0; i < allowedKeys.Length; i++) { - if (usedSlots.Contains(i)) continue; + if (usedSlots.Contains(i)) + continue; var Zx = Cashu.ComputeZx(key, P2PkE); var ri = Cashu.ComputeRi(Zx, i); @@ -229,7 +269,6 @@ public virtual P2PKWitness GenerateBlindWitness(BlindedMessage message, ECPrivKe return (result.Signatures.Length >= requiredSignatures, result); } - public virtual bool VerifyWitness(string message, P2PKWitness witness) { var hash = SHA256.HashData(Encoding.UTF8.GetBytes(message)); @@ -251,8 +290,12 @@ public virtual bool VerifyWitnessHash(byte[] hash, P2PKWitness witness) { try { - var sigs = witness.Signatures - .Select(s => SecpSchnorrSignature.TryCreate(Convert.FromHexString(s), out var sig) ? sig : null) + var sigs = witness + .Signatures.Select(s => + SecpSchnorrSignature.TryCreate(Convert.FromHexString(s), out var sig) + ? sig + : null + ) .Where(signature => signature is not null) .ToArray(); @@ -262,17 +305,23 @@ public virtual bool VerifyWitnessHash(byte[] hash, P2PKWitness witness) { return true; } - + if (VerifyPath(allowedKeys.ToArray(), requiredSignatures, sigs, hash)) return true; - if (requiredRefundSignatures.HasValue && allowedRefundKeys.Any()) { - if (VerifyPath(allowedRefundKeys.ToArray(), requiredRefundSignatures.Value, sigs, hash)) + if ( + VerifyPath( + allowedRefundKeys.ToArray(), + requiredRefundSignatures.Value, + sigs, + hash + ) + ) return true; } - + return false; } catch (Exception e) @@ -280,9 +329,13 @@ public virtual bool VerifyWitnessHash(byte[] hash, P2PKWitness witness) return false; } } - - private bool VerifyPath(ECPubKey[] allowedKeys, int requiredSignatures, - SecpSchnorrSignature[] sigs, byte[] hash) + + private bool VerifyPath( + ECPubKey[] allowedKeys, + int requiredSignatures, + SecpSchnorrSignature[] sigs, + byte[] hash + ) { if (sigs.Length < requiredSignatures) { @@ -290,7 +343,7 @@ private bool VerifyPath(ECPubKey[] allowedKeys, int requiredSignatures, } var xonlyKeys = allowedKeys.Select(k => k.ToXOnlyPubKey()).ToArray(); var usedKeyIndices = new HashSet(); - + foreach (var sig in sigs) { for (int i = 0; i < xonlyKeys.Length; i++) @@ -302,8 +355,7 @@ private bool VerifyPath(ECPubKey[] allowedKeys, int requiredSignatures, } } } - + return usedKeyIndices.Count >= requiredSignatures; } - -} \ No newline at end of file +} diff --git a/DotNut/NUT11/P2PKWitness.cs b/DotNut/NUT11/P2PKWitness.cs index 49185a2..e2325ad 100644 --- a/DotNut/NUT11/P2PKWitness.cs +++ b/DotNut/NUT11/P2PKWitness.cs @@ -4,5 +4,6 @@ namespace DotNut; public class P2PKWitness { - [JsonPropertyName("signatures")] public string[] Signatures { get; set; } = Array.Empty(); -} \ No newline at end of file + [JsonPropertyName("signatures")] + public string[] Signatures { get; set; } = Array.Empty(); +} diff --git a/DotNut/NUT11/P2PkBuilder.cs b/DotNut/NUT11/P2PkBuilder.cs index 4014e7b..f13a23a 100644 --- a/DotNut/NUT11/P2PkBuilder.cs +++ b/DotNut/NUT11/P2PkBuilder.cs @@ -11,11 +11,11 @@ public class P2PkBuilder public ECPubKey[] Pubkeys { get; set; } - //SIG_INPUTS, SIG_ALL + //SIG_INPUTS, SIG_ALL public string? SigFlag { get; set; } public string? Nonce { get; set; } public int? RefundSignatureThreshold { get; set; } - + public P2PKProofSecret Build() { Validate(); @@ -35,14 +35,13 @@ public P2PKProofSecret Build() tags.Add(new[] { "locktime", Lock.Value.ToUnixTimeSeconds().ToString() }); if (RefundPubkeys?.Any() is true) { - tags.Add(new[] { "refund" }.Concat(RefundPubkeys.Select(p => p.ToHex())) - .ToArray()); + tags.Add(new[] { "refund" }.Concat(RefundPubkeys.Select(p => p.ToHex())).ToArray()); RefundSignatureThreshold ??= 1; } if (RefundSignatureThreshold is { } refundSignatureThreshold and > 1) { - tags.Add(new[] {"n_sigs_refund", refundSignatureThreshold.ToString() }); + tags.Add(new[] { "n_sigs_refund", refundSignatureThreshold.ToString() }); } } @@ -50,36 +49,47 @@ public P2PKProofSecret Build() { tags.Add(new[] { "n_sigs", SignatureThreshold.ToString() }); } - + return new P2PKProofSecret() { Data = Pubkeys.First().ToHex(), Nonce = Nonce ?? RandomNumberGenerator.GetHexString(32, true), - Tags = tags.ToArray() + Tags = tags.ToArray(), }; } - public static P2PKBuilder Load(P2PKProofSecret proofSecret) + public static P2PkBuilder Load(P2PKProofSecret proofSecret) { - var builder = new P2PKBuilder(); + var builder = new P2PkBuilder(); var primaryPubkey = proofSecret.Data.ToPubKey(); - var pubkeys = proofSecret.Tags?.FirstOrDefault(strings => strings.FirstOrDefault() == "pubkeys"); + var pubkeys = proofSecret.Tags?.FirstOrDefault(strings => + strings.FirstOrDefault() == "pubkeys" + ); if (pubkeys is not null && pubkeys.Length > 1) { - builder.Pubkeys = pubkeys.Skip(1).Select(s => s.ToPubKey()).Prepend(primaryPubkey).ToArray(); + builder.Pubkeys = pubkeys + .Skip(1) + .Select(s => s.ToPubKey()) + .Prepend(primaryPubkey) + .ToArray(); } else { builder.Pubkeys = [primaryPubkey]; } - var rawUnixTs = proofSecret.Tags?.FirstOrDefault(strings => strings.FirstOrDefault() == "locktime")?.Skip(1) + var rawUnixTs = proofSecret + .Tags?.FirstOrDefault(strings => strings.FirstOrDefault() == "locktime") + ?.Skip(1) ?.FirstOrDefault(); - builder.Lock = rawUnixTs is not null && long.TryParse(rawUnixTs, out var unixTs) - ? DateTimeOffset.FromUnixTimeSeconds(unixTs) - : null; - - var refund = proofSecret.Tags?.FirstOrDefault(strings => strings.FirstOrDefault() == "refund"); + builder.Lock = + rawUnixTs is not null && long.TryParse(rawUnixTs, out var unixTs) + ? DateTimeOffset.FromUnixTimeSeconds(unixTs) + : null; + + var refund = proofSecret.Tags?.FirstOrDefault(strings => + strings.FirstOrDefault() == "refund" + ); if (refund is not null && refund.Length > 1) { builder.RefundPubkeys = refund.Skip(1).Select(s => s.ToPubKey()).ToArray(); @@ -92,14 +102,18 @@ public static P2PKBuilder Load(P2PKProofSecret proofSecret) builder.RefundSignatureThreshold = nSigsRefundValue; } - var sigFlag = proofSecret.Tags?.FirstOrDefault(strings => strings.FirstOrDefault() == "sigflag")?.Skip(1) + var sigFlag = proofSecret + .Tags?.FirstOrDefault(strings => strings.FirstOrDefault() == "sigflag") + ?.Skip(1) ?.FirstOrDefault(); if (!string.IsNullOrEmpty(sigFlag)) { builder.SigFlag = sigFlag; } - var nSigs = proofSecret.Tags?.FirstOrDefault(strings => strings.FirstOrDefault() == "n_sigs")?.Skip(1) + var nSigs = proofSecret + .Tags?.FirstOrDefault(strings => strings.FirstOrDefault() == "n_sigs") + ?.Skip(1) ?.FirstOrDefault(); if (!string.IsNullOrEmpty(nSigs) && int.TryParse(nSigs, out var nSigsValue)) { @@ -110,27 +124,28 @@ public static P2PKBuilder Load(P2PKProofSecret proofSecret) return builder; } - + private void Validate() { if (this.Pubkeys.Count() < SignatureThreshold) { throw new ArgumentException("Signature threshold bigger than provided pubkeys count!"); } - if(this.RefundSignatureThreshold is not null - && (RefundPubkeys is null || RefundPubkeys.Length < RefundSignatureThreshold)) + if ( + this.RefundSignatureThreshold is not null + && (RefundPubkeys is null || RefundPubkeys.Length < RefundSignatureThreshold) + ) { throw new ArgumentException("Signature threshold bigger than provided pubkeys count!"); } } - - + /* * ========================= * NUT-XX Pay to blinded key * ========================= */ - + //For sig_inputs, generates random p2pk_e for each input public P2PKProofSecret BuildBlinded(out ECPubKey p2pkE) { @@ -153,7 +168,7 @@ public P2PKProofSecret BuildBlinded(ECPrivKey p2pke) BlindPubkeys(rs.ToArray()); return Build(); } - + protected void BlindPubkeys(ECPrivKey[] rs) { var expectedLength = Pubkeys.Length + (RefundPubkeys?.Length ?? 0); @@ -172,11 +187,14 @@ protected void BlindPubkeys(ECPrivKey[] rs) if (RefundPubkeys != null) { - RefundPubkeys[i - Pubkeys.Length] = Cashu.ComputeB_(RefundPubkeys[i - Pubkeys.Length], rs[i]); + RefundPubkeys[i - Pubkeys.Length] = Cashu.ComputeB_( + RefundPubkeys[i - Pubkeys.Length], + rs[i] + ); } } } - + public virtual P2PkBuilder Clone() { return new P2PkBuilder() @@ -189,4 +207,4 @@ public virtual P2PkBuilder Clone() Nonce = Nonce, }; } -} \ No newline at end of file +} diff --git a/DotNut/NUT11/SigAllHandler.cs b/DotNut/NUT11/SigAllHandler.cs index b976a11..d0ca29a 100644 --- a/DotNut/NUT11/SigAllHandler.cs +++ b/DotNut/NUT11/SigAllHandler.cs @@ -12,16 +12,21 @@ public class SigAllHandler public List BlindedMessages { get; set; } public string? HTLCPreimage { get; set; } public string? MeltQuoteId { get; set; } - - private Nut10ProofSecret? _firstProofSecret; - - + + private Nut10ProofSecret? _firstProofSecret; + public bool TrySign(out string? witness) { witness = null; - - if ( BlindedMessages is null || Proofs is null || PrivKeys is null || - BlindedMessages.Count == 0 || Proofs.Count == 0 || PrivKeys.Count == 0) + + if ( + BlindedMessages is null + || Proofs is null + || PrivKeys is null + || BlindedMessages.Count == 0 + || Proofs.Count == 0 + || PrivKeys.Count == 0 + ) { return false; } @@ -41,18 +46,19 @@ public bool TrySign(out string? witness) { return false; } - + if (_firstProofSecret is not P2PKProofSecret fps) { return false; } - + P2PKWitness witnessObj; - if (fps is HTLCProofSecret s && HTLCPreimage is {} preimage) + if (fps is HTLCProofSecret s && HTLCPreimage is { } preimage) { if (Proofs.First().P2PkE is { } E) { - witnessObj = s.GenerateBlindWitness(msg, + witnessObj = s.GenerateBlindWitness( + msg, PrivKeys.Select(pk => (ECPrivKey)pk).ToArray(), Convert.FromHexString(preimage), Proofs[0].Id, @@ -61,18 +67,23 @@ public bool TrySign(out string? witness) witness = JsonSerializer.Serialize((HTLCWitness)witnessObj); return true; } - witnessObj = - s.GenerateWitness(msg, PrivKeys.Select(pk => (ECPrivKey)pk).ToArray(), - Convert.FromHexString(preimage) - ); + witnessObj = s.GenerateWitness( + msg, + PrivKeys.Select(pk => (ECPrivKey)pk).ToArray(), + Convert.FromHexString(preimage) + ); witness = JsonSerializer.Serialize((HTLCWitness)witnessObj); return true; } - if (Proofs.First().P2PkE is { } e2) { - witnessObj = fps.GenerateBlindWitness(msg, PrivKeys.Select(pk => (ECPrivKey)pk).ToArray(), Proofs[0].Id, e2); + witnessObj = fps.GenerateBlindWitness( + msg, + PrivKeys.Select(pk => (ECPrivKey)pk).ToArray(), + Proofs[0].Id, + e2 + ); witness = JsonSerializer.Serialize(witnessObj); return true; } @@ -81,34 +92,48 @@ public bool TrySign(out string? witness) return true; } - public static string GetMessageToSign(Proof[] inputs, BlindedMessage[] outputs, string? meltQuoteId = null) + public static string GetMessageToSign( + Proof[] inputs, + BlindedMessage[] outputs, + string? meltQuoteId = null + ) { if (inputs is null || inputs.Length == 0) { - throw new ArgumentException("At least one proof is required for SIG_ALL.", nameof(inputs)); + throw new ArgumentException( + "At least one proof is required for SIG_ALL.", + nameof(inputs) + ); } if (outputs is null || outputs.Length == 0) { - throw new ArgumentException("At least one blinded output is required for SIG_ALL.", nameof(outputs)); + throw new ArgumentException( + "At least one blinded output is required for SIG_ALL.", + nameof(outputs) + ); } if (!ValidateFirstProof(inputs[0], out var firstSecret)) { throw new ArgumentException("Provided first proof is invalid"); } var msg = new StringBuilder(); - + for (var i = 0; i < inputs.Length; i++) { var p = inputs[i]; if (p.Secret is not Nut10Secret nut10) { - throw new ArgumentException("When signing sig_all, every proof must be a nut 10 secret."); + throw new ArgumentException( + "When signing sig_all, every proof must be a nut 10 secret." + ); } - + if (!CheckIfEqualToFirst(firstSecret, nut10.ProofSecret)) { - throw new ArgumentException("When signing sig_all, every proof must have identical tags and data."); + throw new ArgumentException( + "When signing sig_all, every proof must have identical tags and data." + ); } // serialize as raw object var secret = JsonSerializer.Serialize((object)p.Secret); @@ -133,7 +158,8 @@ public static bool VerifySigAllWitness( Proof[] proofs, BlindedMessage[] blindedMessages, P2PKWitness witness, - string? meltQuoteId = null) + string? meltQuoteId = null + ) { if (proofs is null || proofs.Length == 0) { @@ -148,30 +174,37 @@ public static bool VerifySigAllWitness( msg = Encoding.UTF8.GetBytes(msgStr); } - catch(Exception ex) + catch (Exception ex) { return false; } - + if (proofs[0].Secret is not Nut10Secret nut10) return false; - + return nut10.ProofSecret switch { HTLCProofSecret htlcs => htlcs.VerifyWitness(msg, witness), P2PKProofSecret p2pks => p2pks.VerifyWitness(msg, witness), - _ => false + _ => false, }; } - public static bool VerifySigAllWitness(Proof[] proofs, BlindedMessage[] blindedMessages, string? meltQuoteId = null) + public static bool VerifySigAllWitness( + Proof[] proofs, + BlindedMessage[] blindedMessages, + string? meltQuoteId = null + ) { if (proofs is null || proofs.Length == 0) { return false; } var firstProof = proofs.FirstOrDefault(); - if (firstProof?.Secret is not Nut10Secret { ProofSecret: var proofSecret } || firstProof.Witness is null) + if ( + firstProof?.Secret is not Nut10Secret { ProofSecret: var proofSecret } + || firstProof.Witness is null + ) return false; P2PKWitness? witness; @@ -191,13 +224,14 @@ public static bool VerifySigAllWitness(Proof[] proofs, BlindedMessage[] blindedM { return false; } - return witness is not null && VerifySigAllWitness(proofs, blindedMessages, witness, meltQuoteId); + return witness is not null + && VerifySigAllWitness(proofs, blindedMessages, witness, meltQuoteId); } - + private static bool ValidateFirstProof(Proof firstProof, out Nut10ProofSecret? secret) { secret = null; - + if (firstProof.Secret is not Nut10Secret nut10) { return false; @@ -208,9 +242,9 @@ private static bool ValidateFirstProof(Proof firstProof, out Nut10ProofSecret? s HTLCProofSecret htlcs => HTLCBuilder.Load(htlcs), P2PKProofSecret p2pks => P2PkBuilder.Load(p2pks), // won't throw exception if there will be a new type of nut10 secret, but will return false - _ => new P2PkBuilder(){SigFlag = null} + _ => new P2PkBuilder() { SigFlag = null }, }; - + if (builder.SigFlag != "SIG_ALL") { return false; @@ -219,12 +253,18 @@ private static bool ValidateFirstProof(Proof firstProof, out Nut10ProofSecret? s secret = nut10.ProofSecret; return true; } - + private static bool CheckIfEqualToFirst(Nut10ProofSecret first, Nut10ProofSecret other) => - first is { } a && other is { } b && - a.Data == b.Data && - ((a.Tags == null && b.Tags == null) || - (a.Tags != null && b.Tags != null && - a.Tags.Length == b.Tags.Length && - a.Tags.Zip(b.Tags).All(pair => pair.First.SequenceEqual(pair.Second)))); -} \ No newline at end of file + first is { } a + && other is { } b + && a.Data == b.Data + && ( + (a.Tags == null && b.Tags == null) + || ( + a.Tags != null + && b.Tags != null + && a.Tags.Length == b.Tags.Length + && a.Tags.Zip(b.Tags).All(pair => pair.First.SequenceEqual(pair.Second)) + ) + ); +} diff --git a/DotNut/NUT12/DLEQ.cs b/DotNut/NUT12/DLEQ.cs index 9c7b973..0039a92 100644 --- a/DotNut/NUT12/DLEQ.cs +++ b/DotNut/NUT12/DLEQ.cs @@ -4,6 +4,9 @@ namespace DotNut; public class DLEQ { - [JsonPropertyName("e")] public PrivKey E { get; set; } - [JsonPropertyName("s")] public PrivKey S { get; set; } -} \ No newline at end of file + [JsonPropertyName("e")] + public PrivKey E { get; set; } + + [JsonPropertyName("s")] + public PrivKey S { get; set; } +} diff --git a/DotNut/NUT12/DLEQProof.cs b/DotNut/NUT12/DLEQProof.cs index a569e12..5936d5b 100644 --- a/DotNut/NUT12/DLEQProof.cs +++ b/DotNut/NUT12/DLEQProof.cs @@ -2,7 +2,8 @@ namespace DotNut; -public class DLEQProof: DLEQ +public class DLEQProof : DLEQ { - [JsonPropertyName("r")] public PrivKey R { get; set; } -} \ No newline at end of file + [JsonPropertyName("r")] + public PrivKey R { get; set; } +} diff --git a/DotNut/NUT13/BIP32.cs b/DotNut/NUT13/BIP32.cs index dea15f5..1d9ae64 100644 --- a/DotNut/NUT13/BIP32.cs +++ b/DotNut/NUT13/BIP32.cs @@ -8,15 +8,12 @@ namespace DotNut.NUT13; public class BIP32 : IHdKeyAlgo { - public static readonly IHdKeyAlgo Instance = new BIP32(); private static readonly byte[] CurveBytes = "Bitcoin seed"u8.ToArray(); private static readonly BigInteger N = Cashu.N; - private BIP32() - { - } + private BIP32() { } public HdKey GetMasterKeyFromSeed(ReadOnlySpan seed) { @@ -26,7 +23,8 @@ public HdKey GetMasterKeyFromSeed(ReadOnlySpan seed) HMACSHA512.HashData(CurveBytes, seedCopy, seedCopy); var key = seedCopy[..32]; var keyInt = new BigInteger(key, true, true); - if (keyInt > N || keyInt.IsZero) continue; + if (keyInt > N || keyInt.IsZero) + continue; return new HdKey(key, seedCopy[32..]); } } @@ -36,15 +34,15 @@ public HdKey Derive(HdKey parent, KeyPathElement index) Span hash = index.Hardened ? IHdKeyAlgo.Bip32Hash(parent.ChainCode, index, 0x00, parent.PrivateKey) : IHdKeyAlgo.Bip32Hash(parent.ChainCode, index, GetPublic(parent.PrivateKey)); - - var parentKey = new BigInteger (parent.PrivateKey, true, true); + + var parentKey = new BigInteger(parent.PrivateKey, true, true); while (true) { var key = hash[..32]; var cc = hash[32..]; key.Reverse(); - var keyInt = new BigInteger (key, true); + var keyInt = new BigInteger(key, true); var res = BigInteger.Add(keyInt, parentKey) % N; if (keyInt > N || res.IsZero) @@ -66,6 +64,6 @@ public HdKey Derive(HdKey parent, KeyPathElement index) public byte[] GetPublic(ReadOnlySpan privateKey) { - return ECPrivKey.Create(privateKey).CreatePubKey().ToBytes(); + return ECPrivKey.Create(privateKey).CreatePubKey().ToBytes(); } -} \ No newline at end of file +} diff --git a/DotNut/NUT13/Nut13.cs b/DotNut/NUT13/Nut13.cs index a47278f..4dd7b6f 100644 --- a/DotNut/NUT13/Nut13.cs +++ b/DotNut/NUT13/Nut13.cs @@ -7,16 +7,24 @@ namespace DotNut.NUT13; public static class Nut13 { - public static byte[] DeriveBlindingFactor(this Mnemonic mnemonic, KeysetId keysetId, uint counter) => - DeriveBlindingFactor(mnemonic.DeriveSeed(), keysetId, counter); - + public static byte[] DeriveBlindingFactor( + this Mnemonic mnemonic, + KeysetId keysetId, + uint counter + ) => DeriveBlindingFactor(mnemonic.DeriveSeed(), keysetId, counter); - public static StringSecret DeriveSecret(this Mnemonic mnemonic, KeysetId keysetId, uint counter) => - DeriveSecret(mnemonic.DeriveSeed(), keysetId, counter); - + public static StringSecret DeriveSecret( + this Mnemonic mnemonic, + KeysetId keysetId, + uint counter + ) => DeriveSecret(mnemonic.DeriveSeed(), keysetId, counter); - public static List DeriveOutputs(this Mnemonic mnemonic, IEnumerable amounts, KeysetId keysetId, - uint counter) + public static List DeriveOutputs( + this Mnemonic mnemonic, + IEnumerable amounts, + KeysetId keysetId, + uint counter + ) { var outputs = new List(); @@ -25,35 +33,37 @@ public static List DeriveOutputs(this Mnemonic mnemonic, IEnumerable for (uint i = 0; i < amountList.Count; i++) { var secret = DeriveSecret(mnemonic, keysetId, counter + i); - var r = new PrivKey( - DeriveBlindingFactor(mnemonic, keysetId, counter + i) - ); + var r = new PrivKey(DeriveBlindingFactor(mnemonic, keysetId, counter + i)); var Y = secret.ToCurve(); var B_ = Cashu.ComputeB_(Y, r); - outputs.Add(new OutputData() - { - BlindedMessage = new BlindedMessage() + outputs.Add( + new OutputData() { - Amount = amountList[(int)i], - Id = keysetId, - B_ = B_ - }, - Secret = secret, - BlindingFactor = r - }); + BlindedMessage = new BlindedMessage() + { + Amount = amountList[(int)i], + Id = keysetId, + B_ = B_, + }, + Secret = secret, + BlindingFactor = r, + } + ); } return outputs; } + public static byte[] DeriveBlindingFactor(this byte[] seed, KeysetId keysetId, uint counter) { switch (keysetId.GetVersion()) { case 0x00: - return BIP32.Instance.DerivePath(GetNut13DerivationPath(keysetId, counter, false), seed).PrivateKey - .ToArray(); + return BIP32 + .Instance.DerivePath(GetNut13DerivationPath(keysetId, counter, false), seed) + .PrivateKey.ToArray(); case 0x01: { return DeriveHmac(seed, keysetId, counter, false); @@ -62,12 +72,15 @@ public static byte[] DeriveBlindingFactor(this byte[] seed, KeysetId keysetId, u throw new ArgumentException("Invalid keyset id prefix"); } } + public static StringSecret DeriveSecret(this byte[] seed, KeysetId keysetId, uint counter) { switch (keysetId.GetVersion()) { case 0x00: - var key = BIP32.Instance.DerivePath(GetNut13DerivationPath(keysetId, counter, true), seed).PrivateKey; + var key = BIP32 + .Instance.DerivePath(GetNut13DerivationPath(keysetId, counter, true), seed) + .PrivateKey; return new StringSecret(Convert.ToHexString(key).ToLower()); case 0x01: { @@ -77,9 +90,8 @@ public static StringSecret DeriveSecret(this byte[] seed, KeysetId keysetId, uin default: throw new ArgumentException("Invalid keyset id prefix"); } - } - + public static byte[] DeriveHmac(byte[] seed, KeysetId keysetId, uint counter, bool secretOrr) { byte[] counterBuffer = BitConverter.GetBytes((ulong)counter); @@ -87,26 +99,31 @@ public static byte[] DeriveHmac(byte[] seed, KeysetId keysetId, uint counter, bo { Array.Reverse(counterBuffer); } - - var message = "Cashu_KDF_HMAC_SHA256"u8.ToArray() + + var message = "Cashu_KDF_HMAC_SHA256"u8 + .ToArray() .Concat(Convert.FromHexString(keysetId.ToString())) .Concat(counterBuffer) .Append(secretOrr ? (byte)0x00 : (byte)0x01); - + using var hmac = new HMACSHA256(seed); return hmac.ComputeHash(message.ToArray()); } - + public const string Purpose = "129372'"; + public static KeyPath GetNut13DerivationPath(KeysetId keysetId, uint counter, bool secretOrr) - { - return (KeyPath) KeyPath.Parse($"m/{Purpose}/0'/{GetKeysetIdInt(keysetId)}'/{counter}'/{(secretOrr?0:1)}")!; + { + return (KeyPath) + KeyPath.Parse( + $"m/{Purpose}/0'/{GetKeysetIdInt(keysetId)}'/{counter}'/{(secretOrr ? 0 : 1)}" + )!; } - + public static long GetKeysetIdInt(KeysetId keysetId) { var keysetIdInt = long.Parse("0" + keysetId, System.Globalization.NumberStyles.HexNumber); var mod = (long)Math.Pow(2, 31) - 1; return keysetIdInt % mod; } -} \ No newline at end of file +} diff --git a/DotNut/NUT14/HTLCBuilder.cs b/DotNut/NUT14/HTLCBuilder.cs index 1f94a06..4f428ad 100644 --- a/DotNut/NUT14/HTLCBuilder.cs +++ b/DotNut/NUT14/HTLCBuilder.cs @@ -10,26 +10,29 @@ public class HTLCBuilder : P2PkBuilder /* * ugly hack to reuse P2PkBuilder for HTLCs. * P2PkBuilder expects a pubkey in `data` field, but we need to store a hashlock instead - * + * * we inject a dummy pubkey so the loader doesn’t break, then remove it after load/build. */ private static readonly PubKey _dummy = "020000000000000000000000000000000000000000000000000000000000000001".ToPubKey(); - + public static HTLCBuilder Load(HTLCProofSecret proofSecret) { var hashLock = proofSecret.Data; if (hashLock.Length != 64) // hex string { - throw new ArgumentException("HashLock must be 32 bytes (64 chars hex)", nameof(HashLock)); + throw new ArgumentException( + "HashLock must be 32 bytes (64 chars hex)", + nameof(HashLock) + ); } var tempProof = new P2PKProofSecret { Data = _dummy.ToString(), Nonce = proofSecret.Nonce, - Tags = proofSecret.Tags + Tags = proofSecret.Tags, }; - + var innerbuilder = P2PkBuilder.Load(tempProof); innerbuilder.Pubkeys = innerbuilder.Pubkeys.Except([_dummy.Key]).ToArray(); return new HTLCBuilder() @@ -40,16 +43,18 @@ public static HTLCBuilder Load(HTLCProofSecret proofSecret) RefundPubkeys = innerbuilder.RefundPubkeys, SignatureThreshold = innerbuilder.SignatureThreshold, SigFlag = innerbuilder.SigFlag, - Nonce = innerbuilder.Nonce + Nonce = innerbuilder.Nonce, }; - } - + public new HTLCProofSecret Build() { if (HashLock.Length != 64) { - throw new ArgumentException("HashLock must be 32 bytes (64 chars hex)", nameof(HashLock)); + throw new ArgumentException( + "HashLock must be 32 bytes (64 chars hex)", + nameof(HashLock) + ); } var innerBuilder = new P2PkBuilder() { @@ -58,16 +63,16 @@ public static HTLCBuilder Load(HTLCProofSecret proofSecret) RefundPubkeys = RefundPubkeys, SignatureThreshold = SignatureThreshold, SigFlag = SigFlag, - Nonce = Nonce + Nonce = Nonce, }; innerBuilder.Pubkeys = innerBuilder.Pubkeys.Prepend(_dummy.Key).ToArray(); - + var p2pkProof = innerBuilder.Build(); return new HTLCProofSecret() { Data = HashLock, Nonce = p2pkProof.Nonce, - Tags = p2pkProof.Tags + Tags = p2pkProof.Tags, }; } @@ -82,11 +87,11 @@ public static HTLCBuilder Load(HTLCProofSecret proofSecret) { var pubkeys = RefundPubkeys != null ? Pubkeys.Concat(RefundPubkeys).ToArray() : Pubkeys; var rs = new List(); - + var keysetIdBytes = keysetId.GetBytes(); var e = p2pke; - + for (int i = 0; i < pubkeys.Length; i++) { var Zx = Cashu.ComputeZx(e, pubkeys[i]); @@ -96,7 +101,7 @@ public static HTLCBuilder Load(HTLCProofSecret proofSecret) BlindPubkeys(rs.ToArray()); return Build(); } - + public override HTLCBuilder Clone() { return new HTLCBuilder() @@ -110,4 +115,4 @@ public override HTLCBuilder Clone() Nonce = Nonce, }; } -} \ No newline at end of file +} diff --git a/DotNut/NUT14/HTLCProofSecret.cs b/DotNut/NUT14/HTLCProofSecret.cs index 19ded8f..34e52e2 100644 --- a/DotNut/NUT14/HTLCProofSecret.cs +++ b/DotNut/NUT14/HTLCProofSecret.cs @@ -8,7 +8,9 @@ namespace DotNut; public class HTLCProofSecret : P2PKProofSecret { public const string Key = "HTLC"; - [JsonIgnore] public override HTLCBuilder Builder => HTLCBuilder.Load(this); + + [JsonIgnore] + public override HTLCBuilder Builder => HTLCBuilder.Load(this); public override ECPubKey[] GetAllowedPubkeys(out int requiredSignatures) { @@ -16,15 +18,23 @@ public override ECPubKey[] GetAllowedPubkeys(out int requiredSignatures) requiredSignatures = builder.SignatureThreshold; return builder.Pubkeys; } - + public HTLCWitness GenerateWitness(Proof proof, ECPrivKey[] keys, string preimage) { return GenerateWitness(proof.Secret.GetBytes(), keys, Convert.FromHexString(preimage)); } - public HTLCWitness GenerateWitness(BlindedMessage blindedMessage, ECPrivKey[] keys, string preimage) + public HTLCWitness GenerateWitness( + BlindedMessage blindedMessage, + ECPrivKey[] keys, + string preimage + ) { - return GenerateWitness(blindedMessage.B_.Key.ToBytes(), keys, Convert.FromHexString(preimage)); + return GenerateWitness( + blindedMessage.B_.Key.ToBytes(), + keys, + Convert.FromHexString(preimage) + ); } public HTLCWitness GenerateWitness(byte[] msg, ECPrivKey[] keys, byte[] preimage) @@ -35,10 +45,12 @@ public HTLCWitness GenerateWitness(byte[] msg, ECPrivKey[] keys, byte[] preimage public HTLCWitness GenerateWitness(ECPrivKey hash, ECPrivKey[] keys, byte[] preimage) { - // validate hash only if secret is not expired. + // validate hash only if secret is not expired. var builder = Builder; - if (!builder.Lock.HasValue || - builder.Lock.Value.ToUnixTimeSeconds() > DateTimeOffset.Now.ToUnixTimeSeconds()) + if ( + !builder.Lock.HasValue + || builder.Lock.Value.ToUnixTimeSeconds() > DateTimeOffset.Now.ToUnixTimeSeconds() + ) { if (!VerifyPreimage(preimage)) throw new InvalidOperationException("Invalid preimage"); @@ -47,39 +59,73 @@ public HTLCWitness GenerateWitness(ECPrivKey hash, ECPrivKey[] keys, byte[] prei return new HTLCWitness() { Signatures = witness.Signatures, - Preimage = Convert.ToHexString(preimage) + Preimage = Convert.ToHexString(preimage), }; } - - public HTLCWitness GenerateBlindWitness(Proof proof, ECPrivKey[] keys, string preimage) { ArgumentNullException.ThrowIfNull(proof.P2PkE); return GenerateBlindWitness(proof, keys, preimage, proof.P2PkE); } - public HTLCWitness GenerateBlindWitness(Proof proof, ECPrivKey[] keys, string preimage, ECPubKey P2PkE) + + public HTLCWitness GenerateBlindWitness( + Proof proof, + ECPrivKey[] keys, + string preimage, + ECPubKey P2PkE + ) { - return GenerateBlindWitness(proof.Secret.GetBytes(), keys, Convert.FromHexString(preimage), proof.Id, P2PkE); + return GenerateBlindWitness( + proof.Secret.GetBytes(), + keys, + Convert.FromHexString(preimage), + proof.Id, + P2PkE + ); } - public HTLCWitness GenerateBlindWitness(BlindedMessage message, ECPrivKey[] keys, string preimage, ECPubKey P2PkE) + public HTLCWitness GenerateBlindWitness( + BlindedMessage message, + ECPrivKey[] keys, + string preimage, + ECPubKey P2PkE + ) { - return GenerateBlindWitness(message.B_.Key.ToBytes(), keys, Convert.FromHexString(preimage), message.Id, P2PkE); + return GenerateBlindWitness( + message.B_.Key.ToBytes(), + keys, + Convert.FromHexString(preimage), + message.Id, + P2PkE + ); } - public HTLCWitness GenerateBlindWitness(byte[] msg, ECPrivKey[] keys, byte[] preimage, KeysetId keysetId, - ECPubKey P2PkE) + public HTLCWitness GenerateBlindWitness( + byte[] msg, + ECPrivKey[] keys, + byte[] preimage, + KeysetId keysetId, + ECPubKey P2PkE + ) { var hash = SHA256.HashData(msg); return GenerateBlindWitness(ECPrivKey.Create(hash), keys, preimage, keysetId, P2PkE); } - - public HTLCWitness GenerateBlindWitness(ECPrivKey hash, ECPrivKey[] keys, byte[] preimage, KeysetId keysetId, - ECPubKey P2PkE) + + public HTLCWitness GenerateBlindWitness( + ECPrivKey hash, + ECPrivKey[] keys, + byte[] preimage, + KeysetId keysetId, + ECPubKey P2PkE + ) { var builder = Builder; - if (!builder.Lock.HasValue || builder.Lock.Value.ToUnixTimeSeconds() > DateTimeOffset.Now.ToUnixTimeSeconds()) + if ( + !builder.Lock.HasValue + || builder.Lock.Value.ToUnixTimeSeconds() > DateTimeOffset.Now.ToUnixTimeSeconds() + ) { if (!VerifyPreimage(preimage)) throw new InvalidOperationException("Invalid preimage"); @@ -88,15 +134,15 @@ public HTLCWitness GenerateBlindWitness(ECPrivKey hash, ECPrivKey[] keys, byte[] return new HTLCWitness() { Signatures = witness.Signatures, - Preimage = Convert.ToHexString(preimage) + Preimage = Convert.ToHexString(preimage), }; } - - - + public bool VerifyPreimage(string preimage) { - return Convert.FromHexString(Builder.HashLock).SequenceEqual(SHA256.HashData(Convert.FromHexString(preimage))); + return Convert + .FromHexString(Builder.HashLock) + .SequenceEqual(SHA256.HashData(Convert.FromHexString(preimage))); } public bool VerifyPreimage(byte[] preimage) @@ -112,66 +158,100 @@ public bool VerifyWitness(string message, HTLCWitness witness) public bool VerifyWitness(ISecret secret, HTLCWitness witness) { - if (secret is not Nut10Secret {ProofSecret: HTLCProofSecret}) + if (secret is not Nut10Secret { ProofSecret: HTLCProofSecret }) { return false; } return VerifyWitness(secret.GetBytes(), witness); } - [Obsolete("Use GenerateWitness(Proof proof, ECPrivKey[] keys, string preimage)")] public override P2PKWitness GenerateWitness(Proof proof, ECPrivKey[] keys) { - throw new InvalidOperationException("Use GenerateWitness(Proof proof, ECPrivKey[] keys, string preimage)"); + throw new InvalidOperationException( + "Use GenerateWitness(Proof proof, ECPrivKey[] keys, string preimage)" + ); } [Obsolete("Use GenerateWitness(byte[] msg, ECPrivKey[] keys, byte[] preimage)")] public override P2PKWitness GenerateWitness(BlindedMessage message, ECPrivKey[] keys) { - throw new InvalidOperationException("Use GenerateWitness(BlindedMessage message, ECPrivKey[] keys, string preimage)"); + throw new InvalidOperationException( + "Use GenerateWitness(BlindedMessage message, ECPrivKey[] keys, string preimage)" + ); } [Obsolete("Use GenerateWitness(byte[] msg, ECPrivKey[] keys, byte[] preimage)")] public override P2PKWitness GenerateWitness(byte[] msg, ECPrivKey[] keys) { - throw new InvalidOperationException("Use GenerateWitness(byte[] msg, ECPrivKey[] keys, byte[] preimage)"); + throw new InvalidOperationException( + "Use GenerateWitness(byte[] msg, ECPrivKey[] keys, byte[] preimage)" + ); } - - [Obsolete("Use GenerateBlindWitness(Proof proof, ECPrivKey[] keys, byte[] preimage, KeysetId keysetId)")] + [Obsolete( + "Use GenerateBlindWitness(Proof proof, ECPrivKey[] keys, byte[] preimage, KeysetId keysetId)" + )] public override P2PKWitness GenerateBlindWitness(Proof proof, ECPrivKey[] keys) { throw new InvalidOperationException(); } - - [Obsolete("Use GenerateBlindWitness(Proof proof, ECPrivKey[] keys, byte[] preimage, KeysetId keysetId, ECPubKey P2PkE)")] + + [Obsolete( + "Use GenerateBlindWitness(Proof proof, ECPrivKey[] keys, byte[] preimage, KeysetId keysetId, ECPubKey P2PkE)" + )] public override P2PKWitness GenerateBlindWitness(Proof proof, ECPrivKey[] keys, ECPubKey P2PkE) { - throw new InvalidOperationException("Use GenerateBlindWitness(Proof proof, ECPrivKey[] keys, byte[] preimage, KeysetId keysetId, ECPubKey P2PkE)"); + throw new InvalidOperationException( + "Use GenerateBlindWitness(Proof proof, ECPrivKey[] keys, byte[] preimage, KeysetId keysetId, ECPubKey P2PkE)" + ); } - - [Obsolete("Use GenerateBlindWitness(BlindedMessage message, ECPrivKey[] keys, byte[] preimage, KeysetId keysetId, ECPubKey P2PkE)")] - public override P2PKWitness GenerateBlindWitness(BlindedMessage message, ECPrivKey[] keys, ECPubKey P2PkE) + + [Obsolete( + "Use GenerateBlindWitness(BlindedMessage message, ECPrivKey[] keys, byte[] preimage, KeysetId keysetId, ECPubKey P2PkE)" + )] + public override P2PKWitness GenerateBlindWitness( + BlindedMessage message, + ECPrivKey[] keys, + ECPubKey P2PkE + ) { - throw new InvalidOperationException("Use GenerateBlindWitness(BlindedMessage message, ECPrivKey[] keys, byte[] preimage, KeysetId keysetId, ECPubKey P2PkE)"); + throw new InvalidOperationException( + "Use GenerateBlindWitness(BlindedMessage message, ECPrivKey[] keys, byte[] preimage, KeysetId keysetId, ECPubKey P2PkE)" + ); } - - [Obsolete("Use GenerateBlindWitness(byte[] msg, ECPrivKey[] keys, byte[] preimage, KeysetId keysetId, ECPubKey P2PkE)")] - public override P2PKWitness GenerateBlindWitness(byte[] msg, ECPrivKey[] keys, KeysetId keysetId, ECPubKey P2PkE) + + [Obsolete( + "Use GenerateBlindWitness(byte[] msg, ECPrivKey[] keys, byte[] preimage, KeysetId keysetId, ECPubKey P2PkE)" + )] + public override P2PKWitness GenerateBlindWitness( + byte[] msg, + ECPrivKey[] keys, + KeysetId keysetId, + ECPubKey P2PkE + ) { - throw new InvalidOperationException("Use GenerateBlindWitness(byte[] msg, ECPrivKey[] keys, byte[] preimage, KeysetId keysetId, ECPubKey P2PkE)"); + throw new InvalidOperationException( + "Use GenerateBlindWitness(byte[] msg, ECPrivKey[] keys, byte[] preimage, KeysetId keysetId, ECPubKey P2PkE)" + ); } - - [Obsolete("Use GenerateBlindWitness(ECPrivKey hash, ECPrivKey[] keys, byte[] preimage, KeysetId keysetId, ECPubKey P2PkE)")] - public override P2PKWitness GenerateBlindWitness(ECPrivKey hash, ECPrivKey[] keys, KeysetId keysetId, - ECPubKey P2PkE) + + [Obsolete( + "Use GenerateBlindWitness(ECPrivKey hash, ECPrivKey[] keys, byte[] preimage, KeysetId keysetId, ECPubKey P2PkE)" + )] + public override P2PKWitness GenerateBlindWitness( + ECPrivKey hash, + ECPrivKey[] keys, + KeysetId keysetId, + ECPubKey P2PkE + ) { - throw new InvalidOperationException("Use GenerateBlindWitness(ECPrivKey hash, ECPrivKey[] keys, byte[] preimage, KeysetId keysetId, ECPubKey P2PkE)"); + throw new InvalidOperationException( + "Use GenerateBlindWitness(ECPrivKey hash, ECPrivKey[] keys, byte[] preimage, KeysetId keysetId, ECPubKey P2PkE)" + ); } - public override P2PKWitness GenerateWitness(ECPrivKey hash, ECPrivKey[] keys) { return base.GenerateWitness(hash, keys); @@ -191,6 +271,7 @@ public override bool VerifyWitness(byte[] message, P2PKWitness witness) { return base.VerifyWitness(message, witness); } + public override bool VerifyWitnessHash(byte[] hash, P2PKWitness witness) { if (witness is not HTLCWitness htlcWitness) @@ -198,10 +279,13 @@ public override bool VerifyWitnessHash(byte[] hash, P2PKWitness witness) return false; } var builder = Builder; - if (builder.Lock.HasValue && builder.Lock.Value.ToUnixTimeSeconds() <= DateTimeOffset.Now.ToUnixTimeSeconds()) + if ( + builder.Lock.HasValue + && builder.Lock.Value.ToUnixTimeSeconds() <= DateTimeOffset.Now.ToUnixTimeSeconds() + ) { return base.VerifyWitnessHash(hash, witness); } return VerifyPreimage(htlcWitness.Preimage) && base.VerifyWitnessHash(hash, witness); } -} \ No newline at end of file +} diff --git a/DotNut/NUT14/HTLCWitness.cs b/DotNut/NUT14/HTLCWitness.cs index 46fb6cd..63e6ae5 100644 --- a/DotNut/NUT14/HTLCWitness.cs +++ b/DotNut/NUT14/HTLCWitness.cs @@ -2,10 +2,10 @@ namespace DotNut; -public class HTLCWitness: P2PKWitness +public class HTLCWitness : P2PKWitness { // this field is nullable now, because after locktime expiry only signatures are needed. [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - [JsonPropertyName("preimage")] - public string? Preimage { get; set; } -} \ No newline at end of file + [JsonPropertyName("preimage")] + public string? Preimage { get; set; } +} diff --git a/DotNut/NUT15/MultipathPaymentSetting.cs b/DotNut/NUT15/MultipathPaymentSetting.cs index 90134a3..7530421 100644 --- a/DotNut/NUT15/MultipathPaymentSetting.cs +++ b/DotNut/NUT15/MultipathPaymentSetting.cs @@ -2,9 +2,14 @@ namespace DotNut; -public class MultipathPaymentSetting +public class MultipathPaymentSetting { - [JsonPropertyName("method")] public string Method { get; set; } - [JsonPropertyName("unit")] public List Unit { get; set; } - [JsonPropertyName("mpp")] public bool MultiPathPayments { get; set; } -} \ No newline at end of file + [JsonPropertyName("method")] + public string Method { get; set; } + + [JsonPropertyName("unit")] + public List Unit { get; set; } + + [JsonPropertyName("mpp")] + public bool MultiPathPayments { get; set; } +} diff --git a/DotNut/NUT18/HttpPaymentRequestInterfaceHandler.cs b/DotNut/NUT18/HttpPaymentRequestInterfaceHandler.cs index 2dafc54..435f27d 100644 --- a/DotNut/NUT18/HttpPaymentRequestInterfaceHandler.cs +++ b/DotNut/NUT18/HttpPaymentRequestInterfaceHandler.cs @@ -10,16 +10,20 @@ public HttpPaymentRequestInterfaceHandler(HttpClient? httpClient) { _httpClient = httpClient ?? new HttpClient(); } + public bool CanHandle(PaymentRequest request) { return request.Transports.Any(t => t.Type == "post"); } - public async Task SendPayment(PaymentRequest request, PaymentRequestPayload payload, - CancellationToken cancellationToken = default) - { + public async Task SendPayment( + PaymentRequest request, + PaymentRequestPayload payload, + CancellationToken cancellationToken = default + ) + { var endpoint = new Uri(request.Transports.First(t => t.Type == "post").Target); - var response = await _httpClient.PostAsJsonAsync(endpoint, payload,cancellationToken); + var response = await _httpClient.PostAsJsonAsync(endpoint, payload, cancellationToken); response.EnsureSuccessStatusCode(); } -} \ No newline at end of file +} diff --git a/DotNut/NUT18/Nut10LockingCondition.cs b/DotNut/NUT18/Nut10LockingCondition.cs index ba89386..70d52d4 100644 --- a/DotNut/NUT18/Nut10LockingCondition.cs +++ b/DotNut/NUT18/Nut10LockingCondition.cs @@ -5,4 +5,4 @@ public class Nut10LockingCondition public string Kind { get; set; } public string Data { get; set; } public Tag[]? Tags { get; set; } -} \ No newline at end of file +} diff --git a/DotNut/NUT18/PaymentRequest.cs b/DotNut/NUT18/PaymentRequest.cs index 526c178..ca22387 100644 --- a/DotNut/NUT18/PaymentRequest.cs +++ b/DotNut/NUT18/PaymentRequest.cs @@ -40,4 +40,4 @@ public static PaymentRequest Parse(string creq) throw new FormatException("Invalid payment request"); } -} \ No newline at end of file +} diff --git a/DotNut/NUT18/PaymentRequestInterfaceHandler.cs b/DotNut/NUT18/PaymentRequestInterfaceHandler.cs index 27ed535..4a44e9a 100644 --- a/DotNut/NUT18/PaymentRequestInterfaceHandler.cs +++ b/DotNut/NUT18/PaymentRequestInterfaceHandler.cs @@ -3,5 +3,9 @@ public interface PaymentRequestInterfaceHandler { bool CanHandle(PaymentRequest request); - Task SendPayment(PaymentRequest request, PaymentRequestPayload payload, CancellationToken cancellationToken = default); -} \ No newline at end of file + Task SendPayment( + PaymentRequest request, + PaymentRequestPayload payload, + CancellationToken cancellationToken = default + ); +} diff --git a/DotNut/NUT18/PaymentRequestPayload.cs b/DotNut/NUT18/PaymentRequestPayload.cs index 65c9e3c..b8747d4 100644 --- a/DotNut/NUT18/PaymentRequestPayload.cs +++ b/DotNut/NUT18/PaymentRequestPayload.cs @@ -4,10 +4,19 @@ namespace DotNut; public class PaymentRequestPayload { - [JsonPropertyName("id")] public string PaymentId { get; set; } + [JsonPropertyName("id")] + public string PaymentId { get; set; } + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - [JsonPropertyName("memo")] public string? Memo { get; set; } - [JsonPropertyName("mint")] public string Mint { get; set; } - [JsonPropertyName("unit")] public string Unit { get; set; } - [JsonPropertyName("proofs")] public Proof[] Proofs { get; set; } -} \ No newline at end of file + [JsonPropertyName("memo")] + public string? Memo { get; set; } + + [JsonPropertyName("mint")] + public string Mint { get; set; } + + [JsonPropertyName("unit")] + public string Unit { get; set; } + + [JsonPropertyName("proofs")] + public Proof[] Proofs { get; set; } +} diff --git a/DotNut/NUT18/PaymentRequestTransport.cs b/DotNut/NUT18/PaymentRequestTransport.cs index d1c3475..822ba35 100644 --- a/DotNut/NUT18/PaymentRequestTransport.cs +++ b/DotNut/NUT18/PaymentRequestTransport.cs @@ -5,4 +5,4 @@ public class PaymentRequestTransport public string Type { get; set; } public string Target { get; set; } public Tag[]? Tags { get; set; } -} \ No newline at end of file +} diff --git a/DotNut/NUT18/PaymentRequestTransportInitiator.cs b/DotNut/NUT18/PaymentRequestTransportInitiator.cs index a4ab7ad..259bd5a 100644 --- a/DotNut/NUT18/PaymentRequestTransportInitiator.cs +++ b/DotNut/NUT18/PaymentRequestTransportInitiator.cs @@ -5,7 +5,9 @@ namespace DotNut; public class PaymentRequestTransportInitiator { private readonly IEnumerable _handlers; - public static ConcurrentBag Handlers { get; } = [ new HttpPaymentRequestInterfaceHandler(null) ]; + public static ConcurrentBag Handlers { get; } = + [new HttpPaymentRequestInterfaceHandler(null)]; + public PaymentRequestTransportInitiator(IEnumerable handlers) { _handlers = handlers; @@ -15,4 +17,4 @@ public PaymentRequestTransportInitiator() { _handlers = Handlers.ToArray(); } -} \ No newline at end of file +} diff --git a/DotNut/NUT18/PaymentRequestTransportTag.cs b/DotNut/NUT18/PaymentRequestTransportTag.cs index d3ed4ad..7bbab00 100644 --- a/DotNut/NUT18/PaymentRequestTransportTag.cs +++ b/DotNut/NUT18/PaymentRequestTransportTag.cs @@ -4,4 +4,4 @@ public class PaymentRequestTransportTag { public string Key { get; set; } public string Value { get; set; } -} \ No newline at end of file +} diff --git a/DotNut/NUT20/MintQuoteSigner.cs b/DotNut/NUT20/MintQuoteSigner.cs index 630e68e..693a717 100644 --- a/DotNut/NUT20/MintQuoteSigner.cs +++ b/DotNut/NUT20/MintQuoteSigner.cs @@ -5,8 +5,12 @@ namespace DotNut; public static class MintQuoteSigner { - public static string SignMintQuote(this PrivKey pk, string quote, List blindedMessages) - { + public static string SignMintQuote( + this PrivKey pk, + string quote, + List blindedMessages + ) + { var sb = new StringBuilder(); sb.Append(quote); foreach (var blindedMessage in blindedMessages) @@ -14,7 +18,7 @@ public static string SignMintQuote(this PrivKey pk, string quote, List Date: Sun, 11 Jan 2026 23:28:32 +0100 Subject: [PATCH 49/70] fix coderabbit stuff --- DotNut.Tests/UnitTests2.cs | 1 - .../Handlers/MeltHandlerBolt11.cs | 4 +-- .../Abstractions/Interfaces/IWalletBuilder.cs | 2 +- DotNut/Abstractions/MintInfo.cs | 2 +- DotNut/Abstractions/MintQuoteBuilder.cs | 4 +++ DotNut/Abstractions/RestoreBuilder.cs | 7 ++++- DotNut/Abstractions/Wallet.cs | 27 +++++++++++++++---- DotNut/NUT11/P2PKProofSecret.cs | 13 +++++++-- 8 files changed, 47 insertions(+), 13 deletions(-) diff --git a/DotNut.Tests/UnitTests2.cs b/DotNut.Tests/UnitTests2.cs index 2677df7..e374aa8 100644 --- a/DotNut.Tests/UnitTests2.cs +++ b/DotNut.Tests/UnitTests2.cs @@ -69,7 +69,6 @@ public void BuilderChainingPreservesAllSettings() Assert.Equal(mnemonic, mnemonicRef.ToString()); Assert.Same(counter, counterRef); - Assert.NotNull(wallet.GetInfo()); } [Fact] diff --git a/DotNut/Abstractions/Handlers/MeltHandlerBolt11.cs b/DotNut/Abstractions/Handlers/MeltHandlerBolt11.cs index 25146ae..8c2e884 100644 --- a/DotNut/Abstractions/Handlers/MeltHandlerBolt11.cs +++ b/DotNut/Abstractions/Handlers/MeltHandlerBolt11.cs @@ -24,7 +24,7 @@ public async Task> Melt(List inputs, CancellationToken ct = d htlcPreimage, quote.Quote ); - //since nut10 (with p2bk) is processed, now it's safe to strip P2PkE + //since nut10 (with p2bk) is aleady processed, now it's safe to strip P2PkE proofs.ForEach(i => i.StripFingerprints()); var client = await wallet.GetMintApi(ct); @@ -44,7 +44,7 @@ public async Task> Melt(List inputs, CancellationToken ct = d { return []; } - + var keyset = await wallet.GetKeys(res.Change.First().Id, true, false, ct); return Utils.ConstructProofsFromPromises(res.Change.ToList(), blankOutputs, keyset.Keys); } diff --git a/DotNut/Abstractions/Interfaces/IWalletBuilder.cs b/DotNut/Abstractions/Interfaces/IWalletBuilder.cs index 61e2a3a..8688a1a 100644 --- a/DotNut/Abstractions/Interfaces/IWalletBuilder.cs +++ b/DotNut/Abstractions/Interfaces/IWalletBuilder.cs @@ -14,7 +14,7 @@ public interface IWalletBuilder : IDisposable /// Mandatory. Sets a mint in a wallet object /// /// Mint API object. - IWalletBuilder WithMint(ICashuApi mintApi); + IWalletBuilder WithMint(ICashuApi mintApi, bool canDispose = false); /// /// Mandatory. Sets a mint in a wallet object (with default CashuHttpClient) diff --git a/DotNut/Abstractions/MintInfo.cs b/DotNut/Abstractions/MintInfo.cs index 4029fed..1062d36 100644 --- a/DotNut/Abstractions/MintInfo.cs +++ b/DotNut/Abstractions/MintInfo.cs @@ -269,7 +269,7 @@ internal class ProtectedEndpoints internal class ProtectedEndpoint { public string Method { get; set; } = string.Empty; - public System.Text.RegularExpressions.Regex Regex { get; init; } + public System.Text.RegularExpressions.Regex Regex { get; set; } } public class WebSocketSupportResult diff --git a/DotNut/Abstractions/MintQuoteBuilder.cs b/DotNut/Abstractions/MintQuoteBuilder.cs index 9f22d0f..5f85470 100644 --- a/DotNut/Abstractions/MintQuoteBuilder.cs +++ b/DotNut/Abstractions/MintQuoteBuilder.cs @@ -63,6 +63,10 @@ public IMintQuoteBuilder WithKeyset(KeysetId keysetId) public IMintQuoteBuilder WithOutputs(List outputs) { this._outputs = outputs; + if (outputs.Any(o => o.BlindedMessage.Id != outputs[0].BlindedMessage.Id)) + { + throw new ArgumentException("Every output must have the same keyset id!"); + } return this; } diff --git a/DotNut/Abstractions/RestoreBuilder.cs b/DotNut/Abstractions/RestoreBuilder.cs index 57d3897..ba0799d 100644 --- a/DotNut/Abstractions/RestoreBuilder.cs +++ b/DotNut/Abstractions/RestoreBuilder.cs @@ -203,7 +203,12 @@ CancellationToken ct foreach (var output in res.Outputs) { // there can't be any dupes here - returnedOutputs.Add(outputs.Single(o => Equals(o.BlindedMessage.B_, output.B_))); + var matchingOutputs = outputs.SingleOrDefault(o => Equals(o.BlindedMessage.B_, output.B_)); + if (matchingOutputs == null) + { + throw new InvalidOperationException("Invalid outputs returned by mint!"); + } + returnedOutputs.Add(matchingOutputs); } var proofs = Utils.ConstructProofsFromPromises( diff --git a/DotNut/Abstractions/Wallet.cs b/DotNut/Abstractions/Wallet.cs index 9033591..701a540 100644 --- a/DotNut/Abstractions/Wallet.cs +++ b/DotNut/Abstractions/Wallet.cs @@ -14,8 +14,8 @@ public class Wallet : IWalletBuilder private MintInfo? _info; private IProofSelector? _selector; private ICashuApi? _mintApi; - private List? _keysets; - private List? _keys; + private List _keysets; + private List _keys; private Dictionary? _keysetFees => _keysets?.ToDictionary(k => k.Id, k => k.InputFee ?? 0); private Mnemonic? _mnemonic; @@ -28,15 +28,17 @@ public class Wallet : IWalletBuilder private DateTime? _lastSync = DateTime.MinValue; private TimeSpan? _syncThreshold; // if null sync only once private bool _shouldBumpCounter = true; + private bool _ownsHttpClient = false; /* * Fluent Builder Methods */ public static IWalletBuilder Create() => new Wallet(); - public IWalletBuilder WithMint(ICashuApi mintApi) + public IWalletBuilder WithMint(ICashuApi mintApi, bool canDispose = false) { _mintApi = mintApi; + _ownsHttpClient = canDispose; return this; } @@ -44,6 +46,7 @@ public IWalletBuilder WithMint(string mintUrl) { var httpClient = new HttpClient { BaseAddress = new Uri(mintUrl) }; _mintApi = new CashuHttpClient(httpClient, true); + _ownsHttpClient = true; return this; } @@ -51,6 +54,7 @@ public IWalletBuilder WithMint(Uri mintUri) { var httpClient = new HttpClient { BaseAddress = mintUri }; _mintApi = new CashuHttpClient(httpClient, true); + _ownsHttpClient = true; return this; } @@ -257,6 +261,12 @@ public void InvalidateCache() if (keyset != null && _keys != null) { _keys.Add(keyset); + return keyset; + } + + if (keyset != null) + { + return keyset; } } @@ -300,7 +310,11 @@ public async Task> CreateOutputs( "No Keys found. Make sure to fetch them!" ); } - var keyset = this._keys.Single(k => k.Id == id); + var keyset = this._keys.SingleOrDefault(k => k.Id == id); + if (keyset == null) + { + throw new ArgumentNullException(nameof(keyset), $"No matching keys for id {id}"); + } if (this._mnemonic == null) { return Utils.CreateOutputs(amounts, id, keyset.Keys); @@ -555,6 +569,9 @@ internal async Task _maybeSyncKeys(CancellationToken cts = default) public void Dispose() { - _mintApi?.Dispose(); + if (_ownsHttpClient) + { + _mintApi?.Dispose(); + } } } diff --git a/DotNut/NUT11/P2PKProofSecret.cs b/DotNut/NUT11/P2PKProofSecret.cs index a722369..ef069eb 100644 --- a/DotNut/NUT11/P2PKProofSecret.cs +++ b/DotNut/NUT11/P2PKProofSecret.cs @@ -132,8 +132,17 @@ public virtual bool VerifyWitness(Proof proof) { return false; } - var witness = JsonSerializer.Deserialize(proof.Witness) ?? new P2PKWitness(); - return VerifyWitness(proof.Secret, witness); + + try + { + var witness = JsonSerializer.Deserialize(proof.Witness) ?? new P2PKWitness(); + return VerifyWitness(proof.Secret, witness); + } + catch + { + return false; + } + } /* From f08e7098025567fe990ac290dc7a3180aadc9f63 Mon Sep 17 00:00:00 2001 From: d4rp4t Date: Mon, 12 Jan 2026 14:52:07 +0100 Subject: [PATCH 50/70] fixes --- .../Handlers/MeltHandlerBolt11.cs | 18 +++- .../Handlers/MeltHandlerBolt12.cs | 16 +++- .../Abstractions/Interfaces/IWalletBuilder.cs | 11 ++- .../Interfaces/IWebsocketService.cs | 8 +- DotNut/Abstractions/RestoreBuilder.cs | 13 +-- DotNut/Abstractions/Wallet.cs | 93 ++++++++++--------- .../Abstractions/Websockets/Subscription.cs | 57 ++++++++++-- .../Websockets/WebsocketConnection.cs | 10 +- .../Websockets/WebsocketService.cs | 67 ++++++++----- DotNut/ApiModels/GetKeysResponse.cs | 23 +++-- DotNut/NUT11/P2PKProofSecret.cs | 4 +- 11 files changed, 216 insertions(+), 104 deletions(-) diff --git a/DotNut/Abstractions/Handlers/MeltHandlerBolt11.cs b/DotNut/Abstractions/Handlers/MeltHandlerBolt11.cs index 8c2e884..1101ae4 100644 --- a/DotNut/Abstractions/Handlers/MeltHandlerBolt11.cs +++ b/DotNut/Abstractions/Handlers/MeltHandlerBolt11.cs @@ -44,8 +44,20 @@ public async Task> Melt(List inputs, CancellationToken ct = d { return []; } - - var keyset = await wallet.GetKeys(res.Change.First().Id, true, false, ct); - return Utils.ConstructProofsFromPromises(res.Change.ToList(), blankOutputs, keyset.Keys); + + var keysetIds = res.Change.Select(sig => sig.Id).Distinct().ToList(); + var changeProofs = new List(); + foreach (var keysetId in keysetIds) + { + var keyset = await wallet.GetKeys(keysetId, true, false, ct); + if (keyset == null) + { + continue; + } + changeProofs.AddRange( + Utils.ConstructProofsFromPromises(res.Change.ToList(), blankOutputs, keyset.Keys) + ); + } + return changeProofs; } } diff --git a/DotNut/Abstractions/Handlers/MeltHandlerBolt12.cs b/DotNut/Abstractions/Handlers/MeltHandlerBolt12.cs index e5e0cef..a7442d5 100644 --- a/DotNut/Abstractions/Handlers/MeltHandlerBolt12.cs +++ b/DotNut/Abstractions/Handlers/MeltHandlerBolt12.cs @@ -45,7 +45,19 @@ public async Task> Melt(List inputs, CancellationToken ct = d return []; } - var keyset = await wallet.GetKeys(res.Change.First().Id, true, false, ct); - return Utils.ConstructProofsFromPromises(res.Change.ToList(), blankOutputs, keyset.Keys); + var keysetIds = res.Change.Select(sig => sig.Id).Distinct().ToList(); + var changeProofs = new List(); + foreach (var keysetId in keysetIds) + { + var keyset = await wallet.GetKeys(keysetId, true, false, ct); + if (keyset == null) + { + continue; + } + changeProofs.AddRange( + Utils.ConstructProofsFromPromises(res.Change.ToList(), blankOutputs, keyset.Keys) + ); + } + return changeProofs; } } diff --git a/DotNut/Abstractions/Interfaces/IWalletBuilder.cs b/DotNut/Abstractions/Interfaces/IWalletBuilder.cs index 8688a1a..e8731fc 100644 --- a/DotNut/Abstractions/Interfaces/IWalletBuilder.cs +++ b/DotNut/Abstractions/Interfaces/IWalletBuilder.cs @@ -1,7 +1,6 @@ using DotNut.Api; using DotNut.ApiModels; using DotNut.NBitcoin.BIP39; -using NBitcoin.Secp256k1; namespace DotNut.Abstractions; @@ -181,13 +180,17 @@ Task> CreateOutputs( /// Active keysetId Task GetActiveKeysetId(string unit, CancellationToken ct = default); + /// + /// Get all keysets with units + /// + /// Dictionary of (unit, KeysetId) + Task>> GetKeysetIdsWithUnits(CancellationToken ct = default); + /// /// Get active keyset ids for each supported unit /// /// Dictionary of (unit, KeysetId) - Task?> GetActiveKeysetIdsWithUnits( - CancellationToken ct = default - ); + Task> GetActiveKeysetIdsWithUnits(CancellationToken ct = default); Task GetMintApi(CancellationToken ct = default); diff --git a/DotNut/Abstractions/Interfaces/IWebsocketService.cs b/DotNut/Abstractions/Interfaces/IWebsocketService.cs index 77b55cb..57e3b32 100644 --- a/DotNut/Abstractions/Interfaces/IWebsocketService.cs +++ b/DotNut/Abstractions/Interfaces/IWebsocketService.cs @@ -12,10 +12,10 @@ public interface IWebsocketService : IAsyncDisposable Task LazyConnectAsync(string mintUrl, CancellationToken ct = default); - Task DisconnectAsync(string connectionId, CancellationToken ct = default); + Task DisconnectAsync(string mintUrl, CancellationToken ct = default); Task SubscribeAsync( - string connectionId, + string mintUrl, SubscriptionKind kind, string[] filters, CancellationToken ct = default @@ -23,9 +23,9 @@ Task SubscribeAsync( Task UnsubscribeAsync(string subId, CancellationToken ct = default); - WebSocketState GetConnectionState(string connectionId); + WebSocketState GetConnectionState(string mintUrl); - IEnumerable GetSubscriptions(string connectionId); + IEnumerable GetSubscriptions(string mintUrl); IEnumerable GetConnections(); } diff --git a/DotNut/Abstractions/RestoreBuilder.cs b/DotNut/Abstractions/RestoreBuilder.cs index ba0799d..f95f92a 100644 --- a/DotNut/Abstractions/RestoreBuilder.cs +++ b/DotNut/Abstractions/RestoreBuilder.cs @@ -9,8 +9,8 @@ public class RestoreBuilder : IRestoreBuilder { private readonly Wallet _wallet; private List? _specifiedKeysets; - private static uint BATCH_SIZE = 100; - private static uint EMPTY_BATCHES_ALLOWED = 3; + private const uint BATCH_SIZE = 100; + private const uint EMPTY_BATCHES_ALLOWED = 3; public RestoreBuilder(Wallet wallet) { @@ -124,8 +124,7 @@ public async Task> ProcessAsync(CancellationToken ct = defaul private static List CreateBatch( Mnemonic mnemonic, KeysetId keysetId, - int batchNumber, - CancellationToken ct + int batchNumber ) { if (batchNumber < 0) @@ -168,7 +167,7 @@ CancellationToken ct while (emptyBatchesRemaining > 0) { // create batch of 100, and request restore for whole batch - var outputs = CreateBatch(mnemonic, keysetId, (int)batchNumber, ct); + var outputs = CreateBatch(mnemonic, keysetId, (int)batchNumber); var req = new PostRestoreRequest { Outputs = outputs.Select(o => o.BlindedMessage).ToArray(), @@ -203,7 +202,9 @@ CancellationToken ct foreach (var output in res.Outputs) { // there can't be any dupes here - var matchingOutputs = outputs.SingleOrDefault(o => Equals(o.BlindedMessage.B_, output.B_)); + var matchingOutputs = outputs.SingleOrDefault(o => + Equals(o.BlindedMessage.B_, output.B_) + ); if (matchingOutputs == null) { throw new InvalidOperationException("Invalid outputs returned by mint!"); diff --git a/DotNut/Abstractions/Wallet.cs b/DotNut/Abstractions/Wallet.cs index 701a540..766f41b 100644 --- a/DotNut/Abstractions/Wallet.cs +++ b/DotNut/Abstractions/Wallet.cs @@ -1,7 +1,6 @@ using DotNut.Api; using DotNut.ApiModels; using DotNut.NBitcoin.BIP39; -using NBitcoin.Secp256k1; namespace DotNut.Abstractions; @@ -14,10 +13,10 @@ public class Wallet : IWalletBuilder private MintInfo? _info; private IProofSelector? _selector; private ICashuApi? _mintApi; - private List _keysets; - private List _keys; - private Dictionary? _keysetFees => - _keysets?.ToDictionary(k => k.Id, k => k.InputFee ?? 0); + private List _keysets = []; + private List _keys = []; + private Dictionary _keysetFees => + _keysets.ToDictionary(k => k.Id, k => k.InputFee ?? 0); private Mnemonic? _mnemonic; private ICounter? _counter; @@ -25,7 +24,7 @@ public class Wallet : IWalletBuilder //flags private bool _shouldSyncKeyset = true; - private DateTime? _lastSync = DateTime.MinValue; + private DateTime _lastSync = DateTime.MinValue; private TimeSpan? _syncThreshold; // if null sync only once private bool _shouldBumpCounter = true; private bool _ownsHttpClient = false; @@ -197,28 +196,28 @@ public void InvalidateCache() { await _maybeSyncKeys(ct); return _keysets - ?.OrderBy(k => k.InputFee) + .OrderBy(k => k.InputFee) .FirstOrDefault(k => k is { Active: true } && k.Unit == unit, null) ?.Id; } - public async Task>?> GetKeysetIdsWithUnits( + public async Task>> GetKeysetIdsWithUnits( CancellationToken ct = default ) { await _maybeSyncKeys(ct); return _keysets - ?.GroupBy(k => k.Unit) + .GroupBy(k => k.Unit) .ToDictionary(g => g.Key, g => g.OrderBy(k => k.InputFee).Select(k => k.Id).ToList()); } - public async Task?> GetActiveKeysetIdsWithUnits( + public async Task> GetActiveKeysetIdsWithUnits( CancellationToken ct = default ) { await _maybeSyncKeys(ct); return _keysets - ?.Where(k => k.Active) + .Where(k => k.Active) .GroupBy(k => k.Unit) .ToDictionary(g => g.Key, g => g.OrderBy(k => k.InputFee).First().Id); } @@ -231,10 +230,10 @@ public void InvalidateCache() if (forceRefresh) { this._keys = await _fetchKeys(ct); - return this._keys ?? []; + return this._keys; } await _maybeSyncKeys(ct); - return this._keys ?? []; + return this._keys; } public async Task GetKeys( @@ -249,28 +248,24 @@ public void InvalidateCache() return await _fetchKeys(id, ct); } - var localKeyset = this._keys?.SingleOrDefault(k => k.Id == id); + var localKeyset = this._keys.SingleOrDefault(k => k.Id == id); if (localKeyset != null) { return localKeyset; } - if (allowFetch) + if (!allowFetch) { - var keyset = await _fetchKeys(id, ct); - if (keyset != null && _keys != null) - { - _keys.Add(keyset); - return keyset; - } + return null; + } - if (keyset != null) - { - return keyset; - } + var keyset = await _fetchKeys(id, ct); + if (keyset != null) + { + _keys.Add(keyset); } - throw new ArgumentException("No keys found for this keyset!"); + return keyset; } public async Task> GetKeysets( @@ -281,10 +276,10 @@ public void InvalidateCache() if (forceRefresh) { this._keysets = await _fetchKeysets(ct); - return _keysets ?? []; + return _keysets; } await _maybeSyncKeys(ct); - return _keysets ?? []; + return _keysets; } public async Task GetInfo(bool forceRefresh = false, CancellationToken ct = default) @@ -303,11 +298,11 @@ public async Task> CreateOutputs( ) { await _maybeSyncKeys(ct); - if (this._keys == null) + if (this._keys.Count == 0) { - throw new ArgumentNullException( - nameof(this._keys), - "No Keys found. Make sure to fetch them!" + throw new ArgumentException( + "No Keys found. Make sure to fetch them!", + nameof(this._keys) ); } var keyset = this._keys.SingleOrDefault(k => k.Id == id); @@ -362,7 +357,10 @@ public async Task SelectProofsToSend( if (this._selector == null) { await _maybeSyncKeys(ct); - ArgumentNullException.ThrowIfNull(this._keysetFees); + if (this._keysetFees.Count == 0) + { + throw new ArgumentException("No keyset fees found", nameof(this._keysetFees)); + } this._selector = new ProofSelector(this._keysetFees); } @@ -380,7 +378,10 @@ public async Task GetSelector(CancellationToken ct = default) if (this._selector == null) { await _maybeSyncKeys(ct); - ArgumentNullException.ThrowIfNull(this._keysetFees); + if (this._keysetFees.Count == 0) + { + throw new ArgumentException("No keyset fees found", nameof(this._keysetFees)); + } this._selector = new ProofSelector(this._keysetFees); } return this._selector; @@ -447,6 +448,7 @@ internal void _ensureApiConnected(string? msg = null) var keysRaw = await _mintApi!.GetKeys(ct); foreach (var keysetItemResponse in keysRaw.Keysets) { + //todo new derivation var isKeysetIdValid = keysetItemResponse.Keys.VerifyKeysetId( keysetItemResponse.Id, keysetItemResponse.Unit, @@ -471,15 +473,16 @@ internal void _ensureApiConnected(string? msg = null) /// May be thrown if mint is not set. private async Task _fetchKeys( KeysetId id, - CancellationToken cts = default + CancellationToken ct = default ) { _ensureApiConnected("Can't fetch keys without mint api!"); - var keysRaw = (await _mintApi!.GetKeys(id, cts)).Keysets.SingleOrDefault(); + var keysRaw = (await _mintApi!.GetKeys(id, ct)).Keysets.SingleOrDefault(); if (keysRaw == null) { return null; } + //todo new keysetId derivation var isKeysetIdValid = keysRaw.Keys.VerifyKeysetId( keysRaw.Id, keysRaw.Unit, @@ -528,19 +531,18 @@ internal async Task _maybeSyncKeys(CancellationToken cts = default) { return; } - // should sync keysets SINGLE time in the lifespan of object. If already synced - return; - if (_syncThreshold == null && _lastSync != DateTime.MinValue) - { - return; - } - // should sync keysets in some timepsan - if (_syncThreshold != null && _lastSync + _syncThreshold >= DateTime.UtcNow) + + switch (_syncThreshold) { - return; + // should sync keysets SINGLE time in the lifespan of object. If already synced - return; + case null when _lastSync != DateTime.MinValue: + // should sync keysets in some timepsan + case { } threshold when _lastSync + threshold >= DateTime.UtcNow: + return; } this._keysets = await _fetchKeysets(cts); - if (_keys == null) + if (_keys.Count == 0) { this._keys = await _fetchKeys(cts); // we're fetching all keys here, so no need for additional check. return; @@ -557,7 +559,6 @@ internal async Task _maybeSyncKeys(CancellationToken cts = default) foreach (var unknownKeyset in unknownKeysets) { var keyset = await this._fetchKeys(unknownKeyset.Id, cts); - _lastSync = DateTime.UtcNow; if (keyset != null) { _keys.Add(keyset); diff --git a/DotNut/Abstractions/Websockets/Subscription.cs b/DotNut/Abstractions/Websockets/Subscription.cs index 6e9e5a1..f9a8904 100644 --- a/DotNut/Abstractions/Websockets/Subscription.cs +++ b/DotNut/Abstractions/Websockets/Subscription.cs @@ -1,19 +1,24 @@ +using System.Runtime.CompilerServices; using System.Threading.Channels; namespace DotNut.Abstractions.Websockets; -public class Subscription +public class Subscription : IAsyncDisposable { - public string Id { get; set; } = string.Empty; - public string ConnectionId { get; set; } = string.Empty; - public SubscriptionKind Kind { get; set; } - public string[] Filters { get; set; } = Array.Empty(); - public DateTime CreatedAt { get; set; } = DateTime.UtcNow; - public Channel NotificationChannel { get; set; } = + public string Id { get; init; } = string.Empty; + public string ConnectionId { get; init; } = string.Empty; + public SubscriptionKind Kind { get; init; } + public string[] Filters { get; init; } = Array.Empty(); + public DateTime CreatedAt { get; init; } = DateTime.UtcNow; + public Channel NotificationChannel { get; init; } = Channel.CreateUnbounded(); - public EventHandler? OnError { get; set; } + /// + /// Indicates whether the subscription is still active (channel not completed). + /// + public bool IsActive => !_isClosed; + private volatile bool _isClosed; private readonly WeakReference? _serviceRef; public Subscription(IWebsocketService? websocketService = null) @@ -24,12 +29,48 @@ public Subscription(IWebsocketService? websocketService = null) : null; } + /// + /// Reads all notifications as an async stream. Completes when the subscription is closed. + /// + public async IAsyncEnumerable ReadAllAsync( + [EnumeratorCancellation] CancellationToken ct = default + ) + { + await foreach (var msg in NotificationChannel.Reader.ReadAllAsync(ct)) + { + yield return msg; + } + } + + /// + /// Closes the subscription and sends unsubscribe request to the server. + /// public async Task CloseAsync() { + if (_isClosed) + return; + _isClosed = true; + NotificationChannel.Writer.TryComplete(); if (_serviceRef != null && _serviceRef.TryGetTarget(out var service)) { await service.UnsubscribeAsync(Id); } } + + /// + /// Internal close - only closes the channel without server notification. + /// Used when connection is already closed or during cleanup. + /// + internal Task CloseInternalAsync() + { + _isClosed = true; + NotificationChannel.Writer.TryComplete(); + return Task.CompletedTask; + } + + public async ValueTask DisposeAsync() + { + await CloseAsync(); + } } diff --git a/DotNut/Abstractions/Websockets/WebsocketConnection.cs b/DotNut/Abstractions/Websockets/WebsocketConnection.cs index e998382..8c721bc 100644 --- a/DotNut/Abstractions/Websockets/WebsocketConnection.cs +++ b/DotNut/Abstractions/Websockets/WebsocketConnection.cs @@ -2,13 +2,14 @@ namespace DotNut.Abstractions.Websockets; -public class WebsocketConnection +public class WebsocketConnection : IDisposable { public string Id { get; set; } = string.Empty; public string MintUrl { get; set; } = string.Empty; public ClientWebSocket WebSocket { get; set; } = new(); public WebSocketState State { get; set; } public DateTime ConnectedAt { get; set; } = DateTime.UtcNow; + public CancellationTokenSource? CancellationTokenSource { get; set; } public bool Equals(WebsocketConnection? other) { @@ -38,4 +39,11 @@ public override int GetHashCode() { return !object.Equals(left, right); } + + public void Dispose() + { + CancellationTokenSource?.Cancel(); + CancellationTokenSource?.Dispose(); + WebSocket?.Dispose(); + } } diff --git a/DotNut/Abstractions/Websockets/WebsocketService.cs b/DotNut/Abstractions/Websockets/WebsocketService.cs index eb1df88..f90b18f 100644 --- a/DotNut/Abstractions/Websockets/WebsocketService.cs +++ b/DotNut/Abstractions/Websockets/WebsocketService.cs @@ -28,12 +28,15 @@ public async Task ConnectAsync( var wsUrl = GetWebSocketUrl(mintUrl); var clientWebSocket = new ClientWebSocket(); + var connectionCts = new CancellationTokenSource(); + try { await clientWebSocket.ConnectAsync(new Uri(wsUrl), ct); } catch (Exception ex) { + connectionCts.Dispose(); clientWebSocket.Dispose(); throw; } @@ -44,12 +47,23 @@ public async Task ConnectAsync( MintUrl = normalized, WebSocket = clientWebSocket, State = WebSocketState.Open, + CancellationTokenSource = connectionCts, }; _connections[normalized] = connection; OnConnectionStateChanged(connectionId, WebSocketState.Open); - _ = Task.Run(async () => await ListenForMessages(connection, CancellationToken.None)); + try + { + _ = Task.Run(async () => await ListenForMessages(connection, connectionCts.Token)); + } + catch + { + _connections.TryRemove(normalized, out _); + connectionCts.Dispose(); + clientWebSocket.Dispose(); + throw; + } return connection; } @@ -68,7 +82,10 @@ public async Task LazyConnectAsync( return existing; } } - _connections.TryRemove(normalized, out _); + if (_connections.TryRemove(normalized, out var oldConnection)) + { + oldConnection?.Dispose(); + } return await ConnectAsync(mintUrl, ct); } @@ -99,6 +116,8 @@ await connection.WebSocket.CloseAsync( finally { connection.State = WebSocketState.Closed; + connection.CancellationTokenSource?.Cancel(); + connection.CancellationTokenSource?.Dispose(); connection.WebSocket.Dispose(); _connections.TryRemove(normalized, out _); @@ -111,7 +130,7 @@ await connection.WebSocket.CloseAsync( { if (_subscriptions.TryRemove(subId, out var removedSub)) { - await removedSub.CloseAsync(); + await removedSub.CloseInternalAsync(); } } @@ -190,7 +209,7 @@ public async Task SubscribeAsync( if (completedTask != tcs.Task) { _subscriptions.TryRemove(subId, out _); - await subscription.CloseAsync(); + await subscription.CloseInternalAsync(); throw new TimeoutException("Subscription request timed out"); } @@ -199,12 +218,18 @@ public async Task SubscribeAsync( if (result is RequestResult.Failure failure) { _subscriptions.TryRemove(subId, out _); - await subscription.CloseAsync(); + await subscription.CloseInternalAsync(); throw new InvalidOperationException($"Subscription failed: {failure.Message}"); } return subscription; } + catch + { + subscription.NotificationChannel.Writer.TryComplete(); + _subscriptions.TryRemove(subId, out _); + throw; + } finally { _pendingRequests.TryRemove(requestId, out _); @@ -213,16 +238,16 @@ public async Task SubscribeAsync( public async Task UnsubscribeAsync(string subId, CancellationToken ct = default) { - if (!_subscriptions.TryGetValue(subId, out var subscription)) - throw new InvalidOperationException($"Subscription {subId} not found"); + if (!_subscriptions.TryRemove(subId, out var subscription)) + return; // Already unsubscribed - var connection = _connections.Values.FirstOrDefault(c => c.Id == subscription.ConnectionId); - if (connection is null) - throw new InvalidOperationException($"Connection for subscription {subId} not found"); + subscription.NotificationChannel.Writer.TryComplete(); - if (connection.State != WebSocketState.Open) + var connection = _connections.Values.FirstOrDefault(c => c.Id == subscription.ConnectionId); + if (connection is null || connection.State != WebSocketState.Open) { - throw new InvalidOperationException($"Connection is not open"); + // Connection gone or closed - local cleanup is sufficient + return; } var requestId = _getNextRequestId(); @@ -249,7 +274,8 @@ public async Task UnsubscribeAsync(string subId, CancellationToken ct = default) if (completed != tcs.Task) { - throw new TimeoutException("Unsubscribe request timed out"); + // Timeout - local cleanup already done, just log or ignore + return; } await tcs.Task.ConfigureAwait(false); @@ -257,25 +283,20 @@ public async Task UnsubscribeAsync(string subId, CancellationToken ct = default) finally { _pendingRequests.TryRemove(requestId, out _); - _subscriptions.TryRemove(subId, out _); } } public async ValueTask DisposeAsync() { - foreach (var sub in _subscriptions.Values) + var mintUrls = _connections.Keys.ToList(); + foreach (var mintUrl in mintUrls) { try { - await sub.CloseAsync(); + await DisconnectAsync(mintUrl); } catch { } } - var mintUrls = _connections.Keys.ToList(); - foreach (var mintUrl in mintUrls) - { - await DisconnectAsync(mintUrl); - } _subscriptions.Clear(); _connections.Clear(); _pendingRequests.Clear(); @@ -321,6 +342,7 @@ private async Task ListenForMessages(WebsocketConnection connection, Cancellatio if (result.MessageType == WebSocketMessageType.Close) { connection.State = WebSocketState.Closed; + connection.CancellationTokenSource?.Cancel(); OnConnectionStateChanged(connection.Id, WebSocketState.Closed); break; } @@ -344,6 +366,7 @@ private async Task ListenForMessages(WebsocketConnection connection, Cancellatio catch (Exception ex) { connection.State = WebSocketState.Aborted; + connection.CancellationTokenSource?.Cancel(); OnConnectionStateChanged(connection.Id, WebSocketState.Aborted); } finally @@ -357,7 +380,7 @@ private async Task ListenForMessages(WebsocketConnection connection, Cancellatio { try { - await sub.CloseAsync(); + await sub.CloseInternalAsync(); } catch { } _subscriptions.TryRemove(sub.Id, out _); diff --git a/DotNut/ApiModels/GetKeysResponse.cs b/DotNut/ApiModels/GetKeysResponse.cs index abd9d8e..242a799 100644 --- a/DotNut/ApiModels/GetKeysResponse.cs +++ b/DotNut/ApiModels/GetKeysResponse.cs @@ -9,13 +9,24 @@ public class GetKeysResponse public class KeysetItemResponse { - [JsonPropertyName("id")] public KeysetId Id { get; set; } - [JsonPropertyName("unit")] public string Unit { get; set; } - [JsonPropertyName("active")] public bool? Active { get; set; } // nullable until wider adoption + [JsonPropertyName("id")] + public KeysetId Id { get; set; } + + [JsonPropertyName("unit")] + public string Unit { get; set; } + + [JsonPropertyName("active")] + public bool? Active { get; set; } // nullable until wider adoption + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - [JsonPropertyName("input_fee_ppk")] public ulong? InputFeePpk { get; set; } + [JsonPropertyName("input_fee_ppk")] + public ulong? InputFeePpk { get; set; } + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - [JsonPropertyName("final_expiry")] public ulong? FinalExpiry { get; set; } - [JsonPropertyName("keys")] public Keyset Keys { get; set; } + [JsonPropertyName("final_expiry")] + public ulong? FinalExpiry { get; set; } + + [JsonPropertyName("keys")] + public Keyset Keys { get; set; } } } diff --git a/DotNut/NUT11/P2PKProofSecret.cs b/DotNut/NUT11/P2PKProofSecret.cs index ef069eb..b193e07 100644 --- a/DotNut/NUT11/P2PKProofSecret.cs +++ b/DotNut/NUT11/P2PKProofSecret.cs @@ -135,14 +135,14 @@ public virtual bool VerifyWitness(Proof proof) try { - var witness = JsonSerializer.Deserialize(proof.Witness) ?? new P2PKWitness(); + var witness = + JsonSerializer.Deserialize(proof.Witness) ?? new P2PKWitness(); return VerifyWitness(proof.Secret, witness); } catch { return false; } - } /* From 11a2330e70639df2a0d923edcfe3f5c966ab612c Mon Sep 17 00:00:00 2001 From: d4rp4t Date: Fri, 16 Jan 2026 00:38:22 +0100 Subject: [PATCH 51/70] add CheckMeltQuote to ICashuApi --- DotNut/Api/ICashuApi.cs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/DotNut/Api/ICashuApi.cs b/DotNut/Api/ICashuApi.cs index 734ca4a..fbca063 100644 --- a/DotNut/Api/ICashuApi.cs +++ b/DotNut/Api/ICashuApi.cs @@ -31,6 +31,12 @@ Task Melt( CancellationToken cancellationToken = default ); + public Task CheckMeltQuote( + string method, + string quoteId, + CancellationToken cancellationToken = default + ); + Task CheckMintQuote( string method, string quoteId, From af0234bd03abb0650958123a59c360b5c97db084 Mon Sep 17 00:00:00 2001 From: d4rp4t Date: Sun, 18 Jan 2026 14:53:00 +0100 Subject: [PATCH 52/70] throw if dleq proof is null --- DotNut/Abstractions/SwapBuilder.cs | 6 ++++++ DotNut/NUT00/Proof.cs | 1 - 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/DotNut/Abstractions/SwapBuilder.cs b/DotNut/Abstractions/SwapBuilder.cs index df05042..89f9c9d 100644 --- a/DotNut/Abstractions/SwapBuilder.cs +++ b/DotNut/Abstractions/SwapBuilder.cs @@ -144,10 +144,15 @@ await _wallet.GetActiveKeysetId(this._unit, ct) { throw new InvalidOperationException($"Can't find keys for keyset {_targetKeysetId}"); } + if (_verifyDleq) { foreach (var proof in swapInputs) { + if (proof.DLEQ == null) + { + throw new ArgumentNullException(nameof(proof.DLEQ), "Can't verify non-existent DLEQ proof!"); + } // proof may be already inactive - make sure to fetch var keyset = await _wallet.GetKeys(proof.Id, true, false, ct); if (keyset == null) @@ -156,6 +161,7 @@ await _wallet.GetActiveKeysetId(this._unit, ct) $"Can't find keys for keyset id ${proof.Id}" ); } + if (!keyset.Keys.TryGetValue(proof.Amount, out var key)) { throw new InvalidOperationException( diff --git a/DotNut/NUT00/Proof.cs b/DotNut/NUT00/Proof.cs index c2148de..b433c20 100644 --- a/DotNut/NUT00/Proof.cs +++ b/DotNut/NUT00/Proof.cs @@ -1,5 +1,4 @@ using System.Text.Json.Serialization; -using DotNut.JsonConverters; namespace DotNut; From 82cb804979d4d3cd1d2ca1681a92b56d6f6f7804 Mon Sep 17 00:00:00 2001 From: d4rp4t Date: Thu, 22 Jan 2026 16:56:05 +0100 Subject: [PATCH 53/70] rename P2PKBuilder.cs --- DotNut.Tests/Integration.cs | 14 +++++++------- DotNut.Tests/UnitTest1.cs | 4 ++-- DotNut.Tests/UnitTests2.cs | 4 ++-- .../Abstractions/Interfaces/IMintQuoteBuilder.cs | 2 +- DotNut/Abstractions/Interfaces/ISwapBuilder.cs | 2 +- DotNut/Abstractions/MintQuoteBuilder.cs | 4 ++-- DotNut/Abstractions/SwapBuilder.cs | 4 ++-- DotNut/Abstractions/Utils.cs | 6 +++--- DotNut/NUT11/{P2PkBuilder.cs => P2PKBuilder.cs} | 10 +++++----- DotNut/NUT11/P2PKProofSecret.cs | 2 +- DotNut/NUT11/SigAllHandler.cs | 4 ++-- DotNut/NUT14/HTLCBuilder.cs | 6 +++--- 12 files changed, 31 insertions(+), 31 deletions(-) rename DotNut/NUT11/{P2PkBuilder.cs => P2PKBuilder.cs} (96%) diff --git a/DotNut.Tests/Integration.cs b/DotNut.Tests/Integration.cs index 0268304..2a0bf36 100644 --- a/DotNut.Tests/Integration.cs +++ b/DotNut.Tests/Integration.cs @@ -422,7 +422,7 @@ public async Task SwapP2Pk() .CreateMintQuote() .WithAmount(1337) .WithP2PkLock( - new P2PkBuilder() + new P2PKBuilder() { Pubkeys = [privKeyBob.Key.CreatePubKey()], SignatureThreshold = 1, @@ -458,7 +458,7 @@ public async Task MintMeltP2PkMultisig() .CreateMintQuote() .WithAmount(1337) .WithP2PkLock( - new P2PkBuilder() + new P2PKBuilder() { Pubkeys = [privKeyBob.Key.CreatePubKey(), privKeyAlice.Key.CreatePubKey()], SignatureThreshold = 2, @@ -508,7 +508,7 @@ public async Task MintSwapP2PkSigAll() .CreateMintQuote() .WithAmount(1337) .WithP2PkLock( - new P2PkBuilder() + new P2PKBuilder() { SigFlag = "SIG_ALL", Pubkeys = [privKeyBob.Key.CreatePubKey()], @@ -541,7 +541,7 @@ public async Task MintSwapP2Bk() var privKeyBob = new PrivKey(RandomNumberGenerator.GetHexString(64, true)); var privKeyAlice = new PrivKey(RandomNumberGenerator.GetHexString(64, true)); - var builder = new P2PkBuilder() + var builder = new P2PKBuilder() { Pubkeys = [privKeyBob.Key.CreatePubKey(), privKeyAlice.Key.CreatePubKey()], }; @@ -575,7 +575,7 @@ public async Task MintMeltP2Bk() var privKeyBob = new PrivKey(RandomNumberGenerator.GetHexString(64, true)); - var builder = new P2PkBuilder() { Pubkeys = [privKeyBob.Key.CreatePubKey()] }; + var builder = new P2PKBuilder() { Pubkeys = [privKeyBob.Key.CreatePubKey()] }; var quote = await wallet .CreateMintQuote() @@ -608,7 +608,7 @@ public async Task MintMeltP2BkSigAll() var privKeyBob = new PrivKey(RandomNumberGenerator.GetHexString(64, true)); - var builder = new P2PkBuilder() + var builder = new P2PKBuilder() { Pubkeys = [privKeyBob.Key.CreatePubKey()], SigFlag = "SIG_ALL", @@ -650,7 +650,7 @@ public async Task MintSwapP2BkSigAll() .CreateMintQuote() .WithAmount(1337) .WithP2PkLock( - new P2PkBuilder() + new P2PKBuilder() { SigFlag = "SIG_ALL", Pubkeys = [privKeyBob.Key.CreatePubKey()], diff --git a/DotNut.Tests/UnitTest1.cs b/DotNut.Tests/UnitTest1.cs index eb604c7..a568ef9 100644 --- a/DotNut.Tests/UnitTest1.cs +++ b/DotNut.Tests/UnitTest1.cs @@ -1305,7 +1305,7 @@ public void Nut28_P2BK_Flow() var keysetId = new KeysetId("009a1f293253e41e"); - var conditions = new P2PkBuilder() + var conditions = new P2PKBuilder() { Lock = DateTimeOffset.FromUnixTimeSeconds(21000000000), Pubkeys = new[] { signing_key.CreatePubKey(), signing_key_two.CreatePubKey() }, @@ -1358,7 +1358,7 @@ public void Nut28_Flow_WithRandomE() var keysetId = new KeysetId("009a1f293253e41e"); - var conditions = new P2PkBuilder() + var conditions = new P2PKBuilder() { Lock = DateTimeOffset.FromUnixTimeSeconds(21000000000), Pubkeys = new[] { signing_key.CreatePubKey(), signing_key_two.CreatePubKey() }, diff --git a/DotNut.Tests/UnitTests2.cs b/DotNut.Tests/UnitTests2.cs index e374aa8..e0c9956 100644 --- a/DotNut.Tests/UnitTests2.cs +++ b/DotNut.Tests/UnitTests2.cs @@ -588,7 +588,7 @@ public void P2PkBuilder_Build_CreatesValidSecret() var privKey = new PrivKey( "0000000000000000000000000000000000000000000000000000000000000001" ); - var builder = new P2PkBuilder + var builder = new P2PKBuilder { Pubkeys = [privKey.Key.CreatePubKey()], SignatureThreshold = 1, @@ -613,7 +613,7 @@ public void P2PkBuilder_WithMultisig_Build() "0000000000000000000000000000000000000000000000000000000000000002" ); - var builder = new P2PkBuilder + var builder = new P2PKBuilder { Pubkeys = [privKey1.Key.CreatePubKey(), privKey2.Key.CreatePubKey()], SignatureThreshold = 2, diff --git a/DotNut/Abstractions/Interfaces/IMintQuoteBuilder.cs b/DotNut/Abstractions/Interfaces/IMintQuoteBuilder.cs index a6fc50b..df2779b 100644 --- a/DotNut/Abstractions/Interfaces/IMintQuoteBuilder.cs +++ b/DotNut/Abstractions/Interfaces/IMintQuoteBuilder.cs @@ -43,7 +43,7 @@ public interface IMintQuoteBuilder /// /// Optional. Allows providing a P2PK builder when a signature is required for minting. /// - IMintQuoteBuilder WithP2PkLock(P2PkBuilder p2pkBuilder); + IMintQuoteBuilder WithP2PkLock(P2PKBuilder p2pkBuilder); /// /// Optional. When minting P2Pk / HTLC Proofs allows to blind the pubkeys. diff --git a/DotNut/Abstractions/Interfaces/ISwapBuilder.cs b/DotNut/Abstractions/Interfaces/ISwapBuilder.cs index 72fd55b..927c08b 100644 --- a/DotNut/Abstractions/Interfaces/ISwapBuilder.cs +++ b/DotNut/Abstractions/Interfaces/ISwapBuilder.cs @@ -48,7 +48,7 @@ public interface ISwapBuilder /// /// Optional. Generate outputs guarded by P2PK locking. /// - ISwapBuilder ToP2PK(P2PkBuilder p2pkBuilder); + ISwapBuilder ToP2PK(P2PKBuilder p2pkBuilder); /// /// Optional. Blind P2Pk / HTLC proofs. diff --git a/DotNut/Abstractions/MintQuoteBuilder.cs b/DotNut/Abstractions/MintQuoteBuilder.cs index 5f85470..5b119aa 100644 --- a/DotNut/Abstractions/MintQuoteBuilder.cs +++ b/DotNut/Abstractions/MintQuoteBuilder.cs @@ -22,7 +22,7 @@ class MintQuoteBuilder : IMintQuoteBuilder private GetKeysResponse.KeysetItemResponse? _keyset; //for p2pk - private P2PkBuilder? _builder; + private P2PKBuilder? _builder; private bool _shouldBlind = false; public MintQuoteBuilder(Wallet wallet) @@ -70,7 +70,7 @@ public IMintQuoteBuilder WithOutputs(List outputs) return this; } - public IMintQuoteBuilder WithP2PkLock(P2PkBuilder p2pkBuilder) + public IMintQuoteBuilder WithP2PkLock(P2PKBuilder p2pkBuilder) { this._builder = p2pkBuilder; return this; diff --git a/DotNut/Abstractions/SwapBuilder.cs b/DotNut/Abstractions/SwapBuilder.cs index 89f9c9d..7b1e9b9 100644 --- a/DotNut/Abstractions/SwapBuilder.cs +++ b/DotNut/Abstractions/SwapBuilder.cs @@ -26,7 +26,7 @@ class SwapBuilder : ISwapBuilder //nut10 stuff private List? _privKeys; - private P2PkBuilder? _builder; + private P2PKBuilder? _builder; private string? _htlcPreimage; private bool _shouldBlind = false; @@ -96,7 +96,7 @@ public ISwapBuilder WithPrivkeys(IEnumerable privKeys) return this; } - public ISwapBuilder ToP2PK(P2PkBuilder p2pkBuilder) + public ISwapBuilder ToP2PK(P2PKBuilder p2pkBuilder) { this._builder = p2pkBuilder; return this; diff --git a/DotNut/Abstractions/Utils.cs b/DotNut/Abstractions/Utils.cs index 18f76a5..da185ef 100644 --- a/DotNut/Abstractions/Utils.cs +++ b/DotNut/Abstractions/Utils.cs @@ -147,7 +147,7 @@ public static List CreateOutputs( /// /// /// - public static OutputData CreateNut10Output(ulong amount, KeysetId keysetId, P2PkBuilder builder) + public static OutputData CreateNut10Output(ulong amount, KeysetId keysetId, P2PKBuilder builder) { // ugliest hack ever Nut10Secret secret; @@ -185,7 +185,7 @@ public static OutputData CreateNut10Output(ulong amount, KeysetId keysetId, P2Pk public static OutputData CreateNut10BlindedOutput( ulong amount, KeysetId keysetId, - P2PkBuilder builder + P2PKBuilder builder ) { // ugliest hack ever @@ -228,7 +228,7 @@ P2PkBuilder builder public static OutputData CreateNut10BlindedOutput( ulong amount, KeysetId keysetId, - P2PkBuilder builder, + P2PKBuilder builder, PrivKey e ) { diff --git a/DotNut/NUT11/P2PkBuilder.cs b/DotNut/NUT11/P2PKBuilder.cs similarity index 96% rename from DotNut/NUT11/P2PkBuilder.cs rename to DotNut/NUT11/P2PKBuilder.cs index f13a23a..182824e 100644 --- a/DotNut/NUT11/P2PkBuilder.cs +++ b/DotNut/NUT11/P2PKBuilder.cs @@ -3,7 +3,7 @@ namespace DotNut; -public class P2PkBuilder +public class P2PKBuilder { public DateTimeOffset? Lock { get; set; } public ECPubKey[]? RefundPubkeys { get; set; } @@ -58,9 +58,9 @@ public P2PKProofSecret Build() }; } - public static P2PkBuilder Load(P2PKProofSecret proofSecret) + public static P2PKBuilder Load(P2PKProofSecret proofSecret) { - var builder = new P2PkBuilder(); + var builder = new P2PKBuilder(); var primaryPubkey = proofSecret.Data.ToPubKey(); var pubkeys = proofSecret.Tags?.FirstOrDefault(strings => strings.FirstOrDefault() == "pubkeys" @@ -195,9 +195,9 @@ protected void BlindPubkeys(ECPrivKey[] rs) } } - public virtual P2PkBuilder Clone() + public virtual P2PKBuilder Clone() { - return new P2PkBuilder() + return new P2PKBuilder() { Lock = Lock, RefundPubkeys = RefundPubkeys?.ToArray(), diff --git a/DotNut/NUT11/P2PKProofSecret.cs b/DotNut/NUT11/P2PKProofSecret.cs index b193e07..01fd7a6 100644 --- a/DotNut/NUT11/P2PKProofSecret.cs +++ b/DotNut/NUT11/P2PKProofSecret.cs @@ -11,7 +11,7 @@ public class P2PKProofSecret : Nut10ProofSecret public const string Key = "P2PK"; [JsonIgnore] - public virtual P2PkBuilder Builder => P2PkBuilder.Load(this); + public virtual P2PKBuilder Builder => P2PKBuilder.Load(this); public virtual ECPubKey[] GetAllowedPubkeys(out int requiredSignatures) { diff --git a/DotNut/NUT11/SigAllHandler.cs b/DotNut/NUT11/SigAllHandler.cs index d0ca29a..d412799 100644 --- a/DotNut/NUT11/SigAllHandler.cs +++ b/DotNut/NUT11/SigAllHandler.cs @@ -240,9 +240,9 @@ private static bool ValidateFirstProof(Proof firstProof, out Nut10ProofSecret? s var builder = nut10.ProofSecret switch { HTLCProofSecret htlcs => HTLCBuilder.Load(htlcs), - P2PKProofSecret p2pks => P2PkBuilder.Load(p2pks), + P2PKProofSecret p2pks => P2PKBuilder.Load(p2pks), // won't throw exception if there will be a new type of nut10 secret, but will return false - _ => new P2PkBuilder() { SigFlag = null }, + _ => new P2PKBuilder() { SigFlag = null }, }; if (builder.SigFlag != "SIG_ALL") diff --git a/DotNut/NUT14/HTLCBuilder.cs b/DotNut/NUT14/HTLCBuilder.cs index 4f428ad..2edc631 100644 --- a/DotNut/NUT14/HTLCBuilder.cs +++ b/DotNut/NUT14/HTLCBuilder.cs @@ -3,7 +3,7 @@ namespace DotNut; -public class HTLCBuilder : P2PkBuilder +public class HTLCBuilder : P2PKBuilder { public string HashLock { get; set; } @@ -33,7 +33,7 @@ public static HTLCBuilder Load(HTLCProofSecret proofSecret) Tags = proofSecret.Tags, }; - var innerbuilder = P2PkBuilder.Load(tempProof); + var innerbuilder = P2PKBuilder.Load(tempProof); innerbuilder.Pubkeys = innerbuilder.Pubkeys.Except([_dummy.Key]).ToArray(); return new HTLCBuilder() { @@ -56,7 +56,7 @@ public static HTLCBuilder Load(HTLCProofSecret proofSecret) nameof(HashLock) ); } - var innerBuilder = new P2PkBuilder() + var innerBuilder = new P2PKBuilder() { Lock = Lock, Pubkeys = Pubkeys.ToArray(), From 01e05f663959fac2f422cd695ebd1613d3a32caf Mon Sep 17 00:00:00 2001 From: d4rp4t Date: Sun, 25 Jan 2026 01:09:55 +0100 Subject: [PATCH 54/70] fix --- DotNut.Tests/Integration.cs | 1 + DotNut/Abstractions/Nut10Helper.cs | 8 ------- DotNut/Abstractions/Utils.cs | 2 +- .../Websockets/NotificationParser.cs | 6 +++--- DotNut/NUT10/Nut10ProofSecret.cs | 21 ++++++++++++++++++- DotNut/NUT11/SigAllHandler.cs | 9 ++++---- 6 files changed, 30 insertions(+), 17 deletions(-) diff --git a/DotNut.Tests/Integration.cs b/DotNut.Tests/Integration.cs index 2a0bf36..97ec33d 100644 --- a/DotNut.Tests/Integration.cs +++ b/DotNut.Tests/Integration.cs @@ -524,6 +524,7 @@ await Assert.ThrowsAsync(async () => await wallet.Swap().FromInputs(proofs).ProcessAsync() ); + var swappedProofs = await wallet .Swap() .FromInputs(proofs) diff --git a/DotNut/Abstractions/Nut10Helper.cs b/DotNut/Abstractions/Nut10Helper.cs index dc6519a..d449505 100644 --- a/DotNut/Abstractions/Nut10Helper.cs +++ b/DotNut/Abstractions/Nut10Helper.cs @@ -30,14 +30,6 @@ public static void MaybeProcessNut10( if (sigAllHandler.TrySign(out string? witness)) { - if (witness == null) - { - throw new ArgumentNullException( - nameof(witness), - "sig_all input was correct, but couldn't create a witness signature!" - ); - } - proofs[0].Witness = witness; return; } diff --git a/DotNut/Abstractions/Utils.cs b/DotNut/Abstractions/Utils.cs index da185ef..bd72885 100644 --- a/DotNut/Abstractions/Utils.cs +++ b/DotNut/Abstractions/Utils.cs @@ -337,7 +337,7 @@ Keyset keys public static ulong SumProofs(List proofs) { - return proofs.Aggregate(0UL, (current, proof) => current + proof.Amount); + return proofs.Aggregate(0UL, (current, proof) => checked(current + proof.Amount)); } public static ISecret RandomSecret() diff --git a/DotNut/Abstractions/Websockets/NotificationParser.cs b/DotNut/Abstractions/Websockets/NotificationParser.cs index d9136b5..143e963 100644 --- a/DotNut/Abstractions/Websockets/NotificationParser.cs +++ b/DotNut/Abstractions/Websockets/NotificationParser.cs @@ -17,11 +17,11 @@ SubscriptionKind subscriptionKind return subscriptionKind switch { - SubscriptionKind.bolt11_mint_quote => + SubscriptionKind.Bolt11MintQuote => jsonElement.Deserialize(), - SubscriptionKind.bolt11_melt_quote => + SubscriptionKind.Bolt11MeltQuote => jsonElement.Deserialize(), - SubscriptionKind.proof_state => jsonElement.Deserialize(), + SubscriptionKind.ProofState => jsonElement.Deserialize(), _ => notification.Params.Payload, }; } diff --git a/DotNut/NUT10/Nut10ProofSecret.cs b/DotNut/NUT10/Nut10ProofSecret.cs index 47c0b2a..6be496e 100644 --- a/DotNut/NUT10/Nut10ProofSecret.cs +++ b/DotNut/NUT10/Nut10ProofSecret.cs @@ -70,5 +70,24 @@ public override int GetHashCode() return first.Equals(second); } - public static bool operator !=(Nut10ProofSecret first, Nut10ProofSecret second) => !(first == second); + public static bool operator !=(Nut10ProofSecret first, Nut10ProofSecret second) => + !(first == second); + + /// + /// Helper for SIG_ALL equality check. Every proof has to have identical data and tags fields + /// + public bool SigAllEquals(Nut10ProofSecret other) + { + return other is { } o + && this.Data == o.Data + && ( + (this.Tags == null && o.Tags == null) + || ( + this.Tags != null + && o.Tags != null + && this.Tags.Length == o.Tags.Length + && this.Tags.Zip(o.Tags).All(pair => pair.First.SequenceEqual(pair.Second)) + ) + ); + } } diff --git a/DotNut/NUT11/SigAllHandler.cs b/DotNut/NUT11/SigAllHandler.cs index d412799..d26437f 100644 --- a/DotNut/NUT11/SigAllHandler.cs +++ b/DotNut/NUT11/SigAllHandler.cs @@ -34,12 +34,13 @@ BlindedMessages is null byte[] msg; try { - var msgStr = GetMessageToSign(Proofs.ToArray(), BlindedMessages.ToArray(), MeltQuoteId); if (!ValidateFirstProof(Proofs[0], out var sec) || sec is null) { return false; } _firstProofSecret = sec; + + var msgStr = GetMessageToSign(Proofs.ToArray(), BlindedMessages.ToArray(), MeltQuoteId); msg = Encoding.UTF8.GetBytes(msgStr); } catch (ArgumentException) @@ -112,9 +113,9 @@ public static string GetMessageToSign( nameof(outputs) ); } - if (!ValidateFirstProof(inputs[0], out var firstSecret)) + if (!ValidateFirstProof(inputs[0], out var firstSecret) || firstSecret is null) { - throw new ArgumentException("Provided first proof is invalid"); + throw new ArgumentException("Provided first proof is invalid or null"); } var msg = new StringBuilder(); @@ -129,7 +130,7 @@ public static string GetMessageToSign( ); } - if (!CheckIfEqualToFirst(firstSecret, nut10.ProofSecret)) + if (!firstSecret.SigAllEquals(nut10.ProofSecret)) { throw new ArgumentException( "When signing sig_all, every proof must have identical tags and data." From 9d7254dbd8750b36138e510b46940a05b78de8d9 Mon Sep 17 00:00:00 2001 From: d4rp4t Date: Sun, 25 Jan 2026 01:11:25 +0100 Subject: [PATCH 55/70] ws --- .../Websockets/WebsocketConnection.cs | 4 + .../Abstractions/Websockets/WebsocketEnums.cs | 22 +- .../Websockets/WebsocketModels.cs | 1 + .../Websockets/WebsocketService.cs | 535 ++++++++++++------ .../Websockets/WebsocketServiceExtensions.cs | 6 +- .../Websockets/WebsocketServiceOptions.cs | 61 ++ 6 files changed, 459 insertions(+), 170 deletions(-) create mode 100644 DotNut/Abstractions/Websockets/WebsocketServiceOptions.cs diff --git a/DotNut/Abstractions/Websockets/WebsocketConnection.cs b/DotNut/Abstractions/Websockets/WebsocketConnection.cs index 8c721bc..e52d51d 100644 --- a/DotNut/Abstractions/Websockets/WebsocketConnection.cs +++ b/DotNut/Abstractions/Websockets/WebsocketConnection.cs @@ -11,6 +11,10 @@ public class WebsocketConnection : IDisposable public DateTime ConnectedAt { get; set; } = DateTime.UtcNow; public CancellationTokenSource? CancellationTokenSource { get; set; } + public DateTime? LastPingSent { get; set; } + public DateTime? LastMessageReceived { get; set; } + public int ReconnectAttempts { get; set; } + public bool Equals(WebsocketConnection? other) { if (other is null) diff --git a/DotNut/Abstractions/Websockets/WebsocketEnums.cs b/DotNut/Abstractions/Websockets/WebsocketEnums.cs index 44cc448..7f178b1 100644 --- a/DotNut/Abstractions/Websockets/WebsocketEnums.cs +++ b/DotNut/Abstractions/Websockets/WebsocketEnums.cs @@ -1,3 +1,4 @@ +using System.Runtime.Serialization; using System.Text.Json.Serialization; namespace DotNut.Abstractions.Websockets; @@ -5,16 +6,23 @@ namespace DotNut.Abstractions.Websockets; [JsonConverter(typeof(JsonStringEnumConverter))] public enum SubscriptionKind { - bolt11_melt_quote, - bolt11_mint_quote, - bolt12_melt_quote, - bolt12_mint_quote, - proof_state, + + [EnumMember(Value = "bolt11_melt_quote")] Bolt11MeltQuote, + + [EnumMember(Value = "bolt11_mint_quote")] Bolt11MintQuote, + + [EnumMember(Value = "bolt12_melt_quote")] Bolt12MeltQuote, + + [EnumMember(Value = "bolt12_mint_quote")] Bolt12MintQuote, + + [EnumMember(Value = "proof_state")] ProofState, } [JsonConverter(typeof(JsonStringEnumConverter))] public enum WsRequestMethod { - subscribe, - unsubscribe, + [EnumMember(Value = "subscribe")] + Subscribe, + [EnumMember(Value = "unsubscribe")] + Unsubscribe, } diff --git a/DotNut/Abstractions/Websockets/WebsocketModels.cs b/DotNut/Abstractions/Websockets/WebsocketModels.cs index e30afc0..059fc86 100644 --- a/DotNut/Abstractions/Websockets/WebsocketModels.cs +++ b/DotNut/Abstractions/Websockets/WebsocketModels.cs @@ -114,4 +114,5 @@ internal class PendingRequest { public required TaskCompletionSource Tcs { get; set; } public required string SubscriptionId { get; set; } + public DateTime CreatedAt { get; set; } = DateTime.UtcNow; } diff --git a/DotNut/Abstractions/Websockets/WebsocketService.cs b/DotNut/Abstractions/Websockets/WebsocketService.cs index f90b18f..40b45bd 100644 --- a/DotNut/Abstractions/Websockets/WebsocketService.cs +++ b/DotNut/Abstractions/Websockets/WebsocketService.cs @@ -1,3 +1,4 @@ +using System.Buffers; using System.Collections.Concurrent; using System.Net.WebSockets; using System.Text; @@ -13,28 +14,47 @@ public class WebsocketService : IWebsocketService private readonly ConcurrentDictionary _connections = new(); private readonly ConcurrentDictionary _subscriptions = new(); private readonly ConcurrentDictionary _pendingRequests = new(); - private int _nextRequestId = 0; + private readonly ConcurrentDictionary _connectionLocks = new(); + private readonly ConcurrentDictionary _subscriptionInfos = new(); + + private readonly WebsocketServiceOptions _options; + private readonly CancellationTokenSource _disposeCts = new(); + + private int _nextRequestId; + private volatile bool _disposed; + + private static readonly JsonSerializerOptions JsonOptions = new() + { + DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, + }; public event EventHandler? ConnectionStateChanged; + public WebsocketService() : this(new WebsocketServiceOptions()) { } + + public WebsocketService(WebsocketServiceOptions options) + { + _options = options ?? new WebsocketServiceOptions(); + _ = RunRequestCleanupLoopAsync(_disposeCts.Token); + } + public async Task ConnectAsync( string mintUrl, CancellationToken ct = default ) { var normalized = NormalizeMintUrl(mintUrl); - var connectionId = Guid.NewGuid().ToString(); var wsUrl = GetWebSocketUrl(mintUrl); var clientWebSocket = new ClientWebSocket(); - var connectionCts = new CancellationTokenSource(); + var connectionCts = CancellationTokenSource.CreateLinkedTokenSource(_disposeCts.Token, ct); try { await clientWebSocket.ConnectAsync(new Uri(wsUrl), ct); } - catch (Exception ex) + catch { connectionCts.Dispose(); clientWebSocket.Dispose(); @@ -48,22 +68,21 @@ public async Task ConnectAsync( WebSocket = clientWebSocket, State = WebSocketState.Open, CancellationTokenSource = connectionCts, + LastMessageReceived = DateTime.UtcNow, }; _connections[normalized] = connection; OnConnectionStateChanged(connectionId, WebSocketState.Open); - try - { - _ = Task.Run(async () => await ListenForMessages(connection, connectionCts.Token)); - } - catch - { - _connections.TryRemove(normalized, out _); - connectionCts.Dispose(); - clientWebSocket.Dispose(); - throw; - } + _ = RunWithErrorHandlingAsync( + () => ListenForMessagesAsync(connection, connectionCts.Token), + connection + ); + + _ = RunWithErrorHandlingAsync( + () => RunHeartbeatLoopAsync(connection, connectionCts.Token), + connection + ); return connection; } @@ -74,26 +93,35 @@ public async Task LazyConnectAsync( ) { var normalized = NormalizeMintUrl(mintUrl); + var connectionLock = _connectionLocks.GetOrAdd(normalized, _ => new SemaphoreSlim(1, 1)); - if (_connections.TryGetValue(normalized, out var existing)) + await connectionLock.WaitAsync(ct); + try { - if (existing is { State: WebSocketState.Open, WebSocket.State: WebSocketState.Open }) + if (_connections.TryGetValue(normalized, out var existing)) { - return existing; + if (existing is { State: WebSocketState.Open, WebSocket.State: WebSocketState.Open }) + { + return existing; + } + + _connections.TryRemove(normalized, out _); + existing.Dispose(); } + + return await ConnectAsync(mintUrl, ct); } - if (_connections.TryRemove(normalized, out var oldConnection)) + finally { - oldConnection?.Dispose(); + connectionLock.Release(); } - return await ConnectAsync(mintUrl, ct); } public async Task DisconnectAsync(string mintUrl, CancellationToken ct = default) { var normalized = NormalizeMintUrl(mintUrl); - if (!_connections.TryGetValue(normalized, out var connection)) + if (!_connections.TryRemove(normalized, out var connection)) { return; } @@ -102,38 +130,27 @@ public async Task DisconnectAsync(string mintUrl, CancellationToken ct = default { if (connection.State == WebSocketState.Open) { + using var timeoutCts = CancellationTokenSource.CreateLinkedTokenSource(ct); + timeoutCts.CancelAfter(TimeSpan.FromSeconds(5)); + await connection.WebSocket.CloseAsync( WebSocketCloseStatus.NormalClosure, "Client disconnecting", - ct + timeoutCts.Token ); } } - catch (Exception _) + catch { - // ignored + // graceful close failed, continue with cleanup } finally { connection.State = WebSocketState.Closed; connection.CancellationTokenSource?.Cancel(); - connection.CancellationTokenSource?.Dispose(); - connection.WebSocket.Dispose(); - _connections.TryRemove(normalized, out _); - - var subscriptionsToRemove = _subscriptions - .Where(s => s.Value.ConnectionId == connection.Id) - .Select(s => s.Key) - .ToList(); - - foreach (var subId in subscriptionsToRemove) - { - if (_subscriptions.TryRemove(subId, out var removedSub)) - { - await removedSub.CloseInternalAsync(); - } - } + connection.Dispose(); + await CleanupConnectionSubscriptionsAsync(connection); OnConnectionStateChanged(connection.Id, WebSocketState.Closed); } } @@ -158,16 +175,18 @@ public async Task SubscribeAsync( } var subId = Guid.NewGuid().ToString(); - var requestId = _getNextRequestId(); + var requestId = GetNextRequestId(); - var channel = Channel.CreateUnbounded( - new UnboundedChannelOptions { SingleReader = false } - ); + var channel = Channel.CreateBounded(new BoundedChannelOptions(_options.MaxChannelCapacity) + { + FullMode = BoundedChannelFullMode.DropOldest, + SingleReader = false, + }); var request = new WsRequest { JsonRpc = "2.0", - Method = WsRequestMethod.subscribe, + Method = WsRequestMethod.Subscribe, Params = new WsRequestParams { Kind = kind, @@ -188,10 +207,20 @@ public async Task SubscribeAsync( }; _subscriptions[subId] = subscription; + _subscriptionInfos[subId] = new SubscriptionInfo + { + MintUrl = normalized, + Kind = kind, + Filters = filters, + }; var tcs = new TaskCompletionSource(); - - _pendingRequests[requestId] = new PendingRequest { Tcs = tcs, SubscriptionId = subId }; + _pendingRequests[requestId] = new PendingRequest + { + Tcs = tcs, + SubscriptionId = subId, + CreatedAt = DateTime.UtcNow, + }; try { @@ -209,6 +238,7 @@ public async Task SubscribeAsync( if (completedTask != tcs.Task) { _subscriptions.TryRemove(subId, out _); + _subscriptionInfos.TryRemove(subId, out _); await subscription.CloseInternalAsync(); throw new TimeoutException("Subscription request timed out"); } @@ -218,6 +248,7 @@ public async Task SubscribeAsync( if (result is RequestResult.Failure failure) { _subscriptions.TryRemove(subId, out _); + _subscriptionInfos.TryRemove(subId, out _); await subscription.CloseInternalAsync(); throw new InvalidOperationException($"Subscription failed: {failure.Message}"); } @@ -228,6 +259,7 @@ public async Task SubscribeAsync( { subscription.NotificationChannel.Writer.TryComplete(); _subscriptions.TryRemove(subId, out _); + _subscriptionInfos.TryRemove(subId, out _); throw; } finally @@ -239,27 +271,32 @@ public async Task SubscribeAsync( public async Task UnsubscribeAsync(string subId, CancellationToken ct = default) { if (!_subscriptions.TryRemove(subId, out var subscription)) - return; // Already unsubscribed + return; + _subscriptionInfos.TryRemove(subId, out _); subscription.NotificationChannel.Writer.TryComplete(); var connection = _connections.Values.FirstOrDefault(c => c.Id == subscription.ConnectionId); if (connection is null || connection.State != WebSocketState.Open) { - // Connection gone or closed - local cleanup is sufficient return; } - var requestId = _getNextRequestId(); + var requestId = GetNextRequestId(); var tcs = new TaskCompletionSource(); - _pendingRequests[requestId] = new PendingRequest { Tcs = tcs, SubscriptionId = subId }; + _pendingRequests[requestId] = new PendingRequest + { + Tcs = tcs, + SubscriptionId = subId, + CreatedAt = DateTime.UtcNow, + }; try { var request = new WsRequest { JsonRpc = "2.0", - Method = WsRequestMethod.unsubscribe, + Method = WsRequestMethod.Unsubscribe, Params = new WsRequestParams { SubId = subId }, Id = requestId, }; @@ -272,13 +309,14 @@ public async Task UnsubscribeAsync(string subId, CancellationToken ct = default) var completed = await Task.WhenAny(tcs.Task, Task.Delay(Timeout.Infinite, cts.Token)) .ConfigureAwait(false); - if (completed != tcs.Task) + if (completed == tcs.Task) { - // Timeout - local cleanup already done, just log or ignore - return; + await tcs.Task.ConfigureAwait(false); } - - await tcs.Task.ConfigureAwait(false); + } + catch + { + // unsubscribe failed, local cleanup already done } finally { @@ -288,6 +326,12 @@ public async Task UnsubscribeAsync(string subId, CancellationToken ct = default) public async ValueTask DisposeAsync() { + if (_disposed) + return; + + _disposed = true; + _disposeCts.Cancel(); + var mintUrls = _connections.Keys.ToList(); foreach (var mintUrl in mintUrls) { @@ -295,11 +339,24 @@ public async ValueTask DisposeAsync() { await DisconnectAsync(mintUrl); } - catch { } + catch + { + // continue disposing other connections + } } + _subscriptions.Clear(); _connections.Clear(); _pendingRequests.Clear(); + _subscriptionInfos.Clear(); + + foreach (var semaphore in _connectionLocks.Values) + { + semaphore.Dispose(); + } + _connectionLocks.Clear(); + + _disposeCts.Dispose(); } public WebSocketState GetConnectionState(string mintUrl) @@ -315,20 +372,26 @@ public IEnumerable GetSubscriptions(string mintUrl) var normalized = NormalizeMintUrl(mintUrl); if (!_connections.TryGetValue(normalized, out var connection)) { - throw new Exception($"Connection for mint {mintUrl} not found"); + return Enumerable.Empty(); } return _subscriptions.Values.Where(s => s.ConnectionId == connection.Id); } - public IEnumerable GetConnections() + public IEnumerable GetConnections() => _connections.Values; + + public bool IsConnected(string mintUrl) { - return _connections.Values; + var normalized = NormalizeMintUrl(mintUrl); + return _connections.TryGetValue(normalized, out var conn) + && conn.State == WebSocketState.Open; } - private async Task ListenForMessages(WebsocketConnection connection, CancellationToken ct) + #region Message Handling + + private async Task ListenForMessagesAsync(WebsocketConnection connection, CancellationToken ct) { - var buffer = new byte[4096]; - var messageBuffer = new MemoryStream(); + var buffer = ArrayPool.Shared.Rent(4096); + using var messageBuffer = new MemoryStream(); try { @@ -339,11 +402,11 @@ private async Task ListenForMessages(WebsocketConnection connection, Cancellatio ct ); + connection.LastMessageReceived = DateTime.UtcNow; + if (result.MessageType == WebSocketMessageType.Close) { connection.State = WebSocketState.Closed; - connection.CancellationTokenSource?.Cancel(); - OnConnectionStateChanged(connection.Id, WebSocketState.Closed); break; } @@ -352,95 +415,82 @@ private async Task ListenForMessages(WebsocketConnection connection, Cancellatio messageBuffer.Write(buffer, 0, result.Count); if (result.EndOfMessage) { - var message = Encoding.UTF8.GetString(messageBuffer.ToArray()); + var message = Encoding.UTF8.GetString(messageBuffer.GetBuffer(), 0, (int)messageBuffer.Length); messageBuffer.SetLength(0); - _processMessage(connection, message); + ProcessMessage(message); } } } } catch (OperationCanceledException) { - // Expected + // Expected on cancellation } - catch (Exception ex) + catch { connection.State = WebSocketState.Aborted; - connection.CancellationTokenSource?.Cancel(); - OnConnectionStateChanged(connection.Id, WebSocketState.Aborted); } finally { - // Close all subscriptions for this connection - var subscriptionsToClose = _subscriptions - .Values.Where(s => s.ConnectionId == connection.Id) - .ToList(); + ArrayPool.Shared.Return(buffer); + connection.CancellationTokenSource?.Cancel(); - foreach (var sub in subscriptionsToClose) + if (connection.State != WebSocketState.Closed) { - try - { - await sub.CloseInternalAsync(); - } - catch { } - _subscriptions.TryRemove(sub.Id, out _); + OnConnectionStateChanged(connection.Id, WebSocketState.Aborted); + } + + await CleanupConnectionSubscriptionsAsync(connection); + + if (_options.AutoReconnect && !_disposed && !_disposeCts.IsCancellationRequested) + { + _ = ReconnectAsync(connection.MintUrl, _disposeCts.Token); } } } - private void _processMessage(WebsocketConnection connection, string message) + private void ProcessMessage(string message) { try { - var jsonElement = JsonSerializer.Deserialize(message); + using var doc = JsonDocument.Parse(message); + var root = doc.RootElement; - if ( - jsonElement.TryGetProperty("method", out var methodProp) - && methodProp.GetString() == "subscribe" - ) + if (root.TryGetProperty("method", out var methodProp) + && methodProp.GetString() == "subscribe") { - var notification = JsonSerializer.Deserialize(message); + var notification = JsonSerializer.Deserialize(message, JsonOptions); if (notification != null) { - _onNotificationReceived(notification); + OnNotificationReceived(notification); } } - else if (jsonElement.TryGetProperty("result", out _)) + else if (root.TryGetProperty("result", out _)) { - var response = JsonSerializer.Deserialize(message); + var response = JsonSerializer.Deserialize(message, JsonOptions); if (response != null) { HandleResponse(response); } } - else if (jsonElement.TryGetProperty("error", out _)) + else if (root.TryGetProperty("error", out _)) { - var error = JsonSerializer.Deserialize(message); + var error = JsonSerializer.Deserialize(message, JsonOptions); if (error != null) { HandleError(error); } } } - catch (Exception ex) + catch { - // Could be logged if logging is added later + // invalid message format, ignore } } - private async Task SendMessageAsync( - WebsocketConnection connection, - T message, - CancellationToken ct - ) + private async Task SendMessageAsync(WebsocketConnection connection, T message, CancellationToken ct) { - var json = JsonSerializer.Serialize( - message, - new JsonSerializerOptions - { - DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, - } - ); + var json = JsonSerializer.Serialize(message, JsonOptions); var bytes = Encoding.UTF8.GetBytes(json); await connection.WebSocket.SendAsync( @@ -451,92 +501,257 @@ await connection.WebSocket.SendAsync( ); } - public bool IsConnected(string mintUrl) + private void HandleResponse(WsResponse response) { - var normalized = NormalizeMintUrl(mintUrl); - return _connections.TryGetValue(normalized, out var conn) - && conn.State == WebSocketState.Open; + if (!_pendingRequests.TryGetValue(response.Id, out var pr)) + return; + + var result = new RequestResult.Success(response.Result.SubId, response.Result.Status); + pr.Tcs.TrySetResult(result); + + if (_subscriptions.TryGetValue(pr.SubscriptionId, out var sub)) + { + sub.NotificationChannel.Writer.TryWrite(new WsMessage.Response(response)); + } } - private string GetWebSocketUrl(string mintUrl) + private void HandleError(WsError error) { - var uri = new Uri(NormalizeMintUrl(mintUrl)); - var scheme = uri.Scheme == "https" ? "wss" : "ws"; - var hostPort = (uri.IsDefaultPort) ? uri.Host : $"{uri.Host}:{uri.Port}"; - var path = uri.AbsolutePath.TrimEnd('/'); - return $"{scheme}://{hostPort}{path}/v1/ws"; + if (!_pendingRequests.TryGetValue(error.Id, out var pr)) + return; + + var result = new RequestResult.Failure(error.Error.Code, error.Error.Message, error.Id); + pr.Tcs.TrySetResult(result); + + if (_subscriptions.TryGetValue(pr.SubscriptionId, out var sub)) + { + sub.NotificationChannel.Writer.TryWrite(new WsMessage.Error(error)); + } } - private int _getNextRequestId() + private void OnNotificationReceived(WsNotification notification) { - return Interlocked.Increment(ref _nextRequestId); + if (_subscriptions.TryGetValue(notification.Params.SubId, out var sub)) + { + sub.NotificationChannel.Writer.TryWrite(new WsMessage.Notification(notification)); + } } - private void OnConnectionStateChanged(string connectionId, WebSocketState state) + #endregion + + #region Heartbeat & Reconnect + + private async Task RunHeartbeatLoopAsync(WebsocketConnection connection, CancellationToken ct) { - ConnectionStateChanged?.Invoke( - this, - new ConnectionStateChangedEventArgs { ConnectionId = connectionId, State = state } - ); + while (!ct.IsCancellationRequested && connection.State == WebSocketState.Open) + { + try + { + await Task.Delay(_options.HeartbeatInterval, ct); + + if (connection.State != WebSocketState.Open) + break; + + var lastReceived = connection.LastMessageReceived ?? connection.ConnectedAt; + var timeSinceLastMessage = DateTime.UtcNow - lastReceived; + + if (timeSinceLastMessage > _options.HeartbeatInterval + _options.HeartbeatTimeout) + { + connection.State = WebSocketState.Aborted; + connection.CancellationTokenSource?.Cancel(); + break; + } + + connection.LastPingSent = DateTime.UtcNow; + } + catch (OperationCanceledException) + { + break; + } + } } - private static string NormalizeMintUrl(string mintUrl) + private async Task ReconnectAsync(string mintUrl, CancellationToken ct) { - if (!Uri.TryCreate(mintUrl.TrimEnd('/'), UriKind.Absolute, out var uri)) + var normalized = NormalizeMintUrl(mintUrl); + var delay = _options.InitialReconnectDelay; + + for (int attempt = 1; attempt <= _options.MaxReconnectAttempts && !ct.IsCancellationRequested; attempt++) { - return mintUrl.TrimEnd('/').ToLowerInvariant(); + try + { + await Task.Delay(delay, ct); + + var connectionLock = _connectionLocks.GetOrAdd(normalized, _ => new SemaphoreSlim(1, 1)); + await connectionLock.WaitAsync(ct); + + try + { + if (_connections.TryGetValue(normalized, out var existing) + && existing.State == WebSocketState.Open) + { + return; // already reconnected + } + + await ConnectAsync(mintUrl, ct); + await ResubscribeAllAsync(normalized, ct); + return; + } + finally + { + connectionLock.Release(); + } + } + catch (OperationCanceledException) + { + return; + } + catch + { + delay = TimeSpan.FromTicks(Math.Min(delay.Ticks * 2, _options.MaxReconnectDelay.Ticks)); + } } - var host = uri.Host.ToLowerInvariant(); - var builder = new UriBuilder(uri) { Host = host }; - return builder.Uri.ToString().TrimEnd('/'); + + OnConnectionStateChanged(normalized, WebSocketState.Closed); } - private void HandleResponse(WsResponse response) + private async Task ResubscribeAllAsync(string mintUrl, CancellationToken ct) { - if (!_pendingRequests.TryGetValue(response.Id, out var pr)) + var subsToRestore = _subscriptionInfos + .Where(kvp => kvp.Value.MintUrl == mintUrl) + .ToList(); + + foreach (var (subId, info) in subsToRestore) { - return; + try + { + _subscriptions.TryRemove(subId, out var oldSub); + _subscriptionInfos.TryRemove(subId, out _); + if (oldSub != null) + { + await oldSub.CloseInternalAsync(); + } + + await SubscribeAsync(mintUrl, info.Kind, info.Filters, ct); + } + catch + { + // failed to re-subscribe, continue with others + } } - var result = new RequestResult.Success( - SubId: response.Result.SubId, - Status: response.Result.Status - ); - pr.Tcs.TrySetResult(result); + } + + #endregion + + #region Cleanup & Utilities + + private async Task CleanupConnectionSubscriptionsAsync(WebsocketConnection connection) + { + var subscriptionsToClose = _subscriptions + .Where(s => s.Value.ConnectionId == connection.Id) + .ToList(); - if (!_subscriptions.TryGetValue(pr.SubscriptionId, out var sub)) + foreach (var (subId, sub) in subscriptionsToClose) { - return; + try + { + await sub.CloseInternalAsync(); + } + catch + { + // continue cleanup + } + _subscriptions.TryRemove(subId, out _); } - sub.NotificationChannel.Writer.TryWrite(new WsMessage.Response(response)); } - private void HandleError(WsError error) + private async Task RunRequestCleanupLoopAsync(CancellationToken ct) { - if (!_pendingRequests.TryGetValue(error.Id, out var pr)) + while (!ct.IsCancellationRequested) { - return; + try + { + await Task.Delay(_options.RequestCleanupInterval, ct); + CleanupStaleRequests(); + } + catch (OperationCanceledException) + { + break; + } } - var result = new RequestResult.Failure( - Code: error.Error.Code, - Message: error.Error.Message, - RequestId: error.Id - ); - pr.Tcs.TrySetResult(result); + } + + private void CleanupStaleRequests() + { + var staleThreshold = DateTime.UtcNow - _options.RequestTimeout; + var staleRequests = _pendingRequests + .Where(pr => pr.Value.CreatedAt < staleThreshold) + .Select(pr => pr.Key) + .ToList(); - if (!_subscriptions.TryGetValue(pr.SubscriptionId, out var sub)) + foreach (var id in staleRequests) { - return; + if (_pendingRequests.TryRemove(id, out var pr)) + { + pr.Tcs.TrySetException(new TimeoutException("Request expired")); + } + } + } + + private async Task RunWithErrorHandlingAsync(Func action, WebsocketConnection connection) + { + try + { + await action(); + } + catch (OperationCanceledException) + { + // Expected + } + catch + { + connection.State = WebSocketState.Aborted; + OnConnectionStateChanged(connection.Id, WebSocketState.Aborted); } + } - sub.NotificationChannel.Writer.TryWrite(new WsMessage.Error(error)); + private int GetNextRequestId() => Interlocked.Increment(ref _nextRequestId); + + private void OnConnectionStateChanged(string connectionId, WebSocketState state) + { + ConnectionStateChanged?.Invoke(this, new ConnectionStateChangedEventArgs + { + ConnectionId = connectionId, + State = state + }); } - private void _onNotificationReceived(WsNotification notification) + private static string NormalizeMintUrl(string mintUrl) { - if (!_subscriptions.TryGetValue(notification.Params.SubId, out var sub)) + if (!Uri.TryCreate(mintUrl.TrimEnd('/'), UriKind.Absolute, out var uri)) { - return; + return mintUrl.TrimEnd('/').ToLowerInvariant(); } - sub.NotificationChannel.Writer.TryWrite(new WsMessage.Notification(notification)); + var host = uri.Host.ToLowerInvariant(); + var builder = new UriBuilder(uri) { Host = host }; + return builder.Uri.ToString().TrimEnd('/'); + } + + private string GetWebSocketUrl(string mintUrl) + { + var uri = new Uri(NormalizeMintUrl(mintUrl)); + var scheme = uri.Scheme == "https" ? "wss" : "ws"; + var hostPort = uri.IsDefaultPort ? uri.Host : $"{uri.Host}:{uri.Port}"; + var path = uri.AbsolutePath.TrimEnd('/'); + return $"{scheme}://{hostPort}{path}/v1/ws"; + } + + #endregion + + private class SubscriptionInfo + { + public required string MintUrl { get; init; } + public required SubscriptionKind Kind { get; init; } + public required string[] Filters { get; init; } } } diff --git a/DotNut/Abstractions/Websockets/WebsocketServiceExtensions.cs b/DotNut/Abstractions/Websockets/WebsocketServiceExtensions.cs index e2d3005..111317c 100644 --- a/DotNut/Abstractions/Websockets/WebsocketServiceExtensions.cs +++ b/DotNut/Abstractions/Websockets/WebsocketServiceExtensions.cs @@ -12,7 +12,7 @@ public static async Task SubscribeToMintQuoteAsync( await service.LazyConnectAsync(mintUrl, ct); return await service.SubscribeAsync( mintUrl, - SubscriptionKind.bolt11_mint_quote, + SubscriptionKind.Bolt11MintQuote, quoteIds, ct ); @@ -28,7 +28,7 @@ public static async Task SubscribeToMeltQuoteAsync( await service.LazyConnectAsync(mintUrl, ct); return await service.SubscribeAsync( mintUrl, - SubscriptionKind.bolt11_melt_quote, + SubscriptionKind.Bolt11MeltQuote, quoteIds, ct ); @@ -42,7 +42,7 @@ public static async Task SubscribeToProofStateAsync( ) { await service.LazyConnectAsync(mintUrl, ct); - return await service.SubscribeAsync(mintUrl, SubscriptionKind.proof_state, proofYs, ct); + return await service.SubscribeAsync(mintUrl, SubscriptionKind.ProofState, proofYs, ct); } public static async Task SubscribeToSingleProofStateAsync( diff --git a/DotNut/Abstractions/Websockets/WebsocketServiceOptions.cs b/DotNut/Abstractions/Websockets/WebsocketServiceOptions.cs new file mode 100644 index 0000000..fbb672b --- /dev/null +++ b/DotNut/Abstractions/Websockets/WebsocketServiceOptions.cs @@ -0,0 +1,61 @@ +namespace DotNut.Abstractions.Websockets; + +/// +/// Configuration options for WebsocketService +/// +public class WebsocketServiceOptions +{ + /// + /// Whether to automatically reconnect when connection is lost. + /// Default: true + /// + public bool AutoReconnect { get; set; } = true; + + /// + /// Maximum number of reconnect attempts before giving up. + /// Default: 10 + /// + public int MaxReconnectAttempts { get; set; } = 10; + + /// + /// Initial delay before first reconnect attempt. + /// Default: 1 second + /// + public TimeSpan InitialReconnectDelay { get; set; } = TimeSpan.FromSeconds(1); + + /// + /// Maximum delay between reconnect attempts (exponential backoff cap). + /// Default: 5 minutes + /// + public TimeSpan MaxReconnectDelay { get; set; } = TimeSpan.FromMinutes(5); + + /// + /// Interval between heartbeat checks. + /// Default: 30 seconds + /// + public TimeSpan HeartbeatInterval { get; set; } = TimeSpan.FromSeconds(30); + + /// + /// Timeout for heartbeat response before considering connection dead. + /// Default: 10 seconds + /// + public TimeSpan HeartbeatTimeout { get; set; } = TimeSpan.FromSeconds(10); + + /// + /// Maximum number of messages in subscription channel before dropping oldest. + /// Default: 1000 + /// + public int MaxChannelCapacity { get; set; } = 1000; + + /// + /// Timeout for pending requests before they are cleaned up. + /// Default: 5 minutes + /// + public TimeSpan RequestTimeout { get; set; } = TimeSpan.FromMinutes(5); + + /// + /// Interval for cleaning up stale pending requests. + /// Default: 1 minute + /// + public TimeSpan RequestCleanupInterval { get; set; } = TimeSpan.FromMinutes(1); +} From bbb07ea8c67d6a03b2fc92fcdd8c4594a289336b Mon Sep 17 00:00:00 2001 From: d4rp4t Date: Tue, 27 Jan 2026 14:34:18 +0100 Subject: [PATCH 56/70] don't specify enumerable type in method params --- .../Handlers/MeltHandlerBolt11.cs | 2 +- .../Handlers/MeltHandlerBolt12.cs | 2 +- .../Abstractions/Interfaces/IMeltHandler.cs | 2 +- .../Interfaces/IMeltQuoteBuilder.cs | 2 +- .../Interfaces/IMintQuoteBuilder.cs | 2 +- .../Abstractions/Interfaces/IProofSelector.cs | 2 +- .../Abstractions/Interfaces/ISwapBuilder.cs | 2 +- .../Abstractions/Interfaces/IWalletBuilder.cs | 6 +-- DotNut/Abstractions/MeltQuoteBuilder.cs | 4 +- DotNut/Abstractions/MintQuoteBuilder.cs | 12 +++-- DotNut/Abstractions/Nut10Helper.cs | 2 +- DotNut/Abstractions/ProofSelector.cs | 12 ++--- DotNut/Abstractions/SwapBuilder.cs | 4 +- DotNut/Abstractions/Utils.cs | 44 ++++++++++++------- DotNut/Abstractions/Wallet.cs | 18 ++++---- 15 files changed, 69 insertions(+), 47 deletions(-) diff --git a/DotNut/Abstractions/Handlers/MeltHandlerBolt11.cs b/DotNut/Abstractions/Handlers/MeltHandlerBolt11.cs index 1101ae4..f58442a 100644 --- a/DotNut/Abstractions/Handlers/MeltHandlerBolt11.cs +++ b/DotNut/Abstractions/Handlers/MeltHandlerBolt11.cs @@ -12,7 +12,7 @@ public class MeltHandlerBolt11( { public PostMeltQuoteBolt11Response GetQuote() => quote; - public async Task> Melt(List inputs, CancellationToken ct = default) + public async Task> Melt(IEnumerable inputs, CancellationToken ct = default) { //we're operating on copy here since later the proof state is mutated in stripFingerprints var proofs = inputs.DeepCopyList(); diff --git a/DotNut/Abstractions/Handlers/MeltHandlerBolt12.cs b/DotNut/Abstractions/Handlers/MeltHandlerBolt12.cs index a7442d5..c115830 100644 --- a/DotNut/Abstractions/Handlers/MeltHandlerBolt12.cs +++ b/DotNut/Abstractions/Handlers/MeltHandlerBolt12.cs @@ -13,7 +13,7 @@ public class MeltHandlerBolt12( { public PostMeltQuoteBolt12Response GetQuote() => quote; - public async Task> Melt(List inputs, CancellationToken ct = default) + public async Task> Melt(IEnumerable inputs, CancellationToken ct = default) { //we're operating on copy here since later the proof state is mutated in stripFingerprints var proofs = inputs.DeepCopyList(); diff --git a/DotNut/Abstractions/Interfaces/IMeltHandler.cs b/DotNut/Abstractions/Interfaces/IMeltHandler.cs index 5944a15..f65cfb1 100644 --- a/DotNut/Abstractions/Interfaces/IMeltHandler.cs +++ b/DotNut/Abstractions/Interfaces/IMeltHandler.cs @@ -5,5 +5,5 @@ public interface IMeltHandler; public interface IMeltHandler : IMeltHandler { TQuote GetQuote(); - Task Melt(List inputs, CancellationToken ct = default); + Task Melt(IEnumerable inputs, CancellationToken ct = default); } diff --git a/DotNut/Abstractions/Interfaces/IMeltQuoteBuilder.cs b/DotNut/Abstractions/Interfaces/IMeltQuoteBuilder.cs index 06ef3b2..d67ef53 100644 --- a/DotNut/Abstractions/Interfaces/IMeltQuoteBuilder.cs +++ b/DotNut/Abstractions/Interfaces/IMeltQuoteBuilder.cs @@ -21,7 +21,7 @@ public interface IMeltQuoteBuilder /// /// Optional. Supply previously generated blank outputs instead of deriving them. /// - IMeltQuoteBuilder WithBlankOutputs(List blankOutputs); + IMeltQuoteBuilder WithBlankOutputs(IEnumerable blankOutputs); /// /// Optional. Provide private keys for P2PK proofs associated with the inputs. diff --git a/DotNut/Abstractions/Interfaces/IMintQuoteBuilder.cs b/DotNut/Abstractions/Interfaces/IMintQuoteBuilder.cs index df2779b..2d66cdb 100644 --- a/DotNut/Abstractions/Interfaces/IMintQuoteBuilder.cs +++ b/DotNut/Abstractions/Interfaces/IMintQuoteBuilder.cs @@ -33,7 +33,7 @@ public interface IMintQuoteBuilder /// /// Optional. Provide precomputed outputs so blinding factors and secrets are reused safely. /// - IMintQuoteBuilder WithOutputs(List outputs); + IMintQuoteBuilder WithOutputs(IEnumerable outputs); /// /// Optional. Provide description for the mint invoice. diff --git a/DotNut/Abstractions/Interfaces/IProofSelector.cs b/DotNut/Abstractions/Interfaces/IProofSelector.cs index 61ca92c..9b9f370 100644 --- a/DotNut/Abstractions/Interfaces/IProofSelector.cs +++ b/DotNut/Abstractions/Interfaces/IProofSelector.cs @@ -3,7 +3,7 @@ namespace DotNut.Abstractions; public interface IProofSelector { Task SelectProofsToSend( - List proofs, + IEnumerable proofsToSelectFrom, ulong amountToSend, bool includeFees = false, CancellationToken ct = default diff --git a/DotNut/Abstractions/Interfaces/ISwapBuilder.cs b/DotNut/Abstractions/Interfaces/ISwapBuilder.cs index 927c08b..de22d4b 100644 --- a/DotNut/Abstractions/Interfaces/ISwapBuilder.cs +++ b/DotNut/Abstractions/Interfaces/ISwapBuilder.cs @@ -23,7 +23,7 @@ public interface ISwapBuilder /// /// Optional. Supply custom blank outputs instead of deriving them automatically. /// - ISwapBuilder ForOutputs(List outputs); + ISwapBuilder ForOutputs(IEnumerable outputs); /// /// Optional. Toggle DLEQ verification for incoming proofs. diff --git a/DotNut/Abstractions/Interfaces/IWalletBuilder.cs b/DotNut/Abstractions/Interfaces/IWalletBuilder.cs index e8731fc..b6faccd 100644 --- a/DotNut/Abstractions/Interfaces/IWalletBuilder.cs +++ b/DotNut/Abstractions/Interfaces/IWalletBuilder.cs @@ -148,7 +148,7 @@ public interface IWalletBuilder : IDisposable /// Outputs /// If keys not set. If Mnemonic set, but no Counter. Task> CreateOutputs( - List amounts, + IEnumerable amounts, KeysetId id, CancellationToken ct = default ); @@ -162,7 +162,7 @@ Task> CreateOutputs( /// Outputs /// If no keysetID stored in wallet. Task> CreateOutputs( - List amounts, + IEnumerable amounts, string unit, CancellationToken ct = default ); @@ -241,7 +241,7 @@ Task> CreateOutputs( /// /// Task SelectProofsToSend( - List proofs, + IEnumerable proofs, ulong amount, bool includeFees, CancellationToken ct = default diff --git a/DotNut/Abstractions/MeltQuoteBuilder.cs b/DotNut/Abstractions/MeltQuoteBuilder.cs index 8deae7a..04bc696 100644 --- a/DotNut/Abstractions/MeltQuoteBuilder.cs +++ b/DotNut/Abstractions/MeltQuoteBuilder.cs @@ -34,9 +34,9 @@ public IMeltQuoteBuilder WithUnit(string unit) return this; } - public IMeltQuoteBuilder WithBlankOutputs(List blankOutputs) + public IMeltQuoteBuilder WithBlankOutputs(IEnumerable blankOutputs) { - this._blankOutputs = blankOutputs; + this._blankOutputs = blankOutputs as List ?? blankOutputs.ToList(); return this; } diff --git a/DotNut/Abstractions/MintQuoteBuilder.cs b/DotNut/Abstractions/MintQuoteBuilder.cs index 5b119aa..ac9d184 100644 --- a/DotNut/Abstractions/MintQuoteBuilder.cs +++ b/DotNut/Abstractions/MintQuoteBuilder.cs @@ -60,13 +60,19 @@ public IMintQuoteBuilder WithKeyset(KeysetId keysetId) return this; } - public IMintQuoteBuilder WithOutputs(List outputs) + public IMintQuoteBuilder WithOutputs(IEnumerable outputs) { - this._outputs = outputs; - if (outputs.Any(o => o.BlindedMessage.Id != outputs[0].BlindedMessage.Id)) + var os = outputs as List ?? outputs.ToList(); + if (os.Count == 0) + { + throw new ArgumentException("Outputs collection cannot be empty."); + } + if (os.Any(o => o.BlindedMessage.Id != os[0].BlindedMessage.Id)) { throw new ArgumentException("Every output must have the same keyset id!"); } + + this._outputs = os; return this; } diff --git a/DotNut/Abstractions/Nut10Helper.cs b/DotNut/Abstractions/Nut10Helper.cs index d449505..9d50a99 100644 --- a/DotNut/Abstractions/Nut10Helper.cs +++ b/DotNut/Abstractions/Nut10Helper.cs @@ -3,7 +3,7 @@ namespace DotNut.Abstractions; -public static class Nut10Helper +internal static class Nut10Helper { public static void MaybeProcessNut10( List privKeys, diff --git a/DotNut/Abstractions/ProofSelector.cs b/DotNut/Abstractions/ProofSelector.cs index 36e89e1..c5d7c6d 100644 --- a/DotNut/Abstractions/ProofSelector.cs +++ b/DotNut/Abstractions/ProofSelector.cs @@ -55,12 +55,14 @@ private ulong GetProofFeePPK(Proof proof) } public async Task SelectProofsToSend( - List proofs, + IEnumerable proofs, ulong amountToSend, bool includeFees = false, CancellationToken ct = default ) { + var proofsToSelectFrom = proofs as List ?? proofs.ToList(); + // Init vars const int MAX_TRIALS = 60; // 40-80 is optimal (per RGLI paper) const double MAX_OVRPCT = 0; // Acceptable close match overage (percent) @@ -164,7 +166,7 @@ double CalculateDelta(ulong amount, ulong feePPK) */ ulong totalAmount = 0; ulong totalFeePPK = 0; - var proofWithFees = proofs + var proofWithFees = proofsToSelectFrom .Select(p => { ulong ppkfee = GetProofFeePPK(p); @@ -235,7 +237,7 @@ double CalculateDelta(ulong amount, ulong feePPK) double totalNetSum = SumExFees(totalAmount, totalFeePPK); if (amountToSend <= 0 || amountToSend > totalNetSum) { - return new SendResponse { Keep = proofs, Send = new List() }; + return new SendResponse { Keep = proofsToSelectFrom, Send = new List() }; } // Max acceptable amount for non-exact matches @@ -405,11 +407,11 @@ double CalculateDelta(ulong amount, ulong feePPK) { var bestProofs = bestSubset.Select(obj => obj.Proof).ToList(); var bestProofCs = bestProofs.Select(p => p.C).ToHashSet(); - var keep = proofs.Where(p => !bestProofCs.Contains(p.C)).ToList(); + var keep = proofsToSelectFrom.Where(p => !bestProofCs.Contains(p.C)).ToList(); return new SendResponse { Keep = keep, Send = bestProofs }; } - return new SendResponse { Keep = proofs, Send = new List() }; + return new SendResponse { Keep = proofsToSelectFrom, Send = new List() }; } } diff --git a/DotNut/Abstractions/SwapBuilder.cs b/DotNut/Abstractions/SwapBuilder.cs index 7b1e9b9..40dc676 100644 --- a/DotNut/Abstractions/SwapBuilder.cs +++ b/DotNut/Abstractions/SwapBuilder.cs @@ -59,9 +59,9 @@ public ISwapBuilder FromInputs(IEnumerable proofs) return this; } - public ISwapBuilder ForOutputs(List outputs) + public ISwapBuilder ForOutputs(IEnumerable outputs) { - this._outputs = outputs; + this._outputs = outputs.DeepCopyList(); return this; } diff --git a/DotNut/Abstractions/Utils.cs b/DotNut/Abstractions/Utils.cs index bd72885..cc6e542 100644 --- a/DotNut/Abstractions/Utils.cs +++ b/DotNut/Abstractions/Utils.cs @@ -39,6 +39,8 @@ public static List SplitToProofsAmounts(ulong paymentAmount, Keyset keyse /// Amount that blank outputs have to cover /// Active keyset id which will sign outputs /// Keys for given KeysetId + /// Bip39 mnemonic for Nut13 deterministic secret derivation + /// Nut13 counter, for current keysetId. /// Blank Outputs public static List CreateBlankOutputs( ulong amount, @@ -84,21 +86,24 @@ public static int CalculateNumberOfBlankOutputs(ulong amountToCover) /// /// public static List CreateOutputs( - List amounts, + IEnumerable amounts, KeysetId keysetId, Keyset keys, NBitcoin.BIP39.Mnemonic? mnemonic = null, uint? counter = null ) { - if (amounts.Any(a => !keys.Keys.Contains(a))) + var amountsList = amounts as IReadOnlyList + ?? amounts.ToList(); + + if (amountsList.Any(a => !keys.Keys.Contains(a))) throw new ArgumentException("Invalid amounts"); - var outputs = new List(amounts.Count); + var outputs = new List(amountsList.Count); if (mnemonic is not null && counter is { } c) { - for (uint i = 0; i < amounts.Count; i++) + for (uint i = 0; i < amountsList.Count; i++) { var secret = mnemonic.DeriveSecret(keysetId, c + i); var r = new PrivKey(mnemonic.DeriveBlindingFactor(keysetId, c + i)); @@ -107,7 +112,7 @@ public static List CreateOutputs( { BlindedMessage = new BlindedMessage { - Amount = amounts[(int)i], + Amount = amountsList[(int)i], B_ = B_, Id = keysetId, }, @@ -119,7 +124,7 @@ public static List CreateOutputs( return outputs; } - foreach (var amount in amounts) + foreach (var amount in amountsList) { var secret = RandomSecret(); var r = RandomPrivkey(); @@ -308,27 +313,34 @@ public static Proof ConstructProofFromPromise( } public static List ConstructProofsFromPromises( - List promises, - List outputs, + IEnumerable promises, + IEnumerable outputs, Keyset keys ) { - List proofs = new List(); - for (int i = 0; i < promises.Count; i++) + var bs = promises as IReadOnlyList ?? promises.ToList(); + var os = outputs as IReadOnlyList ?? outputs.ToList(); + if (os.Count < bs.Count) { - if (!keys.TryGetValue(promises[i].Amount, out var key)) + throw new ArgumentException("Outputs must as least equal amount of elements!"); + } + + List proofs = new List(bs.Count); + for (int i = 0; i < bs.Count; i++) + { + if (!keys.TryGetValue(bs[i].Amount, out var key)) { throw new ArgumentException( - $"Provided keyset doesn't contain PubKey for amount {promises[i].Amount}" + $"Provided keyset doesn't contain PubKey for amount {bs[i].Amount}" ); } var proof = ConstructProofFromPromise( - promises[i], - outputs[i].BlindingFactor, - outputs[i].Secret, + bs[i], + os[i].BlindingFactor, + os[i].Secret, key, - outputs[i].P2BkE + os[i].P2BkE ); proofs.Add(proof); } diff --git a/DotNut/Abstractions/Wallet.cs b/DotNut/Abstractions/Wallet.cs index 766f41b..5afe1d7 100644 --- a/DotNut/Abstractions/Wallet.cs +++ b/DotNut/Abstractions/Wallet.cs @@ -292,11 +292,12 @@ public async Task GetInfo(bool forceRefresh = false, CancellationToken } public async Task> CreateOutputs( - List amounts, + IEnumerable amounts, KeysetId id, CancellationToken ct = default ) { + var amountsList = amounts as IReadOnlyList ?? amounts.ToList(); await _maybeSyncKeys(ct); if (this._keys.Count == 0) { @@ -312,7 +313,7 @@ public async Task> CreateOutputs( } if (this._mnemonic == null) { - return Utils.CreateOutputs(amounts, id, keyset.Keys); + return Utils.CreateOutputs(amountsList, id, keyset.Keys); } if (this._counter == null) @@ -326,29 +327,30 @@ public async Task> CreateOutputs( var counterValue = await this._counter.GetCounterForId(id, ct); if (!_shouldBumpCounter) { - return Utils.CreateOutputs(amounts, id, keyset.Keys, this._mnemonic, counterValue); + return Utils.CreateOutputs(amountsList, id, keyset.Keys, this._mnemonic, counterValue); } - await this._counter.IncrementCounter(id, (uint)amounts.Count, ct); - return Utils.CreateOutputs(amounts, id, keyset.Keys, this._mnemonic, counterValue); + await this._counter.IncrementCounter(id, (uint)amountsList.Count, ct); + return Utils.CreateOutputs(amountsList, id, keyset.Keys, this._mnemonic, counterValue); } public async Task> CreateOutputs( - List amounts, + IEnumerable amounts, string unit, CancellationToken ct = default ) { + var amountsList = amounts as IReadOnlyList ?? amounts.ToList(); var keysetId = await this.GetActiveKeysetId(unit, ct); if (keysetId == null) { throw new ArgumentNullException(nameof(keysetId)); } - return await this.CreateOutputs(amounts, keysetId, ct); + return await this.CreateOutputs(amountsList, keysetId, ct); } public async Task SelectProofsToSend( - List proofs, + IEnumerable proofs, ulong amount, bool includeFees, CancellationToken ct = default From 1e3f5b0fb65fb9c4c17f14ee2688eb7ee81efba5 Mon Sep 17 00:00:00 2001 From: d4rp4t Date: Wed, 28 Jan 2026 00:56:54 +0100 Subject: [PATCH 57/70] atomic counter operations --- DotNut/Abstractions/InMemoryCounter.cs | 16 ++++++++++++++++ DotNut/Abstractions/Interfaces/ICounter.cs | 5 +++++ DotNut/Abstractions/Wallet.cs | 7 ++++--- 3 files changed, 25 insertions(+), 3 deletions(-) diff --git a/DotNut/Abstractions/InMemoryCounter.cs b/DotNut/Abstractions/InMemoryCounter.cs index 6b1983e..30d1feb 100644 --- a/DotNut/Abstractions/InMemoryCounter.cs +++ b/DotNut/Abstractions/InMemoryCounter.cs @@ -31,6 +31,22 @@ public Task IncrementCounter( return Task.FromResult(next); } + public Task<(uint oldValue, uint newValue)> FetchAndIncrement(KeysetId keysetId, uint bumpBy = 1, CancellationToken ct = default) + { + uint oldValue = 0; + uint newValue = _counter.AddOrUpdate( + keysetId, + bumpBy, + (_, current) => + { + oldValue = current; + return current + bumpBy; + }); + + return Task.FromResult((oldValue, newValue)); + } + + public Task SetCounter(KeysetId keysetId, uint counter, CancellationToken ct = default) { _counter[keysetId] = counter; diff --git a/DotNut/Abstractions/Interfaces/ICounter.cs b/DotNut/Abstractions/Interfaces/ICounter.cs index 81f4c07..27fb30f 100644 --- a/DotNut/Abstractions/Interfaces/ICounter.cs +++ b/DotNut/Abstractions/Interfaces/ICounter.cs @@ -15,6 +15,11 @@ public Task IncrementCounter( uint bumpBy = 1, CancellationToken ct = default ); + public Task<(uint oldValue, uint newValue)> FetchAndIncrement( + KeysetId keysetId, + uint bumpBy = 1, + CancellationToken ct = default + ); public Task SetCounter(KeysetId keysetId, uint counter, CancellationToken ct = default); public Task> Export(); } diff --git a/DotNut/Abstractions/Wallet.cs b/DotNut/Abstractions/Wallet.cs index 5afe1d7..78f6112 100644 --- a/DotNut/Abstractions/Wallet.cs +++ b/DotNut/Abstractions/Wallet.cs @@ -324,14 +324,15 @@ public async Task> CreateOutputs( ); } - var counterValue = await this._counter.GetCounterForId(id, ct); + if (!_shouldBumpCounter) { + var counterValue = await this._counter.GetCounterForId(id, ct); return Utils.CreateOutputs(amountsList, id, keyset.Keys, this._mnemonic, counterValue); } - await this._counter.IncrementCounter(id, (uint)amountsList.Count, ct); - return Utils.CreateOutputs(amountsList, id, keyset.Keys, this._mnemonic, counterValue); + var (old, @new) = await this._counter.FetchAndIncrement(id, (uint)amountsList.Count, ct); + return Utils.CreateOutputs(amountsList, id, keyset.Keys, this._mnemonic, old); } public async Task> CreateOutputs( From 5531a39a6f20e69ab23352c853ca739ef7976e9c Mon Sep 17 00:00:00 2001 From: Andrew Camilleri Date: Thu, 29 Jan 2026 11:05:44 +0100 Subject: [PATCH 58/70] Update DotNut/Abstractions/SwapBuilder.cs Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> --- DotNut/Abstractions/SwapBuilder.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/DotNut/Abstractions/SwapBuilder.cs b/DotNut/Abstractions/SwapBuilder.cs index 40dc676..d62f5b3 100644 --- a/DotNut/Abstractions/SwapBuilder.cs +++ b/DotNut/Abstractions/SwapBuilder.cs @@ -158,7 +158,7 @@ await _wallet.GetActiveKeysetId(this._unit, ct) if (keyset == null) { throw new InvalidOperationException( - $"Can't find keys for keyset id ${proof.Id}" + $"Can't find keys for keyset id {proof.Id}" ); } From 44ce95db612f6a938fc71c3d100af1a820b148f3 Mon Sep 17 00:00:00 2001 From: Andrew Camilleri Date: Thu, 29 Jan 2026 11:06:24 +0100 Subject: [PATCH 59/70] Update DotNut/Abstractions/MintQuoteBuilder.cs Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> --- DotNut/Abstractions/MintQuoteBuilder.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/DotNut/Abstractions/MintQuoteBuilder.cs b/DotNut/Abstractions/MintQuoteBuilder.cs index ac9d184..de1c6f9 100644 --- a/DotNut/Abstractions/MintQuoteBuilder.cs +++ b/DotNut/Abstractions/MintQuoteBuilder.cs @@ -154,7 +154,7 @@ public async Task>> Proces { await this._wallet._maybeSyncKeys(ct); - var api = await this._wallet.GetMintApi(); + var api = await this._wallet.GetMintApi(ct); if (api is null) { throw new ArgumentNullException( From 09d9059593313bc9f857367469b7dbd2075899be Mon Sep 17 00:00:00 2001 From: d4rp4t Date: Mon, 2 Feb 2026 18:58:11 +0100 Subject: [PATCH 60/70] fix WithMint --- DotNut/Abstractions/Wallet.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/DotNut/Abstractions/Wallet.cs b/DotNut/Abstractions/Wallet.cs index 78f6112..fafe83d 100644 --- a/DotNut/Abstractions/Wallet.cs +++ b/DotNut/Abstractions/Wallet.cs @@ -43,7 +43,9 @@ public IWalletBuilder WithMint(ICashuApi mintApi, bool canDispose = false) public IWalletBuilder WithMint(string mintUrl) { - var httpClient = new HttpClient { BaseAddress = new Uri(mintUrl) }; + //add trailing / so mint like https://mint.minibits.cash/Bitcoin will work correctly + var mintUri = new Uri(mintUrl + "/"); + var httpClient = new HttpClient { BaseAddress = mintUri }; _mintApi = new CashuHttpClient(httpClient, true); _ownsHttpClient = true; return this; From b7aeb19686f41c998ff3074bb973d84c749cbf96 Mon Sep 17 00:00:00 2001 From: d4rp4t Date: Tue, 27 Jan 2026 21:38:02 +0100 Subject: [PATCH 61/70] fix gitignore --- .gitignore | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index d0b5773..62e59ab 100644 --- a/.gitignore +++ b/.gitignore @@ -2,4 +2,4 @@ **/obj .idea .vs -+*.DotSettings.user \ No newline at end of file +*.DotSettings.user \ No newline at end of file From c3ce049b86d07ef66ec2020c242df3afcd751373 Mon Sep 17 00:00:00 2001 From: d4rp4t Date: Mon, 2 Feb 2026 19:13:24 +0100 Subject: [PATCH 62/70] fix todos --- DotNut.Tests/UnitTest1.cs | 6 +++--- DotNut/Abstractions/Wallet.cs | 4 ++-- DotNut/NUT01/Keyset.cs | 8 ++++---- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/DotNut.Tests/UnitTest1.cs b/DotNut.Tests/UnitTest1.cs index a568ef9..d1c68cb 100644 --- a/DotNut.Tests/UnitTest1.cs +++ b/DotNut.Tests/UnitTest1.cs @@ -232,7 +232,7 @@ public void Nut01Tests_Keysets_Valid(string keyset) (byte)1, "sat", 100UL, - "2059210353" + 2059210353 )] [InlineData( "01ab6aa4ff30390da34986d84be5274b48ad7a74265d791095bfc39f4098d9764f", @@ -240,7 +240,7 @@ public void Nut01Tests_Keysets_Valid(string keyset) (byte)0x01, "sat", 0UL, - "2059210353" + 2059210353 )] [InlineData( "012fbb01a4e200c76df911eeba3b8fe1831202914b24664f4bccbd25852a6708f8", @@ -255,7 +255,7 @@ public void Nut02Tests_KeysetIdMatch( byte? version = null, string? unit = null, ulong? inputFee = null, - string? finalExpiration = null + ulong? finalExpiration = null ) { var keysetIdParsed = new KeysetId(keysetId); diff --git a/DotNut/Abstractions/Wallet.cs b/DotNut/Abstractions/Wallet.cs index fafe83d..e84b0b6 100644 --- a/DotNut/Abstractions/Wallet.cs +++ b/DotNut/Abstractions/Wallet.cs @@ -453,10 +453,10 @@ internal void _ensureApiConnected(string? msg = null) var keysRaw = await _mintApi!.GetKeys(ct); foreach (var keysetItemResponse in keysRaw.Keysets) { - //todo new derivation var isKeysetIdValid = keysetItemResponse.Keys.VerifyKeysetId( keysetItemResponse.Id, keysetItemResponse.Unit, + keysetItemResponse.InputFeePpk, keysetItemResponse.FinalExpiry ); if (!isKeysetIdValid) @@ -487,10 +487,10 @@ internal void _ensureApiConnected(string? msg = null) { return null; } - //todo new keysetId derivation var isKeysetIdValid = keysRaw.Keys.VerifyKeysetId( keysRaw.Id, keysRaw.Unit, + keysRaw.InputFeePpk, keysRaw.FinalExpiry ); if (!isKeysetIdValid) diff --git a/DotNut/NUT01/Keyset.cs b/DotNut/NUT01/Keyset.cs index 760dfc9..e2c1ec2 100644 --- a/DotNut/NUT01/Keyset.cs +++ b/DotNut/NUT01/Keyset.cs @@ -14,7 +14,7 @@ public KeysetId GetKeysetId( byte version = 0x00, string? unit = null, ulong? inputFeePpk = null, - string? finalExpiration = null + ulong? finalExpiration = null ) { // 1 - sort public keys by their amount in ascending order @@ -82,10 +82,10 @@ public KeysetId GetKeysetId( } // 5 - If a final expiration is specified, add the UTF8-encoded string prefixed with "|final_expiry:" (e.g. "|final_expiry:1896187313") - if (!string.IsNullOrWhiteSpace(finalExpiration)) + if (finalExpiration is not null) { var expiryBytes = Encoding.UTF8.GetBytes( - $"|final_expiry:{finalExpiration.Trim()}" + $"|final_expiry:{finalExpiration.ToString()}" ); stream.Write(expiryBytes, 0, expiryBytes.Length); } @@ -107,7 +107,7 @@ public bool VerifyKeysetId( KeysetId keysetId, string? unit = null, ulong? inputFeePpk = null, - string? finalExpiration = null + ulong? finalExpiration = null ) { byte version = keysetId.GetVersion(); From 9877c25ddbabff1981438d3ec806f2214aa411e1 Mon Sep 17 00:00:00 2001 From: d4rp4t Date: Sat, 7 Feb 2026 13:36:08 +0100 Subject: [PATCH 63/70] Add GetOutputs to MintHandlers --- DotNut/Abstractions/Handlers/MintHandlerBolt11.cs | 1 + DotNut/Abstractions/Handlers/MintHandlerBolt12.cs | 2 ++ DotNut/Abstractions/Interfaces/IMintHandler.cs | 2 ++ 3 files changed, 5 insertions(+) diff --git a/DotNut/Abstractions/Handlers/MintHandlerBolt11.cs b/DotNut/Abstractions/Handlers/MintHandlerBolt11.cs index ab08895..8b879f0 100644 --- a/DotNut/Abstractions/Handlers/MintHandlerBolt11.cs +++ b/DotNut/Abstractions/Handlers/MintHandlerBolt11.cs @@ -32,6 +32,7 @@ public IMintHandler> SignWithPrivkey(Pr } public PostMintQuoteBolt11Response GetQuote() => postMintQuoteBolt11Response; + public List GetOutputs() => outputs; public async Task> Mint(CancellationToken ct = default) { diff --git a/DotNut/Abstractions/Handlers/MintHandlerBolt12.cs b/DotNut/Abstractions/Handlers/MintHandlerBolt12.cs index 7e00381..1c5b3ce 100644 --- a/DotNut/Abstractions/Handlers/MintHandlerBolt12.cs +++ b/DotNut/Abstractions/Handlers/MintHandlerBolt12.cs @@ -33,6 +33,8 @@ public IMintHandler> SignWithPrivkey(Pr } public PostMintQuoteBolt12Response GetQuote() => quote; + public List GetOutputs() => outputs; + public async Task> Mint(CancellationToken ct = default) { diff --git a/DotNut/Abstractions/Interfaces/IMintHandler.cs b/DotNut/Abstractions/Interfaces/IMintHandler.cs index 0dc5744..c4f7b42 100644 --- a/DotNut/Abstractions/Interfaces/IMintHandler.cs +++ b/DotNut/Abstractions/Interfaces/IMintHandler.cs @@ -9,5 +9,7 @@ public interface IMintHandler : IMintHandler public IMintHandler SignWithPrivkey(string privKeyHex); TQuote GetQuote(); + List GetOutputs(); + Task Mint(CancellationToken ct = default); } From 078e721915509fc6fffc2e41f7aab57e9b7c3db7 Mon Sep 17 00:00:00 2001 From: d4rp4t Date: Tue, 3 Mar 2026 10:26:36 +0100 Subject: [PATCH 64/70] update readme and fix failing tests --- DotNut.Tests/UnitTest1.cs | 4 +- README.md | 298 ++++++++++++++++---------------------- 2 files changed, 129 insertions(+), 173 deletions(-) diff --git a/DotNut.Tests/UnitTest1.cs b/DotNut.Tests/UnitTest1.cs index d1c68cb..1e8b9e1 100644 --- a/DotNut.Tests/UnitTest1.cs +++ b/DotNut.Tests/UnitTest1.cs @@ -232,7 +232,7 @@ public void Nut01Tests_Keysets_Valid(string keyset) (byte)1, "sat", 100UL, - 2059210353 + 2059210353UL )] [InlineData( "01ab6aa4ff30390da34986d84be5274b48ad7a74265d791095bfc39f4098d9764f", @@ -240,7 +240,7 @@ public void Nut01Tests_Keysets_Valid(string keyset) (byte)0x01, "sat", 0UL, - 2059210353 + 2059210353UL )] [InlineData( "012fbb01a4e200c76df911eeba3b8fe1831202914b24664f4bccbd25852a6708f8", diff --git a/README.md b/README.md index a5ebc14..3b99e29 100644 --- a/README.md +++ b/README.md @@ -1,231 +1,187 @@ -# DotNut 🥜 +# DotNut -A complete C# implementation of the [Cashu protocol](https://cashu.space) - privacy-preserving electronic cash built on Bitcoin. +C# library for the [Cashu protocol](https://cashu.space) - a Chaumian e-cash system built on Bitcoin/Lightning. [![NuGet](https://img.shields.io/nuget/v/DotNut.svg)](https://www.nuget.org/packages/DotNut/) [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) -## What is Cashu? - -Cashu is a free and open-source Chaumian e-cash system built for Bitcoin. It offers near-perfect privacy for users and can serve as an excellent custodial scaling solution. DotNut provides a full-featured C# client library for interacting with Cashu mints. - ## Installation ```bash dotnet add package DotNut ``` -## Quick Start +## Usage -### 1. Connect to a Mint +The main entry point is the `Wallet` class, which exposes a fluent builder for connecting to a mint and performing operations. -```csharp -using DotNut; -using DotNut.Api; +### Setup -// Connect to a Cashu mint -var httpClient = new HttpClient(); -httpClient.BaseAddress = new Uri("https://testnut.cashu.space/"); -var client = new CashuHttpClient(httpClient); - -// Get mint information -var info = await client.GetInfo(); -Console.WriteLine($"Connected to: {info.Name}"); +```csharp +var wallet = Wallet.Create() + .WithMint("https://testnut.cashu.space") + .WithMnemonic("your twelve word mnemonic phrase here...") + .WithCounter(new InMemoryCounter()); ``` -### 2. Create and Send Tokens +### Mint (Lightning → tokens) ```csharp -using DotNut.Encoding; - -// Create a token from proofs (obtained from minting) -var token = new CashuToken -{ - Unit = "sat", - Memo = "Payment for coffee ☕", - Tokens = new List - { - new CashuToken.Token - { - Mint = "https://testnut.cashu.space", - Proofs = myProofs // Your token proofs - } - } -}; +var mintHandler = await wallet + .CreateMintQuote() + .WithAmount(1000) + .WithUnit("sat") + .ProcessAsyncBolt11(); -// Encode for sharing (creates a cashu token string) -string encodedToken = token.Encode("B"); // V4 format (compact) -Console.WriteLine($"Token to share: {encodedToken}"); +// Pay the Lightning invoice +Console.WriteLine(mintHandler.GetQuote().Request); -// Receive and decode a token -var receivedToken = CashuTokenHelper.Decode(encodedToken, out string version); -Console.WriteLine($"Received {receivedToken.TotalAmount()} sats"); +// After payment, mint the tokens +List proofs = await mintHandler.Mint(); ``` -### 3. Basic Mint Operations +### Swap (rebalance / receive token) ```csharp -using DotNut.ApiModels.Mint; - -// Create a mint quote for 1000 sats via Lightning -var mintQuote = await client.CreateMintQuote( - "bolt11", - new PostMintQuoteBolt11Request { Amount = 1000, Unit = "sat" } -); - -Console.WriteLine($"Pay this invoice: {mintQuote.Request}"); -// After paying the Lightning invoice, mint your tokens... - -// Create a melt quote to convert tokens back to Lightning -var meltQuote = await client.CreateMeltQuote( - "bolt11", - new PostMeltQuoteBolt11Request - { - Request = "lnbc1000n1...", // Lightning invoice to pay - Unit = "sat" - } -); +// From a cashuB... token string +List proofs = await wallet + .Swap() + .WithDLEQVerification() + .ProcessAsync(); // pass token string to SwapBuilder or use FromInputs() + +// From raw proofs +List rebalanced = await wallet + .Swap() + .FromInputs(existingProofs) + .ProcessAsync(); ``` -## Core Concepts - -### Tokens and Proofs -- **CashuToken**: Container for one or more tokens from different mints -- **Proof**: Cryptographic proof representing a specific amount -- **Secret**: The secret behind each proof (can be simple strings or complex conditions) - -### Privacy Features -- **Blind Signatures**: Mint doesn't know which tokens belong to whom -- **DLEQ Proofs**: Verify mint behavior without compromising privacy -- **Token Swapping**: Change denominations while maintaining privacy - -### Advanced Features -- **P2PK (Pay-to-Public-Key)**: Multi-signature spending conditions -- **HTLCs**: Hash Time-Locked Contracts for atomic swaps -- **Deterministic Secrets**: Generate secrets from mnemonic phrases - -## Working with Secrets +### Melt (tokens → Lightning) ```csharp -using DotNut; +var meltHandler = await wallet + .CreateMeltQuote() + .WithInvoice("lnbc...") + .WithUnit("sat") + .ProcessAsyncBolt11(); -// Simple string secret -var secret = new StringSecret("my-random-secret"); +List changeProofs = await meltHandler.Melt(inputProofs); +``` -// Deterministic secret from mnemonic (NUT-13) -var mnemonic = new Mnemonic("abandon abandon abandon..."); -var deterministicSecret = mnemonic.DeriveSecret(keysetId, counter: 0); +### Restore (from mnemonic) -// Pay-to-Public-Key secret (NUT-11) -var p2pkBuilder = new P2PkBuilder -{ - Pubkeys = new[] { pubkey1, pubkey2 }, - SignatureThreshold = 1, // 1-of-2 multisig - SigFlag = "SIG_INPUTS" -}; -var p2pkSecret = new Nut10Secret(P2PKProofSecret.Key, p2pkBuilder.Build()); +```csharp +IEnumerable recovered = await wallet + .Restore() + .ProcessAsync(); ``` -## Token Operations +### Token encoding ```csharp -// Check if proofs are still valid -var stateRequest = new PostCheckStateRequest { Ys = proofs.Select(p => p.Y).ToArray() }; -var stateResponse = await client.CheckState(stateRequest); - -// Swap tokens to different denominations -var swapRequest = new PostSwapRequest +var token = new CashuToken { - Inputs = inputProofs, - Outputs = newBlindedMessages + Unit = "sat", + Tokens = [new CashuToken.Token { Mint = "https://testnut.cashu.space", Proofs = proofs }] }; -var swapResponse = await client.Swap(swapRequest); -// Restore tokens from secrets (if you've lost proofs) -var restoreRequest = new PostRestoreRequest { Outputs = blindedMessages }; -var restoreResponse = await client.Restore(restoreRequest); -``` +string v4 = token.Encode("B"); // cashuB... (CBOR, compact) +string v3 = token.Encode("A"); // cashuA... (JSON) +string uri = token.Encode("B", makeUri: true); // cashu:cashuB... -## Token Encoding Formats +var decoded = CashuTokenHelper.Decode(v4, out string version); +``` -DotNut supports multiple token encoding formats: +### P2PK / HTLC spending conditions ```csharp -// V3 format (JSON-based) -string v3Token = token.Encode("A"); - -// V4 format (CBOR-based, more compact) -string v4Token = token.Encode("B"); - -// As URI for easy sharing -string tokenUri = token.Encode("B", makeUri: true); -// Result: "cashu:cashuB..." +// Mint tokens locked to a pubkey +var mintHandler = await wallet + .CreateMintQuote() + .WithAmount(500) + .WithP2PkLock(new P2PKBuilder { Pubkeys = [pubkey], SignatureThreshold = 1 }) + .ProcessAsyncBolt11(); + +// Spend P2PK-locked tokens by signing during swap +List proofs = await wallet + .Swap() + .FromInputs(lockedProofs) + .WithPrivkeys([privKey]) + .ProcessAsync(); ``` -## Error Handling +### Direct API access + +If you need raw protocol access without the wallet abstraction: ```csharp -try -{ - var response = await client.Swap(swapRequest); -} -catch (CashuProtocolException ex) -{ - Console.WriteLine($"Mint error: {ex.Error.Detail}"); - Console.WriteLine($"Error code: {ex.Error.Code}"); -} +var httpClient = new HttpClient { BaseAddress = new Uri("https://testnut.cashu.space/") }; +var api = new CashuHttpClient(httpClient); + +var info = await api.GetInfo(); +var keysets = await api.GetKeysets(); +var mintQuote = await api.CreateMintQuote( + "bolt11", new PostMintQuoteBolt11Request { Amount = 1000, Unit = "sat" } +); ``` -## Nostr Integration +### WebSockets (NUT-17) -DotNut includes a separate package for Nostr integration: +```csharp +var wallet = Wallet.Create() + .WithMint("https://testnut.cashu.space") + .WithWebsocketService(new WebsocketService()); -```bash -dotnet add package DotNut.Nostr +var ws = await wallet.GetWebsocketService(); +// subscribe to quote state changes, proof state updates, etc. ``` -This enables payment requests over Nostr (NUT-18) and other Nostr-based features. - -## Implemented Specifications - -Complete implementation of the [Cashu protocol specifications](https://github.com/cashubtc/nuts/): - -| NUT | Description | Status | -|-----|-------------|--------| -| [00](https://github.com/cashubtc/nuts/blob/main/00.md) | Cryptographic primitives | ✅ | -| [01](https://github.com/cashubtc/nuts/blob/main/01.md) | Mint public key distribution | ✅ | -| [02](https://github.com/cashubtc/nuts/blob/main/02.md) | Keysets and keyset IDs | ✅ | -| [03](https://github.com/cashubtc/nuts/blob/main/03.md) | Swapping tokens | ✅ | -| [04](https://github.com/cashubtc/nuts/blob/main/04.md) | Minting tokens | ✅ | -| [05](https://github.com/cashubtc/nuts/blob/main/05.md) | Melting tokens | ✅ | -| [06](https://github.com/cashubtc/nuts/blob/main/06.md) | Mint info | ✅ | -| [07](https://github.com/cashubtc/nuts/blob/main/07.md) | Token state check | ✅ | -| [08](https://github.com/cashubtc/nuts/blob/main/08.md) | Lightning fee return | ✅ | -| [09](https://github.com/cashubtc/nuts/blob/main/09.md) | Token restoration | ✅ | -| [10](https://github.com/cashubtc/nuts/blob/main/10.md) | Spending conditions | ✅ | -| [11](https://github.com/cashubtc/nuts/blob/main/11.md) | Pay-to-Public-Key (P2PK) | ✅ | -| [12](https://github.com/cashubtc/nuts/blob/main/12.md) | DLEQ proofs | ✅ | -| [13](https://github.com/cashubtc/nuts/blob/main/13.md) | Deterministic secrets | ✅ | -| [14](https://github.com/cashubtc/nuts/blob/main/14.md) | Hash Time-Locked Contracts | ✅ | -| [15](https://github.com/cashubtc/nuts/blob/main/15.md) | Multipath payments | ✅ | -| [18](https://github.com/cashubtc/nuts/blob/main/18.md) | Payment requests | ✅ | - +## Implemented NUTs + +| NUT | Description | +|----------------------------------------------------------|-----------------------------------------| +| [00](https://github.com/cashubtc/nuts/blob/main/00.md) | Cryptographic primitives & token format | +| [01](https://github.com/cashubtc/nuts/blob/main/01.md) | Mint public key distribution | +| [02](https://github.com/cashubtc/nuts/blob/main/02.md) | Keysets and keyset IDs | +| [03](https://github.com/cashubtc/nuts/blob/main/03.md) | Swapping tokens | +| [04](https://github.com/cashubtc/nuts/blob/main/04.md) | Minting tokens | +| [05](https://github.com/cashubtc/nuts/blob/main/05.md) | Melting tokens | +| [06](https://github.com/cashubtc/nuts/blob/main/06.md) | Mint info | +| [07](https://github.com/cashubtc/nuts/blob/main/07.md) | Token state check | +| [08](https://github.com/cashubtc/nuts/blob/main/08.md) | Lightning fee return | +| [09](https://github.com/cashubtc/nuts/blob/main/09.md) | Token restoration | +| [10](https://github.com/cashubtc/nuts/blob/main/10.md) | Spending conditions | +| [11](https://github.com/cashubtc/nuts/blob/main/11.md) | Pay-to-Public-Key (P2PK) | +| [12](https://github.com/cashubtc/nuts/blob/main/12.md) | DLEQ proofs | +| [13](https://github.com/cashubtc/nuts/blob/main/13.md) | Deterministic secrets (BIP39) | +| [14](https://github.com/cashubtc/nuts/blob/main/14.md) | Hash Time-Locked Contracts (HTLC) | +| [17](https://github.com/cashubtc/nuts/blob/main/17.md) | WebSocket subscriptions | +| [18](https://github.com/cashubtc/nuts/blob/main/18.md) | Payment requests | +| [20](https://github.com/cashubtc/nuts/blob/main/20.md) | Signature on Mint Quote | +| [23](https://github.com/cashubtc/nuts/blob/main/23.md) | BOLT11 | +| [25](https://github.com/cashubtc/nuts/blob/main/25.md) | BOLT12 | +| [26](https://github.com/cashubtc/nuts/blob/main/26.md) | Payment Request Bech32m Encoding | +| [27](https://github.com/cashubtc/nuts/blob/main/27.md) | Nostr Mint Backup | +| [28](https://github.com/cashubtc/nuts/blob/main/28.md) | Pay-to-Blinded-Key (P2BK) | + +TODO: + +| NUT | Description | +|--------------------------------------------------------|-----------------------------------------| +| [15](https://github.com/cashubtc/nuts/blob/main/15.md) | Multipath payments | +| [21](https://github.com/cashubtc/nuts/blob/main/21.md) | Clear Authentication | +| [22](https://github.com/cashubtc/nuts/blob/main/22.md) | Blind Authentication | ## Requirements -- .NET 8.0 or later -- HTTP client for mint communication - -## Contributing - -Contributions are welcome! Please feel free to submit issues, feature requests, or pull requests. +- .NET 8.0+ ## License -This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details. +MIT - see [LICENSE](LICENSE). ## Resources -- [Cashu Protocol](https://cashu.space) -- [Cashu Specifications (NUTs)](https://github.com/cashubtc/nuts/) -- [NuGet Package](https://www.nuget.org/packages/DotNut/) -- [GitHub Repository](https://github.com/Kukks/DotNut) +- [Cashu protocol](https://cashu.space) +- [NUT specifications](https://github.com/cashubtc/nuts/) +- [NuGet package](https://www.nuget.org/packages/DotNut/) +- [GitHub](https://github.com/Kukks/DotNut) From a0f76da481e6d970f36dae91907eca6482b89ebb Mon Sep 17 00:00:00 2001 From: d4rp4t Date: Sun, 8 Mar 2026 22:26:42 +0100 Subject: [PATCH 65/70] rebase format adjust --- DotNut.Demo/Program.cs | 4 +- DotNut.Tests/Integration.cs | 18 +- DotNut.Tests/UnitTest1.cs | 30 +- DotNut.Tests/UnitTests2.cs | 4 +- DotNut.sln.DotSettings.user | 14 +- .../Handlers/MintHandlerBolt11.cs | 1 + .../Handlers/MintHandlerBolt12.cs | 2 +- DotNut/Abstractions/InMemoryCounter.cs | 10 +- DotNut/Abstractions/Interfaces/ICounter.cs | 4 +- .../Abstractions/Interfaces/IMintHandler.cs | 2 +- .../Interfaces/IMintQuoteBuilder.cs | 2 +- .../Abstractions/Interfaces/ISwapBuilder.cs | 2 +- DotNut/Abstractions/MintQuoteBuilder.cs | 4 +- DotNut/Abstractions/ProofSelector.cs | 2 +- DotNut/Abstractions/SwapBuilder.cs | 13 +- DotNut/Abstractions/Utils.cs | 21 +- DotNut/Abstractions/Wallet.cs | 1 - .../Abstractions/Websockets/WebsocketEnums.cs | 25 +- .../Websockets/WebsocketService.cs | 71 +- .../PaymentRequestBech32Encoder.cs | 60 +- DotNut/Encoding/PaymentRequestEncoder.cs | 33 +- DotNut/NBitcoin/Bech32/Bech32Encoder.cs | 3392 +++++++++++++---- DotNut/NUT10/Nut10ProofSecret.cs | 20 +- DotNut/NUT11/P2PKBuilder.cs | 25 +- DotNut/NUT11/P2PKProofSecret.cs | 33 +- DotNut/NUT11/SigAllHandler.cs | 38 +- DotNut/NUT14/HTLCBuilder.cs | 37 +- DotNut/NUT14/HTLCProofSecret.cs | 81 +- DotNut/NUT18/PaymentRequest.cs | 1 - DotNut/NUT18/PaymentRequestTransportTag.cs | 7 - DotNut/{ => NUT18}/Tag.cs | 6 +- DotNut/Nut10LockingCondition.cs | 8 - DotNut/SigAllHandler.cs | 222 -- 33 files changed, 2988 insertions(+), 1205 deletions(-) rename DotNut/{ => Encoding}/PaymentRequestBech32Encoder.cs (92%) delete mode 100644 DotNut/NUT18/PaymentRequestTransportTag.cs rename DotNut/{ => NUT18}/Tag.cs (91%) delete mode 100644 DotNut/Nut10LockingCondition.cs delete mode 100644 DotNut/SigAllHandler.cs diff --git a/DotNut.Demo/Program.cs b/DotNut.Demo/Program.cs index 06ed48b..290ec2a 100644 --- a/DotNut.Demo/Program.cs +++ b/DotNut.Demo/Program.cs @@ -492,7 +492,7 @@ private static async Task P2PKDemo() // Create a 1-of-2 multisig P2PK secret Console.WriteLine("\n🏗️ Creating 1-of-2 multisig P2PK:"); - var p2pkBuilder = new P2PKBuilder + var p2pkBuilder = new P2PkBuilder { Pubkeys = new[] { pubKey1, pubKey2 }, SignatureThreshold = 1, // 1-of-2 multisig @@ -510,7 +510,7 @@ private static async Task P2PKDemo() // Create a time-locked P2PK Console.WriteLine("\n⏰ Creating time-locked P2PK:"); - var timeLockedBuilder = new P2PKBuilder + var timeLockedBuilder = new P2PkBuilder { Pubkeys = new[] { pubKey1 }, SignatureThreshold = 1, diff --git a/DotNut.Tests/Integration.cs b/DotNut.Tests/Integration.cs index 97ec33d..ed19123 100644 --- a/DotNut.Tests/Integration.cs +++ b/DotNut.Tests/Integration.cs @@ -322,6 +322,8 @@ public async Task SubscribeToMintMeltQuoteUpdates() var sub = await service.SubscribeToMintQuoteAsync(MintUrl, new[] { quote.Quote }); + //todo imo this test should be rebuilt. this is a race condition, and it's possible that quote will be marked as paid + // b4 we finish subscribing it. it should be marked as paid int connectedCount = 0; int notificationCount = 0; @@ -334,7 +336,7 @@ public async Task SubscribeToMintMeltQuoteUpdates() async () => { await connectedTcs.Task.WaitAsync(TimeSpan.FromSeconds(10)); - await Task.Delay(1000, cts.Token); + await Task.Delay(1000, cts.Token); // idk about this line. why did I wrote this? await PayInvoice(); }, cts.Token @@ -422,7 +424,7 @@ public async Task SwapP2Pk() .CreateMintQuote() .WithAmount(1337) .WithP2PkLock( - new P2PKBuilder() + new P2PkBuilder() { Pubkeys = [privKeyBob.Key.CreatePubKey()], SignatureThreshold = 1, @@ -458,7 +460,7 @@ public async Task MintMeltP2PkMultisig() .CreateMintQuote() .WithAmount(1337) .WithP2PkLock( - new P2PKBuilder() + new P2PkBuilder() { Pubkeys = [privKeyBob.Key.CreatePubKey(), privKeyAlice.Key.CreatePubKey()], SignatureThreshold = 2, @@ -508,7 +510,7 @@ public async Task MintSwapP2PkSigAll() .CreateMintQuote() .WithAmount(1337) .WithP2PkLock( - new P2PKBuilder() + new P2PkBuilder() { SigFlag = "SIG_ALL", Pubkeys = [privKeyBob.Key.CreatePubKey()], @@ -542,7 +544,7 @@ public async Task MintSwapP2Bk() var privKeyBob = new PrivKey(RandomNumberGenerator.GetHexString(64, true)); var privKeyAlice = new PrivKey(RandomNumberGenerator.GetHexString(64, true)); - var builder = new P2PKBuilder() + var builder = new P2PkBuilder() { Pubkeys = [privKeyBob.Key.CreatePubKey(), privKeyAlice.Key.CreatePubKey()], }; @@ -576,7 +578,7 @@ public async Task MintMeltP2Bk() var privKeyBob = new PrivKey(RandomNumberGenerator.GetHexString(64, true)); - var builder = new P2PKBuilder() { Pubkeys = [privKeyBob.Key.CreatePubKey()] }; + var builder = new P2PkBuilder() { Pubkeys = [privKeyBob.Key.CreatePubKey()] }; var quote = await wallet .CreateMintQuote() @@ -609,7 +611,7 @@ public async Task MintMeltP2BkSigAll() var privKeyBob = new PrivKey(RandomNumberGenerator.GetHexString(64, true)); - var builder = new P2PKBuilder() + var builder = new P2PkBuilder() { Pubkeys = [privKeyBob.Key.CreatePubKey()], SigFlag = "SIG_ALL", @@ -651,7 +653,7 @@ public async Task MintSwapP2BkSigAll() .CreateMintQuote() .WithAmount(1337) .WithP2PkLock( - new P2PKBuilder() + new P2PkBuilder() { SigFlag = "SIG_ALL", Pubkeys = [privKeyBob.Key.CreatePubKey()], diff --git a/DotNut.Tests/UnitTest1.cs b/DotNut.Tests/UnitTest1.cs index 1e8b9e1..7d56cf7 100644 --- a/DotNut.Tests/UnitTest1.cs +++ b/DotNut.Tests/UnitTest1.cs @@ -617,28 +617,6 @@ public void Nut11_SIG_ALL() ); - var validSwapRequestMultisigRefund = - "{\n \"inputs\": [\n {\n \"amount\": 2,\n \"id\": \"00bfa73302d12ffd\",\n \"secret\": \"[\\\"P2PK\\\",{\\\"nonce\\\":\\\"3e9253419a11f0a541dd6baeddecf8356fc864b5d061f12f05632bc3aee6b5c4\\\",\\\"data\\\":\\\"0343cca0e48ce9e3fdcddba4637ff8cdbf6f5ed9cfdf1873e63827e760f0ed4db5\\\",\\\"tags\\\":[[\\\"pubkeys\\\",\\\"0235e0a719f8b046cee90f55a59b1cdd6ca75ce23e49cbcd82c9e5b7310e21ebcd\\\",\\\"020443f98b356e021bae82bdfc05ff433cab21e27fca9ab7b0995aedb2e7aabc43\\\"],[\\\"locktime\\\",\\\"100\\\"],[\\\"n_sigs\\\",\\\"2\\\"],[\\\"refund\\\",\\\"026b432e62b041bf9cdae534203739c73fa506c9a2d6aa58a52bc601a1dec421e1\\\",\\\"02e3494a2e07e7f6e7d4567e0da7a563592bff1e121df2383667f15b83e9168a9e\\\"],[\\\"n_sigs_refund\\\",\\\"2\\\"],[\\\"sigflag\\\",\\\"SIG_ALL\\\"]]}]\",\n \"C\": \"026c12ee3bffa5c617debcf823bf1af6a9b47145b699f2737bba3394f0893eb869\",\n \"witness\": \"{\\\"signatures\\\":[\\\"bfe884145ce6512331324321c3946dfd812428a53656b108b59d26559a186ba2ab45e5be9ce94e2dff0d09078e25ccb82d06a8b3a63cd3dc67065b8f77292776\\\",\\\"236e5cc9c30f85a893a29a4302e41e6f2015caef4229f28fa65e2f5c9d55515cc9a1852093a81a5095055d85fd55bf4da124e55354b56e0a39e83b58b0afc197\\\"]}\"\n }\n ],\n \"outputs\": [\n {\n \"amount\": 1,\n \"id\": \"00bfa73302d12ffd\",\n \"B_\": \"038ec853d65ae1b79b5cdbc2774150b2cb288d6d26e12958a16fb33c32d9a86c39\"\n },\n {\n \"amount\": 1,\n \"id\": \"00bfa73302d12ffd\",\n \"B_\": \"03afe7c87e32d436f0957f1d70a2bca025822a84a8623e3a33aed0a167016e0ca5\"\n }\n ]\n}"; - var validSwapRequestMultisigRefundParsed = JsonSerializer.Deserialize( - validSwapRequestMultisigRefund - ); - var witness4 = JsonSerializer.Deserialize( - validSwapRequestMultisigRefundParsed.Inputs[0].Witness - ); - Assert.True( - SigAllHandler.VerifySigAllWitness( - validSwapRequestMultisigRefundParsed.Inputs, - validSwapRequestMultisigRefundParsed.Outputs, - witness4 - ) - ); - Assert.True( - SigAllHandler.VerifySigAllWitness( - validSwapRequestMultisigRefundParsed.Inputs, - validSwapRequestMultisigRefundParsed.Outputs - ) - ); - var validSwapRequestMultisigRefundLocktime = "{\n \"inputs\": [\n {\n \"amount\": 2,\n \"id\": \"00bfa73302d12ffd\",\n \"secret\": \"[\\\"P2PK\\\",{\\\"nonce\\\":\\\"9ea35553beb18d553d0a53120d0175a0991ca6109370338406eed007b26eacd1\\\",\\\"data\\\":\\\"02af21e09300af92e7b48c48afdb12e22933738cfb9bba67b27c00c679aae3ec25\\\",\\\"tags\\\":[[\\\"locktime\\\",\\\"1\\\"],[\\\"refund\\\",\\\"02637c19143c58b2c58bd378400a7b82bdc91d6dedaeb803b28640ef7d28a887ac\\\",\\\"0345c7fdf7ec7c8e746cca264bf27509eb4edb9ac421f8fbfab1dec64945a4d797\\\"],[\\\"n_sigs_refund\\\",\\\"2\\\"],[\\\"sigflag\\\",\\\"SIG_ALL\\\"]]}]\",\n \"C\": \"03dd83536fbbcbb74ccb3c87147df26753fd499cc2c095f74367fff0fb459c312e\",\n \"witness\": \"{\\\"signatures\\\":[\\\"23b58ef28cd22f3dff421121240ddd621deee83a3bc229fd67019c2e338d91e2c61577e081e1375dbab369307bba265e887857110ca3b4bd949211a0a298805f\\\",\\\"7e75948ef1513564fdcecfcbd389deac67c730f7004f8631ba90c0844d3e8c0cf470b656306877df5141f65fd3b7e85445a8452c3323ab273e6d0d44843817ed\\\"]}\"\n }\n ],\n \"outputs\": [\n {\n \"amount\": 2,\n \"id\": \"00bfa73302d12ffd\",\n \"B_\": \"038ec853d65ae1b79b5cdbc2774150b2cb288d6d26e12958a16fb33c32d9a86c39\"\n }\n ]\n}"; var validSwapRequestMultisigRefundLocktimeParsed = @@ -1160,8 +1138,6 @@ public void NullExpiryTests_PostMeltQuoteBolt11Response() Assert.Null(response3.PaymentPreimage); } - private static readonly byte[] P2BK_PREFIX = "Cashu_P2BK_v1"u8.ToArray(); - [Fact] public void Nut28_P2BK_Tests() { @@ -1179,8 +1155,6 @@ public void Nut28_P2BK_Tests() Assert.Equal(P.Key.ToString()?.ToLowerInvariant(), p.Key.CreatePubKey().ToString()?.ToLowerInvariant()); - // var kid = new KeysetId("009a1f293253e41e"); - var zx = "40d6ba4430a6dfa915bb441579b0f4dee032307434e9957a092bbca73151df8b"; Assert.Equal(zx, Convert.ToHexString(Cashu.ComputeZx(e, P)).ToLowerInvariant()); Assert.Equal(zx, Convert.ToHexString(Cashu.ComputeZx(p, E)).ToLowerInvariant()); @@ -1305,7 +1279,7 @@ public void Nut28_P2BK_Flow() var keysetId = new KeysetId("009a1f293253e41e"); - var conditions = new P2PKBuilder() + var conditions = new P2PkBuilder() { Lock = DateTimeOffset.FromUnixTimeSeconds(21000000000), Pubkeys = new[] { signing_key.CreatePubKey(), signing_key_two.CreatePubKey() }, @@ -1358,7 +1332,7 @@ public void Nut28_Flow_WithRandomE() var keysetId = new KeysetId("009a1f293253e41e"); - var conditions = new P2PKBuilder() + var conditions = new P2PkBuilder() { Lock = DateTimeOffset.FromUnixTimeSeconds(21000000000), Pubkeys = new[] { signing_key.CreatePubKey(), signing_key_two.CreatePubKey() }, diff --git a/DotNut.Tests/UnitTests2.cs b/DotNut.Tests/UnitTests2.cs index e0c9956..e374aa8 100644 --- a/DotNut.Tests/UnitTests2.cs +++ b/DotNut.Tests/UnitTests2.cs @@ -588,7 +588,7 @@ public void P2PkBuilder_Build_CreatesValidSecret() var privKey = new PrivKey( "0000000000000000000000000000000000000000000000000000000000000001" ); - var builder = new P2PKBuilder + var builder = new P2PkBuilder { Pubkeys = [privKey.Key.CreatePubKey()], SignatureThreshold = 1, @@ -613,7 +613,7 @@ public void P2PkBuilder_WithMultisig_Build() "0000000000000000000000000000000000000000000000000000000000000002" ); - var builder = new P2PKBuilder + var builder = new P2PkBuilder { Pubkeys = [privKey1.Key.CreatePubKey(), privKey2.Key.CreatePubKey()], SignatureThreshold = 2, diff --git a/DotNut.sln.DotSettings.user b/DotNut.sln.DotSettings.user index a3dd44a..e4ea523 100644 --- a/DotNut.sln.DotSettings.user +++ b/DotNut.sln.DotSettings.user @@ -15,6 +15,11 @@ ForceIncluded ForceIncluded ForceIncluded + <SessionState ContinuousTestingMode="0" IsActive="True" Name="UnitTest1" xmlns="urn:schemas-jetbrains-com:jetbrains-ut-session"> + <TestAncestor> + <TestId>xUnit::0AAAA85C-9FDC-4FD5-9CC2-ED8385B40106::net8.0::DotNut.Tests.UnitTest1</TestId> + </TestAncestor> +</SessionState> <SessionState ContinuousTestingMode="0" Name="Test1" xmlns="urn:schemas-jetbrains-com:jetbrains-ut-session"> @@ -29,11 +34,18 @@ <TestId>xUnit::0AAAA85C-9FDC-4FD5-9CC2-ED8385B40106::net8.0::DotNut.Tests.UnitTests2.BuilderChainingPreservesAllSettings</TestId> <TestId>xUnit::0AAAA85C-9FDC-4FD5-9CC2-ED8385B40106::net8.0::DotNut.Tests.UnitTests2.WithMintStringVariantCreatesHttpClient</TestId> <TestId>xUnit::0AAAA85C-9FDC-4FD5-9CC2-ED8385B40106::net8.0::DotNut.Tests.Integration</TestId> + <TestId>xUnit::0AAAA85C-9FDC-4FD5-9CC2-ED8385B40106::net8.0::DotNut.Tests.UnitTests2</TestId> </TestAncestor> </SessionState> - <SessionState ContinuousTestingMode="0" IsActive="True" Name="Integration" xmlns="urn:schemas-jetbrains-com:jetbrains-ut-session"> + <SessionState ContinuousTestingMode="0" Name="Integration" xmlns="urn:schemas-jetbrains-com:jetbrains-ut-session"> <TestAncestor> <TestId>xUnit::0AAAA85C-9FDC-4FD5-9CC2-ED8385B40106::net8.0::DotNut.Tests.Integration</TestId> <TestId>xUnit::0AAAA85C-9FDC-4FD5-9CC2-ED8385B40106::net8.0::DotNut.Tests.UnitTests2</TestId> + <TestId>xUnit::0AAAA85C-9FDC-4FD5-9CC2-ED8385B40106::net8.0::DotNut.Tests.UnitTest1</TestId> + </TestAncestor> +</SessionState> + <SessionState ContinuousTestingMode="0" Name="Nut11_SIG_ALL" xmlns="urn:schemas-jetbrains-com:jetbrains-ut-session"> + <TestAncestor> + <TestId>xUnit::0AAAA85C-9FDC-4FD5-9CC2-ED8385B40106::net8.0::DotNut.Tests.UnitTest1.Nut11_SIG_ALL</TestId> </TestAncestor> </SessionState> \ No newline at end of file diff --git a/DotNut/Abstractions/Handlers/MintHandlerBolt11.cs b/DotNut/Abstractions/Handlers/MintHandlerBolt11.cs index 8b879f0..b8db212 100644 --- a/DotNut/Abstractions/Handlers/MintHandlerBolt11.cs +++ b/DotNut/Abstractions/Handlers/MintHandlerBolt11.cs @@ -32,6 +32,7 @@ public IMintHandler> SignWithPrivkey(Pr } public PostMintQuoteBolt11Response GetQuote() => postMintQuoteBolt11Response; + public List GetOutputs() => outputs; public async Task> Mint(CancellationToken ct = default) diff --git a/DotNut/Abstractions/Handlers/MintHandlerBolt12.cs b/DotNut/Abstractions/Handlers/MintHandlerBolt12.cs index 1c5b3ce..1b66790 100644 --- a/DotNut/Abstractions/Handlers/MintHandlerBolt12.cs +++ b/DotNut/Abstractions/Handlers/MintHandlerBolt12.cs @@ -33,8 +33,8 @@ public IMintHandler> SignWithPrivkey(Pr } public PostMintQuoteBolt12Response GetQuote() => quote; + public List GetOutputs() => outputs; - public async Task> Mint(CancellationToken ct = default) { diff --git a/DotNut/Abstractions/InMemoryCounter.cs b/DotNut/Abstractions/InMemoryCounter.cs index 30d1feb..ed2a3c9 100644 --- a/DotNut/Abstractions/InMemoryCounter.cs +++ b/DotNut/Abstractions/InMemoryCounter.cs @@ -31,7 +31,11 @@ public Task IncrementCounter( return Task.FromResult(next); } - public Task<(uint oldValue, uint newValue)> FetchAndIncrement(KeysetId keysetId, uint bumpBy = 1, CancellationToken ct = default) + public Task<(uint oldValue, uint newValue)> FetchAndIncrement( + KeysetId keysetId, + uint bumpBy = 1, + CancellationToken ct = default + ) { uint oldValue = 0; uint newValue = _counter.AddOrUpdate( @@ -41,12 +45,12 @@ public Task IncrementCounter( { oldValue = current; return current + bumpBy; - }); + } + ); return Task.FromResult((oldValue, newValue)); } - public Task SetCounter(KeysetId keysetId, uint counter, CancellationToken ct = default) { _counter[keysetId] = counter; diff --git a/DotNut/Abstractions/Interfaces/ICounter.cs b/DotNut/Abstractions/Interfaces/ICounter.cs index 27fb30f..fdf362e 100644 --- a/DotNut/Abstractions/Interfaces/ICounter.cs +++ b/DotNut/Abstractions/Interfaces/ICounter.cs @@ -17,9 +17,9 @@ public Task IncrementCounter( ); public Task<(uint oldValue, uint newValue)> FetchAndIncrement( KeysetId keysetId, - uint bumpBy = 1, + uint bumpBy = 1, CancellationToken ct = default - ); + ); public Task SetCounter(KeysetId keysetId, uint counter, CancellationToken ct = default); public Task> Export(); } diff --git a/DotNut/Abstractions/Interfaces/IMintHandler.cs b/DotNut/Abstractions/Interfaces/IMintHandler.cs index c4f7b42..243ebaf 100644 --- a/DotNut/Abstractions/Interfaces/IMintHandler.cs +++ b/DotNut/Abstractions/Interfaces/IMintHandler.cs @@ -10,6 +10,6 @@ public interface IMintHandler : IMintHandler TQuote GetQuote(); List GetOutputs(); - + Task Mint(CancellationToken ct = default); } diff --git a/DotNut/Abstractions/Interfaces/IMintQuoteBuilder.cs b/DotNut/Abstractions/Interfaces/IMintQuoteBuilder.cs index 2d66cdb..81a5341 100644 --- a/DotNut/Abstractions/Interfaces/IMintQuoteBuilder.cs +++ b/DotNut/Abstractions/Interfaces/IMintQuoteBuilder.cs @@ -43,7 +43,7 @@ public interface IMintQuoteBuilder /// /// Optional. Allows providing a P2PK builder when a signature is required for minting. /// - IMintQuoteBuilder WithP2PkLock(P2PKBuilder p2pkBuilder); + IMintQuoteBuilder WithP2PkLock(P2PkBuilder p2pkBuilder); /// /// Optional. When minting P2Pk / HTLC Proofs allows to blind the pubkeys. diff --git a/DotNut/Abstractions/Interfaces/ISwapBuilder.cs b/DotNut/Abstractions/Interfaces/ISwapBuilder.cs index de22d4b..4cfa4a7 100644 --- a/DotNut/Abstractions/Interfaces/ISwapBuilder.cs +++ b/DotNut/Abstractions/Interfaces/ISwapBuilder.cs @@ -48,7 +48,7 @@ public interface ISwapBuilder /// /// Optional. Generate outputs guarded by P2PK locking. /// - ISwapBuilder ToP2PK(P2PKBuilder p2pkBuilder); + ISwapBuilder ToP2PK(P2PkBuilder p2pkBuilder); /// /// Optional. Blind P2Pk / HTLC proofs. diff --git a/DotNut/Abstractions/MintQuoteBuilder.cs b/DotNut/Abstractions/MintQuoteBuilder.cs index de1c6f9..0c4257a 100644 --- a/DotNut/Abstractions/MintQuoteBuilder.cs +++ b/DotNut/Abstractions/MintQuoteBuilder.cs @@ -22,7 +22,7 @@ class MintQuoteBuilder : IMintQuoteBuilder private GetKeysResponse.KeysetItemResponse? _keyset; //for p2pk - private P2PKBuilder? _builder; + private P2PkBuilder? _builder; private bool _shouldBlind = false; public MintQuoteBuilder(Wallet wallet) @@ -76,7 +76,7 @@ public IMintQuoteBuilder WithOutputs(IEnumerable outputs) return this; } - public IMintQuoteBuilder WithP2PkLock(P2PKBuilder p2pkBuilder) + public IMintQuoteBuilder WithP2PkLock(P2PkBuilder p2pkBuilder) { this._builder = p2pkBuilder; return this; diff --git a/DotNut/Abstractions/ProofSelector.cs b/DotNut/Abstractions/ProofSelector.cs index c5d7c6d..bbd95ba 100644 --- a/DotNut/Abstractions/ProofSelector.cs +++ b/DotNut/Abstractions/ProofSelector.cs @@ -62,7 +62,7 @@ public async Task SelectProofsToSend( ) { var proofsToSelectFrom = proofs as List ?? proofs.ToList(); - + // Init vars const int MAX_TRIALS = 60; // 40-80 is optimal (per RGLI paper) const double MAX_OVRPCT = 0; // Acceptable close match overage (percent) diff --git a/DotNut/Abstractions/SwapBuilder.cs b/DotNut/Abstractions/SwapBuilder.cs index d62f5b3..d202cfc 100644 --- a/DotNut/Abstractions/SwapBuilder.cs +++ b/DotNut/Abstractions/SwapBuilder.cs @@ -26,7 +26,7 @@ class SwapBuilder : ISwapBuilder //nut10 stuff private List? _privKeys; - private P2PKBuilder? _builder; + private P2PkBuilder? _builder; private string? _htlcPreimage; private bool _shouldBlind = false; @@ -96,7 +96,7 @@ public ISwapBuilder WithPrivkeys(IEnumerable privKeys) return this; } - public ISwapBuilder ToP2PK(P2PKBuilder p2pkBuilder) + public ISwapBuilder ToP2PK(P2PkBuilder p2pkBuilder) { this._builder = p2pkBuilder; return this; @@ -144,14 +144,17 @@ await _wallet.GetActiveKeysetId(this._unit, ct) { throw new InvalidOperationException($"Can't find keys for keyset {_targetKeysetId}"); } - + if (_verifyDleq) { foreach (var proof in swapInputs) { if (proof.DLEQ == null) { - throw new ArgumentNullException(nameof(proof.DLEQ), "Can't verify non-existent DLEQ proof!"); + throw new ArgumentNullException( + nameof(proof.DLEQ), + "Can't verify non-existent DLEQ proof!" + ); } // proof may be already inactive - make sure to fetch var keyset = await _wallet.GetKeys(proof.Id, true, false, ct); @@ -161,7 +164,7 @@ await _wallet.GetActiveKeysetId(this._unit, ct) $"Can't find keys for keyset id {proof.Id}" ); } - + if (!keyset.Keys.TryGetValue(proof.Amount, out var key)) { throw new InvalidOperationException( diff --git a/DotNut/Abstractions/Utils.cs b/DotNut/Abstractions/Utils.cs index cc6e542..4eb7424 100644 --- a/DotNut/Abstractions/Utils.cs +++ b/DotNut/Abstractions/Utils.cs @@ -93,9 +93,8 @@ public static List CreateOutputs( uint? counter = null ) { - var amountsList = amounts as IReadOnlyList - ?? amounts.ToList(); - + var amountsList = amounts as IReadOnlyList ?? amounts.ToList(); + if (amountsList.Any(a => !keys.Keys.Contains(a))) throw new ArgumentException("Invalid amounts"); @@ -152,7 +151,7 @@ public static List CreateOutputs( /// /// /// - public static OutputData CreateNut10Output(ulong amount, KeysetId keysetId, P2PKBuilder builder) + public static OutputData CreateNut10Output(ulong amount, KeysetId keysetId, P2PkBuilder builder) { // ugliest hack ever Nut10Secret secret; @@ -190,7 +189,7 @@ public static OutputData CreateNut10Output(ulong amount, KeysetId keysetId, P2PK public static OutputData CreateNut10BlindedOutput( ulong amount, KeysetId keysetId, - P2PKBuilder builder + P2PkBuilder builder ) { // ugliest hack ever @@ -198,12 +197,12 @@ P2PKBuilder builder PubKey E; if (builder is HTLCBuilder htlc) { - secret = new Nut10Secret("HTLC", htlc.BuildBlinded(keysetId, out var e)); + secret = new Nut10Secret("HTLC", htlc.BuildBlinded(out var e)); E = e; } else { - secret = new Nut10Secret("P2PK", builder.BuildBlinded(keysetId, out var e)); + secret = new Nut10Secret("P2PK", builder.BuildBlinded(out var e)); E = e; } @@ -233,7 +232,7 @@ P2PKBuilder builder public static OutputData CreateNut10BlindedOutput( ulong amount, KeysetId keysetId, - P2PKBuilder builder, + P2PkBuilder builder, PrivKey e ) { @@ -241,11 +240,11 @@ PrivKey e Nut10Secret secret; if (builder is HTLCBuilder htlc) { - secret = new Nut10Secret("HTLC", htlc.BuildBlinded(keysetId, e)); + secret = new Nut10Secret("HTLC", htlc.BuildBlinded(e)); } else { - secret = new Nut10Secret("P2PK", builder.BuildBlinded(keysetId, e)); + secret = new Nut10Secret("P2PK", builder.BuildBlinded(e)); } var r = RandomPrivkey(); @@ -324,7 +323,7 @@ Keyset keys { throw new ArgumentException("Outputs must as least equal amount of elements!"); } - + List proofs = new List(bs.Count); for (int i = 0; i < bs.Count; i++) { diff --git a/DotNut/Abstractions/Wallet.cs b/DotNut/Abstractions/Wallet.cs index e84b0b6..64da6ab 100644 --- a/DotNut/Abstractions/Wallet.cs +++ b/DotNut/Abstractions/Wallet.cs @@ -326,7 +326,6 @@ public async Task> CreateOutputs( ); } - if (!_shouldBumpCounter) { var counterValue = await this._counter.GetCounterForId(id, ct); diff --git a/DotNut/Abstractions/Websockets/WebsocketEnums.cs b/DotNut/Abstractions/Websockets/WebsocketEnums.cs index 7f178b1..bc88448 100644 --- a/DotNut/Abstractions/Websockets/WebsocketEnums.cs +++ b/DotNut/Abstractions/Websockets/WebsocketEnums.cs @@ -6,16 +6,20 @@ namespace DotNut.Abstractions.Websockets; [JsonConverter(typeof(JsonStringEnumConverter))] public enum SubscriptionKind { - - [EnumMember(Value = "bolt11_melt_quote")] Bolt11MeltQuote, - - [EnumMember(Value = "bolt11_mint_quote")] Bolt11MintQuote, - - [EnumMember(Value = "bolt12_melt_quote")] Bolt12MeltQuote, - - [EnumMember(Value = "bolt12_mint_quote")] Bolt12MintQuote, - - [EnumMember(Value = "proof_state")] ProofState, + [EnumMember(Value = "bolt11_melt_quote")] + Bolt11MeltQuote, + + [EnumMember(Value = "bolt11_mint_quote")] + Bolt11MintQuote, + + [EnumMember(Value = "bolt12_melt_quote")] + Bolt12MeltQuote, + + [EnumMember(Value = "bolt12_mint_quote")] + Bolt12MintQuote, + + [EnumMember(Value = "proof_state")] + ProofState, } [JsonConverter(typeof(JsonStringEnumConverter))] @@ -23,6 +27,7 @@ public enum WsRequestMethod { [EnumMember(Value = "subscribe")] Subscribe, + [EnumMember(Value = "unsubscribe")] Unsubscribe, } diff --git a/DotNut/Abstractions/Websockets/WebsocketService.cs b/DotNut/Abstractions/Websockets/WebsocketService.cs index 40b45bd..2d41df7 100644 --- a/DotNut/Abstractions/Websockets/WebsocketService.cs +++ b/DotNut/Abstractions/Websockets/WebsocketService.cs @@ -30,7 +30,8 @@ public class WebsocketService : IWebsocketService public event EventHandler? ConnectionStateChanged; - public WebsocketService() : this(new WebsocketServiceOptions()) { } + public WebsocketService() + : this(new WebsocketServiceOptions()) { } public WebsocketService(WebsocketServiceOptions options) { @@ -100,7 +101,9 @@ public async Task LazyConnectAsync( { if (_connections.TryGetValue(normalized, out var existing)) { - if (existing is { State: WebSocketState.Open, WebSocket.State: WebSocketState.Open }) + if ( + existing is { State: WebSocketState.Open, WebSocket.State: WebSocketState.Open } + ) { return existing; } @@ -177,11 +180,13 @@ public async Task SubscribeAsync( var subId = Guid.NewGuid().ToString(); var requestId = GetNextRequestId(); - var channel = Channel.CreateBounded(new BoundedChannelOptions(_options.MaxChannelCapacity) - { - FullMode = BoundedChannelFullMode.DropOldest, - SingleReader = false, - }); + var channel = Channel.CreateBounded( + new BoundedChannelOptions(_options.MaxChannelCapacity) + { + FullMode = BoundedChannelFullMode.DropOldest, + SingleReader = false, + } + ); var request = new WsRequest { @@ -415,7 +420,11 @@ private async Task ListenForMessagesAsync(WebsocketConnection connection, Cancel messageBuffer.Write(buffer, 0, result.Count); if (result.EndOfMessage) { - var message = Encoding.UTF8.GetString(messageBuffer.GetBuffer(), 0, (int)messageBuffer.Length); + var message = Encoding.UTF8.GetString( + messageBuffer.GetBuffer(), + 0, + (int)messageBuffer.Length + ); messageBuffer.SetLength(0); ProcessMessage(message); } @@ -456,8 +465,10 @@ private void ProcessMessage(string message) using var doc = JsonDocument.Parse(message); var root = doc.RootElement; - if (root.TryGetProperty("method", out var methodProp) - && methodProp.GetString() == "subscribe") + if ( + root.TryGetProperty("method", out var methodProp) + && methodProp.GetString() == "subscribe" + ) { var notification = JsonSerializer.Deserialize(message, JsonOptions); if (notification != null) @@ -488,7 +499,11 @@ private void ProcessMessage(string message) } } - private async Task SendMessageAsync(WebsocketConnection connection, T message, CancellationToken ct) + private async Task SendMessageAsync( + WebsocketConnection connection, + T message, + CancellationToken ct + ) { var json = JsonSerializer.Serialize(message, JsonOptions); var bytes = Encoding.UTF8.GetBytes(json); @@ -576,19 +591,28 @@ private async Task ReconnectAsync(string mintUrl, CancellationToken ct) var normalized = NormalizeMintUrl(mintUrl); var delay = _options.InitialReconnectDelay; - for (int attempt = 1; attempt <= _options.MaxReconnectAttempts && !ct.IsCancellationRequested; attempt++) + for ( + int attempt = 1; + attempt <= _options.MaxReconnectAttempts && !ct.IsCancellationRequested; + attempt++ + ) { try { await Task.Delay(delay, ct); - var connectionLock = _connectionLocks.GetOrAdd(normalized, _ => new SemaphoreSlim(1, 1)); + var connectionLock = _connectionLocks.GetOrAdd( + normalized, + _ => new SemaphoreSlim(1, 1) + ); await connectionLock.WaitAsync(ct); try { - if (_connections.TryGetValue(normalized, out var existing) - && existing.State == WebSocketState.Open) + if ( + _connections.TryGetValue(normalized, out var existing) + && existing.State == WebSocketState.Open + ) { return; // already reconnected } @@ -608,7 +632,9 @@ private async Task ReconnectAsync(string mintUrl, CancellationToken ct) } catch { - delay = TimeSpan.FromTicks(Math.Min(delay.Ticks * 2, _options.MaxReconnectDelay.Ticks)); + delay = TimeSpan.FromTicks( + Math.Min(delay.Ticks * 2, _options.MaxReconnectDelay.Ticks) + ); } } @@ -617,9 +643,7 @@ private async Task ReconnectAsync(string mintUrl, CancellationToken ct) private async Task ResubscribeAllAsync(string mintUrl, CancellationToken ct) { - var subsToRestore = _subscriptionInfos - .Where(kvp => kvp.Value.MintUrl == mintUrl) - .ToList(); + var subsToRestore = _subscriptionInfos.Where(kvp => kvp.Value.MintUrl == mintUrl).ToList(); foreach (var (subId, info) in subsToRestore) { @@ -719,11 +743,10 @@ private async Task RunWithErrorHandlingAsync(Func action, WebsocketConnect private void OnConnectionStateChanged(string connectionId, WebSocketState state) { - ConnectionStateChanged?.Invoke(this, new ConnectionStateChangedEventArgs - { - ConnectionId = connectionId, - State = state - }); + ConnectionStateChanged?.Invoke( + this, + new ConnectionStateChangedEventArgs { ConnectionId = connectionId, State = state } + ); } private static string NormalizeMintUrl(string mintUrl) diff --git a/DotNut/PaymentRequestBech32Encoder.cs b/DotNut/Encoding/PaymentRequestBech32Encoder.cs similarity index 92% rename from DotNut/PaymentRequestBech32Encoder.cs rename to DotNut/Encoding/PaymentRequestBech32Encoder.cs index af50844..0aeacea 100644 --- a/DotNut/PaymentRequestBech32Encoder.cs +++ b/DotNut/Encoding/PaymentRequestBech32Encoder.cs @@ -20,7 +20,7 @@ private enum TlvTag : byte Mint = 0x05, Description = 0x06, Transport = 0x07, - Nut10 = 0x08 + Nut10 = 0x08, } public static string Encode(PaymentRequest paymentRequest) @@ -29,12 +29,15 @@ public static string Encode(PaymentRequest paymentRequest) EncodeTLV(writer, paymentRequest); var tlvBytes = writer.WrittenSpan; - Span words = tlvBytes.Length * 2 > 1024 - ? new byte[tlvBytes.Length * 2] - : stackalloc byte[tlvBytes.Length * 2]; + Span words = + tlvBytes.Length * 2 > 1024 + ? new byte[tlvBytes.Length * 2] + : stackalloc byte[tlvBytes.Length * 2]; var wordsLen = ConvertBits(tlvBytes, words, 8, 5, true); - return Encoder.EncodeRaw(words[..wordsLen].ToArray(), Bech32EncodingType.BECH32M).ToUpperInvariant(); + return Encoder + .EncodeRaw(words[..wordsLen].ToArray(), Bech32EncodingType.BECH32M) + .ToUpperInvariant(); } public static PaymentRequest Decode(string creqb) @@ -117,7 +120,7 @@ private static void EncodeNut10(IBufferWriter writer, Nut10LockingConditio { "P2PK" => (byte)0x00, "HTLC" => (byte)0x01, - _ => throw new ArgumentException("Unknown nut10 kind!") + _ => throw new ArgumentException("Unknown nut10 kind!"), }; WriteTlv(writer, 0x01, [kindByte]); WriteTlvUtf8(writer, 0x02, nut10.Data); @@ -128,7 +131,10 @@ private static void EncodeNut10(IBufferWriter writer, Nut10LockingConditio } } - private static void EncodeTransport(IBufferWriter writer, PaymentRequestTransport transport) + private static void EncodeTransport( + IBufferWriter writer, + PaymentRequestTransport transport + ) { switch (transport.Type.ToLowerInvariant()) { @@ -152,7 +158,6 @@ private static void EncodeTransport(IBufferWriter writer, PaymentRequestTr WriteTagTuple(writer, 0x03, ["r", relay]); } - foreach (var tag in transport.Tags ?? []) { WriteTagTuple(writer, 0x03, tag.ToArray()); @@ -164,7 +169,11 @@ private static void EncodeTransport(IBufferWriter writer, PaymentRequestTr } } - private static void WriteTagTuple(IBufferWriter writer, byte tag, ReadOnlySpan tuple) + private static void WriteTagTuple( + IBufferWriter writer, + byte tag, + ReadOnlySpan tuple + ) { // Calculate total size for the tuple data var totalLen = 0; @@ -196,8 +205,8 @@ private static void WriteTagTuple(IBufferWriter writer, byte tag, ReadOnly writer.Advance(3 + totalLen); } - private static void WriteTlv(IBufferWriter writer, TlvTag tag, ReadOnlySpan data) - => WriteTlv(writer, (byte)tag, data); + private static void WriteTlv(IBufferWriter writer, TlvTag tag, ReadOnlySpan data) => + WriteTlv(writer, (byte)tag, data); private static void WriteTlv(IBufferWriter writer, byte tag, ReadOnlySpan data) { @@ -211,8 +220,8 @@ private static void WriteTlv(IBufferWriter writer, byte tag, ReadOnlySpan< writer.Advance(3 + data.Length); } - private static void WriteTlvUtf8(IBufferWriter writer, TlvTag tag, string value) - => WriteTlvUtf8(writer, (byte)tag, value); + private static void WriteTlvUtf8(IBufferWriter writer, TlvTag tag, string value) => + WriteTlvUtf8(writer, (byte)tag, value); private static void WriteTlvUtf8(IBufferWriter writer, byte tag, string value) { @@ -251,7 +260,10 @@ private static PaymentRequest DecodeTLV(ReadOnlySpan data) pr.Amount = BinaryPrimitives.ReadUInt64BigEndian(value); break; case 0x03: - pr.Unit = value.Length == 1 && value[0] == 0x00 ? "sat" : Encoding.UTF8.GetString(value); + pr.Unit = + value.Length == 1 && value[0] == 0x00 + ? "sat" + : Encoding.UTF8.GetString(value); break; case 0x04: pr.OneTimeUse = value.Length == 1 && value[0] == 0x01; @@ -301,7 +313,7 @@ private static PaymentRequestTransport DecodeTransport(ReadOnlySpan data) { 0x00 => "nostr", 0x01 => "post", - _ => throw new FormatException("Unknown transport kind") + _ => throw new FormatException("Unknown transport kind"), }; break; case 0x02: @@ -363,7 +375,7 @@ private static Nut10LockingCondition DecodeNut10(ReadOnlySpan data) { 0x00 => "P2PK", 0x01 => "HTLC", - _ => throw new FormatException("Unknown nut10 kind") + _ => throw new FormatException("Unknown nut10 kind"), }; break; case 0x02: @@ -453,7 +465,9 @@ private static (byte[] Pubkey, string[] Relays) DecodeNprofile(string nprofile) { case 0x00: if (length != 32) - throw new FormatException($"Invalid pubkey length: expected 32 bytes, got {length}"); + throw new FormatException( + $"Invalid pubkey length: expected 32 bytes, got {length}" + ); pubkey = tlvData.AsSpan(offset, 32).ToArray(); break; case 0x01: @@ -509,8 +523,8 @@ private static string EncodeNprofile(byte[] pubkey, string[] relays) return encoder.EncodeRaw(words, Bech32EncodingType.BECH32); } - private static byte[] ConvertBits(byte[] data, int fromBits, int toBits, bool pad) - => ConvertBits(data.AsSpan(), fromBits, toBits, pad); + private static byte[] ConvertBits(byte[] data, int fromBits, int toBits, bool pad) => + ConvertBits(data.AsSpan(), fromBits, toBits, pad); private static byte[] ConvertBits(ReadOnlySpan data, int fromBits, int toBits, bool pad) { @@ -521,7 +535,13 @@ private static byte[] ConvertBits(ReadOnlySpan data, int fromBits, int toB return output[..written].ToArray(); } - private static int ConvertBits(ReadOnlySpan data, Span output, int fromBits, int toBits, bool pad) + private static int ConvertBits( + ReadOnlySpan data, + Span output, + int fromBits, + int toBits, + bool pad + ) { var acc = 0; var bits = 0; diff --git a/DotNut/Encoding/PaymentRequestEncoder.cs b/DotNut/Encoding/PaymentRequestEncoder.cs index 06aeb8e..0eed2eb 100644 --- a/DotNut/Encoding/PaymentRequestEncoder.cs +++ b/DotNut/Encoding/PaymentRequestEncoder.cs @@ -72,10 +72,6 @@ public CBORObject ToCBORObject(PaymentRequest paymentRequest) cbor.Add("nut10", nut10Obj); } - if (paymentRequest.Nut26 is { } nut26) - { - cbor.Add("nut26", nut26); - } return cbor; } @@ -122,13 +118,14 @@ public PaymentRequest FromCBORObject(CBORObject obj) transport.Target = transportValue.AsString(); break; case "g": - transport.Tags = transportValue.Values - .Where(tag => tag.Type == CBORType.Array) - .Select(tag => - new Tag( - tag.Values.Select(cborObject => cborObject.AsString()).ToArray() - ) - ) + transport.Tags = transportValue + .Values.Where(tag => tag.Type == CBORType.Array) + .Select(tag => new Tag( + tag.Values.Select(cborObject => + cborObject.AsString() + ) + .ToArray() + )) .ToArray(); break; } @@ -152,20 +149,18 @@ public PaymentRequest FromCBORObject(CBORObject obj) lockingCondition.Data = nut10Value.AsString(); break; case "t": - lockingCondition.Tags = nut10Value.Values - .Where(tag => tag.Type == CBORType.Array) - .Select(tag => - new Tag(tag.Values.Select(cborObject => cborObject.AsString()).ToArray()) - ) + lockingCondition.Tags = nut10Value + .Values.Where(tag => tag.Type == CBORType.Array) + .Select(tag => new Tag( + tag.Values.Select(cborObject => cborObject.AsString()) + .ToArray() + )) .ToArray(); break; } } paymentRequest.Nut10 = lockingCondition; break; - case "nut26": - paymentRequest.Nut26 = value.AsBoolean(); - break; } } return paymentRequest; diff --git a/DotNut/NBitcoin/Bech32/Bech32Encoder.cs b/DotNut/NBitcoin/Bech32/Bech32Encoder.cs index 0e6b1c1..1f6f5d9 100644 --- a/DotNut/NBitcoin/Bech32/Bech32Encoder.cs +++ b/DotNut/NBitcoin/Bech32/Bech32Encoder.cs @@ -4,709 +4,2691 @@ namespace DotNut.NBitcoin.Bech32 { - public abstract class DataEncoder - { - // char.IsWhiteSpace fits well but it match other whitespaces - // characters too and also works for unicode characters. - public static bool IsSpace(char c) - { - switch (c) - { - case ' ': - case '\t': - case '\n': - case '\v': - case '\f': - case '\r': - return true; - } - return false; - } - - internal DataEncoder() - { - } - - public string EncodeData(byte[] data) - { - return EncodeData(data, 0, data.Length); - } - - public abstract string EncodeData(byte[] data, int offset, int count); - - public virtual string EncodeData(ReadOnlySpan data) - { - return this.EncodeData(data.ToArray()); - } - - public abstract byte[] DecodeData(string encoded); - } - - public class ASCIIEncoder : DataEncoder - { - //Do not using Encoding.ASCII (not portable) - public override byte[] DecodeData(string encoded) - { - if (String.IsNullOrEmpty(encoded)) - return new byte[0]; - Span r = encoded.Length is int v && v > 256 ? new byte[v] : stackalloc byte[v]; - for (int i = 0; i < r.Length; i++) - { - r[i] = (byte)encoded[i]; - } - return r.ToArray(); - } - - public void DecodeData(string encoded, Span output) - { - var l = encoded.Length; - for (int i = 0; i < l; i++) - { - output[i] = (byte)encoded[i]; - } - } - - public override string EncodeData(byte[] data, int offset, int count) - { - return new String(data.Skip(offset).Take(count).Select(o => (char)o).ToArray()).Replace("\0", ""); - } - } - - public static class Encoders - { - static readonly ASCIIEncoder _ASCII = new ASCIIEncoder(); - public static DataEncoder ASCII - { - get - { - return _ASCII; - } - } - - public static Bech32Encoder Bech32(string hrp) - { - return new Bech32Encoder(hrp); - } - public static Bech32Encoder Bech32(byte[] hrp) - { - return new Bech32Encoder(hrp); - } - } - - public class Bech32FormatException : FormatException - { - public Bech32FormatException(string message, int[] indexes) : base(message) - { - if (indexes == null) - throw new ArgumentNullException(nameof(indexes)); - ErrorIndexes = indexes; - Array.Sort(ErrorIndexes); - } - public int[] ErrorIndexes - { - get; internal set; - } - } - - public class Bech32EncodingType - { - static Bech32EncodingType() - { - BECH32 = new Bech32EncodingType(1); - BECH32M = new Bech32EncodingType(0x2bc830a3); - All = new Bech32EncodingType[] { BECH32, BECH32M }; - } - public readonly static Bech32EncodingType BECH32; - public readonly static Bech32EncodingType BECH32M; - public readonly static Bech32EncodingType[] All; - public Bech32EncodingType(int encodingConstant) - { - EncodingConstant = encodingConstant; - } - public int EncodingConstant { get; } - } - - public class Bech32Encoder - { - - readonly static int[] GF1024_EXP = new int[] { - 1, 303, 635, 446, 997, 640, 121, 142, 959, 420, 350, 438, 166, 39, 543, - 335, 831, 691, 117, 632, 719, 97, 107, 374, 558, 797, 54, 150, 858, 877, - 724, 1013, 294, 23, 354, 61, 164, 633, 992, 538, 469, 659, 174, 868, 184, - 809, 766, 563, 866, 851, 257, 520, 45, 770, 535, 524, 408, 213, 436, 760, - 472, 330, 933, 799, 616, 361, 15, 391, 756, 814, 58, 608, 554, 680, 993, - 821, 942, 813, 843, 484, 193, 935, 321, 919, 572, 741, 423, 559, 562, - 589, 296, 191, 493, 685, 891, 665, 435, 60, 395, 2, 606, 511, 853, 746, - 32, 219, 284, 631, 840, 661, 837, 332, 78, 311, 670, 887, 111, 195, 505, - 190, 194, 214, 709, 380, 819, 69, 261, 957, 1018, 161, 739, 588, 7, 708, - 83, 328, 507, 736, 317, 899, 47, 348, 1000, 345, 882, 245, 367, 996, 943, - 514, 304, 90, 804, 295, 312, 793, 387, 833, 249, 921, 660, 618, 823, 496, - 722, 30, 782, 225, 892, 93, 480, 372, 112, 738, 867, 636, 890, 950, 968, - 386, 622, 642, 551, 369, 234, 846, 382, 365, 442, 592, 343, 986, 122, - 1023, 59, 847, 81, 790, 4, 437, 983, 931, 244, 64, 415, 529, 487, 944, - 35, 938, 664, 156, 583, 53, 999, 222, 390, 987, 341, 388, 389, 170, 721, - 879, 138, 522, 627, 765, 322, 230, 440, 14, 168, 143, 656, 991, 224, 595, - 550, 94, 657, 752, 667, 1005, 451, 734, 744, 638, 292, 585, 157, 872, - 590, 601, 827, 774, 930, 475, 571, 33, 500, 871, 969, 173, 21, 828, 450, - 1009, 147, 960, 705, 201, 228, 998, 497, 1021, 613, 688, 772, 508, 36, - 366, 715, 468, 956, 725, 730, 861, 425, 647, 701, 221, 759, 95, 958, 139, - 805, 8, 835, 679, 614, 449, 128, 791, 299, 974, 617, 70, 628, 57, 273, - 430, 67, 750, 405, 780, 703, 643, 776, 778, 340, 171, 1022, 276, 308, - 495, 243, 644, 460, 857, 28, 336, 286, 41, 695, 448, 431, 364, 149, 43, - 233, 63, 762, 902, 181, 240, 501, 584, 434, 275, 1008, 444, 443, 895, - 812, 612, 927, 383, 66, 961, 1006, 690, 346, 3, 881, 900, 747, 271, 672, - 162, 402, 456, 748, 971, 755, 490, 105, 808, 977, 72, 732, 182, 897, 625, - 163, 189, 947, 850, 46, 115, 403, 231, 151, 629, 278, 874, 16, 934, 110, - 492, 898, 256, 807, 598, 700, 498, 140, 481, 91, 523, 860, 134, 252, 771, - 824, 119, 38, 816, 820, 641, 342, 757, 513, 577, 990, 463, 40, 920, 955, - 17, 649, 533, 82, 103, 896, 862, 728, 259, 86, 466, 87, 253, 556, 323, - 457, 963, 432, 845, 527, 745, 849, 863, 1015, 888, 488, 567, 727, 132, - 674, 764, 109, 669, 6, 1003, 552, 246, 542, 96, 324, 781, 912, 248, 694, - 239, 980, 210, 880, 683, 144, 177, 325, 546, 491, 326, 339, 623, 941, 92, - 207, 783, 462, 263, 483, 517, 1012, 9, 620, 220, 984, 548, 512, 878, 421, - 113, 973, 280, 962, 159, 310, 945, 268, 465, 806, 889, 199, 76, 873, 865, - 34, 645, 227, 290, 418, 693, 926, 80, 569, 639, 11, 50, 291, 141, 206, - 544, 949, 185, 518, 133, 909, 135, 467, 376, 646, 914, 678, 841, 954, - 318, 242, 939, 951, 743, 1017, 976, 359, 167, 264, 100, 241, 218, 51, 12, - 758, 368, 453, 309, 192, 648, 826, 553, 473, 101, 478, 673, 397, 1001, - 118, 265, 331, 650, 356, 982, 652, 655, 510, 634, 145, 414, 830, 924, - 526, 966, 298, 737, 18, 504, 401, 697, 360, 288, 1020, 842, 203, 698, - 537, 676, 279, 581, 619, 536, 907, 876, 1019, 398, 152, 1010, 994, 68, - 42, 454, 580, 836, 99, 565, 137, 379, 503, 22, 77, 582, 282, 412, 352, - 611, 347, 300, 266, 570, 270, 911, 729, 44, 557, 108, 946, 637, 597, 461, - 630, 615, 238, 763, 681, 718, 334, 528, 200, 459, 413, 79, 24, 229, 713, - 906, 579, 384, 48, 893, 370, 923, 202, 917, 98, 794, 754, 197, 530, 662, - 52, 712, 677, 56, 62, 981, 509, 267, 789, 885, 561, 316, 684, 596, 226, - 13, 985, 779, 123, 720, 576, 753, 948, 406, 125, 315, 104, 519, 426, 502, - 313, 566, 1016, 767, 796, 281, 749, 740, 136, 84, 908, 424, 936, 198, - 355, 274, 735, 967, 5, 154, 428, 541, 785, 704, 486, 671, 600, 532, 381, - 540, 574, 187, 88, 378, 216, 621, 499, 419, 922, 485, 494, 476, 255, 114, - 188, 668, 297, 400, 918, 787, 158, 25, 458, 178, 564, 422, 768, 73, 1011, - 717, 575, 404, 547, 196, 829, 237, 394, 301, 37, 65, 176, 106, 89, 85, - 675, 979, 534, 803, 995, 363, 593, 120, 417, 452, 26, 699, 822, 223, 169, - 416, 235, 609, 773, 211, 607, 208, 302, 852, 965, 603, 357, 761, 247, - 817, 539, 250, 232, 272, 129, 568, 848, 624, 396, 710, 525, 183, 686, 10, - 285, 856, 307, 811, 160, 972, 55, 441, 289, 723, 305, 373, 351, 153, 733, - 409, 506, 975, 838, 573, 970, 988, 913, 471, 205, 337, 49, 594, 777, 549, - 815, 277, 27, 916, 333, 353, 844, 800, 146, 751, 186, 375, 769, 358, 392, - 883, 474, 788, 602, 74, 130, 329, 212, 155, 131, 102, 687, 293, 870, 742, - 726, 427, 217, 834, 904, 29, 127, 869, 407, 338, 832, 470, 482, 810, 399, - 439, 393, 604, 929, 682, 447, 714, 251, 455, 875, 319, 477, 464, 521, - 258, 377, 937, 489, 792, 172, 314, 327, 124, 20, 531, 953, 591, 886, 320, - 696, 71, 859, 578, 175, 587, 707, 663, 283, 179, 795, 989, 702, 940, 371, - 692, 689, 555, 903, 410, 651, 75, 429, 818, 362, 894, 515, 31, 545, 666, - 706, 952, 864, 269, 254, 349, 711, 802, 716, 784, 1007, 925, 801, 445, - 148, 260, 658, 385, 287, 262, 204, 126, 586, 1004, 236, 165, 854, 411, - 932, 560, 19, 215, 1002, 775, 653, 928, 901, 964, 884, 798, 839, 786, - 433, 610, 116, 855, 180, 479, 910, 1014, 599, 915, 905, 306, 516, 731, - 626, 978, 825, 344, 605, 654, 209 - }; - - readonly static int[] GF1024_LOG = new[] { - -1, 0, 99, 363, 198, 726, 462, 132, 297, 495, 825, 528, 561, 693, 231, - 66, 396, 429, 594, 990, 924, 264, 627, 33, 660, 759, 792, 858, 330, 891, - 165, 957, 104, 259, 518, 208, 280, 776, 416, 13, 426, 333, 618, 339, 641, - 52, 388, 140, 666, 852, 529, 560, 678, 213, 26, 832, 681, 309, 70, 194, - 97, 35, 682, 341, 203, 777, 358, 312, 617, 125, 307, 931, 379, 765, 875, - 951, 515, 628, 112, 659, 525, 196, 432, 134, 717, 781, 438, 440, 740, - 780, 151, 408, 487, 169, 239, 293, 467, 21, 672, 622, 557, 571, 881, 433, - 704, 376, 779, 22, 643, 460, 398, 116, 172, 503, 751, 389, 1004, 18, 576, - 415, 789, 6, 192, 696, 923, 702, 981, 892, 302, 816, 876, 880, 457, 537, - 411, 539, 716, 624, 224, 295, 406, 531, 7, 233, 478, 586, 864, 268, 974, - 338, 27, 392, 614, 839, 727, 879, 211, 250, 758, 507, 830, 129, 369, 384, - 36, 985, 12, 555, 232, 796, 221, 321, 920, 263, 42, 934, 778, 479, 761, - 939, 1006, 344, 381, 823, 44, 535, 866, 739, 752, 385, 119, 91, 566, 80, - 120, 117, 771, 675, 721, 514, 656, 271, 670, 602, 980, 850, 532, 488, - 803, 1022, 475, 801, 878, 57, 121, 991, 742, 888, 559, 105, 497, 291, - 215, 795, 236, 167, 692, 520, 272, 661, 229, 391, 814, 340, 184, 798, - 984, 773, 650, 473, 345, 558, 548, 326, 202, 145, 465, 810, 471, 158, - 813, 908, 412, 441, 964, 750, 401, 50, 915, 437, 975, 126, 979, 491, 556, - 577, 636, 685, 510, 963, 638, 367, 815, 310, 723, 349, 323, 857, 394, - 606, 505, 713, 630, 938, 106, 826, 332, 978, 599, 834, 521, 530, 248, - 883, 32, 153, 90, 754, 592, 304, 635, 775, 804, 1, 150, 836, 1013, 828, - 324, 565, 508, 113, 154, 708, 921, 703, 689, 138, 547, 911, 929, 82, 228, - 443, 468, 480, 483, 922, 135, 877, 61, 578, 111, 860, 654, 15, 331, 851, - 895, 484, 320, 218, 420, 190, 1019, 143, 362, 634, 141, 965, 10, 838, - 632, 861, 34, 722, 580, 808, 869, 554, 598, 65, 954, 787, 337, 187, 281, - 146, 563, 183, 668, 944, 171, 837, 23, 867, 541, 916, 741, 625, 123, 736, - 186, 357, 665, 977, 179, 156, 219, 220, 216, 67, 870, 902, 774, 98, 820, - 574, 613, 900, 755, 596, 370, 390, 769, 314, 701, 894, 56, 841, 949, 987, - 631, 658, 587, 204, 797, 790, 522, 745, 9, 502, 763, 86, 719, 288, 706, - 887, 728, 952, 311, 336, 446, 1002, 348, 96, 58, 199, 11, 901, 230, 833, - 188, 352, 351, 973, 3, 906, 335, 301, 266, 244, 791, 564, 619, 909, 371, - 444, 760, 657, 328, 647, 490, 425, 913, 511, 439, 540, 283, 40, 897, 849, - 60, 570, 872, 257, 749, 912, 572, 1007, 170, 407, 898, 492, 79, 747, 732, - 206, 454, 918, 375, 482, 399, 92, 748, 325, 163, 274, 405, 744, 260, 346, - 707, 626, 595, 118, 842, 136, 279, 684, 584, 101, 500, 422, 149, 956, - 1014, 493, 536, 705, 51, 914, 225, 409, 55, 822, 590, 448, 655, 205, 676, - 925, 735, 431, 784, 54, 609, 604, 39, 812, 737, 729, 466, 14, 533, 958, - 481, 770, 499, 855, 238, 182, 464, 569, 72, 947, 442, 642, 24, 87, 989, - 688, 88, 47, 762, 623, 709, 455, 817, 526, 637, 258, 84, 845, 738, 768, - 698, 423, 933, 664, 620, 607, 629, 212, 347, 249, 982, 935, 131, 89, 252, - 927, 189, 788, 853, 237, 691, 646, 403, 1010, 734, 253, 874, 807, 903, - 1020, 100, 802, 71, 799, 1003, 633, 355, 276, 300, 649, 64, 306, 161, - 608, 496, 743, 180, 485, 819, 383, 1016, 226, 308, 393, 648, 107, 19, 37, - 585, 2, 175, 645, 247, 527, 5, 419, 181, 317, 327, 519, 542, 289, 567, - 430, 579, 950, 582, 994, 1021, 583, 234, 240, 976, 41, 160, 109, 677, - 937, 210, 95, 959, 242, 753, 461, 114, 733, 368, 573, 458, 782, 605, 680, - 544, 299, 73, 652, 905, 477, 690, 93, 824, 882, 277, 946, 361, 17, 945, - 523, 472, 334, 930, 597, 603, 793, 404, 290, 942, 316, 731, 270, 960, - 936, 133, 122, 821, 966, 679, 662, 907, 282, 968, 767, 653, 20, 697, 222, - 164, 835, 30, 285, 886, 456, 436, 640, 286, 1015, 380, 840, 245, 724, - 137, 593, 173, 130, 715, 85, 885, 551, 246, 449, 103, 366, 372, 714, 313, - 865, 241, 699, 674, 374, 68, 421, 562, 292, 59, 809, 342, 651, 459, 227, - 46, 711, 764, 868, 53, 413, 278, 800, 255, 993, 318, 854, 319, 695, 315, - 469, 166, 489, 969, 730, 1001, 757, 873, 686, 197, 303, 919, 155, 673, - 940, 712, 25, 999, 63, 863, 972, 967, 785, 152, 296, 512, 402, 377, 45, - 899, 829, 354, 77, 69, 856, 417, 811, 953, 124, 418, 75, 794, 162, 414, - 1018, 568, 254, 265, 772, 588, 16, 896, 157, 889, 298, 621, 110, 844, - 1000, 108, 545, 601, 78, 862, 447, 185, 195, 818, 450, 387, 49, 805, 102, - 986, 1005, 827, 329, 28, 932, 410, 287, 435, 451, 962, 517, 48, 174, 43, - 893, 884, 261, 251, 516, 395, 910, 611, 29, 501, 223, 476, 364, 144, 871, - 998, 687, 928, 115, 453, 513, 176, 94, 168, 667, 955, 353, 434, 382, 400, - 139, 365, 996, 343, 948, 890, 1012, 663, 610, 718, 538, 1008, 639, 470, - 848, 543, 1011, 859, 671, 756, 83, 427, 159, 746, 669, 589, 971, 524, - 356, 995, 904, 256, 201, 988, 62, 397, 81, 720, 917, 209, 549, 943, 486, - 76, 148, 207, 509, 644, 386, 700, 534, 177, 550, 961, 926, 546, 428, 284, - 127, 294, 8, 269, 359, 506, 445, 997, 806, 591, 725, 178, 262, 846, 373, - 831, 504, 305, 843, 553, 378, 1017, 783, 474, 683, 581, 200, 498, 694, - 191, 217, 847, 941, 424, 235, 38, 74, 616, 786, 147, 4, 273, 214, 142, - 575, 992, 463, 983, 243, 360, 970, 350, 267, 615, 766, 494, 31, 1009, - 452, 710, 552, 128, 612, 600, 275, 322, 193 - }; - - protected static readonly byte[] Byteset; - static Bech32Encoder() - { - Byteset = Encoders.ASCII.DecodeData("qpzry9x8gf2tvdw0s3jn54khce6mua7l"); - } - private static readonly uint[] Generator = { 0x3b6a57b2U, 0x26508e6dU, 0x1ea119faU, 0x3d4233ddU, 0x2a1462b3U }; - - - uint syndrome(uint residue) - { - var low = residue & 0x1f; - return (uint)(low ^ (low << 10) ^ (low << 20) ^ - (((residue >> 5) & 1) != 0 ? 0x31edd3c4 : 0) ^ - (((residue >> 6) & 1) != 0 ? 0x335f86a8 : 0) ^ - (((residue >> 7) & 1) != 0 ? 0x363b8870 : 0) ^ - (((residue >> 8) & 1) != 0 ? 0x3e6390c9 : 0) ^ - (((residue >> 9) & 1) != 0 ? 0x2ec72192 : 0) ^ - (((residue >> 10) & 1) != 0 ? 0x1046f79d : 0) ^ - (((residue >> 11) & 1) != 0 ? 0x208d4e33 : 0) ^ - (((residue >> 12) & 1) != 0 ? 0x130ebd6f : 0) ^ - (((residue >> 13) & 1) != 0 ? 0x2499fade : 0) ^ - (((residue >> 14) & 1) != 0 ? 0x1b27d4b5 : 0) ^ - (((residue >> 15) & 1) != 0 ? 0x04be1eb4 : 0) ^ - (((residue >> 16) & 1) != 0 ? 0x0968b861 : 0) ^ - (((residue >> 17) & 1) != 0 ? 0x1055f0c2 : 0) ^ - (((residue >> 18) & 1) != 0 ? 0x20ab4584 : 0) ^ - (((residue >> 19) & 1) != 0 ? 0x1342af08 : 0) ^ - (((residue >> 20) & 1) != 0 ? 0x24f1f318 : 0) ^ - (((residue >> 21) & 1) != 0 ? 0x1be34739 : 0) ^ - (((residue >> 22) & 1) != 0 ? 0x35562f7b : 0) ^ - (((residue >> 23) & 1) != 0 ? 0x3a3c5bff : 0) ^ - (((residue >> 24) & 1) != 0 ? 0x266c96f7 : 0) ^ - (((residue >> 25) & 1) != 0 ? 0x25c78b65 : 0) ^ - (((residue >> 26) & 1) != 0 ? 0x1b1f13ea : 0) ^ - (((residue >> 27) & 1) != 0 ? 0x34baa2f4 : 0) ^ - (((residue >> 28) & 1) != 0 ? 0x3b61c0e1 : 0) ^ - (((residue >> 29) & 1) != 0 ? 0x265325c2 : 0)); - } - - - int[] locate_errors(uint residue, int length) - { - if (residue == 0) - { - return new int[0]; - } - var syn = syndrome(residue); - var s0 = syn & 0x3FF; - var s1 = (syn >> 10) & 0x3FF; - var s2 = syn >> 20; - var l_s0 = GF1024_LOG[s0]; - var l_s1 = GF1024_LOG[s1]; - var l_s2 = GF1024_LOG[s2]; - if (l_s0 != -1 && l_s1 != -1 && l_s2 != -1 && (2 * l_s1 - l_s2 - l_s0 + 2046) % 1023 == 0) - { - var p1 = (l_s1 - l_s0 + 1023) % 1023; - if (p1 >= length) - return new int[0]; - var l_e1 = l_s0 + (1023 - 997) * p1; - if ((l_e1 % 33) != 0) - return new int[0]; - return new[] { p1 }; - } - for (var p1 = 0; p1 < length; p1++) - { - var s2_s1p1 = s2 ^ (s1 == 0 ? 0 : GF1024_EXP[(l_s1 + p1) % 1023]); - if (s2_s1p1 == 0) - continue; - var s1_s0p1 = s1 ^ (s0 == 0 ? 0 : GF1024_EXP[(l_s0 + p1) % 1023]); - if (s1_s0p1 == 0) - continue; - var l_s1_s0p1 = GF1024_LOG[s1_s0p1]; - var p2 = (GF1024_LOG[s2_s1p1] - l_s1_s0p1 + 1023) % 1023; - if (p2 >= length || p1 == p2) - continue; - var s1_s0p2 = s1 ^ (s0 == 0 ? 0 : GF1024_EXP[(l_s0 + p2) % 1023]); - if (s1_s0p2 == 0) - continue; - var inv_p1_p2 = 1023 - GF1024_LOG[GF1024_EXP[p1] ^ GF1024_EXP[p2]]; - var l_e2 = l_s1_s0p1 + inv_p1_p2 + (1023 - 997) * p2; - if ((l_e2 % 33) != 0) - continue; - var l_e1 = GF1024_LOG[s1_s0p2] + inv_p1_p2 + (1023 - 997) * p1; - if ((l_e1 % 33) != 0) - continue; - if (p1 < p2) - { - return new int[] { p1, p2 }; - } - else - { - return new int[] { p2, p1 }; - } - } - return new int[0]; - } - - internal Bech32Encoder(string hrp) : this(hrp == null ? null : Encoders.ASCII.DecodeData(hrp.ToLowerInvariant())) - { - } - public Bech32Encoder(byte[] hrp) - { - if (hrp == null) - throw new ArgumentNullException(nameof(hrp)); - - _Hrp = hrp; - var len = hrp.Length; - _HrpExpand = new byte[(2 * len) + 1]; - for (int i = 0; i < len; i++) - { - _HrpExpand[i] = (byte)(hrp[i] >> 5); - _HrpExpand[i + len + 1] = (byte)(hrp[i] & 31); - } - } - - protected readonly byte[] _HrpExpand; - protected readonly byte[] _Hrp; - public byte[] HumanReadablePart - { - get - { - return _Hrp; - } - } - - private static uint Polymod(ReadOnlySpan values) - { - uint chk = 1; - for (int i = 0; i < values.Length; i++) - { - var top = chk >> 25; - chk = values[i] ^ ((chk & 0x1ffffff) << 5); - chk ^= ((top >> 0) & 1) == 1 ? Generator[0] : 0; - chk ^= ((top >> 1) & 1) == 1 ? Generator[1] : 0; - chk ^= ((top >> 2) & 1) == 1 ? Generator[2] : 0; - chk ^= ((top >> 3) & 1) == 1 ? Generator[3] : 0; - chk ^= ((top >> 4) & 1) == 1 ? Generator[4] : 0; - } - return chk; - } - - protected virtual bool VerifyChecksum(byte[] data, int bechStringLen, out Bech32EncodingType encodingType, out int[] errorPosition) - { - return VerifyChecksum(data.AsSpan(), bechStringLen, out encodingType, out errorPosition); - } - - protected virtual bool VerifyChecksum(ReadOnlySpan data, int bechStringLen, out Bech32EncodingType encodingType, out int[] errorPosition) - { - errorPosition = null; - Span values = _HrpExpand.Length + data.Length is int v && v > 256 ? new byte[v] : stackalloc byte[v]; - _HrpExpand.CopyTo(values); - data.CopyTo(values.Slice(_HrpExpand.Length)); - var polymod = Polymod(values); - if (polymod == Bech32EncodingType.BECH32.EncodingConstant) - encodingType = Bech32EncodingType.BECH32; - else if (polymod == Bech32EncodingType.BECH32M.EncodingConstant) - encodingType = Bech32EncodingType.BECH32M; - else - { - encodingType = null; - - var epos = Bech32EncodingType.All - .Select(e => locate_errors(polymod ^ (uint)e.EncodingConstant, bechStringLen - 1)) - .Where(e => e.Length != 0) - .OrderByDescending(e => e.Length) - .FirstOrDefault(); - errorPosition = epos; - if (epos is null || epos.Length == 0) - return false; - for (var ep = 0; ep < epos.Length; ++ep) - { - epos[ep] = bechStringLen - epos[ep] - (epos[ep] >= data.Length ? 2 : 1); - } - return false; - } - return true; - } - - private void CreateChecksum(ReadOnlySpan data, Bech32EncodingType encodingType, Span ret) - { - Span values = _HrpExpand.Length + data.Length + 6 is int v && v > 256 ? new byte[v] : - stackalloc byte[v]; - var valuesOffset = 0; - _HrpExpand.AsSpan().CopyTo(values.Slice(valuesOffset)); - valuesOffset += _HrpExpand.Length; - data.CopyTo(values.Slice(valuesOffset)); - valuesOffset += data.Length; - var polymod = Polymod(values) ^ encodingType.EncodingConstant; - foreach (var i in Enumerable.Range(0, 6)) - { - ret[i] = (byte)((polymod >> 5 * (5 - i)) & 31); - } - } - - public virtual string EncodeData(ReadOnlySpan data, Bech32EncodingType encodingType) - { - if (encodingType == null) - throw new ArgumentNullException(nameof(encodingType)); - if (SquashBytes) - data = ByteSquasher(data, 8, 5).AsSpan(); - - Span combined = _Hrp.Length + 1 + data.Length + 6 is int v && v > 256 ? new byte[v] : - stackalloc byte[v]; - int combinedOffset = 0; - _Hrp.CopyTo(combined); - combinedOffset += _Hrp.Length; - combined[combinedOffset] = 49; - combinedOffset++; - - data.CopyTo(combined.Slice(combinedOffset)); - combinedOffset += data.Length; - Span checkSum = stackalloc byte[6]; - CreateChecksum(data, encodingType, checkSum); - - checkSum.CopyTo(combined.Slice(combinedOffset, 6)); - combinedOffset += 6; - - for (int i = 0; i < data.Length + 6; i++) - { - combined[_Hrp.Length + 1 + i] = Byteset[combined[_Hrp.Length + 1 + i]]; - } - return Encoders.ASCII.EncodeData(combined.ToArray()); - } - - public static Bech32Encoder ExtractEncoderFromString(string test) - { - var i = test.LastIndexOf('1'); - if (i == -1) - throw new FormatException("Invalid Bech32 string"); - - return Encoders.Bech32(test.Substring(0, i)); - } - - protected virtual void CheckCase(string hrp) - { - if (hrp.Length is 0) - return; - bool isLowercase = char.IsUpper(hrp[0]); - for (int i = 1; i < hrp.Length; i++) - { - if (isLowercase != char.IsUpper(hrp[i]) && !char.IsDigit(hrp[i])) - throw new FormatException("Invalid bech32 string, mixed case detected"); - } - } - public byte[] DecodeDataRaw(string encoded, out Bech32EncodingType encodingType) - { - return DecodeDataCore(encoded, out encodingType); - } - public bool StrictLength { get; set; } = true; - public bool SquashBytes { get; set; } = false; - - protected virtual byte[] DecodeDataCore(string encoded, out Bech32EncodingType encodingType) - { - if (encoded == null) - throw new ArgumentNullException(nameof(encoded)); - CheckCase(encoded); - encoded = encoded.ToLowerInvariant(); - Span buffer = encoded.Length is int v && v > 256 ? new byte[v] : stackalloc byte[v]; - ((ASCIIEncoder)Encoders.ASCII).DecodeData(encoded, buffer); - var pos = encoded.LastIndexOf("1", StringComparison.OrdinalIgnoreCase); - if (pos < 1) - { - throw new FormatException("The Bech32 string is missing separator '1'"); - } - else if (pos + 7 > encoded.Length) - { - throw new FormatException("The Bech32 string is too short"); - } - else if (StrictLength && encoded.Length > 90) - { - throw new FormatException("The Bech32 string is too long"); - } - if (pos != _Hrp.Length) - { - throw new FormatException("Mismatching human readable part"); - } - - for (int i = 0; i < _Hrp.Length; i++) - { - if (buffer[i] != _Hrp[i]) - throw new FormatException("Mismatching human readable part"); - } - Span data = encoded.Length - pos - 1 is int v2 && v2 > 256 ? new byte[v2] : stackalloc byte[v2]; - for (int j = 0, i = pos + 1; i < encoded.Length; i++, j++) - { - int index = Array.IndexOf(Byteset, buffer[i]); - if (index == -1) - throw new FormatException("bech chars are out of range"); - data[j] = (byte)index; - } - - int[] error; - if (!VerifyChecksum(data, encoded.Length, out encodingType, out error)) - { - if (error == null || error.Length == 0) - throw new FormatException("Error while verifying Bech32 checksum"); - else - throw new Bech32FormatException($"Error in Bech32 string at {String.Join(",", error)}", error); - } - var arr = data.Slice(0, data.Length - 6).ToArray(); - if (SquashBytes) - { - arr = ByteSquasher(arr, 5, 8); - if (arr is null) - throw new FormatException("Invalid squashed bech32"); - } - return arr; - } - - private static byte[] ByteSquasher(ReadOnlySpan input, int inputWidth, int outputWidth) - { - var bitstash = 0; - var accumulator = 0; - var output = new List(); - var maxOutputValue = (1 << outputWidth) - 1; - for (var i = 0; i < input.Length; i++) - { - var c = input[i]; - if (c >> inputWidth != 0) - { - return null; - } - - accumulator = (accumulator << inputWidth) | c; - bitstash += inputWidth; - while (bitstash >= outputWidth) - { - bitstash -= outputWidth; - output.Add((byte)((accumulator >> bitstash) & maxOutputValue)); - } - } - - // pad if going from 8 to 5 - if (inputWidth == 8 && outputWidth == 5) - { - if (bitstash != 0) output.Add((byte)((accumulator << (outputWidth - bitstash)) & maxOutputValue)); - } - else if (bitstash >= inputWidth || ((accumulator << (outputWidth - bitstash)) & maxOutputValue) != 0) - { - // no pad from 5 to 8 allowed - return null; - } - - return output.ToArray(); - } - - protected virtual byte[] ConvertBits(IEnumerable data, int fromBits, int toBits, bool pad = true) - { - return ConvertBits(data.ToArray().AsSpan(), fromBits, toBits, pad); - } - - protected virtual byte[] ConvertBits(ReadOnlySpan data, int fromBits, int toBits, bool pad = true) - { - var acc = 0; - var bits = 0; - var maxv = (1 << toBits) - 1; - var ret = new List(64); - foreach (var value in data) - { - if ((value >> fromBits) > 0) - throw new FormatException("Invalid Bech32 string"); - acc = (acc << fromBits) | value; - bits += fromBits; - while (bits >= toBits) - { - bits -= toBits; - ret.Add((byte)((acc >> bits) & maxv)); - } - } - if (pad) - { - if (bits > 0) - { - ret.Add((byte)((acc << (toBits - bits)) & maxv)); - } - } - else if (bits >= fromBits || (byte)(((acc << (toBits - bits)) & maxv)) != 0) - { - throw new FormatException("Invalid Bech32 string"); - } - return ret.ToArray(); - } - - public virtual byte[] Decode(string addr, out byte witnessVerion) - { - if (addr == null) - throw new ArgumentNullException(nameof(addr)); - CheckCase(addr); - var data = DecodeDataCore(addr, out var encodingType); - var decoded = ConvertBits(data.AsSpan().Slice(1), 5, 8, false); - if (decoded.Length < 2 || decoded.Length > 40) - throw new FormatException("Invalid decoded data length"); - witnessVerion = data[0]; - if (witnessVerion == 0 && encodingType != Bech32EncodingType.BECH32) - throw new FormatException("Decoded data should have used BECH32 encoding"); - if (witnessVerion != 0 && encodingType != Bech32EncodingType.BECH32M) - throw new FormatException("Decoded data should have used BECH32M encoding"); - if (witnessVerion > 16) - throw new FormatException("Invalid decoded witness version"); - - if (witnessVerion == 0 && decoded.Length != 20 && decoded.Length != 32) - throw new FormatException("Decoded witness program with unknown length"); - return decoded; - } - - public string EncodeRaw(byte[] data, Bech32EncodingType encodingType) - { - return EncodeData(data.AsSpan(), encodingType); - } - - public string EncodeRaw(ReadOnlySpan data, Bech32EncodingType encodingType) - { - return EncodeData(data, encodingType); - } - - public string Encode(byte witnessVerion, byte[] witnessProgramm) - { - if (witnessProgramm == null) - throw new ArgumentNullException(nameof(witnessProgramm)); - return Encode(witnessVerion, witnessProgramm.AsSpan()); - } - - public string Encode(byte witnessVerion, ReadOnlySpan witnessProgramm) - { - if (witnessVerion > 16) - throw new ArgumentOutOfRangeException(nameof(witnessVerion), "Invalid decoded witnessVerion, should <= 0 and > 16"); - var bits = ConvertBits(witnessProgramm, 8, 5); - Span data = 1 + bits.Length is int v && v > 256 ? new byte[v] : stackalloc byte[v]; - data[0] = witnessVerion; - bits.AsSpan().CopyTo(data.Slice(1)); - var ret = EncodeData(data, witnessVerion == 0 ? Bech32EncodingType.BECH32 : Bech32EncodingType.BECH32M); - return ret; - } - } + public abstract class DataEncoder + { + // char.IsWhiteSpace fits well but it match other whitespaces + // characters too and also works for unicode characters. + public static bool IsSpace(char c) + { + switch (c) + { + case ' ': + case '\t': + case '\n': + case '\v': + case '\f': + case '\r': + return true; + } + return false; + } + + internal DataEncoder() { } + + public string EncodeData(byte[] data) + { + return EncodeData(data, 0, data.Length); + } + + public abstract string EncodeData(byte[] data, int offset, int count); + + public virtual string EncodeData(ReadOnlySpan data) + { + return this.EncodeData(data.ToArray()); + } + + public abstract byte[] DecodeData(string encoded); + } + + public class ASCIIEncoder : DataEncoder + { + //Do not using Encoding.ASCII (not portable) + public override byte[] DecodeData(string encoded) + { + if (String.IsNullOrEmpty(encoded)) + return new byte[0]; + Span r = encoded.Length is int v && v > 256 ? new byte[v] : stackalloc byte[v]; + for (int i = 0; i < r.Length; i++) + { + r[i] = (byte)encoded[i]; + } + return r.ToArray(); + } + + public void DecodeData(string encoded, Span output) + { + var l = encoded.Length; + for (int i = 0; i < l; i++) + { + output[i] = (byte)encoded[i]; + } + } + + public override string EncodeData(byte[] data, int offset, int count) + { + return new String(data.Skip(offset).Take(count).Select(o => (char)o).ToArray()).Replace( + "\0", + "" + ); + } + } + + public static class Encoders + { + static readonly ASCIIEncoder _ASCII = new ASCIIEncoder(); + public static DataEncoder ASCII + { + get { return _ASCII; } + } + + public static Bech32Encoder Bech32(string hrp) + { + return new Bech32Encoder(hrp); + } + + public static Bech32Encoder Bech32(byte[] hrp) + { + return new Bech32Encoder(hrp); + } + } + + public class Bech32FormatException : FormatException + { + public Bech32FormatException(string message, int[] indexes) + : base(message) + { + if (indexes == null) + throw new ArgumentNullException(nameof(indexes)); + ErrorIndexes = indexes; + Array.Sort(ErrorIndexes); + } + + public int[] ErrorIndexes { get; internal set; } + } + + public class Bech32EncodingType + { + static Bech32EncodingType() + { + BECH32 = new Bech32EncodingType(1); + BECH32M = new Bech32EncodingType(0x2bc830a3); + All = new Bech32EncodingType[] { BECH32, BECH32M }; + } + + public static readonly Bech32EncodingType BECH32; + public static readonly Bech32EncodingType BECH32M; + public static readonly Bech32EncodingType[] All; + + public Bech32EncodingType(int encodingConstant) + { + EncodingConstant = encodingConstant; + } + + public int EncodingConstant { get; } + } + + public class Bech32Encoder + { + static readonly int[] GF1024_EXP = new int[] + { + 1, + 303, + 635, + 446, + 997, + 640, + 121, + 142, + 959, + 420, + 350, + 438, + 166, + 39, + 543, + 335, + 831, + 691, + 117, + 632, + 719, + 97, + 107, + 374, + 558, + 797, + 54, + 150, + 858, + 877, + 724, + 1013, + 294, + 23, + 354, + 61, + 164, + 633, + 992, + 538, + 469, + 659, + 174, + 868, + 184, + 809, + 766, + 563, + 866, + 851, + 257, + 520, + 45, + 770, + 535, + 524, + 408, + 213, + 436, + 760, + 472, + 330, + 933, + 799, + 616, + 361, + 15, + 391, + 756, + 814, + 58, + 608, + 554, + 680, + 993, + 821, + 942, + 813, + 843, + 484, + 193, + 935, + 321, + 919, + 572, + 741, + 423, + 559, + 562, + 589, + 296, + 191, + 493, + 685, + 891, + 665, + 435, + 60, + 395, + 2, + 606, + 511, + 853, + 746, + 32, + 219, + 284, + 631, + 840, + 661, + 837, + 332, + 78, + 311, + 670, + 887, + 111, + 195, + 505, + 190, + 194, + 214, + 709, + 380, + 819, + 69, + 261, + 957, + 1018, + 161, + 739, + 588, + 7, + 708, + 83, + 328, + 507, + 736, + 317, + 899, + 47, + 348, + 1000, + 345, + 882, + 245, + 367, + 996, + 943, + 514, + 304, + 90, + 804, + 295, + 312, + 793, + 387, + 833, + 249, + 921, + 660, + 618, + 823, + 496, + 722, + 30, + 782, + 225, + 892, + 93, + 480, + 372, + 112, + 738, + 867, + 636, + 890, + 950, + 968, + 386, + 622, + 642, + 551, + 369, + 234, + 846, + 382, + 365, + 442, + 592, + 343, + 986, + 122, + 1023, + 59, + 847, + 81, + 790, + 4, + 437, + 983, + 931, + 244, + 64, + 415, + 529, + 487, + 944, + 35, + 938, + 664, + 156, + 583, + 53, + 999, + 222, + 390, + 987, + 341, + 388, + 389, + 170, + 721, + 879, + 138, + 522, + 627, + 765, + 322, + 230, + 440, + 14, + 168, + 143, + 656, + 991, + 224, + 595, + 550, + 94, + 657, + 752, + 667, + 1005, + 451, + 734, + 744, + 638, + 292, + 585, + 157, + 872, + 590, + 601, + 827, + 774, + 930, + 475, + 571, + 33, + 500, + 871, + 969, + 173, + 21, + 828, + 450, + 1009, + 147, + 960, + 705, + 201, + 228, + 998, + 497, + 1021, + 613, + 688, + 772, + 508, + 36, + 366, + 715, + 468, + 956, + 725, + 730, + 861, + 425, + 647, + 701, + 221, + 759, + 95, + 958, + 139, + 805, + 8, + 835, + 679, + 614, + 449, + 128, + 791, + 299, + 974, + 617, + 70, + 628, + 57, + 273, + 430, + 67, + 750, + 405, + 780, + 703, + 643, + 776, + 778, + 340, + 171, + 1022, + 276, + 308, + 495, + 243, + 644, + 460, + 857, + 28, + 336, + 286, + 41, + 695, + 448, + 431, + 364, + 149, + 43, + 233, + 63, + 762, + 902, + 181, + 240, + 501, + 584, + 434, + 275, + 1008, + 444, + 443, + 895, + 812, + 612, + 927, + 383, + 66, + 961, + 1006, + 690, + 346, + 3, + 881, + 900, + 747, + 271, + 672, + 162, + 402, + 456, + 748, + 971, + 755, + 490, + 105, + 808, + 977, + 72, + 732, + 182, + 897, + 625, + 163, + 189, + 947, + 850, + 46, + 115, + 403, + 231, + 151, + 629, + 278, + 874, + 16, + 934, + 110, + 492, + 898, + 256, + 807, + 598, + 700, + 498, + 140, + 481, + 91, + 523, + 860, + 134, + 252, + 771, + 824, + 119, + 38, + 816, + 820, + 641, + 342, + 757, + 513, + 577, + 990, + 463, + 40, + 920, + 955, + 17, + 649, + 533, + 82, + 103, + 896, + 862, + 728, + 259, + 86, + 466, + 87, + 253, + 556, + 323, + 457, + 963, + 432, + 845, + 527, + 745, + 849, + 863, + 1015, + 888, + 488, + 567, + 727, + 132, + 674, + 764, + 109, + 669, + 6, + 1003, + 552, + 246, + 542, + 96, + 324, + 781, + 912, + 248, + 694, + 239, + 980, + 210, + 880, + 683, + 144, + 177, + 325, + 546, + 491, + 326, + 339, + 623, + 941, + 92, + 207, + 783, + 462, + 263, + 483, + 517, + 1012, + 9, + 620, + 220, + 984, + 548, + 512, + 878, + 421, + 113, + 973, + 280, + 962, + 159, + 310, + 945, + 268, + 465, + 806, + 889, + 199, + 76, + 873, + 865, + 34, + 645, + 227, + 290, + 418, + 693, + 926, + 80, + 569, + 639, + 11, + 50, + 291, + 141, + 206, + 544, + 949, + 185, + 518, + 133, + 909, + 135, + 467, + 376, + 646, + 914, + 678, + 841, + 954, + 318, + 242, + 939, + 951, + 743, + 1017, + 976, + 359, + 167, + 264, + 100, + 241, + 218, + 51, + 12, + 758, + 368, + 453, + 309, + 192, + 648, + 826, + 553, + 473, + 101, + 478, + 673, + 397, + 1001, + 118, + 265, + 331, + 650, + 356, + 982, + 652, + 655, + 510, + 634, + 145, + 414, + 830, + 924, + 526, + 966, + 298, + 737, + 18, + 504, + 401, + 697, + 360, + 288, + 1020, + 842, + 203, + 698, + 537, + 676, + 279, + 581, + 619, + 536, + 907, + 876, + 1019, + 398, + 152, + 1010, + 994, + 68, + 42, + 454, + 580, + 836, + 99, + 565, + 137, + 379, + 503, + 22, + 77, + 582, + 282, + 412, + 352, + 611, + 347, + 300, + 266, + 570, + 270, + 911, + 729, + 44, + 557, + 108, + 946, + 637, + 597, + 461, + 630, + 615, + 238, + 763, + 681, + 718, + 334, + 528, + 200, + 459, + 413, + 79, + 24, + 229, + 713, + 906, + 579, + 384, + 48, + 893, + 370, + 923, + 202, + 917, + 98, + 794, + 754, + 197, + 530, + 662, + 52, + 712, + 677, + 56, + 62, + 981, + 509, + 267, + 789, + 885, + 561, + 316, + 684, + 596, + 226, + 13, + 985, + 779, + 123, + 720, + 576, + 753, + 948, + 406, + 125, + 315, + 104, + 519, + 426, + 502, + 313, + 566, + 1016, + 767, + 796, + 281, + 749, + 740, + 136, + 84, + 908, + 424, + 936, + 198, + 355, + 274, + 735, + 967, + 5, + 154, + 428, + 541, + 785, + 704, + 486, + 671, + 600, + 532, + 381, + 540, + 574, + 187, + 88, + 378, + 216, + 621, + 499, + 419, + 922, + 485, + 494, + 476, + 255, + 114, + 188, + 668, + 297, + 400, + 918, + 787, + 158, + 25, + 458, + 178, + 564, + 422, + 768, + 73, + 1011, + 717, + 575, + 404, + 547, + 196, + 829, + 237, + 394, + 301, + 37, + 65, + 176, + 106, + 89, + 85, + 675, + 979, + 534, + 803, + 995, + 363, + 593, + 120, + 417, + 452, + 26, + 699, + 822, + 223, + 169, + 416, + 235, + 609, + 773, + 211, + 607, + 208, + 302, + 852, + 965, + 603, + 357, + 761, + 247, + 817, + 539, + 250, + 232, + 272, + 129, + 568, + 848, + 624, + 396, + 710, + 525, + 183, + 686, + 10, + 285, + 856, + 307, + 811, + 160, + 972, + 55, + 441, + 289, + 723, + 305, + 373, + 351, + 153, + 733, + 409, + 506, + 975, + 838, + 573, + 970, + 988, + 913, + 471, + 205, + 337, + 49, + 594, + 777, + 549, + 815, + 277, + 27, + 916, + 333, + 353, + 844, + 800, + 146, + 751, + 186, + 375, + 769, + 358, + 392, + 883, + 474, + 788, + 602, + 74, + 130, + 329, + 212, + 155, + 131, + 102, + 687, + 293, + 870, + 742, + 726, + 427, + 217, + 834, + 904, + 29, + 127, + 869, + 407, + 338, + 832, + 470, + 482, + 810, + 399, + 439, + 393, + 604, + 929, + 682, + 447, + 714, + 251, + 455, + 875, + 319, + 477, + 464, + 521, + 258, + 377, + 937, + 489, + 792, + 172, + 314, + 327, + 124, + 20, + 531, + 953, + 591, + 886, + 320, + 696, + 71, + 859, + 578, + 175, + 587, + 707, + 663, + 283, + 179, + 795, + 989, + 702, + 940, + 371, + 692, + 689, + 555, + 903, + 410, + 651, + 75, + 429, + 818, + 362, + 894, + 515, + 31, + 545, + 666, + 706, + 952, + 864, + 269, + 254, + 349, + 711, + 802, + 716, + 784, + 1007, + 925, + 801, + 445, + 148, + 260, + 658, + 385, + 287, + 262, + 204, + 126, + 586, + 1004, + 236, + 165, + 854, + 411, + 932, + 560, + 19, + 215, + 1002, + 775, + 653, + 928, + 901, + 964, + 884, + 798, + 839, + 786, + 433, + 610, + 116, + 855, + 180, + 479, + 910, + 1014, + 599, + 915, + 905, + 306, + 516, + 731, + 626, + 978, + 825, + 344, + 605, + 654, + 209, + }; + + static readonly int[] GF1024_LOG = new[] + { + -1, + 0, + 99, + 363, + 198, + 726, + 462, + 132, + 297, + 495, + 825, + 528, + 561, + 693, + 231, + 66, + 396, + 429, + 594, + 990, + 924, + 264, + 627, + 33, + 660, + 759, + 792, + 858, + 330, + 891, + 165, + 957, + 104, + 259, + 518, + 208, + 280, + 776, + 416, + 13, + 426, + 333, + 618, + 339, + 641, + 52, + 388, + 140, + 666, + 852, + 529, + 560, + 678, + 213, + 26, + 832, + 681, + 309, + 70, + 194, + 97, + 35, + 682, + 341, + 203, + 777, + 358, + 312, + 617, + 125, + 307, + 931, + 379, + 765, + 875, + 951, + 515, + 628, + 112, + 659, + 525, + 196, + 432, + 134, + 717, + 781, + 438, + 440, + 740, + 780, + 151, + 408, + 487, + 169, + 239, + 293, + 467, + 21, + 672, + 622, + 557, + 571, + 881, + 433, + 704, + 376, + 779, + 22, + 643, + 460, + 398, + 116, + 172, + 503, + 751, + 389, + 1004, + 18, + 576, + 415, + 789, + 6, + 192, + 696, + 923, + 702, + 981, + 892, + 302, + 816, + 876, + 880, + 457, + 537, + 411, + 539, + 716, + 624, + 224, + 295, + 406, + 531, + 7, + 233, + 478, + 586, + 864, + 268, + 974, + 338, + 27, + 392, + 614, + 839, + 727, + 879, + 211, + 250, + 758, + 507, + 830, + 129, + 369, + 384, + 36, + 985, + 12, + 555, + 232, + 796, + 221, + 321, + 920, + 263, + 42, + 934, + 778, + 479, + 761, + 939, + 1006, + 344, + 381, + 823, + 44, + 535, + 866, + 739, + 752, + 385, + 119, + 91, + 566, + 80, + 120, + 117, + 771, + 675, + 721, + 514, + 656, + 271, + 670, + 602, + 980, + 850, + 532, + 488, + 803, + 1022, + 475, + 801, + 878, + 57, + 121, + 991, + 742, + 888, + 559, + 105, + 497, + 291, + 215, + 795, + 236, + 167, + 692, + 520, + 272, + 661, + 229, + 391, + 814, + 340, + 184, + 798, + 984, + 773, + 650, + 473, + 345, + 558, + 548, + 326, + 202, + 145, + 465, + 810, + 471, + 158, + 813, + 908, + 412, + 441, + 964, + 750, + 401, + 50, + 915, + 437, + 975, + 126, + 979, + 491, + 556, + 577, + 636, + 685, + 510, + 963, + 638, + 367, + 815, + 310, + 723, + 349, + 323, + 857, + 394, + 606, + 505, + 713, + 630, + 938, + 106, + 826, + 332, + 978, + 599, + 834, + 521, + 530, + 248, + 883, + 32, + 153, + 90, + 754, + 592, + 304, + 635, + 775, + 804, + 1, + 150, + 836, + 1013, + 828, + 324, + 565, + 508, + 113, + 154, + 708, + 921, + 703, + 689, + 138, + 547, + 911, + 929, + 82, + 228, + 443, + 468, + 480, + 483, + 922, + 135, + 877, + 61, + 578, + 111, + 860, + 654, + 15, + 331, + 851, + 895, + 484, + 320, + 218, + 420, + 190, + 1019, + 143, + 362, + 634, + 141, + 965, + 10, + 838, + 632, + 861, + 34, + 722, + 580, + 808, + 869, + 554, + 598, + 65, + 954, + 787, + 337, + 187, + 281, + 146, + 563, + 183, + 668, + 944, + 171, + 837, + 23, + 867, + 541, + 916, + 741, + 625, + 123, + 736, + 186, + 357, + 665, + 977, + 179, + 156, + 219, + 220, + 216, + 67, + 870, + 902, + 774, + 98, + 820, + 574, + 613, + 900, + 755, + 596, + 370, + 390, + 769, + 314, + 701, + 894, + 56, + 841, + 949, + 987, + 631, + 658, + 587, + 204, + 797, + 790, + 522, + 745, + 9, + 502, + 763, + 86, + 719, + 288, + 706, + 887, + 728, + 952, + 311, + 336, + 446, + 1002, + 348, + 96, + 58, + 199, + 11, + 901, + 230, + 833, + 188, + 352, + 351, + 973, + 3, + 906, + 335, + 301, + 266, + 244, + 791, + 564, + 619, + 909, + 371, + 444, + 760, + 657, + 328, + 647, + 490, + 425, + 913, + 511, + 439, + 540, + 283, + 40, + 897, + 849, + 60, + 570, + 872, + 257, + 749, + 912, + 572, + 1007, + 170, + 407, + 898, + 492, + 79, + 747, + 732, + 206, + 454, + 918, + 375, + 482, + 399, + 92, + 748, + 325, + 163, + 274, + 405, + 744, + 260, + 346, + 707, + 626, + 595, + 118, + 842, + 136, + 279, + 684, + 584, + 101, + 500, + 422, + 149, + 956, + 1014, + 493, + 536, + 705, + 51, + 914, + 225, + 409, + 55, + 822, + 590, + 448, + 655, + 205, + 676, + 925, + 735, + 431, + 784, + 54, + 609, + 604, + 39, + 812, + 737, + 729, + 466, + 14, + 533, + 958, + 481, + 770, + 499, + 855, + 238, + 182, + 464, + 569, + 72, + 947, + 442, + 642, + 24, + 87, + 989, + 688, + 88, + 47, + 762, + 623, + 709, + 455, + 817, + 526, + 637, + 258, + 84, + 845, + 738, + 768, + 698, + 423, + 933, + 664, + 620, + 607, + 629, + 212, + 347, + 249, + 982, + 935, + 131, + 89, + 252, + 927, + 189, + 788, + 853, + 237, + 691, + 646, + 403, + 1010, + 734, + 253, + 874, + 807, + 903, + 1020, + 100, + 802, + 71, + 799, + 1003, + 633, + 355, + 276, + 300, + 649, + 64, + 306, + 161, + 608, + 496, + 743, + 180, + 485, + 819, + 383, + 1016, + 226, + 308, + 393, + 648, + 107, + 19, + 37, + 585, + 2, + 175, + 645, + 247, + 527, + 5, + 419, + 181, + 317, + 327, + 519, + 542, + 289, + 567, + 430, + 579, + 950, + 582, + 994, + 1021, + 583, + 234, + 240, + 976, + 41, + 160, + 109, + 677, + 937, + 210, + 95, + 959, + 242, + 753, + 461, + 114, + 733, + 368, + 573, + 458, + 782, + 605, + 680, + 544, + 299, + 73, + 652, + 905, + 477, + 690, + 93, + 824, + 882, + 277, + 946, + 361, + 17, + 945, + 523, + 472, + 334, + 930, + 597, + 603, + 793, + 404, + 290, + 942, + 316, + 731, + 270, + 960, + 936, + 133, + 122, + 821, + 966, + 679, + 662, + 907, + 282, + 968, + 767, + 653, + 20, + 697, + 222, + 164, + 835, + 30, + 285, + 886, + 456, + 436, + 640, + 286, + 1015, + 380, + 840, + 245, + 724, + 137, + 593, + 173, + 130, + 715, + 85, + 885, + 551, + 246, + 449, + 103, + 366, + 372, + 714, + 313, + 865, + 241, + 699, + 674, + 374, + 68, + 421, + 562, + 292, + 59, + 809, + 342, + 651, + 459, + 227, + 46, + 711, + 764, + 868, + 53, + 413, + 278, + 800, + 255, + 993, + 318, + 854, + 319, + 695, + 315, + 469, + 166, + 489, + 969, + 730, + 1001, + 757, + 873, + 686, + 197, + 303, + 919, + 155, + 673, + 940, + 712, + 25, + 999, + 63, + 863, + 972, + 967, + 785, + 152, + 296, + 512, + 402, + 377, + 45, + 899, + 829, + 354, + 77, + 69, + 856, + 417, + 811, + 953, + 124, + 418, + 75, + 794, + 162, + 414, + 1018, + 568, + 254, + 265, + 772, + 588, + 16, + 896, + 157, + 889, + 298, + 621, + 110, + 844, + 1000, + 108, + 545, + 601, + 78, + 862, + 447, + 185, + 195, + 818, + 450, + 387, + 49, + 805, + 102, + 986, + 1005, + 827, + 329, + 28, + 932, + 410, + 287, + 435, + 451, + 962, + 517, + 48, + 174, + 43, + 893, + 884, + 261, + 251, + 516, + 395, + 910, + 611, + 29, + 501, + 223, + 476, + 364, + 144, + 871, + 998, + 687, + 928, + 115, + 453, + 513, + 176, + 94, + 168, + 667, + 955, + 353, + 434, + 382, + 400, + 139, + 365, + 996, + 343, + 948, + 890, + 1012, + 663, + 610, + 718, + 538, + 1008, + 639, + 470, + 848, + 543, + 1011, + 859, + 671, + 756, + 83, + 427, + 159, + 746, + 669, + 589, + 971, + 524, + 356, + 995, + 904, + 256, + 201, + 988, + 62, + 397, + 81, + 720, + 917, + 209, + 549, + 943, + 486, + 76, + 148, + 207, + 509, + 644, + 386, + 700, + 534, + 177, + 550, + 961, + 926, + 546, + 428, + 284, + 127, + 294, + 8, + 269, + 359, + 506, + 445, + 997, + 806, + 591, + 725, + 178, + 262, + 846, + 373, + 831, + 504, + 305, + 843, + 553, + 378, + 1017, + 783, + 474, + 683, + 581, + 200, + 498, + 694, + 191, + 217, + 847, + 941, + 424, + 235, + 38, + 74, + 616, + 786, + 147, + 4, + 273, + 214, + 142, + 575, + 992, + 463, + 983, + 243, + 360, + 970, + 350, + 267, + 615, + 766, + 494, + 31, + 1009, + 452, + 710, + 552, + 128, + 612, + 600, + 275, + 322, + 193, + }; + + protected static readonly byte[] Byteset; + + static Bech32Encoder() + { + Byteset = Encoders.ASCII.DecodeData("qpzry9x8gf2tvdw0s3jn54khce6mua7l"); + } + + private static readonly uint[] Generator = + { + 0x3b6a57b2U, + 0x26508e6dU, + 0x1ea119faU, + 0x3d4233ddU, + 0x2a1462b3U, + }; + + uint syndrome(uint residue) + { + var low = residue & 0x1f; + return (uint)( + low + ^ (low << 10) + ^ (low << 20) + ^ (((residue >> 5) & 1) != 0 ? 0x31edd3c4 : 0) + ^ (((residue >> 6) & 1) != 0 ? 0x335f86a8 : 0) + ^ (((residue >> 7) & 1) != 0 ? 0x363b8870 : 0) + ^ (((residue >> 8) & 1) != 0 ? 0x3e6390c9 : 0) + ^ (((residue >> 9) & 1) != 0 ? 0x2ec72192 : 0) + ^ (((residue >> 10) & 1) != 0 ? 0x1046f79d : 0) + ^ (((residue >> 11) & 1) != 0 ? 0x208d4e33 : 0) + ^ (((residue >> 12) & 1) != 0 ? 0x130ebd6f : 0) + ^ (((residue >> 13) & 1) != 0 ? 0x2499fade : 0) + ^ (((residue >> 14) & 1) != 0 ? 0x1b27d4b5 : 0) + ^ (((residue >> 15) & 1) != 0 ? 0x04be1eb4 : 0) + ^ (((residue >> 16) & 1) != 0 ? 0x0968b861 : 0) + ^ (((residue >> 17) & 1) != 0 ? 0x1055f0c2 : 0) + ^ (((residue >> 18) & 1) != 0 ? 0x20ab4584 : 0) + ^ (((residue >> 19) & 1) != 0 ? 0x1342af08 : 0) + ^ (((residue >> 20) & 1) != 0 ? 0x24f1f318 : 0) + ^ (((residue >> 21) & 1) != 0 ? 0x1be34739 : 0) + ^ (((residue >> 22) & 1) != 0 ? 0x35562f7b : 0) + ^ (((residue >> 23) & 1) != 0 ? 0x3a3c5bff : 0) + ^ (((residue >> 24) & 1) != 0 ? 0x266c96f7 : 0) + ^ (((residue >> 25) & 1) != 0 ? 0x25c78b65 : 0) + ^ (((residue >> 26) & 1) != 0 ? 0x1b1f13ea : 0) + ^ (((residue >> 27) & 1) != 0 ? 0x34baa2f4 : 0) + ^ (((residue >> 28) & 1) != 0 ? 0x3b61c0e1 : 0) + ^ (((residue >> 29) & 1) != 0 ? 0x265325c2 : 0) + ); + } + + int[] locate_errors(uint residue, int length) + { + if (residue == 0) + { + return new int[0]; + } + var syn = syndrome(residue); + var s0 = syn & 0x3FF; + var s1 = (syn >> 10) & 0x3FF; + var s2 = syn >> 20; + var l_s0 = GF1024_LOG[s0]; + var l_s1 = GF1024_LOG[s1]; + var l_s2 = GF1024_LOG[s2]; + if ( + l_s0 != -1 + && l_s1 != -1 + && l_s2 != -1 + && (2 * l_s1 - l_s2 - l_s0 + 2046) % 1023 == 0 + ) + { + var p1 = (l_s1 - l_s0 + 1023) % 1023; + if (p1 >= length) + return new int[0]; + var l_e1 = l_s0 + (1023 - 997) * p1; + if ((l_e1 % 33) != 0) + return new int[0]; + return new[] { p1 }; + } + for (var p1 = 0; p1 < length; p1++) + { + var s2_s1p1 = s2 ^ (s1 == 0 ? 0 : GF1024_EXP[(l_s1 + p1) % 1023]); + if (s2_s1p1 == 0) + continue; + var s1_s0p1 = s1 ^ (s0 == 0 ? 0 : GF1024_EXP[(l_s0 + p1) % 1023]); + if (s1_s0p1 == 0) + continue; + var l_s1_s0p1 = GF1024_LOG[s1_s0p1]; + var p2 = (GF1024_LOG[s2_s1p1] - l_s1_s0p1 + 1023) % 1023; + if (p2 >= length || p1 == p2) + continue; + var s1_s0p2 = s1 ^ (s0 == 0 ? 0 : GF1024_EXP[(l_s0 + p2) % 1023]); + if (s1_s0p2 == 0) + continue; + var inv_p1_p2 = 1023 - GF1024_LOG[GF1024_EXP[p1] ^ GF1024_EXP[p2]]; + var l_e2 = l_s1_s0p1 + inv_p1_p2 + (1023 - 997) * p2; + if ((l_e2 % 33) != 0) + continue; + var l_e1 = GF1024_LOG[s1_s0p2] + inv_p1_p2 + (1023 - 997) * p1; + if ((l_e1 % 33) != 0) + continue; + if (p1 < p2) + { + return new int[] { p1, p2 }; + } + else + { + return new int[] { p2, p1 }; + } + } + return new int[0]; + } + + internal Bech32Encoder(string hrp) + : this(hrp == null ? null : Encoders.ASCII.DecodeData(hrp.ToLowerInvariant())) { } + + public Bech32Encoder(byte[] hrp) + { + if (hrp == null) + throw new ArgumentNullException(nameof(hrp)); + + _Hrp = hrp; + var len = hrp.Length; + _HrpExpand = new byte[(2 * len) + 1]; + for (int i = 0; i < len; i++) + { + _HrpExpand[i] = (byte)(hrp[i] >> 5); + _HrpExpand[i + len + 1] = (byte)(hrp[i] & 31); + } + } + + protected readonly byte[] _HrpExpand; + protected readonly byte[] _Hrp; + public byte[] HumanReadablePart + { + get { return _Hrp; } + } + + private static uint Polymod(ReadOnlySpan values) + { + uint chk = 1; + for (int i = 0; i < values.Length; i++) + { + var top = chk >> 25; + chk = values[i] ^ ((chk & 0x1ffffff) << 5); + chk ^= ((top >> 0) & 1) == 1 ? Generator[0] : 0; + chk ^= ((top >> 1) & 1) == 1 ? Generator[1] : 0; + chk ^= ((top >> 2) & 1) == 1 ? Generator[2] : 0; + chk ^= ((top >> 3) & 1) == 1 ? Generator[3] : 0; + chk ^= ((top >> 4) & 1) == 1 ? Generator[4] : 0; + } + return chk; + } + + protected virtual bool VerifyChecksum( + byte[] data, + int bechStringLen, + out Bech32EncodingType encodingType, + out int[] errorPosition + ) + { + return VerifyChecksum( + data.AsSpan(), + bechStringLen, + out encodingType, + out errorPosition + ); + } + + protected virtual bool VerifyChecksum( + ReadOnlySpan data, + int bechStringLen, + out Bech32EncodingType encodingType, + out int[] errorPosition + ) + { + errorPosition = null; + Span values = + _HrpExpand.Length + data.Length is int v && v > 256 + ? new byte[v] + : stackalloc byte[v]; + _HrpExpand.CopyTo(values); + data.CopyTo(values.Slice(_HrpExpand.Length)); + var polymod = Polymod(values); + if (polymod == Bech32EncodingType.BECH32.EncodingConstant) + encodingType = Bech32EncodingType.BECH32; + else if (polymod == Bech32EncodingType.BECH32M.EncodingConstant) + encodingType = Bech32EncodingType.BECH32M; + else + { + encodingType = null; + + var epos = Bech32EncodingType + .All.Select(e => + locate_errors(polymod ^ (uint)e.EncodingConstant, bechStringLen - 1) + ) + .Where(e => e.Length != 0) + .OrderByDescending(e => e.Length) + .FirstOrDefault(); + errorPosition = epos; + if (epos is null || epos.Length == 0) + return false; + for (var ep = 0; ep < epos.Length; ++ep) + { + epos[ep] = bechStringLen - epos[ep] - (epos[ep] >= data.Length ? 2 : 1); + } + return false; + } + return true; + } + + private void CreateChecksum( + ReadOnlySpan data, + Bech32EncodingType encodingType, + Span ret + ) + { + Span values = + _HrpExpand.Length + data.Length + 6 is int v && v > 256 + ? new byte[v] + : stackalloc byte[v]; + var valuesOffset = 0; + _HrpExpand.AsSpan().CopyTo(values.Slice(valuesOffset)); + valuesOffset += _HrpExpand.Length; + data.CopyTo(values.Slice(valuesOffset)); + valuesOffset += data.Length; + var polymod = Polymod(values) ^ encodingType.EncodingConstant; + foreach (var i in Enumerable.Range(0, 6)) + { + ret[i] = (byte)((polymod >> 5 * (5 - i)) & 31); + } + } + + public virtual string EncodeData(ReadOnlySpan data, Bech32EncodingType encodingType) + { + if (encodingType == null) + throw new ArgumentNullException(nameof(encodingType)); + if (SquashBytes) + data = ByteSquasher(data, 8, 5).AsSpan(); + + Span combined = + _Hrp.Length + 1 + data.Length + 6 is int v && v > 256 + ? new byte[v] + : stackalloc byte[v]; + int combinedOffset = 0; + _Hrp.CopyTo(combined); + combinedOffset += _Hrp.Length; + combined[combinedOffset] = 49; + combinedOffset++; + + data.CopyTo(combined.Slice(combinedOffset)); + combinedOffset += data.Length; + Span checkSum = stackalloc byte[6]; + CreateChecksum(data, encodingType, checkSum); + + checkSum.CopyTo(combined.Slice(combinedOffset, 6)); + combinedOffset += 6; + + for (int i = 0; i < data.Length + 6; i++) + { + combined[_Hrp.Length + 1 + i] = Byteset[combined[_Hrp.Length + 1 + i]]; + } + return Encoders.ASCII.EncodeData(combined.ToArray()); + } + + public static Bech32Encoder ExtractEncoderFromString(string test) + { + var i = test.LastIndexOf('1'); + if (i == -1) + throw new FormatException("Invalid Bech32 string"); + + return Encoders.Bech32(test.Substring(0, i)); + } + + protected virtual void CheckCase(string hrp) + { + if (hrp.Length is 0) + return; + bool isLowercase = char.IsUpper(hrp[0]); + for (int i = 1; i < hrp.Length; i++) + { + if (isLowercase != char.IsUpper(hrp[i]) && !char.IsDigit(hrp[i])) + throw new FormatException("Invalid bech32 string, mixed case detected"); + } + } + + public byte[] DecodeDataRaw(string encoded, out Bech32EncodingType encodingType) + { + return DecodeDataCore(encoded, out encodingType); + } + + public bool StrictLength { get; set; } = true; + public bool SquashBytes { get; set; } = false; + + protected virtual byte[] DecodeDataCore(string encoded, out Bech32EncodingType encodingType) + { + if (encoded == null) + throw new ArgumentNullException(nameof(encoded)); + CheckCase(encoded); + encoded = encoded.ToLowerInvariant(); + Span buffer = + encoded.Length is int v && v > 256 ? new byte[v] : stackalloc byte[v]; + ((ASCIIEncoder)Encoders.ASCII).DecodeData(encoded, buffer); + var pos = encoded.LastIndexOf("1", StringComparison.OrdinalIgnoreCase); + if (pos < 1) + { + throw new FormatException("The Bech32 string is missing separator '1'"); + } + else if (pos + 7 > encoded.Length) + { + throw new FormatException("The Bech32 string is too short"); + } + else if (StrictLength && encoded.Length > 90) + { + throw new FormatException("The Bech32 string is too long"); + } + if (pos != _Hrp.Length) + { + throw new FormatException("Mismatching human readable part"); + } + + for (int i = 0; i < _Hrp.Length; i++) + { + if (buffer[i] != _Hrp[i]) + throw new FormatException("Mismatching human readable part"); + } + Span data = + encoded.Length - pos - 1 is int v2 && v2 > 256 ? new byte[v2] : stackalloc byte[v2]; + for (int j = 0, i = pos + 1; i < encoded.Length; i++, j++) + { + int index = Array.IndexOf(Byteset, buffer[i]); + if (index == -1) + throw new FormatException("bech chars are out of range"); + data[j] = (byte)index; + } + + int[] error; + if (!VerifyChecksum(data, encoded.Length, out encodingType, out error)) + { + if (error == null || error.Length == 0) + throw new FormatException("Error while verifying Bech32 checksum"); + else + throw new Bech32FormatException( + $"Error in Bech32 string at {String.Join(",", error)}", + error + ); + } + var arr = data.Slice(0, data.Length - 6).ToArray(); + if (SquashBytes) + { + arr = ByteSquasher(arr, 5, 8); + if (arr is null) + throw new FormatException("Invalid squashed bech32"); + } + return arr; + } + + private static byte[] ByteSquasher( + ReadOnlySpan input, + int inputWidth, + int outputWidth + ) + { + var bitstash = 0; + var accumulator = 0; + var output = new List(); + var maxOutputValue = (1 << outputWidth) - 1; + for (var i = 0; i < input.Length; i++) + { + var c = input[i]; + if (c >> inputWidth != 0) + { + return null; + } + + accumulator = (accumulator << inputWidth) | c; + bitstash += inputWidth; + while (bitstash >= outputWidth) + { + bitstash -= outputWidth; + output.Add((byte)((accumulator >> bitstash) & maxOutputValue)); + } + } + + // pad if going from 8 to 5 + if (inputWidth == 8 && outputWidth == 5) + { + if (bitstash != 0) + output.Add((byte)((accumulator << (outputWidth - bitstash)) & maxOutputValue)); + } + else if ( + bitstash >= inputWidth + || ((accumulator << (outputWidth - bitstash)) & maxOutputValue) != 0 + ) + { + // no pad from 5 to 8 allowed + return null; + } + + return output.ToArray(); + } + + protected virtual byte[] ConvertBits( + IEnumerable data, + int fromBits, + int toBits, + bool pad = true + ) + { + return ConvertBits(data.ToArray().AsSpan(), fromBits, toBits, pad); + } + + protected virtual byte[] ConvertBits( + ReadOnlySpan data, + int fromBits, + int toBits, + bool pad = true + ) + { + var acc = 0; + var bits = 0; + var maxv = (1 << toBits) - 1; + var ret = new List(64); + foreach (var value in data) + { + if ((value >> fromBits) > 0) + throw new FormatException("Invalid Bech32 string"); + acc = (acc << fromBits) | value; + bits += fromBits; + while (bits >= toBits) + { + bits -= toBits; + ret.Add((byte)((acc >> bits) & maxv)); + } + } + if (pad) + { + if (bits > 0) + { + ret.Add((byte)((acc << (toBits - bits)) & maxv)); + } + } + else if (bits >= fromBits || (byte)(((acc << (toBits - bits)) & maxv)) != 0) + { + throw new FormatException("Invalid Bech32 string"); + } + return ret.ToArray(); + } + + public virtual byte[] Decode(string addr, out byte witnessVerion) + { + if (addr == null) + throw new ArgumentNullException(nameof(addr)); + CheckCase(addr); + var data = DecodeDataCore(addr, out var encodingType); + var decoded = ConvertBits(data.AsSpan().Slice(1), 5, 8, false); + if (decoded.Length < 2 || decoded.Length > 40) + throw new FormatException("Invalid decoded data length"); + witnessVerion = data[0]; + if (witnessVerion == 0 && encodingType != Bech32EncodingType.BECH32) + throw new FormatException("Decoded data should have used BECH32 encoding"); + if (witnessVerion != 0 && encodingType != Bech32EncodingType.BECH32M) + throw new FormatException("Decoded data should have used BECH32M encoding"); + if (witnessVerion > 16) + throw new FormatException("Invalid decoded witness version"); + + if (witnessVerion == 0 && decoded.Length != 20 && decoded.Length != 32) + throw new FormatException("Decoded witness program with unknown length"); + return decoded; + } + + public string EncodeRaw(byte[] data, Bech32EncodingType encodingType) + { + return EncodeData(data.AsSpan(), encodingType); + } + + public string EncodeRaw(ReadOnlySpan data, Bech32EncodingType encodingType) + { + return EncodeData(data, encodingType); + } + + public string Encode(byte witnessVerion, byte[] witnessProgramm) + { + if (witnessProgramm == null) + throw new ArgumentNullException(nameof(witnessProgramm)); + return Encode(witnessVerion, witnessProgramm.AsSpan()); + } + + public string Encode(byte witnessVerion, ReadOnlySpan witnessProgramm) + { + if (witnessVerion > 16) + throw new ArgumentOutOfRangeException( + nameof(witnessVerion), + "Invalid decoded witnessVerion, should <= 0 and > 16" + ); + var bits = ConvertBits(witnessProgramm, 8, 5); + Span data = + 1 + bits.Length is int v && v > 256 ? new byte[v] : stackalloc byte[v]; + data[0] = witnessVerion; + bits.AsSpan().CopyTo(data.Slice(1)); + var ret = EncodeData( + data, + witnessVerion == 0 ? Bech32EncodingType.BECH32 : Bech32EncodingType.BECH32M + ); + return ret; + } + } } diff --git a/DotNut/NUT10/Nut10ProofSecret.cs b/DotNut/NUT10/Nut10ProofSecret.cs index 6be496e..f30ce66 100644 --- a/DotNut/NUT10/Nut10ProofSecret.cs +++ b/DotNut/NUT10/Nut10ProofSecret.cs @@ -16,6 +16,7 @@ public class Nut10ProofSecret public string[][]? Tags { get; set; } public override bool Equals(object obj) => this.Equals(obj as Nut10ProofSecret); + public bool Equals(Nut10ProofSecret s) { if (s is null) @@ -33,12 +34,17 @@ public bool Equals(Nut10ProofSecret s) return false; } - return - this.Nonce == s.Nonce && - this.Data == s.Data && - ((this.Tags == null && s.Tags == null) || - (this.Tags != null && s.Tags != null && this.Tags.Length == s.Tags.Length && - this.Tags.Zip(s.Tags).All(pair => pair.First.SequenceEqual(pair.Second)))); + return this.Nonce == s.Nonce + && this.Data == s.Data + && ( + (this.Tags == null && s.Tags == null) + || ( + this.Tags != null + && s.Tags != null + && this.Tags.Length == s.Tags.Length + && this.Tags.Zip(s.Tags).All(pair => pair.First.SequenceEqual(pair.Second)) + ) + ); } public override int GetHashCode() @@ -78,7 +84,7 @@ public override int GetHashCode() /// public bool SigAllEquals(Nut10ProofSecret other) { - return other is { } o + return other is { } o && this.Data == o.Data && ( (this.Tags == null && o.Tags == null) diff --git a/DotNut/NUT11/P2PKBuilder.cs b/DotNut/NUT11/P2PKBuilder.cs index 182824e..5198018 100644 --- a/DotNut/NUT11/P2PKBuilder.cs +++ b/DotNut/NUT11/P2PKBuilder.cs @@ -3,7 +3,7 @@ namespace DotNut; -public class P2PKBuilder +public class P2PkBuilder { public DateTimeOffset? Lock { get; set; } public ECPubKey[]? RefundPubkeys { get; set; } @@ -37,7 +37,6 @@ public P2PKProofSecret Build() { tags.Add(new[] { "refund" }.Concat(RefundPubkeys.Select(p => p.ToHex())).ToArray()); RefundSignatureThreshold ??= 1; - } if (RefundSignatureThreshold is { } refundSignatureThreshold and > 1) { @@ -58,9 +57,9 @@ public P2PKProofSecret Build() }; } - public static P2PKBuilder Load(P2PKProofSecret proofSecret) + public static P2PkBuilder Load(P2PKProofSecret proofSecret) { - var builder = new P2PKBuilder(); + var builder = new P2PkBuilder(); var primaryPubkey = proofSecret.Data.ToPubKey(); var pubkeys = proofSecret.Tags?.FirstOrDefault(strings => strings.FirstOrDefault() == "pubkeys" @@ -94,10 +93,15 @@ public static P2PKBuilder Load(P2PKProofSecret proofSecret) { builder.RefundPubkeys = refund.Skip(1).Select(s => s.ToPubKey()).ToArray(); } - - var nSigsRefund = proofSecret.Tags?.FirstOrDefault(strings => strings.FirstOrDefault() == "n_sigs_refund")? - .Skip(1)?.FirstOrDefault(); - if (!string.IsNullOrEmpty(nSigsRefund) && int.TryParse(nSigsRefund, out var nSigsRefundValue)) + + var nSigsRefund = proofSecret + .Tags?.FirstOrDefault(strings => strings.FirstOrDefault() == "n_sigs_refund") + ?.Skip(1) + ?.FirstOrDefault(); + if ( + !string.IsNullOrEmpty(nSigsRefund) + && int.TryParse(nSigsRefund, out var nSigsRefundValue) + ) { builder.RefundSignatureThreshold = nSigsRefundValue; } @@ -195,13 +199,14 @@ protected void BlindPubkeys(ECPrivKey[] rs) } } - public virtual P2PKBuilder Clone() + public virtual P2PkBuilder Clone() { - return new P2PKBuilder() + return new P2PkBuilder() { Lock = Lock, RefundPubkeys = RefundPubkeys?.ToArray(), SignatureThreshold = SignatureThreshold, + RefundSignatureThreshold = RefundSignatureThreshold, Pubkeys = Pubkeys.ToArray(), SigFlag = SigFlag, Nonce = Nonce, diff --git a/DotNut/NUT11/P2PKProofSecret.cs b/DotNut/NUT11/P2PKProofSecret.cs index 01fd7a6..466dc61 100644 --- a/DotNut/NUT11/P2PKProofSecret.cs +++ b/DotNut/NUT11/P2PKProofSecret.cs @@ -1,4 +1,4 @@ -using System.Text; +using System.Text; using System.Text.Json; using System.Text.Json.Serialization; using NBitcoin.Secp256k1; @@ -11,7 +11,7 @@ public class P2PKProofSecret : Nut10ProofSecret public const string Key = "P2PK"; [JsonIgnore] - public virtual P2PKBuilder Builder => P2PKBuilder.Load(this); + public virtual P2PkBuilder Builder => P2PkBuilder.Load(this); public virtual ECPubKey[] GetAllowedPubkeys(out int requiredSignatures) { @@ -20,6 +20,13 @@ public virtual ECPubKey[] GetAllowedPubkeys(out int requiredSignatures) return builder.Pubkeys; } + /// + /// Can have 3 types of values + /// null - no refund condition / timelock still on + /// 0 - proof is spendable without any signature + /// 1,2 ... int.MaxValue - amount of required signatures + /// + /// public virtual ECPubKey[] GetAllowedRefundPubkeys(out int? requiredSignatures) { var builder = Builder; @@ -151,41 +158,35 @@ public virtual bool VerifyWitness(Proof proof) * ========================= */ - public virtual P2PKWitness GenerateBlindWitness(Proof proof, ECPrivKey[] keys) + public virtual P2PKWitness? GenerateBlindWitness(Proof proof, ECPrivKey[] keys) { ArgumentNullException.ThrowIfNull(proof.P2PkE); - return GenerateBlindWitness(proof.Secret.GetBytes(), keys, proof.Id, proof.P2PkE); + return GenerateBlindWitness(proof.Secret.GetBytes(), keys, proof.P2PkE); } - public virtual P2PKWitness GenerateBlindWitness(Proof proof, ECPrivKey[] keys, ECPubKey P2PkE) + public virtual P2PKWitness? GenerateBlindWitness(Proof proof, ECPrivKey[] keys, ECPubKey P2PkE) { - return GenerateBlindWitness(proof.Secret.GetBytes(), keys, proof.Id, P2PkE); + return GenerateBlindWitness(proof.Secret.GetBytes(), keys, P2PkE); } - public virtual P2PKWitness GenerateBlindWitness( + public virtual P2PKWitness? GenerateBlindWitness( BlindedMessage message, ECPrivKey[] keys, ECPubKey P2PkE ) { - return GenerateBlindWitness(message.B_.Key.ToBytes(), keys, message.Id, P2PkE); + return GenerateBlindWitness(message.B_.Key.ToBytes(), keys, P2PkE); } - public virtual P2PKWitness? GenerateBlindWitness( - byte[] msg, - ECPrivKey[] keys, - KeysetId keysetId, - ECPubKey P2PkE - ) + public virtual P2PKWitness? GenerateBlindWitness(byte[] msg, ECPrivKey[] keys, ECPubKey P2PkE) { var hash = SHA256.HashData(msg); - return GenerateBlindWitness(ECPrivKey.Create(hash), keys, keysetId, P2PkE); + return GenerateBlindWitness(ECPrivKey.Create(hash), keys, P2PkE); } public virtual P2PKWitness? GenerateBlindWitness( ECPrivKey hash, ECPrivKey[] keys, - KeysetId keysetId, ECPubKey P2PkE ) { diff --git a/DotNut/NUT11/SigAllHandler.cs b/DotNut/NUT11/SigAllHandler.cs index d26437f..6d5b106 100644 --- a/DotNut/NUT11/SigAllHandler.cs +++ b/DotNut/NUT11/SigAllHandler.cs @@ -39,7 +39,7 @@ BlindedMessages is null return false; } _firstProofSecret = sec; - + var msgStr = GetMessageToSign(Proofs.ToArray(), BlindedMessages.ToArray(), MeltQuoteId); msg = Encoding.UTF8.GetBytes(msgStr); } @@ -53,7 +53,7 @@ BlindedMessages is null return false; } - P2PKWitness witnessObj; + P2PKWitness? witnessObj; if (fps is HTLCProofSecret s && HTLCPreimage is { } preimage) { if (Proofs.First().P2PkE is { } E) @@ -62,18 +62,19 @@ BlindedMessages is null msg, PrivKeys.Select(pk => (ECPrivKey)pk).ToArray(), Convert.FromHexString(preimage), - Proofs[0].Id, E ); - witness = JsonSerializer.Serialize((HTLCWitness)witnessObj); - return true; } - witnessObj = s.GenerateWitness( - msg, - PrivKeys.Select(pk => (ECPrivKey)pk).ToArray(), - Convert.FromHexString(preimage) - ); - witness = JsonSerializer.Serialize((HTLCWitness)witnessObj); + else + { + witnessObj = s.GenerateWitness( + msg, + PrivKeys.Select(pk => (ECPrivKey)pk).ToArray(), + Convert.FromHexString(preimage) + ); + } + if (witnessObj is not null) + witness = JsonSerializer.Serialize((HTLCWitness)witnessObj); return true; } @@ -82,14 +83,15 @@ BlindedMessages is null witnessObj = fps.GenerateBlindWitness( msg, PrivKeys.Select(pk => (ECPrivKey)pk).ToArray(), - Proofs[0].Id, e2 ); - witness = JsonSerializer.Serialize(witnessObj); - return true; } - witnessObj = fps.GenerateWitness(msg, PrivKeys.Select(pk => (ECPrivKey)pk).ToArray()); - witness = JsonSerializer.Serialize(witnessObj); + else + { + witnessObj = fps.GenerateWitness(msg, PrivKeys.Select(pk => (ECPrivKey)pk).ToArray()); + } + if (witnessObj is not null) + witness = JsonSerializer.Serialize(witnessObj); return true; } @@ -241,9 +243,9 @@ private static bool ValidateFirstProof(Proof firstProof, out Nut10ProofSecret? s var builder = nut10.ProofSecret switch { HTLCProofSecret htlcs => HTLCBuilder.Load(htlcs), - P2PKProofSecret p2pks => P2PKBuilder.Load(p2pks), + P2PKProofSecret p2pks => P2PkBuilder.Load(p2pks), // won't throw exception if there will be a new type of nut10 secret, but will return false - _ => new P2PKBuilder() { SigFlag = null }, + _ => new P2PkBuilder() { SigFlag = null }, }; if (builder.SigFlag != "SIG_ALL") diff --git a/DotNut/NUT14/HTLCBuilder.cs b/DotNut/NUT14/HTLCBuilder.cs index 2edc631..b976331 100644 --- a/DotNut/NUT14/HTLCBuilder.cs +++ b/DotNut/NUT14/HTLCBuilder.cs @@ -3,7 +3,7 @@ namespace DotNut; -public class HTLCBuilder : P2PKBuilder +public class HTLCBuilder : P2PkBuilder { public string HashLock { get; set; } @@ -33,17 +33,17 @@ public static HTLCBuilder Load(HTLCProofSecret proofSecret) Tags = proofSecret.Tags, }; - var innerbuilder = P2PKBuilder.Load(tempProof); - innerbuilder.Pubkeys = innerbuilder.Pubkeys.Except([_dummy.Key]).ToArray(); + var innerBuilder = P2PkBuilder.Load(tempProof); + innerBuilder.Pubkeys = innerBuilder.Pubkeys.Except([_dummy.Key]).ToArray(); return new HTLCBuilder() { HashLock = hashLock, - Lock = innerbuilder.Lock, - Pubkeys = innerbuilder.Pubkeys, - RefundPubkeys = innerbuilder.RefundPubkeys, - SignatureThreshold = innerbuilder.SignatureThreshold, - SigFlag = innerbuilder.SigFlag, - Nonce = innerbuilder.Nonce, + Lock = innerBuilder.Lock, + Pubkeys = innerBuilder.Pubkeys, + RefundPubkeys = innerBuilder.RefundPubkeys, + SignatureThreshold = innerBuilder.SignatureThreshold, + SigFlag = innerBuilder.SigFlag, + Nonce = innerBuilder.Nonce, }; } @@ -56,7 +56,7 @@ public static HTLCBuilder Load(HTLCProofSecret proofSecret) nameof(HashLock) ); } - var innerBuilder = new P2PKBuilder() + var innerBuilder = new P2PkBuilder() { Lock = Lock, Pubkeys = Pubkeys.ToArray(), @@ -76,26 +76,22 @@ public static HTLCBuilder Load(HTLCProofSecret proofSecret) }; } - public new HTLCProofSecret BuildBlinded(KeysetId keysetId, out ECPubKey p2pkE) + public new HTLCProofSecret BuildBlinded(out ECPubKey p2pkE) { var e = new PrivKey(RandomNumberGenerator.GetHexString(64)); p2pkE = e.Key.CreatePubKey(); - return BuildBlinded(keysetId, e); + return BuildBlinded(e); } - public new HTLCProofSecret BuildBlinded(KeysetId keysetId, ECPrivKey p2pke) + public new HTLCProofSecret BuildBlinded(ECPrivKey p2pke) { var pubkeys = RefundPubkeys != null ? Pubkeys.Concat(RefundPubkeys).ToArray() : Pubkeys; var rs = new List(); - var keysetIdBytes = keysetId.GetBytes(); - - var e = p2pke; - for (int i = 0; i < pubkeys.Length; i++) { - var Zx = Cashu.ComputeZx(e, pubkeys[i]); - var Ri = Cashu.ComputeRi(Zx, keysetIdBytes, i); + var Zx = Cashu.ComputeZx(p2pke, pubkeys[i]); + var Ri = Cashu.ComputeRi(Zx, i); rs.Add(Ri); } BlindPubkeys(rs.ToArray()); @@ -104,12 +100,13 @@ public static HTLCBuilder Load(HTLCProofSecret proofSecret) public override HTLCBuilder Clone() { - return new HTLCBuilder() + return new HTLCBuilder { HashLock = HashLock, Lock = Lock, RefundPubkeys = RefundPubkeys?.ToArray(), SignatureThreshold = SignatureThreshold, + RefundSignatureThreshold = RefundSignatureThreshold, Pubkeys = Pubkeys.ToArray(), SigFlag = SigFlag, Nonce = Nonce, diff --git a/DotNut/NUT14/HTLCProofSecret.cs b/DotNut/NUT14/HTLCProofSecret.cs index 34e52e2..8bcdade 100644 --- a/DotNut/NUT14/HTLCProofSecret.cs +++ b/DotNut/NUT14/HTLCProofSecret.cs @@ -15,16 +15,17 @@ public class HTLCProofSecret : P2PKProofSecret public override ECPubKey[] GetAllowedPubkeys(out int requiredSignatures) { var builder = Builder; - requiredSignatures = builder.SignatureThreshold; + // Pure hashlock (no pubkeys) - signatures are not required + requiredSignatures = builder.Pubkeys.Length == 0 ? 0 : builder.SignatureThreshold; return builder.Pubkeys; } - public HTLCWitness GenerateWitness(Proof proof, ECPrivKey[] keys, string preimage) + public HTLCWitness? GenerateWitness(Proof proof, ECPrivKey[] keys, string preimage) { return GenerateWitness(proof.Secret.GetBytes(), keys, Convert.FromHexString(preimage)); } - public HTLCWitness GenerateWitness( + public HTLCWitness? GenerateWitness( BlindedMessage blindedMessage, ECPrivKey[] keys, string preimage @@ -37,16 +38,16 @@ string preimage ); } - public HTLCWitness GenerateWitness(byte[] msg, ECPrivKey[] keys, byte[] preimage) + public HTLCWitness? GenerateWitness(byte[] msg, ECPrivKey[] keys, byte[] preimage) { var hash = SHA256.HashData(msg); return GenerateWitness(ECPrivKey.Create(hash), keys, preimage); } - public HTLCWitness GenerateWitness(ECPrivKey hash, ECPrivKey[] keys, byte[] preimage) + public HTLCWitness? GenerateWitness(ECPrivKey hash, ECPrivKey[] keys, byte[] preimage) { - // validate hash only if secret is not expired. var builder = Builder; + // validate preimage only if timelock hasn't expired if ( !builder.Lock.HasValue || builder.Lock.Value.ToUnixTimeSeconds() > DateTimeOffset.Now.ToUnixTimeSeconds() @@ -56,6 +57,8 @@ public HTLCWitness GenerateWitness(ECPrivKey hash, ECPrivKey[] keys, byte[] prei throw new InvalidOperationException("Invalid preimage"); } var witness = base.GenerateWitness(hash, keys); + if (witness is null) + return null; // freely spendable (timelock expired, no refund keys) return new HTLCWitness() { Signatures = witness.Signatures, @@ -63,13 +66,13 @@ public HTLCWitness GenerateWitness(ECPrivKey hash, ECPrivKey[] keys, byte[] prei }; } - public HTLCWitness GenerateBlindWitness(Proof proof, ECPrivKey[] keys, string preimage) + public HTLCWitness? GenerateBlindWitness(Proof proof, ECPrivKey[] keys, string preimage) { ArgumentNullException.ThrowIfNull(proof.P2PkE); return GenerateBlindWitness(proof, keys, preimage, proof.P2PkE); } - public HTLCWitness GenerateBlindWitness( + public HTLCWitness? GenerateBlindWitness( Proof proof, ECPrivKey[] keys, string preimage, @@ -80,12 +83,11 @@ ECPubKey P2PkE proof.Secret.GetBytes(), keys, Convert.FromHexString(preimage), - proof.Id, P2PkE ); } - public HTLCWitness GenerateBlindWitness( + public HTLCWitness? GenerateBlindWitness( BlindedMessage message, ECPrivKey[] keys, string preimage, @@ -96,32 +98,30 @@ ECPubKey P2PkE message.B_.Key.ToBytes(), keys, Convert.FromHexString(preimage), - message.Id, P2PkE ); } - public HTLCWitness GenerateBlindWitness( + public HTLCWitness? GenerateBlindWitness( byte[] msg, ECPrivKey[] keys, byte[] preimage, - KeysetId keysetId, ECPubKey P2PkE ) { var hash = SHA256.HashData(msg); - return GenerateBlindWitness(ECPrivKey.Create(hash), keys, preimage, keysetId, P2PkE); + return GenerateBlindWitness(ECPrivKey.Create(hash), keys, preimage, P2PkE); } - public HTLCWitness GenerateBlindWitness( + public HTLCWitness? GenerateBlindWitness( ECPrivKey hash, ECPrivKey[] keys, byte[] preimage, - KeysetId keysetId, ECPubKey P2PkE ) { var builder = Builder; + // validate preimage only if timelock hasn't expired if ( !builder.Lock.HasValue || builder.Lock.Value.ToUnixTimeSeconds() > DateTimeOffset.Now.ToUnixTimeSeconds() @@ -130,7 +130,9 @@ ECPubKey P2PkE if (!VerifyPreimage(preimage)) throw new InvalidOperationException("Invalid preimage"); } - var witness = base.GenerateBlindWitness(hash, keys, keysetId, P2PkE); + var witness = base.GenerateBlindWitness(hash, keys, P2PkE); + if (witness is null) + return null; // freely spendable (timelock expired, no refund keys) return new HTLCWitness() { Signatures = witness.Signatures, @@ -190,27 +192,23 @@ public override P2PKWitness GenerateWitness(byte[] msg, ECPrivKey[] keys) ); } - [Obsolete( - "Use GenerateBlindWitness(Proof proof, ECPrivKey[] keys, byte[] preimage, KeysetId keysetId)" - )] + [Obsolete("Use GenerateBlindWitness(Proof proof, ECPrivKey[] keys, string preimage)")] public override P2PKWitness GenerateBlindWitness(Proof proof, ECPrivKey[] keys) { - throw new InvalidOperationException(); + throw new InvalidOperationException( + "Use GenerateBlindWitness(Proof proof, ECPrivKey[] keys, string preimage)" + ); } - [Obsolete( - "Use GenerateBlindWitness(Proof proof, ECPrivKey[] keys, byte[] preimage, KeysetId keysetId, ECPubKey P2PkE)" - )] + [Obsolete("Use GenerateBlindWitness(Proof proof, ECPrivKey[] keys, string preimage, ECPubKey P2PkE)")] public override P2PKWitness GenerateBlindWitness(Proof proof, ECPrivKey[] keys, ECPubKey P2PkE) { throw new InvalidOperationException( - "Use GenerateBlindWitness(Proof proof, ECPrivKey[] keys, byte[] preimage, KeysetId keysetId, ECPubKey P2PkE)" + "Use GenerateBlindWitness(Proof proof, ECPrivKey[] keys, string preimage, ECPubKey P2PkE)" ); } - [Obsolete( - "Use GenerateBlindWitness(BlindedMessage message, ECPrivKey[] keys, byte[] preimage, KeysetId keysetId, ECPubKey P2PkE)" - )] + [Obsolete("Use GenerateBlindWitness(BlindedMessage message, ECPrivKey[] keys, string preimage, ECPubKey P2PkE)")] public override P2PKWitness GenerateBlindWitness( BlindedMessage message, ECPrivKey[] keys, @@ -218,43 +216,36 @@ ECPubKey P2PkE ) { throw new InvalidOperationException( - "Use GenerateBlindWitness(BlindedMessage message, ECPrivKey[] keys, byte[] preimage, KeysetId keysetId, ECPubKey P2PkE)" + "Use GenerateBlindWitness(BlindedMessage message, ECPrivKey[] keys, string preimage, ECPubKey P2PkE)" ); } - [Obsolete( - "Use GenerateBlindWitness(byte[] msg, ECPrivKey[] keys, byte[] preimage, KeysetId keysetId, ECPubKey P2PkE)" - )] - public override P2PKWitness GenerateBlindWitness( - byte[] msg, - ECPrivKey[] keys, - KeysetId keysetId, - ECPubKey P2PkE - ) + [Obsolete("Use GenerateBlindWitness(byte[] msg, ECPrivKey[] keys, byte[] preimage, ECPubKey P2PkE)")] + public override P2PKWitness GenerateBlindWitness(byte[] msg, ECPrivKey[] keys, ECPubKey P2PkE) { throw new InvalidOperationException( - "Use GenerateBlindWitness(byte[] msg, ECPrivKey[] keys, byte[] preimage, KeysetId keysetId, ECPubKey P2PkE)" + "Use GenerateBlindWitness(byte[] msg, ECPrivKey[] keys, byte[] preimage, ECPubKey P2PkE)" ); } - [Obsolete( - "Use GenerateBlindWitness(ECPrivKey hash, ECPrivKey[] keys, byte[] preimage, KeysetId keysetId, ECPubKey P2PkE)" - )] + [Obsolete("Use GenerateBlindWitness(ECPrivKey hash, ECPrivKey[] keys, byte[] preimage, ECPubKey P2PkE)")] public override P2PKWitness GenerateBlindWitness( ECPrivKey hash, ECPrivKey[] keys, - KeysetId keysetId, ECPubKey P2PkE ) { throw new InvalidOperationException( - "Use GenerateBlindWitness(ECPrivKey hash, ECPrivKey[] keys, byte[] preimage, KeysetId keysetId, ECPubKey P2PkE)" + "Use GenerateBlindWitness(ECPrivKey hash, ECPrivKey[] keys, byte[] preimage, ECPubKey P2PkE)" ); } + [Obsolete("Use GenerateWitness(ECPrivKey hash, ECPrivKey[] keys, byte[] preimage)")] public override P2PKWitness GenerateWitness(ECPrivKey hash, ECPrivKey[] keys) { - return base.GenerateWitness(hash, keys); + throw new InvalidOperationException( + "Use GenerateWitness(ECPrivKey hash, ECPrivKey[] keys, byte[] preimage)" + ); } public override bool VerifyWitness(string message, P2PKWitness witness) diff --git a/DotNut/NUT18/PaymentRequest.cs b/DotNut/NUT18/PaymentRequest.cs index ca22387..1df8094 100644 --- a/DotNut/NUT18/PaymentRequest.cs +++ b/DotNut/NUT18/PaymentRequest.cs @@ -12,7 +12,6 @@ public class PaymentRequest public string? Memo { get; set; } public PaymentRequestTransport[] Transports { get; set; } public Nut10LockingCondition? Nut10 { get; set; } - public bool? Nut26 { get; set; } public override string ToString() { diff --git a/DotNut/NUT18/PaymentRequestTransportTag.cs b/DotNut/NUT18/PaymentRequestTransportTag.cs deleted file mode 100644 index 7bbab00..0000000 --- a/DotNut/NUT18/PaymentRequestTransportTag.cs +++ /dev/null @@ -1,7 +0,0 @@ -namespace DotNut; - -public class PaymentRequestTransportTag -{ - public string Key { get; set; } - public string Value { get; set; } -} diff --git a/DotNut/Tag.cs b/DotNut/NUT18/Tag.cs similarity index 91% rename from DotNut/Tag.cs rename to DotNut/NUT18/Tag.cs index 3a86dbd..c182fb7 100644 --- a/DotNut/Tag.cs +++ b/DotNut/NUT18/Tag.cs @@ -4,7 +4,7 @@ public class Tag { public string Key { get; set; } public List Value { get; set; } - + public Tag(string[] tag) { if (tag == null || tag.Length == 0) @@ -17,6 +17,6 @@ public Tag(string[] tag) public string[] ToArray() { - return [Key, ..Value]; + return [Key, .. Value]; } -} \ No newline at end of file +} diff --git a/DotNut/Nut10LockingCondition.cs b/DotNut/Nut10LockingCondition.cs deleted file mode 100644 index ba89386..0000000 --- a/DotNut/Nut10LockingCondition.cs +++ /dev/null @@ -1,8 +0,0 @@ -namespace DotNut; - -public class Nut10LockingCondition -{ - public string Kind { get; set; } - public string Data { get; set; } - public Tag[]? Tags { get; set; } -} \ No newline at end of file diff --git a/DotNut/SigAllHandler.cs b/DotNut/SigAllHandler.cs deleted file mode 100644 index 1953cf9..0000000 --- a/DotNut/SigAllHandler.cs +++ /dev/null @@ -1,222 +0,0 @@ -using System.Text; -using System.Text.Json; -using NBitcoin.Secp256k1; - -namespace DotNut; - -//Handles both P2PK and HTLC (if preimage added) -public class SigAllHandler -{ - public List Proofs { get; set; } - public List PrivKeys { get; set; } - public List BlindedMessages { get; set; } - public string? HTLCPreimage { get; set; } - public string? MeltQuoteId { get; set; } - - private Nut10ProofSecret? _firstProofSecret; - - - public bool TrySign(out string? witness) - { - witness = null; - - if ( BlindedMessages is null || Proofs is null || PrivKeys is null || - BlindedMessages.Count == 0 || Proofs.Count == 0 || PrivKeys.Count == 0) - { - return false; - } - - byte[] msg; - try - { - var msgStr = GetMessageToSign(Proofs.ToArray(), BlindedMessages.ToArray(), MeltQuoteId); - if (!ValidateFirstProof(Proofs[0], out var sec) || sec is null) - { - return false; - } - _firstProofSecret = sec; - msg = Encoding.UTF8.GetBytes(msgStr); - } - catch (ArgumentException) - { - return false; - } - - if (_firstProofSecret is not P2PKProofSecret fps) - { - return false; - } - - P2PKWitness witnessObj; - if (fps is HTLCProofSecret s && HTLCPreimage is {} preimage) - { - if (Proofs.First().P2PkE is { } E) - { - witnessObj = s.GenerateBlindWitness(msg, - PrivKeys.Select(pk => (ECPrivKey)pk).ToArray(), - Convert.FromHexString(preimage), - Proofs[0].Id, - E - ); - witness = JsonSerializer.Serialize((HTLCWitness)witnessObj); - return true; - } - witnessObj = - s.GenerateWitness(msg, PrivKeys.Select(pk => (ECPrivKey)pk).ToArray(), - Convert.FromHexString(preimage) - ); - witness = JsonSerializer.Serialize((HTLCWitness)witnessObj); - return true; - } - - - if (Proofs.First().P2PkE is { } e2) - { - witnessObj = fps.GenerateBlindWitness(msg, PrivKeys.Select(pk => (ECPrivKey)pk).ToArray(), Proofs[0].Id, e2); - witness = JsonSerializer.Serialize(witnessObj); - return true; - } - witnessObj = fps.GenerateWitness(msg, PrivKeys.Select(pk => (ECPrivKey)pk).ToArray()); - witness = JsonSerializer.Serialize(witnessObj); - return true; - } - - public static string GetMessageToSign(Proof[] inputs, BlindedMessage[] outputs, string? meltQuoteId = null) - { - if (inputs is null || inputs.Length == 0) - { - throw new ArgumentException("At least one proof is required for SIG_ALL.", nameof(inputs)); - } - if (outputs is null || outputs.Length == 0) - { - throw new ArgumentException("At least one blinded output is required for SIG_ALL.", nameof(outputs)); - } - if (!ValidateFirstProof(inputs[0], out var firstSecret)) - { - throw new ArgumentException("Provided first proof is invalid"); - } - var msg = new StringBuilder(); - - for (var i = 0; i < inputs.Length; i++) - { - var p = inputs[i]; - - if (p.Secret is not Nut10Secret nut10) - { - throw new ArgumentException("When signing sig_all, every proof must be a nut 10 secret."); - } - - if (firstSecret != nut10.ProofSecret) - { - throw new ArgumentException("When signing sig_all, every proof must have identical tags and data."); - } - // serialize as raw object - var secret = JsonSerializer.Serialize((object)p.Secret); - msg.Append(secret); - msg.Append(p.C); - } - - foreach (var b in outputs) - { - msg.Append(b.Amount); - msg.Append(b.B_); - } - - if (meltQuoteId is not null) - { - msg.Append(meltQuoteId); - } - return msg.ToString(); - } - - public static bool VerifySigAllWitness( - Proof[] proofs, - BlindedMessage[] blindedMessages, - P2PKWitness witness, - string? meltQuoteId = null) - { - if (proofs is null || proofs.Length == 0) - { - return false; - } - byte[] msg; - try - { - var msgStr = meltQuoteId is null - ? GetMessageToSign(proofs, blindedMessages) - : GetMessageToSign(proofs, blindedMessages, meltQuoteId); - - msg = Encoding.UTF8.GetBytes(msgStr); - } - catch(Exception ex) - { - return false; - } - - if (proofs[0].Secret is not Nut10Secret nut10) - return false; - - return nut10.ProofSecret switch - { - HTLCProofSecret htlcs => htlcs.VerifyWitness(msg, witness), - P2PKProofSecret p2pks => p2pks.VerifyWitness(msg, witness), - _ => false - }; - } - - public static bool VerifySigAllWitness(Proof[] proofs, BlindedMessage[] blindedMessages, string? meltQuoteId = null) - { - if (proofs is null || proofs.Length == 0) - { - return false; - } - var firstProof = proofs.FirstOrDefault(); - if (firstProof?.Secret is not Nut10Secret { ProofSecret: var proofSecret } || firstProof.Witness is null) - return false; - - P2PKWitness? witness; - try - { - var htlcWitness = JsonSerializer.Deserialize(firstProof.Witness); - if (htlcWitness?.Preimage is not null) - { - witness = htlcWitness; - } - else - { - witness = JsonSerializer.Deserialize(firstProof.Witness); - } - } - catch - { - return false; - } - return witness is not null && VerifySigAllWitness(proofs, blindedMessages, witness, meltQuoteId); - } - - private static bool ValidateFirstProof(Proof firstProof, out Nut10ProofSecret? secret) - { - secret = null; - - if (firstProof.Secret is not Nut10Secret nut10) - { - return false; - } - - var builder = nut10.ProofSecret switch - { - HTLCProofSecret htlcs => HTLCBuilder.Load(htlcs), - P2PKProofSecret p2pks => P2PkBuilder.Load(p2pks), - // won't throw exception if there will be a new type of nut10 secret, but will return false - _ => new P2PkBuilder(){SigFlag = null} - }; - - if (builder.SigFlag != "SIG_ALL") - { - return false; - } - - secret = nut10.ProofSecret; - return true; - } -} \ No newline at end of file From 37267a98ad5e776c6251552d95ac9bfbae0290d4 Mon Sep 17 00:00:00 2001 From: d4rp4t Date: Tue, 10 Mar 2026 21:00:17 +0100 Subject: [PATCH 66/70] fix race condition in tests --- DotNut.Tests/Integration.cs | 39 +++++++++--------------------------- DotNut/Abstractions/Utils.cs | 2 -- 2 files changed, 10 insertions(+), 31 deletions(-) diff --git a/DotNut.Tests/Integration.cs b/DotNut.Tests/Integration.cs index ed19123..bf3f827 100644 --- a/DotNut.Tests/Integration.cs +++ b/DotNut.Tests/Integration.cs @@ -2,6 +2,7 @@ using DotNut.Abstractions; using DotNut.Abstractions.Websockets; using DotNut.Api; +using DotNut.ApiModels; namespace DotNut.Tests; @@ -322,25 +323,10 @@ public async Task SubscribeToMintMeltQuoteUpdates() var sub = await service.SubscribeToMintQuoteAsync(MintUrl, new[] { quote.Quote }); - //todo imo this test should be rebuilt. this is a race condition, and it's possible that quote will be marked as paid - // b4 we finish subscribing it. it should be marked as paid - int connectedCount = 0; - int notificationCount = 0; - using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(240)); - var connectedTcs = new TaskCompletionSource(); - var paidTcs = new TaskCompletionSource(); - - _ = Task.Run( - async () => - { - await connectedTcs.Task.WaitAsync(TimeSpan.FromSeconds(10)); - await Task.Delay(1000, cts.Token); // idk about this line. why did I wrote this? - await PayInvoice(); - }, - cts.Token - ); + int connectedCount = 0; + bool gotPaid = false; await foreach (var msg in sub.NotificationChannel.Reader.ReadAllAsync(cts.Token)) { @@ -348,32 +334,27 @@ public async Task SubscribeToMintMeltQuoteUpdates() { case WsMessage.Response: connectedCount++; - connectedTcs.TrySetResult(); break; case WsMessage.Notification notification: - notificationCount++; - - if (notificationCount >= 2) - paidTcs.TrySetResult(); - + var parsed = NotificationParser.ParsePayload( + notification.Value + ); + if (parsed?.State == "PAID") + gotPaid = true; break; case WsMessage.Error error: Assert.Fail($"WebSocket error: {error}"); break; - - default: - Assert.Fail($"Unexpected message type: {msg.GetType().Name}"); - break; } - if (paidTcs.Task.IsCompleted) + if (gotPaid) break; } Assert.Equal(1, connectedCount); - Assert.True(notificationCount >= 2, $"Expected >=2 notifications, got {notificationCount}"); + Assert.True(gotPaid, "Expected to receive PAID notification"); var proofs = await mintHandler.Mint(); Assert.NotEmpty(proofs); diff --git a/DotNut/Abstractions/Utils.cs b/DotNut/Abstractions/Utils.cs index 4eb7424..9349cbb 100644 --- a/DotNut/Abstractions/Utils.cs +++ b/DotNut/Abstractions/Utils.cs @@ -192,7 +192,6 @@ public static OutputData CreateNut10BlindedOutput( P2PkBuilder builder ) { - // ugliest hack ever Nut10Secret secret; PubKey E; if (builder is HTLCBuilder htlc) @@ -236,7 +235,6 @@ public static OutputData CreateNut10BlindedOutput( PrivKey e ) { - // ugliest hack ever Nut10Secret secret; if (builder is HTLCBuilder htlc) { From cb49112aa625243a3762a83b5d10b8bf65246b83 Mon Sep 17 00:00:00 2001 From: d4rp4t Date: Tue, 10 Mar 2026 22:03:53 +0100 Subject: [PATCH 67/70] fix ws enums --- .../Abstractions/Websockets/WebsocketEnums.cs | 17 ----------------- .../Abstractions/Websockets/WebsocketService.cs | 4 +++- 2 files changed, 3 insertions(+), 18 deletions(-) diff --git a/DotNut/Abstractions/Websockets/WebsocketEnums.cs b/DotNut/Abstractions/Websockets/WebsocketEnums.cs index bc88448..cd34e03 100644 --- a/DotNut/Abstractions/Websockets/WebsocketEnums.cs +++ b/DotNut/Abstractions/Websockets/WebsocketEnums.cs @@ -1,33 +1,16 @@ -using System.Runtime.Serialization; -using System.Text.Json.Serialization; - namespace DotNut.Abstractions.Websockets; -[JsonConverter(typeof(JsonStringEnumConverter))] public enum SubscriptionKind { - [EnumMember(Value = "bolt11_melt_quote")] Bolt11MeltQuote, - - [EnumMember(Value = "bolt11_mint_quote")] Bolt11MintQuote, - - [EnumMember(Value = "bolt12_melt_quote")] Bolt12MeltQuote, - - [EnumMember(Value = "bolt12_mint_quote")] Bolt12MintQuote, - - [EnumMember(Value = "proof_state")] ProofState, } -[JsonConverter(typeof(JsonStringEnumConverter))] public enum WsRequestMethod { - [EnumMember(Value = "subscribe")] Subscribe, - - [EnumMember(Value = "unsubscribe")] Unsubscribe, } diff --git a/DotNut/Abstractions/Websockets/WebsocketService.cs b/DotNut/Abstractions/Websockets/WebsocketService.cs index 2d41df7..3656050 100644 --- a/DotNut/Abstractions/Websockets/WebsocketService.cs +++ b/DotNut/Abstractions/Websockets/WebsocketService.cs @@ -26,6 +26,7 @@ public class WebsocketService : IWebsocketService private static readonly JsonSerializerOptions JsonOptions = new() { DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, + Converters = { new JsonStringEnumConverter(JsonNamingPolicy.SnakeCaseLower) }, }; public event EventHandler? ConnectionStateChanged; @@ -335,7 +336,6 @@ public async ValueTask DisposeAsync() return; _disposed = true; - _disposeCts.Cancel(); var mintUrls = _connections.Keys.ToList(); foreach (var mintUrl in mintUrls) @@ -350,6 +350,8 @@ public async ValueTask DisposeAsync() } } + _disposeCts.Cancel(); + _subscriptions.Clear(); _connections.Clear(); _pendingRequests.Clear(); From 23aa085afa05a91c4e4322c078753839831b756a Mon Sep 17 00:00:00 2001 From: d4rp4t Date: Tue, 10 Mar 2026 22:05:27 +0100 Subject: [PATCH 68/70] close sub in integration tests --- DotNut.Tests/Integration.cs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/DotNut.Tests/Integration.cs b/DotNut.Tests/Integration.cs index bf3f827..b9ed52f 100644 --- a/DotNut.Tests/Integration.cs +++ b/DotNut.Tests/Integration.cs @@ -341,7 +341,11 @@ public async Task SubscribeToMintMeltQuoteUpdates() notification.Value ); if (parsed?.State == "PAID") + { gotPaid = true; + await sub.CloseAsync(); + } + break; case WsMessage.Error error: @@ -356,7 +360,7 @@ public async Task SubscribeToMintMeltQuoteUpdates() Assert.Equal(1, connectedCount); Assert.True(gotPaid, "Expected to receive PAID notification"); - var proofs = await mintHandler.Mint(); + var proofs = await mintHandler.Mint(cts.Token); Assert.NotEmpty(proofs); } From ced4d9458c1eb2d7ecd9f740dc38772fcdc3feff Mon Sep 17 00:00:00 2001 From: d4rp4t Date: Tue, 10 Mar 2026 22:10:50 +0100 Subject: [PATCH 69/70] bump version to 2.0.0 --- DotNut/DotNut.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/DotNut/DotNut.csproj b/DotNut/DotNut.csproj index 5c40026..1eec5fc 100644 --- a/DotNut/DotNut.csproj +++ b/DotNut/DotNut.csproj @@ -9,7 +9,7 @@ A full C# native implementation of the Cashu protocol MIT https://github.com/Kukks/DotNut - 1.0.6 + 2.0.0 https://github.com/Kukks/DotNut git bitcoin cashu ecash secp256k1 From 2c9117afc320b7cc19dea56064c3d21c1adee9ac Mon Sep 17 00:00:00 2001 From: d4rp4t Date: Mon, 9 Mar 2026 09:48:39 +0100 Subject: [PATCH 70/70] add blank outputs getter --- DotNut/Abstractions/Handlers/MeltHandlerBolt11.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/DotNut/Abstractions/Handlers/MeltHandlerBolt11.cs b/DotNut/Abstractions/Handlers/MeltHandlerBolt11.cs index f58442a..4570f4f 100644 --- a/DotNut/Abstractions/Handlers/MeltHandlerBolt11.cs +++ b/DotNut/Abstractions/Handlers/MeltHandlerBolt11.cs @@ -11,6 +11,7 @@ public class MeltHandlerBolt11( ) : IMeltHandler> { public PostMeltQuoteBolt11Response GetQuote() => quote; + public List GetBlankOutputs() => blankOutputs; public async Task> Melt(IEnumerable inputs, CancellationToken ct = default) {