├── DotNut.Tests ├── GlobalUsings.cs └── DotNut.Tests.csproj ├── .gitignore ├── .github ├── FUNDING.yml └── workflows │ └── ci.yml ├── DotNut ├── NBitcoin │ ├── BIP39 │ │ ├── IWordlistSource.cs │ │ ├── WordCount.cs │ │ ├── Language.cs │ │ ├── Mnemonic.cs │ │ ├── Wordlist.cs │ │ └── KDTable.cs │ ├── LICENSE │ └── BitWriter.cs ├── DLEQProof.cs ├── PaymentRequestTransportTag.cs ├── Encoding │ ├── ICashuTokenEncoder.cs │ ├── ConvertUtils.cs │ ├── CashuTokenV3Encoder.cs │ ├── Base64UrlSafe.cs │ ├── CashuTokenHelper.cs │ └── CashuTokenV4Encoder.cs ├── HTLCWitness.cs ├── P2PKWitness.cs ├── DLEQ.cs ├── ApiModels │ ├── PostCheckStateRequest.cs │ ├── PostSwapResponse.cs │ ├── PostRestoreRequest.cs │ ├── Mint │ │ ├── PostMintResponse.cs │ │ ├── PostMintRequest.cs │ │ ├── bolt11 │ │ │ ├── PostMintQuoteBolt11Request.cs │ │ │ └── PostMintQuoteBolt11Response.cs │ │ └── bolt12 │ │ │ ├── PostMintQuoteBolt12Request.cs │ │ │ └── PostMintQuoteBolt12Response.cs │ ├── PostCheckStateResponse.cs │ ├── ContactInfo.cs │ ├── PostSwapRequest.cs │ ├── PostRestoreResponse.cs │ ├── Melt │ │ ├── bolt11 │ │ │ ├── PostMeltQuoteBolt11Request.cs │ │ │ └── PostMeltQuoteBolt11Response.cs │ │ ├── bolt12 │ │ │ ├── PostMeltQuoteBolt12Request.cs │ │ │ └── PostMeltQuoteBolt12Response.cs │ │ └── PostMeltRequest.cs │ ├── StateResponseItem.cs │ ├── GetKeysResponse.cs │ ├── GetKeysetsResponse.cs │ └── GetInfoResponse.cs ├── PaymentRequestTransport.cs ├── Api │ ├── CashuProtocolError.cs │ ├── CashuProtocolException.cs │ ├── ICashuApi.cs │ └── CashuHttpClient.cs ├── PaymentRequestInterfaceHandler.cs ├── ISecret.cs ├── MultipathPaymentSetting.cs ├── MeltMethodSetting.cs ├── BlindedMessage.cs ├── PaymentRequestPayload.cs ├── StringSecret.cs ├── MintMethodSetting.cs ├── Nut10ProofSecret.cs ├── FeeHelper.cs ├── BlindSignature.cs ├── PaymentRequestTransportInitiator.cs ├── CashuToken.cs ├── JsonConverters │ ├── UnixDateTimeOffsetConverter.cs │ ├── PrivKeyJsonConverter.cs │ ├── PubKeyJsonConverter.cs │ ├── KeysetIdJsonConverter.cs │ ├── SecretJsonConverter.cs │ ├── Nut10SecretJsonConverter.cs │ └── KeysetJsonConverter.cs ├── PrivKey.cs ├── HttpPaymentRequestInterfaceHandler.cs ├── Proof.cs ├── Nut10Secret.cs ├── PaymentRequest.cs ├── DotNut.csproj ├── PubKey.cs ├── NUT13 │ ├── BIP32.cs │ └── Nut13.cs ├── KeysetId.cs ├── HTLCBuilder.cs ├── Keyset.cs ├── PaymentRequestEncoder.cs ├── P2PkBuilder.cs ├── Cashu.cs ├── HTLCProofSecret.cs ├── P2PKProofSecret.cs └── ProofSelector.cs ├── DotNut.Demo ├── DotNut.Demo.csproj └── README.md ├── LICENSE ├── DotNut.Nostr ├── DotNut.Nostr.csproj └── NostrNip17PaymentRequestInterfaceHandler.cs ├── DotNuts.sln.DotSettings.user ├── DotNut.sln ├── DotNut.sln.DotSettings.user └── README.md /DotNut.Tests/GlobalUsings.cs: -------------------------------------------------------------------------------- 1 | global using Xunit; -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | **/bin 2 | **/obj 3 | .idea 4 | .idea 5 | .vs -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: [kukks] 2 | custom: ['https://donate.kukks.org'] -------------------------------------------------------------------------------- /DotNut/NBitcoin/BIP39/IWordlistSource.cs: -------------------------------------------------------------------------------- 1 | namespace DotNut.NBitcoin.BIP39 2 | { 3 | public interface IWordlistSource 4 | { 5 | Task? Load(string name); 6 | } 7 | } -------------------------------------------------------------------------------- /DotNut/DLEQProof.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json.Serialization; 2 | 3 | namespace DotNut; 4 | 5 | public class DLEQProof: DLEQ 6 | { 7 | [JsonPropertyName("r")] public PrivKey R { get; set; } 8 | } -------------------------------------------------------------------------------- /DotNut/PaymentRequestTransportTag.cs: -------------------------------------------------------------------------------- 1 | namespace DotNut; 2 | 3 | public class PaymentRequestTransportTag 4 | { 5 | public string Key { get; set; } 6 | public string Value { get; set; } 7 | } -------------------------------------------------------------------------------- /DotNut/Encoding/ICashuTokenEncoder.cs: -------------------------------------------------------------------------------- 1 | namespace DotNut; 2 | 3 | public interface ICashuTokenEncoder 4 | { 5 | string Encode(CashuToken token); 6 | CashuToken Decode(string token); 7 | 8 | } -------------------------------------------------------------------------------- /DotNut/HTLCWitness.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json.Serialization; 2 | 3 | namespace DotNut; 4 | 5 | public class HTLCWitness: P2PKWitness 6 | { 7 | [JsonPropertyName("preimage")] public string Preimage { get; set; } 8 | } -------------------------------------------------------------------------------- /DotNut/P2PKWitness.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json.Serialization; 2 | 3 | namespace DotNut; 4 | 5 | public class P2PKWitness 6 | { 7 | [JsonPropertyName("signatures")] public string[] Signatures { get; set; } = Array.Empty(); 8 | } -------------------------------------------------------------------------------- /DotNut/DLEQ.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json.Serialization; 2 | 3 | namespace DotNut; 4 | 5 | public class DLEQ 6 | { 7 | [JsonPropertyName("e")] public PrivKey E { get; set; } 8 | [JsonPropertyName("s")] public PrivKey S { get; set; } 9 | } -------------------------------------------------------------------------------- /DotNut/NBitcoin/BIP39/WordCount.cs: -------------------------------------------------------------------------------- 1 | namespace DotNut.NBitcoin.BIP39; 2 | 3 | public enum WordCount : int 4 | { 5 | Twelve = 12, 6 | Fifteen = 15, 7 | Eighteen = 18, 8 | TwentyOne = 21, 9 | TwentyFour = 24 10 | } -------------------------------------------------------------------------------- /DotNut/ApiModels/PostCheckStateRequest.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json.Serialization; 2 | 3 | namespace DotNut.ApiModels; 4 | 5 | public class PostCheckStateRequest 6 | { 7 | [JsonPropertyName("Ys")] 8 | public string[] Ys { get; set; } 9 | } -------------------------------------------------------------------------------- /DotNut/ApiModels/PostSwapResponse.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json.Serialization; 2 | 3 | namespace DotNut.ApiModels; 4 | 5 | public class PostSwapResponse 6 | { 7 | [JsonPropertyName("signatures")] public BlindSignature[] Signatures { get; set; } 8 | } -------------------------------------------------------------------------------- /DotNut/ApiModels/PostRestoreRequest.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json.Serialization; 2 | 3 | namespace DotNut.ApiModels; 4 | 5 | public class PostRestoreRequest 6 | { 7 | [JsonPropertyName("outputs")] 8 | public BlindedMessage[] Outputs { get; set; } 9 | } -------------------------------------------------------------------------------- /DotNut/PaymentRequestTransport.cs: -------------------------------------------------------------------------------- 1 | namespace DotNut; 2 | 3 | public class PaymentRequestTransport 4 | { 5 | public string Type { get; set; } 6 | public string Target { get; set; } 7 | public PaymentRequestTransportTag[] Tags { get; set; } 8 | } -------------------------------------------------------------------------------- /DotNut/ApiModels/Mint/PostMintResponse.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json.Serialization; 2 | 3 | namespace DotNut.ApiModels; 4 | 5 | public class PostMintResponse 6 | { 7 | [JsonPropertyName("signatures")] 8 | public BlindSignature[] Signatures { get; set; } 9 | } -------------------------------------------------------------------------------- /DotNut/ApiModels/PostCheckStateResponse.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json.Serialization; 2 | 3 | namespace DotNut.ApiModels; 4 | 5 | public class PostCheckStateResponse 6 | { 7 | 8 | [JsonPropertyName("states")] 9 | public StateResponseItem[] States { get; set; } 10 | } -------------------------------------------------------------------------------- /DotNut/Api/CashuProtocolError.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json.Serialization; 2 | 3 | namespace DotNut.Api; 4 | 5 | public class CashuProtocolError 6 | { 7 | [JsonPropertyName("detail")] public string Detail { get; set; } 8 | [JsonPropertyName("code")] public int Code { get; set; } 9 | } -------------------------------------------------------------------------------- /DotNut/ApiModels/ContactInfo.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json.Serialization; 2 | 3 | namespace DotNut.ApiModels; 4 | 5 | public class ContactInfo 6 | { 7 | [JsonPropertyName("method")] public string Method { get; set; } 8 | [JsonPropertyName("info")] public string Info { get; set; } 9 | } -------------------------------------------------------------------------------- /DotNut/NBitcoin/BIP39/Language.cs: -------------------------------------------------------------------------------- 1 | namespace DotNut.NBitcoin.BIP39 2 | { 3 | public enum Language 4 | { 5 | English, 6 | Japanese, 7 | Spanish, 8 | ChineseSimplified, 9 | ChineseTraditional, 10 | French, 11 | PortugueseBrazil, 12 | Czech, 13 | Unknown 14 | }; 15 | } 16 | -------------------------------------------------------------------------------- /DotNut/PaymentRequestInterfaceHandler.cs: -------------------------------------------------------------------------------- 1 | namespace DotNut; 2 | 3 | public interface PaymentRequestInterfaceHandler 4 | { 5 | bool CanHandle(PaymentRequest request); 6 | Task SendPayment(PaymentRequest request, PaymentRequestPayload payload, CancellationToken cancellationToken = default); 7 | } -------------------------------------------------------------------------------- /DotNut/ISecret.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json.Serialization; 2 | using DotNut.JsonConverters; 3 | using NBitcoin.Secp256k1; 4 | 5 | namespace DotNut; 6 | 7 | [JsonConverter(typeof(SecretJsonConverter))] 8 | public interface ISecret 9 | { 10 | byte[] GetBytes(); 11 | ECPubKey ToCurve(); 12 | } -------------------------------------------------------------------------------- /DotNut/ApiModels/PostSwapRequest.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json.Serialization; 2 | 3 | namespace DotNut.ApiModels; 4 | 5 | public class PostSwapRequest 6 | { 7 | [JsonPropertyName("inputs")] public Proof[] Inputs { get; set; } 8 | [JsonPropertyName("outputs")] public BlindedMessage[] Outputs { get; set; } 9 | } -------------------------------------------------------------------------------- /DotNut/Api/CashuProtocolException.cs: -------------------------------------------------------------------------------- 1 | namespace DotNut.Api; 2 | 3 | public class CashuProtocolException : Exception 4 | { 5 | public CashuProtocolException(CashuProtocolError error) : base(error.Detail) 6 | { 7 | Error = error; 8 | } 9 | 10 | public CashuProtocolError Error { get; } 11 | } -------------------------------------------------------------------------------- /DotNut/ApiModels/Mint/PostMintRequest.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json.Serialization; 2 | 3 | namespace DotNut.ApiModels; 4 | 5 | public class PostMintRequest 6 | { 7 | [JsonPropertyName("quote")] 8 | public string Quote { get; set; } 9 | 10 | [JsonPropertyName("outputs")] 11 | public BlindedMessage[] Outputs { get; set; } 12 | } -------------------------------------------------------------------------------- /DotNut/ApiModels/PostRestoreResponse.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json.Serialization; 2 | 3 | namespace DotNut.ApiModels; 4 | 5 | public class PostRestoreResponse 6 | { 7 | [JsonPropertyName("outputs")] 8 | public BlindedMessage[] Outputs { get; set; } 9 | [JsonPropertyName("signatures")] 10 | public BlindSignature[] Signatures { get; set; } 11 | } -------------------------------------------------------------------------------- /DotNut/ApiModels/Melt/bolt11/PostMeltQuoteBolt11Request.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json.Serialization; 2 | 3 | namespace DotNut.ApiModels; 4 | 5 | public class PostMeltQuoteBolt11Request 6 | { 7 | 8 | [JsonPropertyName("request")] 9 | public string Request { get; set; } 10 | 11 | [JsonPropertyName("unit")] 12 | public string Unit { get; set; } 13 | } -------------------------------------------------------------------------------- /DotNut/MultipathPaymentSetting.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json.Serialization; 2 | 3 | namespace DotNut; 4 | 5 | public class MultipathPaymentSetting 6 | { 7 | [JsonPropertyName("method")] public string Method { get; set; } 8 | [JsonPropertyName("unit")] public List Unit { get; set; } 9 | [JsonPropertyName("mpp")] public bool MultiPathPayments { get; set; } 10 | } -------------------------------------------------------------------------------- /DotNut.Demo/DotNut.Demo.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Exe 5 | net8.0 6 | enable 7 | enable 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /DotNut/MeltMethodSetting.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json.Serialization; 2 | 3 | namespace DotNut; 4 | 5 | public class MeltMethodSetting 6 | { 7 | [JsonPropertyName("method")] public string Method { get; set; } 8 | [JsonPropertyName("unit")] public string Unit { get; set; } 9 | [JsonPropertyName("min_amount")] public ulong? Min { get; set; } 10 | [JsonPropertyName("max_amount")] public ulong? Max { get; set; } 11 | } -------------------------------------------------------------------------------- /DotNut/Encoding/ConvertUtils.cs: -------------------------------------------------------------------------------- 1 | using NBitcoin.Secp256k1; 2 | 3 | namespace DotNut; 4 | 5 | public static class ConvertUtils 6 | { 7 | public static ECPubKey ToPubKey(this string hex) 8 | { 9 | return ECPubKey.Create(global::System.Convert.FromHexString(hex)); 10 | } 11 | 12 | public static ECPrivKey ToPrivKey(this string hex) 13 | { 14 | return ECPrivKey.Create(global::System.Convert.FromHexString(hex)); 15 | } 16 | } -------------------------------------------------------------------------------- /DotNut/ApiModels/Melt/bolt12/PostMeltQuoteBolt12Request.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json; 2 | using System.Text.Json.Serialization; 3 | 4 | namespace DotNut.ApiModels.Melt.bolt12; 5 | 6 | public class PostMeltQuoteBolt12Request 7 | { 8 | [JsonPropertyName("request")] 9 | public string Request { get; set; } 10 | 11 | [JsonPropertyName("unit")] 12 | public string Unit { get; set; } 13 | 14 | public JsonDocument? Options { get; set; } 15 | } 16 | 17 | -------------------------------------------------------------------------------- /DotNut/BlindedMessage.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json.Serialization; 2 | 3 | namespace DotNut; 4 | 5 | public class BlindedMessage 6 | { 7 | [JsonPropertyName("amount")] public ulong Amount { get; set; } 8 | [JsonPropertyName("id")] public KeysetId Id { get; set; } 9 | [JsonPropertyName("B_")] public PubKey B_ { get; set; } 10 | [JsonPropertyName("witness")][JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public string? Witness { get; set; } 11 | } -------------------------------------------------------------------------------- /DotNut/ApiModels/StateResponseItem.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json.Serialization; 2 | 3 | namespace DotNut.ApiModels; 4 | 5 | public class StateResponseItem 6 | { 7 | 8 | public string Y { get; set; } 9 | [JsonConverter(typeof(JsonStringEnumConverter))] 10 | public TokenState State { get; set; } 11 | public string? Witness { get; set; } 12 | 13 | public enum TokenState 14 | { 15 | UNSPENT, 16 | PENDING, 17 | SPENT 18 | } 19 | } -------------------------------------------------------------------------------- /DotNut/PaymentRequestPayload.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json.Serialization; 2 | 3 | namespace DotNut; 4 | 5 | public class PaymentRequestPayload 6 | { 7 | [JsonPropertyName("id")] public string PaymentId { get; set; } 8 | [JsonPropertyName("memo")] public string? Memo { get; set; } 9 | [JsonPropertyName("mint")] public string Mint { get; set; } 10 | [JsonPropertyName("unit")] public string Unit { get; set; } 11 | 12 | [JsonPropertyName("proofs")] public Proof[] Proofs { get; set; } 13 | } -------------------------------------------------------------------------------- /DotNut/StringSecret.cs: -------------------------------------------------------------------------------- 1 | using NBitcoin.Secp256k1; 2 | 3 | namespace DotNut; 4 | 5 | public class StringSecret : ISecret 6 | { 7 | public StringSecret(string secret) 8 | { 9 | Secret = secret; 10 | } 11 | 12 | public string Secret { get; init; } 13 | public byte[] GetBytes() 14 | { 15 | return System.Text.Encoding.UTF8.GetBytes(Secret); 16 | } 17 | 18 | public ECPubKey ToCurve() 19 | { 20 | return Cashu.HashToCurve(GetBytes()); 21 | } 22 | } -------------------------------------------------------------------------------- /DotNut/ApiModels/Melt/PostMeltRequest.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json.Serialization; 2 | 3 | namespace DotNut.ApiModels; 4 | 5 | public class PostMeltRequest 6 | { 7 | 8 | [JsonPropertyName("quote")] 9 | public string Quote { get; set; } 10 | 11 | [JsonPropertyName("inputs")] 12 | public Proof[] Inputs { get; set; } 13 | 14 | [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] 15 | [JsonPropertyName("outputs")] 16 | public BlindedMessage[]? Outputs { get; set; } 17 | } -------------------------------------------------------------------------------- /DotNut/MintMethodSetting.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json; 2 | using System.Text.Json.Serialization; 3 | 4 | namespace DotNut; 5 | 6 | public class MintMethodSetting 7 | { 8 | [JsonPropertyName("method")] public string Method { get; set; } 9 | [JsonPropertyName("unit")] public string Unit { get; set; } 10 | [JsonPropertyName("min_amount")] public ulong? Min { get; set; } 11 | [JsonPropertyName("max_amount")] public ulong? Max { get; set; } 12 | [JsonPropertyName("options")] public JsonDocument? Options { get; set; } 13 | } 14 | 15 | -------------------------------------------------------------------------------- /DotNut/Nut10ProofSecret.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json.Serialization; 2 | 3 | namespace DotNut; 4 | 5 | public class Nut10ProofSecret 6 | { 7 | 8 | [JsonPropertyName("nonce")] 9 | public string Nonce { get; set; } 10 | 11 | [JsonPropertyName("data")] 12 | [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] 13 | public string Data { get; set; } 14 | 15 | [JsonPropertyName("tags")] 16 | [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] 17 | public string[][]? Tags { get; set; } 18 | } -------------------------------------------------------------------------------- /DotNut/FeeHelper.cs: -------------------------------------------------------------------------------- 1 | using DotNut.ApiModels; 2 | 3 | namespace DotNut; 4 | 5 | public static class FeeHelper 6 | { 7 | 8 | public static ulong ComputeFee(this IEnumerable proofsToSpend, Dictionary keysetFees) 9 | { 10 | ulong sum = 0; 11 | foreach (var proof in proofsToSpend) 12 | { 13 | if (keysetFees.TryGetValue(proof.Id, out var fee)) 14 | { 15 | sum += fee; 16 | } 17 | } 18 | 19 | return (sum + 999) / 1000; 20 | } 21 | } -------------------------------------------------------------------------------- /DotNut/ApiModels/Mint/bolt11/PostMintQuoteBolt11Request.cs: -------------------------------------------------------------------------------- 1 | using System.Diagnostics; 2 | using System.Text.Json.Serialization; 3 | 4 | namespace DotNut.ApiModels; 5 | 6 | public class PostMintQuoteBolt11Request 7 | { 8 | 9 | [JsonPropertyName("amount")] 10 | public ulong Amount {get; set;} 11 | 12 | [JsonPropertyName("unit")] 13 | public string Unit {get; set;} 14 | 15 | [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] 16 | [JsonPropertyName("description")] 17 | public string? Description {get; set;} 18 | } -------------------------------------------------------------------------------- /DotNut/Encoding/CashuTokenV3Encoder.cs: -------------------------------------------------------------------------------- 1 | using System.Text; 2 | using System.Text.Json; 3 | 4 | namespace DotNut; 5 | 6 | public class CashuTokenV3Encoder : ICashuTokenEncoder 7 | { 8 | public string Encode(CashuToken token) 9 | { 10 | var json = JsonSerializer.Serialize(token); 11 | return Base64UrlSafe.Encode(Encoding.UTF8.GetBytes(json)); 12 | } 13 | 14 | public CashuToken Decode(string token) 15 | { 16 | var json = Encoding.UTF8.GetString(Base64UrlSafe.Decode(token)); 17 | return JsonSerializer.Deserialize(json)!; 18 | } 19 | } -------------------------------------------------------------------------------- /DotNut/BlindSignature.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json.Serialization; 2 | using DotNut.JsonConverters; 3 | 4 | namespace DotNut; 5 | 6 | public class BlindSignature 7 | { 8 | [JsonPropertyName("amount")] public ulong Amount { get; set; } 9 | 10 | [JsonConverter(typeof(KeysetIdJsonConverter))] 11 | [JsonPropertyName("id")] 12 | public KeysetId Id { get; set; } 13 | 14 | [JsonPropertyName("C_")] public PubKey C_ { get; set; } 15 | 16 | 17 | [JsonPropertyName("dleq")] 18 | [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] 19 | public DLEQProof? DLEQ { get; set; } 20 | } -------------------------------------------------------------------------------- /DotNut/PaymentRequestTransportInitiator.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Concurrent; 2 | 3 | namespace DotNut; 4 | 5 | public class PaymentRequestTransportInitiator 6 | { 7 | private readonly IEnumerable _handlers; 8 | public static ConcurrentBag Handlers { get; } = [ new HttpPaymentRequestInterfaceHandler(null) ]; 9 | public PaymentRequestTransportInitiator(IEnumerable handlers) 10 | { 11 | _handlers = handlers; 12 | } 13 | 14 | public PaymentRequestTransportInitiator() 15 | { 16 | _handlers = Handlers.ToArray(); 17 | } 18 | } -------------------------------------------------------------------------------- /DotNut/ApiModels/GetKeysResponse.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json.Serialization; 2 | 3 | namespace DotNut.ApiModels; 4 | 5 | public class GetKeysResponse 6 | { 7 | [JsonPropertyName("keysets")] public KeysetItemResponse[] Keysets { get; set; } 8 | 9 | public class KeysetItemResponse 10 | { 11 | [JsonPropertyName("id")] public KeysetId Id { get; set; } 12 | [JsonPropertyName("unit")] public string Unit { get; set; } 13 | [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] 14 | [JsonPropertyName("final_expiry")] public ulong? FinalExpiry { get; set; } 15 | [JsonPropertyName("keys")] public Keyset Keys { get; set; } 16 | } 17 | } -------------------------------------------------------------------------------- /DotNut/ApiModels/Mint/bolt12/PostMintQuoteBolt12Request.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json.Serialization; 2 | 3 | namespace DotNut.ApiModels.Mint.bolt12; 4 | 5 | public class PostMintQuoteBolt12Request 6 | { 7 | [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] 8 | [JsonPropertyName("amount")] 9 | public ulong? Amount { get; set; } 10 | 11 | [JsonPropertyName("unit")] 12 | public string Unit {get; set;} 13 | 14 | [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] 15 | [JsonPropertyName("description")] 16 | public string? Description { get; set; } 17 | 18 | [JsonPropertyName("pubkey")] 19 | public string Pubkey { get; set; } 20 | 21 | } -------------------------------------------------------------------------------- /DotNut/CashuToken.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json.Serialization; 2 | 3 | namespace DotNut; 4 | 5 | public class CashuToken 6 | { 7 | public class Token 8 | { 9 | [JsonPropertyName("mint")] public string Mint { get; set; } 10 | [JsonPropertyName("proofs")] public List Proofs { get; set; } 11 | } 12 | 13 | [JsonPropertyName("token")] public List Tokens { get; set; } 14 | 15 | [JsonPropertyName("unit")] 16 | [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] 17 | public string? Unit { get; set; } 18 | 19 | [JsonPropertyName("memo")] 20 | [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] 21 | public string? Memo { get; set; } 22 | } -------------------------------------------------------------------------------- /DotNut/JsonConverters/UnixDateTimeOffsetConverter.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json; 2 | using System.Text.Json.Serialization; 3 | 4 | namespace DotNut.JsonConverters; 5 | 6 | public class UnixDateTimeOffsetConverter : JsonConverter 7 | { 8 | public override DateTimeOffset Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) 9 | { 10 | var val = reader.TokenType == JsonTokenType.Number? reader.GetInt64() : long.Parse(reader.GetString()!); 11 | 12 | 13 | return DateTimeOffset.FromUnixTimeSeconds(val); 14 | } 15 | 16 | public override void Write(Utf8JsonWriter writer, DateTimeOffset value, JsonSerializerOptions options) 17 | { 18 | writer.WriteNumberValue(value.ToUnixTimeSeconds()); 19 | } 20 | } -------------------------------------------------------------------------------- /DotNut/ApiModels/Melt/bolt12/PostMeltQuoteBolt12Response.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json.Serialization; 2 | 3 | namespace DotNut.ApiModels.Melt.bolt12; 4 | 5 | public class PostMeltQuoteBolt12Response 6 | { 7 | [JsonPropertyName("quote")] public string Quote { get; set; } 8 | 9 | [JsonPropertyName("request")] public string Request { get; set; } 10 | 11 | [JsonPropertyName("amount")] public ulong Amount { get; set; } 12 | 13 | [JsonPropertyName("fee_reserve")] public ulong FeeReserve { get; set; } 14 | 15 | [JsonPropertyName("state")] public string State { get; set; } 16 | 17 | [JsonPropertyName("expiry")] public int Expiry { get; set; } 18 | 19 | [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] 20 | [JsonPropertyName("payment_preimage")] public string PaymentPreimage { get; set; } 21 | 22 | } -------------------------------------------------------------------------------- /DotNut/ApiModels/GetKeysetsResponse.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json.Serialization; 2 | 3 | namespace DotNut.ApiModels; 4 | 5 | public class GetKeysetsResponse 6 | { 7 | [JsonPropertyName("keysets")] public KeysetItemResponse[] Keysets { get; set; } 8 | 9 | public class KeysetItemResponse 10 | { 11 | [JsonPropertyName("id")] public KeysetId Id { get; set; } 12 | [JsonPropertyName("unit")] public string Unit { get; set; } 13 | [JsonPropertyName("active")] public bool Active { get; set; } 14 | [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] 15 | [JsonPropertyName("input_fee_ppk")] public ulong? InputFee { get; set; } 16 | [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] 17 | [JsonPropertyName("final_expiry")] public ulong? FinalExpiry { get; set; } 18 | } 19 | } -------------------------------------------------------------------------------- /DotNut/Encoding/Base64UrlSafe.cs: -------------------------------------------------------------------------------- 1 | namespace DotNut; 2 | 3 | public static class Base64UrlSafe 4 | { 5 | static readonly char[] padding = {'='}; 6 | 7 | //(base64 encoding with / replaced by _ and + by -) 8 | public static string Encode(byte[] data) 9 | { 10 | return System.Convert.ToBase64String(data) 11 | .TrimEnd(padding).Replace('+', '-').Replace('/', '_').TrimEnd(padding); 12 | } 13 | 14 | public static byte[] Decode(string base64) 15 | { 16 | string incoming = base64.Replace('_', '/').Replace('-', '+'); 17 | switch (base64.Length % 4) 18 | { 19 | case 2: 20 | incoming += "=="; 21 | break; 22 | case 3: 23 | incoming += "="; 24 | break; 25 | } 26 | 27 | return System.Convert.FromBase64String(incoming); 28 | } 29 | } -------------------------------------------------------------------------------- /DotNut/PrivKey.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json.Serialization; 2 | using DotNut.JsonConverters; 3 | using NBitcoin.Secp256k1; 4 | 5 | namespace DotNut; 6 | 7 | [JsonConverter(typeof(PrivKeyJsonConverter))] 8 | public class PrivKey 9 | { 10 | [JsonIgnore(Condition = JsonIgnoreCondition.Always)] public readonly ECPrivKey Key; 11 | 12 | public PrivKey(string hex) 13 | { 14 | Key = hex.ToPrivKey(); 15 | } 16 | 17 | private PrivKey(ECPrivKey ecPrivKey) 18 | { 19 | Key = ecPrivKey; 20 | } 21 | 22 | public override string ToString() 23 | { 24 | return Convert.ToHexString(Key.ToBytes()).ToLower(); 25 | } 26 | 27 | public static implicit operator PrivKey(ECPrivKey ecPubKey) 28 | { 29 | return new PrivKey(ecPubKey); 30 | } 31 | 32 | public static implicit operator ECPrivKey(PrivKey privKey) 33 | { 34 | return privKey.Key; 35 | } 36 | } -------------------------------------------------------------------------------- /DotNut/HttpPaymentRequestInterfaceHandler.cs: -------------------------------------------------------------------------------- 1 | using System.Net.Http.Json; 2 | 3 | namespace DotNut; 4 | 5 | public class HttpPaymentRequestInterfaceHandler : PaymentRequestInterfaceHandler 6 | { 7 | private readonly HttpClient _httpClient; 8 | 9 | public HttpPaymentRequestInterfaceHandler(HttpClient? httpClient) 10 | { 11 | _httpClient = httpClient ?? new HttpClient(); 12 | } 13 | public bool CanHandle(PaymentRequest request) 14 | { 15 | return request.Transports.Any(t => t.Type == "post"); 16 | } 17 | 18 | public async Task SendPayment(PaymentRequest request, PaymentRequestPayload payload, 19 | CancellationToken cancellationToken = default) 20 | { 21 | var endpoint = new Uri(request.Transports.First(t => t.Type == "post").Target); 22 | var response = await _httpClient.PostAsJsonAsync(endpoint, payload,cancellationToken); 23 | response.EnsureSuccessStatusCode(); 24 | } 25 | } -------------------------------------------------------------------------------- /DotNut/ApiModels/Melt/bolt11/PostMeltQuoteBolt11Response.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json.Serialization; 2 | 3 | namespace DotNut.ApiModels; 4 | 5 | public class PostMeltQuoteBolt11Response 6 | { 7 | [JsonPropertyName("quote")] 8 | public string Quote { get; set; } 9 | 10 | [JsonPropertyName("amount")] 11 | public ulong Amount { get; set; } 12 | 13 | [JsonPropertyName("fee_reserve")] 14 | public int FeeReserve { get; set; } 15 | 16 | [JsonPropertyName("state")] 17 | public string State {get; set;} 18 | 19 | [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] 20 | [JsonPropertyName("expiry")] 21 | public int? Expiry {get; set;} 22 | 23 | [JsonPropertyName("payment_preimage")] 24 | public string? PaymentPreimage {get; set;} 25 | 26 | [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] 27 | [JsonPropertyName("change")] 28 | public BlindSignature[]? Change { get; set; } 29 | } -------------------------------------------------------------------------------- /DotNut/Proof.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json.Serialization; 2 | using DotNut.JsonConverters; 3 | 4 | 5 | namespace DotNut; 6 | 7 | public class Proof 8 | { 9 | [JsonPropertyName("amount")] public ulong Amount { get; set; } 10 | 11 | [JsonConverter(typeof(KeysetIdJsonConverter))] 12 | [JsonPropertyName("id")] 13 | public KeysetId Id { get; set; } 14 | 15 | [JsonPropertyName("secret")] public ISecret Secret { get; set; } 16 | 17 | [JsonPropertyName("C")] public PubKey C { get; set; } 18 | 19 | [JsonPropertyName("witness")] 20 | [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] 21 | public string? Witness { get; set; } 22 | 23 | [JsonPropertyName("dleq")] 24 | [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] 25 | public DLEQProof? DLEQ { get; set; } 26 | 27 | [JsonPropertyName("p2pk_e")] 28 | [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] 29 | public PubKey? P2PkE { get; set; } // must not be exposed to mint 30 | 31 | } -------------------------------------------------------------------------------- /DotNut/ApiModels/Mint/bolt11/PostMintQuoteBolt11Response.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json.Serialization; 2 | 3 | namespace DotNut.ApiModels; 4 | 5 | public class PostMintQuoteBolt11Response 6 | { 7 | [JsonPropertyName("quote")] 8 | public string Quote { get; set; } 9 | 10 | [JsonPropertyName("request")] 11 | public string Request { get; set; } 12 | 13 | [JsonPropertyName("state")] 14 | public string State { get; set; } 15 | 16 | [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] 17 | [JsonPropertyName("expiry")] 18 | public int? Expiry { get; set; } 19 | 20 | // 'amount' and 'unit' were recently added to the spec in PostMintQuoteBolt11Response, so they are optional for now 21 | [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] 22 | [JsonPropertyName("amount")] 23 | public ulong? Amount { get; set; } 24 | 25 | [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] 26 | [JsonPropertyName("unit")] 27 | public string? Unit {get; set;} 28 | } -------------------------------------------------------------------------------- /DotNut/ApiModels/Mint/bolt12/PostMintQuoteBolt12Response.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json.Serialization; 2 | 3 | namespace DotNut.ApiModels.Mint.bolt12; 4 | 5 | public class PostMintQuoteBolt12Response 6 | { 7 | [JsonPropertyName("quote")] 8 | public string Quote { get; set; } 9 | 10 | [JsonPropertyName("request")] 11 | public string Request {get; set;} 12 | 13 | [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] 14 | [JsonPropertyName("amount")] 15 | public ulong? Amount { get; set; } 16 | 17 | [JsonPropertyName("unit")] 18 | public string Unit {get; set;} 19 | 20 | [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] 21 | [JsonPropertyName("expiry")] 22 | public int? Expiry {get; set;} 23 | 24 | [JsonPropertyName("pubkey")] 25 | public string Pubkey {get; set;} 26 | 27 | [JsonPropertyName("amount_paid")] 28 | public ulong AmountPaid {get; set;} 29 | 30 | [JsonPropertyName("amount_issued")] 31 | public ulong AmountIssued {get; set;} 32 | } -------------------------------------------------------------------------------- /DotNut/Nut10Secret.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json; 2 | using System.Text.Json.Serialization; 3 | using DotNut.JsonConverters; 4 | using NBitcoin.Secp256k1; 5 | 6 | namespace DotNut; 7 | 8 | [JsonConverter(typeof(Nut10SecretJsonConverter))] 9 | public class Nut10Secret : ISecret 10 | { 11 | private readonly string? _originalString; 12 | 13 | public Nut10Secret(string key, Nut10ProofSecret proofSecret) 14 | { 15 | Key = key; 16 | ProofSecret = proofSecret; 17 | } 18 | 19 | public Nut10Secret(string originalString) 20 | { 21 | _originalString = originalString; 22 | } 23 | 24 | public string Key { get; set; } 25 | public Nut10ProofSecret ProofSecret { get; set; } 26 | 27 | 28 | public byte[] GetBytes() 29 | { 30 | return _originalString != null 31 | ? System.Text.Encoding.UTF8.GetBytes(_originalString) 32 | : JsonSerializer.SerializeToUtf8Bytes(this); 33 | } 34 | 35 | public ECPubKey ToCurve() 36 | { 37 | return Cashu.HashToCurve(GetBytes()); 38 | } 39 | } -------------------------------------------------------------------------------- /DotNut/JsonConverters/PrivKeyJsonConverter.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json; 2 | using System.Text.Json.Serialization; 3 | 4 | namespace DotNut.JsonConverters; 5 | 6 | public class PrivKeyJsonConverter : JsonConverter 7 | { 8 | public override PrivKey? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) 9 | { 10 | if (reader.TokenType == JsonTokenType.Null) 11 | { 12 | return null; 13 | } 14 | 15 | if (reader.TokenType != JsonTokenType.String || 16 | reader.GetString() is not { } str || 17 | string.IsNullOrEmpty(str)) 18 | { 19 | throw new JsonException("Expected string"); 20 | } 21 | 22 | return new PrivKey(str); 23 | } 24 | 25 | public override void Write(Utf8JsonWriter writer, PrivKey? value, JsonSerializerOptions options) 26 | { 27 | if (value is null) 28 | { 29 | writer.WriteNullValue(); 30 | return; 31 | } 32 | 33 | writer.WriteStringValue(value.ToString()); 34 | } 35 | } -------------------------------------------------------------------------------- /DotNut/JsonConverters/PubKeyJsonConverter.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json; 2 | using System.Text.Json.Serialization; 3 | 4 | namespace DotNut.JsonConverters; 5 | 6 | public class PubKeyJsonConverter : JsonConverter 7 | { 8 | public override PubKey? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) 9 | { 10 | if (reader.TokenType == JsonTokenType.Null) 11 | { 12 | return null; 13 | } 14 | 15 | if (reader.TokenType != JsonTokenType.String || 16 | reader.GetString() is not { } str || 17 | string.IsNullOrEmpty(str)) 18 | { 19 | throw new JsonException("Expected string"); 20 | } 21 | 22 | return new PubKey(str, true); 23 | } 24 | 25 | public override void Write(Utf8JsonWriter writer, PubKey? value, JsonSerializerOptions options) 26 | { 27 | if (value is null) 28 | { 29 | writer.WriteNullValue(); 30 | return; 31 | } 32 | 33 | writer.WriteStringValue(value.ToString()); 34 | } 35 | } -------------------------------------------------------------------------------- /DotNut/JsonConverters/KeysetIdJsonConverter.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json; 2 | using System.Text.Json.Serialization; 3 | 4 | namespace DotNut.JsonConverters; 5 | 6 | public class KeysetIdJsonConverter : JsonConverter 7 | { 8 | public override KeysetId? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) 9 | { 10 | if (reader.TokenType == JsonTokenType.Null) 11 | { 12 | return null; 13 | } 14 | 15 | if (reader.TokenType != JsonTokenType.String || 16 | reader.GetString() is not { } str || 17 | string.IsNullOrEmpty(str)) 18 | { 19 | throw new JsonException("Expected string"); 20 | } 21 | 22 | return new KeysetId(str); 23 | } 24 | 25 | public override void Write(Utf8JsonWriter writer, KeysetId? value, JsonSerializerOptions options) 26 | { 27 | if (value is null) 28 | { 29 | writer.WriteNullValue(); 30 | return; 31 | } 32 | 33 | writer.WriteStringValue(value.ToString()); 34 | } 35 | } -------------------------------------------------------------------------------- /DotNut/PaymentRequest.cs: -------------------------------------------------------------------------------- 1 | using PeterO.Cbor; 2 | 3 | namespace DotNut; 4 | 5 | public class PaymentRequest 6 | { 7 | public string? PaymentId { get; set; } 8 | public ulong? Amount { get; set; } 9 | public string? Unit { get; set; } 10 | public bool? OneTimeUse { get; set; } 11 | public string[]? Mints { get; set; } 12 | public string? Memo { get; set; } 13 | public PaymentRequestTransport[] Transports { get; set; } 14 | 15 | public override string ToString() 16 | { 17 | var obj = PaymentRequestEncoder.Instance.ToCBORObject(this); 18 | return $"creqA{Base64UrlSafe.Encode(obj.EncodeToBytes())}"; 19 | } 20 | 21 | public static PaymentRequest Parse(string creqA) 22 | { 23 | if (!creqA.StartsWith("creqA", StringComparison.InvariantCultureIgnoreCase)) 24 | { 25 | throw new FormatException("Invalid payment request"); 26 | } 27 | 28 | var data = Base64UrlSafe.Decode(creqA.Substring(5)); 29 | return PaymentRequestEncoder.Instance.FromCBORObject(CBORObject.DecodeFromBytes(data)); 30 | } 31 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Andrew Camilleri 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /DotNut/NBitcoin/LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2023 Metaco SA 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /DotNut.Nostr/DotNut.Nostr.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net8.0 5 | enable 6 | enable 7 | true 8 | DotNut.Nostr 9 | Kukks 10 | Support Cashu payment requests through Nostr 11 | MIT 12 | https://github.com/Kukks/DotNut 13 | 1.0.0 14 | https://github.com/Kukks/DotNut 15 | git 16 | bitcoin cashu ecash secp256k1 nostr 17 | https://github.com/Kukks/DotNut/blob/master/LICENSE 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /DotNut.Tests/DotNut.Tests.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net8.0 5 | enable 6 | enable 7 | 8 | false 9 | true 10 | 11 | 12 | 13 | 14 | 15 | 16 | runtime; build; native; contentfiles; analyzers; buildtransitive 17 | all 18 | 19 | 20 | runtime; build; native; contentfiles; analyzers; buildtransitive 21 | all 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /DotNut/DotNut.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net8.0 5 | enable 6 | enable 7 | true 8 | DotNut 9 | Kukks 10 | A full C# native implementation of the Cashu protocol 11 | MIT 12 | https://github.com/Kukks/DotNut 13 | 1.0.6 14 | https://github.com/Kukks/DotNut 15 | git 16 | bitcoin cashu ecash secp256k1 17 | https://github.com/Kukks/DotNut/blob/master/LICENSE 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /DotNuts.sln.DotSettings.user: -------------------------------------------------------------------------------- 1 | 2 | <SessionState ContinuousTestingMode="0" Name="Test1" xmlns="urn:schemas-jetbrains-com:jetbrains-ut-session"> 3 | <TestAncestor> 4 | <TestId>xUnit::0AAAA85C-9FDC-4FD5-9CC2-ED8385B40106::net8.0::DotNuts.Tests.UnitTest1</TestId> 5 | </TestAncestor> 6 | </SessionState> 7 | <SessionState ContinuousTestingMode="0" IsActive="True" Name="Nut11_Signatures" xmlns="urn:schemas-jetbrains-com:jetbrains-ut-session"> 8 | <And> 9 | <Namespace>DotNuts.Tests</Namespace> 10 | <Project Location="C:\Git\DotNuts\DotNuts.Tests" Presentation="&lt;DotNuts.Tests&gt;" /> 11 | </And> 12 | </SessionState> -------------------------------------------------------------------------------- /DotNut/PubKey.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json.Serialization; 2 | using DotNut.JsonConverters; 3 | using NBitcoin.Secp256k1; 4 | 5 | namespace DotNut; 6 | 7 | [JsonConverter(typeof(PubKeyJsonConverter))] 8 | public class PubKey 9 | { 10 | [JsonIgnore(Condition = JsonIgnoreCondition.Always)] public readonly ECPubKey Key; 11 | 12 | public PubKey(string hex, bool onlyAllowCompressed = false) 13 | { 14 | if (onlyAllowCompressed && hex.Length != 66) 15 | { 16 | throw new ArgumentException("Only compressed public keys are allowed"); 17 | } 18 | Key = hex.ToPubKey(); 19 | } 20 | 21 | private PubKey(ECPubKey ecPubKey) 22 | { 23 | Key = ecPubKey; 24 | } 25 | 26 | public override string ToString() 27 | { 28 | return Convert.ToHexString(Key.ToBytes()).ToLower(); 29 | } 30 | 31 | public static implicit operator PubKey(ECPubKey ecPubKey) 32 | { 33 | return new PubKey(ecPubKey); 34 | } 35 | 36 | public static implicit operator ECPubKey(PubKey pubKey) 37 | { 38 | return pubKey.Key; 39 | } 40 | 41 | public override bool Equals(object? obj) 42 | { 43 | if (ReferenceEquals(this, obj)) return true; 44 | if (obj is not PubKey other) return false; 45 | return this.Key == other.Key; 46 | } 47 | 48 | public override int GetHashCode() 49 | { 50 | return Key.GetHashCode(); 51 | } 52 | 53 | } -------------------------------------------------------------------------------- /DotNut/Api/ICashuApi.cs: -------------------------------------------------------------------------------- 1 | using DotNut.ApiModels; 2 | 3 | namespace DotNut.Api; 4 | 5 | public interface ICashuApi 6 | { 7 | Task GetKeys(CancellationToken cancellationToken = default); 8 | Task GetKeys(KeysetId keysetId, CancellationToken cancellationToken = default); 9 | Task GetKeysets(CancellationToken cancellationToken = default); 10 | Task Swap(PostSwapRequest request, CancellationToken cancellationToken = default); 11 | 12 | Task CreateMintQuote(string method, TRequest request, CancellationToken 13 | cancellationToken = default); 14 | 15 | Task CreateMeltQuote(string method, TRequest request, CancellationToken 16 | cancellationToken = default); 17 | 18 | Task Melt(string method, TRequest request, CancellationToken 19 | cancellationToken = default); 20 | 21 | Task CheckMintQuote(string method, string quoteId, CancellationToken 22 | cancellationToken = default); 23 | 24 | Task Mint(string method, TRequest request, CancellationToken cancellationToken = default); 25 | Task CheckState(PostCheckStateRequest request, CancellationToken cancellationToken = default); 26 | Task Restore(PostRestoreRequest request, CancellationToken cancellationToken = default); 27 | Task GetInfo(CancellationToken cancellationToken = default); 28 | } -------------------------------------------------------------------------------- /DotNut/JsonConverters/SecretJsonConverter.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json; 2 | using System.Text.Json.Serialization; 3 | 4 | namespace DotNut.JsonConverters; 5 | 6 | public class SecretJsonConverter : JsonConverter 7 | { 8 | public override ISecret? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) 9 | { 10 | if (reader.TokenType == JsonTokenType.Null) 11 | { 12 | return null; 13 | } 14 | 15 | if (reader.TokenType == JsonTokenType.StartArray && reader.CurrentDepth == 0) 16 | { 17 | //we are converting a nut10 secret directly 18 | return JsonSerializer.Deserialize(ref reader, options); 19 | } 20 | if (reader.TokenType != JsonTokenType.String) 21 | { 22 | throw new JsonException("Expected string"); 23 | } 24 | 25 | var str = reader.GetString(); 26 | if (string.IsNullOrEmpty(str)) 27 | { 28 | throw new JsonException("Secret was not nut10 or a (not empty) string"); 29 | } 30 | try 31 | { 32 | return JsonSerializer.Deserialize(str); 33 | } 34 | catch (Exception e) 35 | { 36 | 37 | return new StringSecret(str); 38 | } 39 | } 40 | 41 | public override void Write(Utf8JsonWriter writer, ISecret? value, JsonSerializerOptions options) 42 | { 43 | switch (value) 44 | { 45 | case null: 46 | writer.WriteNullValue(); 47 | return; 48 | case Nut10Secret nut10Secret: 49 | writer.WriteStringValue(JsonSerializer.Serialize(nut10Secret)); 50 | return; 51 | case StringSecret stringSecret: 52 | writer.WriteStringValue(stringSecret.Secret); 53 | break; 54 | } 55 | } 56 | } -------------------------------------------------------------------------------- /DotNut.Nostr/NostrNip17PaymentRequestInterfaceHandler.cs: -------------------------------------------------------------------------------- 1 | using System.Security.Cryptography; 2 | using System.Text.Json; 3 | using NBitcoin.Secp256k1; 4 | using NNostr.Client; 5 | using NNostr.Client.Protocols; 6 | 7 | namespace DotNut.Nostr; 8 | 9 | public class NostrNip17PaymentRequestInterfaceHandler : PaymentRequestInterfaceHandler 10 | { 11 | public static void Register() 12 | { 13 | PaymentRequestTransportInitiator.Handlers.Add(new NostrNip17PaymentRequestInterfaceHandler()); 14 | } 15 | 16 | public bool CanHandle(PaymentRequest request) 17 | { 18 | return request.Transports.Any(t => t.Type == "nostr" && t.Tags.Any( t => t.Key == "n" && t.Value == "17")); 19 | } 20 | 21 | public async Task SendPayment(PaymentRequest request, PaymentRequestPayload payload, 22 | CancellationToken cancellationToken = default) 23 | { 24 | var nprofileStr = request.Transports.First(t => t.Type == "nostr" && t.Tags.Any( t => t.Key == "n" && t.Value == "17")).Target; 25 | var nprofile = (NIP19.NosteProfileNote) NIP19.FromNIP19Note(nprofileStr); 26 | using var client = new CompositeNostrClient(nprofile.Relays.Select(r => new Uri(r)).ToArray()); 27 | await client.Connect(cancellationToken); 28 | var ephemeralKey = ECPrivKey.Create(RandomNumberGenerator.GetBytes(32)); 29 | var msg = new NostrEvent() 30 | { 31 | Kind = 14, 32 | Content = JsonSerializer.Serialize(payload), 33 | CreatedAt = DateTimeOffset.Now, 34 | PublicKey = ephemeralKey.CreateXOnlyPubKey().ToHex(), 35 | Tags = new(), 36 | }; 37 | msg.Id = msg.ComputeId(); 38 | 39 | var giftWrap = await NIP17.Create(msg, ephemeralKey,ECXOnlyPubKey.Create(Convert.FromHexString(nprofile.PubKey)), null); 40 | await client.SendEventsAndWaitUntilReceived(new []{giftWrap}, cancellationToken); 41 | 42 | } 43 | } -------------------------------------------------------------------------------- /DotNut/JsonConverters/Nut10SecretJsonConverter.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json; 2 | using System.Text.Json.Serialization; 3 | 4 | namespace DotNut.JsonConverters; 5 | 6 | public class Nut10SecretJsonConverter : JsonConverter 7 | { 8 | 9 | 10 | 11 | public override Nut10Secret? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) 12 | { 13 | if(reader.TokenType == JsonTokenType.Null) 14 | return null; 15 | if (reader.TokenType != JsonTokenType.StartArray) 16 | { 17 | throw new JsonException("Expected array"); 18 | } 19 | reader.Read(); 20 | if(reader.TokenType != JsonTokenType.String) 21 | throw new JsonException("Expected string"); 22 | var key = reader.GetString(); 23 | reader.Read(); 24 | 25 | Nut10ProofSecret? proofSecret; 26 | switch (key) 27 | { 28 | case P2PKProofSecret.Key: 29 | proofSecret = JsonSerializer.Deserialize(ref reader, options); 30 | break; 31 | case HTLCProofSecret.Key: 32 | proofSecret = JsonSerializer.Deserialize(ref reader, options); 33 | 34 | break; 35 | default: 36 | throw new JsonException("Unknown secret type"); 37 | } 38 | if(proofSecret is null) 39 | throw new JsonException("Invalid proof secret"); 40 | reader.Read(); 41 | if (reader.TokenType != JsonTokenType.EndArray) 42 | { 43 | throw new JsonException("Expected end array"); 44 | } 45 | 46 | return new Nut10Secret(key, proofSecret); 47 | 48 | 49 | } 50 | 51 | public override void Write(Utf8JsonWriter writer, Nut10Secret? value, JsonSerializerOptions options) 52 | { 53 | if (value is null) 54 | { 55 | writer.WriteNullValue(); 56 | return; 57 | } 58 | 59 | writer.WriteStartArray(); 60 | JsonSerializer.Serialize(writer, value.Key, options); 61 | JsonSerializer.Serialize(writer, value.ProofSecret, options); 62 | writer.WriteEndArray(); 63 | } 64 | } -------------------------------------------------------------------------------- /DotNut/ApiModels/GetInfoResponse.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json; 2 | using System.Text.Json.Serialization; 3 | using DotNut.JsonConverters; 4 | 5 | namespace DotNut.ApiModels; 6 | 7 | public class GetInfoResponse 8 | { 9 | [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] 10 | [JsonPropertyName("name")] 11 | public string? Name { get; set; } 12 | 13 | [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] 14 | [JsonPropertyName("pubkey")] 15 | public string? Pubkey { get; set; } 16 | 17 | [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] 18 | [JsonPropertyName("version")] 19 | public string? Version { get; set; } 20 | 21 | [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] 22 | [JsonPropertyName("description")] 23 | public string? Description { get; set; } 24 | 25 | [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] 26 | [JsonPropertyName("description_long")] 27 | public string? DescriptionLong { get; set; } 28 | 29 | [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] 30 | [JsonPropertyName("contact")] 31 | public List? Contact { get; set; } 32 | 33 | [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] 34 | [JsonPropertyName("motd")] 35 | public string? Motd { get; set; } 36 | 37 | [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] 38 | [JsonPropertyName("icon_url")] 39 | public string? IconUrl { get; set; } 40 | 41 | [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] 42 | [JsonPropertyName("urls")] 43 | public string[]? Urls { get; set; } 44 | 45 | [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] 46 | [JsonConverter(typeof(UnixDateTimeOffsetConverter))] 47 | [JsonPropertyName("time")] 48 | public DateTimeOffset? Time { get; set; } 49 | 50 | [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] 51 | [JsonPropertyName("tos_url")] 52 | public string? TosUrl { get; set; } 53 | 54 | [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] 55 | [JsonPropertyName("nuts")] 56 | public Dictionary? Nuts { get; set; } 57 | } -------------------------------------------------------------------------------- /DotNut/NBitcoin/BitWriter.cs: -------------------------------------------------------------------------------- 1 | using System.Collections; 2 | using DotNut.NBitcoin.BIP39; 3 | 4 | namespace DotNut.NBitcoin 5 | { 6 | 7 | class BitWriter 8 | { 9 | List values = new List(); 10 | public void Write(bool value) 11 | { 12 | values.Insert(Position, value); 13 | _Position++; 14 | } 15 | 16 | internal void Write(byte[] bytes) 17 | { 18 | Write(bytes, bytes.Length * 8); 19 | } 20 | 21 | public void Write(byte[] bytes, int bitCount) 22 | { 23 | bytes = SwapEndianBytes(bytes); 24 | BitArray array = new BitArray(bytes); 25 | values.InsertRange(Position, array.OfType().Take(bitCount)); 26 | _Position += bitCount; 27 | } 28 | 29 | public byte[] ToBytes() 30 | { 31 | var array = ToBitArray(); 32 | var bytes = ToByteArray(array); 33 | bytes = SwapEndianBytes(bytes); 34 | return bytes; 35 | } 36 | 37 | //BitArray.CopyTo do not exist in portable lib 38 | static byte[] ToByteArray(BitArray bits) 39 | { 40 | int arrayLength = bits.Length / 8; 41 | if (bits.Length % 8 != 0) 42 | arrayLength++; 43 | byte[] array = new byte[arrayLength]; 44 | 45 | for (int i = 0; i < bits.Length; i++) 46 | { 47 | int b = i / 8; 48 | int offset = i % 8; 49 | array[b] |= bits.Get(i) ? (byte)(1 << offset) : (byte)0; 50 | } 51 | return array; 52 | } 53 | 54 | 55 | public BitArray ToBitArray() 56 | { 57 | return new BitArray(values.ToArray()); 58 | } 59 | 60 | public int[] ToIntegers() 61 | { 62 | var array = new BitArray(values.ToArray()); 63 | return Wordlist.ToIntegers(array); 64 | } 65 | 66 | 67 | static byte[] SwapEndianBytes(byte[] bytes) 68 | { 69 | byte[] output = new byte[bytes.Length]; 70 | for (int i = 0; i < output.Length; i++) 71 | { 72 | byte newByte = 0; 73 | for (int ib = 0; ib < 8; ib++) 74 | { 75 | newByte += (byte)(((bytes[i] >> ib) & 1) << (7 - ib)); 76 | } 77 | output[i] = newByte; 78 | } 79 | return output; 80 | } 81 | 82 | 83 | int _Position; 84 | public int Position 85 | { 86 | get => _Position; 87 | set => _Position = value; 88 | } 89 | public void Write(BitArray bitArray, int bitCount) 90 | { 91 | for (int i = 0; i < bitCount; i++) 92 | { 93 | Write(bitArray.Get(i)); 94 | } 95 | } 96 | } 97 | 98 | } 99 | -------------------------------------------------------------------------------- /DotNut/NUT13/BIP32.cs: -------------------------------------------------------------------------------- 1 | using System.Numerics; 2 | using System.Security.Cryptography; 3 | using NBip32Fast; 4 | using NBip32Fast.Interfaces; 5 | using NBitcoin.Secp256k1; 6 | 7 | namespace DotNut.NUT13; 8 | 9 | public class BIP32 : IHdKeyAlgo 10 | { 11 | 12 | public static readonly IHdKeyAlgo Instance = new BIP32(); 13 | private static readonly byte[] CurveBytes = "Bitcoin seed"u8.ToArray(); 14 | 15 | private static readonly BigInteger N = Cashu.N; 16 | 17 | private BIP32() 18 | { 19 | } 20 | 21 | public HdKey GetMasterKeyFromSeed(ReadOnlySpan seed) 22 | { 23 | var seedCopy = new Span(seed.ToArray()); 24 | while (true) 25 | { 26 | HMACSHA512.HashData(CurveBytes, seedCopy, seedCopy); 27 | var key = seedCopy[..32]; 28 | var keyInt = new BigInteger(key, true, true); 29 | if (keyInt > N || keyInt.IsZero) continue; 30 | return new HdKey(key, seedCopy[32..]); 31 | } 32 | } 33 | 34 | public HdKey Derive(HdKey parent, KeyPathElement index) 35 | { 36 | Span hash = index.Hardened 37 | ? IHdKeyAlgo.Bip32Hash(parent.ChainCode, index, 0x00, parent.PrivateKey) 38 | : IHdKeyAlgo.Bip32Hash(parent.ChainCode, index, GetPublic(parent.PrivateKey)); 39 | 40 | var parentKey = new BigInteger (parent.PrivateKey, true, true); 41 | 42 | while (true) 43 | { 44 | var key = hash[..32]; 45 | var cc = hash[32..]; 46 | key.Reverse(); 47 | var keyInt = new BigInteger (key, true); 48 | var res = BigInteger.Add(keyInt, parentKey) % N; 49 | 50 | if (keyInt > N || res.IsZero) 51 | { 52 | hash = IHdKeyAlgo.Bip32Hash(parent.ChainCode, index, 0x01, cc); 53 | continue; 54 | } 55 | 56 | var keyBytes = res.ToByteArray(true, true); 57 | if (keyBytes.Length < 32) 58 | { 59 | var paddedKey = new byte[32]; 60 | keyBytes.CopyTo(paddedKey, 32 - keyBytes.Length); 61 | keyBytes = paddedKey; 62 | } 63 | return new HdKey(keyBytes, cc); 64 | } 65 | } 66 | 67 | public byte[] GetPublic(ReadOnlySpan privateKey) 68 | { 69 | return ECPrivKey.Create(privateKey).CreatePubKey().ToBytes(); 70 | } 71 | } -------------------------------------------------------------------------------- /DotNut/KeysetId.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Encodings.Web; 2 | using System.Text.Json.Serialization; 3 | using DotNut.JsonConverters; 4 | 5 | namespace DotNut; 6 | 7 | [JsonConverter(typeof(KeysetIdJsonConverter))] 8 | public class KeysetId : IEquatable,IEqualityComparer 9 | { 10 | public bool Equals(KeysetId? x, KeysetId? y) 11 | { 12 | if (ReferenceEquals(x, y)) return true; 13 | if (ReferenceEquals(x, null)) return false; 14 | if (ReferenceEquals(y, null)) return false; 15 | if (x.GetType() != y.GetType()) return false; 16 | return string.Equals(x._id, y._id, StringComparison.InvariantCultureIgnoreCase); 17 | } 18 | 19 | public int GetHashCode(KeysetId obj) 20 | { 21 | return StringComparer.InvariantCultureIgnoreCase.GetHashCode(obj._id); 22 | } 23 | 24 | public bool Equals(KeysetId? other) 25 | { 26 | return Equals(this, other); 27 | } 28 | 29 | public override bool Equals(object? obj) 30 | { 31 | 32 | return Equals(this, obj as KeysetId); 33 | } 34 | 35 | public override int GetHashCode() 36 | { 37 | return StringComparer.InvariantCultureIgnoreCase.GetHashCode(_id); 38 | } 39 | 40 | public static bool operator ==(KeysetId? left, KeysetId? right) 41 | { 42 | return (left is null && right is null) || left?.Equals(right) is true || right?.Equals(left) is true; 43 | } 44 | 45 | public static bool operator !=(KeysetId? left, KeysetId? right) 46 | { 47 | return !(left == right); 48 | } 49 | 50 | 51 | private readonly string _id; 52 | 53 | public KeysetId(string Id) 54 | { 55 | // Legacy support for all keyset formats 56 | if (Id.Length != 66 && Id.Length != 16 && Id.Length != 12) 57 | { 58 | throw new ArgumentException("KeysetId must be 66, 16 or 12 (legacy) characters long"); 59 | } 60 | _id = Id; 61 | } 62 | 63 | public KeysetId(Keyset keyset) 64 | { 65 | _id = keyset.GetKeysetId().ToString(); 66 | } 67 | 68 | public override string ToString() 69 | { 70 | return _id; 71 | } 72 | 73 | public byte GetVersion() 74 | { 75 | string versionStr = _id.Substring(0, 2); 76 | return Convert.ToByte(versionStr, 16); 77 | } 78 | 79 | public byte[] GetBytes() 80 | { 81 | return Convert.FromHexString(_id); 82 | } 83 | } -------------------------------------------------------------------------------- /DotNut/JsonConverters/KeysetJsonConverter.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json; 2 | using System.Text.Json.Serialization; 3 | 4 | namespace DotNut.JsonConverters; 5 | 6 | public class KeysetJsonConverter : JsonConverter 7 | { 8 | public override Keyset? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) 9 | { 10 | if (reader.TokenType == JsonTokenType.Null) 11 | { 12 | return null; 13 | } 14 | else if (reader.TokenType != JsonTokenType.StartObject) 15 | { 16 | throw new JsonException("Expected object"); 17 | } 18 | 19 | var keyset = new Keyset(); 20 | while (reader.Read()) 21 | { 22 | if (reader.TokenType == JsonTokenType.EndObject) 23 | { 24 | 25 | return keyset; 26 | } 27 | 28 | ulong amount; 29 | if (reader.TokenType == JsonTokenType.Number) 30 | { 31 | amount = reader.GetUInt64(); 32 | } 33 | else if (reader.TokenType is JsonTokenType.String or JsonTokenType.PropertyName) 34 | { 35 | var str = reader.GetString(); 36 | if (string.IsNullOrEmpty(str)) 37 | throw new JsonException("Expected string"); 38 | amount = ulong.Parse(str); 39 | } 40 | else 41 | { 42 | throw new JsonException("Expected number or string"); 43 | } 44 | 45 | 46 | reader.Read(); 47 | var pubkey = JsonSerializer.Deserialize(ref reader, options); 48 | if(pubkey is null || pubkey.Key.ToBytes().Length != 33) 49 | throw new JsonException("Invalid public key (not compressed?)"); 50 | keyset.Add(amount, pubkey); 51 | } 52 | 53 | throw new JsonException("Missing end object"); 54 | } 55 | 56 | public override void Write(Utf8JsonWriter writer, Keyset? value, JsonSerializerOptions options) 57 | { 58 | if (value is null) 59 | { 60 | writer.WriteNullValue(); 61 | return; 62 | } 63 | 64 | writer.WriteStartObject(); 65 | foreach (var pair in value) 66 | { 67 | writer.WritePropertyName(pair.Key.ToString()); 68 | JsonSerializer.Serialize(writer, pair.Value, options); 69 | } 70 | 71 | writer.WriteEndObject(); 72 | } 73 | } -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: 'Publish application' 2 | on: push 3 | 4 | jobs: 5 | build: 6 | runs-on: ubuntu-latest 7 | steps: 8 | # Checkout the code 9 | - uses: actions/checkout@v2 10 | 11 | # Install .NET Core SDK 12 | - name: Setup .NET Core 13 | uses: actions/setup-dotnet@v1 14 | with: 15 | dotnet-version: 8.0.x 16 | 17 | - name: Test 18 | run: dotnet test 19 | 20 | - name: Publish NuGet 21 | if: ${{ github.ref == 'refs/heads/master' }} # Publish only when the push is on master 22 | uses: Rebel028/publish-nuget@v2.7.0 23 | with: 24 | PROJECT_FILE_PATH: DotNut/DotNut.csproj 25 | NUGET_KEY: ${{secrets.NUGET_KEY}} 26 | PACKAGE_NAME: DotNut 27 | INCLUDE_SYMBOLS: false 28 | VERSION_REGEX: ^\s*(.*)<\/PackageVersion>\s*$ 29 | TAG_COMMIT: true 30 | TAG_FORMAT: DotNut/v* 31 | - name: Publish Github Package Registry 32 | if: ${{ github.ref == 'refs/heads/master' }} # Publish only when the push is on master 33 | uses: Rebel028/publish-nuget@v2.7.0 34 | with: 35 | PROJECT_FILE_PATH: DotNut/DotNut.csproj 36 | NUGET_SOURCE: "https://nuget.pkg.github.com/Kukks" 37 | NUGET_KEY: ${{secrets.GH_TOKEN}} 38 | PACKAGE_NAME: DotNut 39 | INCLUDE_SYMBOLS: false 40 | VERSION_REGEX: ^\s*(.*)<\/PackageVersion>\s*$ 41 | TAG_COMMIT: true 42 | TAG_FORMAT: DotNut/v* 43 | - name: Publish NuGet for nostr 44 | if: ${{ github.ref == 'refs/heads/master' }} # Publish only when the push is on master 45 | uses: Rebel028/publish-nuget@v2.7.0 46 | with: 47 | PROJECT_FILE_PATH: DotNut.Nostr/DotNut.Nostr.csproj 48 | NUGET_KEY: ${{secrets.NUGET_KEY}} 49 | PACKAGE_NAME: DotNut.Nostr 50 | INCLUDE_SYMBOLS: false 51 | VERSION_REGEX: ^\s*(.*)<\/PackageVersion>\s*$ 52 | TAG_COMMIT: true 53 | TAG_FORMAT: DotNut.Nostr/v* 54 | - name: Publish Github Package Registry for nostr 55 | if: ${{ github.ref == 'refs/heads/master' }} # Publish only when the push is on master 56 | uses: Rebel028/publish-nuget@v2.7.0 57 | with: 58 | PROJECT_FILE_PATH: DotNut.Nostr/DotNut.Nostr.csproj 59 | NUGET_SOURCE: "https://nuget.pkg.github.com/Kukks" 60 | NUGET_KEY: ${{secrets.GH_TOKEN}} 61 | PACKAGE_NAME: DotNut.Nostr 62 | INCLUDE_SYMBOLS: false 63 | VERSION_REGEX: ^\s*(.*)<\/PackageVersion>\s*$ 64 | TAG_COMMIT: true 65 | TAG_FORMAT: DotNut/v* -------------------------------------------------------------------------------- /DotNut/HTLCBuilder.cs: -------------------------------------------------------------------------------- 1 | using NBitcoin.Secp256k1; 2 | 3 | namespace DotNut; 4 | 5 | public class HTLCBuilder : P2PkBuilder 6 | { 7 | public string HashLock { get; set; } 8 | 9 | /* 10 | * ugly hack to reuse P2PkBuilder for HTLCs. 11 | * P2PkBuilder expects a pubkey in `data` field, but we need to store a hashlock instead 12 | * 13 | * we inject a dummy pubkey so the loader doesn’t break, then remove it after load/build. 14 | */ 15 | private static readonly PubKey _dummy = 16 | "020000000000000000000000000000000000000000000000000000000000000001".ToPubKey(); 17 | 18 | public static HTLCBuilder Load(HTLCProofSecret proofSecret) 19 | { 20 | var hashLock = proofSecret.Data; 21 | if (hashLock.Length != 64) // hex string 22 | { 23 | throw new ArgumentException("HashLock must be 32 bytes (64 chars hex)", nameof(HashLock)); 24 | } 25 | var tempProof = new P2PKProofSecret 26 | { 27 | Data = _dummy.ToString(), 28 | Nonce = proofSecret.Nonce, 29 | Tags = proofSecret.Tags 30 | }; 31 | 32 | var innerbuilder = P2PkBuilder.Load(tempProof); 33 | innerbuilder.Pubkeys = innerbuilder.Pubkeys.Except([_dummy.Key]).ToArray(); 34 | return new HTLCBuilder() 35 | { 36 | HashLock = hashLock, 37 | Lock = innerbuilder.Lock, 38 | Pubkeys = innerbuilder.Pubkeys, 39 | RefundPubkeys = innerbuilder.RefundPubkeys, 40 | SignatureThreshold = innerbuilder.SignatureThreshold, 41 | SigFlag = innerbuilder.SigFlag, 42 | Nonce = innerbuilder.Nonce 43 | }; 44 | 45 | } 46 | 47 | public new HTLCProofSecret Build() 48 | { 49 | if (HashLock.Length != 64) 50 | { 51 | throw new ArgumentException("HashLock must be 32 bytes (64 chars hex)", nameof(HashLock)); 52 | } 53 | var innerBuilder = new P2PkBuilder() 54 | { 55 | Lock = Lock, 56 | Pubkeys = Pubkeys.ToArray(), 57 | RefundPubkeys = RefundPubkeys, 58 | SignatureThreshold = SignatureThreshold, 59 | SigFlag = SigFlag, 60 | Nonce = Nonce 61 | }; 62 | innerBuilder.Pubkeys = innerBuilder.Pubkeys.Prepend(_dummy.Key).ToArray(); 63 | 64 | var p2pkProof = innerBuilder.Build(); 65 | return new HTLCProofSecret() 66 | { 67 | Data = HashLock, 68 | Nonce = p2pkProof.Nonce, 69 | Tags = p2pkProof.Tags 70 | }; 71 | } 72 | 73 | public new HTLCProofSecret BuildBlinded(KeysetId keysetId, out ECPubKey p2pkE) 74 | { 75 | throw new NotImplementedException(); 76 | } 77 | 78 | public HTLCProofSecret BuildBlinded(KeysetId keysetId, ECPrivKey p2pke) 79 | { 80 | throw new NotImplementedException(); 81 | } 82 | } -------------------------------------------------------------------------------- /DotNut/NUT13/Nut13.cs: -------------------------------------------------------------------------------- 1 | using System.Buffers.Binary; 2 | using System.Security.Cryptography; 3 | using DotNut.NBitcoin.BIP39; 4 | using NBip32Fast; 5 | 6 | namespace DotNut.NUT13; 7 | 8 | public static class Nut13 9 | { 10 | public static byte[] DeriveBlindingFactor(this Mnemonic mnemonic, KeysetId keysetId, int counter) => 11 | DeriveBlindingFactor(mnemonic.DeriveSeed(), keysetId, counter); 12 | 13 | 14 | public static StringSecret DeriveSecret(this Mnemonic mnemonic, KeysetId keysetId, int counter) => 15 | DeriveSecret(mnemonic.DeriveSeed(), keysetId, counter); 16 | 17 | 18 | public static byte[] DeriveBlindingFactor(this byte[] seed, KeysetId keysetId, int counter) 19 | { 20 | switch (keysetId.GetVersion()) 21 | { 22 | case 0x00: 23 | return BIP32.Instance.DerivePath(GetNut13DerivationPath(keysetId, counter, false), seed).PrivateKey 24 | .ToArray(); 25 | case 0x01: 26 | { 27 | return DeriveHmac(seed, keysetId, counter, false); 28 | } 29 | default: 30 | throw new ArgumentException("Invalid keyset id prefix"); 31 | } 32 | } 33 | public static StringSecret DeriveSecret(this byte[] seed, KeysetId keysetId, int counter) 34 | { 35 | switch (keysetId.GetVersion()) 36 | { 37 | case 0x00: 38 | var key = BIP32.Instance.DerivePath(GetNut13DerivationPath(keysetId, counter, true), seed).PrivateKey; 39 | return new StringSecret(Convert.ToHexString(key).ToLower()); 40 | case 0x01: 41 | { 42 | var secretBytes = DeriveHmac(seed, keysetId, counter, true); 43 | return new StringSecret(Convert.ToHexString(secretBytes).ToLower()); 44 | } 45 | default: 46 | throw new ArgumentException("Invalid keyset id prefix"); 47 | } 48 | 49 | } 50 | 51 | public static byte[] DeriveHmac(byte[] seed, KeysetId keysetId, int counter, bool secretOrr) 52 | { 53 | byte[] counterBuffer = BitConverter.GetBytes((long)counter); 54 | if (BitConverter.IsLittleEndian) 55 | Array.Reverse(counterBuffer); 56 | var message = "Cashu_KDF_HMAC_SHA256"u8.ToArray() 57 | .Concat(Convert.FromHexString(keysetId.ToString())) 58 | .Concat(counterBuffer) 59 | .Append(secretOrr ? (byte)0x00 : (byte)0x01); 60 | 61 | using var hmac = new HMACSHA256(seed); 62 | return hmac.ComputeHash(message.ToArray()); 63 | } 64 | 65 | public const string Purpose = "129372'"; 66 | public static KeyPath GetNut13DerivationPath(KeysetId keysetId, int counter, bool secretOrr) 67 | { 68 | return (KeyPath) KeyPath.Parse($"m/{Purpose}/0'/{GetKeysetIdInt(keysetId)}'/{counter}'/{(secretOrr?0:1)}")!; 69 | } 70 | 71 | public static long GetKeysetIdInt(KeysetId keysetId) 72 | { 73 | var keysetIdInt = long.Parse("0" + keysetId, System.Globalization.NumberStyles.HexNumber); 74 | var mod = (long)Math.Pow(2, 31) - 1; 75 | return keysetIdInt % mod; 76 | } 77 | } -------------------------------------------------------------------------------- /DotNut/Keyset.cs: -------------------------------------------------------------------------------- 1 | using System.Net.Mime; 2 | using System.Text; 3 | using System.Text.Encodings.Web; 4 | using System.Text.Json.Serialization; 5 | using DotNut.JsonConverters; 6 | using SHA256 = System.Security.Cryptography.SHA256; 7 | 8 | namespace DotNut; 9 | 10 | [JsonConverter(typeof(KeysetJsonConverter))] 11 | public class Keyset : Dictionary 12 | { 13 | public KeysetId GetKeysetId(byte version = 0x00, string? unit = null, string? finalExpiration = null) 14 | { 15 | // 1 - sort public keys by their amount in ascending order 16 | // 2 - concatenate all public keys to a single byte array 17 | if (Count == 0) throw new InvalidOperationException("Keyset cannot be empty."); 18 | var sortedBytes = this 19 | .OrderBy(x => x.Key) 20 | .Select(pair => pair.Value.Key.ToBytes()) 21 | .SelectMany(b => b) 22 | .ToArray(); 23 | 24 | using SHA256 sha256 = SHA256.Create(); 25 | 26 | 27 | switch (version) 28 | { 29 | // 3 - HASH_SHA256 the concatenated public keys 30 | // 4 - take the first 14 characters of the hex-encoded hash 31 | // 5 - prefix it with a keyset ID version byte 32 | case 0x00: 33 | { 34 | var hash = sha256.ComputeHash(sortedBytes); 35 | return new KeysetId(Convert.ToHexString(new []{version}) + Convert.ToHexString(hash).Substring(0, 14).ToLower()); 36 | } 37 | // 3 - add the lowercase unit string to the byte array (e.g. "unit:sat") 38 | // 4 - If a final expiration is specified, convert it into a radix-10 string and add it (e.g "final_expiry:1896187313") 39 | // 4 - HASH_SHA256 the concatenated byte array 40 | // 5 - prefix it with a keyset ID version byte 41 | case 0x01: 42 | { 43 | if (String.IsNullOrWhiteSpace(unit)) 44 | { 45 | throw new ArgumentNullException( nameof(unit), $"Unit parameter is required with version: {version}"); 46 | } 47 | sortedBytes = sortedBytes.Concat(Encoding.UTF8.GetBytes($"unit:{unit.Trim().ToLowerInvariant()}")).ToArray(); 48 | 49 | if (!string.IsNullOrWhiteSpace(finalExpiration)) 50 | { 51 | sortedBytes = sortedBytes.Concat(Encoding.UTF8.GetBytes($"final_expiry:{finalExpiration.Trim()}")) 52 | .ToArray(); 53 | } 54 | 55 | var hash = sha256.ComputeHash(sortedBytes); 56 | return new KeysetId(Convert.ToHexString(new[] { version }) + 57 | Convert.ToHexString(hash).ToLower()); 58 | } 59 | default: 60 | throw new ArgumentException($"Unsupported keyset version: {version}"); 61 | } 62 | 63 | } 64 | 65 | public bool VerifyKeysetId(KeysetId keysetId, string? unit = null, string? finalExpiration = null) 66 | { 67 | byte version = keysetId.GetVersion(); 68 | var derived = GetKeysetId(version, unit, finalExpiration).ToString(); 69 | var presented = keysetId.ToString(); 70 | if (presented.Length > derived.Length) return false; 71 | return string.Equals(derived, presented, StringComparison.Ordinal) || 72 | derived.StartsWith(presented, StringComparison.Ordinal); 73 | } 74 | } -------------------------------------------------------------------------------- /DotNut/Encoding/CashuTokenHelper.cs: -------------------------------------------------------------------------------- 1 | namespace DotNut; 2 | 3 | public static class CashuTokenHelper 4 | { 5 | public static Dictionary Encoders { get; } = new(); 6 | 7 | static CashuTokenHelper() 8 | { 9 | Encoders.Add("A", new CashuTokenV3Encoder()); 10 | Encoders.Add("B", new CashuTokenV4Encoder()); 11 | } 12 | 13 | public const string CashuUriScheme = "cashu:"; 14 | public const string CashuPrefix = "cashu"; 15 | 16 | public static string Encode(this CashuToken token, string version = "B", bool makeUri = false) 17 | { 18 | if (!Encoders.TryGetValue(version, out var encoder)) 19 | { 20 | throw new NotSupportedException($"Version {version} is not supported"); 21 | } 22 | 23 | //trim trailing slash from mint url 24 | foreach (var token1 in token.Tokens) 25 | { 26 | if (token1.Mint.EndsWith("/")) 27 | { 28 | token1.Mint = token1.Mint.TrimEnd('/'); 29 | } 30 | foreach (var proof in token1.Proofs) 31 | { 32 | proof.Id = MaybeShortId(proof.Id); 33 | } 34 | } 35 | 36 | 37 | 38 | var encoded = encoder.Encode(token); 39 | 40 | var result = $"{CashuPrefix}{version}{encoded}"; 41 | 42 | if (makeUri) 43 | { 44 | return CashuUriScheme + result; 45 | } 46 | 47 | return result; 48 | } 49 | 50 | public static CashuToken Decode(string token, out string? version, List? keysets = null) 51 | { 52 | version = null; 53 | if (Uri.IsWellFormedUriString(token, UriKind.Absolute)) 54 | { 55 | token = token.Replace(CashuUriScheme, ""); 56 | } 57 | 58 | if (!token.StartsWith(CashuPrefix)) 59 | { 60 | throw new FormatException("Invalid cashu token"); 61 | } 62 | 63 | token = token.Substring(CashuPrefix.Length); 64 | version = token[0].ToString(); 65 | 66 | if (!Encoders.TryGetValue(version, out var encoder)) 67 | { 68 | throw new NotSupportedException($"Version {version} is not supported"); 69 | } 70 | 71 | token = token.Substring(1); 72 | var decoded = encoder.Decode(token); 73 | 74 | if (keysets is null) 75 | { 76 | return decoded; 77 | } 78 | 79 | foreach (var innerToken in decoded.Tokens) 80 | { 81 | innerToken.Proofs = MapShortKeysetIds(innerToken.Proofs, keysets); 82 | } 83 | return decoded; 84 | } 85 | 86 | private static KeysetId MaybeShortId(KeysetId id) 87 | { 88 | if (id.GetVersion() != 0x01) return id; 89 | var s = id.ToString(); 90 | return s.Length <= 16 ? id : new KeysetId(s.Substring(0, 16)); 91 | } 92 | private static List MapShortKeysetIds(List proofs, List? keysets = null) 93 | { 94 | if (proofs.Count == 0) 95 | return proofs; 96 | 97 | if (proofs.All(p => p.Id.GetVersion() != 0x01 || p.Id.ToString().Length != 16)) 98 | return proofs; 99 | 100 | if (keysets is null) 101 | throw new ArgumentNullException(nameof(keysets), 102 | "Encountered short keyset IDs but no keysets were provided for mapping."); 103 | 104 | return proofs.Select(proof => 105 | { 106 | if (proof.Id.GetVersion() != 0x01) 107 | return proof; 108 | 109 | var proofShortId = proof.Id.ToString(); 110 | var match = keysets.FirstOrDefault(ks => ks.GetKeysetId().ToString().StartsWith(proofShortId, StringComparison.OrdinalIgnoreCase)); 111 | 112 | if (match is null) 113 | throw new Exception($"Couldn't map short keyset ID {proof.Id} to any known keysets of the current Mint"); 114 | 115 | return new Proof 116 | { 117 | Amount = proof.Amount, 118 | Secret = proof.Secret, 119 | C = proof.C, 120 | Witness = proof.Witness, 121 | DLEQ = proof.DLEQ, 122 | Id = new KeysetId(match.GetKeysetId().ToString()) 123 | }; 124 | }).ToList(); 125 | } 126 | } -------------------------------------------------------------------------------- /DotNut/PaymentRequestEncoder.cs: -------------------------------------------------------------------------------- 1 | using PeterO.Cbor; 2 | 3 | namespace DotNut; 4 | 5 | public class PaymentRequestEncoder : ICBORToFromConverter 6 | { 7 | public static readonly PaymentRequestEncoder Instance = new(); 8 | 9 | public CBORObject ToCBORObject(PaymentRequest paymentRequest) 10 | { 11 | var cbor = CBORObject.NewMap(); 12 | if (paymentRequest.PaymentId is not null) 13 | cbor.Add("i", paymentRequest.PaymentId); 14 | if (paymentRequest.Amount is not null) 15 | cbor.Add("a", paymentRequest.Amount); 16 | if (paymentRequest.Unit is not null) 17 | cbor.Add("u", paymentRequest.Unit); 18 | if (paymentRequest.OneTimeUse is not null) 19 | cbor.Add("s", paymentRequest.OneTimeUse); 20 | if (paymentRequest.Mints is not null) 21 | cbor.Add("m", paymentRequest.Mints); 22 | if (paymentRequest.Memo is not null) 23 | cbor.Add("d", paymentRequest.Memo); 24 | var transports = CBORObject.NewArray(); 25 | foreach (var transport in paymentRequest.Transports) 26 | { 27 | var transportItem = CBORObject.NewMap() 28 | .Add("t", transport.Type) 29 | .Add("a", transport.Target); 30 | if (transport.Tags is not null) 31 | { 32 | var tags = CBORObject.NewArray(); 33 | foreach (var tag in transport.Tags) 34 | { 35 | var tagItem = CBORObject.NewArray(); 36 | tagItem.Add(tag.Key); 37 | tagItem.Add(tag.Value); 38 | tags.Add(tagItem); 39 | } 40 | 41 | transportItem.Add("g", tags); 42 | } 43 | 44 | transports.Add(transportItem); 45 | } 46 | 47 | cbor.Add("t", transports); 48 | return cbor; 49 | } 50 | 51 | public PaymentRequest FromCBORObject(CBORObject obj) 52 | { 53 | var paymentRequest = new PaymentRequest(); 54 | foreach (var key in obj.Keys) 55 | { 56 | var value = obj[key]; 57 | switch (key.AsString()) 58 | { 59 | case "i": 60 | paymentRequest.PaymentId = value.AsString(); 61 | break; 62 | case "a": 63 | paymentRequest.Amount = value.ToObject(); 64 | break; 65 | case "u": 66 | paymentRequest.Unit = value.AsString(); 67 | break; 68 | case "s": 69 | paymentRequest.OneTimeUse = value.AsBoolean(); 70 | break; 71 | case "m": 72 | paymentRequest.Mints = value.Values.Select(v => v.AsString()).ToArray(); 73 | break; 74 | case "d": 75 | paymentRequest.Memo = value.AsString(); 76 | break; 77 | case "t": 78 | paymentRequest.Transports = value.Values.Select(v => 79 | { 80 | var transport = new PaymentRequestTransport(); 81 | foreach (var transportKey in v.Keys) 82 | { 83 | var transportValue = v[transportKey]; 84 | switch (transportKey.AsString()) 85 | { 86 | case "t": 87 | transport.Type = transportValue.AsString(); 88 | break; 89 | case "a": 90 | transport.Target = transportValue.AsString(); 91 | break; 92 | case "g": 93 | transport.Tags = transportValue.Values.Select(tag => 94 | { 95 | var tagItem = new PaymentRequestTransportTag 96 | { 97 | Key = tag[0].AsString(), 98 | Value = tag[1].AsString() 99 | }; 100 | return tagItem; 101 | }).ToArray(); 102 | break; 103 | } 104 | } 105 | 106 | return transport; 107 | }).ToArray(); 108 | break; 109 | } 110 | } 111 | 112 | return paymentRequest; 113 | } 114 | } -------------------------------------------------------------------------------- /DotNut/Encoding/CashuTokenV4Encoder.cs: -------------------------------------------------------------------------------- 1 | using System.Text; 2 | using System.Text.Json; 3 | using NBitcoin.Secp256k1; 4 | using PeterO.Cbor; 5 | 6 | namespace DotNut; 7 | 8 | public class CashuTokenV4Encoder : ICashuTokenEncoder, ICBORToFromConverter 9 | { 10 | public string Encode(CashuToken token) 11 | { 12 | var obj = ToCBORObject(token); 13 | return Base64UrlSafe.Encode(obj.EncodeToBytes()); 14 | } 15 | 16 | public CashuToken Decode(string token) 17 | { 18 | var obj = CBORObject.DecodeFromBytes(Base64UrlSafe.Decode(token)); 19 | return FromCBORObject(obj); 20 | } 21 | 22 | public CBORObject ToCBORObject(CashuToken token) 23 | { 24 | //ensure that all token mints are the same 25 | var mints = token.Tokens.Select(token1 => token1.Mint).ToArray(); 26 | if (mints.Distinct().Count() != 1) 27 | throw new FormatException("All proofs must have the same mint in v4 tokens"); 28 | var proofSets = CBORObject.NewArray(); 29 | foreach (var proofSet in token.Tokens.SelectMany(token1 => token1.Proofs).GroupBy(proof => proof.Id)) 30 | { 31 | var proofSetItem = CBORObject.NewOrderedMap(); 32 | proofSetItem.Add("i", Convert.FromHexString(proofSet.Key.ToString())); 33 | var proofSetItemArray = CBORObject.NewArray(); 34 | foreach (var proof in proofSet) 35 | { 36 | var proofItem = CBORObject.NewOrderedMap() 37 | .Add("a", proof.Amount) 38 | .Add("s", Encoding.UTF8.GetString(proof.Secret.GetBytes())) 39 | .Add("c", proof.C.Key.ToBytes()); 40 | if (proof.DLEQ is not null) 41 | { 42 | proofItem.Add("d", CBORObject 43 | .NewOrderedMap() 44 | .Add("e", proof.DLEQ.E.Key.ToBytes()) 45 | .Add("s", proof.DLEQ.S.Key.ToBytes()) 46 | .Add("r", proof.DLEQ.R.Key.ToBytes())); 47 | } 48 | 49 | if (proof.Witness is not null) 50 | { 51 | proofItem.Add("w", proof.Witness); 52 | } 53 | 54 | if (proof.P2PkE?.Key is not null) 55 | { 56 | proofItem.Add("pe", Convert.FromHexString(proof.P2PkE.Key.ToString())); 57 | } 58 | 59 | proofSetItemArray.Add(proofItem); 60 | } 61 | 62 | proofSetItem.Add("p", proofSetItemArray); 63 | proofSets.Add(proofSetItem); 64 | } 65 | 66 | var cbor = CBORObject.NewOrderedMap(); 67 | 68 | 69 | if (token.Memo is not null) 70 | cbor.Add("d", token.Memo); 71 | cbor.Add("t", proofSets) 72 | .Add("m", mints.First()) 73 | .Add("u", token.Unit!); 74 | return cbor; 75 | } 76 | 77 | public CashuToken FromCBORObject(CBORObject obj) 78 | { 79 | return new CashuToken 80 | { 81 | Unit = obj["u"].AsString(), 82 | Memo = obj.GetOrDefault("d", null)?.AsString(), 83 | Tokens = 84 | [ 85 | new CashuToken.Token() 86 | { 87 | Mint = obj["m"].AsString(), 88 | Proofs = obj["t"].Values.SelectMany(proofSet => 89 | { 90 | var id = new KeysetId(Convert.ToHexString(proofSet["i"].GetByteString()).ToLowerInvariant()); 91 | 92 | return proofSet["p"].Values.Select(proof => new Proof() 93 | { 94 | Amount = proof["a"].ToObject(), 95 | Secret = JsonSerializer.Deserialize(proof["s"].ToJSONString())!, 96 | C = ECPubKey.Create(proof["c"].GetByteString()), 97 | Witness = proof.GetOrDefault("w", null)?.AsString(), 98 | DLEQ = proof.GetOrDefault("d", null) is { } cborDLEQ 99 | ? new DLEQProof 100 | { 101 | E = ECPrivKey.Create(cborDLEQ["e"].GetByteString()), 102 | S = ECPrivKey.Create(cborDLEQ["s"].GetByteString()), 103 | R = ECPrivKey.Create(cborDLEQ["r"].GetByteString()) 104 | } 105 | : null, 106 | Id = id, 107 | 108 | P2PkE = proof.GetOrDefault("pe", null) is { } p2pkE? 109 | ECPubKey.Create(p2pkE.GetByteString()) : null 110 | 111 | }); 112 | }).ToList() 113 | } 114 | ] 115 | }; 116 | } 117 | } -------------------------------------------------------------------------------- /DotNut.sln: -------------------------------------------------------------------------------- 1 | 2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DotNut", "DotNut\DotNut.csproj", "{997966AE-DC46-4473-9C82-A0F1750B2481}" 4 | EndProject 5 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DotNut.Tests", "DotNut.Tests\DotNut.Tests.csproj", "{0AAAA85C-9FDC-4FD5-9CC2-ED8385B40106}" 6 | EndProject 7 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DotNut.Nostr", "DotNut.Nostr\DotNut.Nostr.csproj", "{EDBCFA42-B7A0-4443-9359-8A44BDA17935}" 8 | EndProject 9 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DotNut.Demo", "DotNut.Demo\DotNut.Demo.csproj", "{305097F3-A4E5-4511-8E4E-0C4C12A953C6}" 10 | EndProject 11 | Global 12 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 13 | Debug|Any CPU = Debug|Any CPU 14 | Debug|x64 = Debug|x64 15 | Debug|x86 = Debug|x86 16 | Release|Any CPU = Release|Any CPU 17 | Release|x64 = Release|x64 18 | Release|x86 = Release|x86 19 | EndGlobalSection 20 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 21 | {997966AE-DC46-4473-9C82-A0F1750B2481}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 22 | {997966AE-DC46-4473-9C82-A0F1750B2481}.Debug|Any CPU.Build.0 = Debug|Any CPU 23 | {997966AE-DC46-4473-9C82-A0F1750B2481}.Debug|x64.ActiveCfg = Debug|Any CPU 24 | {997966AE-DC46-4473-9C82-A0F1750B2481}.Debug|x64.Build.0 = Debug|Any CPU 25 | {997966AE-DC46-4473-9C82-A0F1750B2481}.Debug|x86.ActiveCfg = Debug|Any CPU 26 | {997966AE-DC46-4473-9C82-A0F1750B2481}.Debug|x86.Build.0 = Debug|Any CPU 27 | {997966AE-DC46-4473-9C82-A0F1750B2481}.Release|Any CPU.ActiveCfg = Release|Any CPU 28 | {997966AE-DC46-4473-9C82-A0F1750B2481}.Release|Any CPU.Build.0 = Release|Any CPU 29 | {997966AE-DC46-4473-9C82-A0F1750B2481}.Release|x64.ActiveCfg = Release|Any CPU 30 | {997966AE-DC46-4473-9C82-A0F1750B2481}.Release|x64.Build.0 = Release|Any CPU 31 | {997966AE-DC46-4473-9C82-A0F1750B2481}.Release|x86.ActiveCfg = Release|Any CPU 32 | {997966AE-DC46-4473-9C82-A0F1750B2481}.Release|x86.Build.0 = Release|Any CPU 33 | {0AAAA85C-9FDC-4FD5-9CC2-ED8385B40106}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 34 | {0AAAA85C-9FDC-4FD5-9CC2-ED8385B40106}.Debug|Any CPU.Build.0 = Debug|Any CPU 35 | {0AAAA85C-9FDC-4FD5-9CC2-ED8385B40106}.Debug|x64.ActiveCfg = Debug|Any CPU 36 | {0AAAA85C-9FDC-4FD5-9CC2-ED8385B40106}.Debug|x64.Build.0 = Debug|Any CPU 37 | {0AAAA85C-9FDC-4FD5-9CC2-ED8385B40106}.Debug|x86.ActiveCfg = Debug|Any CPU 38 | {0AAAA85C-9FDC-4FD5-9CC2-ED8385B40106}.Debug|x86.Build.0 = Debug|Any CPU 39 | {0AAAA85C-9FDC-4FD5-9CC2-ED8385B40106}.Release|Any CPU.ActiveCfg = Release|Any CPU 40 | {0AAAA85C-9FDC-4FD5-9CC2-ED8385B40106}.Release|Any CPU.Build.0 = Release|Any CPU 41 | {0AAAA85C-9FDC-4FD5-9CC2-ED8385B40106}.Release|x64.ActiveCfg = Release|Any CPU 42 | {0AAAA85C-9FDC-4FD5-9CC2-ED8385B40106}.Release|x64.Build.0 = Release|Any CPU 43 | {0AAAA85C-9FDC-4FD5-9CC2-ED8385B40106}.Release|x86.ActiveCfg = Release|Any CPU 44 | {0AAAA85C-9FDC-4FD5-9CC2-ED8385B40106}.Release|x86.Build.0 = Release|Any CPU 45 | {EDBCFA42-B7A0-4443-9359-8A44BDA17935}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 46 | {EDBCFA42-B7A0-4443-9359-8A44BDA17935}.Debug|Any CPU.Build.0 = Debug|Any CPU 47 | {EDBCFA42-B7A0-4443-9359-8A44BDA17935}.Debug|x64.ActiveCfg = Debug|Any CPU 48 | {EDBCFA42-B7A0-4443-9359-8A44BDA17935}.Debug|x64.Build.0 = Debug|Any CPU 49 | {EDBCFA42-B7A0-4443-9359-8A44BDA17935}.Debug|x86.ActiveCfg = Debug|Any CPU 50 | {EDBCFA42-B7A0-4443-9359-8A44BDA17935}.Debug|x86.Build.0 = Debug|Any CPU 51 | {EDBCFA42-B7A0-4443-9359-8A44BDA17935}.Release|Any CPU.ActiveCfg = Release|Any CPU 52 | {EDBCFA42-B7A0-4443-9359-8A44BDA17935}.Release|Any CPU.Build.0 = Release|Any CPU 53 | {EDBCFA42-B7A0-4443-9359-8A44BDA17935}.Release|x64.ActiveCfg = Release|Any CPU 54 | {EDBCFA42-B7A0-4443-9359-8A44BDA17935}.Release|x64.Build.0 = Release|Any CPU 55 | {EDBCFA42-B7A0-4443-9359-8A44BDA17935}.Release|x86.ActiveCfg = Release|Any CPU 56 | {EDBCFA42-B7A0-4443-9359-8A44BDA17935}.Release|x86.Build.0 = Release|Any CPU 57 | {305097F3-A4E5-4511-8E4E-0C4C12A953C6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 58 | {305097F3-A4E5-4511-8E4E-0C4C12A953C6}.Debug|Any CPU.Build.0 = Debug|Any CPU 59 | {305097F3-A4E5-4511-8E4E-0C4C12A953C6}.Debug|x64.ActiveCfg = Debug|Any CPU 60 | {305097F3-A4E5-4511-8E4E-0C4C12A953C6}.Debug|x64.Build.0 = Debug|Any CPU 61 | {305097F3-A4E5-4511-8E4E-0C4C12A953C6}.Debug|x86.ActiveCfg = Debug|Any CPU 62 | {305097F3-A4E5-4511-8E4E-0C4C12A953C6}.Debug|x86.Build.0 = Debug|Any CPU 63 | {305097F3-A4E5-4511-8E4E-0C4C12A953C6}.Release|Any CPU.ActiveCfg = Release|Any CPU 64 | {305097F3-A4E5-4511-8E4E-0C4C12A953C6}.Release|Any CPU.Build.0 = Release|Any CPU 65 | {305097F3-A4E5-4511-8E4E-0C4C12A953C6}.Release|x64.ActiveCfg = Release|Any CPU 66 | {305097F3-A4E5-4511-8E4E-0C4C12A953C6}.Release|x64.Build.0 = Release|Any CPU 67 | {305097F3-A4E5-4511-8E4E-0C4C12A953C6}.Release|x86.ActiveCfg = Release|Any CPU 68 | {305097F3-A4E5-4511-8E4E-0C4C12A953C6}.Release|x86.Build.0 = Release|Any CPU 69 | EndGlobalSection 70 | GlobalSection(SolutionProperties) = preSolution 71 | HideSolutionNode = FALSE 72 | EndGlobalSection 73 | EndGlobal 74 | -------------------------------------------------------------------------------- /DotNut/P2PkBuilder.cs: -------------------------------------------------------------------------------- 1 | using System.Security.Cryptography; 2 | using NBitcoin.Secp256k1; 3 | 4 | namespace DotNut; 5 | 6 | public class P2PkBuilder 7 | { 8 | public DateTimeOffset? Lock { get; set; } 9 | public ECPubKey[]? RefundPubkeys { get; set; } 10 | public int SignatureThreshold { get; set; } = 1; 11 | 12 | public ECPubKey[] Pubkeys { get; set; } 13 | 14 | //SIG_INPUTS, SIG_ALL 15 | public string? SigFlag { get; set; } 16 | public string? Nonce { get; set; } 17 | 18 | public P2PKProofSecret Build() 19 | { 20 | var tags = new List(); 21 | if (Pubkeys.Length > 1) 22 | { 23 | tags.Add(new[] { "pubkeys" }.Concat(Pubkeys.Skip(1).Select(p => p.ToHex())).ToArray()); 24 | } 25 | 26 | if (!string.IsNullOrEmpty(SigFlag)) 27 | { 28 | tags.Add(new[] { "sigflag", SigFlag }); 29 | } 30 | 31 | if (Lock.HasValue) 32 | { 33 | tags.Add(new[] { "locktime", Lock.Value.ToUnixTimeSeconds().ToString() }); 34 | if (RefundPubkeys?.Any() is true) 35 | { 36 | tags.Add(new[] { "refund" }.Concat(RefundPubkeys.Select(p => p.ToHex())) 37 | .ToArray()); 38 | } 39 | } 40 | 41 | if (SignatureThreshold > 1 && Pubkeys.Length >= SignatureThreshold) 42 | { 43 | tags.Add(new[] { "n_sigs", SignatureThreshold.ToString() }); 44 | } 45 | 46 | 47 | return new P2PKProofSecret() 48 | { 49 | Data = Pubkeys.First().ToHex(), 50 | Nonce = Nonce ?? RandomNumberGenerator.GetHexString(32, true), 51 | Tags = tags.ToArray() 52 | }; 53 | } 54 | 55 | public static P2PkBuilder Load(P2PKProofSecret proofSecret) 56 | { 57 | var builder = new P2PkBuilder(); 58 | var primaryPubkey = proofSecret.Data.ToPubKey(); 59 | var pubkeys = proofSecret.Tags?.FirstOrDefault(strings => strings.FirstOrDefault() == "pubkeys"); 60 | if (pubkeys is not null && pubkeys.Length > 1) 61 | { 62 | builder.Pubkeys = pubkeys.Skip(1).Select(s => s.ToPubKey()).Prepend(primaryPubkey).ToArray(); 63 | } 64 | else 65 | { 66 | builder.Pubkeys = [primaryPubkey]; 67 | } 68 | 69 | var rawUnixTs = proofSecret.Tags?.FirstOrDefault(strings => strings.FirstOrDefault() == "locktime")?.Skip(1) 70 | ?.FirstOrDefault(); 71 | builder.Lock = rawUnixTs is not null && long.TryParse(rawUnixTs, out var unixTs) 72 | ? DateTimeOffset.FromUnixTimeSeconds(unixTs) 73 | : null; 74 | 75 | var refund = proofSecret.Tags?.FirstOrDefault(strings => strings.FirstOrDefault() == "refund"); 76 | if (refund is not null && refund.Length > 1) 77 | { 78 | builder.RefundPubkeys = refund.Skip(1).Select(s => s.ToPubKey()).ToArray(); 79 | } 80 | 81 | var sigFlag = proofSecret.Tags?.FirstOrDefault(strings => strings.FirstOrDefault() == "sigflag")?.Skip(1) 82 | ?.FirstOrDefault(); 83 | if (!string.IsNullOrEmpty(sigFlag)) 84 | { 85 | builder.SigFlag = sigFlag; 86 | } 87 | 88 | var nSigs = proofSecret.Tags?.FirstOrDefault(strings => strings.FirstOrDefault() == "n_sigs")?.Skip(1) 89 | ?.FirstOrDefault(); 90 | if (!string.IsNullOrEmpty(nSigs) && int.TryParse(nSigs, out var nSigsValue)) 91 | { 92 | builder.SignatureThreshold = nSigsValue; 93 | } 94 | 95 | builder.Nonce = proofSecret.Nonce; 96 | 97 | return builder; 98 | } 99 | 100 | 101 | /* 102 | * ========================= 103 | * NUT-XX Pay to blinded key 104 | * ========================= 105 | */ 106 | 107 | //For sig_inputs, generates random p2pk_e for each input 108 | public P2PKProofSecret BuildBlinded(KeysetId keysetId, out ECPubKey p2pkE) 109 | { 110 | var e = new PrivKey(RandomNumberGenerator.GetHexString(64)); 111 | p2pkE = e.Key.CreatePubKey(); 112 | return BuildBlinded(keysetId, e); 113 | } 114 | 115 | //For sig_all, p2pk_e must be provided 116 | public P2PKProofSecret BuildBlinded(KeysetId keysetId, ECPrivKey p2pke) 117 | { 118 | var pubkeys = RefundPubkeys != null ? Pubkeys.Concat(RefundPubkeys).ToArray() : Pubkeys; 119 | var rs = new List(); 120 | bool extraByte = false; 121 | 122 | var keysetIdBytes = keysetId.GetBytes(); 123 | 124 | var e = p2pke; 125 | 126 | for (int i = 0; i < pubkeys.Length; i++) 127 | { 128 | var Zx = Cashu.ComputeZx(e, pubkeys[i]); 129 | var Ri = Cashu.ComputeRi(Zx, keysetIdBytes, i); 130 | rs.Add(Ri); 131 | } 132 | _blindPubkeys(rs.ToArray()); 133 | return Build(); 134 | } 135 | 136 | private void _blindPubkeys(ECPrivKey[] rs) 137 | { 138 | var expectedLength = Pubkeys.Length + (RefundPubkeys?.Length ?? 0); 139 | if (expectedLength != rs.Length) 140 | { 141 | throw new ArgumentException("Invalid P2Pk blinding factors length"); 142 | } 143 | 144 | for (var i = 0; i < rs.Length; i++) 145 | { 146 | if (i < Pubkeys.Length) 147 | { 148 | Pubkeys[i] = Cashu.ComputeB_(Pubkeys[i], rs[i]); 149 | continue; 150 | } 151 | 152 | if (RefundPubkeys != null) 153 | { 154 | RefundPubkeys[i - Pubkeys.Length] = Cashu.ComputeB_(RefundPubkeys[i - Pubkeys.Length], rs[i]); 155 | } 156 | } 157 | } 158 | } -------------------------------------------------------------------------------- /DotNut.sln.DotSettings.user: -------------------------------------------------------------------------------- 1 | 2 | ForceIncluded 3 | ForceIncluded 4 | ForceIncluded 5 | ForceIncluded 6 | ForceIncluded 7 | ForceIncluded 8 | ForceIncluded 9 | ForceIncluded 10 | ForceIncluded 11 | ForceIncluded 12 | C:\Users\evilk\AppData\Local\JetBrains\Rider2024.2\resharper-host\temp\Rider\vAny\CoverageData\_DotNut.1481064820\Snapshot\snapshot.utdcvr 13 | 14 | <SessionState ContinuousTestingMode="0" Name="Test1" xmlns="urn:schemas-jetbrains-com:jetbrains-ut-session"> 15 | <TestAncestor> 16 | <TestId>xUnit::0AAAA85C-9FDC-4FD5-9CC2-ED8385B40106::net8.0::DotNut.Tests.UnitTest1.Nut00Tests_TokenSerialization</TestId> 17 | </TestAncestor> 18 | </SessionState> 19 | <SessionState ContinuousTestingMode="0" IsActive="True" Name="Nut11_Signatures" xmlns="urn:schemas-jetbrains-com:jetbrains-ut-session"> 20 | <Project Location="C:\Git\DotNuts\DotNut.Tests" Presentation="&lt;DotNut.Tests&gt;" /> 21 | </SessionState> -------------------------------------------------------------------------------- /DotNut.Demo/README.md: -------------------------------------------------------------------------------- 1 | # DotNut Demo Application 2 | 3 | A comprehensive interactive demo showcasing all features of the DotNut Cashu library. 4 | 5 | ## 🚀 Quick Start 6 | 7 | 1. **Clone and build the project:** 8 | ```bash 9 | git clone https://github.com/Kukks/DotNut.git 10 | cd DotNut 11 | dotnet build 12 | ``` 13 | 14 | 2. **Run the demo:** 15 | ```bash 16 | cd DotNut.Demo 17 | dotnet run 18 | ``` 19 | 20 | 3. **Follow the interactive menu to explore different features!** 21 | 22 | ## 📋 Available Demos 23 | 24 | ### 1. Connect to Mint & Get Info 25 | - Connects to a live Cashu mint (testnut.cashu.space) 26 | - Retrieves mint information, keysets, and public keys 27 | - Demonstrates basic API communication 28 | 29 | ### 2. Create Cashu Token 30 | - Creates example proofs and tokens 31 | - Shows token structure and properties 32 | - Demonstrates wallet management 33 | 34 | ### 3. Token Encoding/Decoding 35 | - Shows V3 (JSON) vs V4 (CBOR) encoding formats 36 | - Demonstrates URI format for sharing 37 | - Compares encoding efficiency 38 | - Shows decode and verification process 39 | 40 | ### 4. Lightning Mint Quote (Demo) 41 | - Creates real mint quotes from the test mint 42 | - Shows Lightning invoice generation 43 | - Explains the minting process flow 44 | - **Note:** Shows API usage with real responses 45 | 46 | ### 5. Lightning Melt Quote (Demo) 47 | - Demonstrates melt quote creation process 48 | - Shows expected API structure 49 | - Explains melting workflow 50 | - **Note:** Uses example invoice for demonstration 51 | 52 | ### 6. Token Swapping (Demo) 53 | - Explains the token swapping concept 54 | - Shows input/output structure 55 | - Demonstrates denomination management 56 | - **Note:** Educational walkthrough of the process 57 | 58 | ### 7. Working with Secrets 59 | - Simple string secrets 60 | - Random secret generation 61 | - Demonstrates hash-to-curve operations 62 | - Shows secret uniqueness properties 63 | 64 | ### 8. Mnemonic Secrets (NUT-13) 65 | - Generates BIP39 mnemonic phrases 66 | - Derives deterministic secrets and blinding factors 67 | - Shows counter-based secret derivation 68 | - Explains security considerations 69 | 70 | ### 9. P2PK Secrets (NUT-11) 71 | - Creates Pay-to-Public-Key secrets 72 | - Demonstrates multisignature setup 73 | - Shows time-locked spending conditions 74 | - Explains advanced spending scenarios 75 | 76 | ### 10. Show Current Wallet 77 | - Displays current proof inventory 78 | - Shows denomination breakdown 79 | - Demonstrates wallet state management 80 | 81 | ### 11. Check Proof States 82 | - Shows proof state checking API 83 | - Explains UNSPENT/SPENT/RESERVED states 84 | - Demonstrates proof validation 85 | 86 | ## 🧪 What's Educational vs Real 87 | 88 | ### Real Interactions 89 | - **Mint Connection**: Connects to actual testnet mint 90 | - **Mint Info/Keys**: Real data from the mint 91 | - **Lightning Mint Quotes**: Real Lightning invoices generated 92 | - **Token Encoding/Decoding**: Fully functional 93 | - **Cryptographic Operations**: All secret/key operations are real 94 | 95 | ### Educational Demos 96 | - **Token Creation**: Uses example proofs (not minted from actual operations) 97 | - **Melt Quotes**: Uses fake invoice for demonstration 98 | - **Swapping**: Explains process without actual mint interaction 99 | - **Proof States**: Uses example proofs (will show expected errors) 100 | 101 | ## 🔧 Technical Details 102 | 103 | ### Dependencies 104 | - **.NET 8.0**: Target framework 105 | - **DotNut**: Main Cashu library (project reference) 106 | - **NBitcoin.Secp256k1**: For cryptographic operations 107 | 108 | ### Architecture 109 | The demo is structured as an interactive console application with: 110 | - **Menu-driven interface**: Easy navigation between features 111 | - **Comprehensive error handling**: Shows both expected and unexpected errors 112 | - **Educational output**: Explains concepts and processes 113 | - **Real API integration**: Uses live testnet mint when possible 114 | 115 | ### Code Structure 116 | - `Program.cs`: Main application with all demo implementations 117 | - Interactive menu system for easy exploration 118 | - Modular demo methods for each feature 119 | - Helper methods for creating example data 120 | - Extension methods for utility functions 121 | 122 | ## 🎯 Learning Objectives 123 | 124 | After running through the demos, you'll understand: 125 | 126 | 1. **Basic Cashu Concepts**: Tokens, proofs, secrets, keysets 127 | 2. **API Usage**: How to interact with Cashu mints 128 | 3. **Token Management**: Creation, encoding, decoding, sharing 129 | 4. **Advanced Features**: P2PK, mnemonics, time-locks 130 | 5. **Cryptographic Foundations**: Hash-to-curve, blind signatures 131 | 6. **Real-world Workflows**: Mint → Send → Receive → Melt cycles 132 | 133 | ## 🛠️ Extending the Demo 134 | 135 | Want to add more features? Consider: 136 | 137 | - **Real Minting**: Implement actual Lightning payment and minting 138 | - **Token Persistence**: Save/load wallet state 139 | - **Multi-mint Support**: Connect to multiple mints 140 | - **HTLC Demos**: Hash Time-Locked Contract examples 141 | - **Nostr Integration**: Payment requests over Nostr 142 | 143 | ## 🤝 Contributing 144 | 145 | Found issues or want to improve the demo? Contributions are welcome: 146 | 147 | 1. Fork the repository 148 | 2. Create your feature branch 149 | 3. Add your demo or improvement 150 | 4. Submit a pull request 151 | 152 | ## 📚 Next Steps 153 | 154 | After exploring the demo: 155 | 156 | 1. **Read the main README**: Understand the full library capabilities 157 | 2. **Check the documentation**: Deep dive into specific features 158 | 3. **Try with real Lightning**: Test minting with actual payments 159 | 4. **Build your own app**: Use DotNut in your projects 160 | 5. **Join the community**: Connect with other Cashu developers 161 | 162 | ## ⚠️ Important Notes 163 | 164 | - **Testnet Only**: This demo uses testnet Bitcoin/Lightning 165 | - **Educational Purpose**: Not for production use without modifications 166 | - **API Keys**: No API keys required for the demo 167 | - **Internet Required**: Connects to external mint for live demos 168 | 169 | Happy exploring! 🥜✨ -------------------------------------------------------------------------------- /DotNut/Api/CashuHttpClient.cs: -------------------------------------------------------------------------------- 1 | using System.Net; 2 | using System.Net.Http.Json; 3 | using System.Text; 4 | using System.Text.Json; 5 | using DotNut.ApiModels; 6 | 7 | namespace DotNut.Api; 8 | 9 | public class CashuHttpClient : ICashuApi 10 | { 11 | private readonly HttpClient _httpClient; 12 | 13 | public CashuHttpClient(HttpClient httpClient) 14 | { 15 | _httpClient = httpClient; 16 | } 17 | 18 | public async Task GetKeys(CancellationToken cancellationToken = default) 19 | { 20 | var response = await _httpClient.GetAsync("v1/keys", cancellationToken); 21 | return await HandleResponse(response, cancellationToken); 22 | } 23 | 24 | public async Task GetKeysets(CancellationToken cancellationToken = default) 25 | { 26 | var response = await _httpClient.GetAsync("v1/keysets", cancellationToken); 27 | return await HandleResponse(response, cancellationToken); 28 | } 29 | 30 | public async Task GetKeys(KeysetId keysetId, CancellationToken cancellationToken = default) 31 | { 32 | var response = await _httpClient.GetAsync($"v1/keys/{keysetId}", cancellationToken); 33 | return await HandleResponse(response, cancellationToken); 34 | } 35 | 36 | public async Task Swap(PostSwapRequest request, CancellationToken cancellationToken = default) 37 | { 38 | var response = await _httpClient.PostAsync($"v1/swap", 39 | new StringContent(JsonSerializer.Serialize(request), Encoding.UTF8, "application/json"), cancellationToken); 40 | return await HandleResponse(response, cancellationToken); 41 | } 42 | 43 | public async Task CreateMintQuote(string method, TRequest request, CancellationToken 44 | cancellationToken = default) 45 | { 46 | var response = await _httpClient.PostAsync($"v1/mint/quote/{method}", 47 | new StringContent(JsonSerializer.Serialize(request), Encoding.UTF8, "application/json"), cancellationToken); 48 | return await HandleResponse(response, cancellationToken); 49 | } 50 | 51 | public async Task CreateMeltQuote(string method, TRequest request, CancellationToken 52 | cancellationToken = default) 53 | { 54 | var response = await _httpClient.PostAsync($"v1/melt/quote/{method}", 55 | new StringContent(JsonSerializer.Serialize(request), Encoding.UTF8, "application/json"), cancellationToken); 56 | return await HandleResponse(response, cancellationToken); 57 | } 58 | 59 | public async Task Melt(string method, TRequest request, CancellationToken 60 | cancellationToken = default) 61 | { 62 | var response = await _httpClient.PostAsync($"v1/melt/{method}", 63 | new StringContent(JsonSerializer.Serialize(request), Encoding.UTF8, "application/json"), cancellationToken); 64 | return await HandleResponse(response, cancellationToken); 65 | } 66 | 67 | public async Task CheckMeltQuote(string method, string quoteId, CancellationToken 68 | cancellationToken = default) 69 | { 70 | var response = await _httpClient.GetAsync($"v1/melt/quote/{method}/{quoteId}", cancellationToken); 71 | return await HandleResponse(response, cancellationToken); 72 | } 73 | 74 | public async Task CheckMintQuote(string method, string quoteId, CancellationToken 75 | cancellationToken = default) 76 | { 77 | var response = await _httpClient.GetAsync($"v1/mint/quote/{method}/{quoteId}", cancellationToken); 78 | return await HandleResponse(response, cancellationToken); 79 | } 80 | 81 | public async Task Mint(string method, TRequest request, 82 | CancellationToken cancellationToken = default) 83 | { 84 | var response = await _httpClient.PostAsync($"v1/mint/{method}", 85 | new StringContent(JsonSerializer.Serialize(request), Encoding.UTF8, "application/json"), cancellationToken); 86 | return await HandleResponse(response, cancellationToken); 87 | } 88 | 89 | public async Task CheckState(PostCheckStateRequest request, 90 | CancellationToken cancellationToken = default) 91 | { 92 | var response = await _httpClient.PostAsync($"v1/checkstate", 93 | new StringContent(JsonSerializer.Serialize(request), Encoding.UTF8, "application/json"), cancellationToken); 94 | return await HandleResponse(response, cancellationToken); 95 | } 96 | 97 | public async Task Restore(PostRestoreRequest request, 98 | CancellationToken cancellationToken = default) 99 | { 100 | var response = await _httpClient.PostAsync($"v1/restore", 101 | new StringContent(JsonSerializer.Serialize(request), Encoding.UTF8, "application/json"), cancellationToken); 102 | return await HandleResponse(response, cancellationToken); 103 | } 104 | 105 | public async Task GetInfo(CancellationToken cancellationToken = default) 106 | { 107 | var response = await _httpClient.GetAsync("v1/info", cancellationToken); 108 | return await HandleResponse(response, cancellationToken); 109 | } 110 | 111 | protected async Task HandleResponse(HttpResponseMessage response, CancellationToken cancellationToken) 112 | { 113 | if (response.StatusCode == HttpStatusCode.BadRequest) 114 | { 115 | var error = 116 | await response.Content.ReadFromJsonAsync(cancellationToken: cancellationToken); 117 | throw new CashuProtocolException(error); 118 | } 119 | 120 | response.EnsureSuccessStatusCode(); 121 | 122 | var result = await response.Content.ReadFromJsonAsync(cancellationToken); 123 | if (result is null) 124 | { 125 | var t = typeof(T); 126 | if (Nullable.GetUnderlyingType(t) != null) 127 | { 128 | return result!; 129 | } 130 | } 131 | 132 | return result!; 133 | } 134 | } -------------------------------------------------------------------------------- /DotNut/NBitcoin/BIP39/Mnemonic.cs: -------------------------------------------------------------------------------- 1 | #pragma warning disable CS0618 // Type or member is obsolete 2 | using System.Security.Cryptography; 3 | using System.Text; 4 | 5 | namespace DotNut.NBitcoin.BIP39 6 | { 7 | /// 8 | /// A .NET implementation of the Bitcoin Improvement Proposal - 39 (BIP39) 9 | /// BIP39 specification used as reference located here: https://github.com/bitcoin/bips/blob/master/bip-0039.mediawiki 10 | /// Made by thashiznets@yahoo.com.au 11 | /// v1.0.1.1 12 | /// I ♥ Bitcoin :) 13 | /// Bitcoin:1ETQjMkR1NNh4jwLuN5LxY7bMsHC9PUPSV 14 | /// 15 | public class Mnemonic 16 | { 17 | public Mnemonic(string mnemonic, Wordlist wordlist = null) 18 | { 19 | if (mnemonic == null) 20 | throw new ArgumentNullException(nameof(mnemonic)); 21 | _Mnemonic = mnemonic.Trim(); 22 | if (wordlist == null) 23 | wordlist = Wordlist.AutoDetect(mnemonic) ?? Wordlist.English; 24 | var words = mnemonic.Split((char[])null, StringSplitOptions.RemoveEmptyEntries); 25 | _Mnemonic = string.Join(wordlist.Space.ToString(), words); 26 | //if the sentence is not at least 12 characters or cleanly divisible by 3, it is bad! 27 | if (!CorrectWordCount(words.Length)) 28 | { 29 | throw new FormatException("Word count should be 12,15,18,21 or 24"); 30 | } 31 | _Words = words; 32 | _WordList = wordlist; 33 | _Indices = wordlist.ToIndices(words); 34 | } 35 | 36 | /// 37 | /// Generate a mnemonic 38 | /// 39 | /// 40 | /// 41 | public Mnemonic(Wordlist wordList, byte[] entropy = null) 42 | { 43 | wordList = wordList ?? Wordlist.English; 44 | _WordList = wordList; 45 | if (entropy == null) 46 | entropy = RandomNumberGenerator.GetBytes(32); 47 | 48 | var i = Array.IndexOf(entArray, entropy.Length * 8); 49 | if (i == -1) 50 | throw new ArgumentException("The length for entropy should be " + String.Join(",", entArray) + " bits", "entropy"); 51 | 52 | int cs = csArray[i]; 53 | byte[] checksum = SHA256.HashData(entropy); 54 | BitWriter entcsResult = new BitWriter(); 55 | 56 | entcsResult.Write(entropy); 57 | entcsResult.Write(checksum, cs); 58 | _Indices = entcsResult.ToIntegers(); 59 | _Words = _WordList.GetWords(_Indices); 60 | _Mnemonic = _WordList.GetSentence(_Indices); 61 | } 62 | 63 | public Mnemonic(Wordlist wordList, WordCount wordCount) 64 | : this(wordList, GenerateEntropy(wordCount)) 65 | { 66 | 67 | } 68 | 69 | private static byte[] GenerateEntropy(WordCount wordCount) 70 | { 71 | var ms = (int)wordCount; 72 | if (!CorrectWordCount(ms)) 73 | throw new ArgumentException("Word count should be 12,15,18,21 or 24", "wordCount"); 74 | int i = Array.IndexOf(msArray, (int)wordCount); 75 | return RandomNumberGenerator.GetBytes(entArray[i] / 8); 76 | } 77 | 78 | static readonly int[] msArray = new[] { 12, 15, 18, 21, 24 }; 79 | static readonly int[] csArray = new[] { 4, 5, 6, 7, 8 }; 80 | static readonly int[] entArray = new[] { 128, 160, 192, 224, 256 }; 81 | 82 | bool? _IsValidChecksum; 83 | public bool IsValidChecksum 84 | { 85 | get 86 | { 87 | if (_IsValidChecksum == null) 88 | { 89 | int i = Array.IndexOf(msArray, _Indices.Length); 90 | int cs = csArray[i]; 91 | int ent = entArray[i]; 92 | 93 | BitWriter writer = new BitWriter(); 94 | var bits = Wordlist.ToBits(_Indices); 95 | writer.Write(bits, ent); 96 | var entropy = writer.ToBytes(); 97 | var checksum = SHA256.HashData(entropy as byte[]); 98 | 99 | writer.Write(checksum, cs); 100 | var expectedIndices = writer.ToIntegers(); 101 | _IsValidChecksum = expectedIndices.SequenceEqual(_Indices); 102 | } 103 | return _IsValidChecksum.Value; 104 | } 105 | } 106 | 107 | private static bool CorrectWordCount(int ms) 108 | { 109 | return msArray.Any(_ => _ == ms); 110 | } 111 | 112 | private readonly Wordlist _WordList; 113 | public Wordlist WordList 114 | { 115 | get 116 | { 117 | return _WordList; 118 | } 119 | } 120 | 121 | private readonly int[] _Indices; 122 | public int[] Indices 123 | { 124 | get 125 | { 126 | return _Indices; 127 | } 128 | } 129 | private readonly string[] _Words; 130 | public string[] Words 131 | { 132 | get 133 | { 134 | return _Words; 135 | } 136 | } 137 | 138 | static Encoding NoBOMUTF8 = new UTF8Encoding(false); 139 | public byte[] DeriveSeed(string passphrase = null) 140 | { 141 | passphrase = passphrase ?? ""; 142 | var salt = Concat(NoBOMUTF8.GetBytes("mnemonic"), Normalize(passphrase)); 143 | var bytes = Normalize(_Mnemonic); 144 | 145 | using Rfc2898DeriveBytes derive = new Rfc2898DeriveBytes(bytes, salt, 2048, HashAlgorithmName.SHA512); 146 | return derive.GetBytes(64); 147 | } 148 | 149 | internal static byte[] Normalize(string str) 150 | { 151 | return NoBOMUTF8.GetBytes(NormalizeString(str)); 152 | } 153 | 154 | internal static string NormalizeString(string word) 155 | { 156 | if (!SupportOsNormalization()) 157 | { 158 | return KDTable.NormalizeKD(word); 159 | } 160 | else 161 | { 162 | return word.Normalize(NormalizationForm.FormKD); 163 | } 164 | 165 | } 166 | 167 | static bool? _SupportOSNormalization; 168 | internal static bool SupportOsNormalization() 169 | { 170 | if (_SupportOSNormalization == null) 171 | { 172 | var notNormalized = "あおぞら"; 173 | var normalized = "あおぞら"; 174 | if (notNormalized.Equals(normalized, StringComparison.Ordinal)) 175 | { 176 | _SupportOSNormalization = false; 177 | } 178 | else 179 | { 180 | try 181 | { 182 | _SupportOSNormalization = notNormalized.Normalize(NormalizationForm.FormKD).Equals(normalized, StringComparison.Ordinal); 183 | } 184 | catch { _SupportOSNormalization = false; } 185 | } 186 | } 187 | return _SupportOSNormalization.Value; 188 | } 189 | 190 | 191 | static Byte[] Concat(Byte[] source1, Byte[] source2) 192 | { 193 | //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 194 | Byte[] buffer = new Byte[source1.Length + source2.Length]; 195 | System.Buffer.BlockCopy(source1, 0, buffer, 0, source1.Length); 196 | System.Buffer.BlockCopy(source2, 0, buffer, source1.Length, source2.Length); 197 | 198 | return buffer; 199 | } 200 | 201 | 202 | string _Mnemonic; 203 | public override string ToString() 204 | { 205 | return _Mnemonic; 206 | } 207 | 208 | 209 | } 210 | } 211 | #pragma warning restore CS0618 // Type or member is obsolete 212 | -------------------------------------------------------------------------------- /DotNut/Cashu.cs: -------------------------------------------------------------------------------- 1 | using System.Numerics; 2 | using System.Text; 3 | using NBitcoin.Secp256k1; 4 | using SHA256 = System.Security.Cryptography.SHA256; 5 | 6 | namespace DotNut; 7 | 8 | public static class Cashu 9 | { 10 | private static readonly byte[] DOMAIN_SEPARATOR = "Secp256k1_HashToCurve_Cashu_"u8.ToArray(); 11 | 12 | private static readonly byte[] P2BK_PREFIX = "Cashu_P2BK_v1"u8.ToArray(); 13 | 14 | internal static readonly BigInteger N = 15 | BigInteger.Parse("115792089237316195423570985008687907852837564279074904382605163141518161494337"); 16 | public static ECPubKey MessageToCurve(string message) 17 | { 18 | var hash = Encoding.UTF8.GetBytes(message); 19 | return HashToCurve(hash); 20 | } 21 | 22 | public static ECPubKey HexToCurve(string hex) 23 | { 24 | var bytes = Convert.FromHexString(hex); 25 | return HashToCurve(bytes); 26 | } 27 | public static ECPubKey HashToCurve(byte[] x) 28 | { 29 | var msgHash = SHA256.HashData(Concat(DOMAIN_SEPARATOR, x)); 30 | for (uint counter = 0;; counter++) 31 | { 32 | var counterBytes = BitConverter.GetBytes(counter); 33 | var publicKeyBytes = Concat([0x02], SHA256.HashData(Concat(msgHash, counterBytes))); 34 | try 35 | { 36 | return ECPubKey.Create(publicKeyBytes); 37 | } 38 | catch (FormatException) 39 | { 40 | } 41 | } 42 | } 43 | 44 | public static GE ToGE(this Scalar scalar) 45 | { 46 | // Multiply the scalar by the generator point to get the group element 47 | GEJ gej = Context.Instance.EcMultGenContext.MultGen(scalar); 48 | return gej.ToGroupElement(); 49 | } 50 | 51 | public static ECPubKey ToPubkey(this Scalar scalar) 52 | { 53 | return new ECPubKey(scalar.ToGE(), Context.Instance); 54 | } 55 | 56 | public static ECPrivKey ToPrivateKey(this Scalar scalar) 57 | { 58 | return ECPrivKey.TryCreate(scalar, out var key) ? key : throw new InvalidOperationException(); 59 | } 60 | 61 | public static ECPubKey ToPubkey(this GEJ gej) 62 | { 63 | return new ECPubKey(gej.ToGroupElement(), Context.Instance); 64 | } 65 | 66 | public static ECPubKey ToPubkey(this GE ge) 67 | { 68 | return new ECPubKey(ge, Context.Instance); 69 | } 70 | 71 | public static ECPubKey ComputeB_(ECPubKey Y, ECPrivKey r) 72 | { 73 | //B_ = Y + rG 74 | return Y.Q.ToGroupElementJacobian().Add(r.CreatePubKey().Q).ToPubkey(); 75 | } 76 | 77 | public static ECPubKey ComputeC_(ECPubKey B_, ECPrivKey k) 78 | { 79 | //C_ = kB_ 80 | return (B_.Q * k.sec).ToPubkey(); 81 | } 82 | 83 | 84 | public static (ECPrivKey e, ECPrivKey s) ComputeProof(ECPubKey B_, ECPrivKey a, ECPrivKey p) 85 | { 86 | //C_ - rK = kY + krG - krG = kY = C 87 | var r1 = p.CreatePubKey(); 88 | var r2 = (B_.Q * p.sec).ToPubkey(); 89 | var C_ = ComputeC_(B_, a); 90 | var A = a.CreatePubKey(); 91 | 92 | var e = ComputeE(r1, r2, A, C_); 93 | var s = p.TweakAdd(a.TweakMul(e.ToBytes()).ToBytes()); 94 | return (e.ToPrivateKey(), s); 95 | } 96 | 97 | public static Scalar ComputeE(ECPubKey R1, ECPubKey R2, ECPubKey K, ECPubKey C_) 98 | { 99 | byte[] eBytes = Encoding.UTF8.GetBytes(string.Concat(new[] {R1, R2, K, C_}.Select(pk => pk.ToHex(false)))); 100 | return new Scalar(SHA256.HashData(eBytes)); 101 | } 102 | 103 | public static bool Verify(this Proof proof, ECPubKey A) 104 | { 105 | return VerifyProof(proof.Secret.ToCurve(),proof.DLEQ.R, proof.C, proof.DLEQ.E, proof.DLEQ.S, A); 106 | } 107 | public static bool Verify(this BlindSignature blindSig, ECPubKey A, ECPubKey B_) 108 | { 109 | return Cashu.VerifyProof(B_, blindSig.C_, blindSig.DLEQ.E, blindSig.DLEQ.S, A); 110 | } 111 | 112 | public static bool VerifyProof(ECPubKey B_, ECPubKey C_, ECPrivKey e, ECPrivKey s, ECPubKey A) 113 | { 114 | 115 | var r1 = s.CreatePubKey().Q.ToGroupElementJacobian().Add((A.Q * e.sec.Negate()).ToGroupElement()).ToPubkey(); 116 | var r2 = (B_.Q * s.sec).Add((C_.Q * e.sec.Negate()).ToGroupElement()).ToPubkey(); 117 | var e_ = ComputeE(r1, r2, A, C_); 118 | return e.sec.Equals(e_); 119 | } 120 | 121 | public static bool VerifyProof(ECPubKey Y, ECPrivKey r, ECPubKey C, ECPrivKey e, ECPrivKey s, ECPubKey A) 122 | { 123 | var C_ = C.Q.ToGroupElementJacobian().Add((A.Q * r.sec).ToGroupElement()).ToPubkey(); 124 | var B_ = Y.Q.ToGroupElementJacobian().Add(r.CreatePubKey().Q).ToPubkey(); 125 | return VerifyProof(B_, C_, e, s, A); 126 | } 127 | 128 | public static ECPubKey ComputeC(ECPubKey C_, ECPrivKey r, ECPubKey A) 129 | { 130 | //C_ - rA = C 131 | return C_.Q.ToGroupElementJacobian().Add((A.Q * r.sec).ToGroupElement().Negate()).ToPubkey(); 132 | } 133 | 134 | public static byte[] ComputeZx(ECPrivKey e, ECPubKey P) 135 | { 136 | var x = (e.sec * P.Q).ToGroupElement().x; 137 | if (!ECXOnlyPubKey.TryCreate(x, Context.Instance, out var xOnly)) 138 | { 139 | // should never happen 140 | throw new InvalidOperationException("Could not create xOnly pubkey"); 141 | } 142 | return xOnly.ToBytes(); 143 | } 144 | 145 | public static ECPrivKey ComputeRi(byte[] Zx, byte[] keysetId, int i) 146 | { 147 | byte[] hash; 148 | 149 | hash = SHA256.HashData(Concat(P2BK_PREFIX, Zx, keysetId, [(byte)(i & 0xFF)])); 150 | var hashValue = new BigInteger(hash); 151 | if (hashValue == 0 || hashValue.CompareTo(N) != -1) 152 | { 153 | hash = SHA256.HashData(Concat(P2BK_PREFIX, Zx, keysetId, [(byte)(i & 0xFF)], [0xff])); 154 | } 155 | return ECPrivKey.Create(hash); 156 | } 157 | 158 | 159 | private static byte[] Concat(params byte[][] arrays) 160 | { 161 | int totalLength = arrays.Sum(a => a?.Length ?? 0); 162 | var result = new byte[totalLength]; 163 | int offset = 0; 164 | 165 | foreach (var arr in arrays) 166 | { 167 | if (arr == null || arr.Length == 0) continue; 168 | Buffer.BlockCopy(arr, 0, result, offset, arr.Length); 169 | offset += arr.Length; 170 | } 171 | 172 | return result; 173 | } 174 | 175 | public static string ToHex(this ECPrivKey key) 176 | { 177 | return Convert.ToHexString(key.ToBytes()).ToLower(); 178 | } 179 | 180 | public static byte[] ToBytes(this ECPrivKey key) 181 | { 182 | Span output = stackalloc byte[32]; 183 | key.WriteToSpan(output); 184 | return output.ToArray(); 185 | } 186 | 187 | public static byte[] ToUncompressedBytes(this ECPubKey key) 188 | { 189 | Span output = stackalloc byte[65]; 190 | key.WriteToSpan(false, output, out _); 191 | return output.ToArray(); 192 | } 193 | public static string ToHex(this ECPubKey key, bool compressed = true) 194 | { 195 | return compressed ? Convert.ToHexString(key.ToBytes(true)).ToLower() : Convert.ToHexString(key.ToUncompressedBytes()).ToLower(); 196 | } 197 | public static string ToHex(this Scalar scalar) 198 | { 199 | return Convert.ToHexString(scalar.ToBytes()).ToLower(); 200 | } 201 | public static string ToHex(this SecpSchnorrSignature sig) 202 | { 203 | return Convert.ToHexString(sig.ToBytes()).ToLower(); 204 | } 205 | } -------------------------------------------------------------------------------- /DotNut/HTLCProofSecret.cs: -------------------------------------------------------------------------------- 1 | using System.Text; 2 | using System.Text.Json.Serialization; 3 | using NBitcoin.Secp256k1; 4 | using SHA256 = System.Security.Cryptography.SHA256; 5 | 6 | namespace DotNut; 7 | 8 | public class HTLCProofSecret : P2PKProofSecret 9 | { 10 | public const string Key = "HTLC"; 11 | 12 | [JsonIgnore] public HTLCBuilder Builder => HTLCBuilder.Load(this); 13 | 14 | public override ECPubKey[] GetAllowedPubkeys(out int requiredSignatures) 15 | { 16 | var builder = Builder; 17 | if (builder.Lock.HasValue && builder.Lock.Value.ToUnixTimeSeconds() < DateTimeOffset.Now.ToUnixTimeSeconds()) 18 | { 19 | requiredSignatures = Math.Min(builder.RefundPubkeys?.Length ?? 0, 1); 20 | return builder.RefundPubkeys ?? Array.Empty(); 21 | } 22 | 23 | requiredSignatures = builder.SignatureThreshold; 24 | return builder.Pubkeys; 25 | } 26 | 27 | 28 | 29 | public HTLCWitness GenerateWitness(Proof proof, ECPrivKey[] keys, string preimage) 30 | { 31 | return GenerateWitness(proof.Secret.GetBytes(), keys, Convert.FromHexString(preimage)); 32 | } 33 | 34 | public HTLCWitness GenerateWitness(BlindedMessage blindedMessage, ECPrivKey[] keys, string preimage) 35 | { 36 | return GenerateWitness(blindedMessage.B_.Key.ToBytes(), keys, Convert.FromHexString(preimage)); 37 | } 38 | 39 | public HTLCWitness GenerateWitness(byte[] msg, ECPrivKey[] keys, byte[] preimage) 40 | { 41 | var hash = SHA256.HashData(msg); 42 | return GenerateWitness(ECPrivKey.Create(hash), keys, preimage); 43 | } 44 | 45 | public HTLCWitness GenerateWitness(ECPrivKey hash, ECPrivKey[] keys, byte[] preimage) 46 | { 47 | if (!VerifyPreimage(preimage)) 48 | throw new InvalidOperationException("Invalid preimage"); 49 | var p2pkhWitness = base.GenerateWitness(hash, keys); 50 | return new HTLCWitness() 51 | { 52 | Signatures = p2pkhWitness.Signatures, 53 | Preimage = Convert.ToHexString(preimage) 54 | }; 55 | } 56 | 57 | 58 | 59 | public HTLCWitness GenerateBlindWitness(Proof proof, ECPrivKey[] keys, byte[] preimage, KeysetId keysetId) 60 | { 61 | throw new NotImplementedException(); 62 | } 63 | public HTLCWitness GenerateBlindWitness(Proof proof, ECPrivKey[] keys, byte[] preimage, KeysetId keysetId, 64 | ECPubKey P2PkE) 65 | { 66 | throw new NotImplementedException(); 67 | } 68 | 69 | public HTLCWitness GenerateBlindWitness(BlindedMessage message, ECPrivKey[] keys, byte[] preimage, KeysetId keysetId, ECPubKey P2PkE) 70 | { 71 | throw new NotImplementedException(); 72 | } 73 | 74 | public HTLCWitness GenerateBlindWitness(byte[] msg, ECPrivKey[] keys, byte[] preimage, KeysetId keysetId, 75 | ECPubKey P2PkE) 76 | { 77 | throw new NotImplementedException(); 78 | } 79 | 80 | public HTLCWitness GenerateBlindWitness(ECPrivKey hash, ECPrivKey[] keys, byte[] preimage, KeysetId keysetId, 81 | ECPubKey P2PkE) 82 | { 83 | throw new NotImplementedException(); 84 | } 85 | 86 | 87 | 88 | public bool VerifyPreimage(string preimage) 89 | { 90 | return Convert.FromHexString(Builder.HashLock).SequenceEqual(SHA256.HashData(Convert.FromHexString(preimage))); 91 | } 92 | 93 | public bool VerifyPreimage(byte[] preimage) 94 | { 95 | return Convert.FromHexString(Builder.HashLock).SequenceEqual(SHA256.HashData(preimage)); 96 | } 97 | 98 | public bool VerifyWitness(string message, HTLCWitness witness) 99 | { 100 | var hash = SHA256.HashData(Encoding.UTF8.GetBytes(message)); 101 | return VerifyWitnessHash(hash, witness); 102 | } 103 | 104 | public bool VerifyWitness(ISecret secret, HTLCWitness witness) 105 | { 106 | if (secret is not Nut10Secret {ProofSecret: HTLCProofSecret}) 107 | { 108 | return false; 109 | } 110 | 111 | return VerifyWitness(secret.GetBytes(), witness); 112 | } 113 | 114 | 115 | [Obsolete("Use GenerateWitness(Proof proof, ECPrivKey[] keys, string preimage)")] 116 | public override P2PKWitness GenerateWitness(Proof proof, ECPrivKey[] keys) 117 | { 118 | throw new InvalidOperationException("Use GenerateWitness(Proof proof, ECPrivKey[] keys, string preimage)"); 119 | } 120 | 121 | [Obsolete("Use GenerateWitness(byte[] msg, ECPrivKey[] keys, byte[] preimage)")] 122 | public override P2PKWitness GenerateWitness(BlindedMessage message, ECPrivKey[] keys) 123 | { 124 | throw new InvalidOperationException("Use GenerateWitness(BlindedMessage message, ECPrivKey[] keys, string preimage)"); 125 | } 126 | 127 | [Obsolete("Use GenerateWitness(byte[] msg, ECPrivKey[] keys, byte[] preimage)")] 128 | public override P2PKWitness GenerateWitness(byte[] msg, ECPrivKey[] keys) 129 | { 130 | throw new InvalidOperationException("Use GenerateWitness(byte[] msg, ECPrivKey[] keys, byte[] preimage)"); 131 | } 132 | 133 | 134 | [Obsolete("Use GenerateBlindWitness(Proof proof, ECPrivKey[] keys, byte[] preimage, KeysetId keysetId)")] 135 | public override P2PKWitness GenerateBlindWitness(Proof proof, ECPrivKey[] keys, KeysetId keysetId) 136 | { 137 | throw new InvalidOperationException(); 138 | } 139 | 140 | [Obsolete("Use GenerateBlindWitness(Proof proof, ECPrivKey[] keys, byte[] preimage, KeysetId keysetId, ECPubKey P2PkE)")] 141 | public override P2PKWitness GenerateBlindWitness(Proof proof, ECPrivKey[] keys, KeysetId keysetId, ECPubKey P2PkE) 142 | { 143 | throw new InvalidOperationException("Use GenerateBlindWitness(Proof proof, ECPrivKey[] keys, byte[] preimage, KeysetId keysetId, ECPubKey P2PkE)"); 144 | } 145 | 146 | [Obsolete("Use GenerateBlindWitness(BlindedMessage message, ECPrivKey[] keys, byte[] preimage, KeysetId keysetId, ECPubKey P2PkE)")] 147 | public override P2PKWitness GenerateBlindWitness(BlindedMessage message, ECPrivKey[] keys, KeysetId keysetId, 148 | ECPubKey P2PkE) 149 | { 150 | throw new InvalidOperationException("Use GenerateBlindWitness(BlindedMessage message, ECPrivKey[] keys, byte[] preimage, KeysetId keysetId, ECPubKey P2PkE)"); 151 | } 152 | 153 | [Obsolete("Use GenerateBlindWitness(byte[] msg, ECPrivKey[] keys, byte[] preimage, KeysetId keysetId, ECPubKey P2PkE)")] 154 | public override P2PKWitness GenerateBlindWitness(byte[] msg, ECPrivKey[] keys, KeysetId keysetId, ECPubKey P2PkE) 155 | { 156 | throw new InvalidOperationException("Use GenerateBlindWitness(byte[] msg, ECPrivKey[] keys, byte[] preimage, KeysetId keysetId, ECPubKey P2PkE)"); 157 | } 158 | 159 | [Obsolete("Use GenerateBlindWitness(ECPrivKey hash, ECPrivKey[] keys, byte[] preimage, KeysetId keysetId, ECPubKey P2PkE)")] 160 | public override P2PKWitness GenerateBlindWitness(ECPrivKey hash, ECPrivKey[] keys, KeysetId keysetId, 161 | ECPubKey P2PkE) 162 | { 163 | throw new InvalidOperationException("Use GenerateBlindWitness(ECPrivKey hash, ECPrivKey[] keys, byte[] preimage, KeysetId keysetId, ECPubKey P2PkE)"); 164 | } 165 | 166 | 167 | public override P2PKWitness GenerateWitness(ECPrivKey hash, ECPrivKey[] keys) 168 | { 169 | return base.GenerateWitness(hash, keys); 170 | } 171 | 172 | public override bool VerifyWitness(string message, P2PKWitness witness) 173 | { 174 | return base.VerifyWitness(message, witness); 175 | } 176 | 177 | public override bool VerifyWitness(ISecret secret, P2PKWitness witness) 178 | { 179 | return base.VerifyWitness(secret, witness); 180 | } 181 | 182 | public override bool VerifyWitness(byte[] message, P2PKWitness witness) 183 | { 184 | return base.VerifyWitness(message, witness); 185 | } 186 | public override bool VerifyWitnessHash(byte[] hash, P2PKWitness witness) 187 | { 188 | if (witness is not HTLCWitness htlcWitness) 189 | { 190 | return false; 191 | } 192 | if (!VerifyPreimage(htlcWitness.Preimage)) 193 | { 194 | return false; 195 | } 196 | 197 | return base.VerifyWitnessHash(hash, witness); 198 | } 199 | } -------------------------------------------------------------------------------- /DotNut/P2PKProofSecret.cs: -------------------------------------------------------------------------------- 1 | using System.Text; 2 | using System.Text.Json.Serialization; 3 | using NBitcoin.Secp256k1; 4 | using SHA256 = System.Security.Cryptography.SHA256; 5 | 6 | namespace DotNut; 7 | 8 | public class P2PKProofSecret : Nut10ProofSecret 9 | { 10 | public const string Key = "P2PK"; 11 | 12 | [JsonIgnore] P2PkBuilder Builder => P2PkBuilder.Load(this); 13 | 14 | public virtual ECPubKey[] GetAllowedPubkeys(out int requiredSignatures) 15 | { 16 | var builder = Builder; 17 | if (builder.Lock.HasValue && builder.Lock.Value.ToUnixTimeSeconds() < DateTimeOffset.Now.ToUnixTimeSeconds()) 18 | { 19 | requiredSignatures = Math.Min(builder.RefundPubkeys?.Length ?? 0, 1); 20 | return builder.RefundPubkeys ?? Array.Empty(); 21 | } 22 | 23 | requiredSignatures = builder.SignatureThreshold; 24 | return builder.Pubkeys; 25 | } 26 | 27 | 28 | public virtual P2PKWitness GenerateWitness(Proof proof, ECPrivKey[] keys) 29 | { 30 | return GenerateWitness(proof.Secret.GetBytes(), keys); 31 | } 32 | 33 | public virtual P2PKWitness GenerateWitness(BlindedMessage message, ECPrivKey[] keys) 34 | { 35 | return GenerateWitness(message.B_.Key.ToBytes(), keys); 36 | } 37 | 38 | public virtual P2PKWitness GenerateWitness(byte[] msg, ECPrivKey[] keys) 39 | { 40 | var hash = SHA256.HashData(msg); 41 | return GenerateWitness(ECPrivKey.Create(hash), keys); 42 | } 43 | 44 | public virtual P2PKWitness GenerateWitness(ECPrivKey hash, ECPrivKey[] keys) 45 | { 46 | var msg = hash.ToBytes(); 47 | //filter out keys that matter 48 | var allowedKeys = GetAllowedPubkeys(out var requiredSignatures); 49 | var keysRequiredLeft = requiredSignatures; 50 | var availableKeysLeft = keys; 51 | var result = new P2PKWitness(); 52 | while (keysRequiredLeft > 0 && availableKeysLeft.Any()) 53 | { 54 | var key = availableKeysLeft.First(); 55 | var pubkey = key.CreatePubKey(); 56 | var isAllowed = allowedKeys.Any(p => p == pubkey); 57 | if (isAllowed) 58 | { 59 | var sig = key.SignBIP340(msg); 60 | 61 | 62 | key.CreateXOnlyPubKey().SigVerifyBIP340(sig, msg); 63 | result.Signatures = result.Signatures.Append(sig.ToHex()).ToArray(); 64 | } 65 | 66 | availableKeysLeft = availableKeysLeft.Except(new[] {key}).ToArray(); 67 | keysRequiredLeft = requiredSignatures - result.Signatures.Length; 68 | } 69 | 70 | if (keysRequiredLeft > 0) 71 | throw new InvalidOperationException("Not enough valid keys to sign"); 72 | 73 | return result; 74 | } 75 | 76 | /* 77 | * ========================= 78 | * NUT-XX Pay to blinded key 79 | * ========================= 80 | */ 81 | 82 | public virtual P2PKWitness GenerateBlindWitness(Proof proof, ECPrivKey[] keys, KeysetId keysetId) 83 | { 84 | ArgumentNullException.ThrowIfNull(proof.P2PkE); 85 | return GenerateBlindWitness(proof.Secret.GetBytes(), keys, keysetId, proof.P2PkE); 86 | } 87 | 88 | public virtual P2PKWitness GenerateBlindWitness(Proof proof, ECPrivKey[] keys, KeysetId keysetId, ECPubKey P2PkE) 89 | { 90 | return GenerateBlindWitness(proof.Secret.GetBytes(), keys, keysetId, P2PkE); 91 | } 92 | 93 | public virtual P2PKWitness GenerateBlindWitness(BlindedMessage message, ECPrivKey[] keys, KeysetId keysetId, ECPubKey P2PkE) 94 | { 95 | return GenerateBlindWitness(message.B_.Key.ToBytes(), keys, keysetId, P2PkE); 96 | } 97 | 98 | public virtual P2PKWitness GenerateBlindWitness(byte[] msg, ECPrivKey[] keys, KeysetId keysetId, ECPubKey P2PkE) 99 | { 100 | var hash = SHA256.HashData(msg); 101 | return GenerateBlindWitness(ECPrivKey.Create(hash), keys, keysetId, P2PkE); 102 | } 103 | 104 | public virtual P2PKWitness GenerateBlindWitness(ECPrivKey hash, ECPrivKey[] keys, KeysetId keysetId, ECPubKey P2PkE) 105 | { 106 | var msg = hash.ToBytes(); 107 | var allowedKeys = GetAllowedPubkeys(out var requiredSignatures); 108 | var keysRequiredLeft = requiredSignatures; 109 | var availableKeysLeft = keys; 110 | var result = new P2PKWitness(); 111 | 112 | var keysetIdBytes = keysetId.GetBytes(); 113 | var pubkeysTotalCount = Builder.Pubkeys.Length + (Builder.RefundPubkeys?.Length ?? 0); 114 | 115 | HashSet usedSlots = new(); 116 | 117 | while (keysRequiredLeft > 0 && availableKeysLeft.Any()) 118 | { 119 | var key = availableKeysLeft.First(); 120 | var remainingKeys = availableKeysLeft.Skip(1).ToArray(); 121 | 122 | for (int i = 0; i < pubkeysTotalCount; i++) 123 | { 124 | if (usedSlots.Contains(i)) 125 | { 126 | continue; 127 | } 128 | 129 | var Zx = Cashu.ComputeZx(key, P2PkE); 130 | var ri = Cashu.ComputeRi(Zx, keysetIdBytes, i); 131 | 132 | var tweakedPrivkey = key.TweakAdd(ri.ToBytes()); 133 | var tweakedPubkey = tweakedPrivkey.CreatePubKey(); 134 | 135 | var tweakedPrivkeyNeg = key.sec.Negate().Add(ri.sec).ToPrivateKey(); 136 | var tweakedPubkeyNeg = tweakedPrivkeyNeg.CreatePubKey(); 137 | 138 | if (allowedKeys.Contains(tweakedPubkey)) 139 | { 140 | usedSlots.Add(i); 141 | var sig = tweakedPrivkey.SignBIP340(msg); 142 | tweakedPrivkey.CreateXOnlyPubKey().SigVerifyBIP340(sig, msg); 143 | result.Signatures = result.Signatures.Append(sig.ToHex()).ToArray(); 144 | keysRequiredLeft = requiredSignatures - result.Signatures.Length; 145 | break; 146 | } 147 | 148 | if (allowedKeys.Contains(tweakedPubkeyNeg)) 149 | { 150 | usedSlots.Add(i); 151 | var sig = tweakedPrivkeyNeg.SignBIP340(msg); 152 | tweakedPrivkeyNeg.CreateXOnlyPubKey().SigVerifyBIP340(sig, msg); 153 | result.Signatures = result.Signatures.Append(sig.ToHex()).ToArray(); 154 | keysRequiredLeft = requiredSignatures - result.Signatures.Length; 155 | break; 156 | 157 | } 158 | } 159 | availableKeysLeft = remainingKeys; 160 | } 161 | if (keysRequiredLeft > 0) 162 | throw new InvalidOperationException("Not enough valid keys to sign"); 163 | return result; 164 | } 165 | 166 | 167 | public virtual bool VerifyWitness(string message, P2PKWitness witness) 168 | { 169 | var hash = SHA256.HashData(Encoding.UTF8.GetBytes(message)); 170 | return VerifyWitnessHash(hash, witness); 171 | } 172 | 173 | public virtual bool VerifyWitness(ISecret secret, P2PKWitness witness) 174 | { 175 | return VerifyWitness(secret.GetBytes(), witness); 176 | } 177 | 178 | public virtual bool VerifyWitness(byte[] message, P2PKWitness witness) 179 | { 180 | var hash = SHA256.HashData(message); 181 | return VerifyWitnessHash(hash, witness); 182 | } 183 | 184 | public virtual bool VerifyWitnessHash(byte[] hash, P2PKWitness witness) 185 | { 186 | try 187 | { 188 | var allowedKeys = GetAllowedPubkeys(out var requiredSignatures); 189 | if (witness.Signatures.Length < requiredSignatures) 190 | return false; 191 | var sigs = witness.Signatures 192 | .Select(s => SecpSchnorrSignature.TryCreate(Convert.FromHexString(s), out var sig) ? sig : null) 193 | .Where(signature => signature is not null).ToArray(); 194 | return sigs.Count(s => allowedKeys.Any(p => p.ToXOnlyPubKey().SigVerifyBIP340(s, hash))) >= 195 | requiredSignatures; 196 | } 197 | catch (Exception e) 198 | { 199 | return false; 200 | } 201 | } 202 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # DotNut 🥜 2 | 3 | A complete C# implementation of the [Cashu protocol](https://cashu.space) - privacy-preserving electronic cash built on Bitcoin. 4 | 5 | [![NuGet](https://img.shields.io/nuget/v/DotNut.svg)](https://www.nuget.org/packages/DotNut/) 6 | [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) 7 | 8 | ## What is Cashu? 9 | 10 | 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. 11 | 12 | ## Installation 13 | 14 | ```bash 15 | dotnet add package DotNut 16 | ``` 17 | 18 | ## Quick Start 19 | 20 | ### 1. Connect to a Mint 21 | 22 | ```csharp 23 | using DotNut; 24 | using DotNut.Api; 25 | 26 | // Connect to a Cashu mint 27 | var httpClient = new HttpClient(); 28 | httpClient.BaseAddress = new Uri("https://testnut.cashu.space/"); 29 | var client = new CashuHttpClient(httpClient); 30 | 31 | // Get mint information 32 | var info = await client.GetInfo(); 33 | Console.WriteLine($"Connected to: {info.Name}"); 34 | ``` 35 | 36 | ### 2. Create and Send Tokens 37 | 38 | ```csharp 39 | using DotNut.Encoding; 40 | 41 | // Create a token from proofs (obtained from minting) 42 | var token = new CashuToken 43 | { 44 | Unit = "sat", 45 | Memo = "Payment for coffee ☕", 46 | Tokens = new List 47 | { 48 | new CashuToken.Token 49 | { 50 | Mint = "https://testnut.cashu.space", 51 | Proofs = myProofs // Your token proofs 52 | } 53 | } 54 | }; 55 | 56 | // Encode for sharing (creates a cashu token string) 57 | string encodedToken = token.Encode("B"); // V4 format (compact) 58 | Console.WriteLine($"Token to share: {encodedToken}"); 59 | 60 | // Receive and decode a token 61 | var receivedToken = CashuTokenHelper.Decode(encodedToken, out string version); 62 | Console.WriteLine($"Received {receivedToken.TotalAmount()} sats"); 63 | ``` 64 | 65 | ### 3. Basic Mint Operations 66 | 67 | ```csharp 68 | using DotNut.ApiModels.Mint; 69 | 70 | // Create a mint quote for 1000 sats via Lightning 71 | var mintQuote = await client.CreateMintQuote( 72 | "bolt11", 73 | new PostMintQuoteBolt11Request { Amount = 1000, Unit = "sat" } 74 | ); 75 | 76 | Console.WriteLine($"Pay this invoice: {mintQuote.Request}"); 77 | // After paying the Lightning invoice, mint your tokens... 78 | 79 | // Create a melt quote to convert tokens back to Lightning 80 | var meltQuote = await client.CreateMeltQuote( 81 | "bolt11", 82 | new PostMeltQuoteBolt11Request 83 | { 84 | Request = "lnbc1000n1...", // Lightning invoice to pay 85 | Unit = "sat" 86 | } 87 | ); 88 | ``` 89 | 90 | ## Core Concepts 91 | 92 | ### Tokens and Proofs 93 | - **CashuToken**: Container for one or more tokens from different mints 94 | - **Proof**: Cryptographic proof representing a specific amount 95 | - **Secret**: The secret behind each proof (can be simple strings or complex conditions) 96 | 97 | ### Privacy Features 98 | - **Blind Signatures**: Mint doesn't know which tokens belong to whom 99 | - **DLEQ Proofs**: Verify mint behavior without compromising privacy 100 | - **Token Swapping**: Change denominations while maintaining privacy 101 | 102 | ### Advanced Features 103 | - **P2PK (Pay-to-Public-Key)**: Multi-signature spending conditions 104 | - **HTLCs**: Hash Time-Locked Contracts for atomic swaps 105 | - **Deterministic Secrets**: Generate secrets from mnemonic phrases 106 | 107 | ## Working with Secrets 108 | 109 | ```csharp 110 | using DotNut; 111 | 112 | // Simple string secret 113 | var secret = new StringSecret("my-random-secret"); 114 | 115 | // Deterministic secret from mnemonic (NUT-13) 116 | var mnemonic = new Mnemonic("abandon abandon abandon..."); 117 | var deterministicSecret = mnemonic.DeriveSecret(keysetId, counter: 0); 118 | 119 | // Pay-to-Public-Key secret (NUT-11) 120 | var p2pkBuilder = new P2PkBuilder 121 | { 122 | Pubkeys = new[] { pubkey1, pubkey2 }, 123 | SignatureThreshold = 1, // 1-of-2 multisig 124 | SigFlag = "SIG_INPUTS" 125 | }; 126 | var p2pkSecret = new Nut10Secret(P2PKProofSecret.Key, p2pkBuilder.Build()); 127 | ``` 128 | 129 | ## Token Operations 130 | 131 | ```csharp 132 | // Check if proofs are still valid 133 | var stateRequest = new PostCheckStateRequest { Ys = proofs.Select(p => p.Y).ToArray() }; 134 | var stateResponse = await client.CheckState(stateRequest); 135 | 136 | // Swap tokens to different denominations 137 | var swapRequest = new PostSwapRequest 138 | { 139 | Inputs = inputProofs, 140 | Outputs = newBlindedMessages 141 | }; 142 | var swapResponse = await client.Swap(swapRequest); 143 | 144 | // Restore tokens from secrets (if you've lost proofs) 145 | var restoreRequest = new PostRestoreRequest { Outputs = blindedMessages }; 146 | var restoreResponse = await client.Restore(restoreRequest); 147 | ``` 148 | 149 | ## Token Encoding Formats 150 | 151 | DotNut supports multiple token encoding formats: 152 | 153 | ```csharp 154 | // V3 format (JSON-based) 155 | string v3Token = token.Encode("A"); 156 | 157 | // V4 format (CBOR-based, more compact) 158 | string v4Token = token.Encode("B"); 159 | 160 | // As URI for easy sharing 161 | string tokenUri = token.Encode("B", makeUri: true); 162 | // Result: "cashu:cashuB..." 163 | ``` 164 | 165 | ## Error Handling 166 | 167 | ```csharp 168 | try 169 | { 170 | var response = await client.Swap(swapRequest); 171 | } 172 | catch (CashuProtocolException ex) 173 | { 174 | Console.WriteLine($"Mint error: {ex.Error.Detail}"); 175 | Console.WriteLine($"Error code: {ex.Error.Code}"); 176 | } 177 | ``` 178 | 179 | ## Nostr Integration 180 | 181 | DotNut includes a separate package for Nostr integration: 182 | 183 | ```bash 184 | dotnet add package DotNut.Nostr 185 | ``` 186 | 187 | This enables payment requests over Nostr (NUT-18) and other Nostr-based features. 188 | 189 | ## Implemented Specifications 190 | 191 | Complete implementation of the [Cashu protocol specifications](https://github.com/cashubtc/nuts/): 192 | 193 | | NUT | Description | Status | 194 | |-----|-------------|--------| 195 | | [00](https://github.com/cashubtc/nuts/blob/main/00.md) | Cryptographic primitives | ✅ | 196 | | [01](https://github.com/cashubtc/nuts/blob/main/01.md) | Mint public key distribution | ✅ | 197 | | [02](https://github.com/cashubtc/nuts/blob/main/02.md) | Keysets and keyset IDs | ✅ | 198 | | [03](https://github.com/cashubtc/nuts/blob/main/03.md) | Swapping tokens | ✅ | 199 | | [04](https://github.com/cashubtc/nuts/blob/main/04.md) | Minting tokens | ✅ | 200 | | [05](https://github.com/cashubtc/nuts/blob/main/05.md) | Melting tokens | ✅ | 201 | | [06](https://github.com/cashubtc/nuts/blob/main/06.md) | Mint info | ✅ | 202 | | [07](https://github.com/cashubtc/nuts/blob/main/07.md) | Token state check | ✅ | 203 | | [08](https://github.com/cashubtc/nuts/blob/main/08.md) | Lightning fee return | ✅ | 204 | | [09](https://github.com/cashubtc/nuts/blob/main/09.md) | Token restoration | ✅ | 205 | | [10](https://github.com/cashubtc/nuts/blob/main/10.md) | Spending conditions | ✅ | 206 | | [11](https://github.com/cashubtc/nuts/blob/main/11.md) | Pay-to-Public-Key (P2PK) | ✅ | 207 | | [12](https://github.com/cashubtc/nuts/blob/main/12.md) | DLEQ proofs | ✅ | 208 | | [13](https://github.com/cashubtc/nuts/blob/main/13.md) | Deterministic secrets | ✅ | 209 | | [14](https://github.com/cashubtc/nuts/blob/main/14.md) | Hash Time-Locked Contracts | ✅ | 210 | | [15](https://github.com/cashubtc/nuts/blob/main/15.md) | Multipath payments | ✅ | 211 | | [18](https://github.com/cashubtc/nuts/blob/main/18.md) | Payment requests | ✅ | 212 | 213 | ## Requirements 214 | 215 | - .NET 8.0 or later 216 | - HTTP client for mint communication 217 | 218 | ## Contributing 219 | 220 | Contributions are welcome! Please feel free to submit issues, feature requests, or pull requests. 221 | 222 | ## License 223 | 224 | This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details. 225 | 226 | ## Resources 227 | 228 | - [Cashu Protocol](https://cashu.space) 229 | - [Cashu Specifications (NUTs)](https://github.com/cashubtc/nuts/) 230 | - [NuGet Package](https://www.nuget.org/packages/DotNut/) 231 | - [GitHub Repository](https://github.com/Kukks/DotNut) 232 | -------------------------------------------------------------------------------- /DotNut/NBitcoin/BIP39/Wordlist.cs: -------------------------------------------------------------------------------- 1 | using System.Collections; 2 | using System.Collections.ObjectModel; 3 | 4 | namespace DotNut.NBitcoin.BIP39 5 | { 6 | public class Wordlist 7 | { 8 | static Wordlist() 9 | { 10 | WordlistSource = new HardcodedWordlistSource(); 11 | } 12 | private static Wordlist _Japanese; 13 | public static Wordlist Japanese 14 | { 15 | get 16 | { 17 | if (_Japanese == null) 18 | _Japanese = LoadWordList(Language.Japanese).Result; 19 | return _Japanese; 20 | } 21 | } 22 | 23 | private static Wordlist _ChineseSimplified; 24 | public static Wordlist ChineseSimplified 25 | { 26 | get 27 | { 28 | if (_ChineseSimplified == null) 29 | _ChineseSimplified = LoadWordList(Language.ChineseSimplified).Result; 30 | return _ChineseSimplified; 31 | } 32 | } 33 | 34 | private static Wordlist _ChineseTraditional; 35 | public static Wordlist ChineseTraditional 36 | { 37 | get 38 | { 39 | if (_ChineseTraditional == null) 40 | _ChineseTraditional = LoadWordList(Language.ChineseTraditional).Result; 41 | return _ChineseTraditional; 42 | } 43 | } 44 | 45 | private static Wordlist _Spanish; 46 | public static Wordlist Spanish 47 | { 48 | get 49 | { 50 | if (_Spanish == null) 51 | _Spanish = LoadWordList(Language.Spanish).Result; 52 | return _Spanish; 53 | } 54 | } 55 | 56 | private static Wordlist _English; 57 | public static Wordlist English 58 | { 59 | get 60 | { 61 | if (_English == null) 62 | _English = LoadWordList(Language.English).Result; 63 | return _English; 64 | } 65 | } 66 | 67 | private static Wordlist _French; 68 | public static Wordlist French 69 | { 70 | get 71 | { 72 | if (_French == null) 73 | _French = LoadWordList(Language.French).Result; 74 | return _French; 75 | } 76 | } 77 | 78 | private static Wordlist _PortugueseBrazil; 79 | public static Wordlist PortugueseBrazil 80 | { 81 | get 82 | { 83 | if (_PortugueseBrazil == null) 84 | _PortugueseBrazil = LoadWordList(Language.PortugueseBrazil).Result; 85 | return _PortugueseBrazil; 86 | } 87 | } 88 | 89 | private static Wordlist _Czech; 90 | public static Wordlist Czech 91 | { 92 | get 93 | { 94 | if (_Czech == null) 95 | _Czech = LoadWordList(Language.Czech).Result; 96 | return _Czech; 97 | } 98 | } 99 | 100 | public static Task LoadWordList(Language language) 101 | { 102 | string name = GetLanguageFileName(language); 103 | return LoadWordList(name); 104 | } 105 | 106 | internal static string GetLanguageFileName(Language language) 107 | { 108 | string name = null; 109 | switch (language) 110 | { 111 | case Language.ChineseTraditional: 112 | name = "chinese_traditional"; 113 | break; 114 | case Language.ChineseSimplified: 115 | name = "chinese_simplified"; 116 | break; 117 | case Language.English: 118 | name = "english"; 119 | break; 120 | case Language.Japanese: 121 | name = "japanese"; 122 | break; 123 | case Language.Spanish: 124 | name = "spanish"; 125 | break; 126 | case Language.French: 127 | name = "french"; 128 | break; 129 | case Language.PortugueseBrazil: 130 | name = "portuguese_brazil"; 131 | break; 132 | case Language.Czech: 133 | name = "czech"; 134 | break; 135 | default: 136 | throw new NotSupportedException(language.ToString()); 137 | } 138 | return name; 139 | } 140 | 141 | static Dictionary _LoadedLists = new Dictionary(); 142 | public static async Task LoadWordList(string name) 143 | { 144 | if (name == null) 145 | throw new ArgumentNullException(nameof(name)); 146 | Wordlist result = null; 147 | lock (_LoadedLists) 148 | { 149 | _LoadedLists.TryGetValue(name, out result); 150 | } 151 | if (result != null) 152 | return await Task.FromResult(result).ConfigureAwait(false); 153 | 154 | 155 | if (WordlistSource == null) 156 | throw new InvalidOperationException("Wordlist.WordlistSource is not set, impossible to fetch word list."); 157 | result = await WordlistSource.Load(name).ConfigureAwait(false); 158 | if (result != null) 159 | lock (_LoadedLists) 160 | { 161 | _LoadedLists.Remove(name); 162 | _LoadedLists.Add(name, result); 163 | } 164 | return result; 165 | } 166 | 167 | public static IWordlistSource WordlistSource 168 | { 169 | get; 170 | set; 171 | } 172 | 173 | private String[] _words; 174 | 175 | /// 176 | /// Constructor used by inheritence only 177 | /// 178 | /// The words to be used in the wordlist 179 | public Wordlist(String[] words, char space, string name) 180 | { 181 | _words = words 182 | .Select(w => Mnemonic.NormalizeString(w)) 183 | .ToArray(); 184 | _Space = space; 185 | _Name = name; 186 | } 187 | 188 | private readonly string _Name; 189 | public string Name 190 | { 191 | get 192 | { 193 | return _Name; 194 | } 195 | } 196 | private readonly char _Space; 197 | public char Space 198 | { 199 | get 200 | { 201 | return _Space; 202 | } 203 | } 204 | 205 | /// 206 | /// Method to determine if word exists in word list, great for auto language detection 207 | /// 208 | /// The word to check for existence 209 | /// Exists (true/false) 210 | public bool WordExists(string word, out int index) 211 | { 212 | word = Mnemonic.NormalizeString(word); 213 | if (_words.Contains(word)) 214 | { 215 | index = Array.IndexOf(_words, word); 216 | return true; 217 | } 218 | 219 | //index -1 means word is not in wordlist 220 | index = -1; 221 | return false; 222 | } 223 | 224 | /// 225 | /// Returns a string containing the word at the specified index of the wordlist 226 | /// 227 | /// Index of word to return 228 | /// Word 229 | public string GetWordAtIndex(int index) 230 | { 231 | return _words[index]; 232 | } 233 | 234 | /// 235 | /// The number of all the words in the wordlist 236 | /// 237 | public int WordCount 238 | { 239 | get 240 | { 241 | return _words.Length; 242 | } 243 | } 244 | 245 | 246 | public static Task AutoDetectAsync(string sentence) 247 | { 248 | return LoadWordList(AutoDetectLanguage(sentence)); 249 | } 250 | public static Wordlist AutoDetect(string sentence) 251 | { 252 | return LoadWordList(AutoDetectLanguage(sentence)).Result; 253 | } 254 | public static Language AutoDetectLanguage(string[] words) 255 | { 256 | List languageCount = new List(new int[] { 0, 0, 0, 0, 0, 0, 0, 0 }); 257 | int index; 258 | 259 | foreach (string s in words) 260 | { 261 | if (Wordlist.English.WordExists(s, out index)) 262 | { 263 | //english is at 0 264 | languageCount[0]++; 265 | } 266 | 267 | if (Wordlist.Japanese.WordExists(s, out index)) 268 | { 269 | //japanese is at 1 270 | languageCount[1]++; 271 | } 272 | 273 | if (Wordlist.Spanish.WordExists(s, out index)) 274 | { 275 | //spanish is at 2 276 | languageCount[2]++; 277 | } 278 | 279 | if (Wordlist.ChineseSimplified.WordExists(s, out index)) 280 | { 281 | //chinese simplified is at 3 282 | languageCount[3]++; 283 | } 284 | 285 | if (Wordlist.ChineseTraditional.WordExists(s, out index) && !Wordlist.ChineseSimplified.WordExists(s, out index)) 286 | { 287 | //chinese traditional is at 4 288 | languageCount[4]++; 289 | } 290 | if (Wordlist.French.WordExists(s, out index)) 291 | { 292 | languageCount[5]++; 293 | } 294 | 295 | if (Wordlist.PortugueseBrazil.WordExists(s, out index)) 296 | { 297 | //portuguese_brazil is at 6 298 | languageCount[6]++; 299 | } 300 | 301 | if (Wordlist.Czech.WordExists(s, out index)) 302 | { 303 | //czech is at 7 304 | languageCount[7]++; 305 | } 306 | } 307 | 308 | //no hits found for any language unknown 309 | if (languageCount.Max() == 0) 310 | { 311 | return Language.Unknown; 312 | } 313 | 314 | if (languageCount.IndexOf(languageCount.Max()) == 0) 315 | { 316 | return Language.English; 317 | } 318 | else if (languageCount.IndexOf(languageCount.Max()) == 1) 319 | { 320 | return Language.Japanese; 321 | } 322 | else if (languageCount.IndexOf(languageCount.Max()) == 2) 323 | { 324 | return Language.Spanish; 325 | } 326 | else if (languageCount.IndexOf(languageCount.Max()) == 3) 327 | { 328 | if (languageCount[4] > 0) 329 | { 330 | //has traditional characters so not simplified but instead traditional 331 | return Language.ChineseTraditional; 332 | } 333 | 334 | return Language.ChineseSimplified; 335 | } 336 | else if (languageCount.IndexOf(languageCount.Max()) == 4) 337 | { 338 | return Language.ChineseTraditional; 339 | } 340 | else if (languageCount.IndexOf(languageCount.Max()) == 5) 341 | { 342 | return Language.French; 343 | } 344 | else if (languageCount.IndexOf(languageCount.Max()) == 6) 345 | { 346 | return Language.PortugueseBrazil; 347 | } 348 | else if (languageCount.IndexOf(languageCount.Max()) == 7) 349 | { 350 | return Language.Czech; 351 | } 352 | return Language.Unknown; 353 | } 354 | public static Language AutoDetectLanguage(string sentence) 355 | { 356 | string[] words = sentence.Split(new char[] { ' ', ' ' }); //normal space and JP space 357 | 358 | return AutoDetectLanguage(words); 359 | } 360 | 361 | public string[] Split(string mnemonic) 362 | { 363 | return mnemonic.Split(new char[] { Space }, StringSplitOptions.RemoveEmptyEntries); 364 | } 365 | 366 | public override string ToString() 367 | { 368 | return _Name; 369 | } 370 | 371 | public ReadOnlyCollection GetWords() 372 | { 373 | return new ReadOnlyCollection(_words); 374 | } 375 | 376 | public string[] GetWords(int[] indices) 377 | { 378 | return 379 | indices 380 | .Select(i => GetWordAtIndex(i)) 381 | .ToArray(); 382 | } 383 | 384 | public string GetSentence(int[] indices) 385 | { 386 | return String.Join(Space.ToString(), GetWords(indices)); 387 | 388 | } 389 | 390 | public int[] ToIndices(string[] words) 391 | { 392 | var indices = new int[words.Length]; 393 | for (int i = 0; i < words.Length; i++) 394 | { 395 | int idx = -1; 396 | 397 | if (!WordExists(words[i], out idx)) 398 | { 399 | throw new FormatException("Word " + words[i] + " is not in the wordlist for this language, cannot continue to rebuild entropy from wordlist"); 400 | } 401 | indices[i] = idx; 402 | } 403 | return indices; 404 | } 405 | 406 | public int[] ToIndices(string sentence) 407 | { 408 | return ToIndices(Split(sentence)); 409 | } 410 | 411 | public static BitArray ToBits(int[] values) 412 | { 413 | if (values.Any(v => v >= 2048)) 414 | throw new ArgumentException("values should be between 0 and 2048", "values"); 415 | BitArray result = new BitArray(values.Length * 11); 416 | int i = 0; 417 | foreach (var val in values) 418 | { 419 | for (int p = 0; p < 11; p++) 420 | { 421 | var v = (val & (1 << (10 - p))) != 0; 422 | result.Set(i, v); 423 | i++; 424 | } 425 | } 426 | return result; 427 | } 428 | public static int[] ToIntegers(BitArray bits) 429 | { 430 | return 431 | bits 432 | .OfType() 433 | .Select((v, i) => new 434 | { 435 | Group = i / 11, 436 | Value = v ? 1 << (10 - (i % 11)) : 0 437 | }) 438 | .GroupBy(_ => _.Group, _ => _.Value) 439 | .Select(g => g.Sum()) 440 | .ToArray(); 441 | } 442 | 443 | public BitArray ToBits(string sentence) 444 | { 445 | return ToBits(ToIndices(sentence)); 446 | } 447 | 448 | public string[] GetWords(string sentence) 449 | { 450 | return ToIndices(sentence).Select(i => GetWordAtIndex(i)).ToArray(); 451 | } 452 | } 453 | } 454 | -------------------------------------------------------------------------------- /DotNut/NBitcoin/BIP39/KDTable.cs: -------------------------------------------------------------------------------- 1 | using System.Text; 2 | 3 | namespace DotNut.NBitcoin.BIP39 4 | { 5 | class KDTable 6 | { 7 | public static string NormalizeKD(string str) 8 | { 9 | StringBuilder builder = new StringBuilder(str.Length); 10 | foreach (char c in str.ToCharArray()) 11 | { 12 | if (!Supported(c)) 13 | { 14 | throw new PlatformNotSupportedException("the input string can't be normalized on this platform"); 15 | } 16 | Substitute(c, builder); 17 | } 18 | return builder.ToString(); 19 | } 20 | 21 | private static void Substitute(char c, StringBuilder builder) 22 | { 23 | for (int i = 0; i < _SubstitutionTable.Length; i++) 24 | { 25 | var substituedChar = _SubstitutionTable[i]; 26 | if (substituedChar == c) 27 | { 28 | Substitute(i, builder); 29 | return; 30 | } 31 | if (substituedChar > c) 32 | break; 33 | while (_SubstitutionTable[i] != '\n') 34 | i++; 35 | } 36 | builder.Append(c); 37 | } 38 | 39 | private static void Substitute(int pos, StringBuilder builder) 40 | { 41 | for (int i = pos + 1; i < _SubstitutionTable.Length; i++) 42 | { 43 | if (_SubstitutionTable[i] == '\n') 44 | break; 45 | builder.Append(_SubstitutionTable[i]); 46 | } 47 | } 48 | 49 | private static bool Supported(char c) 50 | { 51 | return _SupportedChars.Any(r => r[0] <= c && c <= r[1]); 52 | } 53 | 54 | 55 | static int[][] _SupportedChars = new int[][]{ 56 | new[]{0,1000}, 57 | new[]{12352,12447}, 58 | new[]{12448,12543}, 59 | new[]{19968,40959}, 60 | new[]{13312,19967}, 61 | new[]{131072,173791}, 62 | new[]{63744,64255}, 63 | new[]{194560,195103}, 64 | new[]{13056,13311}, 65 | new[]{12288,12351}, 66 | new[]{65280,65535}, 67 | new[]{8192,8303}, 68 | new[]{8352,8399}, 69 | }; 70 | 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"; 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /DotNut/ProofSelector.cs: -------------------------------------------------------------------------------- 1 | using System.Diagnostics; 2 | 3 | namespace DotNut; 4 | 5 | public class SendResponse 6 | { 7 | public List Keep { get; set; } = new(); 8 | public List Send { get; set; } = new(); 9 | } 10 | 11 | // Borrowed from cashu-ts 12 | // see https://github.com/cashubtc/cashu-ts/pull/314 13 | public class ProofSelector 14 | { 15 | private class ProofWithFee 16 | { 17 | public Proof Proof { get; set; } 18 | public double ExFee { get; set; } 19 | public ulong PpkFee { get; set; } 20 | 21 | public ProofWithFee(Proof proof, double exFee, ulong ppkFee) 22 | { 23 | Proof = proof; 24 | ExFee = exFee; 25 | PpkFee = ppkFee; 26 | } 27 | } 28 | 29 | private class Timer 30 | { 31 | private readonly Stopwatch _stopwatch; 32 | 33 | public Timer() 34 | { 35 | _stopwatch = Stopwatch.StartNew(); 36 | } 37 | 38 | public long Elapsed() => _stopwatch.ElapsedMilliseconds; 39 | } 40 | 41 | private readonly Dictionary _keysetFees; 42 | 43 | /// 44 | /// Creates a new ProofSelector instance. 45 | /// 46 | /// Dictionary mapping keyset IDs to their per-proof-per-thousand fees 47 | /// Optional logger action for debug information 48 | public ProofSelector(Dictionary keysetFees) 49 | { 50 | _keysetFees = keysetFees ?? throw new ArgumentNullException(nameof(keysetFees)); 51 | } 52 | 53 | /// 54 | /// Gets the fee per thousand for a specific proof. 55 | /// 56 | /// The proof to get fee for 57 | /// Fee per thousand units 58 | private ulong GetProofFeePPK(Proof proof) 59 | { 60 | return _keysetFees.TryGetValue(proof.Id, out var fee) ? fee : 0; 61 | } 62 | 63 | public SendResponse SelectProofsToSend(List proofs, ulong amountToSend, bool includeFees = false) 64 | { 65 | // Init vars 66 | const int MAX_TRIALS = 60; // 40-80 is optimal (per RGLI paper) 67 | const double MAX_OVRPCT = 0; // Acceptable close match overage (percent) 68 | const ulong MAX_OVRAMT = 0; // Acceptable close match overage (absolute) 69 | const long MAX_TIMEMS = 1000; // Halt new trials if over time (in ms) 70 | const int MAX_P2SWAP = 5000; // Max number of Phase 2 improvement swaps 71 | const bool exactMatch = false; // Allows close match (> amountToSend + fee) 72 | 73 | var timer = new Timer(); // start the clock 74 | List? bestSubset = null; 75 | double bestDelta = double.PositiveInfinity; 76 | ulong bestAmount = 0; 77 | ulong bestFeePPK = 0; 78 | 79 | /* 80 | * Helper Functions. 81 | */ 82 | 83 | // Calculate net amount after fees 84 | double SumExFees(ulong amount, ulong feePPK) 85 | { 86 | return amount - (includeFees ? Math.Ceiling(feePPK / 1000.0) : 0); 87 | } 88 | 89 | // Shuffle array for randomization 90 | List ShuffleArray(IEnumerable array) 91 | { 92 | var shuffled = array.ToList(); 93 | var random = new Random(); 94 | for (int i = shuffled.Count - 1; i > 0; i--) 95 | { 96 | int j = random.Next(i + 1); 97 | (shuffled[i], shuffled[j]) = (shuffled[j], shuffled[i]); 98 | } 99 | return shuffled; 100 | } 101 | 102 | // Performs a binary search on a sorted (ascending) array of ProofWithFee objects by exFee. 103 | // If lessOrEqual=true, returns the rightmost index where exFee <= value 104 | // If lessOrEqual=false, returns the leftmost index where exFee >= value 105 | int? BinarySearchIndex(List arr, double value, bool lessOrEqual) 106 | { 107 | int left = 0, right = arr.Count - 1; 108 | int? result = null; 109 | 110 | while (left <= right) 111 | { 112 | int mid = (left + right) / 2; 113 | double midValue = arr[mid].ExFee; 114 | 115 | if (lessOrEqual ? midValue <= value : midValue >= value) 116 | { 117 | result = mid; 118 | if (lessOrEqual) 119 | left = mid + 1; 120 | else 121 | right = mid - 1; 122 | } 123 | else 124 | { 125 | if (lessOrEqual) 126 | right = mid - 1; 127 | else 128 | left = mid + 1; 129 | } 130 | } 131 | return lessOrEqual ? result : (left < arr.Count ? left : null); 132 | } 133 | 134 | // Insert into array of ProofWithFee objects sorted by exFee 135 | void InsertSorted(List arr, ProofWithFee obj) 136 | { 137 | double value = obj.ExFee; 138 | int left = 0, right = arr.Count; 139 | 140 | while (left < right) 141 | { 142 | int mid = (left + right) / 2; 143 | if (arr[mid].ExFee < value) 144 | left = mid + 1; 145 | else 146 | right = mid; 147 | } 148 | arr.Insert(left, obj); 149 | } 150 | 151 | // "Delta" is the excess over amountToSend including fees 152 | // plus a tiebreaker to favour lower PPK keysets 153 | // NB: Solutions under amountToSend are invalid (delta: Infinity) 154 | double CalculateDelta(ulong amount, ulong feePPK) 155 | { 156 | double netSum = SumExFees(amount, feePPK); 157 | if (netSum < amountToSend) 158 | return double.PositiveInfinity; // no good 159 | return amount + feePPK / 1000.0 - amountToSend; 160 | } 161 | 162 | /* 163 | * Pre-processing. 164 | */ 165 | ulong totalAmount = 0; 166 | ulong totalFeePPK = 0; 167 | var proofWithFees = proofs.Select(p => 168 | { 169 | ulong ppkfee = GetProofFeePPK(p); 170 | double exFee = includeFees ? p.Amount - ppkfee / 1000.0 : p.Amount; 171 | var obj = new ProofWithFee(p, exFee, ppkfee); 172 | 173 | // Sum all economical proofs (filtered below) 174 | if (!includeFees || exFee > 0) 175 | { 176 | totalAmount += p.Amount; 177 | totalFeePPK += ppkfee; 178 | } 179 | return obj; 180 | }).ToList(); 181 | 182 | // Filter uneconomical proofs (totals computed above) 183 | var spendableProofs = includeFees 184 | ? proofWithFees.Where(obj => obj.ExFee > 0).ToList() 185 | : proofWithFees; 186 | 187 | // Sort by exFee ascending 188 | spendableProofs.Sort((a, b) => a.ExFee.CompareTo(b.ExFee)); 189 | 190 | // Remove proofs too large to be useful and adjust totals 191 | // Exact Match: Keep proofs where exFee <= amountToSend 192 | // Close Match: Keep proofs where exFee <= nextBiggerExFee 193 | if (spendableProofs.Count > 0) 194 | { 195 | int endIndex; 196 | if (exactMatch) 197 | { 198 | var rightIndex = BinarySearchIndex(spendableProofs, amountToSend, true); 199 | endIndex = rightIndex != null ? rightIndex.Value + 1 : 0; 200 | } 201 | else 202 | { 203 | var biggerIndex = BinarySearchIndex(spendableProofs, amountToSend, false); 204 | if (biggerIndex != null) 205 | { 206 | double nextBiggerExFee = spendableProofs[biggerIndex.Value].ExFee; 207 | var rightIndex = BinarySearchIndex(spendableProofs, nextBiggerExFee, true); 208 | if (rightIndex == null) 209 | { 210 | throw new InvalidOperationException("Unexpected null rightIndex in binary search"); 211 | } 212 | endIndex = rightIndex.Value + 1; 213 | } 214 | else 215 | { 216 | // Keep all proofs if all exFee < amountToSend 217 | endIndex = spendableProofs.Count; 218 | } 219 | } 220 | 221 | // Adjust totals for removed proofs 222 | for (int i = endIndex; i < spendableProofs.Count; i++) 223 | { 224 | totalAmount -= spendableProofs[i].Proof.Amount; 225 | totalFeePPK -= spendableProofs[i].PpkFee; 226 | } 227 | spendableProofs = spendableProofs.Take(endIndex).ToList(); 228 | } 229 | 230 | // Validate using precomputed totals 231 | double totalNetSum = SumExFees(totalAmount, totalFeePPK); 232 | if (amountToSend <= 0 || amountToSend > totalNetSum) 233 | { 234 | return new SendResponse { Keep = proofs, Send = new List() }; 235 | } 236 | 237 | // Max acceptable amount for non-exact matches 238 | double maxOverAmount = Math.Min( 239 | Math.Ceiling(amountToSend * (1 + MAX_OVRPCT / 100)), 240 | Math.Min(amountToSend + MAX_OVRAMT, totalNetSum)); 241 | 242 | /* 243 | * RGLI algorithm: Runs multiple trials (up to MAX_TRIALS) Each trial starts with randomized 244 | * greedy subset (S) and then tries to improve that subset to get a valid solution. NOTE: Fees 245 | * are dynamic, based on number of proofs (PPK), so we perform all calculations based on net 246 | * amounts. 247 | */ 248 | for (int trial = 0; trial < MAX_TRIALS; trial++) 249 | { 250 | // PHASE 1: Randomized Greedy Selection 251 | // Add proofs up to amountToSend (after adjusting for fees) 252 | // for exact match or the first amount over target otherwise 253 | var S = new List(); 254 | ulong amount = 0; 255 | ulong feePPK = 0; 256 | 257 | foreach (var obj in ShuffleArray(spendableProofs)) 258 | { 259 | ulong newAmount = amount + obj.Proof.Amount; 260 | ulong newFeePPK = feePPK + obj.PpkFee; 261 | double netSum = SumExFees(newAmount, newFeePPK); 262 | 263 | if (exactMatch && netSum > amountToSend) 264 | break; 265 | 266 | S.Add(obj); 267 | amount = newAmount; 268 | feePPK = newFeePPK; 269 | 270 | if (netSum >= amountToSend) 271 | break; 272 | } 273 | 274 | // PHASE 2: Local Improvement 275 | // Examine all the amounts found in the first phase, and find the 276 | // amount not in the current solution (others), which would get us 277 | // closest to the amountToSend. 278 | 279 | // Calculate the "others" array (note: spendableProofs is sorted ASC) 280 | // Using set.Contains() for filtering gives faster lookups: O(n+m) 281 | // Using array.Contains() would be way slower: O(n*m) 282 | var selectedCs = S.Select(pwf => pwf.Proof.C).ToHashSet(); 283 | var others = spendableProofs.Where(obj => !selectedCs.Contains(obj.Proof.C)).ToList(); 284 | 285 | // Generate a random order for accessing the trial subset ('S') 286 | var indices = ShuffleArray(Enumerable.Range(0, S.Count)).Take(MAX_P2SWAP).ToList(); 287 | 288 | foreach (int i in indices) 289 | { 290 | // Exact or acceptable close match solution found? 291 | double netSum = SumExFees(amount, feePPK); 292 | if (Math.Abs(netSum - amountToSend) < 0.0001 || 293 | (!exactMatch && netSum >= amountToSend && netSum <= maxOverAmount)) 294 | { 295 | break; 296 | } 297 | 298 | // Get details for proof being replaced (objP), and temporarily 299 | // calculate the subset amount/fee with that proof removed. 300 | var objP = S[i]; 301 | ulong tempAmount = amount - objP.Proof.Amount; 302 | ulong tempFeePPK = feePPK - objP.PpkFee; 303 | double tempNetSum = SumExFees(tempAmount, tempFeePPK); 304 | double target = amountToSend - tempNetSum; 305 | 306 | // Find a better replacement proof (objQ) and swap it in 307 | // Exact match can only replace larger to close on the target 308 | // Close match can replace larger or smaller as needed, but will 309 | // not replace larger unless it closes on the target 310 | var qIndex = BinarySearchIndex(others, target, exactMatch); 311 | if (qIndex != null) 312 | { 313 | var objQ = others[qIndex.Value]; 314 | if (!exactMatch || objQ.ExFee > objP.ExFee) 315 | { 316 | if (target >= 0 || objQ.ExFee <= objP.ExFee) 317 | { 318 | S[i] = objQ; 319 | amount = tempAmount + objQ.Proof.Amount; 320 | feePPK = tempFeePPK + objQ.PpkFee; 321 | others.RemoveAt(qIndex.Value); 322 | InsertSorted(others, objP); 323 | } 324 | } 325 | } 326 | } 327 | 328 | // Update best solution 329 | double delta = CalculateDelta(amount, feePPK); 330 | if (delta < bestDelta) 331 | { 332 | 333 | bestSubset = S.OrderByDescending(a => a.ExFee).ToList(); // copy & sort 334 | bestDelta = delta; 335 | bestAmount = amount; 336 | bestFeePPK = feePPK; 337 | 338 | // "PHASE 3": Final check to make sure we haven't overpaid fees 339 | // and see if we can improve the solution. This is an adaptation 340 | // to the original RGLI, which helps us identify close match and 341 | // optimal fee solutions more consistently 342 | var tempS = bestSubset.ToList(); // copy 343 | while (tempS.Count > 1 && bestDelta > 0) 344 | { 345 | var objP = tempS.Last(); 346 | tempS.RemoveAt(tempS.Count - 1); 347 | 348 | ulong tempAmount2 = amount - objP.Proof.Amount; 349 | ulong tempFeePPK2 = feePPK - objP.PpkFee; 350 | double tempDelta = CalculateDelta(tempAmount2, tempFeePPK2); 351 | 352 | if (double.IsPositiveInfinity(tempDelta)) 353 | break; 354 | 355 | if (tempDelta < bestDelta) 356 | { 357 | bestSubset = tempS.ToList(); 358 | bestDelta = tempDelta; 359 | bestAmount = tempAmount2; 360 | bestFeePPK = tempFeePPK2; 361 | amount = tempAmount2; 362 | feePPK = tempFeePPK2; 363 | } 364 | } 365 | } 366 | 367 | // Check if solution is acceptable 368 | if (bestSubset != null && !double.IsPositiveInfinity(bestDelta)) 369 | { 370 | double bestSum = SumExFees(bestAmount, bestFeePPK); 371 | if (Math.Abs(bestSum - amountToSend) < 0.0001 || 372 | (!exactMatch && bestSum >= amountToSend && bestSum <= maxOverAmount)) 373 | { 374 | break; 375 | } 376 | } 377 | 378 | // Time limit reached? 379 | if (timer.Elapsed() > MAX_TIMEMS) 380 | { 381 | if (exactMatch) 382 | { 383 | throw new TimeoutException("Proof selection took too long. Try again with a smaller proof set."); 384 | } 385 | else 386 | { 387 | break; 388 | } 389 | } 390 | } 391 | 392 | // Return Result 393 | if (bestSubset != null && !double.IsPositiveInfinity(bestDelta)) 394 | { 395 | var bestProofs = bestSubset.Select(obj => obj.Proof).ToList(); 396 | var bestProofCs = bestProofs.Select(p => p.C).ToHashSet(); 397 | var keep = proofs.Where(p => !bestProofCs.Contains(p.C)).ToList(); 398 | 399 | return new SendResponse { Keep = keep, Send = bestProofs }; 400 | } 401 | 402 | return new SendResponse { Keep = proofs, Send = new List() }; 403 | } 404 | } 405 | --------------------------------------------------------------------------------