diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index fd8dfba..cc52ad9 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -2,6 +2,37 @@ on: push jobs: + tests: + runs-on: ubuntu-latest + steps: + # Checkout the code + - uses: actions/checkout@v5 + + # Install .NET Core SDK + - name: Setup .NET Core + uses: actions/setup-dotnet@v4 + with: + dotnet-version: 8.0.x + + - name: Pull and start mint + run: | + docker run -d -p 3338:3338 \ + --name cdk-mint \ + -e CDK_MINTD_DATABASE=sqlite \ + -e CDK_MINTD_LN_BACKEND=fakewallet \ + -e CDK_MINTD_INPUT_FEE_PPK=100 \ + -e CDK_MINTD_LISTEN_HOST=0.0.0.0 \ + -e CDK_MINTD_LISTEN_PORT=3338 \ + -e CDK_MINTD_FAKE_WALLET_MIN_DELAY=0 \ + -e CDK_MINTD_MNEMONIC='abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about' \ + cashubtc/mintd:latest-amd64 + + - name: Wait for mint to be ready + run: | + timeout 60s bash -c 'until curl -f localhost:3338/v1/info; do sleep 2; done' + + - name: Test + run: dotnet test build: runs-on: ubuntu-latest steps: @@ -13,9 +44,6 @@ jobs: uses: actions/setup-dotnet@v1 with: dotnet-version: 8.0.x - - - name: Test - run: dotnet test - name: Publish NuGet if: ${{ github.ref == 'refs/heads/master' }} # Publish only when the push is on master diff --git a/.gitignore b/.gitignore index cddd443..62e59ab 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,5 @@ **/bin **/obj .idea -.idea -.vs \ No newline at end of file +.vs +*.DotSettings.user \ No newline at end of file diff --git a/DotNut.Demo/DotNut.Demo.csproj b/DotNut.Demo/DotNut.Demo.csproj index bb84a1f..232ca1c 100644 --- a/DotNut.Demo/DotNut.Demo.csproj +++ b/DotNut.Demo/DotNut.Demo.csproj @@ -1,5 +1,4 @@  - Exe net8.0 @@ -10,5 +9,4 @@ - diff --git a/DotNut.Demo/Program.cs b/DotNut.Demo/Program.cs index 9dd99ee..290ec2a 100644 --- a/DotNut.Demo/Program.cs +++ b/DotNut.Demo/Program.cs @@ -1,9 +1,9 @@ +using System.Security.Cryptography; using DotNut.Api; using DotNut.ApiModels; -using NBitcoin.Secp256k1; -using System.Security.Cryptography; using DotNut.NBitcoin.BIP39; using DotNut.NUT13; +using NBitcoin.Secp256k1; namespace DotNut.Demo; @@ -12,20 +12,20 @@ class Program private static readonly string DefaultMintUrl = "https://testnut.cashu.space"; private static CashuHttpClient? _client; private static List _wallet = new(); - + static async Task Main(string[] args) { Console.WriteLine("🥜 DotNut - Cashu Library Demo"); Console.WriteLine("=============================="); Console.WriteLine(); - + await InitializeMint(); - + while (true) { ShowMenu(); var choice = Console.ReadLine(); - + try { switch (choice?.ToLower()) @@ -81,13 +81,13 @@ static async Task Main(string[] args) Console.WriteLine($" Detail: {cashuEx.Error.Detail}"); } } - + Console.WriteLine("\nPress any key to continue..."); Console.Read(); Console.Clear(); } } - + private static void ShowMenu() { Console.WriteLine("📋 Available Demos:"); @@ -106,7 +106,7 @@ private static void ShowMenu() Console.WriteLine(); Console.Write("Choose an option: "); } - + private static async Task InitializeMint() { try @@ -114,7 +114,7 @@ private static async Task InitializeMint() var httpClient = new HttpClient(); httpClient.BaseAddress = new Uri(DefaultMintUrl); _client = new CashuHttpClient(httpClient); - + Console.WriteLine($"🔗 Initialized connection to: {DefaultMintUrl}"); } catch (Exception ex) @@ -122,18 +122,18 @@ private static async Task InitializeMint() Console.WriteLine($"❌ Failed to initialize mint connection: {ex.Message}"); } } - + private static async Task ConnectToMintDemo() { Console.WriteLine("🔗 Connect to Mint & Get Info Demo"); Console.WriteLine("=================================="); - + if (_client == null) { Console.WriteLine("❌ Client not initialized"); return; } - + try { // Get mint information @@ -141,24 +141,26 @@ private static async Task ConnectToMintDemo() Console.WriteLine($"✅ Connected to mint: {info.Name}"); Console.WriteLine($" Description: {info.Description}"); Console.WriteLine($" Version: {info.Version}"); - Console.WriteLine($" Contact: {string.Join(", ", info.Contact?.Select(c => $"{c.Method}: {c.Info}") ?? new[] { "N/A" })}"); - + Console.WriteLine( + $" Contact: {string.Join(", ", info.Contact?.Select(c => $"{c.Method}: {c.Info}") ?? new[] { "N/A" })}" + ); + // Get available keysets var keysets = await _client.GetKeysets(); Console.WriteLine($" Available keysets: {keysets.Keysets.Length}"); - + foreach (var keyset in keysets.Keysets.Take(3)) { Console.WriteLine($" - {keyset.Id} ({keyset.Unit}) [{keyset.Active}]"); } - + // Get keys for the first active keyset var activeKeyset = keysets.Keysets.FirstOrDefault(k => k.Active); if (activeKeyset != null) { var keys = await _client.GetKeys(activeKeyset.Id); Console.WriteLine($" Keys in active keyset ({activeKeyset.Id}):"); - + foreach (var key in keys.Keysets.First().Keys.Take(5)) { Console.WriteLine($" - Amount {key.Key}: {key.Value}"); @@ -170,16 +172,16 @@ private static async Task ConnectToMintDemo() Console.WriteLine($"❌ Failed to connect to mint: {ex.Message}"); } } - + private static async Task TokenCreationDemo() { Console.WriteLine("🪙 Token Creation Demo"); Console.WriteLine("======================"); - + // Create some example proofs for demonstration var proofs = CreateExampleProofs(); _wallet.AddRange(proofs); - + // Create a token var token = new CashuToken { @@ -187,19 +189,15 @@ private static async Task TokenCreationDemo() Memo = "Demo payment - Coffee ☕", Tokens = new List { - new CashuToken.Token - { - Mint = DefaultMintUrl, - Proofs = proofs - } - } + new CashuToken.Token { Mint = DefaultMintUrl, Proofs = proofs }, + }, }; - + Console.WriteLine($"✅ Created token with {proofs.Count} proofs"); Console.WriteLine($" Total amount: {token.TotalAmount()} sats"); Console.WriteLine($" Memo: {token.Memo}"); Console.WriteLine($" Mint: {token.Tokens.First().Mint}"); - + // Show proof details Console.WriteLine(" Proofs:"); foreach (var proof in proofs) @@ -207,12 +205,12 @@ private static async Task TokenCreationDemo() Console.WriteLine($" - {proof.Amount} sats (ID: {proof.Id})"); } } - + private static async Task TokenEncodingDemo() { Console.WriteLine("🔄 Token Encoding/Decoding Demo"); Console.WriteLine("==============================="); - + var proofs = CreateExampleProofs(); var token = new CashuToken { @@ -220,31 +218,27 @@ private static async Task TokenEncodingDemo() Memo = "Encoding demo token", Tokens = new List { - new CashuToken.Token - { - Mint = DefaultMintUrl, - Proofs = proofs - } - } + new CashuToken.Token { Mint = DefaultMintUrl, Proofs = proofs }, + }, }; - + // V3 Encoding (JSON-based) Console.WriteLine("📝 V3 Encoding (JSON-based):"); var v3Token = token.Encode("A"); Console.WriteLine($" Length: {v3Token.Length} characters"); Console.WriteLine($" Token: {v3Token.Substring(0, Math.Min(80, v3Token.Length))}..."); - + // V4 Encoding (CBOR-based, more compact) Console.WriteLine("\n📦 V4 Encoding (CBOR-based, compact):"); var v4Token = token.Encode("B"); Console.WriteLine($" Length: {v4Token.Length} characters"); Console.WriteLine($" Token: {v4Token.Substring(0, Math.Min(80, v4Token.Length))}..."); - + // URI format Console.WriteLine("\n🔗 URI Format:"); var uriToken = token.Encode("B", makeUri: true); Console.WriteLine($" URI: {uriToken.Substring(0, Math.Min(80, uriToken.Length))}..."); - + // Decode and verify Console.WriteLine("\n🔍 Decoding V4 token:"); var decoded = CashuTokenHelper.Decode(v4Token, out string version); @@ -252,47 +246,57 @@ private static async Task TokenEncodingDemo() Console.WriteLine($" Amount: {decoded.TotalAmount()} sats"); Console.WriteLine($" Memo: {decoded.Memo}"); Console.WriteLine($" Proofs: {decoded.Tokens.First().Proofs.Count}"); - - Console.WriteLine($"\n💾 Space savings: V4 is {((double)(v3Token.Length - v4Token.Length) / v3Token.Length * 100):F1}% smaller than V3"); + + Console.WriteLine( + $"\n💾 Space savings: V4 is {((double)(v3Token.Length - v4Token.Length) / v3Token.Length * 100):F1}% smaller than V3" + ); } - + private static async Task LightningMintDemo() { Console.WriteLine("⚡ Lightning Mint Quote Demo"); Console.WriteLine("============================"); - Console.WriteLine("ℹ️ This demo shows how to create mint quotes - actual minting requires paying a real Lightning invoice"); - + Console.WriteLine( + "ℹ️ This demo shows how to create mint quotes - actual minting requires paying a real Lightning invoice" + ); + if (_client == null) { Console.WriteLine("❌ Client not initialized"); return; } - + try { // Create mint quote var mintRequest = new PostMintQuoteBolt11Request { Amount = 1000, // 1000 sats - Unit = "sat" + Unit = "sat", }; - - var mintQuote = await _client.CreateMintQuote( - "bolt11", mintRequest); - + + var mintQuote = await _client.CreateMintQuote< + PostMintQuoteBolt11Response, + PostMintQuoteBolt11Request + >("bolt11", mintRequest); + Console.WriteLine("✅ Mint quote created successfully!"); Console.WriteLine($" Quote ID: {mintQuote.Quote}"); Console.WriteLine($" Amount: {mintQuote.Amount} {mintRequest.Unit}"); Console.WriteLine($" Unit: {mintQuote.Unit ?? mintRequest.Unit}"); - Console.WriteLine($" Expiry: {DateTimeOffset.FromUnixTimeSeconds(mintQuote.Expiry ?? 0).UtcDateTime}"); + Console.WriteLine( + $" Expiry: {DateTimeOffset.FromUnixTimeSeconds(mintQuote.Expiry ?? 0).UtcDateTime}" + ); Console.WriteLine($" State: {mintQuote.State}"); Console.WriteLine("\n📄 Lightning Invoice:"); Console.WriteLine($" {mintQuote.Request}"); - + Console.WriteLine("\n💡 To complete minting:"); Console.WriteLine(" 1. Pay the Lightning invoice above"); Console.WriteLine(" 2. Create blinded messages for desired denominations"); - Console.WriteLine(" 3. Call the mint endpoint with the quote ID and blinded messages"); + Console.WriteLine( + " 3. Call the mint endpoint with the quote ID and blinded messages" + ); Console.WriteLine(" 4. Unblind the returned signatures to get your proofs"); } catch (Exception ex) @@ -300,44 +304,51 @@ private static async Task LightningMintDemo() Console.WriteLine($"❌ Failed to create mint quote: {ex.Message}"); } } - + private static async Task LightningMeltDemo() { Console.WriteLine("⚡ Lightning Melt Quote Demo"); Console.WriteLine("============================"); - Console.WriteLine("ℹ️ This demo shows how to create melt quotes - actual melting requires valid proofs"); - + Console.WriteLine( + "ℹ️ This demo shows how to create melt quotes - actual melting requires valid proofs" + ); + if (_client == null) { Console.WriteLine("❌ Client not initialized"); return; } - + // Example Lightning invoice (fake for demo purposes) - var exampleInvoice = "lnbc10n1pj9x8x8pp5k6k6k6k6k6k6k6k6k6k6k6k6k6k6k6k6k6k6k6k6k6k6k6k6k6qdqqcqzpgxqyz5vqsp5example"; - + var exampleInvoice = + "lnbc10n1pj9x8x8pp5k6k6k6k6k6k6k6k6k6k6k6k6k6k6k6k6k6k6k6k6k6k6k6k6k6qdqqcqzpgxqyz5vqsp5example"; + try { var meltRequest = new PostMeltQuoteBolt11Request { Request = exampleInvoice, - Unit = "sat" + Unit = "sat", }; - + // Note: This will likely fail with the example invoice, but shows the API usage Console.WriteLine("📤 Attempting to create melt quote..."); Console.WriteLine($" Invoice: {exampleInvoice.Substring(0, 50)}..."); - - var meltQuote = await _client.CreateMeltQuote( - "bolt11", meltRequest); - + + var meltQuote = await _client.CreateMeltQuote< + PostMeltQuoteBolt11Response, + PostMeltQuoteBolt11Request + >("bolt11", meltRequest); + Console.WriteLine("✅ Melt quote created successfully!"); Console.WriteLine($" Quote ID: {meltQuote.Quote}"); Console.WriteLine($" Amount: {meltQuote.Amount} {meltRequest.Unit}"); Console.WriteLine($" Fee reserve: {meltQuote.FeeReserve} {meltRequest.Unit}"); - Console.WriteLine($" Expiry: {DateTimeOffset.FromUnixTimeSeconds(meltQuote.Expiry ?? 0).UtcDateTime}"); + Console.WriteLine( + $" Expiry: {DateTimeOffset.FromUnixTimeSeconds(meltQuote.Expiry ?? 0).UtcDateTime}" + ); Console.WriteLine($" State: {meltQuote.State}"); - + Console.WriteLine("\n💡 To complete melting:"); Console.WriteLine(" 1. Provide proofs with sufficient value (amount + fee)"); Console.WriteLine(" 2. Call the melt endpoint with quote ID and proofs"); @@ -352,58 +363,64 @@ private static async Task LightningMeltDemo() Console.WriteLine(" 3. Ensure you have sufficient proofs in your wallet"); } } - + private static async Task TokenSwapDemo() { Console.WriteLine("🔄 Token Swapping Demo"); Console.WriteLine("======================"); - Console.WriteLine("ℹ️ Swapping allows you to change token denominations or refresh secrets"); - + Console.WriteLine( + "ℹ️ Swapping allows you to change token denominations or refresh secrets" + ); + if (_wallet.Count == 0) { Console.WriteLine("⚠️ No proofs in wallet. Creating example proofs for demo..."); _wallet.AddRange(CreateExampleProofs()); } - + var inputProofs = _wallet.Take(2).ToList(); - Console.WriteLine($"📥 Input proofs: {inputProofs.Count} proofs totaling {inputProofs.Sum(p => (long)p.Amount)} sats"); - + Console.WriteLine( + $"📥 Input proofs: {inputProofs.Count} proofs totaling {inputProofs.Sum(p => (long)p.Amount)} sats" + ); + foreach (var proof in inputProofs) { Console.WriteLine($" - {proof.Amount} sats (Secret: {proof.Secret})"); } - + // In a real implementation, you would: // 1. Create blinded messages for new denominations // 2. Send swap request to mint // 3. Unblind the returned signatures - + Console.WriteLine("\n💡 Swap process would involve:"); Console.WriteLine(" 1. Creating blinded messages for desired output amounts"); - Console.WriteLine(" 2. Sending PostSwapRequest with input proofs and output blinded messages"); + Console.WriteLine( + " 2. Sending PostSwapRequest with input proofs and output blinded messages" + ); Console.WriteLine(" 3. Receiving BlindSignatures from the mint"); Console.WriteLine(" 4. Unblinding signatures to get new proofs with fresh secrets"); Console.WriteLine(" 5. The old proofs become invalid, new proofs are added to wallet"); } - + private static async Task SecretsDemo() { Console.WriteLine("🔐 Working with Secrets Demo"); Console.WriteLine("============================"); - + // Simple string secret Console.WriteLine("1️⃣ Simple String Secret:"); var stringSecret = new StringSecret("my-random-secret-12345"); Console.WriteLine($" Secret: {stringSecret}"); Console.WriteLine($" Curve point: {stringSecret.ToCurve().ToHex()}"); - + // Random secret generation Console.WriteLine("\n2️⃣ Random Secret Generation:"); var randomBytes = new byte[32]; RandomNumberGenerator.Fill(randomBytes); var randomSecret = new StringSecret(Convert.ToHexString(randomBytes).ToLower()); Console.WriteLine($" Random secret: {randomSecret}"); - + // Demonstrate secret uniqueness Console.WriteLine("\n3️⃣ Secret Uniqueness:"); var secret1 = new StringSecret("test-secret-1"); @@ -411,145 +428,147 @@ private static async Task SecretsDemo() Console.WriteLine($" Secret 1 → Curve: {secret1.ToCurve().ToHex()}"); Console.WriteLine($" Secret 2 → Curve: {secret2.ToCurve().ToHex()}"); Console.WriteLine($" Different secrets produce different curve points ✅"); - + Console.WriteLine("\n💡 Key points about secrets:"); Console.WriteLine(" - Secrets are hashed to elliptic curve points"); Console.WriteLine(" - Each secret maps to a unique point on the curve"); Console.WriteLine(" - Changing even one character creates a completely different point"); Console.WriteLine(" - Secrets should be random and unpredictable"); } - + private static async Task MnemonicDemo() { Console.WriteLine("🎲 Mnemonic Secrets Demo (NUT-13)"); Console.WriteLine("=================================="); - + // Create a mnemonic var mnemonic = new Mnemonic(Wordlist.English, WordCount.Twelve); Console.WriteLine($"📝 Generated mnemonic:"); Console.WriteLine($" {mnemonic}"); - + // Example keyset ID (normally you'd get this from the mint) var keysetId = new KeysetId("009a1f293253e41e"); - + Console.WriteLine($"\n🔑 Deriving secrets from mnemonic:"); Console.WriteLine($" Keyset ID: {keysetId}"); - + // Derive multiple secrets for (uint i = 0; i < 5; i++) { var secret = mnemonic.DeriveSecret(keysetId, counter: i); var blindingFactor = mnemonic.DeriveBlindingFactor(keysetId, counter: i); - + Console.WriteLine($" Counter {i}:"); Console.WriteLine($" Secret: {secret}"); Console.WriteLine($" Blinding: {Convert.ToHexString(blindingFactor).ToLower()}"); } - + Console.WriteLine("\n💡 Benefits of deterministic secrets:"); Console.WriteLine(" - Reproducible from mnemonic phrase"); Console.WriteLine(" - No need to store individual secrets"); Console.WriteLine(" - Can recover proofs if you lose wallet data"); Console.WriteLine(" - Counter ensures each secret is unique"); - + Console.WriteLine("\n⚠️ Security considerations:"); Console.WriteLine(" - Keep your mnemonic phrase secure"); Console.WriteLine(" - Anyone with the mnemonic can recreate your secrets"); Console.WriteLine(" - Use proper entropy when generating mnemonics"); } - + private static async Task P2PKDemo() { Console.WriteLine("🔒 Pay-to-Public-Key Demo (NUT-11)"); Console.WriteLine("==================================="); - + // Create some public keys for the demo var privKey1 = ECPrivKey.Create(RandomNumberGenerator.GetBytes(32)); var privKey2 = ECPrivKey.Create(RandomNumberGenerator.GetBytes(32)); var pubKey1 = privKey1.CreatePubKey(); var pubKey2 = privKey2.CreatePubKey(); - + Console.WriteLine("🔑 Generated demo keys:"); Console.WriteLine($" PubKey 1: {pubKey1}"); Console.WriteLine($" PubKey 2: {pubKey2}"); - + // Create a 1-of-2 multisig P2PK secret Console.WriteLine("\n🏗️ Creating 1-of-2 multisig P2PK:"); - var p2pkBuilder = new P2PKBuilder + var p2pkBuilder = new P2PkBuilder { Pubkeys = new[] { pubKey1, pubKey2 }, SignatureThreshold = 1, // 1-of-2 multisig - SigFlag = "SIG_INPUTS" + SigFlag = "SIG_INPUTS", }; - + var p2pkSecret = p2pkBuilder.Build(); var nut10Secret = new Nut10Secret(P2PKProofSecret.Key, p2pkSecret); - - Console.WriteLine($" Signature threshold: {p2pkBuilder.SignatureThreshold}-of-{p2pkBuilder.Pubkeys.Length}"); + + Console.WriteLine( + $" Signature threshold: {p2pkBuilder.SignatureThreshold}-of-{p2pkBuilder.Pubkeys.Length}" + ); Console.WriteLine($" Signature flag: {p2pkBuilder.SigFlag}"); Console.WriteLine($" P2PK secret created ✅"); - + // Create a time-locked P2PK Console.WriteLine("\n⏰ Creating time-locked P2PK:"); - var timeLockedBuilder = new P2PKBuilder + var timeLockedBuilder = new P2PkBuilder { Pubkeys = new[] { pubKey1 }, SignatureThreshold = 1, SigFlag = "SIG_INPUTS", Lock = DateTimeOffset.UtcNow.AddHours(1), // Lock for 1 hour - RefundPubkeys = new[] { pubKey2 } // Refund key after timeout + RefundPubkeys = new[] { pubKey2 }, // Refund key after timeout }; - + var timeLockedSecret = timeLockedBuilder.Build(); var timeLockedNut10 = new Nut10Secret(P2PKProofSecret.Key, timeLockedSecret); - + Console.WriteLine($" Lock time: {timeLockedBuilder.Lock}"); Console.WriteLine($" Refund key: {pubKey2}"); Console.WriteLine($" Time-locked P2PK secret created ✅"); - + Console.WriteLine("\n💡 P2PK use cases:"); Console.WriteLine(" - Multisignature wallets"); Console.WriteLine(" - Escrow services"); Console.WriteLine(" - Time-locked payments"); Console.WriteLine(" - Conditional spending"); - + Console.WriteLine("\n🔓 To spend P2PK proofs:"); Console.WriteLine(" - Create signatures with required private keys"); Console.WriteLine(" - Include witness data in the proof"); Console.WriteLine(" - Mint validates signatures against public keys"); } - + private static async Task CheckProofStatesDemo() { Console.WriteLine("🔍 Check Proof States Demo"); Console.WriteLine("=========================="); - + if (_client == null) { Console.WriteLine("❌ Client not initialized"); return; } - + if (_wallet.Count == 0) { Console.WriteLine("⚠️ No proofs in wallet. Creating example proofs..."); _wallet.AddRange(CreateExampleProofs()); } - + try { var proofsToCheck = _wallet.Take(3).ToList(); Console.WriteLine($"📋 Checking state of {proofsToCheck.Count} proofs..."); - + // Create check state request var stateRequest = new PostCheckStateRequest { - Ys = proofsToCheck.Select(p => p.C.ToString()).ToArray() + Ys = proofsToCheck.Select(p => p.C.ToString()).ToArray(), }; - + // Note: This will likely fail with fake proofs, but shows the API usage var stateResponse = await _client.CheckState(stateRequest); - + Console.WriteLine("✅ State check successful:"); for (int i = 0; i < stateResponse.States.Length; i++) { @@ -572,58 +591,62 @@ private static async Task CheckProofStatesDemo() Console.WriteLine(" - Check states before attempting to spend proofs"); } } - + private static void ShowWallet() { Console.WriteLine("💰 Current Wallet"); Console.WriteLine("================="); - + if (_wallet.Count == 0) { Console.WriteLine(" Empty wallet - no proofs stored"); return; } - + var totalAmount = _wallet.Sum(p => (long)p.Amount); Console.WriteLine($" Total balance: {totalAmount} sats"); Console.WriteLine($" Number of proofs: {_wallet.Count}"); Console.WriteLine(); - + Console.WriteLine(" Proof details:"); foreach (var proof in _wallet.Take(10)) // Show first 10 proofs { - Console.WriteLine($" - {proof.Amount,4} sats | ID: {proof.Id} | Secret: {proof.Secret.ToString().Substring(0, Math.Min(20, proof.Secret.ToString().Length))}..."); + Console.WriteLine( + $" - {proof.Amount, 4} sats | ID: {proof.Id} | Secret: {proof.Secret.ToString().Substring(0, Math.Min(20, proof.Secret.ToString().Length))}..." + ); } - + if (_wallet.Count > 10) { Console.WriteLine($" ... and {_wallet.Count - 10} more proofs"); } - + // Show denomination breakdown var denominations = _wallet.GroupBy(p => p.Amount).OrderBy(g => g.Key); Console.WriteLine("\n Denomination breakdown:"); foreach (var denom in denominations) { - Console.WriteLine($" {denom.Key,4} sats: {denom.Count()} proofs = {denom.Key * (ulong)denom.Count()} sats"); + Console.WriteLine( + $" {denom.Key, 4} sats: {denom.Count()} proofs = {denom.Key * (ulong)denom.Count()} sats" + ); } } - + private static List CreateExampleProofs() { // Create example proofs for demonstration // In a real application, these would come from minting operations var keysetId = new KeysetId("009a1f293253e41e"); - + var proofs = new List(); var amounts = new ulong[] { 1, 2, 4, 8, 16, 32, 64, 128, 256, 512 }; - + foreach (var amount in amounts.Take(5)) // Create 5 demo proofs { var secret = new StringSecret($"demo-secret-{amount}-{Guid.NewGuid()}"); var privKey = ECPrivKey.Create(RandomNumberGenerator.GetBytes(32)); var pubKey = privKey.CreatePubKey(); - + var proof = new Proof { Amount = amount, @@ -632,10 +655,10 @@ private static List CreateExampleProofs() C = pubKey, // Note: In real usage, these would be proper cryptographic proofs from the mint }; - + proofs.Add(proof); } - + return proofs; } } @@ -647,4 +670,4 @@ public static ulong TotalAmount(this CashuToken token) { return (ulong)token.Tokens.SelectMany(t => t.Proofs).Sum(p => (long)p.Amount); } -} \ No newline at end of file +} diff --git a/DotNut.Nostr/DotNut.Nostr.csproj b/DotNut.Nostr/DotNut.Nostr.csproj index 4bed5fa..05d32c2 100644 --- a/DotNut.Nostr/DotNut.Nostr.csproj +++ b/DotNut.Nostr/DotNut.Nostr.csproj @@ -1,27 +1,23 @@  + + net8.0 + enable + enable + true + DotNut.Nostr + Kukks + Support Cashu payment requests through Nostr + MIT + https://github.com/Kukks/DotNut + 1.0.0 + https://github.com/Kukks/DotNut + git + bitcoin cashu ecash secp256k1 nostr + https://github.com/Kukks/DotNut/blob/master/LICENSE + - - net8.0 - enable - enable - true - DotNut.Nostr - Kukks - Support Cashu payment requests through Nostr - MIT - https://github.com/Kukks/DotNut - 1.0.0 - https://github.com/Kukks/DotNut - git - bitcoin cashu ecash secp256k1 nostr - https://github.com/Kukks/DotNut/blob/master/LICENSE - - - - - - - - - + + + + diff --git a/DotNut.Nostr/NostrNip17PaymentRequestInterfaceHandler.cs b/DotNut.Nostr/NostrNip17PaymentRequestInterfaceHandler.cs index 28d25b4..7c43f12 100644 --- a/DotNut.Nostr/NostrNip17PaymentRequestInterfaceHandler.cs +++ b/DotNut.Nostr/NostrNip17PaymentRequestInterfaceHandler.cs @@ -10,7 +10,9 @@ public class NostrNip17PaymentRequestInterfaceHandler : PaymentRequestInterfaceH { public static void Register() { - PaymentRequestTransportInitiator.Handlers.Add(new NostrNip17PaymentRequestInterfaceHandler()); + PaymentRequestTransportInitiator.Handlers.Add( + new NostrNip17PaymentRequestInterfaceHandler() + ); } public bool CanHandle(PaymentRequest request) @@ -18,13 +20,15 @@ public bool CanHandle(PaymentRequest request) return request.Transports.Any(t => t.Type == "nostr" && t.Tags?.Any(tag => tag.Key == "n" && tag.Value.Any(v => v == "17")) == true); - } - public async Task SendPayment(PaymentRequest request, PaymentRequestPayload payload, - CancellationToken cancellationToken = default) - { - var nostrTransport = request.Transports.FirstOrDefault(t => + public async Task SendPayment( + PaymentRequest request, + PaymentRequestPayload payload, + CancellationToken cancellationToken = default + ) + { + var nostrTransport = request.Transports.FirstOrDefault(t => t.Type == "nostr" && t.Tags?.Any(tag => tag.Key == "n" && tag.Value.Any(v => v == "17")) == true); if (nostrTransport is null) @@ -32,9 +36,8 @@ public async Task SendPayment(PaymentRequest request, PaymentRequestPayload payl throw new InvalidOperationException("No NIP17 nostr transport found."); } var nprofileStr = nostrTransport.Target; - - var nprofile = (NIP19.NosteProfileNote) NIP19.FromNIP19Note(nprofileStr); - using var client = new CompositeNostrClient(nprofile.Relays.Select(r => new Uri(r)).ToArray()); + var nprofile = (NIP19.NosteProfileNote)NIP19.FromNIP19Note(nprofileStr); + using var client = new CompositeNostrClient(nprofile.Relays.Select(r => new Uri(r)).ToArray()); await client.Connect(cancellationToken); var ephemeralKey = ECPrivKey.Create(RandomNumberGenerator.GetBytes(32)); var msg = new NostrEvent() @@ -46,9 +49,13 @@ public async Task SendPayment(PaymentRequest request, PaymentRequestPayload payl Tags = new(), }; msg.Id = msg.ComputeId(); - - var giftWrap = await NIP17.Create(msg, ephemeralKey,ECXOnlyPubKey.Create(Convert.FromHexString(nprofile.PubKey)), null); - await client.SendEventsAndWaitUntilReceived(new []{giftWrap}, cancellationToken); + var giftWrap = await NIP17.Create( + msg, + ephemeralKey, + ECXOnlyPubKey.Create(Convert.FromHexString(nprofile.PubKey)), + null + ); + await client.SendEventsAndWaitUntilReceived(new[] { giftWrap }, cancellationToken); } -} \ No newline at end of file +} diff --git a/DotNut.Tests/DotNut.Tests.csproj b/DotNut.Tests/DotNut.Tests.csproj index 122cff3..fcb505b 100644 --- a/DotNut.Tests/DotNut.Tests.csproj +++ b/DotNut.Tests/DotNut.Tests.csproj @@ -1,29 +1,27 @@ + + net8.0 + enable + enable - - net8.0 - enable - enable + false + true + - false - true - - - - - - - runtime; build; native; contentfiles; analyzers; buildtransitive - all - - - runtime; build; native; contentfiles; analyzers; buildtransitive - all - - - - - - + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + diff --git a/DotNut.Tests/GlobalUsings.cs b/DotNut.Tests/GlobalUsings.cs index 8c927eb..c802f44 100644 --- a/DotNut.Tests/GlobalUsings.cs +++ b/DotNut.Tests/GlobalUsings.cs @@ -1 +1 @@ -global using Xunit; \ No newline at end of file +global using Xunit; diff --git a/DotNut.Tests/Integration.cs b/DotNut.Tests/Integration.cs new file mode 100644 index 0000000..b9ed52f --- /dev/null +++ b/DotNut.Tests/Integration.cs @@ -0,0 +1,981 @@ +using System.Security.Cryptography; +using DotNut.Abstractions; +using DotNut.Abstractions.Websockets; +using DotNut.Api; +using DotNut.ApiModels; + +namespace DotNut.Tests; + +public class Integration +{ + private static string MintUrl = "http://localhost:3338"; + + // private static string MintUrl = "https://fake.thesimplekid.dev"; + // private static string MintUrl = "https://testnut.cashu.space"; + + private static string seed = + "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about"; + + // for now cdk mint returns 500 if there's created melt quote for the same invoice twice + private static readonly Dictionary valuesInvoices = new Dictionary() + { + { + 500, + "lnbc5u1p5sh0yvsp53seej3qkkxe6xxk9mufaj7y3jc9s9kvfn4g3whppwqcl4vcjraaspp5vtv793xc9ksch8zekkhqtv54a2evh7vq4zuywcmk9nzt69qma5yqhp5uwcvgs5clswpfxhm7nyfjmaeysn6us0yvjdexn9yjkv3k7zjhp2sxq9z0rgqcqpnrzjq0ly0l075re9ltgqzdycartvas6g4c7kwwzpasj7a98c0ss679hdsr080vqqdcgqqqqqqqqnqqqqryqqxv9qxpqysgqwq50283v8asna95fktaeg80kq9evs0chaw44y6y649qsql9vsfc5gfcsp8rdwwyccepwy83n7g0s25n3lpv3hjgcr220n5w806fja8gp2xjvd7" + }, + { + 501, + "lnbc5010n1p5shs9rsp5a2qhmn05xsd8vcm5jx9v2aswkz0pxguk4jqlaxsazzcg5rduan2qpp5al2k5zwruvlx34sxxdys2sj696m58uqgjvzxxrxhvuyswhmzg5cqhp5uwcvgs5clswpfxhm7nyfjmaeysn6us0yvjdexn9yjkv3k7zjhp2sxq9z0rgqcqpnrzjqw7c9dkkx4nur9sztw2zzpzj8u8rgsqgdsykylg5pwplh26824lc7rvlqcqqn3gqqyqqqqlgqqqqqqgq2q9qxpqysgqgpj2x2aw2dv5tzhx86th6a5vutpcdxz9htewqgvzjgqkzwmh6xs5mw5xcgrzyq77f35shv0gg5ygtjmn7e73wg8v0a9g836ufszdxmqqqu3642" + }, + { + 502, + "lnbc5020n1p5j3nxasp5qz7utfrp954nxp8049tqzg0t23krdj59thfcrc2g5h6lsemzvyfqpp5ms6xd7grtak0nr8lwytsclmq3d233v7gy7j0kuw32txhjq0f8ngqhp5uwcvgs5clswpfxhm7nyfjmaeysn6us0yvjdexn9yjkv3k7zjhp2sxq9z0rgqcqpnrzjqg587a2yyuqeua9c3j8nw7wwpx709slwl5lzfs0t0vq3kdwemzp67rtwevqq95gqqyqqq5sgqqq9yzqq2q9qxpqysgqgpp0zsetj9fedvr0szpwjfw2weckygmjthhnfpp2cerjtrn8n0pxyvrtc00l0jwzkqhwedcvgqljtwx3a7qplqp43jlxe4mpmw5svlgqfwa9yy" + }, + { + 503, + "lnbc5030n1p5n9kk6sp5ee6rsflv9rnnyt80ucc0fzlwa975nmufs2dn0x3u0hlerxxtc4nqpp5ew5mxfmu966c8wywvnvtgsljduq0jduvdpc3jzqzq9uqafx7ejgshp5uwcvgs5clswpfxhm7nyfjmaeysn6us0yvjdexn9yjkv3k7zjhp2sxq9z0rgqcqpnrzjqdqne4nrkxmz96ktnngat4nzx7sv0kf5uqmgfvqvvars7pac7fn9wr8fjvqq3csqqqqqqqqqqqqq0scqvs9qxpqysgq5lqwgfk6vv36tnlx2tv6reu2587x8ha2wsht0s75dpzvmknpgepsqaq9wnlx7n87j3x6w0vvkvc4qgda6mhacygn9f0xgagwt84uxtcqgtcpfs" + }, + { + 999, + "lnbc9990n1p5j3cf7sp575w4pw93kfrghl2gh68885v76gwjpzuv435t52q846cvx4w7yuvqpp5hdzvm3yf0r3vj99c7esmcv7zuj2fralf2twhl6s9xqcgr8g7nwyqhp5uwcvgs5clswpfxhm7nyfjmaeysn6us0yvjdexn9yjkv3k7zjhp2sxq9z0rgqcqpnrzjqt0mfswatysklf4z358sztscs5t0vdghmd5vfe9c9sa0gy6r5pdugrs7myqqvgqqqyqqqqqqqqqq86qq8s9qxpqysgq5wh9l4fy32ww4770mqm7yqvhwllaqyssvp335gjz6t59ca03gecyvdd9uv0ztrcm2uf2352wvwxcfh7yukucp4p6zu6ll867aj686wsqz0jlmt" + }, + { + 1000, + "lnbc10u1p5w6vggsp5gn5xhswgn5299w6elu2z0vzjxhf9hwd6pwjcgfwphaxunyu0dx6spp5a60trrhce2u6tzqjwjczem8rpdesgzkawcqg2xqaesz2kd50z4uqhp5uwcvgs5clswpfxhm7nyfjmaeysn6us0yvjdexn9yjkv3k7zjhp2sxq9z0rgqcqpnrzjqw22kc09xj0dm65ew5h5r003vtn72eyzchdgjag66l0yhwdudfmuzrwvesqq8qgqqgqqqqqqqqqqzhsq2q9qxpqysgq955gcfr95wwz0ehtnk3xraatkyhj88z44ku7yqutnwnt3gkh82jxehvdff7n2js2p54jgpvg6dmwmq8t9d8x05j63mqjrsr4cwd4lpcpnc39ru" + }, + { + 1150, + "lnbc11500n1p5jnmr7sp5u0s2wpuqn4mp0axyzgmsxzf5v8sy3zmzz9a7jyq38luyx9cntazqpp57j3carehwt4tqthxz9z7ea80t0htklh4v6v96dtn4vxuu4kwsershp53mwsvrcmkv743nyfzjp5a5fqrg2yngda3apf7jf9rzsuwt82wt3sxq9z0rgqcqpnrzjqg587a2yyuqeua9c3j8nw7wwpx709slwl5lzfs0t0vq3kdwemzp67rtwevqq95gqqyqqq5sgqqq9yzqq2q9qxpqysgqe97nwd9q74ua0sl9877sdprjcuc6jpyy8c52azpz8au6ur8q3838c0a0upnahs8w3sec8kxh26m3v9rkgqej36652t3sa5t25svacdcq5qwwjp" + }, + { + 1151, + "lnbc11510n1p5n9hzpsp5ey8npxa4nsaet73nc74lky0mv780h6890ua3kqhffvn8heqzk33spp5df0xt9s0e0kh0rx7dcy39u4q3g7cknk88wr4s90cldv6z2vwspgqhp5uwcvgs5clswpfxhm7nyfjmaeysn6us0yvjdexn9yjkv3k7zjhp2sxq9z0rgqcqpnrzjq0xp6zfjhwvmq6tltd09jcdc82ml6eh3alzvnaw8httxcx7tu78syrvfkqqqm0qqqyqqqqlgqqqvx5qqjq9qxpqysgqwnys3mklnsnrw5ysa8cjtynlqnllyxskcsamr7x96nl5kllyqcznlyeeuklr3zydeq43k6ckyrgqqfg965dsdjc675lvlssn0z4sxusq0lzrx6" + }, + { + 2000, + "lnbc20u1p5094fksp54vrdcymel5awhrpc0m6z4kvhhyvqlwkshkyt2wr6eyljkz8c798qpp59f2vc8td8tu62gtf4qfwzkrkxedsey7a5ajrd48a25z2kkwg407shp5nklhn663zgwcdnh7pe5jxt6td0cchhre6hxzdxrjdlfwtpq60f5sxq9z0rgqcqpnrzjqw0de9yc0j8n4hpgm269tm7qph4gwcyf5ys02uaapvpugrva87c7zr045uqq4jsqpsqqqqlgqqqqrcgq2q9qxpqysgq6g2pamgjumh6uw5k5rj2ket44wh8nfzs5gzyygl54hu5cefuxdhxp9h5mrg64rh07znktn9x9d5vg6fc0rw7m63x8rg4qk3kw6d8sycpywn48m" + }, + }; + + private static readonly Dictionary bolt12Invoices = new() + { + { + 1200, + "lno1zrxq8pjw7qjlm68mtp7e3yvxee4y5xrgjhhyf2fxhlphpckrvevh50u0qwumyhd9aa7p77jkp946nkphl2lutxa3e5zp8yx36pyycqas85txgqsre4qsrhyu2jqk5svgnwe5tng78r24dlwwglluetkdv4a5ppc3wanqqvlfkaqp5hhc6jl8eq0mau6wsdxevary7e0e3rpmma7plggygs7fr4e6dj8vflurnt7ajhgwxfu9hmqmf48wqd6tzuxmwdcgk9p6wspfqer0xj883lysflutn8qvudzakypdv8a7kqqsv0vcrt5w208yr5uzregj7whghy" + }, + }; + private static ICounter counter = new InMemoryCounter(); + + [Fact] + public async Task FetchesInfoSuccessfully() + { + var wallet = Wallet.Create().WithMint(MintUrl); + var info = await wallet.GetInfo(); + Assert.NotNull(info); + } + + [Fact] + public async Task MintsBolt11Successfully() + { + var wallet = Wallet.Create().WithMint(MintUrl); + + var mintQuote = await wallet + .CreateMintQuote() + .WithUnit("sat") + .WithAmount(1337) + .ProcessAsyncBolt11(); + + Assert.NotNull(mintQuote); + + var paymentRequest = mintQuote.GetQuote().Request; + Assert.Contains("lnbc1337", paymentRequest); + + await PayInvoice(); + + var mintResponse = await mintQuote.Mint(); + Assert.NotNull(mintResponse); + Assert.Equal(1337UL, Utils.SumProofs(mintResponse)); + } + + [Fact] + public async Task MintsBolt12Successfully() + { + var wallet = Wallet.Create().WithMint(MintUrl); + var privkey = new PrivKey(RandomNumberGenerator.GetHexString(64)); + + var mintQuote = await wallet + .CreateMintQuote() + .WithPubkey(privkey.Key.CreatePubKey()) + .WithUnit("sat") + .WithAmount(1337) + .ProcessAsyncBolt12(); + + Assert.NotNull(mintQuote); + + var paymentRequest = mintQuote.GetQuote().Request; + Assert.NotNull(paymentRequest); + mintQuote.SignWithPrivkey(privkey); + + await PayInvoice(); + var mintResponse = await mintQuote.Mint(); + Assert.NotNull(mintResponse); + Assert.Equal(1337UL, Utils.SumProofs(mintResponse)); + } + + [Fact] + public async Task MintsDeterministicSuccessfully() + { + var wallet = Wallet.Create().WithMint(MintUrl).WithMnemonic(seed).WithCounter(counter); + + var mintQuote = await wallet + .CreateMintQuote() + .WithUnit("sat") + .WithAmount(1337) + .ProcessAsyncBolt11(); + + Assert.NotNull(mintQuote); + + var paymentRequest = mintQuote.GetQuote().Request; + Assert.Contains("lnbc1337", paymentRequest); + + await PayInvoice(); + var mintedProofs = await mintQuote.Mint(); + + var keysetId = mintedProofs.First().Id; + var currentCounter = await counter.GetCounterForId(keysetId); + // counter is bumped after every use, so its already one more + Assert.Equal(currentCounter, (uint)mintedProofs.Count); + } + + [Fact] + public async Task RestoresSuccessfully() + { + var phreshCounter = new InMemoryCounter(); + + var wallet = Wallet + .Create() + .WithCounter(phreshCounter) + .WithMint(MintUrl) + .WithMnemonic(seed); + + var restoredProofs = await wallet.Restore().ProcessAsync(); + + var keys = (await wallet.GetKeys()).First().Keys; + var expectedAmount = Utils.SplitToProofsAmounts(1336UL, keys).Count; // (one for fee) + var keysets = await wallet.GetKeysets(); + + foreach (var keyset in keysets) + { + // new counter will be bumped to newest state + Assert.Equal( + await counter.GetCounterForId(keyset.Id) + expectedAmount, + await phreshCounter.GetCounterForId(keyset.Id) + ); + } + Assert.Equal(expectedAmount, restoredProofs.Count()); + + // assign restored counter to previous one, so next tests can use it safely + counter = phreshCounter; + } + + [Fact] + public async Task SwapsSuccessfully() + { + var wallet = Wallet.Create().WithMint(MintUrl); + + // 1. mint some proofs (deterministic, because why not) + var mintQuote = await wallet + .CreateMintQuote() + .WithAmount(64) + .WithUnit("sat") + .ProcessAsyncBolt11(); + + await PayInvoice(); + var mintedProofs = await mintQuote.Mint(); + Assert.NotEmpty(mintedProofs); + + //2. Swap them + var newProofs = await wallet.Swap().FromInputs(mintedProofs).ProcessAsync(); + + Assert.NotEmpty(newProofs); + } + + [Fact] + public async Task SwapsDeterministicSuccessfully() + { + var wallet = Wallet.Create().WithMint(MintUrl).WithMnemonic(seed).WithCounter(counter); + + // 1. mint some proofs (deterministic, because why not) + var mintQuote = await wallet + .CreateMintQuote() + .WithAmount(64) + .WithUnit("sat") + .ProcessAsyncBolt11(); + + await PayInvoice(); + var mintedProofs = await mintQuote.Mint(); + Assert.NotEmpty(mintedProofs); + + //2. Swap them + var newProofs = await wallet.Swap().FromInputs(mintedProofs).ProcessAsync(); + + Assert.NotEmpty(newProofs); + } + + [Fact] + public async Task MeltsBolt11Successfully() + { + // mint proofs + var wallet = Wallet.Create().WithMint(MintUrl).WithMnemonic(seed).WithCounter(counter); + + var mintQuote = await wallet + .CreateMintQuote() + .WithUnit("sat") + .WithAmount(1337) + .ProcessAsyncBolt11(); + await Task.Delay(3000); + var mintedProofs = await mintQuote.Mint(); + Assert.NotEmpty(mintedProofs); + + // create melt quote + var meltQuote = await wallet + .CreateMeltQuote() + .WithInvoice(valuesInvoices[999]) + .WithUnit("sat") + .ProcessAsyncBolt11(); + + // select proofs to send + var q = meltQuote.GetQuote(); + var selectedProofs = await wallet.SelectProofsToSend( + mintedProofs, + q.Amount + (ulong)q.FeeReserve, + true + ); + + //melt proofs + var change = await meltQuote.Melt(selectedProofs.Send); + + Assert.NotEmpty(change); + } + + [Fact] + public async Task MeltsBolt12Successfully() + { + var privkeyBob = new PrivKey(RandomNumberGenerator.GetBytes(32)); + + // mint proofs + var wallet = Wallet.Create().WithMint(MintUrl); + + var mintQuote = await wallet + .CreateMintQuote() + .WithUnit("sat") + .WithAmount(1337) + .WithPubkey(privkeyBob.Key.CreatePubKey()) + .ProcessAsyncBolt12(); + + await Task.Delay(3000); + + mintQuote.SignWithPrivkey(privkeyBob); + var mintedProofs = await mintQuote.Mint(); + Assert.NotEmpty(mintedProofs); + + var Ids = mintedProofs.Select(proof => proof.Id).Count(); + + Console.WriteLine($"amounts {Ids}"); + // create melt quote + var meltQuote = await wallet + .CreateMeltQuote() + .WithInvoice(bolt12Invoices[1200]) + .WithUnit("sat") + .WithAmount(1200) // it turns out that this invoice is amountless + .ProcessAsyncBolt12(); + + // select proofs to send + var q = meltQuote.GetQuote(); + var selectedProofs = await wallet.SelectProofsToSend( + mintedProofs, + q.Amount + (ulong)q.FeeReserve, + true + ); + + //melt proofs + var change = await meltQuote.Melt(selectedProofs.Send); + + Assert.NotEmpty(change); + } + + [Fact] + public async Task SubscribeToMintMeltQuoteUpdates() + { + await using var service = new WebsocketService(); + var connection = await service.ConnectAsync(MintUrl); + Assert.NotNull(connection); + + var wallet = Wallet.Create().WithMint(MintUrl).WithWebsocketService(service); + + var mintHandler = await wallet + .CreateMintQuote() + .WithAmount(3338) + .WithUnit("sat") + .ProcessAsyncBolt11(); + + var quote = mintHandler.GetQuote(); + + var sub = await service.SubscribeToMintQuoteAsync(MintUrl, new[] { quote.Quote }); + + using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(240)); + + int connectedCount = 0; + bool gotPaid = false; + + await foreach (var msg in sub.NotificationChannel.Reader.ReadAllAsync(cts.Token)) + { + switch (msg) + { + case WsMessage.Response: + connectedCount++; + break; + + case WsMessage.Notification notification: + var parsed = NotificationParser.ParsePayload( + notification.Value + ); + if (parsed?.State == "PAID") + { + gotPaid = true; + await sub.CloseAsync(); + } + + break; + + case WsMessage.Error error: + Assert.Fail($"WebSocket error: {error}"); + break; + } + + if (gotPaid) + break; + } + + Assert.Equal(1, connectedCount); + Assert.True(gotPaid, "Expected to receive PAID notification"); + + var proofs = await mintHandler.Mint(cts.Token); + Assert.NotEmpty(proofs); + } + + [Fact] + public async Task InvoiceWithDescription() + { + var wallet = Wallet.Create().WithMint(MintUrl); + + var quote = await wallet + .CreateMintQuote() + .WithDescription("Test Description") + .WithAmount(1337) + .ProcessAsyncBolt11(); + + Assert.NotNull(quote); + } + + [Fact] + public async Task FeeForExternalInvoice() + { + var wallet = Wallet.Create().WithMint(MintUrl); + + var meltHandler = await wallet + .CreateMeltQuote() + .WithInvoice(valuesInvoices[2000]) + .ProcessAsyncBolt11(); + + Assert.NotNull(meltHandler); + + var quote = meltHandler.GetQuote(); + + Assert.NotNull(quote); + Assert.True(quote.FeeReserve > 0); + } + + [Fact] + public async Task SwapP2Pk() + { + // p2pk aren't deterministic, so wallet is initialized without mnemonic and counter + var wallet = Wallet.Create().WithMint(MintUrl); + + var privKeyAlice = new PrivKey(RandomNumberGenerator.GetHexString(64, true)); + var privKeyBob = new PrivKey(RandomNumberGenerator.GetHexString(64, true)); + + var mintHandler = await wallet + .CreateMintQuote() + .WithAmount(1337) + .WithP2PkLock( + new P2PkBuilder() + { + Pubkeys = [privKeyBob.Key.CreatePubKey()], + SignatureThreshold = 1, + } + ) + .ProcessAsyncBolt11(); + + await PayInvoice(); + var proofs = await mintHandler.Mint(); + + await Assert.ThrowsAsync(async () => + await wallet.Swap().FromInputs(proofs).ProcessAsync() + ); + + var swappedProofs = await wallet + .Swap() + .FromInputs(proofs) + .WithPrivkeys([privKeyBob]) + .ProcessAsync(); + + Assert.NotEmpty(swappedProofs); + } + + [Fact] + public async Task MintMeltP2PkMultisig() + { + var wallet = Wallet.Create().WithMint(MintUrl); + + var privKeyAlice = new PrivKey(RandomNumberGenerator.GetHexString(64, true)); + var privKeyBob = new PrivKey(RandomNumberGenerator.GetHexString(64, true)); + + var mintHandler = await wallet + .CreateMintQuote() + .WithAmount(1337) + .WithP2PkLock( + new P2PkBuilder() + { + Pubkeys = [privKeyBob.Key.CreatePubKey(), privKeyAlice.Key.CreatePubKey()], + SignatureThreshold = 2, + } + ) + .ProcessAsyncBolt11(); + await PayInvoice(); + + var proofs = await mintHandler.Mint(); + + Assert.NotEmpty(proofs); + + // no privkeys + await Assert.ThrowsAsync(async () => + { + var meltHandler = await wallet + .CreateMeltQuote() + .WithInvoice(valuesInvoices[500]) + .ProcessAsyncBolt11(); + await meltHandler.Melt(proofs); + }); + + var handler = await wallet + .CreateMeltQuote() + .WithInvoice(valuesInvoices[501]) + .WithPrivKeys([privKeyBob, privKeyAlice]) + .ProcessAsyncBolt11(); + + var q = handler.GetQuote(); + + var amountToPay = q.Amount + (ulong)q.FeeReserve; + var selectorResponse = await wallet.SelectProofsToSend(proofs, amountToPay, true); + var change = await handler.Melt(selectorResponse.Send); + + Assert.NotEmpty(change); + } + + [Fact] + public async Task MintSwapP2PkSigAll() + { + var wallet = Wallet.Create().WithMint(MintUrl); + + var privKeyAlice = new PrivKey(RandomNumberGenerator.GetHexString(64, true)); + var privKeyBob = new PrivKey(RandomNumberGenerator.GetHexString(64, true)); + + var mintHandler = await wallet + .CreateMintQuote() + .WithAmount(1337) + .WithP2PkLock( + new P2PkBuilder() + { + SigFlag = "SIG_ALL", + Pubkeys = [privKeyBob.Key.CreatePubKey()], + SignatureThreshold = 1, + } + ) + .ProcessAsyncBolt11(); + + await PayInvoice(); + var proofs = await mintHandler.Mint(); + + await Assert.ThrowsAsync(async () => + await wallet.Swap().FromInputs(proofs).ProcessAsync() + ); + + + var swappedProofs = await wallet + .Swap() + .FromInputs(proofs) + .WithPrivkeys([privKeyBob]) + .ProcessAsync(); + + Assert.NotEmpty(swappedProofs); + } + + [Fact] + public async Task MintSwapP2Bk() + { + var wallet = Wallet.Create().WithMint(MintUrl); + + var privKeyBob = new PrivKey(RandomNumberGenerator.GetHexString(64, true)); + var privKeyAlice = new PrivKey(RandomNumberGenerator.GetHexString(64, true)); + + var builder = new P2PkBuilder() + { + Pubkeys = [privKeyBob.Key.CreatePubKey(), privKeyAlice.Key.CreatePubKey()], + }; + + var quote = await wallet + .CreateMintQuote() + .WithAmount(1337) + .WithP2PkLock(builder) + .BlindPubkeys() + .ProcessAsyncBolt11(); + + await PayInvoice(); + var proofs = await quote.Mint(); + + Assert.NotEmpty(proofs); + Assert.NotEmpty(proofs.Select(p => p.P2PkE)); + + var newProofs = await wallet + .Swap() + .FromInputs(proofs) + .WithPrivkeys([privKeyBob, privKeyAlice]) + .ProcessAsync(); + + Assert.NotEmpty(newProofs); + } + + [Fact] + public async Task MintMeltP2Bk() + { + var wallet = Wallet.Create().WithMint(MintUrl); + + var privKeyBob = new PrivKey(RandomNumberGenerator.GetHexString(64, true)); + + var builder = new P2PkBuilder() { Pubkeys = [privKeyBob.Key.CreatePubKey()] }; + + var quote = await wallet + .CreateMintQuote() + .WithAmount(1337) + .WithP2PkLock(builder) + .BlindPubkeys() + .ProcessAsyncBolt11(); + + await PayInvoice(); + var proofs = await quote.Mint(); + + Assert.NotEmpty(proofs); + Assert.NotEmpty(proofs.Select(p => p.P2PkE)); + + var meltHandler = await wallet + .CreateMeltQuote() + .WithInvoice(valuesInvoices[502]) + .WithPrivKeys([privKeyBob]) + .ProcessAsyncBolt11(); + + var change = await meltHandler.Melt(proofs); + + Assert.NotEmpty(change); + } + + [Fact] + public async Task MintMeltP2BkSigAll() + { + var wallet = Wallet.Create().WithMint(MintUrl); + + var privKeyBob = new PrivKey(RandomNumberGenerator.GetHexString(64, true)); + + var builder = new P2PkBuilder() + { + Pubkeys = [privKeyBob.Key.CreatePubKey()], + SigFlag = "SIG_ALL", + }; + + var quote = await wallet + .CreateMintQuote() + .WithAmount(1337) + .WithP2PkLock(builder) + .BlindPubkeys() + .ProcessAsyncBolt11(); + + await PayInvoice(); + var proofs = await quote.Mint(); + + Assert.NotEmpty(proofs); + Assert.NotEmpty(proofs.Select(p => p.P2PkE)); + + var meltHandler = await wallet + .CreateMeltQuote() + .WithInvoice(valuesInvoices[503]) + .WithPrivKeys([privKeyBob]) + .ProcessAsyncBolt11(); + + var change = await meltHandler.Melt(proofs); + + Assert.NotEmpty(change); + } + + [Fact] + public async Task MintSwapP2BkSigAll() + { + var wallet = Wallet.Create().WithMint(MintUrl); + + var privKeyAlice = new PrivKey(RandomNumberGenerator.GetHexString(64, true)); + var privKeyBob = new PrivKey(RandomNumberGenerator.GetHexString(64, true)); + + var mintHandler = await wallet + .CreateMintQuote() + .WithAmount(1337) + .WithP2PkLock( + new P2PkBuilder() + { + SigFlag = "SIG_ALL", + Pubkeys = [privKeyBob.Key.CreatePubKey()], + SignatureThreshold = 1, + } + ) + .BlindPubkeys() + .ProcessAsyncBolt11(); + + await PayInvoice(); + var proofs = await mintHandler.Mint(); + + await Assert.ThrowsAsync(async () => + await wallet.Swap().FromInputs(proofs).ProcessAsync() + ); + + var swappedProofs = await wallet + .Swap() + .FromInputs(proofs) + .WithPrivkeys([privKeyBob]) + .ProcessAsync(); + + Assert.NotEmpty(swappedProofs); + } + + [Fact] + public async Task MintSwapHTLC() + { + var wallet = Wallet.Create().WithMint(MintUrl); + + var privKeyBob = new PrivKey(RandomNumberGenerator.GetHexString(64, true)); + var preimage = "0000000000000000000000000000000000000000000000000000000000000001"; + var hashLock = Convert.ToHexString(SHA256.HashData(Convert.FromHexString(preimage))); + + var mintHandler = await wallet + .CreateMintQuote() + .WithAmount(1337) + .WithHTLCLock( + new HTLCBuilder() + { + HashLock = hashLock, + Pubkeys = [privKeyBob.Key.CreatePubKey()], + SignatureThreshold = 1, + } + ) + .ProcessAsyncBolt11(); + + await PayInvoice(); + var htlcProofs = await mintHandler.Mint(); + + Assert.NotEmpty(htlcProofs); + Assert.Equal(1337UL, Utils.SumProofs(htlcProofs)); + + // try swap without preimage - should fail + await Assert.ThrowsAsync(async () => + { + await wallet.Swap().FromInputs(htlcProofs).WithPrivkeys([privKeyBob]).ProcessAsync(); + }); + + // swap with correct preimage and signature + var swappedProofs = await wallet + .Swap() + .FromInputs(htlcProofs) + .WithPrivkeys([privKeyBob]) + .WithHtlcPreimage(preimage) + .ProcessAsync(); + + Assert.NotEmpty(swappedProofs); + // fee is 100 ppk - it can be calculated before but here we don't care + Assert.Equal(1337UL - 1, Utils.SumProofs(swappedProofs)); + } + + [Fact] + public async Task MintSwapHTLCSigAll() + { + var wallet = Wallet.Create().WithMint(MintUrl); + + var privKeyBob = new PrivKey(RandomNumberGenerator.GetHexString(64, true)); + var preimage = "0000000000000000000000000000000000000000000000000000000000000001"; + var hashLock = Convert.ToHexString(SHA256.HashData(Convert.FromHexString(preimage))); + + var mintHandler = await wallet + .CreateMintQuote() + .WithAmount(1337) + .WithHTLCLock( + new HTLCBuilder() + { + HashLock = hashLock, + Pubkeys = [privKeyBob.Key.CreatePubKey()], + SignatureThreshold = 1, + SigFlag = "SIG_ALL", + } + ) + .ProcessAsyncBolt11(); + + await PayInvoice(); + var htlcProofs = await mintHandler.Mint(); + + Assert.NotEmpty(htlcProofs); + Assert.Equal(1337UL, Utils.SumProofs(htlcProofs)); + + await Assert.ThrowsAsync(async () => + { + await wallet.Swap().FromInputs(htlcProofs).WithPrivkeys([privKeyBob]).ProcessAsync(); + }); + + var swappedProofs = await wallet + .Swap() + .FromInputs(htlcProofs) + .WithPrivkeys([privKeyBob]) + .WithHtlcPreimage(preimage) + .ProcessAsync(); + + Assert.NotEmpty(swappedProofs); + Assert.Equal(1337UL - 1, Utils.SumProofs(swappedProofs)); + } + + [Fact] + public async Task MintSwapHtlcP2BkSigAll() + { + var wallet = Wallet.Create().WithMint(MintUrl); + + var privKeyBob = new PrivKey(RandomNumberGenerator.GetHexString(64, true)); + var preimage = "0000000000000000000000000000000000000000000000000000000000000001"; + var hashLock = Convert.ToHexString(SHA256.HashData(Convert.FromHexString(preimage))); + + var mintHandler = await wallet + .CreateMintQuote() + .WithAmount(1337) + .WithHTLCLock( + new HTLCBuilder() + { + HashLock = hashLock, + Pubkeys = [privKeyBob.Key.CreatePubKey()], + SignatureThreshold = 1, + SigFlag = "SIG_ALL", + } + ) + .BlindPubkeys() + .ProcessAsyncBolt11(); + + await PayInvoice(); + var htlcProofs = await mintHandler.Mint(); + + Assert.NotEmpty(htlcProofs); + Assert.Equal(1337UL, Utils.SumProofs(htlcProofs)); + + await Assert.ThrowsAsync(async () => + { + await wallet.Swap().FromInputs(htlcProofs).WithPrivkeys([privKeyBob]).ProcessAsync(); + }); + + var swappedProofs = await wallet + .Swap() + .FromInputs(htlcProofs) + .WithPrivkeys([privKeyBob]) + .WithHtlcPreimage(preimage) + .ProcessAsync(); + + Assert.NotEmpty(swappedProofs); + Assert.Equal(1337UL - 1, Utils.SumProofs(swappedProofs)); + } + + [Fact] + public async Task MintMeltHTLCP2Bk() + { + var wallet = Wallet.Create().WithMint(MintUrl); + + var privKeyBob = new PrivKey(RandomNumberGenerator.GetHexString(64, true)); + var preimage = new string('0', 63) + "1"; + + var builder = new HTLCBuilder() + { + Pubkeys = [privKeyBob.Key.CreatePubKey()], + HashLock = Convert.ToHexString(SHA256.HashData(Convert.FromHexString(preimage))), + }; + + var quote = await wallet + .CreateMintQuote() + .WithAmount(1337) + .WithHTLCLock(builder) + .BlindPubkeys() + .ProcessAsyncBolt11(); + + await PayInvoice(); + var proofs = await quote.Mint(); + + Assert.NotEmpty(proofs); + Assert.NotEmpty(proofs.Select(p => p.P2PkE)); + + var meltHandler = await wallet + .CreateMeltQuote() + .WithInvoice(valuesInvoices[1150]) + .WithPrivKeys([privKeyBob]) + .WithHTLCPreimage(preimage) + .ProcessAsyncBolt11(); + + var change = await meltHandler.Melt(proofs); + + Assert.NotEmpty(change); + } + + [Fact] + public async Task MintMeltHTLCP2BkSigAll() + { + var wallet = Wallet.Create().WithMint(MintUrl); + + var privKeyBob = new PrivKey(RandomNumberGenerator.GetHexString(64, true)); + var preimage = new string('0', 63) + "1"; + + var builder = new HTLCBuilder() + { + SigFlag = "SIG_ALL", + Pubkeys = [privKeyBob.Key.CreatePubKey()], + HashLock = Convert.ToHexString(SHA256.HashData(Convert.FromHexString(preimage))), + }; + + var quote = await wallet + .CreateMintQuote() + .WithAmount(1337) + .WithHTLCLock(builder) + .BlindPubkeys() + .ProcessAsyncBolt11(); + + await PayInvoice(); + var proofs = await quote.Mint(); + + Assert.NotEmpty(proofs); + Assert.NotEmpty(proofs.Select(p => p.P2PkE)); + + var meltHandler = await wallet + .CreateMeltQuote() + .WithInvoice(valuesInvoices[1151]) + .WithPrivKeys([privKeyBob]) + .WithHTLCPreimage(preimage) + .ProcessAsyncBolt11(); + + var change = await meltHandler.Melt(proofs); + + Assert.NotEmpty(change); + } + + [Fact] + public async Task SwapWithCustomAmounts() + { + var wallet = Wallet.Create().WithMint(MintUrl); + + // mint some proofs + var mintQuote = await wallet + .CreateMintQuote() + .WithAmount(100) + .WithUnit("sat") + .ProcessAsyncBolt11(); + + await PayInvoice(); + var mintedProofs = await mintQuote.Mint(); + Assert.NotEmpty(mintedProofs); + + // swap with specific amounts + var desiredAmounts = new List { 32, 32, 32, 2, 1 }; // 96 sat (should consume 1 for fees) + var newProofs = await wallet + .Swap() + .FromInputs(mintedProofs) + .WithAmounts(desiredAmounts) + .ProcessAsync(); + + Assert.NotEmpty(newProofs); + // amount should be at least the requested amounts + Assert.True(Utils.SumProofs(newProofs) >= 96); + } + + [Fact] + public async Task SwapToSpecificKeyset() + { + var wallet = Wallet.Create().WithMint(MintUrl); + + // get active keyset + var activeKeysetId = await wallet.GetActiveKeysetId("sat"); + Assert.NotNull(activeKeysetId); + + // mint some proofs + var mintQuote = await wallet + .CreateMintQuote() + .WithAmount(64) + .WithUnit("sat") + .ProcessAsyncBolt11(); + + await PayInvoice(); + var mintedProofs = await mintQuote.Mint(); + Assert.NotEmpty(mintedProofs); + + // swap to specific keyset + var newProofs = await wallet + .Swap() + .FromInputs(mintedProofs) + .ForKeyset(activeKeysetId) + .ProcessAsync(); + + Assert.NotEmpty(newProofs); + Assert.All(newProofs, p => Assert.Equal(activeKeysetId, p.Id)); + } + + [Fact] + public async Task MeltWithInsufficientFunds() + { + var wallet = Wallet.Create().WithMint(MintUrl); + + // mint small amount + var mintQuote = await wallet + .CreateMintQuote() + .WithAmount(10) + .WithUnit("sat") + .ProcessAsyncBolt11(); + + await PayInvoice(); + var mintedProofs = await mintQuote.Mint(); + Assert.NotEmpty(mintedProofs); + + // try to melt for larger invoice - should fail during proof selection + var meltHandler = await wallet + .CreateMeltQuote() + .WithInvoice(valuesInvoices[1000]) // 1000 sat invoice + .WithUnit("sat") + .ProcessAsyncBolt11(); + + var quote = meltHandler.GetQuote(); + var amountNeeded = quote.Amount + (ulong)quote.FeeReserve; + + // selectProofsToSend should return empty Send list when insufficient + var selection = await wallet.SelectProofsToSend(mintedProofs, amountNeeded, true); + Assert.Empty(selection.Send); + Assert.NotEmpty(selection.Keep); + } + + private async Task PayInvoice() + { + //We're using fakewallet, so after 3 secs it will get paid automatically. After 3.5 sec its 1000% paid. + await Task.Delay(3500); + } +} diff --git a/DotNut.Tests/UnitTest1.cs b/DotNut.Tests/UnitTest1.cs index 94b899b..7d56cf7 100644 --- a/DotNut.Tests/UnitTest1.cs +++ b/DotNut.Tests/UnitTest1.cs @@ -17,12 +17,18 @@ public UnitTest1(ITestOutputHelper testOutputHelper) _testOutputHelper = testOutputHelper; } - [InlineData("0000000000000000000000000000000000000000000000000000000000000000", - "024cce997d3b518f739663b757deaec95bcd9473c30a14ac2fd04023a739d1a725")] - [InlineData("0000000000000000000000000000000000000000000000000000000000000001", - "022e7158e11c9506f1aa4248bf531298daa7febd6194f003edcd9b93ade6253acf")] - [InlineData("0000000000000000000000000000000000000000000000000000000000000002", - "026cdbe15362df59cd1dd3c9c11de8aedac2106eca69236ecd9fbe117af897be4f")] + [InlineData( + "0000000000000000000000000000000000000000000000000000000000000000", + "024cce997d3b518f739663b757deaec95bcd9473c30a14ac2fd04023a739d1a725" + )] + [InlineData( + "0000000000000000000000000000000000000000000000000000000000000001", + "022e7158e11c9506f1aa4248bf531298daa7febd6194f003edcd9b93ade6253acf" + )] + [InlineData( + "0000000000000000000000000000000000000000000000000000000000000002", + "026cdbe15362df59cd1dd3c9c11de8aedac2106eca69236ecd9fbe117af897be4f" + )] [Theory] public void Nut00Tests_HashToCurve(string message, string point) { @@ -30,13 +36,16 @@ public void Nut00Tests_HashToCurve(string message, string point) Assert.Equal(point, result.ToHex()); } - - [InlineData("d341ee4871f1f889041e63cf0d3823c713eea6aff01e80f1719f08f9e5be98f6", + [InlineData( + "d341ee4871f1f889041e63cf0d3823c713eea6aff01e80f1719f08f9e5be98f6", "99fce58439fc37412ab3468b73db0569322588f62fb3a49182d67e23d877824a", - "033b1a9737a40cc3fd9b6af4b723632b76a67a36782596304612a6c2bfb5197e6d")] - [InlineData("f1aaf16c2239746f369572c0784d9dd3d032d952c2d992175873fb58fae31a60", + "033b1a9737a40cc3fd9b6af4b723632b76a67a36782596304612a6c2bfb5197e6d" + )] + [InlineData( + "f1aaf16c2239746f369572c0784d9dd3d032d952c2d992175873fb58fae31a60", "f78476ea7cc9ade20f9e05e58a804cf19533f03ea805ece5fee88c8e2874ba50", - "029bdf2d716ee366eddf599ba252786c1033f47e230248a4612a5670ab931f1763")] + "029bdf2d716ee366eddf599ba252786c1033f47e230248a4612a5670ab931f1763" + )] [Theory] public void Nut00Tests_BlindedMessages(string x, string r, string b) { @@ -47,12 +56,16 @@ public void Nut00Tests_BlindedMessages(string x, string r, string b) Assert.Equal(b, computedB.ToHex()); } - [InlineData("0000000000000000000000000000000000000000000000000000000000000001", + [InlineData( + "0000000000000000000000000000000000000000000000000000000000000001", "02a9acc1e48c25eeeb9289b5031cc57da9fe72f3fe2861d264bdc074209b107ba2", - "02a9acc1e48c25eeeb9289b5031cc57da9fe72f3fe2861d264bdc074209b107ba2")] - [InlineData("7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f", + "02a9acc1e48c25eeeb9289b5031cc57da9fe72f3fe2861d264bdc074209b107ba2" + )] + [InlineData( + "7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f", "02a9acc1e48c25eeeb9289b5031cc57da9fe72f3fe2861d264bdc074209b107ba2", - "0398bc70ce8184d27ba89834d19f5199c84443c31131e48d3c1214db24247d005d")] + "0398bc70ce8184d27ba89834d19f5199c84443c31131e48d3c1214db24247d005d" + )] [Theory] public void Nut00Tests_BlindedSignatures(string k, string b_, string blindedKey) { @@ -69,94 +82,122 @@ public void Nut00Tests_TokenSerialization() { string originalToken = "cashuAeyJ0b2tlbiI6W3sibWludCI6Imh0dHBzOi8vODMzMy5zcGFjZTozMzM4IiwicHJvb2ZzIjpbeyJhbW91bnQiOjIsImlkIjoiMDA5YTFmMjkzMjUzZTQxZSIsInNlY3JldCI6IjQwNzkxNWJjMjEyYmU2MWE3N2UzZTZkMmFlYjRjNzI3OTgwYmRhNTFjZDA2YTZhZmMyOWUyODYxNzY4YTc4MzciLCJDIjoiMDJiYzkwOTc5OTdkODFhZmIyY2M3MzQ2YjVlNDM0NWE5MzQ2YmQyYTUwNmViNzk1ODU5OGE3MmYwY2Y4NTE2M2VhIn0seyJhbW91bnQiOjgsImlkIjoiMDA5YTFmMjkzMjUzZTQxZSIsInNlY3JldCI6ImZlMTUxMDkzMTRlNjFkNzc1NmIwZjhlZTBmMjNhNjI0YWNhYTNmNGUwNDJmNjE0MzNjNzI4YzcwNTdiOTMxYmUiLCJDIjoiMDI5ZThlNTA1MGI4OTBhN2Q2YzA5NjhkYjE2YmMxZDVkNWZhMDQwZWExZGUyODRmNmVjNjlkNjEyOTlmNjcxMDU5In1dfV0sInVuaXQiOiJzYXQiLCJtZW1vIjoiVGhhbmsgeW91LiJ9"; - var result = CashuTokenHelper.Decode( - originalToken, - out var v); + var result = CashuTokenHelper.Decode(originalToken, out var v); Assert.Equal("A", v); Assert.Equal("Thank you.", result.Memo); Assert.Equal("sat", result.Unit); var token = Assert.Single(result.Tokens); Assert.Equal("https://8333.space:3338", token.Mint); Assert.Equal(2, token.Proofs.Count); - Assert.Collection(token.Proofs, proof => + Assert.Collection( + token.Proofs, + proof => { Assert.Equal((ulong)2, proof.Amount); Assert.Equal(new KeysetId("009a1f293253e41e"), proof.Id); - Assert.Equal("407915bc212be61a77e3e6d2aeb4c727980bda51cd06a6afc29e2861768a7837", - Assert.IsType(proof.Secret).Secret); - Assert.Equal("02bc9097997d81afb2cc7346b5e4345a9346bd2a506eb7958598a72f0cf85163ea".ToPubKey(), - (ECPubKey) proof.C); - }, proof => + Assert.Equal( + "407915bc212be61a77e3e6d2aeb4c727980bda51cd06a6afc29e2861768a7837", + Assert.IsType(proof.Secret).Secret + ); + Assert.Equal( + "02bc9097997d81afb2cc7346b5e4345a9346bd2a506eb7958598a72f0cf85163ea".ToPubKey(), + (ECPubKey)proof.C + ); + }, + proof => { Assert.Equal((ulong)8, proof.Amount); Assert.Equal(new KeysetId("009a1f293253e41e"), proof.Id); - Assert.Equal("fe15109314e61d7756b0f8ee0f23a624acaa3f4e042f61433c728c7057b931be", - Assert.IsType(proof.Secret).Secret); - Assert.Equal("029e8e5050b890a7d6c0968db16bc1d5d5fa040ea1de284f6ec69d61299f671059".ToPubKey(), - (ECPubKey) proof.C); + Assert.Equal( + "fe15109314e61d7756b0f8ee0f23a624acaa3f4e042f61433c728c7057b931be", + Assert.IsType(proof.Secret).Secret + ); + Assert.Equal( + "029e8e5050b890a7d6c0968db16bc1d5d5fa040ea1de284f6ec69d61299f671059".ToPubKey(), + (ECPubKey)proof.C + ); } ); Assert.Equal(originalToken, result.Encode("A", false)); - Assert.Throws(() => CashuTokenHelper.Decode( - "casshuAeyJ0b2tlbiI6W3sibWludCI6Imh0dHBzOi8vODMzMy5zcGFjZTozMzM4IiwicHJvb2ZzIjpbeyJhbW91bnQiOjIsImlkIjoiMDA5YTFmMjkzMjUzZTQxZSIsInNlY3JldCI6IjQwNzkxNWJjMjEyYmU2MWE3N2UzZTZkMmFlYjRjNzI3OTgwYmRhNTFjZDA2YTZhZmMyOWUyODYxNzY4YTc4MzciLCJDIjoiMDJiYzkwOTc5OTdkODFhZmIyY2M3MzQ2YjVlNDM0NWE5MzQ2YmQyYTUwNmViNzk1ODU5OGE3MmYwY2Y4NTE2M2VhIn0seyJhbW91bnQiOjgsImlkIjoiMDA5YTFmMjkzMjUzZTQxZSIsInNlY3JldCI6ImZlMTUxMDkzMTRlNjFkNzc1NmIwZjhlZTBmMjNhNjI0YWNhYTNmNGUwNDJmNjE0MzNjNzI4YzcwNTdiOTMxYmUiLCJDIjoiMDI5ZThlNTA1MGI4OTBhN2Q2YzA5NjhkYjE2YmMxZDVkNWZhMDQwZWExZGUyODRmNmVjNjlkNjEyOTlmNjcxMDU5In1dfV0sInVuaXQiOiJzYXQiLCJtZW1vIjoiVGhhbmsgeW91LiJ9", - out _)); - Assert.Throws(() => CashuTokenHelper.Decode( - "eyJ0b2tlbiI6W3sibWludCI6Imh0dHBzOi8vODMzMy5zcGFjZTozMzM4IiwicHJvb2ZzIjpbeyJhbW91bnQiOjIsImlkIjoiMDA5YTFmMjkzMjUzZTQxZSIsInNlY3JldCI6IjQwNzkxNWJjMjEyYmU2MWE3N2UzZTZkMmFlYjRjNzI3OTgwYmRhNTFjZDA2YTZhZmMyOWUyODYxNzY4YTc4MzciLCJDIjoiMDJiYzkwOTc5OTdkODFhZmIyY2M3MzQ2YjVlNDM0NWE5MzQ2YmQyYTUwNmViNzk1ODU5OGE3MmYwY2Y4NTE2M2VhIn0seyJhbW91bnQiOjgsImlkIjoiMDA5YTFmMjkzMjUzZTQxZSIsInNlY3JldCI6ImZlMTUxMDkzMTRlNjFkNzc1NmIwZjhlZTBmMjNhNjI0YWNhYTNmNGUwNDJmNjE0MzNjNzI4YzcwNTdiOTMxYmUiLCJDIjoiMDI5ZThlNTA1MGI4OTBhN2Q2YzA5NjhkYjE2YmMxZDVkNWZhMDQwZWExZGUyODRmNmVjNjlkNjEyOTlmNjcxMDU5In1dfV0sInVuaXQiOiJzYXQiLCJtZW1vIjoiVGhhbmsgeW91LiJ9", - out _)); - - + Assert.Throws(() => + CashuTokenHelper.Decode( + "casshuAeyJ0b2tlbiI6W3sibWludCI6Imh0dHBzOi8vODMzMy5zcGFjZTozMzM4IiwicHJvb2ZzIjpbeyJhbW91bnQiOjIsImlkIjoiMDA5YTFmMjkzMjUzZTQxZSIsInNlY3JldCI6IjQwNzkxNWJjMjEyYmU2MWE3N2UzZTZkMmFlYjRjNzI3OTgwYmRhNTFjZDA2YTZhZmMyOWUyODYxNzY4YTc4MzciLCJDIjoiMDJiYzkwOTc5OTdkODFhZmIyY2M3MzQ2YjVlNDM0NWE5MzQ2YmQyYTUwNmViNzk1ODU5OGE3MmYwY2Y4NTE2M2VhIn0seyJhbW91bnQiOjgsImlkIjoiMDA5YTFmMjkzMjUzZTQxZSIsInNlY3JldCI6ImZlMTUxMDkzMTRlNjFkNzc1NmIwZjhlZTBmMjNhNjI0YWNhYTNmNGUwNDJmNjE0MzNjNzI4YzcwNTdiOTMxYmUiLCJDIjoiMDI5ZThlNTA1MGI4OTBhN2Q2YzA5NjhkYjE2YmMxZDVkNWZhMDQwZWExZGUyODRmNmVjNjlkNjEyOTlmNjcxMDU5In1dfV0sInVuaXQiOiJzYXQiLCJtZW1vIjoiVGhhbmsgeW91LiJ9", + out _ + ) + ); + Assert.Throws(() => + CashuTokenHelper.Decode( + "eyJ0b2tlbiI6W3sibWludCI6Imh0dHBzOi8vODMzMy5zcGFjZTozMzM4IiwicHJvb2ZzIjpbeyJhbW91bnQiOjIsImlkIjoiMDA5YTFmMjkzMjUzZTQxZSIsInNlY3JldCI6IjQwNzkxNWJjMjEyYmU2MWE3N2UzZTZkMmFlYjRjNzI3OTgwYmRhNTFjZDA2YTZhZmMyOWUyODYxNzY4YTc4MzciLCJDIjoiMDJiYzkwOTc5OTdkODFhZmIyY2M3MzQ2YjVlNDM0NWE5MzQ2YmQyYTUwNmViNzk1ODU5OGE3MmYwY2Y4NTE2M2VhIn0seyJhbW91bnQiOjgsImlkIjoiMDA5YTFmMjkzMjUzZTQxZSIsInNlY3JldCI6ImZlMTUxMDkzMTRlNjFkNzc1NmIwZjhlZTBmMjNhNjI0YWNhYTNmNGUwNDJmNjE0MzNjNzI4YzcwNTdiOTMxYmUiLCJDIjoiMDI5ZThlNTA1MGI4OTBhN2Q2YzA5NjhkYjE2YmMxZDVkNWZhMDQwZWExZGUyODRmNmVjNjlkNjEyOTlmNjcxMDU5In1dfV0sInVuaXQiOiJzYXQiLCJtZW1vIjoiVGhhbmsgeW91LiJ9", + out _ + ) + ); - var v4Token = "cashuBo2F0gqJhaUgA_9SLj17PgGFwgaNhYQFhc3hAYWNjMTI0MzVlN2I4NDg0YzNjZjE4NTAxNDkyMThhZjkwZjcxNmE1MmJmNGE1ZWQzNDdlNDhlY2MxM2Y3NzM4OGFjWCECRFODGd5IXVW-07KaZCvuWHk3WrnnpiDhHki6SCQh88-iYWlIAK0mjE0fWCZhcIKjYWECYXN4QDEzMjNkM2Q0NzA3YTU4YWQyZTIzYWRhNGU5ZjFmNDlmNWE1YjRhYzdiNzA4ZWIwZDYxZjczOGY0ODMwN2U4ZWVhY1ghAjRWqhENhLSsdHrr2Cw7AFrKUL9Ffr1XN6RBT6w659lNo2FhAWFzeEA1NmJjYmNiYjdjYzY0MDZiM2ZhNWQ1N2QyMTc0ZjRlZmY4YjQ0MDJiMTc2OTI2ZDNhNTdkM2MzZGNiYjU5ZDU3YWNYIQJzEpxXGeWZN5qXSmJjY8MzxWyvwObQGr5G1YCCgHicY2FtdWh0dHA6Ly9sb2NhbGhvc3Q6MzMzOGF1Y3NhdA"; result = CashuTokenHelper.Decode(v4Token, out v); - + Assert.Equal("B", v); Assert.Null(result.Memo); Assert.Equal("sat", result.Unit); token = Assert.Single(result.Tokens); Assert.Equal("http://localhost:3338", token.Mint); Assert.Equal(3, token.Proofs.Count); - Assert.Collection(token.Proofs, proof => + Assert.Collection( + token.Proofs, + proof => { Assert.Equal((ulong)1, proof.Amount); Assert.Equal(new KeysetId("00ffd48b8f5ecf80"), proof.Id); - Assert.Equal("acc12435e7b8484c3cf1850149218af90f716a52bf4a5ed347e48ecc13f77388", - Assert.IsType(proof.Secret).Secret); - Assert.Equal("0244538319de485d55bed3b29a642bee5879375ab9e7a620e11e48ba482421f3cf".ToPubKey(), - (ECPubKey) proof.C); - }, proof => + Assert.Equal( + "acc12435e7b8484c3cf1850149218af90f716a52bf4a5ed347e48ecc13f77388", + Assert.IsType(proof.Secret).Secret + ); + Assert.Equal( + "0244538319de485d55bed3b29a642bee5879375ab9e7a620e11e48ba482421f3cf".ToPubKey(), + (ECPubKey)proof.C + ); + }, + proof => { Assert.Equal((ulong)2, proof.Amount); Assert.Equal(new KeysetId("00ad268c4d1f5826"), proof.Id); - Assert.Equal("1323d3d4707a58ad2e23ada4e9f1f49f5a5b4ac7b708eb0d61f738f48307e8ee", - Assert.IsType(proof.Secret).Secret); - Assert.Equal("023456aa110d84b4ac747aebd82c3b005aca50bf457ebd5737a4414fac3ae7d94d".ToPubKey(), - (ECPubKey) proof.C); - }, proof => + Assert.Equal( + "1323d3d4707a58ad2e23ada4e9f1f49f5a5b4ac7b708eb0d61f738f48307e8ee", + Assert.IsType(proof.Secret).Secret + ); + Assert.Equal( + "023456aa110d84b4ac747aebd82c3b005aca50bf457ebd5737a4414fac3ae7d94d".ToPubKey(), + (ECPubKey)proof.C + ); + }, + proof => { Assert.Equal((ulong)1, proof.Amount); Assert.Equal(new KeysetId("00ad268c4d1f5826"), proof.Id); - Assert.Equal("56bcbcbb7cc6406b3fa5d57d2174f4eff8b4402b176926d3a57d3c3dcbb59d57", - Assert.IsType(proof.Secret).Secret); - Assert.Equal("0273129c5719e599379a974a626363c333c56cafc0e6d01abe46d5808280789c63".ToPubKey(), - (ECPubKey) proof.C); + Assert.Equal( + "56bcbcbb7cc6406b3fa5d57d2174f4eff8b4402b176926d3a57d3c3dcbb59d57", + Assert.IsType(proof.Secret).Secret + ); + Assert.Equal( + "0273129c5719e599379a974a626363c333c56cafc0e6d01abe46d5808280789c63".ToPubKey(), + (ECPubKey)proof.C + ); } ); Assert.Equal(v4Token, result.Encode("B", false)); - - } [Theory] [InlineData( - "{\n \"1\":\"03a40f20667ed53513075dc51e715ff2046cad64eb68960632269ba7f0210e38\",\"2\":\"03fd4ce5a16b65576145949e6f99f445f8249fee17c606b688b504a849cdc452de\",\"4\":\"02648eccfa4c026960966276fa5a4cae46ce0fd432211a4f449bf84f13aa5f8303\",\"8\":\"02fdfd6796bfeac490cbee12f778f867f0a2c68f6508d17c649759ea0dc3547528\"\n}")] + "{\n \"1\":\"03a40f20667ed53513075dc51e715ff2046cad64eb68960632269ba7f0210e38\",\"2\":\"03fd4ce5a16b65576145949e6f99f445f8249fee17c606b688b504a849cdc452de\",\"4\":\"02648eccfa4c026960966276fa5a4cae46ce0fd432211a4f449bf84f13aa5f8303\",\"8\":\"02fdfd6796bfeac490cbee12f778f867f0a2c68f6508d17c649759ea0dc3547528\"\n}" + )] [InlineData( - "{\n \"1\":\"03a40f20667ed53513075dc51e715ff2046cad64eb68960632269ba7f0210e38bc\",\"2\":\"04fd4ce5a16b65576145949e6f99f445f8249fee17c606b688b504a849cdc452de3625246cb2c27dac965cb7200a5986467eee92eb7d496bbf1453b074e223e481\",\"4\":\"02648eccfa4c026960966276fa5a4cae46ce0fd432211a4f449bf84f13aa5f8303\",\"8\":\"02fdfd6796bfeac490cbee12f778f867f0a2c68f6508d17c649759ea0dc3547528\"\n}")] + "{\n \"1\":\"03a40f20667ed53513075dc51e715ff2046cad64eb68960632269ba7f0210e38bc\",\"2\":\"04fd4ce5a16b65576145949e6f99f445f8249fee17c606b688b504a849cdc452de3625246cb2c27dac965cb7200a5986467eee92eb7d496bbf1453b074e223e481\",\"4\":\"02648eccfa4c026960966276fa5a4cae46ce0fd432211a4f449bf84f13aa5f8303\",\"8\":\"02fdfd6796bfeac490cbee12f778f867f0a2c68f6508d17c649759ea0dc3547528\"\n}" + )] public void Nut01Tests_Keysets_Invalid(string keyset) { Assert.ThrowsAny(() => JsonSerializer.Deserialize(keyset)); @@ -164,9 +205,11 @@ public void Nut01Tests_Keysets_Invalid(string keyset) [Theory] [InlineData( - "{\n \"1\":\"03a40f20667ed53513075dc51e715ff2046cad64eb68960632269ba7f0210e38bc\",\"2\":\"03fd4ce5a16b65576145949e6f99f445f8249fee17c606b688b504a849cdc452de\",\"4\":\"02648eccfa4c026960966276fa5a4cae46ce0fd432211a4f449bf84f13aa5f8303\",\"8\":\"02fdfd6796bfeac490cbee12f778f867f0a2c68f6508d17c649759ea0dc3547528\"\n}")] + "{\n \"1\":\"03a40f20667ed53513075dc51e715ff2046cad64eb68960632269ba7f0210e38bc\",\"2\":\"03fd4ce5a16b65576145949e6f99f445f8249fee17c606b688b504a849cdc452de\",\"4\":\"02648eccfa4c026960966276fa5a4cae46ce0fd432211a4f449bf84f13aa5f8303\",\"8\":\"02fdfd6796bfeac490cbee12f778f867f0a2c68f6508d17c649759ea0dc3547528\"\n}" + )] [InlineData( - "{\n \"1\":\"03ba786a2c0745f8c30e490288acd7a72dd53d65afd292ddefa326a4a3fa14c566\",\"2\":\"03361cd8bd1329fea797a6add1cf1990ffcf2270ceb9fc81eeee0e8e9c1bd0cdf5\",\"4\":\"036e378bcf78738ddf68859293c69778035740e41138ab183c94f8fee7572214c7\",\"8\":\"03909d73beaf28edfb283dbeb8da321afd40651e8902fcf5454ecc7d69788626c0\",\"16\":\"028a36f0e6638ea7466665fe174d958212723019ec08f9ce6898d897f88e68aa5d\",\"32\":\"03a97a40e146adee2687ac60c2ba2586a90f970de92a9d0e6cae5a4b9965f54612\",\"64\":\"03ce86f0c197aab181ddba0cfc5c5576e11dfd5164d9f3d4a3fc3ffbbf2e069664\",\"128\":\"0284f2c06d938a6f78794814c687560a0aabab19fe5e6f30ede38e113b132a3cb9\",\"256\":\"03b99f475b68e5b4c0ba809cdecaae64eade2d9787aa123206f91cd61f76c01459\",\"512\":\"03d4db82ea19a44d35274de51f78af0a710925fe7d9e03620b84e3e9976e3ac2eb\",\"1024\":\"031fbd4ba801870871d46cf62228a1b748905ebc07d3b210daf48de229e683f2dc\",\"2048\":\"0276cedb9a3b160db6a158ad4e468d2437f021293204b3cd4bf6247970d8aff54b\",\"4096\":\"02fc6b89b403ee9eb8a7ed457cd3973638080d6e04ca8af7307c965c166b555ea2\",\"8192\":\"0320265583e916d3a305f0d2687fcf2cd4e3cd03a16ea8261fda309c3ec5721e21\",\"16384\":\"036e41de58fdff3cb1d8d713f48c63bc61fa3b3e1631495a444d178363c0d2ed50\",\"32768\":\"0365438f613f19696264300b069d1dad93f0c60a37536b72a8ab7c7366a5ee6c04\",\"65536\":\"02408426cfb6fc86341bac79624ba8708a4376b2d92debdf4134813f866eb57a8d\",\"131072\":\"031063e9f11c94dc778c473e968966eac0e70b7145213fbaff5f7a007e71c65f41\",\"262144\":\"02f2a3e808f9cd168ec71b7f328258d0c1dda250659c1aced14c7f5cf05aab4328\",\"524288\":\"038ac10de9f1ff9395903bb73077e94dbf91e9ef98fd77d9a2debc5f74c575bc86\",\"1048576\":\"0203eaee4db749b0fc7c49870d082024b2c31d889f9bc3b32473d4f1dfa3625788\",\"2097152\":\"033cdb9d36e1e82ae652b7b6a08e0204569ec7ff9ebf85d80a02786dc7fe00b04c\",\"4194304\":\"02c8b73f4e3a470ae05e5f2fe39984d41e9f6ae7be9f3b09c9ac31292e403ac512\",\"8388608\":\"025bbe0cfce8a1f4fbd7f3a0d4a09cb6badd73ef61829dc827aa8a98c270bc25b0\",\"16777216\":\"037eec3d1651a30a90182d9287a5c51386fe35d4a96839cf7969c6e2a03db1fc21\",\"33554432\":\"03280576b81a04e6abd7197f305506476f5751356b7643988495ca5c3e14e5c262\",\"67108864\":\"03268bfb05be1dbb33ab6e7e00e438373ca2c9b9abc018fdb452d0e1a0935e10d3\",\"134217728\":\"02573b68784ceba9617bbcc7c9487836d296aa7c628c3199173a841e7a19798020\",\"268435456\":\"0234076b6e70f7fbf755d2227ecc8d8169d662518ee3a1401f729e2a12ccb2b276\",\"536870912\":\"03015bd88961e2a466a2163bd4248d1d2b42c7c58a157e594785e7eb34d880efc9\",\"1073741824\":\"02c9b076d08f9020ebee49ac8ba2610b404d4e553a4f800150ceb539e9421aaeee\",\"2147483648\":\"034d592f4c366afddc919a509600af81b489a03caf4f7517c2b3f4f2b558f9a41a\",\"4294967296\":\"037c09ecb66da082981e4cbdb1ac65c0eb631fc75d85bed13efb2c6364148879b5\",\"8589934592\":\"02b4ebb0dda3b9ad83b39e2e31024b777cc0ac205a96b9a6cfab3edea2912ed1b3\",\"17179869184\":\"026cc4dacdced45e63f6e4f62edbc5779ccd802e7fabb82d5123db879b636176e9\",\"34359738368\":\"02b2cee01b7d8e90180254459b8f09bbea9aad34c3a2fd98c85517ecfc9805af75\",\"68719476736\":\"037a0c0d564540fc574b8bfa0253cca987b75466e44b295ed59f6f8bd41aace754\",\"137438953472\":\"021df6585cae9b9ca431318a713fd73dbb76b3ef5667957e8633bca8aaa7214fb6\",\"274877906944\":\"02b8f53dde126f8c85fa5bb6061c0be5aca90984ce9b902966941caf963648d53a\",\"549755813888\":\"029cc8af2840d59f1d8761779b2496623c82c64be8e15f9ab577c657c6dd453785\",\"1099511627776\":\"03e446fdb84fad492ff3a25fc1046fb9a93a5b262ebcd0151caa442ea28959a38a\",\"2199023255552\":\"02d6b25bd4ab599dd0818c55f75702fde603c93f259222001246569018842d3258\",\"4398046511104\":\"03397b522bb4e156ec3952d3f048e5a986c20a00718e5e52cd5718466bf494156a\",\"8796093022208\":\"02d1fb9e78262b5d7d74028073075b80bb5ab281edcfc3191061962c1346340f1e\",\"17592186044416\":\"030d3f2ad7a4ca115712ff7f140434f802b19a4c9b2dd1c76f3e8e80c05c6a9310\",\"35184372088832\":\"03e325b691f292e1dfb151c3fb7cad440b225795583c32e24e10635a80e4221c06\",\"70368744177664\":\"03bee8f64d88de3dee21d61f89efa32933da51152ddbd67466bef815e9f93f8fd1\",\"140737488355328\":\"0327244c9019a4892e1f04ba3bf95fe43b327479e2d57c25979446cc508cd379ed\",\"281474976710656\":\"02fb58522cd662f2f8b042f8161caae6e45de98283f74d4e99f19b0ea85e08a56d\",\"562949953421312\":\"02adde4b466a9d7e59386b6a701a39717c53f30c4810613c1b55e6b6da43b7bc9a\",\"1125899906842624\":\"038eeda11f78ce05c774f30e393cda075192b890d68590813ff46362548528dca9\",\"2251799813685248\":\"02ec13e0058b196db80f7079d329333b330dc30c000dbdd7397cbbc5a37a664c4f\",\"4503599627370496\":\"02d2d162db63675bd04f7d56df04508840f41e2ad87312a3c93041b494efe80a73\",\"9007199254740992\":\"0356969d6aef2bb40121dbd07c68b6102339f4ea8e674a9008bb69506795998f49\",\"18014398509481984\":\"02f4e667567ebb9f4e6e180a4113bb071c48855f657766bb5e9c776a880335d1d6\",\"36028797018963968\":\"0385b4fe35e41703d7a657d957c67bb536629de57b7e6ee6fe2130728ef0fc90b0\",\"72057594037927936\":\"02b2bc1968a6fddbcc78fb9903940524824b5f5bed329c6ad48a19b56068c144fd\",\"144115188075855872\":\"02e0dbb24f1d288a693e8a49bc14264d1276be16972131520cf9e055ae92fba19a\",\"288230376151711744\":\"03efe75c106f931a525dc2d653ebedddc413a2c7d8cb9da410893ae7d2fa7d19cc\",\"576460752303423488\":\"02c7ec2bd9508a7fc03f73c7565dc600b30fd86f3d305f8f139c45c404a52d958a\",\"1152921504606846976\":\"035a6679c6b25e68ff4e29d1c7ef87f21e0a8fc574f6a08c1aa45ff352c1d59f06\",\"2305843009213693952\":\"033cdc225962c052d485f7cfbf55a5b2367d200fe1fe4373a347deb4cc99e9a099\",\"4611686018427387904\":\"024a4b806cf413d14b294719090a9da36ba75209c7657135ad09bc65328fba9e6f\",\"9223372036854775808\":\"0377a6fe114e291a8d8e991627c38001c8305b23b9e98b1c7b1893f5cd0dda6cad\"\n}")] + "{\n \"1\":\"03ba786a2c0745f8c30e490288acd7a72dd53d65afd292ddefa326a4a3fa14c566\",\"2\":\"03361cd8bd1329fea797a6add1cf1990ffcf2270ceb9fc81eeee0e8e9c1bd0cdf5\",\"4\":\"036e378bcf78738ddf68859293c69778035740e41138ab183c94f8fee7572214c7\",\"8\":\"03909d73beaf28edfb283dbeb8da321afd40651e8902fcf5454ecc7d69788626c0\",\"16\":\"028a36f0e6638ea7466665fe174d958212723019ec08f9ce6898d897f88e68aa5d\",\"32\":\"03a97a40e146adee2687ac60c2ba2586a90f970de92a9d0e6cae5a4b9965f54612\",\"64\":\"03ce86f0c197aab181ddba0cfc5c5576e11dfd5164d9f3d4a3fc3ffbbf2e069664\",\"128\":\"0284f2c06d938a6f78794814c687560a0aabab19fe5e6f30ede38e113b132a3cb9\",\"256\":\"03b99f475b68e5b4c0ba809cdecaae64eade2d9787aa123206f91cd61f76c01459\",\"512\":\"03d4db82ea19a44d35274de51f78af0a710925fe7d9e03620b84e3e9976e3ac2eb\",\"1024\":\"031fbd4ba801870871d46cf62228a1b748905ebc07d3b210daf48de229e683f2dc\",\"2048\":\"0276cedb9a3b160db6a158ad4e468d2437f021293204b3cd4bf6247970d8aff54b\",\"4096\":\"02fc6b89b403ee9eb8a7ed457cd3973638080d6e04ca8af7307c965c166b555ea2\",\"8192\":\"0320265583e916d3a305f0d2687fcf2cd4e3cd03a16ea8261fda309c3ec5721e21\",\"16384\":\"036e41de58fdff3cb1d8d713f48c63bc61fa3b3e1631495a444d178363c0d2ed50\",\"32768\":\"0365438f613f19696264300b069d1dad93f0c60a37536b72a8ab7c7366a5ee6c04\",\"65536\":\"02408426cfb6fc86341bac79624ba8708a4376b2d92debdf4134813f866eb57a8d\",\"131072\":\"031063e9f11c94dc778c473e968966eac0e70b7145213fbaff5f7a007e71c65f41\",\"262144\":\"02f2a3e808f9cd168ec71b7f328258d0c1dda250659c1aced14c7f5cf05aab4328\",\"524288\":\"038ac10de9f1ff9395903bb73077e94dbf91e9ef98fd77d9a2debc5f74c575bc86\",\"1048576\":\"0203eaee4db749b0fc7c49870d082024b2c31d889f9bc3b32473d4f1dfa3625788\",\"2097152\":\"033cdb9d36e1e82ae652b7b6a08e0204569ec7ff9ebf85d80a02786dc7fe00b04c\",\"4194304\":\"02c8b73f4e3a470ae05e5f2fe39984d41e9f6ae7be9f3b09c9ac31292e403ac512\",\"8388608\":\"025bbe0cfce8a1f4fbd7f3a0d4a09cb6badd73ef61829dc827aa8a98c270bc25b0\",\"16777216\":\"037eec3d1651a30a90182d9287a5c51386fe35d4a96839cf7969c6e2a03db1fc21\",\"33554432\":\"03280576b81a04e6abd7197f305506476f5751356b7643988495ca5c3e14e5c262\",\"67108864\":\"03268bfb05be1dbb33ab6e7e00e438373ca2c9b9abc018fdb452d0e1a0935e10d3\",\"134217728\":\"02573b68784ceba9617bbcc7c9487836d296aa7c628c3199173a841e7a19798020\",\"268435456\":\"0234076b6e70f7fbf755d2227ecc8d8169d662518ee3a1401f729e2a12ccb2b276\",\"536870912\":\"03015bd88961e2a466a2163bd4248d1d2b42c7c58a157e594785e7eb34d880efc9\",\"1073741824\":\"02c9b076d08f9020ebee49ac8ba2610b404d4e553a4f800150ceb539e9421aaeee\",\"2147483648\":\"034d592f4c366afddc919a509600af81b489a03caf4f7517c2b3f4f2b558f9a41a\",\"4294967296\":\"037c09ecb66da082981e4cbdb1ac65c0eb631fc75d85bed13efb2c6364148879b5\",\"8589934592\":\"02b4ebb0dda3b9ad83b39e2e31024b777cc0ac205a96b9a6cfab3edea2912ed1b3\",\"17179869184\":\"026cc4dacdced45e63f6e4f62edbc5779ccd802e7fabb82d5123db879b636176e9\",\"34359738368\":\"02b2cee01b7d8e90180254459b8f09bbea9aad34c3a2fd98c85517ecfc9805af75\",\"68719476736\":\"037a0c0d564540fc574b8bfa0253cca987b75466e44b295ed59f6f8bd41aace754\",\"137438953472\":\"021df6585cae9b9ca431318a713fd73dbb76b3ef5667957e8633bca8aaa7214fb6\",\"274877906944\":\"02b8f53dde126f8c85fa5bb6061c0be5aca90984ce9b902966941caf963648d53a\",\"549755813888\":\"029cc8af2840d59f1d8761779b2496623c82c64be8e15f9ab577c657c6dd453785\",\"1099511627776\":\"03e446fdb84fad492ff3a25fc1046fb9a93a5b262ebcd0151caa442ea28959a38a\",\"2199023255552\":\"02d6b25bd4ab599dd0818c55f75702fde603c93f259222001246569018842d3258\",\"4398046511104\":\"03397b522bb4e156ec3952d3f048e5a986c20a00718e5e52cd5718466bf494156a\",\"8796093022208\":\"02d1fb9e78262b5d7d74028073075b80bb5ab281edcfc3191061962c1346340f1e\",\"17592186044416\":\"030d3f2ad7a4ca115712ff7f140434f802b19a4c9b2dd1c76f3e8e80c05c6a9310\",\"35184372088832\":\"03e325b691f292e1dfb151c3fb7cad440b225795583c32e24e10635a80e4221c06\",\"70368744177664\":\"03bee8f64d88de3dee21d61f89efa32933da51152ddbd67466bef815e9f93f8fd1\",\"140737488355328\":\"0327244c9019a4892e1f04ba3bf95fe43b327479e2d57c25979446cc508cd379ed\",\"281474976710656\":\"02fb58522cd662f2f8b042f8161caae6e45de98283f74d4e99f19b0ea85e08a56d\",\"562949953421312\":\"02adde4b466a9d7e59386b6a701a39717c53f30c4810613c1b55e6b6da43b7bc9a\",\"1125899906842624\":\"038eeda11f78ce05c774f30e393cda075192b890d68590813ff46362548528dca9\",\"2251799813685248\":\"02ec13e0058b196db80f7079d329333b330dc30c000dbdd7397cbbc5a37a664c4f\",\"4503599627370496\":\"02d2d162db63675bd04f7d56df04508840f41e2ad87312a3c93041b494efe80a73\",\"9007199254740992\":\"0356969d6aef2bb40121dbd07c68b6102339f4ea8e674a9008bb69506795998f49\",\"18014398509481984\":\"02f4e667567ebb9f4e6e180a4113bb071c48855f657766bb5e9c776a880335d1d6\",\"36028797018963968\":\"0385b4fe35e41703d7a657d957c67bb536629de57b7e6ee6fe2130728ef0fc90b0\",\"72057594037927936\":\"02b2bc1968a6fddbcc78fb9903940524824b5f5bed329c6ad48a19b56068c144fd\",\"144115188075855872\":\"02e0dbb24f1d288a693e8a49bc14264d1276be16972131520cf9e055ae92fba19a\",\"288230376151711744\":\"03efe75c106f931a525dc2d653ebedddc413a2c7d8cb9da410893ae7d2fa7d19cc\",\"576460752303423488\":\"02c7ec2bd9508a7fc03f73c7565dc600b30fd86f3d305f8f139c45c404a52d958a\",\"1152921504606846976\":\"035a6679c6b25e68ff4e29d1c7ef87f21e0a8fc574f6a08c1aa45ff352c1d59f06\",\"2305843009213693952\":\"033cdc225962c052d485f7cfbf55a5b2367d200fe1fe4373a347deb4cc99e9a099\",\"4611686018427387904\":\"024a4b806cf413d14b294719090a9da36ba75209c7657135ad09bc65328fba9e6f\",\"9223372036854775808\":\"0377a6fe114e291a8d8e991627c38001c8305b23b9e98b1c7b1893f5cd0dda6cad\"\n}" + )] public void Nut01Tests_Keysets_Valid(string keyset) { JsonSerializer.Deserialize(keyset); @@ -174,35 +217,53 @@ public void Nut01Tests_Keysets_Valid(string keyset) [Theory] // v1 - [InlineData("00456a94ab4e1c46", - "{\n \"1\":\"03a40f20667ed53513075dc51e715ff2046cad64eb68960632269ba7f0210e38bc\",\"2\":\"03fd4ce5a16b65576145949e6f99f445f8249fee17c606b688b504a849cdc452de\",\"4\":\"02648eccfa4c026960966276fa5a4cae46ce0fd432211a4f449bf84f13aa5f8303\",\"8\":\"02fdfd6796bfeac490cbee12f778f867f0a2c68f6508d17c649759ea0dc3547528\"\n}")] - [InlineData("000f01df73ea149a", - "{\n \"1\":\"03ba786a2c0745f8c30e490288acd7a72dd53d65afd292ddefa326a4a3fa14c566\",\"2\":\"03361cd8bd1329fea797a6add1cf1990ffcf2270ceb9fc81eeee0e8e9c1bd0cdf5\",\"4\":\"036e378bcf78738ddf68859293c69778035740e41138ab183c94f8fee7572214c7\",\"8\":\"03909d73beaf28edfb283dbeb8da321afd40651e8902fcf5454ecc7d69788626c0\",\"16\":\"028a36f0e6638ea7466665fe174d958212723019ec08f9ce6898d897f88e68aa5d\",\"32\":\"03a97a40e146adee2687ac60c2ba2586a90f970de92a9d0e6cae5a4b9965f54612\",\"64\":\"03ce86f0c197aab181ddba0cfc5c5576e11dfd5164d9f3d4a3fc3ffbbf2e069664\",\"128\":\"0284f2c06d938a6f78794814c687560a0aabab19fe5e6f30ede38e113b132a3cb9\",\"256\":\"03b99f475b68e5b4c0ba809cdecaae64eade2d9787aa123206f91cd61f76c01459\",\"512\":\"03d4db82ea19a44d35274de51f78af0a710925fe7d9e03620b84e3e9976e3ac2eb\",\"1024\":\"031fbd4ba801870871d46cf62228a1b748905ebc07d3b210daf48de229e683f2dc\",\"2048\":\"0276cedb9a3b160db6a158ad4e468d2437f021293204b3cd4bf6247970d8aff54b\",\"4096\":\"02fc6b89b403ee9eb8a7ed457cd3973638080d6e04ca8af7307c965c166b555ea2\",\"8192\":\"0320265583e916d3a305f0d2687fcf2cd4e3cd03a16ea8261fda309c3ec5721e21\",\"16384\":\"036e41de58fdff3cb1d8d713f48c63bc61fa3b3e1631495a444d178363c0d2ed50\",\"32768\":\"0365438f613f19696264300b069d1dad93f0c60a37536b72a8ab7c7366a5ee6c04\",\"65536\":\"02408426cfb6fc86341bac79624ba8708a4376b2d92debdf4134813f866eb57a8d\",\"131072\":\"031063e9f11c94dc778c473e968966eac0e70b7145213fbaff5f7a007e71c65f41\",\"262144\":\"02f2a3e808f9cd168ec71b7f328258d0c1dda250659c1aced14c7f5cf05aab4328\",\"524288\":\"038ac10de9f1ff9395903bb73077e94dbf91e9ef98fd77d9a2debc5f74c575bc86\",\"1048576\":\"0203eaee4db749b0fc7c49870d082024b2c31d889f9bc3b32473d4f1dfa3625788\",\"2097152\":\"033cdb9d36e1e82ae652b7b6a08e0204569ec7ff9ebf85d80a02786dc7fe00b04c\",\"4194304\":\"02c8b73f4e3a470ae05e5f2fe39984d41e9f6ae7be9f3b09c9ac31292e403ac512\",\"8388608\":\"025bbe0cfce8a1f4fbd7f3a0d4a09cb6badd73ef61829dc827aa8a98c270bc25b0\",\"16777216\":\"037eec3d1651a30a90182d9287a5c51386fe35d4a96839cf7969c6e2a03db1fc21\",\"33554432\":\"03280576b81a04e6abd7197f305506476f5751356b7643988495ca5c3e14e5c262\",\"67108864\":\"03268bfb05be1dbb33ab6e7e00e438373ca2c9b9abc018fdb452d0e1a0935e10d3\",\"134217728\":\"02573b68784ceba9617bbcc7c9487836d296aa7c628c3199173a841e7a19798020\",\"268435456\":\"0234076b6e70f7fbf755d2227ecc8d8169d662518ee3a1401f729e2a12ccb2b276\",\"536870912\":\"03015bd88961e2a466a2163bd4248d1d2b42c7c58a157e594785e7eb34d880efc9\",\"1073741824\":\"02c9b076d08f9020ebee49ac8ba2610b404d4e553a4f800150ceb539e9421aaeee\",\"2147483648\":\"034d592f4c366afddc919a509600af81b489a03caf4f7517c2b3f4f2b558f9a41a\",\"4294967296\":\"037c09ecb66da082981e4cbdb1ac65c0eb631fc75d85bed13efb2c6364148879b5\",\"8589934592\":\"02b4ebb0dda3b9ad83b39e2e31024b777cc0ac205a96b9a6cfab3edea2912ed1b3\",\"17179869184\":\"026cc4dacdced45e63f6e4f62edbc5779ccd802e7fabb82d5123db879b636176e9\",\"34359738368\":\"02b2cee01b7d8e90180254459b8f09bbea9aad34c3a2fd98c85517ecfc9805af75\",\"68719476736\":\"037a0c0d564540fc574b8bfa0253cca987b75466e44b295ed59f6f8bd41aace754\",\"137438953472\":\"021df6585cae9b9ca431318a713fd73dbb76b3ef5667957e8633bca8aaa7214fb6\",\"274877906944\":\"02b8f53dde126f8c85fa5bb6061c0be5aca90984ce9b902966941caf963648d53a\",\"549755813888\":\"029cc8af2840d59f1d8761779b2496623c82c64be8e15f9ab577c657c6dd453785\",\"1099511627776\":\"03e446fdb84fad492ff3a25fc1046fb9a93a5b262ebcd0151caa442ea28959a38a\",\"2199023255552\":\"02d6b25bd4ab599dd0818c55f75702fde603c93f259222001246569018842d3258\",\"4398046511104\":\"03397b522bb4e156ec3952d3f048e5a986c20a00718e5e52cd5718466bf494156a\",\"8796093022208\":\"02d1fb9e78262b5d7d74028073075b80bb5ab281edcfc3191061962c1346340f1e\",\"17592186044416\":\"030d3f2ad7a4ca115712ff7f140434f802b19a4c9b2dd1c76f3e8e80c05c6a9310\",\"35184372088832\":\"03e325b691f292e1dfb151c3fb7cad440b225795583c32e24e10635a80e4221c06\",\"70368744177664\":\"03bee8f64d88de3dee21d61f89efa32933da51152ddbd67466bef815e9f93f8fd1\",\"140737488355328\":\"0327244c9019a4892e1f04ba3bf95fe43b327479e2d57c25979446cc508cd379ed\",\"281474976710656\":\"02fb58522cd662f2f8b042f8161caae6e45de98283f74d4e99f19b0ea85e08a56d\",\"562949953421312\":\"02adde4b466a9d7e59386b6a701a39717c53f30c4810613c1b55e6b6da43b7bc9a\",\"1125899906842624\":\"038eeda11f78ce05c774f30e393cda075192b890d68590813ff46362548528dca9\",\"2251799813685248\":\"02ec13e0058b196db80f7079d329333b330dc30c000dbdd7397cbbc5a37a664c4f\",\"4503599627370496\":\"02d2d162db63675bd04f7d56df04508840f41e2ad87312a3c93041b494efe80a73\",\"9007199254740992\":\"0356969d6aef2bb40121dbd07c68b6102339f4ea8e674a9008bb69506795998f49\",\"18014398509481984\":\"02f4e667567ebb9f4e6e180a4113bb071c48855f657766bb5e9c776a880335d1d6\",\"36028797018963968\":\"0385b4fe35e41703d7a657d957c67bb536629de57b7e6ee6fe2130728ef0fc90b0\",\"72057594037927936\":\"02b2bc1968a6fddbcc78fb9903940524824b5f5bed329c6ad48a19b56068c144fd\",\"144115188075855872\":\"02e0dbb24f1d288a693e8a49bc14264d1276be16972131520cf9e055ae92fba19a\",\"288230376151711744\":\"03efe75c106f931a525dc2d653ebedddc413a2c7d8cb9da410893ae7d2fa7d19cc\",\"576460752303423488\":\"02c7ec2bd9508a7fc03f73c7565dc600b30fd86f3d305f8f139c45c404a52d958a\",\"1152921504606846976\":\"035a6679c6b25e68ff4e29d1c7ef87f21e0a8fc574f6a08c1aa45ff352c1d59f06\",\"2305843009213693952\":\"033cdc225962c052d485f7cfbf55a5b2367d200fe1fe4373a347deb4cc99e9a099\",\"4611686018427387904\":\"024a4b806cf413d14b294719090a9da36ba75209c7657135ad09bc65328fba9e6f\",\"9223372036854775808\":\"0377a6fe114e291a8d8e991627c38001c8305b23b9e98b1c7b1893f5cd0dda6cad\"\n}")] + [InlineData( + "00456a94ab4e1c46", + "{\n \"1\":\"03a40f20667ed53513075dc51e715ff2046cad64eb68960632269ba7f0210e38bc\",\"2\":\"03fd4ce5a16b65576145949e6f99f445f8249fee17c606b688b504a849cdc452de\",\"4\":\"02648eccfa4c026960966276fa5a4cae46ce0fd432211a4f449bf84f13aa5f8303\",\"8\":\"02fdfd6796bfeac490cbee12f778f867f0a2c68f6508d17c649759ea0dc3547528\"\n}" + )] + [InlineData( + "000f01df73ea149a", + "{\n \"1\":\"03ba786a2c0745f8c30e490288acd7a72dd53d65afd292ddefa326a4a3fa14c566\",\"2\":\"03361cd8bd1329fea797a6add1cf1990ffcf2270ceb9fc81eeee0e8e9c1bd0cdf5\",\"4\":\"036e378bcf78738ddf68859293c69778035740e41138ab183c94f8fee7572214c7\",\"8\":\"03909d73beaf28edfb283dbeb8da321afd40651e8902fcf5454ecc7d69788626c0\",\"16\":\"028a36f0e6638ea7466665fe174d958212723019ec08f9ce6898d897f88e68aa5d\",\"32\":\"03a97a40e146adee2687ac60c2ba2586a90f970de92a9d0e6cae5a4b9965f54612\",\"64\":\"03ce86f0c197aab181ddba0cfc5c5576e11dfd5164d9f3d4a3fc3ffbbf2e069664\",\"128\":\"0284f2c06d938a6f78794814c687560a0aabab19fe5e6f30ede38e113b132a3cb9\",\"256\":\"03b99f475b68e5b4c0ba809cdecaae64eade2d9787aa123206f91cd61f76c01459\",\"512\":\"03d4db82ea19a44d35274de51f78af0a710925fe7d9e03620b84e3e9976e3ac2eb\",\"1024\":\"031fbd4ba801870871d46cf62228a1b748905ebc07d3b210daf48de229e683f2dc\",\"2048\":\"0276cedb9a3b160db6a158ad4e468d2437f021293204b3cd4bf6247970d8aff54b\",\"4096\":\"02fc6b89b403ee9eb8a7ed457cd3973638080d6e04ca8af7307c965c166b555ea2\",\"8192\":\"0320265583e916d3a305f0d2687fcf2cd4e3cd03a16ea8261fda309c3ec5721e21\",\"16384\":\"036e41de58fdff3cb1d8d713f48c63bc61fa3b3e1631495a444d178363c0d2ed50\",\"32768\":\"0365438f613f19696264300b069d1dad93f0c60a37536b72a8ab7c7366a5ee6c04\",\"65536\":\"02408426cfb6fc86341bac79624ba8708a4376b2d92debdf4134813f866eb57a8d\",\"131072\":\"031063e9f11c94dc778c473e968966eac0e70b7145213fbaff5f7a007e71c65f41\",\"262144\":\"02f2a3e808f9cd168ec71b7f328258d0c1dda250659c1aced14c7f5cf05aab4328\",\"524288\":\"038ac10de9f1ff9395903bb73077e94dbf91e9ef98fd77d9a2debc5f74c575bc86\",\"1048576\":\"0203eaee4db749b0fc7c49870d082024b2c31d889f9bc3b32473d4f1dfa3625788\",\"2097152\":\"033cdb9d36e1e82ae652b7b6a08e0204569ec7ff9ebf85d80a02786dc7fe00b04c\",\"4194304\":\"02c8b73f4e3a470ae05e5f2fe39984d41e9f6ae7be9f3b09c9ac31292e403ac512\",\"8388608\":\"025bbe0cfce8a1f4fbd7f3a0d4a09cb6badd73ef61829dc827aa8a98c270bc25b0\",\"16777216\":\"037eec3d1651a30a90182d9287a5c51386fe35d4a96839cf7969c6e2a03db1fc21\",\"33554432\":\"03280576b81a04e6abd7197f305506476f5751356b7643988495ca5c3e14e5c262\",\"67108864\":\"03268bfb05be1dbb33ab6e7e00e438373ca2c9b9abc018fdb452d0e1a0935e10d3\",\"134217728\":\"02573b68784ceba9617bbcc7c9487836d296aa7c628c3199173a841e7a19798020\",\"268435456\":\"0234076b6e70f7fbf755d2227ecc8d8169d662518ee3a1401f729e2a12ccb2b276\",\"536870912\":\"03015bd88961e2a466a2163bd4248d1d2b42c7c58a157e594785e7eb34d880efc9\",\"1073741824\":\"02c9b076d08f9020ebee49ac8ba2610b404d4e553a4f800150ceb539e9421aaeee\",\"2147483648\":\"034d592f4c366afddc919a509600af81b489a03caf4f7517c2b3f4f2b558f9a41a\",\"4294967296\":\"037c09ecb66da082981e4cbdb1ac65c0eb631fc75d85bed13efb2c6364148879b5\",\"8589934592\":\"02b4ebb0dda3b9ad83b39e2e31024b777cc0ac205a96b9a6cfab3edea2912ed1b3\",\"17179869184\":\"026cc4dacdced45e63f6e4f62edbc5779ccd802e7fabb82d5123db879b636176e9\",\"34359738368\":\"02b2cee01b7d8e90180254459b8f09bbea9aad34c3a2fd98c85517ecfc9805af75\",\"68719476736\":\"037a0c0d564540fc574b8bfa0253cca987b75466e44b295ed59f6f8bd41aace754\",\"137438953472\":\"021df6585cae9b9ca431318a713fd73dbb76b3ef5667957e8633bca8aaa7214fb6\",\"274877906944\":\"02b8f53dde126f8c85fa5bb6061c0be5aca90984ce9b902966941caf963648d53a\",\"549755813888\":\"029cc8af2840d59f1d8761779b2496623c82c64be8e15f9ab577c657c6dd453785\",\"1099511627776\":\"03e446fdb84fad492ff3a25fc1046fb9a93a5b262ebcd0151caa442ea28959a38a\",\"2199023255552\":\"02d6b25bd4ab599dd0818c55f75702fde603c93f259222001246569018842d3258\",\"4398046511104\":\"03397b522bb4e156ec3952d3f048e5a986c20a00718e5e52cd5718466bf494156a\",\"8796093022208\":\"02d1fb9e78262b5d7d74028073075b80bb5ab281edcfc3191061962c1346340f1e\",\"17592186044416\":\"030d3f2ad7a4ca115712ff7f140434f802b19a4c9b2dd1c76f3e8e80c05c6a9310\",\"35184372088832\":\"03e325b691f292e1dfb151c3fb7cad440b225795583c32e24e10635a80e4221c06\",\"70368744177664\":\"03bee8f64d88de3dee21d61f89efa32933da51152ddbd67466bef815e9f93f8fd1\",\"140737488355328\":\"0327244c9019a4892e1f04ba3bf95fe43b327479e2d57c25979446cc508cd379ed\",\"281474976710656\":\"02fb58522cd662f2f8b042f8161caae6e45de98283f74d4e99f19b0ea85e08a56d\",\"562949953421312\":\"02adde4b466a9d7e59386b6a701a39717c53f30c4810613c1b55e6b6da43b7bc9a\",\"1125899906842624\":\"038eeda11f78ce05c774f30e393cda075192b890d68590813ff46362548528dca9\",\"2251799813685248\":\"02ec13e0058b196db80f7079d329333b330dc30c000dbdd7397cbbc5a37a664c4f\",\"4503599627370496\":\"02d2d162db63675bd04f7d56df04508840f41e2ad87312a3c93041b494efe80a73\",\"9007199254740992\":\"0356969d6aef2bb40121dbd07c68b6102339f4ea8e674a9008bb69506795998f49\",\"18014398509481984\":\"02f4e667567ebb9f4e6e180a4113bb071c48855f657766bb5e9c776a880335d1d6\",\"36028797018963968\":\"0385b4fe35e41703d7a657d957c67bb536629de57b7e6ee6fe2130728ef0fc90b0\",\"72057594037927936\":\"02b2bc1968a6fddbcc78fb9903940524824b5f5bed329c6ad48a19b56068c144fd\",\"144115188075855872\":\"02e0dbb24f1d288a693e8a49bc14264d1276be16972131520cf9e055ae92fba19a\",\"288230376151711744\":\"03efe75c106f931a525dc2d653ebedddc413a2c7d8cb9da410893ae7d2fa7d19cc\",\"576460752303423488\":\"02c7ec2bd9508a7fc03f73c7565dc600b30fd86f3d305f8f139c45c404a52d958a\",\"1152921504606846976\":\"035a6679c6b25e68ff4e29d1c7ef87f21e0a8fc574f6a08c1aa45ff352c1d59f06\",\"2305843009213693952\":\"033cdc225962c052d485f7cfbf55a5b2367d200fe1fe4373a347deb4cc99e9a099\",\"4611686018427387904\":\"024a4b806cf413d14b294719090a9da36ba75209c7657135ad09bc65328fba9e6f\",\"9223372036854775808\":\"0377a6fe114e291a8d8e991627c38001c8305b23b9e98b1c7b1893f5cd0dda6cad\"\n}" + )] // v2 - [InlineData("015ba18a8adcd02e715a58358eb618da4a4b3791151a4bee5e968bb88406ccf76a", + [InlineData( + "015ba18a8adcd02e715a58358eb618da4a4b3791151a4bee5e968bb88406ccf76a", "{\n \"1\": \"03a40f20667ed53513075dc51e715ff2046cad64eb68960632269ba7f0210e38bc\",\n \"2\": \"03fd4ce5a16b65576145949e6f99f445f8249fee17c606b688b504a849cdc452de\",\n \"4\": \"02648eccfa4c026960966276fa5a4cae46ce0fd432211a4f449bf84f13aa5f8303\",\n \"8\": \"02fdfd6796bfeac490cbee12f778f867f0a2c68f6508d17c649759ea0dc3547528\"\n}", - (byte)1, + (byte)1, "sat", - 100UL, - "2059210353" - )] - [InlineData("01ab6aa4ff30390da34986d84be5274b48ad7a74265d791095bfc39f4098d9764f", + 100UL, + 2059210353UL + )] + [InlineData( + "01ab6aa4ff30390da34986d84be5274b48ad7a74265d791095bfc39f4098d9764f", "{\n \"1\": \"03ba786a2c0745f8c30e490288acd7a72dd53d65afd292ddefa326a4a3fa14c566\",\n \"2\": \"03361cd8bd1329fea797a6add1cf1990ffcf2270ceb9fc81eeee0e8e9c1bd0cdf5\",\n \"4\": \"036e378bcf78738ddf68859293c69778035740e41138ab183c94f8fee7572214c7\",\n \"8\": \"03909d73beaf28edfb283dbeb8da321afd40651e8902fcf5454ecc7d69788626c0\",\n \"16\": \"028a36f0e6638ea7466665fe174d958212723019ec08f9ce6898d897f88e68aa5d\",\n \"32\": \"03a97a40e146adee2687ac60c2ba2586a90f970de92a9d0e6cae5a4b9965f54612\",\n \"64\": \"03ce86f0c197aab181ddba0cfc5c5576e11dfd5164d9f3d4a3fc3ffbbf2e069664\",\n \"128\": \"0284f2c06d938a6f78794814c687560a0aabab19fe5e6f30ede38e113b132a3cb9\",\n \"256\": \"03b99f475b68e5b4c0ba809cdecaae64eade2d9787aa123206f91cd61f76c01459\",\n \"512\": \"03d4db82ea19a44d35274de51f78af0a710925fe7d9e03620b84e3e9976e3ac2eb\",\n \"1024\": \"031fbd4ba801870871d46cf62228a1b748905ebc07d3b210daf48de229e683f2dc\",\n \"2048\": \"0276cedb9a3b160db6a158ad4e468d2437f021293204b3cd4bf6247970d8aff54b\",\n \"4096\": \"02fc6b89b403ee9eb8a7ed457cd3973638080d6e04ca8af7307c965c166b555ea2\",\n \"8192\": \"0320265583e916d3a305f0d2687fcf2cd4e3cd03a16ea8261fda309c3ec5721e21\",\n \"16384\": \"036e41de58fdff3cb1d8d713f48c63bc61fa3b3e1631495a444d178363c0d2ed50\",\n \"32768\": \"0365438f613f19696264300b069d1dad93f0c60a37536b72a8ab7c7366a5ee6c04\",\n \"65536\": \"02408426cfb6fc86341bac79624ba8708a4376b2d92debdf4134813f866eb57a8d\",\n \"131072\": \"031063e9f11c94dc778c473e968966eac0e70b7145213fbaff5f7a007e71c65f41\",\n \"262144\": \"02f2a3e808f9cd168ec71b7f328258d0c1dda250659c1aced14c7f5cf05aab4328\",\n \"524288\": \"038ac10de9f1ff9395903bb73077e94dbf91e9ef98fd77d9a2debc5f74c575bc86\",\n \"1048576\": \"0203eaee4db749b0fc7c49870d082024b2c31d889f9bc3b32473d4f1dfa3625788\",\n \"2097152\": \"033cdb9d36e1e82ae652b7b6a08e0204569ec7ff9ebf85d80a02786dc7fe00b04c\",\n \"4194304\": \"02c8b73f4e3a470ae05e5f2fe39984d41e9f6ae7be9f3b09c9ac31292e403ac512\",\n \"8388608\": \"025bbe0cfce8a1f4fbd7f3a0d4a09cb6badd73ef61829dc827aa8a98c270bc25b0\",\n \"16777216\": \"037eec3d1651a30a90182d9287a5c51386fe35d4a96839cf7969c6e2a03db1fc21\",\n \"33554432\": \"03280576b81a04e6abd7197f305506476f5751356b7643988495ca5c3e14e5c262\",\n \"67108864\": \"03268bfb05be1dbb33ab6e7e00e438373ca2c9b9abc018fdb452d0e1a0935e10d3\",\n \"134217728\": \"02573b68784ceba9617bbcc7c9487836d296aa7c628c3199173a841e7a19798020\",\n \"268435456\": \"0234076b6e70f7fbf755d2227ecc8d8169d662518ee3a1401f729e2a12ccb2b276\",\n \"536870912\": \"03015bd88961e2a466a2163bd4248d1d2b42c7c58a157e594785e7eb34d880efc9\",\n \"1073741824\": \"02c9b076d08f9020ebee49ac8ba2610b404d4e553a4f800150ceb539e9421aaeee\",\n \"2147483648\": \"034d592f4c366afddc919a509600af81b489a03caf4f7517c2b3f4f2b558f9a41a\",\n \"4294967296\": \"037c09ecb66da082981e4cbdb1ac65c0eb631fc75d85bed13efb2c6364148879b5\",\n \"8589934592\": \"02b4ebb0dda3b9ad83b39e2e31024b777cc0ac205a96b9a6cfab3edea2912ed1b3\",\n \"17179869184\": \"026cc4dacdced45e63f6e4f62edbc5779ccd802e7fabb82d5123db879b636176e9\",\n \"34359738368\": \"02b2cee01b7d8e90180254459b8f09bbea9aad34c3a2fd98c85517ecfc9805af75\",\n \"68719476736\": \"037a0c0d564540fc574b8bfa0253cca987b75466e44b295ed59f6f8bd41aace754\",\n \"137438953472\": \"021df6585cae9b9ca431318a713fd73dbb76b3ef5667957e8633bca8aaa7214fb6\",\n \"274877906944\": \"02b8f53dde126f8c85fa5bb6061c0be5aca90984ce9b902966941caf963648d53a\",\n \"549755813888\": \"029cc8af2840d59f1d8761779b2496623c82c64be8e15f9ab577c657c6dd453785\",\n \"1099511627776\": \"03e446fdb84fad492ff3a25fc1046fb9a93a5b262ebcd0151caa442ea28959a38a\",\n \"2199023255552\": \"02d6b25bd4ab599dd0818c55f75702fde603c93f259222001246569018842d3258\",\n \"4398046511104\": \"03397b522bb4e156ec3952d3f048e5a986c20a00718e5e52cd5718466bf494156a\",\n \"8796093022208\": \"02d1fb9e78262b5d7d74028073075b80bb5ab281edcfc3191061962c1346340f1e\",\n \"17592186044416\": \"030d3f2ad7a4ca115712ff7f140434f802b19a4c9b2dd1c76f3e8e80c05c6a9310\",\n \"35184372088832\": \"03e325b691f292e1dfb151c3fb7cad440b225795583c32e24e10635a80e4221c06\",\n \"70368744177664\": \"03bee8f64d88de3dee21d61f89efa32933da51152ddbd67466bef815e9f93f8fd1\",\n \"140737488355328\": \"0327244c9019a4892e1f04ba3bf95fe43b327479e2d57c25979446cc508cd379ed\",\n \"281474976710656\": \"02fb58522cd662f2f8b042f8161caae6e45de98283f74d4e99f19b0ea85e08a56d\",\n \"562949953421312\": \"02adde4b466a9d7e59386b6a701a39717c53f30c4810613c1b55e6b6da43b7bc9a\",\n \"1125899906842624\": \"038eeda11f78ce05c774f30e393cda075192b890d68590813ff46362548528dca9\",\n \"2251799813685248\": \"02ec13e0058b196db80f7079d329333b330dc30c000dbdd7397cbbc5a37a664c4f\",\n \"4503599627370496\": \"02d2d162db63675bd04f7d56df04508840f41e2ad87312a3c93041b494efe80a73\",\n \"9007199254740992\": \"0356969d6aef2bb40121dbd07c68b6102339f4ea8e674a9008bb69506795998f49\",\n \"18014398509481984\": \"02f4e667567ebb9f4e6e180a4113bb071c48855f657766bb5e9c776a880335d1d6\",\n \"36028797018963968\": \"0385b4fe35e41703d7a657d957c67bb536629de57b7e6ee6fe2130728ef0fc90b0\",\n \"72057594037927936\": \"02b2bc1968a6fddbcc78fb9903940524824b5f5bed329c6ad48a19b56068c144fd\",\n \"144115188075855872\": \"02e0dbb24f1d288a693e8a49bc14264d1276be16972131520cf9e055ae92fba19a\",\n \"288230376151711744\": \"03efe75c106f931a525dc2d653ebedddc413a2c7d8cb9da410893ae7d2fa7d19cc\",\n \"576460752303423488\": \"02c7ec2bd9508a7fc03f73c7565dc600b30fd86f3d305f8f139c45c404a52d958a\",\n \"1152921504606846976\": \"035a6679c6b25e68ff4e29d1c7ef87f21e0a8fc574f6a08c1aa45ff352c1d59f06\",\n \"2305843009213693952\": \"033cdc225962c052d485f7cfbf55a5b2367d200fe1fe4373a347deb4cc99e9a099\",\n \"4611686018427387904\": \"024a4b806cf413d14b294719090a9da36ba75209c7657135ad09bc65328fba9e6f\",\n \"9223372036854775808\": \"0377a6fe114e291a8d8e991627c38001c8305b23b9e98b1c7b1893f5cd0dda6cad\"\n}", (byte)0x01, "sat", 0UL, - "2059210353" - )] - [InlineData("012fbb01a4e200c76df911eeba3b8fe1831202914b24664f4bccbd25852a6708f8", + 2059210353UL + )] + [InlineData( + "012fbb01a4e200c76df911eeba3b8fe1831202914b24664f4bccbd25852a6708f8", "{\n \"1\": \"03ba786a2c0745f8c30e490288acd7a72dd53d65afd292ddefa326a4a3fa14c566\",\n \"2\": \"03361cd8bd1329fea797a6add1cf1990ffcf2270ceb9fc81eeee0e8e9c1bd0cdf5\",\n \"4\": \"036e378bcf78738ddf68859293c69778035740e41138ab183c94f8fee7572214c7\",\n \"8\": \"03909d73beaf28edfb283dbeb8da321afd40651e8902fcf5454ecc7d69788626c0\",\n \"16\": \"028a36f0e6638ea7466665fe174d958212723019ec08f9ce6898d897f88e68aa5d\",\n \"32\": \"03a97a40e146adee2687ac60c2ba2586a90f970de92a9d0e6cae5a4b9965f54612\",\n \"64\": \"03ce86f0c197aab181ddba0cfc5c5576e11dfd5164d9f3d4a3fc3ffbbf2e069664\",\n \"128\": \"0284f2c06d938a6f78794814c687560a0aabab19fe5e6f30ede38e113b132a3cb9\",\n \"256\": \"03b99f475b68e5b4c0ba809cdecaae64eade2d9787aa123206f91cd61f76c01459\",\n \"512\": \"03d4db82ea19a44d35274de51f78af0a710925fe7d9e03620b84e3e9976e3ac2eb\",\n \"1024\": \"031fbd4ba801870871d46cf62228a1b748905ebc07d3b210daf48de229e683f2dc\",\n \"2048\": \"0276cedb9a3b160db6a158ad4e468d2437f021293204b3cd4bf6247970d8aff54b\",\n \"4096\": \"02fc6b89b403ee9eb8a7ed457cd3973638080d6e04ca8af7307c965c166b555ea2\",\n \"8192\": \"0320265583e916d3a305f0d2687fcf2cd4e3cd03a16ea8261fda309c3ec5721e21\",\n \"16384\": \"036e41de58fdff3cb1d8d713f48c63bc61fa3b3e1631495a444d178363c0d2ed50\",\n \"32768\": \"0365438f613f19696264300b069d1dad93f0c60a37536b72a8ab7c7366a5ee6c04\",\n \"65536\": \"02408426cfb6fc86341bac79624ba8708a4376b2d92debdf4134813f866eb57a8d\",\n \"131072\": \"031063e9f11c94dc778c473e968966eac0e70b7145213fbaff5f7a007e71c65f41\",\n \"262144\": \"02f2a3e808f9cd168ec71b7f328258d0c1dda250659c1aced14c7f5cf05aab4328\",\n \"524288\": \"038ac10de9f1ff9395903bb73077e94dbf91e9ef98fd77d9a2debc5f74c575bc86\",\n \"1048576\": \"0203eaee4db749b0fc7c49870d082024b2c31d889f9bc3b32473d4f1dfa3625788\",\n \"2097152\": \"033cdb9d36e1e82ae652b7b6a08e0204569ec7ff9ebf85d80a02786dc7fe00b04c\",\n \"4194304\": \"02c8b73f4e3a470ae05e5f2fe39984d41e9f6ae7be9f3b09c9ac31292e403ac512\",\n \"8388608\": \"025bbe0cfce8a1f4fbd7f3a0d4a09cb6badd73ef61829dc827aa8a98c270bc25b0\",\n \"16777216\": \"037eec3d1651a30a90182d9287a5c51386fe35d4a96839cf7969c6e2a03db1fc21\",\n \"33554432\": \"03280576b81a04e6abd7197f305506476f5751356b7643988495ca5c3e14e5c262\",\n \"67108864\": \"03268bfb05be1dbb33ab6e7e00e438373ca2c9b9abc018fdb452d0e1a0935e10d3\",\n \"134217728\": \"02573b68784ceba9617bbcc7c9487836d296aa7c628c3199173a841e7a19798020\",\n \"268435456\": \"0234076b6e70f7fbf755d2227ecc8d8169d662518ee3a1401f729e2a12ccb2b276\",\n \"536870912\": \"03015bd88961e2a466a2163bd4248d1d2b42c7c58a157e594785e7eb34d880efc9\",\n \"1073741824\": \"02c9b076d08f9020ebee49ac8ba2610b404d4e553a4f800150ceb539e9421aaeee\",\n \"2147483648\": \"034d592f4c366afddc919a509600af81b489a03caf4f7517c2b3f4f2b558f9a41a\",\n \"4294967296\": \"037c09ecb66da082981e4cbdb1ac65c0eb631fc75d85bed13efb2c6364148879b5\",\n \"8589934592\": \"02b4ebb0dda3b9ad83b39e2e31024b777cc0ac205a96b9a6cfab3edea2912ed1b3\",\n \"17179869184\": \"026cc4dacdced45e63f6e4f62edbc5779ccd802e7fabb82d5123db879b636176e9\",\n \"34359738368\": \"02b2cee01b7d8e90180254459b8f09bbea9aad34c3a2fd98c85517ecfc9805af75\",\n \"68719476736\": \"037a0c0d564540fc574b8bfa0253cca987b75466e44b295ed59f6f8bd41aace754\",\n \"137438953472\": \"021df6585cae9b9ca431318a713fd73dbb76b3ef5667957e8633bca8aaa7214fb6\",\n \"274877906944\": \"02b8f53dde126f8c85fa5bb6061c0be5aca90984ce9b902966941caf963648d53a\",\n \"549755813888\": \"029cc8af2840d59f1d8761779b2496623c82c64be8e15f9ab577c657c6dd453785\",\n \"1099511627776\": \"03e446fdb84fad492ff3a25fc1046fb9a93a5b262ebcd0151caa442ea28959a38a\",\n \"2199023255552\": \"02d6b25bd4ab599dd0818c55f75702fde603c93f259222001246569018842d3258\",\n \"4398046511104\": \"03397b522bb4e156ec3952d3f048e5a986c20a00718e5e52cd5718466bf494156a\",\n \"8796093022208\": \"02d1fb9e78262b5d7d74028073075b80bb5ab281edcfc3191061962c1346340f1e\",\n \"17592186044416\": \"030d3f2ad7a4ca115712ff7f140434f802b19a4c9b2dd1c76f3e8e80c05c6a9310\",\n \"35184372088832\": \"03e325b691f292e1dfb151c3fb7cad440b225795583c32e24e10635a80e4221c06\",\n \"70368744177664\": \"03bee8f64d88de3dee21d61f89efa32933da51152ddbd67466bef815e9f93f8fd1\",\n \"140737488355328\": \"0327244c9019a4892e1f04ba3bf95fe43b327479e2d57c25979446cc508cd379ed\",\n \"281474976710656\": \"02fb58522cd662f2f8b042f8161caae6e45de98283f74d4e99f19b0ea85e08a56d\",\n \"562949953421312\": \"02adde4b466a9d7e59386b6a701a39717c53f30c4810613c1b55e6b6da43b7bc9a\",\n \"1125899906842624\": \"038eeda11f78ce05c774f30e393cda075192b890d68590813ff46362548528dca9\",\n \"2251799813685248\": \"02ec13e0058b196db80f7079d329333b330dc30c000dbdd7397cbbc5a37a664c4f\",\n \"4503599627370496\": \"02d2d162db63675bd04f7d56df04508840f41e2ad87312a3c93041b494efe80a73\",\n \"9007199254740992\": \"0356969d6aef2bb40121dbd07c68b6102339f4ea8e674a9008bb69506795998f49\",\n \"18014398509481984\": \"02f4e667567ebb9f4e6e180a4113bb071c48855f657766bb5e9c776a880335d1d6\",\n \"36028797018963968\": \"0385b4fe35e41703d7a657d957c67bb536629de57b7e6ee6fe2130728ef0fc90b0\",\n \"72057594037927936\": \"02b2bc1968a6fddbcc78fb9903940524824b5f5bed329c6ad48a19b56068c144fd\",\n \"144115188075855872\": \"02e0dbb24f1d288a693e8a49bc14264d1276be16972131520cf9e055ae92fba19a\",\n \"288230376151711744\": \"03efe75c106f931a525dc2d653ebedddc413a2c7d8cb9da410893ae7d2fa7d19cc\",\n \"576460752303423488\": \"02c7ec2bd9508a7fc03f73c7565dc600b30fd86f3d305f8f139c45c404a52d958a\",\n \"1152921504606846976\": \"035a6679c6b25e68ff4e29d1c7ef87f21e0a8fc574f6a08c1aa45ff352c1d59f06\",\n \"2305843009213693952\": \"033cdc225962c052d485f7cfbf55a5b2367d200fe1fe4373a347deb4cc99e9a099\",\n \"4611686018427387904\": \"024a4b806cf413d14b294719090a9da36ba75209c7657135ad09bc65328fba9e6f\",\n \"9223372036854775808\": \"0377a6fe114e291a8d8e991627c38001c8305b23b9e98b1c7b1893f5cd0dda6cad\"\n}", (byte)0x01, "sat", - 0UL)] - public void Nut02Tests_KeysetIdMatch(string keysetId, string keyset, byte? version = null, string? unit = null, ulong? inputFee = null, string? finalExpiration = null) + 0UL + )] + public void Nut02Tests_KeysetIdMatch( + string keysetId, + string keyset, + byte? version = null, + string? unit = null, + ulong? inputFee = null, + ulong? finalExpiration = null + ) { var keysetIdParsed = new KeysetId(keysetId); var keysetParsed = JsonSerializer.Deserialize(keyset); - Assert.Equal(keysetIdParsed, keysetParsed.GetKeysetId(version ?? 0x00, unit, inputFee, finalExpiration)); + Assert.Equal( + keysetIdParsed, + keysetParsed.GetKeysetId(version ?? 0x00, unit, inputFee, finalExpiration) + ); } [Theory] @@ -214,23 +275,25 @@ public void Nut02Tests_KeysetIdVersion(string keysetId, byte version) var keysetIdParsed = new KeysetId(keysetId); Assert.Equal(version, keysetIdParsed.GetVersion()); } - - [Fact] public void Nut04Tests_Proofs_1() { var a = "0000000000000000000000000000000000000000000000000000000000000001".ToPrivKey(); var A = a.CreatePubKey(); - Assert.Equal("0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798".ToPubKey(), A); + Assert.Equal( + "0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798".ToPubKey(), + A + ); var message = new StringSecret("secret_msg"); - var blindingFactor = "0000000000000000000000000000000000000000000000000000000000000001".ToPrivKey(); + var blindingFactor = + "0000000000000000000000000000000000000000000000000000000000000001".ToPrivKey(); // var Y = Cashu.MessageToCurve(message); var Y = message.ToCurve(); var B_ = Cashu.ComputeB_(Y, blindingFactor); var C_ = Cashu.ComputeC_(B_, a); - //p doesn;t have to be blinding factor. in fact it should be random nonce - + //p doesn;t have to be blinding factor. in fact it should be random nonce + var proof = Cashu.ComputeProof(B_, a, blindingFactor); Cashu.VerifyProof(B_, C_, proof.e, proof.s, A); var C = Cashu.ComputeC(C_, blindingFactor, A); @@ -238,12 +301,12 @@ public void Nut04Tests_Proofs_1() Cashu.VerifyProof(Y, blindingFactor, C, proof.e, proof.s, A); } - [Fact] public void Nut04Tests_Proofs_2() { var A = "0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798".ToPubKey(); - var proof = JsonSerializer.Deserialize(@" + var proof = JsonSerializer.Deserialize( + @" { ""amount"": 1, @@ -257,32 +320,48 @@ public void Nut04Tests_Proofs_2() } } -"); +" + ); Assert.NotNull(proof?.DLEQ); - Cashu.VerifyProof(Cashu.HexToCurve(Assert.IsType(proof.Secret).Secret), proof.DLEQ.R, proof.C, - proof.DLEQ.E, proof.DLEQ.S, A); + Cashu.VerifyProof( + Cashu.HexToCurve(Assert.IsType(proof.Secret).Secret), + proof.DLEQ.R, + proof.C, + proof.DLEQ.E, + proof.DLEQ.S, + A + ); } [Fact] public void Nut11_Signatures() { - var secretKey = - ECPrivKey.Create(Convert.FromHexString("99590802251e78ee1051648439eedb003dc539093a48a44e7b8f2642c909ea37")); + var secretKey = ECPrivKey.Create( + Convert.FromHexString( + "99590802251e78ee1051648439eedb003dc539093a48a44e7b8f2642c909ea37" + ) + ); - var signing_key_two = - ECPrivKey.Create(Convert.FromHexString("0000000000000000000000000000000000000000000000000000000000000001")); + var signing_key_two = ECPrivKey.Create( + Convert.FromHexString( + "0000000000000000000000000000000000000000000000000000000000000001" + ) + ); - var signing_key_three = - ECPrivKey.Create(Convert.FromHexString("7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f")); + var signing_key_three = ECPrivKey.Create( + Convert.FromHexString( + "7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f" + ) + ); - var conditions = new P2PKBuilder + var conditions = new P2PkBuilder { Lock = DateTimeOffset.FromUnixTimeSeconds(21000000000), - Pubkeys = new[] {signing_key_two.CreatePubKey(), signing_key_three.CreatePubKey()}, - RefundPubkeys = new[] {secretKey.CreatePubKey()}, + Pubkeys = new[] { signing_key_two.CreatePubKey(), signing_key_three.CreatePubKey() }, + RefundPubkeys = new[] { secretKey.CreatePubKey() }, SignatureThreshold = 2, - SigFlag = "SIG_INPUTS" + SigFlag = "SIG_INPUTS", }; var p2pkProofSecret = conditions.Build(); @@ -295,10 +374,13 @@ public void Nut11_Signatures() Secret = secret, C = "02698c4e2b5f9534cd0687d87513c759790cf829aa5739184a3e3735471fbda904".ToPubKey(), }; - var witness = p2pkProofSecret.GenerateWitness(proof, new[] {signing_key_two, signing_key_three}); + var witness = p2pkProofSecret.GenerateWitness( + proof, + new[] { signing_key_two, signing_key_three } + ); proof.Witness = JsonSerializer.Serialize(witness); Assert.True(p2pkProofSecret.VerifyWitness(proof.Secret, witness)); - + // SIG_INPUTS var valid1 = @@ -306,76 +388,119 @@ public void Nut11_Signatures() var valid1Proof = JsonSerializer.Deserialize(valid1); var valid1ProofSecret = Assert.IsType(valid1Proof.Secret); Assert.Equal(P2PKProofSecret.Key, valid1ProofSecret!.Key); - var valid1ProofSecretp2pkValue = Assert.IsType(valid1ProofSecret.ProofSecret); + var valid1ProofSecretp2pkValue = Assert.IsType( + valid1ProofSecret.ProofSecret + ); var valid1ProofWitnessP2pk = JsonSerializer.Deserialize(valid1Proof.Witness); - Assert.True(valid1ProofSecretp2pkValue.VerifyWitness(valid1Proof.Secret, valid1ProofWitnessP2pk)); + Assert.True( + valid1ProofSecretp2pkValue.VerifyWitness(valid1Proof.Secret, valid1ProofWitnessP2pk) + ); var invalid1 = "{\n \"amount\": 1,\n \"secret\": \"[\\\"P2PK\\\",{\\\"nonce\\\":\\\"859d4935c4907062a6297cf4e663e2835d90d97ecdd510745d32f6816323a41f\\\",\\\"data\\\":\\\"0249098aa8b9d2fbec49ff8598feb17b592b986e62319a4fa488a3dc36387157a7\\\",\\\"tags\\\":[[\\\"sigflag\\\",\\\"SIG_INPUTS\\\"]]}]\",\n \"C\": \"02698c4e2b5f9534cd0687d87513c759790cf829aa5739184a3e3735471fbda904\",\n \"id\": \"009a1f293253e41e\",\n \"witness\": \"{\\\"signatures\\\":[\\\"83564aca48c668f50d022a426ce0ed19d3a9bdcffeeaee0dc1e7ea7e98e9eff1840fcc821724f623468c94f72a8b0a7280fa9ef5a54a1b130ef3055217f467b3\\\"]}\"\n}"; var invalid1Proof = JsonSerializer.Deserialize(invalid1); var invalid1ProofSecret = Assert.IsType(invalid1Proof.Secret); Assert.Equal(P2PKProofSecret.Key, invalid1ProofSecret!.Key); - var invalid1ProofSecretp2pkValue = Assert.IsType(invalid1ProofSecret.ProofSecret); - var invalid1ProofWitnessP2pk = JsonSerializer.Deserialize(invalid1Proof.Witness); - Assert.False(invalid1ProofSecretp2pkValue.VerifyWitness(invalid1Proof.Secret, invalid1ProofWitnessP2pk)); + var invalid1ProofSecretp2pkValue = Assert.IsType( + invalid1ProofSecret.ProofSecret + ); + var invalid1ProofWitnessP2pk = JsonSerializer.Deserialize( + invalid1Proof.Witness + ); + Assert.False( + invalid1ProofSecretp2pkValue.VerifyWitness( + invalid1Proof.Secret, + invalid1ProofWitnessP2pk + ) + ); var validMultisig = "{\"amount\":1,\"secret\":\"[\\\"P2PK\\\",{\\\"nonce\\\":\\\"0ed3fcb22c649dd7bbbdcca36e0c52d4f0187dd3b6a19efcc2bfbebb5f85b2a1\\\",\\\"data\\\":\\\"0249098aa8b9d2fbec49ff8598feb17b592b986e62319a4fa488a3dc36387157a7\\\",\\\"tags\\\":[[\\\"pubkeys\\\",\\\"0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798\\\",\\\"02142715675faf8da1ecc4d51e0b9e539fa0d52fdd96ed60dbe99adb15d6b05ad9\\\"],[\\\"n_sigs\\\",\\\"2\\\"],[\\\"sigflag\\\",\\\"SIG_INPUTS\\\"]]}]\",\"C\":\"02698c4e2b5f9534cd0687d87513c759790cf829aa5739184a3e3735471fbda904\",\"id\":\"009a1f293253e41e\",\"witness\":\"{\\\"signatures\\\":[\\\"83564aca48c668f50d022a426ce0ed19d3a9bdcffeeaee0dc1e7ea7e98e9eff1840fcc821724f623468c94f72a8b0a7280fa9ef5a54a1b130ef3055217f467b3\\\",\\\"9a72ca2d4d5075be5b511ee48dbc5e45f259bcf4a4e8bf18587f433098a9cd61ff9737dc6e8022de57c76560214c4568377792d4c2c6432886cc7050487a1f22\\\"]}\"}"; var validMultisigProof = JsonSerializer.Deserialize(validMultisig); var validMultisigProofSecret = Assert.IsType(validMultisigProof.Secret); Assert.Equal(P2PKProofSecret.Key, validMultisigProofSecret!.Key); - var validMultisigProofSecretp2pkValue = Assert.IsType(validMultisigProofSecret.ProofSecret); - var validMultisigProofWitnessP2pk = JsonSerializer.Deserialize(validMultisigProof.Witness); + var validMultisigProofSecretp2pkValue = Assert.IsType( + validMultisigProofSecret.ProofSecret + ); + var validMultisigProofWitnessP2pk = JsonSerializer.Deserialize( + validMultisigProof.Witness + ); Assert.True( - validMultisigProofSecretp2pkValue.VerifyWitness(validMultisigProof.Secret, validMultisigProofWitnessP2pk)); + validMultisigProofSecretp2pkValue.VerifyWitness( + validMultisigProof.Secret, + validMultisigProofWitnessP2pk + ) + ); var invalidMultisig = "{\"amount\":1,\"secret\":\"[\\\"P2PK\\\",{\\\"nonce\\\":\\\"0ed3fcb22c649dd7bbbdcca36e0c52d4f0187dd3b6a19efcc2bfbebb5f85b2a1\\\",\\\"data\\\":\\\"0249098aa8b9d2fbec49ff8598feb17b592b986e62319a4fa488a3dc36387157a7\\\",\\\"tags\\\":[[\\\"pubkeys\\\",\\\"0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798\\\",\\\"02142715675faf8da1ecc4d51e0b9e539fa0d52fdd96ed60dbe99adb15d6b05ad9\\\"],[\\\"n_sigs\\\",\\\"2\\\"],[\\\"sigflag\\\",\\\"SIG_INPUTS\\\"]]}]\",\"C\":\"02698c4e2b5f9534cd0687d87513c759790cf829aa5739184a3e3735471fbda904\",\"id\":\"009a1f293253e41e\",\"witness\":\"{\\\"signatures\\\":[\\\"83564aca48c668f50d022a426ce0ed19d3a9bdcffeeaee0dc1e7ea7e98e9eff1840fcc821724f623468c94f72a8b0a7280fa9ef5a54a1b130ef3055217f467b3\\\"]}\"}"; var invalidMultisigProof = JsonSerializer.Deserialize(invalidMultisig); var invalidMultisigProofSecret = Assert.IsType(invalidMultisigProof.Secret); Assert.Equal(P2PKProofSecret.Key, invalidMultisigProofSecret!.Key); - var invalidMultisigProofSecretp2pkValue = - Assert.IsType(invalidMultisigProofSecret.ProofSecret); - var invalidMultisigProofWitnessP2pk = JsonSerializer.Deserialize(invalidMultisigProof.Witness); - Assert.False(invalidMultisigProofSecretp2pkValue.VerifyWitness(invalidMultisigProof.Secret, - invalidMultisigProofWitnessP2pk)); + var invalidMultisigProofSecretp2pkValue = Assert.IsType( + invalidMultisigProofSecret.ProofSecret + ); + var invalidMultisigProofWitnessP2pk = JsonSerializer.Deserialize( + invalidMultisigProof.Witness + ); + Assert.False( + invalidMultisigProofSecretp2pkValue.VerifyWitness( + invalidMultisigProof.Secret, + invalidMultisigProofWitnessP2pk + ) + ); var validProofRefund = "{\n \"amount\": 64,\n \"C\": \"0257353051c02e2d650dede3159915c8be123ba4f47cf33183c7fedd20bd91a79b\",\n \"id\": \"001b6c716bf42c7e\",\n \"secret\": \"[\\\"P2PK\\\",{\\\"nonce\\\":\\\"4bc88ee09d1886c7461d45da205ca3274e1e3d9da2667c4865045cb18265a407\\\",\\\"data\\\":\\\"03d5edeb839be873df2348785506d36565f3b8f390fb931709a422b5a247ddefb1\\\",\\\"tags\\\":[[\\\"locktime\\\",\\\"21\\\"],[\\\"refund\\\",\\\"0234ad87e907e117db1590cc20a3942ffdfd5137aa563d36095d5cf5f96bada122\\\"]]}]\",\n \"witness\": \"{\\\"signatures\\\":[\\\"b316c2ff9c15f0c5c3d230e99ad94bc76a11dfccbdc820366a3db7210288f22ef6cedcded1152904ec31056d1d5176d83a2d96df5cd4ff86afdde1c90c63af5e\\\"]}\"\n}"; var validProofRefundParsed = JsonSerializer.Deserialize(validProofRefund); var validProofRefundSecret = Assert.IsType(validProofRefundParsed.Secret); Assert.Equal(P2PKProofSecret.Key, validProofRefundSecret!.Key); - var validProofRefundSecretp2pkValue = Assert.IsType(validProofRefundSecret.ProofSecret); - var validProofRefundWitnessP2pk = JsonSerializer.Deserialize(validProofRefundParsed.Witness); + var validProofRefundSecretp2pkValue = Assert.IsType( + validProofRefundSecret.ProofSecret + ); + var validProofRefundWitnessP2pk = JsonSerializer.Deserialize( + validProofRefundParsed.Witness + ); Assert.True( - validProofRefundSecretp2pkValue.VerifyWitness(validProofRefundParsed.Secret, validProofRefundWitnessP2pk)); - + validProofRefundSecretp2pkValue.VerifyWitness( + validProofRefundParsed.Secret, + validProofRefundWitnessP2pk + ) + ); var invalidProofRefund = "{\n \"amount\": 64,\n \"C\": \"0215865e3b30bdf6f5cdc1ee2c33379d5629bdf2eff2595603d939ff8c65d80586\",\n \"id\": \"001b6c716bf42c7e\",\n \"secret\": \"[\\\"P2PK\\\",{\\\"nonce\\\":\\\"0c3d085898f1abf2b5521035f4d0f4ecf68c6a5109f6bc836833a1188f06be65\\\",\\\"data\\\":\\\"03206e0d488387a816bbafd957be51b073432c6c7a403ec4c2a0b27647326c5150\\\",\\\"tags\\\":[[\\\"locktime\\\",\\\"99999999999\\\"],[\\\"refund\\\",\\\"026acbcd0fff3a424499c83ec892d3155c9d1984438659f448d9d0f1af3e92276a\\\"]]}]\",\n \"witness\": \"{\\\"signatures\\\":[\\\"e5b10d7627ab39bd0cefa219c63752a0026aa5ae754b91a0c7ee2596222f87942c442aca2957166a6b468350c09c9968792784d2ae7c42fc91739b55689f4c7a\\\"]}\"\n}"; var invalidProofRefundParsed = JsonSerializer.Deserialize(invalidProofRefund); var invalidProofRefundSecret = Assert.IsType(invalidProofRefundParsed.Secret); Assert.Equal(P2PKProofSecret.Key, invalidProofRefundSecret!.Key); - var invalidProofRefundSecretp2pkValue = Assert.IsType(invalidProofRefundSecret.ProofSecret); - var invalidProofRefundWitnessP2pk = JsonSerializer.Deserialize(invalidProofRefundParsed.Witness); - Assert.False(invalidProofRefundSecretp2pkValue.VerifyWitness(invalidProofRefundParsed.Secret, - invalidProofRefundWitnessP2pk)); + var invalidProofRefundSecretp2pkValue = Assert.IsType( + invalidProofRefundSecret.ProofSecret + ); + var invalidProofRefundWitnessP2pk = JsonSerializer.Deserialize( + invalidProofRefundParsed.Witness + ); + Assert.False( + invalidProofRefundSecretp2pkValue.VerifyWitness( + invalidProofRefundParsed.Secret, + invalidProofRefundWitnessP2pk + ) + ); } [Fact] public void Nut11_New_P2PkRules() { - // since https://github.com/cashubtc/nuts/pull/315 p2pk and htlc behavior will be changed. After locktime, the + // since https://github.com/cashubtc/nuts/pull/315 p2pk and htlc behavior will be changed. After locktime, the // proof will be spendable on both (refund and normal) paths. - - var spendableProof = + + var spendableProof = "{\n \"amount\": 64,\n \"C\": \"02d7cd858d866fca404b5cb1ffd813946e6d19efa1af00d654080fd20266bdc0b1\",\n \"id\": \"001b6c716bf42c7e\",\n \"secret\": \"[\\\"P2PK\\\",{\\\"nonce\\\":\\\"395162bf2d0add3c66aea9f22c45251dbee6e04bd9282addbb366a94cd4fb482\\\",\\\"data\\\":\\\"03ab50a667926fac858bac540766254c14b2b0334d10e8ec766455310224bbecf4\\\",\\\"tags\\\":[[\\\"locktime\\\",\\\"21\\\"],[\\\"pubkeys\\\",\\\"0229a91adec8dd9badb228c628a07fc1bf707a9b7d95dd505c490b1766fa7dc541\\\",\\\"033281c37677ea273eb7183b783067f5244933ef78d8c3f15b1a77cb246099c26e\\\"],[\\\"n_sigs\\\",\\\"2\\\"],[\\\"refund\\\",\\\"03ab50a667926fac858bac540766254c14b2b0334d10e8ec766455310224bbecf4\\\",\\\"033281c37677ea273eb7183b783067f5244933ef78d8c3f15b1a77cb246099c26e\\\"]]}]\"\n}"; var spendableProofParsed = JsonSerializer.Deserialize(spendableProof); Assert.NotNull(spendableProofParsed); var spendableProofSecret = Assert.IsType(spendableProofParsed.Secret); Assert.Equal(P2PKProofSecret.Key, spendableProofSecret.Key); var secretValue = Assert.IsType(spendableProofSecret.ProofSecret); - + // "standard path" witness, n_sigs = 2. // since locktime is expired, it would fail under old conditions. now the proof should remain spendable var validWitness1 = @@ -398,91 +523,247 @@ public void Nut11_SIG_ALL() var swapRequest = "{\n \"inputs\": [\n {\n \"amount\": 2,\n \"id\": \"00bfa73302d12ffd\",\n \"secret\": \"[\\\"P2PK\\\",{\\\"nonce\\\":\\\"c7f280eb55c1e8564e03db06973e94bc9b666d9e1ca42ad278408fe625950303\\\",\\\"data\\\":\\\"030d8acedfe072c9fa449a1efe0817157403fbec460d8e79f957966056e5dd76c1\\\",\\\"tags\\\":[[\\\"sigflag\\\",\\\"SIG_ALL\\\"]]}]\",\n \"C\": \"02c97ee3d1db41cf0a3ddb601724be8711a032950811bf326f8219c50c4808d3cd\",\n \"witness\": \"{\\\"signatures\\\":[\\\"ce017ca25b1b97df2f72e4b49f69ac26a240ce14b3690a8fe619d41ccc42d3c1282e073f85acd36dc50011638906f35b56615f24e4d03e8effe8257f6a808538\\\"]}\"\n }\n ],\n \"outputs\": [\n {\n \"amount\": 2,\n \"id\": \"00bfa73302d12ffd\",\n \"B_\": \"038ec853d65ae1b79b5cdbc2774150b2cb288d6d26e12958a16fb33c32d9a86c39\"\n }\n ]\n}"; var swapRequestParsed = JsonSerializer.Deserialize(swapRequest); - var msgToSign = "[\"P2PK\",{\"nonce\":\"c7f280eb55c1e8564e03db06973e94bc9b666d9e1ca42ad278408fe625950303\",\"data\":\"030d8acedfe072c9fa449a1efe0817157403fbec460d8e79f957966056e5dd76c1\",\"tags\":[[\"sigflag\",\"SIG_ALL\"]]}]02c97ee3d1db41cf0a3ddb601724be8711a032950811bf326f8219c50c4808d3cd2038ec853d65ae1b79b5cdbc2774150b2cb288d6d26e12958a16fb33c32d9a86c39"; - Assert.Equal(msgToSign, SigAllHandler.GetMessageToSign(swapRequestParsed.Inputs, swapRequestParsed.Outputs)); + var msgToSign = + "[\"P2PK\",{\"nonce\":\"c7f280eb55c1e8564e03db06973e94bc9b666d9e1ca42ad278408fe625950303\",\"data\":\"030d8acedfe072c9fa449a1efe0817157403fbec460d8e79f957966056e5dd76c1\",\"tags\":[[\"sigflag\",\"SIG_ALL\"]]}]02c97ee3d1db41cf0a3ddb601724be8711a032950811bf326f8219c50c4808d3cd2038ec853d65ae1b79b5cdbc2774150b2cb288d6d26e12958a16fb33c32d9a86c39"; + Assert.Equal( + msgToSign, + SigAllHandler.GetMessageToSign(swapRequestParsed.Inputs, swapRequestParsed.Outputs) + ); var signedSwapRequest = "{\n \"inputs\": [\n {\n \"amount\": 2,\n \"id\": \"00bfa73302d12ffd\",\n \"secret\": \"[\\\"P2PK\\\",{\\\"nonce\\\":\\\"c7f280eb55c1e8564e03db06973e94bc9b666d9e1ca42ad278408fe625950303\\\",\\\"data\\\":\\\"030d8acedfe072c9fa449a1efe0817157403fbec460d8e79f957966056e5dd76c1\\\",\\\"tags\\\":[[\\\"sigflag\\\",\\\"SIG_ALL\\\"]]}]\",\n \"C\": \"02c97ee3d1db41cf0a3ddb601724be8711a032950811bf326f8219c50c4808d3cd\",\n \"witness\": \"{\\\"signatures\\\":[\\\"ce017ca25b1b97df2f72e4b49f69ac26a240ce14b3690a8fe619d41ccc42d3c1282e073f85acd36dc50011638906f35b56615f24e4d03e8effe8257f6a808538\\\"]}\"\n }\n ],\n \"outputs\": [\n {\n \"amount\": 2,\n \"id\": \"00bfa73302d12ffd\",\n \"B_\": \"038ec853d65ae1b79b5cdbc2774150b2cb288d6d26e12958a16fb33c32d9a86c39\"\n }\n ]\n}"; - var signedSwapRequestParsed = JsonSerializer.Deserialize(signedSwapRequest); - Assert.True(SigAllHandler.VerifySigAllWitness(signedSwapRequestParsed.Inputs, signedSwapRequestParsed.Outputs)); - var witness = JsonSerializer.Deserialize(signedSwapRequestParsed.Inputs.First().Witness); - Assert.True(SigAllHandler.VerifySigAllWitness(signedSwapRequestParsed.Inputs, signedSwapRequestParsed.Outputs, witness)); + var signedSwapRequestParsed = JsonSerializer.Deserialize( + signedSwapRequest + ); + Assert.True( + SigAllHandler.VerifySigAllWitness( + signedSwapRequestParsed.Inputs, + signedSwapRequestParsed.Outputs + ) + ); + var witness = JsonSerializer.Deserialize( + signedSwapRequestParsed.Inputs.First().Witness + ); + Assert.True( + SigAllHandler.VerifySigAllWitness( + signedSwapRequestParsed.Inputs, + signedSwapRequestParsed.Outputs, + witness + ) + ); var validSwapRequest = "{\n \"inputs\": [\n {\n \"amount\": 2,\n \"id\": \"00bfa73302d12ffd\",\n \"secret\": \"[\\\"P2PK\\\",{\\\"nonce\\\":\\\"c7f280eb55c1e8564e03db06973e94bc9b666d9e1ca42ad278408fe625950303\\\",\\\"data\\\":\\\"030d8acedfe072c9fa449a1efe0817157403fbec460d8e79f957966056e5dd76c1\\\",\\\"tags\\\":[[\\\"sigflag\\\",\\\"SIG_ALL\\\"]]}]\",\n \"C\": \"02c97ee3d1db41cf0a3ddb601724be8711a032950811bf326f8219c50c4808d3cd\",\n \"witness\": \"{\\\"signatures\\\":[\\\"ce017ca25b1b97df2f72e4b49f69ac26a240ce14b3690a8fe619d41ccc42d3c1282e073f85acd36dc50011638906f35b56615f24e4d03e8effe8257f6a808538\\\"]}\"\n }\n ],\n \"outputs\": [\n {\n \"amount\": 2,\n \"id\": \"00bfa73302d12ffd\",\n \"B_\": \"038ec853d65ae1b79b5cdbc2774150b2cb288d6d26e12958a16fb33c32d9a86c39\"\n }\n ]\n}"; var validSwapRequestParsed = JsonSerializer.Deserialize(validSwapRequest); - var witness1 = JsonSerializer.Deserialize(validSwapRequestParsed?.Inputs[0].Witness); - Assert.True(SigAllHandler.VerifySigAllWitness(validSwapRequestParsed.Inputs, validSwapRequestParsed.Outputs, witness1)); - Assert.True(SigAllHandler.VerifySigAllWitness(validSwapRequestParsed.Inputs, validSwapRequestParsed.Outputs)); - + var witness1 = JsonSerializer.Deserialize( + validSwapRequestParsed?.Inputs[0].Witness + ); + Assert.True( + SigAllHandler.VerifySigAllWitness( + validSwapRequestParsed.Inputs, + validSwapRequestParsed.Outputs, + witness1 + ) + ); + Assert.True( + SigAllHandler.VerifySigAllWitness( + validSwapRequestParsed.Inputs, + validSwapRequestParsed.Outputs + ) + ); + var invalidSwapRequest = "{\n \"inputs\": [\n {\n \"amount\": 1,\n \"id\": \"00bfa73302d12ffd\",\n \"secret\": \"[\\\"P2PK\\\",{\\\"nonce\\\":\\\"fa6dd3fac9086c153878dec90b9e37163d38ff2ecf8b37db6470e9d185abbbae\\\",\\\"data\\\":\\\"033b42b04e659fed13b669f8b16cdaffc3ee5738608810cf97a7631d09bd01399d\\\",\\\"tags\\\":[[\\\"sigflag\\\",\\\"SIG_ALL\\\"]]}]\",\n \"C\": \"024d232312bab25af2e73f41d56864d378edca9109ae8f76e1030e02e585847786\",\n \"witness\": \"{\\\"signatures\\\":[\\\"27b4d260a1186e3b62a26c0d14ffeab3b9f7c3889e78707b8fd3836b473a00601afbd53a2288ad20a624a8bbe3344453215ea075fc0ce479dd8666fd3d9162cc\\\"]}\"\n },\n {\n \"amount\": 2,\n \"id\": \"00bfa73302d12ffd\",\n \"secret\": \"[\\\"P2PK\\\",{\\\"nonce\\\":\\\"4007b21fc5f5b1d4920bc0a08b158d98fd0fb2b0b0262b57ff53c6c5d6c2ae8c\\\",\\\"data\\\":\\\"033b42b04e659fed13b669f8b16cdaffc3ee5738608810cf97a7631d09bd01399d\\\",\\\"tags\\\":[[\\\"locktime\\\",\\\"122222222222222\\\"],[\\\"sigflag\\\",\\\"SIG_ALL\\\"]]}]\",\n \"C\": \"02417400f2af09772219c831501afcbab4efb3b2e75175635d5474069608deb641\"\n }\n ],\n \"outputs\": [\n {\n \"amount\": 1,\n \"id\": \"00bfa73302d12ffd\",\n \"B_\": \"038ec853d65ae1b79b5cdbc2774150b2cb288d6d26e12958a16fb33c32d9a86c39\"\n },\n {\n \"amount\": 1,\n \"id\": \"00bfa73302d12ffd\",\n \"B_\": \"03afe7c87e32d436f0957f1d70a2bca025822a84a8623e3a33aed0a167016e0ca5\"\n },\n {\n \"amount\": 1,\n \"id\": \"00bfa73302d12ffd\",\n \"B_\": \"02c0d4fce02a7a0f09e3f1bca952db910b17e81a7ebcbce62cd8dcfb127d21e37b\"\n }\n ]\n}"; - var invalidSwapRequestParsed = JsonSerializer.Deserialize(invalidSwapRequest); - Assert.False(SigAllHandler.VerifySigAllWitness(invalidSwapRequestParsed.Inputs, invalidSwapRequestParsed.Outputs)); - var witness2 = JsonSerializer.Deserialize(invalidSwapRequestParsed?.Inputs[0].Witness); - Assert.False(SigAllHandler.VerifySigAllWitness(invalidSwapRequestParsed.Inputs, invalidSwapRequestParsed.Outputs, witness2)); - - var validSwapRequestMultisig = + var invalidSwapRequestParsed = JsonSerializer.Deserialize( + invalidSwapRequest + ); + Assert.False( + SigAllHandler.VerifySigAllWitness( + invalidSwapRequestParsed.Inputs, + invalidSwapRequestParsed.Outputs + ) + ); + var witness2 = JsonSerializer.Deserialize( + invalidSwapRequestParsed?.Inputs[0].Witness + ); + Assert.False( + SigAllHandler.VerifySigAllWitness( + invalidSwapRequestParsed.Inputs, + invalidSwapRequestParsed.Outputs, + witness2 + ) + ); + + var validSwapRequestMultisig = "{\n \"inputs\": [\n {\n \"amount\": 2,\n \"id\": \"00bfa73302d12ffd\",\n \"secret\": \"[\\\"P2PK\\\",{\\\"nonce\\\":\\\"04bfd885fc982d553711092d037fdceb7320fd8f96b0d4fd6d31a65b83b94272\\\",\\\"data\\\":\\\"0275e78025b558dbe6cb8fdd032a2e7613ca14fda5c1f4c4e3427f5077a7bd90e4\\\",\\\"tags\\\":[[\\\"pubkeys\\\",\\\"035163650bbd5ed4be7693f40f340346ba548b941074e9138b67ef6c42755f3449\\\",\\\"02817d22a8edc44c4141e192995a7976647c335092199f9e076a170c7336e2f5cc\\\"],[\\\"n_sigs\\\",\\\"2\\\"],[\\\"sigflag\\\",\\\"SIG_ALL\\\"]]}]\",\n \"C\": \"03866a09946562482c576ca989d06371e412b221890804c7da8887d321380755be\",\n \"witness\": \"{\\\"signatures\\\":[\\\"be1d72c5ca16a93c5a34f25ec63ce632ddc3176787dac363321af3fd0f55d1927e07451bc451ffe5c682d76688ea9925d7977dffbb15bd79763b527f474734b0\\\",\\\"669d6d10d7ed35395009f222f6c7bdc28a378a1ebb72ee43117be5754648501da3bedf2fd6ff0c7849ac92683538c60af0af504102e40f2d8daca8e08b1ca16b\\\"]}\"\n }\n ],\n \"outputs\": [\n {\n \"amount\": 2,\n \"id\": \"00bfa73302d12ffd\",\n \"B_\": \"038ec853d65ae1b79b5cdbc2774150b2cb288d6d26e12958a16fb33c32d9a86c39\"\n }\n ]\n}"; - var validSwapRequestMultisigParsed = JsonSerializer.Deserialize(validSwapRequestMultisig); - var witness3 = JsonSerializer.Deserialize(validSwapRequestMultisigParsed.Inputs[0].Witness); - Assert.True(SigAllHandler.VerifySigAllWitness(validSwapRequestMultisigParsed.Inputs, validSwapRequestMultisigParsed.Outputs, witness3)); - Assert.True(SigAllHandler.VerifySigAllWitness(validSwapRequestMultisigParsed.Inputs, validSwapRequestMultisigParsed.Outputs)); - - var validSwapRequestMultisigRefundLocktime = + var validSwapRequestMultisigParsed = JsonSerializer.Deserialize( + validSwapRequestMultisig + ); + var witness3 = JsonSerializer.Deserialize( + validSwapRequestMultisigParsed.Inputs[0].Witness + ); + Assert.True( + SigAllHandler.VerifySigAllWitness( + validSwapRequestMultisigParsed.Inputs, + validSwapRequestMultisigParsed.Outputs, + witness3 + ) + ); + Assert.True( + SigAllHandler.VerifySigAllWitness( + validSwapRequestMultisigParsed.Inputs, + validSwapRequestMultisigParsed.Outputs + ) + ); + + + var validSwapRequestMultisigRefundLocktime = "{\n \"inputs\": [\n {\n \"amount\": 2,\n \"id\": \"00bfa73302d12ffd\",\n \"secret\": \"[\\\"P2PK\\\",{\\\"nonce\\\":\\\"9ea35553beb18d553d0a53120d0175a0991ca6109370338406eed007b26eacd1\\\",\\\"data\\\":\\\"02af21e09300af92e7b48c48afdb12e22933738cfb9bba67b27c00c679aae3ec25\\\",\\\"tags\\\":[[\\\"locktime\\\",\\\"1\\\"],[\\\"refund\\\",\\\"02637c19143c58b2c58bd378400a7b82bdc91d6dedaeb803b28640ef7d28a887ac\\\",\\\"0345c7fdf7ec7c8e746cca264bf27509eb4edb9ac421f8fbfab1dec64945a4d797\\\"],[\\\"n_sigs_refund\\\",\\\"2\\\"],[\\\"sigflag\\\",\\\"SIG_ALL\\\"]]}]\",\n \"C\": \"03dd83536fbbcbb74ccb3c87147df26753fd499cc2c095f74367fff0fb459c312e\",\n \"witness\": \"{\\\"signatures\\\":[\\\"23b58ef28cd22f3dff421121240ddd621deee83a3bc229fd67019c2e338d91e2c61577e081e1375dbab369307bba265e887857110ca3b4bd949211a0a298805f\\\",\\\"7e75948ef1513564fdcecfcbd389deac67c730f7004f8631ba90c0844d3e8c0cf470b656306877df5141f65fd3b7e85445a8452c3323ab273e6d0d44843817ed\\\"]}\"\n }\n ],\n \"outputs\": [\n {\n \"amount\": 2,\n \"id\": \"00bfa73302d12ffd\",\n \"B_\": \"038ec853d65ae1b79b5cdbc2774150b2cb288d6d26e12958a16fb33c32d9a86c39\"\n }\n ]\n}"; var validSwapRequestMultisigRefundLocktimeParsed = JsonSerializer.Deserialize(validSwapRequestMultisigRefundLocktime); - var witness5 = JsonSerializer.Deserialize(validSwapRequestMultisigRefundLocktimeParsed.Inputs[0].Witness); - Assert.True(SigAllHandler.VerifySigAllWitness(validSwapRequestMultisigRefundLocktimeParsed.Inputs, validSwapRequestMultisigRefundLocktimeParsed.Outputs, witness5)); - Assert.True(SigAllHandler.VerifySigAllWitness(validSwapRequestMultisigRefundLocktimeParsed.Inputs, validSwapRequestMultisigRefundLocktimeParsed.Outputs)); - - var validSwapRequestHTLC = + var witness5 = JsonSerializer.Deserialize( + validSwapRequestMultisigRefundLocktimeParsed.Inputs[0].Witness + ); + Assert.True( + SigAllHandler.VerifySigAllWitness( + validSwapRequestMultisigRefundLocktimeParsed.Inputs, + validSwapRequestMultisigRefundLocktimeParsed.Outputs, + witness5 + ) + ); + Assert.True( + SigAllHandler.VerifySigAllWitness( + validSwapRequestMultisigRefundLocktimeParsed.Inputs, + validSwapRequestMultisigRefundLocktimeParsed.Outputs + ) + ); + + var validSwapRequestHTLC = "{\n \"inputs\": [\n {\n \"amount\": 2,\n \"id\": \"00bfa73302d12ffd\",\n \"secret\": \"[\\\"HTLC\\\",{\\\"nonce\\\":\\\"d730dd70cd7ec6e687829857de8e70aab2b970712f4dbe288343eca20e63c28c\\\",\\\"data\\\":\\\"ec4916dd28fc4c10d78e287ca5d9cc51ee1ae73cbfde08c6b37324cbfaac8bc5\\\",\\\"tags\\\":[[\\\"pubkeys\\\",\\\"0350cda8a1d5257dbd6ba8401a9a27384b9ab699e636e986101172167799469b14\\\"],[\\\"sigflag\\\",\\\"SIG_ALL\\\"]]}]\",\n \"C\": \"03ff6567e2e6c31db5cb7189dab2b5121930086791c93899e4eff3dda61cb57273\",\n \"witness\": \"{\\\"preimage\\\":\\\"0000000000000000000000000000000000000000000000000000000000000001\\\",\\\"signatures\\\":[\\\"a4c00a9ad07f9936e404494fda99a9b935c82d7c053173b304b8663124c81d4b00f64a225f5acf41043ca52b06382722bd04ded0fbeb0fcc404eed3b24778b88\\\"]}\"\n }\n ],\n \"outputs\": [\n {\n \"amount\": 2,\n \"id\": \"00bfa73302d12ffd\",\n \"B_\": \"038ec853d65ae1b79b5cdbc2774150b2cb288d6d26e12958a16fb33c32d9a86c39\"\n }\n ]\n}"; - var validSwapRequestHTLCParsed = - JsonSerializer.Deserialize(validSwapRequestHTLC); - var witness6 = JsonSerializer.Deserialize(validSwapRequestHTLCParsed.Inputs[0].Witness); - var b = SigAllHandler.VerifySigAllWitness(validSwapRequestHTLCParsed.Inputs, validSwapRequestHTLCParsed.Outputs, - witness6); + var validSwapRequestHTLCParsed = JsonSerializer.Deserialize( + validSwapRequestHTLC + ); + var witness6 = JsonSerializer.Deserialize( + validSwapRequestHTLCParsed.Inputs[0].Witness + ); + var b = SigAllHandler.VerifySigAllWitness( + validSwapRequestHTLCParsed.Inputs, + validSwapRequestHTLCParsed.Outputs, + witness6 + ); Assert.True(b); - Assert.True(SigAllHandler.VerifySigAllWitness(validSwapRequestHTLCParsed.Inputs, validSwapRequestHTLCParsed.Outputs)); + Assert.True( + SigAllHandler.VerifySigAllWitness( + validSwapRequestHTLCParsed.Inputs, + validSwapRequestHTLCParsed.Outputs + ) + ); - var invalidSwapRequestHTLC = + var invalidSwapRequestHTLC = "{\n \"inputs\": [\n {\n \"amount\": 2,\n \"id\": \"00bfa73302d12ffd\",\n \"secret\": \"[\\\"HTLC\\\",{\\\"nonce\\\":\\\"512c4045f12fdfd6f55059669c189e040c37c1ce2f8be104ed6aec296acce4e9\\\",\\\"data\\\":\\\"ec4916dd28fc4c10d78e287ca5d9cc51ee1ae73cbfde08c6b37324cbfaac8bc5\\\",\\\"tags\\\":[[\\\"pubkeys\\\",\\\"03ba83defd31c63f8841d188f0d41b5bb3af1bb3c08d0ba46f8f1d26a4d45e8cad\\\"],[\\\"locktime\\\",\\\"4854185133\\\"],[\\\"refund\\\",\\\"032f1008a79c722e93a1b4b853f85f38283f9ef74ee4c5c91293eb1cc3c5e46e34\\\"],[\\\"sigflag\\\",\\\"SIG_ALL\\\"]]}]\",\n \"C\": \"02207abeff828146f1fc3909c74613d5605bd057f16791994b3c91f045b39a6939\",\n \"witness\": \"{\\\"preimage\\\":\\\"0000000000000000000000000000000000000000000000000000000000000001\\\",\\\"signatures\\\":[\\\"7816d57871bde5be2e4281065dbe5b15f641d8f1ed9437a3ae556464d6f9b8a0a2e6660337a915f2c26dce1453a416daf682b8fb593b67a0750fce071e0759b9\\\"]}\"\n }\n ],\n \"outputs\": [\n {\n \"amount\": 1,\n \"id\": \"00bfa73302d12ffd\",\n \"B_\": \"038ec853d65ae1b79b5cdbc2774150b2cb288d6d26e12958a16fb33c32d9a86c39\"\n },\n {\n \"amount\": 1,\n \"id\": \"00bfa73302d12ffd\",\n \"B_\": \"03afe7c87e32d436f0957f1d70a2bca025822a84a8623e3a33aed0a167016e0ca5\"\n }\n ]\n}"; - var invalidSwapRequestHTLCParsed = JsonSerializer.Deserialize(invalidSwapRequestHTLC); - Assert.False(SigAllHandler.VerifySigAllWitness(invalidSwapRequestHTLCParsed.Inputs, invalidSwapRequestHTLCParsed.Outputs)); - var witness7 = JsonSerializer.Deserialize(invalidSwapRequestHTLCParsed.Inputs[0].Witness); - Assert.False(SigAllHandler.VerifySigAllWitness(invalidSwapRequestHTLCParsed.Inputs, invalidSwapRequestHTLCParsed.Outputs, witness7)); + var invalidSwapRequestHTLCParsed = JsonSerializer.Deserialize( + invalidSwapRequestHTLC + ); + Assert.False( + SigAllHandler.VerifySigAllWitness( + invalidSwapRequestHTLCParsed.Inputs, + invalidSwapRequestHTLCParsed.Outputs + ) + ); + var witness7 = JsonSerializer.Deserialize( + invalidSwapRequestHTLCParsed.Inputs[0].Witness + ); + Assert.False( + SigAllHandler.VerifySigAllWitness( + invalidSwapRequestHTLCParsed.Inputs, + invalidSwapRequestHTLCParsed.Outputs, + witness7 + ) + ); var validSwapRequestHTLCMultisig = "{\n \"inputs\": [\n {\n \"amount\": 2,\n \"id\": \"00bfa73302d12ffd\",\n \"secret\": \"[\\\"HTLC\\\",{\\\"nonce\\\":\\\"c9b0fabb8007c0db4bef64d5d128cdcf3c79e8bb780c3294adf4c88e96c32647\\\",\\\"data\\\":\\\"ec4916dd28fc4c10d78e287ca5d9cc51ee1ae73cbfde08c6b37324cbfaac8bc5\\\",\\\"tags\\\":[[\\\"pubkeys\\\",\\\"039e6ec7e922abb4162235b3a42965eb11510b07b7461f6b1a17478b1c9c64d100\\\"],[\\\"locktime\\\",\\\"1\\\"],[\\\"refund\\\",\\\"02ce1bbd2c9a4be8029c9a6435ad601c45677f5cde81f8a7f0ed535e0039d0eb6c\\\",\\\"03c43c00ff57f63cfa9e732f0520c342123e21331d0121139f1b636921eeec095f\\\"],[\\\"n_sigs_refund\\\",\\\"2\\\"],[\\\"sigflag\\\",\\\"SIG_ALL\\\"]]}]\",\n \"C\": \"0344b6f1471cf18a8cbae0e624018c816be5e3a9b04dcb7689f64173c1ae90a3a5\",\n \"witness\": \"{\\\"preimage\\\":\\\"0000000000000000000000000000000000000000000000000000000000000001\\\",\\\"signatures\\\":[\\\"98e21672d409cc782c720f203d8284f0af0c8713f18167499f9f101b7050c3e657fb0e57478ebd8bd561c31aa6c30f4cd20ec38c73f5755b7b4ddee693bca5a5\\\",\\\"693f40129dbf905ed9c8008081c694f72a36de354f9f4fa7a61b389cf781f62a0ae0586612fb2eb504faaf897fefb6742309186117f4743bcebcb8e350e975e2\\\"]}\"\n }\n ],\n \"outputs\": [\n {\n \"amount\": 2,\n \"id\": \"00bfa73302d12ffd\",\n \"B_\": \"038ec853d65ae1b79b5cdbc2774150b2cb288d6d26e12958a16fb33c32d9a86c39\"\n }\n ]\n}"; - var validSwapRequestHTLCMultisigParsed = JsonSerializer.Deserialize(validSwapRequestHTLCMultisig); - var witness8 = JsonSerializer.Deserialize(validSwapRequestHTLCMultisigParsed.Inputs[0].Witness); - Assert.True(SigAllHandler.VerifySigAllWitness(validSwapRequestHTLCMultisigParsed.Inputs, validSwapRequestHTLCMultisigParsed.Outputs, witness8)); - Assert.True(SigAllHandler.VerifySigAllWitness(validSwapRequestHTLCMultisigParsed.Inputs, validSwapRequestHTLCMultisigParsed.Outputs)); + var validSwapRequestHTLCMultisigParsed = JsonSerializer.Deserialize( + validSwapRequestHTLCMultisig + ); + var witness8 = JsonSerializer.Deserialize( + validSwapRequestHTLCMultisigParsed.Inputs[0].Witness + ); + Assert.True( + SigAllHandler.VerifySigAllWitness( + validSwapRequestHTLCMultisigParsed.Inputs, + validSwapRequestHTLCMultisigParsed.Outputs, + witness8 + ) + ); + Assert.True( + SigAllHandler.VerifySigAllWitness( + validSwapRequestHTLCMultisigParsed.Inputs, + validSwapRequestHTLCMultisigParsed.Outputs + ) + ); - var meltRequest = + var meltRequest = "{\n \"quote\": \"cF8911fzT88aEi1d-6boZZkq5lYxbUSVs-HbJxK0\",\n \"inputs\": [\n {\n \"amount\": 2,\n \"id\": \"00bfa73302d12ffd\",\n \"secret\": \"[\\\"P2PK\\\",{\\\"nonce\\\":\\\"bbf9edf441d17097e39f5095a3313ba24d3055ab8a32f758ff41c10d45c4f3de\\\",\\\"data\\\":\\\"029116d32e7da635c8feeb9f1f4559eb3d9b42d400f9d22a64834d89cde0eb6835\\\",\\\"tags\\\":[[\\\"sigflag\\\",\\\"SIG_ALL\\\"]]}]\",\n \"C\": \"02a9d461ff36448469dccf828fa143833ae71c689886ac51b62c8d61ddaa10028b\",\n \"witness\": \"{\\\"signatures\\\":[\\\"478224fbe715e34f78cb33451db6fcf8ab948afb8bd04ff1a952c92e562ac0f7c1cb5e61809410635be0aa94d0448f7f7959bd5762cc3802b0a00ff58b2da747\\\"]}\"\n }\n ],\n \"outputs\": [\n {\n \"amount\": 0,\n \"id\": \"00bfa73302d12ffd\",\n \"B_\": \"038ec853d65ae1b79b5cdbc2774150b2cb288d6d26e12958a16fb33c32d9a86c39\"\n }\n ]\n}"; var meltRequestParsed = JsonSerializer.Deserialize(meltRequest); var msg2 = "[\"P2PK\",{\"nonce\":\"bbf9edf441d17097e39f5095a3313ba24d3055ab8a32f758ff41c10d45c4f3de\",\"data\":\"029116d32e7da635c8feeb9f1f4559eb3d9b42d400f9d22a64834d89cde0eb6835\",\"tags\":[[\"sigflag\",\"SIG_ALL\"]]}]02a9d461ff36448469dccf828fa143833ae71c689886ac51b62c8d61ddaa10028b0038ec853d65ae1b79b5cdbc2774150b2cb288d6d26e12958a16fb33c32d9a86c39cF8911fzT88aEi1d-6boZZkq5lYxbUSVs-HbJxK0"; - Assert.Equal(SigAllHandler.GetMessageToSign(meltRequestParsed.Inputs, meltRequestParsed.Outputs, meltRequestParsed.Quote), msg2); + Assert.Equal( + SigAllHandler.GetMessageToSign( + meltRequestParsed.Inputs, + meltRequestParsed.Outputs, + meltRequestParsed.Quote + ), + msg2 + ); - var meltRequestValid = + var meltRequestValid = "{\n \"quote\": \"cF8911fzT88aEi1d-6boZZkq5lYxbUSVs-HbJxK0\",\n \"inputs\": [\n {\n \"amount\": 2,\n \"id\": \"00bfa73302d12ffd\",\n \"secret\": \"[\\\"P2PK\\\",{\\\"nonce\\\":\\\"bbf9edf441d17097e39f5095a3313ba24d3055ab8a32f758ff41c10d45c4f3de\\\",\\\"data\\\":\\\"029116d32e7da635c8feeb9f1f4559eb3d9b42d400f9d22a64834d89cde0eb6835\\\",\\\"tags\\\":[[\\\"sigflag\\\",\\\"SIG_ALL\\\"]]}]\",\n \"C\": \"02a9d461ff36448469dccf828fa143833ae71c689886ac51b62c8d61ddaa10028b\",\n \"witness\": \"{\\\"signatures\\\":[\\\"478224fbe715e34f78cb33451db6fcf8ab948afb8bd04ff1a952c92e562ac0f7c1cb5e61809410635be0aa94d0448f7f7959bd5762cc3802b0a00ff58b2da747\\\"]}\"\n }\n ],\n \"outputs\": [\n {\n \"amount\": 0,\n \"id\": \"00bfa73302d12ffd\",\n \"B_\": \"038ec853d65ae1b79b5cdbc2774150b2cb288d6d26e12958a16fb33c32d9a86c39\"\n }\n ]\n}"; var meltRequestValidParsed = JsonSerializer.Deserialize(meltRequestValid); - Assert.True(SigAllHandler.VerifySigAllWitness(meltRequestValidParsed.Inputs, meltRequestValidParsed.Outputs, meltRequestValidParsed.Quote)); - var witness9 = JsonSerializer.Deserialize(meltRequestValidParsed.Inputs[0].Witness); - Assert.True(SigAllHandler.VerifySigAllWitness(meltRequestValidParsed.Inputs, meltRequestValidParsed.Outputs, witness9, meltRequestValidParsed.Quote)); + Assert.True( + SigAllHandler.VerifySigAllWitness( + meltRequestValidParsed.Inputs, + meltRequestValidParsed.Outputs, + meltRequestValidParsed.Quote + ) + ); + var witness9 = JsonSerializer.Deserialize( + meltRequestValidParsed.Inputs[0].Witness + ); + Assert.True( + SigAllHandler.VerifySigAllWitness( + meltRequestValidParsed.Inputs, + meltRequestValidParsed.Outputs, + witness9, + meltRequestValidParsed.Quote + ) + ); var meltRequestMultisig = "{\n \"quote\": \"Db3qEMVwFN2tf_1JxbZp29aL5cVXpSMIwpYfyOVF\",\n \"inputs\": [\n {\n \"amount\": 2,\n \"id\": \"00bfa73302d12ffd\",\n \"secret\": \"[\\\"P2PK\\\",{\\\"nonce\\\":\\\"68d7822538740e4f9c9ebf5183ef6c4501c7a9bca4e509ce2e41e1d62e7b8a99\\\",\\\"data\\\":\\\"0394e841bd59aeadce16380df6174cb29c9fea83b0b65b226575e6d73cc5a1bd59\\\",\\\"tags\\\":[[\\\"pubkeys\\\",\\\"033d892d7ad2a7d53708b7a5a2af101cbcef69522bd368eacf55fcb4f1b0494058\\\"],[\\\"n_sigs\\\",\\\"2\\\"],[\\\"sigflag\\\",\\\"SIG_ALL\\\"]]}]\",\n \"C\": \"03a70c42ec9d7192422c7f7a3ad017deda309fb4a2453fcf9357795ea706cc87a9\",\n \"witness\": \"{\\\"signatures\\\":[\\\"ed739970d003f703da2f101a51767b63858f4894468cc334be04aa3befab1617a81e3eef093441afb499974152d279e59d9582a31dc68adbc17ffc22a2516086\\\",\\\"f9efe1c70eb61e7ad8bd615c50ff850410a4135ea73ba5fd8e12a734743ad045e575e9e76ea5c52c8e7908d3ad5c0eaae93337e5c11109e52848dc328d6757a2\\\"]}\"\n }\n ],\n \"outputs\": [\n {\n \"amount\": 0,\n \"id\": \"00bfa73302d12ffd\",\n \"B_\": \"038ec853d65ae1b79b5cdbc2774150b2cb288d6d26e12958a16fb33c32d9a86c39\"\n }\n ]\n}"; - var meltRequestMultisigParsed = JsonSerializer.Deserialize(meltRequestMultisig); - Assert.True(SigAllHandler.VerifySigAllWitness(meltRequestMultisigParsed.Inputs, meltRequestMultisigParsed.Outputs, meltRequestMultisigParsed.Quote)); - var witness10 = JsonSerializer.Deserialize(meltRequestMultisigParsed.Inputs[0].Witness); - Assert.True(SigAllHandler.VerifySigAllWitness(meltRequestMultisigParsed.Inputs, meltRequestMultisigParsed.Outputs, witness10, meltRequestMultisigParsed.Quote)); + var meltRequestMultisigParsed = JsonSerializer.Deserialize( + meltRequestMultisig + ); + Assert.True( + SigAllHandler.VerifySigAllWitness( + meltRequestMultisigParsed.Inputs, + meltRequestMultisigParsed.Outputs, + meltRequestMultisigParsed.Quote + ) + ); + var witness10 = JsonSerializer.Deserialize( + meltRequestMultisigParsed.Inputs[0].Witness + ); + Assert.True( + SigAllHandler.VerifySigAllWitness( + meltRequestMultisigParsed.Inputs, + meltRequestMultisigParsed.Outputs, + witness10, + meltRequestMultisigParsed.Quote + ) + ); } - + [Fact] public void Nut12Tests_Hash_e() { @@ -500,7 +781,8 @@ public void Nut12Tests_BlindSignaturesDLEQ() var A = "0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798".ToPubKey(); var B_ = "02a9acc1e48c25eeeb9289b5031cc57da9fe72f3fe2861d264bdc074209b107ba2".ToPubKey(); var blindSig = JsonSerializer.Deserialize( - "{\n \"amount\": 8,\n \"id\": \"00882760bfa2eb41\",\n \"C_\": \"02a9acc1e48c25eeeb9289b5031cc57da9fe72f3fe2861d264bdc074209b107ba2\",\n \"dleq\": {\n \"e\": \"9818e061ee51d5c8edc3342369a554998ff7b4381c8652d724cdf46429be73d9\",\n \"s\": \"9818e061ee51d5c8edc3342369a554998ff7b4381c8652d724cdf46429be73da\"\n }\n}"); + "{\n \"amount\": 8,\n \"id\": \"00882760bfa2eb41\",\n \"C_\": \"02a9acc1e48c25eeeb9289b5031cc57da9fe72f3fe2861d264bdc074209b107ba2\",\n \"dleq\": {\n \"e\": \"9818e061ee51d5c8edc3342369a554998ff7b4381c8652d724cdf46429be73d9\",\n \"s\": \"9818e061ee51d5c8edc3342369a554998ff7b4381c8652d724cdf46429be73da\"\n }\n}" + ); Assert.NotNull(blindSig?.DLEQ); blindSig.Verify(A, B_); @@ -511,10 +793,13 @@ public void Nut12Tests_ProofDLEQ() { var A = "0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798".ToPubKey(); var proof = JsonSerializer.Deserialize( - "{\"amount\": 1,\"id\": \"00882760bfa2eb41\",\"secret\": \"daf4dd00a2b68a0858a80450f52c8a7d2ccf87d375e43e216e0c571f089f63e9\",\"C\": \"024369d2d22a80ecf78f3937da9d5f30c1b9f74f0c32684d583cca0fa6a61cdcfc\",\"dleq\": {\"e\": \"b31e58ac6527f34975ffab13e70a48b6d2b0d35abc4b03f0151f09ee1a9763d4\",\"s\": \"8fbae004c59e754d71df67e392b6ae4e29293113ddc2ec86592a0431d16306d8\",\"r\": \"a6d13fcd7a18442e6076f5e1e7c887ad5de40a019824bdfa9fe740d302e8d861\"}}"); + "{\"amount\": 1,\"id\": \"00882760bfa2eb41\",\"secret\": \"daf4dd00a2b68a0858a80450f52c8a7d2ccf87d375e43e216e0c571f089f63e9\",\"C\": \"024369d2d22a80ecf78f3937da9d5f30c1b9f74f0c32684d583cca0fa6a61cdcfc\",\"dleq\": {\"e\": \"b31e58ac6527f34975ffab13e70a48b6d2b0d35abc4b03f0151f09ee1a9763d4\",\"s\": \"8fbae004c59e754d71df67e392b6ae4e29293113ddc2ec86592a0431d16306d8\",\"r\": \"a6d13fcd7a18442e6076f5e1e7c887ad5de40a019824bdfa9fe740d302e8d861\"}}" + ); Assert.NotNull(proof?.DLEQ); - Assert.Equal("024369d2d22a80ecf78f3937da9d5f30c1b9f74f0c32684d583cca0fa6a61cdcfc".ToPubKey(), - proof.Secret.ToCurve()); + Assert.Equal( + "024369d2d22a80ecf78f3937da9d5f30c1b9f74f0c32684d583cca0fa6a61cdcfc".ToPubKey(), + proof.Secret.ToCurve() + ); Assert.True(proof.Verify(A)); } @@ -540,46 +825,58 @@ public void Nut18Tests() var creqA = "creqApWF0gaNhdGVub3N0cmFheKlucHJvZmlsZTFxeTI4d3VtbjhnaGo3dW45ZDNzaGp0bnl2OWtoMnVld2Q5aHN6OW1od2RlbjV0ZTB3ZmprY2N0ZTljdXJ4dmVuOWVlaHFjdHJ2NWhzenJ0aHdkZW41dGUwZGVoaHh0bnZkYWtxcWd5ZGFxeTdjdXJrNDM5eWtwdGt5c3Y3dWRoZGh1NjhzdWNtMjk1YWtxZWZkZWhrZjBkNDk1Y3d1bmw1YWeBgmFuYjE3YWloYjdhOTAxNzZhYQphdWNzYXRhbYF4Imh0dHBzOi8vbm9mZWVzLnRlc3RudXQuY2FzaHUuc3BhY2U="; var pr = PaymentRequest.Parse(creqA); - Assert.Equal("https://nofees.testnut.cashu.space", Assert.Single( pr.Mints)); + Assert.Equal("https://nofees.testnut.cashu.space", Assert.Single(pr.Mints)); Assert.Equal((ulong)10, pr.Amount); Assert.Equal("b7a90176", pr.PaymentId); Assert.Equal("sat", pr.Unit); var t = Assert.Single(pr.Transports); Assert.Equal("nostr", t.Type); - Assert.Equal("nprofile1qy28wumn8ghj7un9d3shjtnyv9kh2uewd9hsz9mhwden5te0wfjkccte9curxven9eehqctrv5hszrthwden5te0dehhxtnvdakqqgydaqy7curk439ykptkysv7udhdhu68sucm295akqefdehkf0d495cwunl5", t.Target); - Assert.Equal("n",Assert.Single(t.Tags).Key ); - Assert.Equal("17",Assert.Single(t.Tags).Value[0] ); + Assert.Equal( + "nprofile1qy28wumn8ghj7un9d3shjtnyv9kh2uewd9hsz9mhwden5te0wfjkccte9curxven9eehqctrv5hszrthwden5te0dehhxtnvdakqqgydaqy7curk439ykptkysv7udhdhu68sucm295akqefdehkf0d495cwunl5", + t.Target + ); + Assert.Equal("n", Assert.Single(t.Tags).Key); + Assert.Equal("17", Assert.Single(t.Tags).Value[0]); // Assert.Equal(creqA, pr.ToString()); - } - + internal readonly struct TestCase(in string path, in string keyHex, in string ccHex) { internal static readonly ReadOnlyMemory Seed = Convert.FromHexString( - "e4a964f4973ce5750a6a5a5126e8258442c197b2e71b683ccba58688f21242eae1b0f12bee21d6e983d4a5c61f081bf3f0669546eb576dec1b22ec8d481b00fb"); + "e4a964f4973ce5750a6a5a5126e8258442c197b2e71b683ccba58688f21242eae1b0f12bee21d6e983d4a5c61f081bf3f0669546eb576dec1b22ec8d481b00fb" + ); internal readonly ReadOnlyMemory Key = Convert.FromHexString(keyHex); internal readonly ReadOnlyMemory ChainCode = Convert.FromHexString(ccHex); internal readonly KeyPath Path = path; } - + private static readonly TestCase Case1SecP256K1 = new( "m/0'/0/0", "6144c1daf8222d6dab77e7a20c2f338519b83bd1423602c56c7dfb5e9ea99c02", - "55b36970e7ab8434f9b04f1c2e52da7422d2bce7e284ca353419dddfa2e34bdb"); + "55b36970e7ab8434f9b04f1c2e52da7422d2bce7e284ca353419dddfa2e34bdb" + ); [Fact] public void Bip32Test() { var masterKeyFromSeed = BIP32.Instance.GetMasterKeyFromSeed(TestCase.Seed.Span); - - Assert.Equal("5A876CC4B4AB2F6717951AEE7F97AB69844DBFFFF7074E6E6F71D2BA04BD6EC9", Convert.ToHexString( masterKeyFromSeed.ChainCode)); - Assert.Equal("8D18D3F0CF9D74B53A935D97E8DE85955ED9F6EEFC6D6D45F0C169031A11B669", Convert.ToHexString( masterKeyFromSeed.PrivateKey)); - - - Assert.Equal("026cf0d14fcfa930347e7da26281319ac5959d02f1b6331812261efdb7e347788b",ECPrivKey.Create(masterKeyFromSeed.PrivateKey).CreatePubKey().ToHex()); - + + Assert.Equal( + "5A876CC4B4AB2F6717951AEE7F97AB69844DBFFFF7074E6E6F71D2BA04BD6EC9", + Convert.ToHexString(masterKeyFromSeed.ChainCode) + ); + Assert.Equal( + "8D18D3F0CF9D74B53A935D97E8DE85955ED9F6EEFC6D6D45F0C169031A11B669", + Convert.ToHexString(masterKeyFromSeed.PrivateKey) + ); + + Assert.Equal( + "026cf0d14fcfa930347e7da26281319ac5959d02f1b6331812261efdb7e347788b", + ECPrivKey.Create(masterKeyFromSeed.PrivateKey).CreatePubKey().ToHex() + ); + var der1 = BIP32.Instance.DerivePath(Case1SecP256K1.Path, TestCase.Seed.Span); Assert.True(der1.PrivateKey.SequenceEqual(Case1SecP256K1.Key.Span)); Assert.True(der1.ChainCode.SequenceEqual(Case1SecP256K1.ChainCode.Span)); @@ -592,57 +889,119 @@ public void Nut13Tests() Assert.Equal(864559728, Nut13.GetKeysetIdInt(keysetId)); var path = "m/129372'/0'/864559728'/{counter}'"; - var mnemonicPhrase = "half depart obvious quality work element tank gorilla view sugar picture humble"; + var mnemonicPhrase = + "half depart obvious quality work element tank gorilla view sugar picture humble"; var mnemonic = new Mnemonic(mnemonicPhrase); - Assert.Equal("dd44ee516b0647e80b488e8dcc56d736a148f15276bef588b37057476d4b2b25780d3688a32b37353d6995997842c0fd8b412475c891c16310471fbc86dcbda8", - Convert.ToHexString(mnemonic.DeriveSeed()).ToLowerInvariant()); - - Assert.Equal("m/129372'/0'/864559728'/0'/0", Nut13.GetNut13DerivationPath(keysetId, 0, true)); - Assert.Equal("m/129372'/0'/864559728'/0'/1", Nut13.GetNut13DerivationPath(keysetId, 0, false)); - - Assert.Equal("485875df74771877439ac06339e284c3acfcd9be7abf3bc20b516faeadfe77ae", - mnemonic.DeriveSecret(keysetId, 0).Secret); - Assert.Equal("8f2b39e8e594a4056eb1e6dbb4b0c38ef13b1b2c751f64f810ec04ee35b77270", - mnemonic.DeriveSecret(keysetId, 1).Secret); - Assert.Equal("bc628c79accd2364fd31511216a0fab62afd4a18ff77a20deded7b858c9860c8", - mnemonic.DeriveSecret(keysetId, 2).Secret); - Assert.Equal("59284fd1650ea9fa17db2b3acf59ecd0f2d52ec3261dd4152785813ff27a33bf", - mnemonic.DeriveSecret(keysetId, 3).Secret); - Assert.Equal("576c23393a8b31cc8da6688d9c9a96394ec74b40fdaf1f693a6bb84284334ea0", - mnemonic.DeriveSecret(keysetId, 4).Secret); - - Assert.Equal("ad00d431add9c673e843d4c2bf9a778a5f402b985b8da2d5550bf39cda41d679", - Convert.ToHexString(mnemonic.DeriveBlindingFactor(keysetId, 0)).ToLowerInvariant()); - Assert.Equal("967d5232515e10b81ff226ecf5a9e2e2aff92d66ebc3edf0987eb56357fd6248", - Convert.ToHexString(mnemonic.DeriveBlindingFactor(keysetId, 1)).ToLowerInvariant()); - Assert.Equal("b20f47bb6ae083659f3aa986bfa0435c55c6d93f687d51a01f26862d9b9a4899", - Convert.ToHexString(mnemonic.DeriveBlindingFactor(keysetId, 2)).ToLowerInvariant()); - Assert.Equal("fb5fca398eb0b1deb955a2988b5ac77d32956155f1c002a373535211a2dfdc29", - Convert.ToHexString(mnemonic.DeriveBlindingFactor(keysetId, 3)).ToLowerInvariant()); - Assert.Equal("5f09bfbfe27c439a597719321e061e2e40aad4a36768bb2bcc3de547c9644bf9", - Convert.ToHexString(mnemonic.DeriveBlindingFactor(keysetId, 4)).ToLowerInvariant()); + Assert.Equal( + "dd44ee516b0647e80b488e8dcc56d736a148f15276bef588b37057476d4b2b25780d3688a32b37353d6995997842c0fd8b412475c891c16310471fbc86dcbda8", + Convert.ToHexString(mnemonic.DeriveSeed()).ToLowerInvariant() + ); + + Assert.Equal( + "m/129372'/0'/864559728'/0'/0", + Nut13.GetNut13DerivationPath(keysetId, 0, true) + ); + Assert.Equal( + "m/129372'/0'/864559728'/0'/1", + Nut13.GetNut13DerivationPath(keysetId, 0, false) + ); + + Assert.Equal( + "485875df74771877439ac06339e284c3acfcd9be7abf3bc20b516faeadfe77ae", + mnemonic.DeriveSecret(keysetId, 0).Secret + ); + Assert.Equal( + "8f2b39e8e594a4056eb1e6dbb4b0c38ef13b1b2c751f64f810ec04ee35b77270", + mnemonic.DeriveSecret(keysetId, 1).Secret + ); + Assert.Equal( + "bc628c79accd2364fd31511216a0fab62afd4a18ff77a20deded7b858c9860c8", + mnemonic.DeriveSecret(keysetId, 2).Secret + ); + Assert.Equal( + "59284fd1650ea9fa17db2b3acf59ecd0f2d52ec3261dd4152785813ff27a33bf", + mnemonic.DeriveSecret(keysetId, 3).Secret + ); + Assert.Equal( + "576c23393a8b31cc8da6688d9c9a96394ec74b40fdaf1f693a6bb84284334ea0", + mnemonic.DeriveSecret(keysetId, 4).Secret + ); + + Assert.Equal( + "ad00d431add9c673e843d4c2bf9a778a5f402b985b8da2d5550bf39cda41d679", + Convert.ToHexString(mnemonic.DeriveBlindingFactor(keysetId, 0)).ToLowerInvariant() + ); + Assert.Equal( + "967d5232515e10b81ff226ecf5a9e2e2aff92d66ebc3edf0987eb56357fd6248", + Convert.ToHexString(mnemonic.DeriveBlindingFactor(keysetId, 1)).ToLowerInvariant() + ); + Assert.Equal( + "b20f47bb6ae083659f3aa986bfa0435c55c6d93f687d51a01f26862d9b9a4899", + Convert.ToHexString(mnemonic.DeriveBlindingFactor(keysetId, 2)).ToLowerInvariant() + ); + Assert.Equal( + "fb5fca398eb0b1deb955a2988b5ac77d32956155f1c002a373535211a2dfdc29", + Convert.ToHexString(mnemonic.DeriveBlindingFactor(keysetId, 3)).ToLowerInvariant() + ); + Assert.Equal( + "5f09bfbfe27c439a597719321e061e2e40aad4a36768bb2bcc3de547c9644bf9", + Convert.ToHexString(mnemonic.DeriveBlindingFactor(keysetId, 4)).ToLowerInvariant() + ); } [Fact] public void Nut13HMACTests() { - KeysetId keysetId = new KeysetId("015ba18a8adcd02e715a58358eb618da4a4b3791151a4bee5e968bb88406ccf76a"); - Mnemonic mnemonic = - new Mnemonic("half depart obvious quality work element tank gorilla view sugar picture humble"); - - Assert.Equal("db5561a07a6e6490f8dadeef5be4e92f7cebaecf2f245356b5b2a4ec40687298", mnemonic.DeriveSecret(keysetId, 0).Secret); - Assert.Equal("b70e7b10683da3bf1cdf0411206f8180c463faa16014663f39f2529b2fda922e", mnemonic.DeriveSecret(keysetId, 1).Secret); - Assert.Equal("78a7ac32ccecc6b83311c6081b89d84bb4128f5a0d0c5e1af081f301c7a513f5", mnemonic.DeriveSecret(keysetId, 2).Secret); - Assert.Equal("094a2b6c63bfa7970bc09cda0e1cfc9cd3d7c619b8e98fabcfc60aea9e4963e5", mnemonic.DeriveSecret(keysetId, 3).Secret); - Assert.Equal("5e89fc5d30d0bf307ddf0a3ac34aa7a8ee3702169dafa3d3fe1d0cae70ecd5ef", mnemonic.DeriveSecret(keysetId, 4).Secret); - - - Assert.Equal("6d26181a3695e32e9f88b80f039ba1ae2ab5a200ad4ce9dbc72c6d3769f2b035", Convert.ToHexString(mnemonic.DeriveBlindingFactor(keysetId, 0)).ToLowerInvariant()); - Assert.Equal("bde4354cee75545bea1a2eee035a34f2d524cee2bb01613823636e998386952e", Convert.ToHexString(mnemonic.DeriveBlindingFactor(keysetId, 1)).ToLowerInvariant()); - Assert.Equal("f40cc1218f085b395c8e1e5aaa25dccc851be3c6c7526a0f4e57108f12d6dac4", Convert.ToHexString(mnemonic.DeriveBlindingFactor(keysetId, 2)).ToLowerInvariant()); - Assert.Equal("099ed70fc2f7ac769bc20b2a75cb662e80779827b7cc358981318643030577d0", Convert.ToHexString(mnemonic.DeriveBlindingFactor(keysetId, 3)).ToLowerInvariant()); - Assert.Equal("5550337312d223ba62e3f75cfe2ab70477b046d98e3e71804eade3956c7b98cf", Convert.ToHexString(mnemonic.DeriveBlindingFactor(keysetId, 4)).ToLowerInvariant()); + KeysetId keysetId = new KeysetId( + "015ba18a8adcd02e715a58358eb618da4a4b3791151a4bee5e968bb88406ccf76a" + ); + Mnemonic mnemonic = new Mnemonic( + "half depart obvious quality work element tank gorilla view sugar picture humble" + ); + + Assert.Equal( + "db5561a07a6e6490f8dadeef5be4e92f7cebaecf2f245356b5b2a4ec40687298", + mnemonic.DeriveSecret(keysetId, 0).Secret + ); + Assert.Equal( + "b70e7b10683da3bf1cdf0411206f8180c463faa16014663f39f2529b2fda922e", + mnemonic.DeriveSecret(keysetId, 1).Secret + ); + Assert.Equal( + "78a7ac32ccecc6b83311c6081b89d84bb4128f5a0d0c5e1af081f301c7a513f5", + mnemonic.DeriveSecret(keysetId, 2).Secret + ); + Assert.Equal( + "094a2b6c63bfa7970bc09cda0e1cfc9cd3d7c619b8e98fabcfc60aea9e4963e5", + mnemonic.DeriveSecret(keysetId, 3).Secret + ); + Assert.Equal( + "5e89fc5d30d0bf307ddf0a3ac34aa7a8ee3702169dafa3d3fe1d0cae70ecd5ef", + mnemonic.DeriveSecret(keysetId, 4).Secret + ); + + Assert.Equal( + "6d26181a3695e32e9f88b80f039ba1ae2ab5a200ad4ce9dbc72c6d3769f2b035", + Convert.ToHexString(mnemonic.DeriveBlindingFactor(keysetId, 0)).ToLowerInvariant() + ); + Assert.Equal( + "bde4354cee75545bea1a2eee035a34f2d524cee2bb01613823636e998386952e", + Convert.ToHexString(mnemonic.DeriveBlindingFactor(keysetId, 1)).ToLowerInvariant() + ); + Assert.Equal( + "f40cc1218f085b395c8e1e5aaa25dccc851be3c6c7526a0f4e57108f12d6dac4", + Convert.ToHexString(mnemonic.DeriveBlindingFactor(keysetId, 2)).ToLowerInvariant() + ); + Assert.Equal( + "099ed70fc2f7ac769bc20b2a75cb662e80779827b7cc358981318643030577d0", + Convert.ToHexString(mnemonic.DeriveBlindingFactor(keysetId, 3)).ToLowerInvariant() + ); + Assert.Equal( + "5550337312d223ba62e3f75cfe2ab70477b046d98e3e71804eade3956c7b98cf", + Convert.ToHexString(mnemonic.DeriveBlindingFactor(keysetId, 4)).ToLowerInvariant() + ); } + [Fact] public void NullExpiryTests_PostMintQuoteBolt11Response() { @@ -659,7 +1018,7 @@ public void NullExpiryTests_PostMintQuoteBolt11Response() """; var response = JsonSerializer.Deserialize(jsonWithNullExpiry); - + Assert.NotNull(response); Assert.Equal("test-quote-id", response.Quote); Assert.Equal("test-request", response.Request); @@ -680,7 +1039,7 @@ public void NullExpiryTests_PostMintQuoteBolt11Response() """; var response2 = JsonSerializer.Deserialize(jsonWithoutExpiry); - + Assert.NotNull(response2); Assert.Equal("test-quote-id-2", response2.Quote); Assert.Equal("test-request-2", response2.Request); @@ -702,7 +1061,7 @@ public void NullExpiryTests_PostMintQuoteBolt11Response() """; var response3 = JsonSerializer.Deserialize(jsonWithExpiry); - + Assert.NotNull(response3); Assert.Equal("test-quote-id-3", response3.Quote); Assert.Equal("test-request-3", response3.Request); @@ -728,7 +1087,7 @@ public void NullExpiryTests_PostMeltQuoteBolt11Response() """; var response = JsonSerializer.Deserialize(jsonWithNullExpiry); - + Assert.NotNull(response); Assert.Equal("melt-quote-id", response.Quote); Assert.Equal((ulong)1000, response.Amount); @@ -748,7 +1107,7 @@ public void NullExpiryTests_PostMeltQuoteBolt11Response() """; var response2 = JsonSerializer.Deserialize(jsonWithoutExpiry); - + Assert.NotNull(response2); Assert.Equal("melt-quote-id-2", response2.Quote); Assert.Equal((ulong)500, response2.Amount); @@ -769,7 +1128,7 @@ public void NullExpiryTests_PostMeltQuoteBolt11Response() """; var response3 = JsonSerializer.Deserialize(jsonWithExpiry); - + Assert.NotNull(response3); Assert.Equal("melt-quote-id-3", response3.Quote); Assert.Equal((ulong)2000, response3.Amount); @@ -778,7 +1137,6 @@ public void NullExpiryTests_PostMeltQuoteBolt11Response() Assert.Equal(1640995200, response3.Expiry); Assert.Null(response3.PaymentPreimage); } - private static readonly byte[] P2BK_PREFIX = "Cashu_P2BK_v1"u8.ToArray(); [Fact] public void Nut28_P2BK_Tests() @@ -786,15 +1144,16 @@ public void Nut28_P2BK_Tests() // sender ephemeral keypair var e = new PrivKey("1cedb9df0c6872188b560ace9e35fd55c2532d53e19ae65b46159073886482ca"); var E = new PubKey("02a8cda4cf448bfce9a9e46e588c06ea1780fcb94e3bbdf3277f42995d403a8b0c"); - Assert.Equal(E.Key.ToString()?.ToLowerInvariant(), e.Key.CreatePubKey().ToString()?.ToLowerInvariant()); + Assert.Equal( + E.Key.ToString()?.ToLowerInvariant(), + e.Key.CreatePubKey().ToString()?.ToLowerInvariant() + ); // receiver keypair var p = new PrivKey("ad37e8abd800be3e8272b14045873f4353327eedeb702b72ddcc5c5adff5129c"); var P = new PubKey("02771fed6cb88aaac38b8b32104a942bf4b8f4696bc361171b3c7d06fa2ebddf06"); Assert.Equal(P.Key.ToString()?.ToLowerInvariant(), p.Key.CreatePubKey().ToString()?.ToLowerInvariant()); - - // var kid = new KeysetId("009a1f293253e41e"); var zx = "40d6ba4430a6dfa915bb441579b0f4dee032307434e9957a092bbca73151df8b"; Assert.Equal(zx, Convert.ToHexString(Cashu.ComputeZx(e, P)).ToLowerInvariant()); @@ -821,7 +1180,6 @@ public void Nut28_P2BK_Tests() Assert.Equal(rs[i], ri.ToString()); } - string[] blindedPublicKeys = [ "03b7c03eb05a0a539cfc438e81bcf38b65b7bb8685e8790f9b853bfe3d77ad5315", @@ -839,7 +1197,10 @@ public void Nut28_P2BK_Tests() //it's the same blinding as with computeB_ for (int i = 0; i <= 10; i++) { - Assert.Equal(blindedPublicKeys[i], ((PubKey)Cashu.ComputeB_(P, new PrivKey(rs[i]))).ToString()); + Assert.Equal( + blindedPublicKeys[i], + ((PubKey)Cashu.ComputeB_(P, new PrivKey(rs[i]))).ToString() + ); } string[] skStd = @@ -887,7 +1248,6 @@ public void Nut28_P2BK_Tests() Assert.Equal(skNeg[i], Convert.ToHexString(derivedKeyNeg.ToBytes()).ToLowerInvariant()); } - } [Fact] @@ -896,26 +1256,36 @@ public void Nut28_P2BK_Flow() // sender generates ephermal keypair var e = new PrivKey("1cedb9df0c6872188b560ace9e35fd55c2532d53e19ae65b46159073886482ca"); var E = new PubKey("02a8cda4cf448bfce9a9e46e588c06ea1780fcb94e3bbdf3277f42995d403a8b0c"); - - + // receiver privkeys, with corresponding pubkeys that will get blinded - var signing_key = - ECPrivKey.Create(Convert.FromHexString("0000000000000000000000000000000000000000000000000000000000000001")); - var signing_key_two = - ECPrivKey.Create(Convert.FromHexString("7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f")); - - var refundPubkey = - ECPrivKey.Create(Convert.FromHexString("99590802251e78ee1051648439eedb003dc539093a48a44e7b8f2642c909ea37")).CreatePubKey(); + var signing_key = ECPrivKey.Create( + Convert.FromHexString( + "0000000000000000000000000000000000000000000000000000000000000001" + ) + ); + var signing_key_two = ECPrivKey.Create( + Convert.FromHexString( + "7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f" + ) + ); + + var refundPubkey = ECPrivKey + .Create( + Convert.FromHexString( + "99590802251e78ee1051648439eedb003dc539093a48a44e7b8f2642c909ea37" + ) + ) + .CreatePubKey(); var keysetId = new KeysetId("009a1f293253e41e"); - - var conditions = new P2PKBuilder() + + var conditions = new P2PkBuilder() { Lock = DateTimeOffset.FromUnixTimeSeconds(21000000000), - Pubkeys = new[] {signing_key.CreatePubKey(), signing_key_two.CreatePubKey()}, - RefundPubkeys = new[] {refundPubkey}, + Pubkeys = new[] { signing_key.CreatePubKey(), signing_key_two.CreatePubKey() }, + RefundPubkeys = new[] { refundPubkey }, SignatureThreshold = 2, - SigFlag = "SIG_INPUTS" + SigFlag = "SIG_INPUTS", }; var p2pkProofSecret = conditions.BuildBlinded(e); @@ -927,33 +1297,48 @@ public void Nut28_P2BK_Flow() Amount = 0, Secret = secret, C = "02698c4e2b5f9534cd0687d87513c759790cf829aa5739184a3e3735471fbda904".ToPubKey(), - P2PkE = E + P2PkE = E, }; - var witness = p2pkProofSecret.GenerateBlindWitness(proof, new[] {signing_key, signing_key_two}, keysetId, E); - + var witness = p2pkProofSecret.GenerateBlindWitness( + proof, + new[] { signing_key, signing_key_two }, + E + ); + Assert.True(p2pkProofSecret.VerifyWitness(secret, witness)); } [Fact] public void Nut28_Flow_WithRandomE() { - var signing_key = - ECPrivKey.Create(Convert.FromHexString("0000000000000000000000000000000000000000000000000000000000000001")); - var signing_key_two = - ECPrivKey.Create(Convert.FromHexString("7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f")); - - var refundPubkey = - ECPrivKey.Create(Convert.FromHexString("99590802251e78ee1051648439eedb003dc539093a48a44e7b8f2642c909ea37")).CreatePubKey(); + var signing_key = ECPrivKey.Create( + Convert.FromHexString( + "0000000000000000000000000000000000000000000000000000000000000001" + ) + ); + var signing_key_two = ECPrivKey.Create( + Convert.FromHexString( + "7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f" + ) + ); + + var refundPubkey = ECPrivKey + .Create( + Convert.FromHexString( + "99590802251e78ee1051648439eedb003dc539093a48a44e7b8f2642c909ea37" + ) + ) + .CreatePubKey(); var keysetId = new KeysetId("009a1f293253e41e"); - - var conditions = new P2PKBuilder() + + var conditions = new P2PkBuilder() { Lock = DateTimeOffset.FromUnixTimeSeconds(21000000000), - Pubkeys = new[] {signing_key.CreatePubKey(), signing_key_two.CreatePubKey()}, - RefundPubkeys = new[] {refundPubkey}, + Pubkeys = new[] { signing_key.CreatePubKey(), signing_key_two.CreatePubKey() }, + RefundPubkeys = new[] { refundPubkey }, SignatureThreshold = 2, - SigFlag = "SIG_INPUTS" + SigFlag = "SIG_INPUTS", }; var p2pkProofSecret = conditions.BuildBlinded(out var E); @@ -965,10 +1350,14 @@ public void Nut28_Flow_WithRandomE() Amount = 0, Secret = secret, C = "02698c4e2b5f9534cd0687d87513c759790cf829aa5739184a3e3735471fbda904".ToPubKey(), - P2PkE = E + P2PkE = E, }; - var witness = p2pkProofSecret.GenerateBlindWitness(proof, new[] {signing_key, signing_key_two}, keysetId, E); - + var witness = p2pkProofSecret.GenerateBlindWitness( + proof, + new[] { signing_key, signing_key_two }, + E + ); + Assert.True(p2pkProofSecret.VerifyWitness(secret, witness)); } diff --git a/DotNut.Tests/UnitTests2.cs b/DotNut.Tests/UnitTests2.cs new file mode 100644 index 0000000..e374aa8 --- /dev/null +++ b/DotNut.Tests/UnitTests2.cs @@ -0,0 +1,690 @@ +using System.Text.Json; +using DotNut.Abstractions; +using DotNut.ApiModels; +using DotNut.NBitcoin.BIP39; +using NBitcoin.Secp256k1; + +namespace DotNut.Tests; + +public class UnitTests2 +{ + private static string MintUrl = "http://localhost:3338"; + + [Fact] + public void CreatesWalletSuccesfully() + { + var wallet = Wallet.Create(); + Assert.NotNull(wallet); + } + + [Fact] + public async Task ThrowsWhenMintNotFound() + { + var wallet = Wallet.Create(); + await Assert.ThrowsAsync(async () => await wallet.GetInfo()); + await Assert.ThrowsAsync(async () => wallet.Restore()); + await Assert.ThrowsAsync(async () => wallet.Swap()); + await Assert.ThrowsAsync(async () => wallet.CreateMeltQuote()); + await Assert.ThrowsAsync(async () => wallet.CreateMintQuote()); + } + + [Fact] + public void BuilderChainingPreservesAllSettings() + { + var counter = new InMemoryCounter(); + var info = new MintInfo(new GetInfoResponse { Version = "0.15.0" }); + var keysets = new GetKeysetsResponse { Keysets = [] }; + var keys = new GetKeysResponse { Keysets = [] }; + var selector = new ProofSelector(new Dictionary()); + var mnemonic = + "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about"; + + var wallet = Wallet + .Create() + .WithMint(MintUrl) + .WithInfo(info) + .WithKeysets(keysets) + .WithKeys(keys) + .WithSelector(selector) + .WithMnemonic(mnemonic) + .WithCounter(counter) + .WithKeysetSync(true) + .ShouldBumpCounter(false); + + var mnemonicField = wallet + .GetType() + .GetField( + "_mnemonic", + System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance + ); + var mnemonicRef = (Mnemonic?)mnemonicField?.GetValue(wallet); + + var counterField = wallet + .GetType() + .GetField( + "_counter", + System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance + ); + var counterRef = (InMemoryCounter?)counterField?.GetValue(wallet); + + Assert.Equal(mnemonic, mnemonicRef.ToString()); + Assert.Same(counter, counterRef); + } + + [Fact] + public void WithMintStringVariantCreatesHttpClient() + { + var wallet = Wallet.Create().WithMint(MintUrl); + var api = wallet.GetMintApi().Result; + Assert.NotNull(api); + } + + [Fact] + public async Task InMemoryCounter() + { + var ctr = new InMemoryCounter(); + Assert.NotNull(ctr); + var testId1 = new KeysetId("00qwertyuiopasdf"); + var ctrNum = await ctr.GetCounterForId(testId1); + Assert.Equal((uint)0, ctrNum); + + await ctr.IncrementCounter(testId1); + Assert.Equal((uint)0, ctrNum); + ctrNum = await ctr.GetCounterForId(testId1); + Assert.Equal((uint)1, ctrNum); + + await ctr.IncrementCounter(testId1, 5); + ctrNum = await ctr.GetCounterForId(testId1); + Assert.Equal((uint)6, ctrNum); + + await ctr.SetCounter(testId1, 1337); + ctrNum = await ctr.GetCounterForId(testId1); + Assert.Equal((uint)1337, ctrNum); + } + + [Fact] + public void SplitAmountsForPayment_ExactAmount_ReturnsCorrectSplit() + { + var amounts = Utils.SplitToProofsAmounts(30, _testKeyset); + Assert.Equal(new List() { 16, 8, 4, 2 }, amounts); + } + + private Keyset? _testKeyset = JsonSerializer.Deserialize( + "{\n \"1\": \"03ba786a2c0745f8c30e490288acd7a72dd53d65afd292ddefa326a4a3fa14c566\",\n \"2\": \"03361cd8bd1329fea797a6add1cf1990ffcf2270ceb9fc81eeee0e8e9c1bd0cdf5\",\n \"4\": \"036e378bcf78738ddf68859293c69778035740e41138ab183c94f8fee7572214c7\",\n \"8\": \"03909d73beaf28edfb283dbeb8da321afd40651e8902fcf5454ecc7d69788626c0\",\n \"16\": \"028a36f0e6638ea7466665fe174d958212723019ec08f9ce6898d897f88e68aa5d\",\n \"32\": \"03a97a40e146adee2687ac60c2ba2586a90f970de92a9d0e6cae5a4b9965f54612\",\n \"64\": \"03ce86f0c197aab181ddba0cfc5c5576e11dfd5164d9f3d4a3fc3ffbbf2e069664\",\n \"128\": \"0284f2c06d938a6f78794814c687560a0aabab19fe5e6f30ede38e113b132a3cb9\",\n \"256\": \"03b99f475b68e5b4c0ba809cdecaae64eade2d9787aa123206f91cd61f76c01459\",\n \"512\": \"03d4db82ea19a44d35274de51f78af0a710925fe7d9e03620b84e3e9976e3ac2eb\",\n \"1024\": \"031fbd4ba801870871d46cf62228a1b748905ebc07d3b210daf48de229e683f2dc\",\n \"2048\": \"0276cedb9a3b160db6a158ad4e468d2437f021293204b3cd4bf6247970d8aff54b\",\n \"4096\": \"02fc6b89b403ee9eb8a7ed457cd3973638080d6e04ca8af7307c965c166b555ea2\",\n \"8192\": \"0320265583e916d3a305f0d2687fcf2cd4e3cd03a16ea8261fda309c3ec5721e21\",\n \"16384\": \"036e41de58fdff3cb1d8d713f48c63bc61fa3b3e1631495a444d178363c0d2ed50\",\n \"32768\": \"0365438f613f19696264300b069d1dad93f0c60a37536b72a8ab7c7366a5ee6c04\",\n \"65536\": \"02408426cfb6fc86341bac79624ba8708a4376b2d92debdf4134813f866eb57a8d\",\n \"131072\": \"031063e9f11c94dc778c473e968966eac0e70b7145213fbaff5f7a007e71c65f41\",\n \"262144\": \"02f2a3e808f9cd168ec71b7f328258d0c1dda250659c1aced14c7f5cf05aab4328\",\n \"524288\": \"038ac10de9f1ff9395903bb73077e94dbf91e9ef98fd77d9a2debc5f74c575bc86\",\n \"1048576\": \"0203eaee4db749b0fc7c49870d082024b2c31d889f9bc3b32473d4f1dfa3625788\",\n \"2097152\": \"033cdb9d36e1e82ae652b7b6a08e0204569ec7ff9ebf85d80a02786dc7fe00b04c\",\n \"4194304\": \"02c8b73f4e3a470ae05e5f2fe39984d41e9f6ae7be9f3b09c9ac31292e403ac512\",\n \"8388608\": \"025bbe0cfce8a1f4fbd7f3a0d4a09cb6badd73ef61829dc827aa8a98c270bc25b0\",\n \"16777216\": \"037eec3d1651a30a90182d9287a5c51386fe35d4a96839cf7969c6e2a03db1fc21\",\n \"33554432\": \"03280576b81a04e6abd7197f305506476f5751356b7643988495ca5c3e14e5c262\",\n \"67108864\": \"03268bfb05be1dbb33ab6e7e00e438373ca2c9b9abc018fdb452d0e1a0935e10d3\",\n \"134217728\": \"02573b68784ceba9617bbcc7c9487836d296aa7c628c3199173a841e7a19798020\",\n \"268435456\": \"0234076b6e70f7fbf755d2227ecc8d8169d662518ee3a1401f729e2a12ccb2b276\",\n \"536870912\": \"03015bd88961e2a466a2163bd4248d1d2b42c7c58a157e594785e7eb34d880efc9\",\n \"1073741824\": \"02c9b076d08f9020ebee49ac8ba2610b404d4e553a4f800150ceb539e9421aaeee\",\n \"2147483648\": \"034d592f4c366afddc919a509600af81b489a03caf4f7517c2b3f4f2b558f9a41a\",\n \"4294967296\": \"037c09ecb66da082981e4cbdb1ac65c0eb631fc75d85bed13efb2c6364148879b5\",\n \"8589934592\": \"02b4ebb0dda3b9ad83b39e2e31024b777cc0ac205a96b9a6cfab3edea2912ed1b3\",\n \"17179869184\": \"026cc4dacdced45e63f6e4f62edbc5779ccd802e7fabb82d5123db879b636176e9\",\n \"34359738368\": \"02b2cee01b7d8e90180254459b8f09bbea9aad34c3a2fd98c85517ecfc9805af75\",\n \"68719476736\": \"037a0c0d564540fc574b8bfa0253cca987b75466e44b295ed59f6f8bd41aace754\",\n \"137438953472\": \"021df6585cae9b9ca431318a713fd73dbb76b3ef5667957e8633bca8aaa7214fb6\",\n \"274877906944\": \"02b8f53dde126f8c85fa5bb6061c0be5aca90984ce9b902966941caf963648d53a\",\n \"549755813888\": \"029cc8af2840d59f1d8761779b2496623c82c64be8e15f9ab577c657c6dd453785\",\n \"1099511627776\": \"03e446fdb84fad492ff3a25fc1046fb9a93a5b262ebcd0151caa442ea28959a38a\",\n \"2199023255552\": \"02d6b25bd4ab599dd0818c55f75702fde603c93f259222001246569018842d3258\",\n \"4398046511104\": \"03397b522bb4e156ec3952d3f048e5a986c20a00718e5e52cd5718466bf494156a\",\n \"8796093022208\": \"02d1fb9e78262b5d7d74028073075b80bb5ab281edcfc3191061962c1346340f1e\",\n \"17592186044416\": \"030d3f2ad7a4ca115712ff7f140434f802b19a4c9b2dd1c76f3e8e80c05c6a9310\",\n \"35184372088832\": \"03e325b691f292e1dfb151c3fb7cad440b225795583c32e24e10635a80e4221c06\",\n \"70368744177664\": \"03bee8f64d88de3dee21d61f89efa32933da51152ddbd67466bef815e9f93f8fd1\",\n \"140737488355328\": \"0327244c9019a4892e1f04ba3bf95fe43b327479e2d57c25979446cc508cd379ed\",\n \"281474976710656\": \"02fb58522cd662f2f8b042f8161caae6e45de98283f74d4e99f19b0ea85e08a56d\",\n \"562949953421312\": \"02adde4b466a9d7e59386b6a701a39717c53f30c4810613c1b55e6b6da43b7bc9a\",\n \"1125899906842624\": \"038eeda11f78ce05c774f30e393cda075192b890d68590813ff46362548528dca9\",\n \"2251799813685248\": \"02ec13e0058b196db80f7079d329333b330dc30c000dbdd7397cbbc5a37a664c4f\",\n \"4503599627370496\": \"02d2d162db63675bd04f7d56df04508840f41e2ad87312a3c93041b494efe80a73\",\n \"9007199254740992\": \"0356969d6aef2bb40121dbd07c68b6102339f4ea8e674a9008bb69506795998f49\",\n \"18014398509481984\": \"02f4e667567ebb9f4e6e180a4113bb071c48855f657766bb5e9c776a880335d1d6\",\n \"36028797018963968\": \"0385b4fe35e41703d7a657d957c67bb536629de57b7e6ee6fe2130728ef0fc90b0\",\n \"72057594037927936\": \"02b2bc1968a6fddbcc78fb9903940524824b5f5bed329c6ad48a19b56068c144fd\",\n \"144115188075855872\": \"02e0dbb24f1d288a693e8a49bc14264d1276be16972131520cf9e055ae92fba19a\",\n \"288230376151711744\": \"03efe75c106f931a525dc2d653ebedddc413a2c7d8cb9da410893ae7d2fa7d19cc\",\n \"576460752303423488\": \"02c7ec2bd9508a7fc03f73c7565dc600b30fd86f3d305f8f139c45c404a52d958a\",\n \"1152921504606846976\": \"035a6679c6b25e68ff4e29d1c7ef87f21e0a8fc574f6a08c1aa45ff352c1d59f06\",\n \"2305843009213693952\": \"033cdc225962c052d485f7cfbf55a5b2367d200fe1fe4373a347deb4cc99e9a099\",\n \"4611686018427387904\": \"024a4b806cf413d14b294719090a9da36ba75209c7657135ad09bc65328fba9e6f\",\n \"9223372036854775808\": \"0377a6fe114e291a8d8e991627c38001c8305b23b9e98b1c7b1893f5cd0dda6cad\"\n}" + ); + + private static KeysetId _testKeysetId = new KeysetId("000f01df73ea149a"); + + [Fact] + public void SumProofs_EmptyList_ReturnsZero() + { + var proofs = new List(); + var sum = Utils.SumProofs(proofs); + Assert.Equal(0UL, sum); + } + + [Fact] + public void SumProofs_SingleProof_ReturnsAmount() + { + var proofs = new List { new Proof { Amount = 64 } }; + var sum = Utils.SumProofs(proofs); + Assert.Equal(64UL, sum); + } + + [Fact] + public void SumProofs_MultipleProofs_ReturnsCorrectSum() + { + var proofs = new List + { + new Proof { Amount = 1 }, + new Proof { Amount = 2 }, + new Proof { Amount = 4 }, + new Proof { Amount = 8 }, + new Proof { Amount = 16 }, + }; + var sum = Utils.SumProofs(proofs); + Assert.Equal(31UL, sum); + } + + [Theory] + [InlineData(1UL, new ulong[] { 1 })] + [InlineData(2UL, new ulong[] { 2 })] + [InlineData(3UL, new ulong[] { 2, 1 })] + [InlineData(7UL, new ulong[] { 4, 2, 1 })] + [InlineData(15UL, new ulong[] { 8, 4, 2, 1 })] + [InlineData(63UL, new ulong[] { 32, 16, 8, 4, 2, 1 })] + [InlineData(64UL, new ulong[] { 64 })] + [InlineData(100UL, new ulong[] { 64, 32, 4 })] + [InlineData(1337UL, new ulong[] { 1024, 256, 32, 16, 8, 1 })] + public void SplitToProofsAmounts_VariousAmounts_ReturnsCorrectSplit( + ulong amount, + ulong[] expected + ) + { + var result = Utils.SplitToProofsAmounts(amount, _testKeyset!); + Assert.Equal(expected.ToList(), result); + } + + [Fact] + public void SplitToProofsAmounts_ZeroAmount_ReturnsEmptyList() + { + var result = Utils.SplitToProofsAmounts(0, _testKeyset!); + Assert.Empty(result); + } + + [Theory] + [InlineData(0UL, 0)] + [InlineData(1UL, 1)] + [InlineData(2UL, 1)] + [InlineData(3UL, 2)] + [InlineData(4UL, 2)] + [InlineData(7UL, 3)] + [InlineData(8UL, 3)] + [InlineData(15UL, 4)] + [InlineData(16UL, 4)] + [InlineData(100UL, 7)] + [InlineData(1000UL, 10)] + public void CalculateNumberOfBlankOutputs_VariousAmounts(ulong amount, int expected) + { + var result = Utils.CalculateNumberOfBlankOutputs(amount); + Assert.Equal(expected, result); + } + + [Fact] + public void CreateOutputs_ValidAmounts_ReturnsCorrectOutputData() + { + var amounts = new List { 1, 2, 4 }; + var outputs = Utils.CreateOutputs(amounts, _testKeysetId, _testKeyset!); + + Assert.Equal(3, outputs.Count); + + Assert.Equal(1UL, outputs[0].BlindedMessage.Amount); + Assert.Equal(2UL, outputs[1].BlindedMessage.Amount); + Assert.Equal(4UL, outputs[2].BlindedMessage.Amount); + + Assert.All(outputs, o => Assert.Equal(_testKeysetId, o.BlindedMessage.Id)); + } + + [Fact] + public void CreateOutputs_InvalidAmount_ThrowsException() + { + var amounts = new List { 1, 3 }; // 3 is not a valid amount + Assert.Throws(() => + Utils.CreateOutputs(amounts, _testKeysetId, _testKeyset!) + ); + } + + [Fact] + public void CreateOutputs_DeterministicWithMnemonic() + { + var mnemonic = new Mnemonic( + "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about" + ); + var amounts = new List { 1, 2, 4 }; + + var outputs1 = Utils.CreateOutputs(amounts, _testKeysetId, _testKeyset!, mnemonic, 0); + var outputs2 = Utils.CreateOutputs(amounts, _testKeysetId, _testKeyset!, mnemonic, 0); + + // Same mnemonic and counter should produce same outputs + for (int i = 0; i < outputs1.Count; i++) + { + Assert.Equal( + ((StringSecret)outputs1[i].Secret).Secret, + ((StringSecret)outputs2[i].Secret).Secret + ); + } + } + + [Fact] + public void CreateOutputs_RandomWithoutMnemonic() + { + var amounts = new List { 1 }; + + var outputs1 = Utils.CreateOutputs(amounts, _testKeysetId, _testKeyset!); + var outputs2 = Utils.CreateOutputs(amounts, _testKeysetId, _testKeyset!); + + // without mnemonic, outputs should be random (different) + Assert.NotEqual( + ((StringSecret)outputs1[0].Secret).Secret, + ((StringSecret)outputs2[0].Secret).Secret + ); + } + + private static PubKey CreateTestPubKey(int seed) + { + var seedBytes = new byte[32]; + BitConverter.GetBytes(seed).CopyTo(seedBytes, 0); + seedBytes[31] = 1; + var privKey = ECPrivKey.Create(seedBytes); + ECPubKey ecPubKey = privKey.CreatePubKey(); + return ecPubKey; + } + + [Fact] + public async Task ProofSelector_ExactMatch_SelectsCorrectProofs() + { + var keysetId = _testKeysetId; + var fees = new Dictionary { { keysetId, 0 } }; + var selector = new ProofSelector(fees); + + var proofs = new List + { + new Proof + { + Amount = 1, + Id = keysetId, + C = CreateTestPubKey(1), + }, + new Proof + { + Amount = 2, + Id = keysetId, + C = CreateTestPubKey(2), + }, + new Proof + { + Amount = 4, + Id = keysetId, + C = CreateTestPubKey(3), + }, + new Proof + { + Amount = 8, + Id = keysetId, + C = CreateTestPubKey(4), + }, + }; + + var result = await selector.SelectProofsToSend(proofs, 7, false); + + Assert.Equal(7UL, Utils.SumProofs(result.Send)); + Assert.Equal(8UL, Utils.SumProofs(result.Keep)); + } + + [Fact] + public async Task ProofSelector_InsufficientFunds_ReturnsEmptySend() + { + var keysetId = _testKeysetId; + var fees = new Dictionary { { keysetId, 0 } }; + var selector = new ProofSelector(fees); + + var proofs = new List + { + new Proof + { + Amount = 1, + Id = keysetId, + C = CreateTestPubKey(1), + }, + new Proof + { + Amount = 2, + Id = keysetId, + C = CreateTestPubKey(2), + }, + }; + + var result = await selector.SelectProofsToSend(proofs, 100, false); + + Assert.Empty(result.Send); + Assert.Equal(2, result.Keep.Count); + } + + [Fact] + public async Task ProofSelector_ZeroAmount_ReturnsEmptySend() + { + var keysetId = _testKeysetId; + var fees = new Dictionary { { keysetId, 0 } }; + var selector = new ProofSelector(fees); + + var proofs = new List + { + new Proof + { + Amount = 8, + Id = keysetId, + C = CreateTestPubKey(1), + }, + }; + + var result = await selector.SelectProofsToSend(proofs, 0, false); + + Assert.Empty(result.Send); + } + + [Fact] + public async Task ProofSelector_WithFees_AccountsForFees() + { + var keysetId = _testKeysetId; + var fees = new Dictionary { { keysetId, 1000 } }; // 1 sat per proof + var selector = new ProofSelector(fees); + + var proofs = new List + { + new Proof + { + Amount = 1, + Id = keysetId, + C = CreateTestPubKey(1), + }, + new Proof + { + Amount = 2, + Id = keysetId, + C = CreateTestPubKey(2), + }, + new Proof + { + Amount = 4, + Id = keysetId, + C = CreateTestPubKey(3), + }, + new Proof + { + Amount = 8, + Id = keysetId, + C = CreateTestPubKey(4), + }, + new Proof + { + Amount = 16, + Id = keysetId, + C = CreateTestPubKey(5), + }, + }; + + var result = await selector.SelectProofsToSend(proofs, 10, true); + + Assert.True(Utils.SumProofs(result.Send) >= 10); + } + + [Fact] + public async Task ProofSelector_SingleLargeProof_SelectsIt() + { + var keysetId = _testKeysetId; + var fees = new Dictionary { { keysetId, 0 } }; + var selector = new ProofSelector(fees); + + var proofs = new List + { + new Proof + { + Amount = 100, + Id = keysetId, + C = CreateTestPubKey(1), + }, + }; + + var result = await selector.SelectProofsToSend(proofs, 50, false); + + Assert.Single(result.Send); + Assert.Equal(100UL, result.Send[0].Amount); + Assert.Empty(result.Keep); + } + + [Fact] + public void TokenEncode_V4_RoundTrip() + { + var keysetId = new KeysetId("00ffd48b8f5ecf80"); + var proofs = new List + { + new Proof + { + Amount = 1, + Id = keysetId, + Secret = new StringSecret( + "acc12435e7b8484c3cf1850149218af90f716a52bf4a5ed347e48ecc13f77388" + ), + C = "0244538319de485d55bed3b29a642bee5879375ab9e7a620e11e48ba482421f3cf".ToPubKey(), + }, + }; + + var token = new CashuToken + { + Unit = "sat", + Tokens = new List + { + new CashuToken.Token { Mint = "http://localhost:3338", Proofs = proofs }, + }, + }; + + var encoded = token.Encode("B", false); + Assert.StartsWith("cashuB", encoded); + + var decoded = CashuTokenHelper.Decode(encoded, out var version); + Assert.Equal("B", version); + Assert.Equal("sat", decoded.Unit); + Assert.Single(decoded.Tokens); + Assert.Equal("http://localhost:3338", decoded.Tokens[0].Mint); + Assert.Single(decoded.Tokens[0].Proofs); + Assert.Equal(1UL, decoded.Tokens[0].Proofs[0].Amount); + } + + [Fact] + public void TokenDecode_InvalidPrefix_ThrowsException() + { + var invalidToken = "invalidTokenString123"; + Assert.Throws(() => CashuTokenHelper.Decode(invalidToken, out _)); + } + + [Fact] + public void KeysetId_Equality() + { + var id1 = new KeysetId("009a1f293253e41e"); + var id2 = new KeysetId("009a1f293253e41e"); + var id3 = new KeysetId("000f01df73ea149a"); + + Assert.Equal(id1, id2); + Assert.NotEqual(id1, id3); + Assert.True(id1 == id2); + Assert.False(id1 == id3); + } + + [Fact] + public void KeysetId_GetVersion() + { + var v0Id = new KeysetId("009a1f293253e41e"); + var v1Id = new KeysetId( + "01adc013fa9d85171586660abab27579888611659d357bc86bc09cb26eee8bc035" + ); + + Assert.Equal(0x00, v0Id.GetVersion()); + Assert.Equal(0x01, v1Id.GetVersion()); + } + + [Fact] + public void ComputeFee_NoFees_ReturnsZero() + { + var keysetId = _testKeysetId; + var fees = new Dictionary { { keysetId, 0 } }; + + var proofs = new List + { + new Proof { Amount = 1, Id = keysetId }, + new Proof { Amount = 2, Id = keysetId }, + }; + + var fee = proofs.ComputeFee(fees); + Assert.Equal(0UL, fee); + } + + [Fact] + public void ComputeFee_WithFees_ReturnsCorrectFee() + { + var keysetId = _testKeysetId; + var fees = new Dictionary { { keysetId, 1000 } }; // 1 sat per proof (1000 ppk) + + var proofs = new List + { + new Proof { Amount = 1, Id = keysetId }, + new Proof { Amount = 2, Id = keysetId }, + new Proof { Amount = 4, Id = keysetId }, + }; + + var fee = proofs.ComputeFee(fees); + Assert.Equal(3UL, fee); // 3 proofs * 1 sat + } + + [Fact] + public void SendResponse_DefaultsToEmptyLists() + { + var response = new SendResponse(); + Assert.NotNull(response.Keep); + Assert.NotNull(response.Send); + Assert.Empty(response.Keep); + Assert.Empty(response.Send); + } + + [Fact] + public void Wallet_WithKeysetSyncThreshold_SetsCorrectly() + { + var wallet = Wallet + .Create() + .WithMint(MintUrl) + .WithKeysetSync(true, TimeSpan.FromMinutes(30)); + + Assert.NotNull(wallet); + } + + [Fact] + public void Wallet_ShouldBumpCounter_Default() + { + var counter = new InMemoryCounter(); + var wallet = Wallet.Create().WithMint(MintUrl).WithCounter(counter); + + Assert.NotNull(wallet); + } + + [Fact] + public void Wallet_ShouldBumpCounter_Disabled() + { + var counter = new InMemoryCounter(); + var wallet = Wallet + .Create() + .WithMint(MintUrl) + .WithCounter(counter) + .ShouldBumpCounter(false); + + Assert.NotNull(wallet); + } + + [Fact] + public void MintInfo_FromGetInfoResponse() + { + var response = new GetInfoResponse + { + Version = "0.15.0", + Name = "Test Mint", + Description = "A test mint", + }; + + var info = new MintInfo(response); + Assert.NotNull(info); + } + + [Fact] + public void P2PkBuilder_Build_CreatesValidSecret() + { + var privKey = new PrivKey( + "0000000000000000000000000000000000000000000000000000000000000001" + ); + var builder = new P2PkBuilder + { + Pubkeys = [privKey.Key.CreatePubKey()], + SignatureThreshold = 1, + SigFlag = "SIG_INPUTS", + }; + + var secret = builder.Build(); + Assert.NotNull(secret); + + var allowedPubkeys = secret.GetAllowedPubkeys(out var threshold); + Assert.Single(allowedPubkeys); + Assert.Equal(1, threshold); + } + + [Fact] + public void P2PkBuilder_WithMultisig_Build() + { + var privKey1 = new PrivKey( + "0000000000000000000000000000000000000000000000000000000000000001" + ); + var privKey2 = new PrivKey( + "0000000000000000000000000000000000000000000000000000000000000002" + ); + + var builder = new P2PkBuilder + { + Pubkeys = [privKey1.Key.CreatePubKey(), privKey2.Key.CreatePubKey()], + SignatureThreshold = 2, + SigFlag = "SIG_INPUTS", + }; + + var secret = builder.Build(); + var allowedPubkeys = secret.GetAllowedPubkeys(out var threshold); + + Assert.Equal(2, allowedPubkeys.Count()); + Assert.Equal(2, threshold); + } + + [Fact] + public void HTLCBuilder_Build_CreatesValidSecret() + { + var preimage = "0000000000000000000000000000000000000000000000000000000000000001"; + using var sha = System.Security.Cryptography.SHA256.Create(); + var hashLockBytes = sha.ComputeHash(Convert.FromHexString(preimage)); + var hashLock = Convert.ToHexString(hashLockBytes).ToLower(); + var privKey = new PrivKey( + "0000000000000000000000000000000000000000000000000000000000000001" + ); + + var builder = new HTLCBuilder + { + HashLock = hashLock, + Pubkeys = [privKey.Key.CreatePubKey()], + SignatureThreshold = 1, + }; + + var secret = builder.Build(); + Assert.NotNull(secret); + + var allowedPubkeys = secret.GetAllowedPubkeys(out var threshold); + Assert.Single(allowedPubkeys); + } + + [Fact] + public async Task Wallet_ThrowsOnMissingMint_ForAllOperations() + { + var wallet = Wallet.Create(); + + await Assert.ThrowsAsync(() => wallet.GetInfo()); + Assert.Throws(() => wallet.CreateMintQuote()); + Assert.Throws(() => wallet.CreateMeltQuote()); + Assert.Throws(() => wallet.Swap()); + Assert.Throws(() => wallet.Restore()); + } + + [Fact] + public async Task Counter_ReturnsZeroForUnknownKeysetId() + { + var counter = new InMemoryCounter(); + var unknownKeysetId = new KeysetId("00unknown1234567"); + + var value = await counter.GetCounterForId(unknownKeysetId); + Assert.Equal((uint)0, value); + } + + [Fact] + public async Task Counter_MultipleKeysets_IndependentCounters() + { + var counter = new InMemoryCounter(); + var keysetId1 = new KeysetId("00keyset11234567"); + var keysetId2 = new KeysetId("00keyset21234567"); + + await counter.IncrementCounter(keysetId1, 10); + await counter.IncrementCounter(keysetId2, 20); + + Assert.Equal((uint)10, await counter.GetCounterForId(keysetId1)); + Assert.Equal((uint)20, await counter.GetCounterForId(keysetId2)); + } +} diff --git a/DotNut.sln.DotSettings.user b/DotNut.sln.DotSettings.user index 47f8615..e4ea523 100644 --- a/DotNut.sln.DotSettings.user +++ b/DotNut.sln.DotSettings.user @@ -2,20 +2,50 @@ ForceIncluded ForceIncluded ForceIncluded + ForceIncluded + ForceIncluded + ForceIncluded ForceIncluded ForceIncluded ForceIncluded + ForceIncluded + ForceIncluded ForceIncluded + ForceIncluded ForceIncluded ForceIncluded ForceIncluded - C:\Users\evilk\AppData\Local\JetBrains\Rider2024.2\resharper-host\temp\Rider\vAny\CoverageData\_DotNut.1481064820\Snapshot\snapshot.utdcvr + <SessionState ContinuousTestingMode="0" IsActive="True" Name="UnitTest1" xmlns="urn:schemas-jetbrains-com:jetbrains-ut-session"> + <TestAncestor> + <TestId>xUnit::0AAAA85C-9FDC-4FD5-9CC2-ED8385B40106::net8.0::DotNut.Tests.UnitTest1</TestId> + </TestAncestor> +</SessionState> + - <SessionState ContinuousTestingMode="0" Name="Test1" xmlns="urn:schemas-jetbrains-com:jetbrains-ut-session"> - <TestAncestor> - <TestId>xUnit::0AAAA85C-9FDC-4FD5-9CC2-ED8385B40106::net8.0::DotNut.Tests.UnitTest1.Nut00Tests_TokenSerialization</TestId> - </TestAncestor> + <SessionState ContinuousTestingMode="0" Name="Test1" xmlns="urn:schemas-jetbrains-com:jetbrains-ut-session"> + <TestAncestor> + <TestId>xUnit::0AAAA85C-9FDC-4FD5-9CC2-ED8385B40106::net8.0::DotNut.Tests.UnitTest1</TestId> + <TestId>xUnit::0AAAA85C-9FDC-4FD5-9CC2-ED8385B40106::net8.0::DotNut.Tests.Integration</TestId> + </TestAncestor> +</SessionState> + <SessionState ContinuousTestingMode="0" Name="Nut11_Signatures" xmlns="urn:schemas-jetbrains-com:jetbrains-ut-session"> + <TestAncestor> + <TestId>xUnit::0AAAA85C-9FDC-4FD5-9CC2-ED8385B40106::net8.0::DotNut.Tests.UnitTest1</TestId> + <TestId>xUnit::0AAAA85C-9FDC-4FD5-9CC2-ED8385B40106::net8.0::DotNut.Tests.UnitTests2.BuilderChainingPreservesAllSettings</TestId> + <TestId>xUnit::0AAAA85C-9FDC-4FD5-9CC2-ED8385B40106::net8.0::DotNut.Tests.UnitTests2.WithMintStringVariantCreatesHttpClient</TestId> + <TestId>xUnit::0AAAA85C-9FDC-4FD5-9CC2-ED8385B40106::net8.0::DotNut.Tests.Integration</TestId> + <TestId>xUnit::0AAAA85C-9FDC-4FD5-9CC2-ED8385B40106::net8.0::DotNut.Tests.UnitTests2</TestId> + </TestAncestor> +</SessionState> + <SessionState ContinuousTestingMode="0" Name="Integration" xmlns="urn:schemas-jetbrains-com:jetbrains-ut-session"> + <TestAncestor> + <TestId>xUnit::0AAAA85C-9FDC-4FD5-9CC2-ED8385B40106::net8.0::DotNut.Tests.Integration</TestId> + <TestId>xUnit::0AAAA85C-9FDC-4FD5-9CC2-ED8385B40106::net8.0::DotNut.Tests.UnitTests2</TestId> + <TestId>xUnit::0AAAA85C-9FDC-4FD5-9CC2-ED8385B40106::net8.0::DotNut.Tests.UnitTest1</TestId> + </TestAncestor> </SessionState> - <SessionState ContinuousTestingMode="0" IsActive="True" Name="Nut11_Signatures" xmlns="urn:schemas-jetbrains-com:jetbrains-ut-session"> - <Project Location="C:\Git\DotNuts\DotNut.Tests" Presentation="&lt;DotNut.Tests&gt;" /> + <SessionState ContinuousTestingMode="0" Name="Nut11_SIG_ALL" xmlns="urn:schemas-jetbrains-com:jetbrains-ut-session"> + <TestAncestor> + <TestId>xUnit::0AAAA85C-9FDC-4FD5-9CC2-ED8385B40106::net8.0::DotNut.Tests.UnitTest1.Nut11_SIG_ALL</TestId> + </TestAncestor> </SessionState> \ No newline at end of file diff --git a/DotNut/Abstractions/Handlers/MeltHandlerBolt11.cs b/DotNut/Abstractions/Handlers/MeltHandlerBolt11.cs new file mode 100644 index 0000000..4570f4f --- /dev/null +++ b/DotNut/Abstractions/Handlers/MeltHandlerBolt11.cs @@ -0,0 +1,64 @@ +using DotNut.ApiModels; + +namespace DotNut.Abstractions.Handlers; + +public class MeltHandlerBolt11( + IWalletBuilder wallet, + PostMeltQuoteBolt11Response quote, + List blankOutputs, + List? privKeys = null, + string? htlcPreimage = null +) : IMeltHandler> +{ + public PostMeltQuoteBolt11Response GetQuote() => quote; + public List GetBlankOutputs() => blankOutputs; + + public async Task> Melt(IEnumerable inputs, CancellationToken ct = default) + { + //we're operating on copy here since later the proof state is mutated in stripFingerprints + var proofs = inputs.DeepCopyList(); + + Nut10Helper.MaybeProcessNut10( + privKeys ?? [], + proofs, + blankOutputs, + htlcPreimage, + quote.Quote + ); + //since nut10 (with p2bk) is aleady processed, now it's safe to strip P2PkE + proofs.ForEach(i => i.StripFingerprints()); + + var client = await wallet.GetMintApi(ct); + var req = new PostMeltRequest + { + Quote = quote.Quote, + Inputs = proofs.ToArray(), + Outputs = blankOutputs.Select(bo => bo.BlindedMessage).ToArray(), + }; + + var res = await client.Melt( + "bolt11", + req, + ct + ); + if (res.Change == null || res.Change.Length == 0) + { + return []; + } + + var keysetIds = res.Change.Select(sig => sig.Id).Distinct().ToList(); + var changeProofs = new List(); + foreach (var keysetId in keysetIds) + { + var keyset = await wallet.GetKeys(keysetId, true, false, ct); + if (keyset == null) + { + continue; + } + changeProofs.AddRange( + Utils.ConstructProofsFromPromises(res.Change.ToList(), blankOutputs, keyset.Keys) + ); + } + return changeProofs; + } +} diff --git a/DotNut/Abstractions/Handlers/MeltHandlerBolt12.cs b/DotNut/Abstractions/Handlers/MeltHandlerBolt12.cs new file mode 100644 index 0000000..c115830 --- /dev/null +++ b/DotNut/Abstractions/Handlers/MeltHandlerBolt12.cs @@ -0,0 +1,63 @@ +using DotNut.ApiModels; +using DotNut.ApiModels.Melt.bolt12; + +namespace DotNut.Abstractions.Handlers; + +public class MeltHandlerBolt12( + IWalletBuilder wallet, + PostMeltQuoteBolt12Response quote, + List blankOutputs, + List? privKeys = null, + string? htlcPreimage = null +) : IMeltHandler> +{ + public PostMeltQuoteBolt12Response GetQuote() => quote; + + public async Task> Melt(IEnumerable inputs, CancellationToken ct = default) + { + //we're operating on copy here since later the proof state is mutated in stripFingerprints + var proofs = inputs.DeepCopyList(); + + Nut10Helper.MaybeProcessNut10( + privKeys ?? [], + proofs, + blankOutputs, + htlcPreimage, + quote.Quote + ); + proofs.ForEach(i => i.StripFingerprints()); + + var client = await wallet.GetMintApi(ct); + var req = new PostMeltRequest + { + Quote = quote.Quote, + Inputs = proofs.ToArray(), + Outputs = blankOutputs.Select(bo => bo.BlindedMessage).ToArray(), + }; + + var res = await client.Melt( + "bolt12", + req, + ct + ); + if (res.Change == null || res.Change.Length == 0) + { + return []; + } + + var keysetIds = res.Change.Select(sig => sig.Id).Distinct().ToList(); + var changeProofs = new List(); + foreach (var keysetId in keysetIds) + { + var keyset = await wallet.GetKeys(keysetId, true, false, ct); + if (keyset == null) + { + continue; + } + changeProofs.AddRange( + Utils.ConstructProofsFromPromises(res.Change.ToList(), blankOutputs, keyset.Keys) + ); + } + return changeProofs; + } +} diff --git a/DotNut/Abstractions/Handlers/MintHandlerBolt11.cs b/DotNut/Abstractions/Handlers/MintHandlerBolt11.cs new file mode 100644 index 0000000..b8db212 --- /dev/null +++ b/DotNut/Abstractions/Handlers/MintHandlerBolt11.cs @@ -0,0 +1,63 @@ +using DotNut.ApiModels; + +namespace DotNut.Abstractions.Handlers; + +public class MintHandlerBolt11( + IWalletBuilder wallet, + PostMintQuoteBolt11Response postMintQuoteBolt11Response, + GetKeysResponse.KeysetItemResponse keyset, + List outputs +) : IMintHandler> +{ + private string? _signature; + + public IMintHandler> WithSignature(string signature) + { + _signature = signature; + return this; + } + + public IMintHandler> SignWithPrivkey(string privKeyHex) + { + return this.SignWithPrivkey(new PrivKey(privKeyHex)); + } + + public IMintHandler> SignWithPrivkey(PrivKey privkey) + { + this._signature = privkey.SignMintQuote( + postMintQuoteBolt11Response.Quote, + outputs.Select(o => o.BlindedMessage).ToList() + ); + return this; + } + + public PostMintQuoteBolt11Response GetQuote() => postMintQuoteBolt11Response; + + public List GetOutputs() => outputs; + + public async Task> Mint(CancellationToken ct = default) + { + if (postMintQuoteBolt11Response.PubKey is not null && this._signature is null) + { + throw new ArgumentNullException( + nameof(_signature), + $"Signature for mint quote {postMintQuoteBolt11Response.Quote} is required!" + ); + } + var client = await wallet.GetMintApi(ct); + + var req = new PostMintRequest + { + Outputs = outputs.Select(o => o.BlindedMessage).ToArray(), + Quote = postMintQuoteBolt11Response.Quote, + Signature = _signature, + }; + + var promises = await client.Mint("bolt11", req, ct); + return Utils.ConstructProofsFromPromises( + promises.Signatures.ToList(), + outputs, + keyset.Keys + ); + } +} diff --git a/DotNut/Abstractions/Handlers/MintHandlerBolt12.cs b/DotNut/Abstractions/Handlers/MintHandlerBolt12.cs new file mode 100644 index 0000000..1b66790 --- /dev/null +++ b/DotNut/Abstractions/Handlers/MintHandlerBolt12.cs @@ -0,0 +1,64 @@ +using DotNut.ApiModels; +using DotNut.ApiModels.Mint.bolt12; + +namespace DotNut.Abstractions.Handlers; + +public class MintHandlerBolt12( + IWalletBuilder wallet, + PostMintQuoteBolt12Response quote, + GetKeysResponse.KeysetItemResponse keyset, + List outputs +) : IMintHandler> +{ + private string? _signature; + + public IMintHandler> WithSignature(string signature) + { + _signature = signature; + return this; + } + + public IMintHandler> SignWithPrivkey(string privKeyHex) + { + return this.SignWithPrivkey(new PrivKey(privKeyHex)); + } + + public IMintHandler> SignWithPrivkey(PrivKey privkey) + { + this._signature = privkey.SignMintQuote( + quote.Quote, + outputs.Select(o => o.BlindedMessage).ToList() + ); + return this; + } + + public PostMintQuoteBolt12Response GetQuote() => quote; + + public List GetOutputs() => outputs; + + public async Task> Mint(CancellationToken ct = default) + { + if (this._signature is null) + { + throw new ArgumentNullException( + nameof(this._signature), + $"Signature for mint quote {quote.Quote} is required!" + ); + } + + var client = await wallet.GetMintApi(ct); + var req = new PostMintRequest + { + Outputs = outputs.Select(o => o.BlindedMessage).ToArray(), + Quote = quote.Quote, + Signature = _signature, + }; + + var promises = await client.Mint("bolt12", req, ct); + return Utils.ConstructProofsFromPromises( + promises.Signatures.ToList(), + outputs, + keyset.Keys + ); + } +} diff --git a/DotNut/Abstractions/InMemoryCounter.cs b/DotNut/Abstractions/InMemoryCounter.cs new file mode 100644 index 0000000..ed2a3c9 --- /dev/null +++ b/DotNut/Abstractions/InMemoryCounter.cs @@ -0,0 +1,64 @@ +using System.Collections.Concurrent; + +namespace DotNut.Abstractions; + +public class InMemoryCounter : ICounter +{ + private readonly ConcurrentDictionary _counter; + + public InMemoryCounter(IDictionary counter) + { + this._counter = new ConcurrentDictionary(counter); + } + + public InMemoryCounter() + { + this._counter = new ConcurrentDictionary(); + } + + public Task GetCounterForId(KeysetId keysetId, CancellationToken ct = default) + { + return Task.FromResult(_counter.GetOrAdd(keysetId, 0)); + } + + public Task IncrementCounter( + KeysetId keysetId, + uint bumpBy = 1, + CancellationToken ct = default + ) + { + var next = _counter.AddOrUpdate(keysetId, bumpBy, (_, current) => current + bumpBy); + return Task.FromResult(next); + } + + public Task<(uint oldValue, uint newValue)> FetchAndIncrement( + KeysetId keysetId, + uint bumpBy = 1, + CancellationToken ct = default + ) + { + uint oldValue = 0; + uint newValue = _counter.AddOrUpdate( + keysetId, + bumpBy, + (_, current) => + { + oldValue = current; + return current + bumpBy; + } + ); + + return Task.FromResult((oldValue, newValue)); + } + + public Task SetCounter(KeysetId keysetId, uint counter, CancellationToken ct = default) + { + _counter[keysetId] = counter; + return Task.CompletedTask; + } + + public async Task> Export() + { + return new Dictionary(_counter); + } +} diff --git a/DotNut/Abstractions/Interfaces/ICounter.cs b/DotNut/Abstractions/Interfaces/ICounter.cs new file mode 100644 index 0000000..fdf362e --- /dev/null +++ b/DotNut/Abstractions/Interfaces/ICounter.cs @@ -0,0 +1,25 @@ +namespace DotNut.Abstractions; + +public interface ICounter +{ + /// + /// Gets counter for current keysetID. This counter will be used for next proof generation, so make sure it's + /// always set to last used proof + 1 + /// + /// + /// + /// + public Task GetCounterForId(KeysetId keysetId, CancellationToken ct = default); + public Task IncrementCounter( + KeysetId keysetId, + uint bumpBy = 1, + CancellationToken ct = default + ); + public Task<(uint oldValue, uint newValue)> FetchAndIncrement( + KeysetId keysetId, + uint bumpBy = 1, + CancellationToken ct = default + ); + public Task SetCounter(KeysetId keysetId, uint counter, CancellationToken ct = default); + public Task> Export(); +} diff --git a/DotNut/Abstractions/Interfaces/IMeltHandler.cs b/DotNut/Abstractions/Interfaces/IMeltHandler.cs new file mode 100644 index 0000000..f65cfb1 --- /dev/null +++ b/DotNut/Abstractions/Interfaces/IMeltHandler.cs @@ -0,0 +1,9 @@ +namespace DotNut.Abstractions; + +public interface IMeltHandler; + +public interface IMeltHandler : IMeltHandler +{ + TQuote GetQuote(); + Task Melt(IEnumerable inputs, CancellationToken ct = default); +} diff --git a/DotNut/Abstractions/Interfaces/IMeltQuoteBuilder.cs b/DotNut/Abstractions/Interfaces/IMeltQuoteBuilder.cs new file mode 100644 index 0000000..d67ef53 --- /dev/null +++ b/DotNut/Abstractions/Interfaces/IMeltQuoteBuilder.cs @@ -0,0 +1,56 @@ +using DotNut.ApiModels; +using DotNut.ApiModels.Melt.bolt12; + +namespace DotNut.Abstractions; + +/// +/// Melt operation builder (pay invoices) +/// +public interface IMeltQuoteBuilder +{ + /// + /// Optional. Sets the base unit for the quote; defaults to "sat". + /// + IMeltQuoteBuilder WithUnit(string unit); + + /// + /// Mandatory. A bolt11 invoice is required to create a melt quote. + /// + IMeltQuoteBuilder WithInvoice(string bolt11Invoice); + + /// + /// Optional. Supply previously generated blank outputs instead of deriving them. + /// + IMeltQuoteBuilder WithBlankOutputs(IEnumerable blankOutputs); + + /// + /// Optional. Provide private keys for P2PK proofs associated with the inputs. + /// + IMeltQuoteBuilder WithPrivKeys(IEnumerable privKeys); + + /// + /// Optional and mandatory if amountless invoice provided. + /// + /// Melt quote amount in millisatoshis + /// + IMeltQuoteBuilder WithAmount(ulong msat); + + /// + /// Optional. Supply HTLC preimage to sign HTLC-based proofs. + /// + IMeltQuoteBuilder WithHTLCPreimage(string preimage); + + /// + /// Create a bolt11 melt handler. + /// + Task>> ProcessAsyncBolt11( + CancellationToken ct = default + ); + + /// + /// Create a bolt12 melt handler. + /// + Task>> ProcessAsyncBolt12( + CancellationToken ct = default + ); +} diff --git a/DotNut/Abstractions/Interfaces/IMintHandler.cs b/DotNut/Abstractions/Interfaces/IMintHandler.cs new file mode 100644 index 0000000..243ebaf --- /dev/null +++ b/DotNut/Abstractions/Interfaces/IMintHandler.cs @@ -0,0 +1,15 @@ +namespace DotNut.Abstractions; + +public interface IMintHandler; + +public interface IMintHandler : IMintHandler +{ + public IMintHandler WithSignature(string signature); + public IMintHandler SignWithPrivkey(PrivKey privkey); + public IMintHandler SignWithPrivkey(string privKeyHex); + + TQuote GetQuote(); + List GetOutputs(); + + Task Mint(CancellationToken ct = default); +} diff --git a/DotNut/Abstractions/Interfaces/IMintQuoteBuilder.cs b/DotNut/Abstractions/Interfaces/IMintQuoteBuilder.cs new file mode 100644 index 0000000..81a5341 --- /dev/null +++ b/DotNut/Abstractions/Interfaces/IMintQuoteBuilder.cs @@ -0,0 +1,73 @@ +using DotNut.ApiModels; +using DotNut.ApiModels.Mint.bolt12; + +namespace DotNut.Abstractions; + +/// +/// Mint operation builder (receive from invoice) +/// +public interface IMintQuoteBuilder +{ + /// + /// Optional. Sets unit of tokens being minted; defaults to satoshi. + /// + IMintQuoteBuilder WithUnit(string unit); + + /// + /// Mandatory. Amount of tokens to mint in the current unit. + /// + IMintQuoteBuilder WithAmount(ulong amount); + + /// + /// Optional for bolt11 and mandatory for bolt12. + /// + /// + /// + IMintQuoteBuilder WithPubkey(string pubkey); + + /// + /// Optional for bolt11 and mandatory for bolt12. + /// + IMintQuoteBuilder WithPubkey(PubKey pubkey); + + /// + /// Optional. Provide precomputed outputs so blinding factors and secrets are reused safely. + /// + IMintQuoteBuilder WithOutputs(IEnumerable outputs); + + /// + /// Optional. Provide description for the mint invoice. + /// + IMintQuoteBuilder WithDescription(string description); + + /// + /// Optional. Allows providing a P2PK builder when a signature is required for minting. + /// + IMintQuoteBuilder WithP2PkLock(P2PkBuilder p2pkBuilder); + + /// + /// Optional. When minting P2Pk / HTLC Proofs allows to blind the pubkeys. + /// + /// + /// + IMintQuoteBuilder BlindPubkeys(bool withBlinding = true); + + /// + /// Optional. Allows adding HTLC-based outputs. + /// + IMintQuoteBuilder WithHTLCLock(HTLCBuilder htlcBuilder); + + /// + /// Creates a bolt11 mint quote and handler. + /// + Task>> ProcessAsyncBolt11( + CancellationToken ct = default + ); + + /// + /// Creates a bolt12 mint quote and handler. + /// + Task>> ProcessAsyncBolt12( + CancellationToken ct = default + ); +} diff --git a/DotNut/Abstractions/Interfaces/IProofSelector.cs b/DotNut/Abstractions/Interfaces/IProofSelector.cs new file mode 100644 index 0000000..9b9f370 --- /dev/null +++ b/DotNut/Abstractions/Interfaces/IProofSelector.cs @@ -0,0 +1,11 @@ +namespace DotNut.Abstractions; + +public interface IProofSelector +{ + Task SelectProofsToSend( + IEnumerable proofsToSelectFrom, + ulong amountToSend, + bool includeFees = false, + CancellationToken ct = default + ); +} diff --git a/DotNut/Abstractions/Interfaces/IRestoreBuilder.cs b/DotNut/Abstractions/Interfaces/IRestoreBuilder.cs new file mode 100644 index 0000000..71e4e2f --- /dev/null +++ b/DotNut/Abstractions/Interfaces/IRestoreBuilder.cs @@ -0,0 +1,17 @@ +namespace DotNut.Abstractions; + +/// +/// Restore operation builder +/// +public interface IRestoreBuilder +{ + /// + /// Optional and usually not-advised. Allows to specify keysets that we want to restore. + /// If not set, every keyset is grinded. + /// + /// + /// + IRestoreBuilder FromKeysetIds(IEnumerable keysetIds); + + Task> ProcessAsync(CancellationToken ct = default); +} diff --git a/DotNut/Abstractions/Interfaces/ISwapBuilder.cs b/DotNut/Abstractions/Interfaces/ISwapBuilder.cs new file mode 100644 index 0000000..4cfa4a7 --- /dev/null +++ b/DotNut/Abstractions/Interfaces/ISwapBuilder.cs @@ -0,0 +1,74 @@ +namespace DotNut.Abstractions; + +/// +/// Swap operation builder +/// +public interface ISwapBuilder +{ + /// + /// Optional. Sets wallet unit for the swap; defaults to "sat". + /// + ISwapBuilder WithUnit(string unit); + + /// + /// Optional. Choose target keyset for the swapped proofs. + /// + ISwapBuilder ForKeyset(KeysetId targetKeysetId); + + /// + /// Provide proofs that will be used as inputs for the swap. + /// + ISwapBuilder FromInputs(IEnumerable inputs); + + /// + /// Optional. Supply custom blank outputs instead of deriving them automatically. + /// + ISwapBuilder ForOutputs(IEnumerable outputs); + + /// + /// Optional. Toggle DLEQ verification for incoming proofs. + /// + ISwapBuilder WithDLEQVerification(bool verify = true); + + /// + /// Optional. Include or skip fee calculations when creating outputs. + /// + ISwapBuilder WithFeeCalculation(bool includeFees = true); + + /// + /// Optional. Explicitly select output amounts. + /// + ISwapBuilder WithAmounts(IEnumerable amounts); + + /// + /// Optional. Provide private keys associated with the proofs. + /// + ISwapBuilder WithPrivkeys(IEnumerable privKeys); + + /// + /// Optional. Generate outputs guarded by P2PK locking. + /// + ISwapBuilder ToP2PK(P2PkBuilder p2pkBuilder); + + /// + /// Optional. Blind P2Pk / HTLC proofs. + /// + /// + /// + ISwapBuilder BlindPubkeys(bool withBlinding = true); + + /// + /// Optional. Supply preimage for HTLC-based proofs. + /// + ISwapBuilder WithHtlcPreimage(string preimage); + + /// + /// Optional. Generate HTLC outputs. + /// + ISwapBuilder ToHTLC(HTLCBuilder htlcBuilder); + + /// + /// Executes the swap flow and returns newly minted proofs. + /// + Task> ProcessAsync(CancellationToken ct = default); +} diff --git a/DotNut/Abstractions/Interfaces/IWalletBuilder.cs b/DotNut/Abstractions/Interfaces/IWalletBuilder.cs new file mode 100644 index 0000000..b6faccd --- /dev/null +++ b/DotNut/Abstractions/Interfaces/IWalletBuilder.cs @@ -0,0 +1,308 @@ +using DotNut.Api; +using DotNut.ApiModels; +using DotNut.NBitcoin.BIP39; + +namespace DotNut.Abstractions; + +/// +/// Fluent builder interface for Cashu Wallet operations +/// +public interface IWalletBuilder : IDisposable +{ + /// + /// Mandatory. Sets a mint in a wallet object + /// + /// Mint API object. + IWalletBuilder WithMint(ICashuApi mintApi, bool canDispose = false); + + /// + /// Mandatory. Sets a mint in a wallet object (with default CashuHttpClient) + /// + /// Mint URL string. + IWalletBuilder WithMint(string mintUrl); + + /// + /// Mandatory. Sets a mint in a wallet object (with default CashuHttpClient) + /// + /// Mint URI. + IWalletBuilder WithMint(Uri mintUri); + + /// + /// Optional. Import Mint Info to CashuWallet. Otherwise, it will be fetched from /v1/info endpoint. + /// + /// MintInfo object + IWalletBuilder WithInfo(MintInfo info); + + /// + /// Optional. Import Mint Info to CashuWallet. Otherwise, it will be fetched from /v1/info endpoint. + /// + /// GetInfoResponse payload returned from mints API + IWalletBuilder WithInfo(GetInfoResponse info); + + /// + /// Optional. Import Keysets into CashuWallet class. Otherwise, they will be fetched from /v1/keysets endpoint. + /// + /// List of Keysets + IWalletBuilder WithKeysets(IEnumerable keysets); + + /// + /// Optional. Import Keysets into CashuWallet class. Otherwise, they will be fetched from /v1/keysets endpoint. + /// + /// GetKeysetsResponse payload returned from mints API + IWalletBuilder WithKeysets(GetKeysetsResponse keysets); + + /// + /// Optional. Import Keys into CashuWallet class. Otherwise, they will be fetched from /v1/keys endpoint. + /// + /// List of mints Keys + IWalletBuilder WithKeys(IEnumerable keys); + + /// + /// Optional. Import Keys into CashuWallet class. Otherwise, they will be fetched from /v1/keys endpoint. + /// + /// GetKeysResponse payload returned from mints API + IWalletBuilder WithKeys(GetKeysResponse keys); + + /// + /// Optional. Flag suggesting if CashuWallet should sync provided Keys and Keysets with actual mints state. + /// Very useful if wallet stores keys in storage. + /// + /// boolean, true by default + IWalletBuilder WithKeysetSync(bool syncKeyset = true); + + /// + /// Optional. Flag suggesting if CashuWallet should sync provided Keys and Keysets with actual mints state. + /// Has an additional field limiting how often keysets can be refetched. If not set, keysets will be synced only single time, + /// with first operation requiring keysets. (I'd go for like, 60 minutes) + /// + /// + /// + /// + IWalletBuilder WithKeysetSync(bool syncKeyset, TimeSpan syncThreshold); + + /// + /// Optional. Proof selecting algorithm. If not set, defaults to RGLI proof selector. + /// + /// + IWalletBuilder WithSelector(IProofSelector selector); + + /// + /// Optional. BIP39 seed for secret and blinding factors derivation. All proofs generated by CashuWallet will be recoverable. + /// + /// Mnemonic object + IWalletBuilder WithMnemonic(Mnemonic mnemonic); + + /// + /// Optional. BIP39 seed for secret and blinding factors derivation. All proofs generated by CashuWallet will be recoverable. + /// + /// Bip39 seed string separated by spaces. + IWalletBuilder WithMnemonic(string mnemonic); + + /// + /// Optional and mandatory if Mnemonic provided. Counter for each Keyset Id for derivation purposes. + /// + /// Counter object + IWalletBuilder WithCounter(ICounter counter); + + /// + /// Optional and mandatory if Mnemonic provided. Counter for each Keyset Id for derivation purposes. + /// + /// Counter dictionary + /// + public IWalletBuilder WithCounter(IDictionary counter); + + /// + /// Optional and if not set, always true. Controls automatic counter incrementation for secret generation. + /// + /// If true, counter increments automatically. If false, requires manual management. + /// + /// WARNING: Disabling auto-increment is potentially dangerous. Manual counter management is required + /// to prevent secret reuse, which will cause mint rejection and operation failures. + /// + IWalletBuilder ShouldBumpCounter(bool shouldBumpCounter = true); + + /// + /// Optional. + /// Adds websocket service. You should use single websocket service (singleton at best) for multiple wallets, in order to handle everything in nice manner. + /// If not set, but requested it'll be created automatically (which won't be so optimal). + /// + /// + /// + IWalletBuilder WithWebsocketService(IWebsocketService websocketService); + + /// + /// Get Mints info, supported methods etc. + /// + /// Refetch flag + /// + /// MintInfo object + Task GetInfo(bool forceRefresh = false, CancellationToken ct = default); + + /// + /// Create Outputs (BlindedMessags, Blinding Factors, Secrets), for given keysetId. + /// Deterministic if Mnemonic and Counter set up. + /// + /// List of amounts in Outputs. + /// Keyset ID + /// + /// Outputs + /// If keys not set. If Mnemonic set, but no Counter. + Task> CreateOutputs( + IEnumerable amounts, + KeysetId id, + CancellationToken ct = default + ); + + /// + /// Create Outputs for active KeysetId for given unit. Fetches a keyset for given unit automatically. + /// + /// List of amounts. + /// + /// + /// Outputs + /// If no keysetID stored in wallet. + Task> CreateOutputs( + IEnumerable amounts, + string unit, + CancellationToken ct = default + ); + + /// + /// Set Last sync date to DateTime.MinValue - keysets will be synced before next operation + /// + public void InvalidateCache(); + + /// + /// Get active keyset id for chosen unit. + /// + /// keyset unit, e.g. sat + /// + /// Active keysetId + Task GetActiveKeysetId(string unit, CancellationToken ct = default); + + /// + /// Get all keysets with units + /// + /// Dictionary of (unit, KeysetId) + Task>> GetKeysetIdsWithUnits(CancellationToken ct = default); + + /// + /// Get active keyset ids for each supported unit + /// + /// Dictionary of (unit, KeysetId) + Task> GetActiveKeysetIdsWithUnits(CancellationToken ct = default); + + Task GetMintApi(CancellationToken ct = default); + + /// + /// Get keys of current mint stored in wallet. + /// + /// Refetch flag + /// + /// Mints keys + Task> GetKeys( + bool forceRefresh = false, + CancellationToken ct = default + ); + + /// + /// Get Keys for given KeysetID. At first it tries to find corresponding keys, if allowFetch is true, will try to + /// fetch keys if not present in wallet. + /// + /// KeysetId + /// If keyset not present not in db, it can be fetched + /// Refetch flag + /// + /// Keys for given keyset + Task GetKeys( + KeysetId id, + bool allowFetch, + bool forceRefresh = false, + CancellationToken ct = default + ); + + /// + /// Get Keysets stored in wallet + /// + /// Refetch flag + /// + /// List of Keysets + Task> GetKeysets( + bool forceRefresh = false, + CancellationToken ct = default + ); + + /// + /// Select proofs for sending purposes. By default uses RGLI algorithm, unless another one provided. + /// + /// + /// + /// + /// + /// + Task SelectProofsToSend( + IEnumerable proofs, + ulong amount, + bool includeFees, + CancellationToken ct = default + ); + + /// + /// Getter for proof selector. If not set, returns RGLI algorithm by default. + /// + /// + /// + Task GetSelector(CancellationToken ct = default); + + /// + /// Returns websocket service, that can be shared between multiple wallets. + /// + /// + /// + Task GetWebsocketService(CancellationToken ct = default); + + /// + /// Returns current Counter instance. + /// + /// + ICounter? GetCounter(); + + /// + /// Create swap transaction builder. + /// + /// Swap transaction builder + ISwapBuilder Swap(); + + /// + /// Create melt quote builder. + /// + /// + IMeltQuoteBuilder CreateMeltQuote(); + + /// + /// Create Mint Quote + /// + /// Method-agnostic Mint Quote builder abstraction. + IMintQuoteBuilder CreateMintQuote(); + + /// + /// Can restore proofs if mnemonic provided. + /// + /// + IRestoreBuilder Restore(); + + /// + /// Check state of provided proofs. + /// + /// + Task CheckState( + IEnumerable proofs, + CancellationToken ct = default + ); + + /// + /// Check state of provided proofs. + /// + /// + Task CheckState(IEnumerable Ys, CancellationToken ct = default); +} diff --git a/DotNut/Abstractions/Interfaces/IWebsocketService.cs b/DotNut/Abstractions/Interfaces/IWebsocketService.cs new file mode 100644 index 0000000..57e3b32 --- /dev/null +++ b/DotNut/Abstractions/Interfaces/IWebsocketService.cs @@ -0,0 +1,31 @@ +using System.Net.WebSockets; +using DotNut.Abstractions.Websockets; + +namespace DotNut.Abstractions; + +public interface IWebsocketService : IAsyncDisposable +{ + /// + /// Raised when a connection's state changes. Handlers should be thread-safe. + /// + event EventHandler? ConnectionStateChanged; + + Task LazyConnectAsync(string mintUrl, CancellationToken ct = default); + + Task DisconnectAsync(string mintUrl, CancellationToken ct = default); + + Task SubscribeAsync( + string mintUrl, + SubscriptionKind kind, + string[] filters, + CancellationToken ct = default + ); + + Task UnsubscribeAsync(string subId, CancellationToken ct = default); + + WebSocketState GetConnectionState(string mintUrl); + + IEnumerable GetSubscriptions(string mintUrl); + + IEnumerable GetConnections(); +} diff --git a/DotNut/Abstractions/MeltQuoteBuilder.cs b/DotNut/Abstractions/MeltQuoteBuilder.cs new file mode 100644 index 0000000..04bc696 --- /dev/null +++ b/DotNut/Abstractions/MeltQuoteBuilder.cs @@ -0,0 +1,125 @@ +using DotNut.Abstractions.Handlers; +using DotNut.ApiModels; +using DotNut.ApiModels.Melt; +using DotNut.ApiModels.Melt.bolt12; + +namespace DotNut.Abstractions; + +class MeltQuoteBuilder : IMeltQuoteBuilder +{ + private readonly Wallet _wallet; + private string? _invoice; + private List? _blankOutputs; + private string _unit = "sat"; + + private ulong? _amount; + + private List? _privKeys; + private string? _htlcPreimage; + + public MeltQuoteBuilder(Wallet wallet) + { + _wallet = wallet; + } + + public IMeltQuoteBuilder WithInvoice(string invoice) + { + this._invoice = invoice; + return this; + } + + public IMeltQuoteBuilder WithUnit(string unit) + { + this._unit = unit; + return this; + } + + public IMeltQuoteBuilder WithBlankOutputs(IEnumerable blankOutputs) + { + this._blankOutputs = blankOutputs as List ?? blankOutputs.ToList(); + return this; + } + + // when proofs were p2pk + public IMeltQuoteBuilder WithPrivKeys(IEnumerable privKeys) + { + this._privKeys = privKeys.ToList(); + return this; + } + + public IMeltQuoteBuilder WithHTLCPreimage(string preimage) + { + this._htlcPreimage = preimage; + return this; + } + + public IMeltQuoteBuilder WithAmount(ulong msat) + { + this._amount = msat; + return this; + } + + public async Task>> ProcessAsyncBolt11( + CancellationToken ct = default + ) + { + var mintApi = await _wallet.GetMintApi(ct); + await _wallet._maybeSyncKeys(ct); + ArgumentNullException.ThrowIfNull(this._invoice); + + var req = new PostMeltQuoteBolt11Request { Request = this._invoice, Unit = this._unit }; + + if (this._amount != null) + { + req.Options = new MeltQuoteRequestOptions + { + Amountless = new AmountlessMeltQuoteOptions { AmountMsat = this._amount.Value }, + }; + } + + var quote = await mintApi.CreateMeltQuote< + PostMeltQuoteBolt11Response, + PostMeltQuoteBolt11Request + >("bolt11", req, ct); + + if (_blankOutputs == null) + { + var outputsAmount = Utils.CalculateNumberOfBlankOutputs((ulong)quote.FeeReserve); + var amounts = Enumerable.Repeat(1UL, outputsAmount).ToList(); + this._blankOutputs = await this._wallet.CreateOutputs(amounts, this._unit, ct); + } + return new MeltHandlerBolt11(_wallet, quote, _blankOutputs, _privKeys, _htlcPreimage); + } + + public async Task>> ProcessAsyncBolt12( + CancellationToken ct = default + ) + { + var mintApi = await _wallet.GetMintApi(ct); + await _wallet._maybeSyncKeys(ct); + ArgumentNullException.ThrowIfNull(this._invoice); + + var req = new PostMeltQuoteBolt12Request() { Request = this._invoice, Unit = this._unit }; + + if (this._amount != null) + { + req.Options = new MeltQuoteRequestOptions + { + Amountless = new AmountlessMeltQuoteOptions { AmountMsat = this._amount.Value }, + }; + } + + var quote = await mintApi.CreateMeltQuote< + PostMeltQuoteBolt12Response, + PostMeltQuoteBolt12Request + >("bolt12", req, ct); + + if (_blankOutputs == null) + { + var outputsAmount = Utils.CalculateNumberOfBlankOutputs((ulong)quote.FeeReserve); + var amounts = Enumerable.Repeat(1UL, outputsAmount).ToList(); + this._blankOutputs = await this._wallet.CreateOutputs(amounts, this._unit, ct); + } + return new MeltHandlerBolt12(_wallet, quote, _blankOutputs, _privKeys, _htlcPreimage); + } +} diff --git a/DotNut/Abstractions/MintInfo.cs b/DotNut/Abstractions/MintInfo.cs new file mode 100644 index 0000000..1062d36 --- /dev/null +++ b/DotNut/Abstractions/MintInfo.cs @@ -0,0 +1,284 @@ +using System.Collections.Concurrent; +using System.Text.Json; +using System.Text.Json.Serialization; +using System.Text.RegularExpressions; +using DotNut.ApiModels; +using DotNut.ApiModels.Info; + +namespace DotNut.Abstractions; + +public class MintInfo +{ + private readonly GetInfoResponse _mintInfo; + private readonly ProtectedEndpoints? _protectedEndpoints; + + public MintInfo(GetInfoResponse info) + { + _mintInfo = info; + + if (info.Nuts?.TryGetValue(22, out var nut22Json) == true) + { + try + { + var nut22 = JsonSerializer.Deserialize(nut22Json.RootElement.GetRawText()); + if (nut22?.ProtectedEndpoints != null) + { + _protectedEndpoints = new ProtectedEndpoints + { + Cache = new ConcurrentDictionary(), + ApiReturn = nut22 + .ProtectedEndpoints.Select(o => new ProtectedEndpoint + { + Method = o.Method, + Regex = new Regex( + o.Path, + RegexOptions.None, + TimeSpan.FromMilliseconds(100) + ), + }) + .ToArray(), + }; + } + } + catch (JsonException) + { + // Ignore parsing errors for NUT-22 + } + } + } + + /// + /// Checks support for NUTs 4 and 5 (mint/melt operations) + /// + public SwapInfo IsSupportedMintMelt(int nutNumber) + { + if (nutNumber != 4 && nutNumber != 5) + throw new ArgumentException( + "Only NUT 4 and 5 are supported by this method", + nameof(nutNumber) + ); + + return CheckMintMelt(nutNumber); + } + + /// + /// Checks support for generic NUTs (7, 8, 9, 10, 11, 12, 14, 20) + /// + public GenericNut IsSupportedGeneric(int nutNumber) + { + var supportedNuts = new[] { 7, 8, 9, 10, 11, 12, 14, 20 }; + if (!supportedNuts.Contains(nutNumber)) + throw new ArgumentException( + $"NUT {nutNumber} is not supported by this method", + nameof(nutNumber) + ); + + return CheckGenericNut(nutNumber); + } + + /// + /// Checks support for NUT 17 (WebSocket) + /// + public WebSocketSupportResult IsSupportedWebSocket() + { + return CheckNut17(); + } + + /// + /// Checks support for NUT 15 (MPP) + /// + public MppSupport IsSupportedMpp() + { + return CheckNut15(); + } + + /// + /// Determines if an endpoint requires blind authentication token based on NUT-22 + /// + public bool RequiresBlindAuthToken(string path) + { + if (_protectedEndpoints == null) + return false; + + if (_protectedEndpoints.Cache.TryGetValue(path, out var cachedValue)) + return cachedValue; + + var isProtectedEndpoint = _protectedEndpoints.ApiReturn.Any(e => e.Regex.IsMatch(path)); + + _protectedEndpoints.Cache[path] = isProtectedEndpoint; + return isProtectedEndpoint; + } + + private GenericNut CheckGenericNut(int nutNumber) + { + if (_mintInfo.Nuts?.TryGetValue(nutNumber, out var nutJson) == true) + { + try + { + var nut = JsonSerializer.Deserialize(nutJson.RootElement.GetRawText()); + return new GenericNut { Supported = nut?.Supported == true }; + } + catch (JsonException) + { + return new GenericNut { Supported = false }; + } + } + return new GenericNut { Supported = false }; + } + + private SwapInfo CheckMintMelt(int nutNumber) + { + if (_mintInfo.Nuts?.TryGetValue(nutNumber, out var nutJson) == true) + { + try + { + var nut = JsonSerializer.Deserialize(nutJson.RootElement.GetRawText()); + if (nut?.Methods != null && nut.Methods.Length > 0 && nut.Disabled != true) + { + return new SwapInfo { Disabled = false, Methods = nut.Methods }; + } + return new SwapInfo { Disabled = true, Methods = nut?.Methods ?? [] }; + } + catch (JsonException) + { + return new SwapInfo { Disabled = true, Methods = [] }; + } + } + return new SwapInfo { Disabled = true, Methods = [] }; + } + + private WebSocketSupportResult CheckNut17() + { + if (_mintInfo.Nuts?.TryGetValue(17, out var nutJson) == true) + { + try + { + var nut = JsonSerializer.Deserialize( + nutJson.RootElement.GetRawText() + ); + if (nut?.Supported != null && nut.Supported.Length > 0) + { + return new WebSocketSupportResult { Supported = true, Params = nut.Supported }; + } + } + catch (JsonException) + { + // Ignore parsing errors + } + } + return new WebSocketSupportResult { Supported = false }; + } + + private MppSupport CheckNut15() + { + if (_mintInfo.Nuts?.TryGetValue(15, out var nutJson) == true) + { + try + { + var nut = JsonSerializer.Deserialize(nutJson.RootElement.GetRawText()); + if (nut?.Methods != null && nut.Methods.Length > 0) + { + return new MppSupport { Supported = true, Methods = nut.Methods }; + } + } + catch (JsonException) + { + // Ignore parsing errors + } + } + return new MppSupport() { Supported = false }; + } + + public bool SupportsBolt12Description + { + get + { + if (_mintInfo.Nuts?.TryGetValue(4, out var nut4Json) == true) + { + try + { + var nut4 = JsonSerializer.Deserialize( + nut4Json.RootElement.GetRawText() + ); + return nut4?.Methods?.Any(method => + method.Method == "bolt12" && method.Options?.Description == true + ) == true; + } + catch (JsonException) + { + return false; + } + } + return false; + } + } + + public List? Contact => _mintInfo.Contact; + public string? Description => _mintInfo.Description; + public string? DescriptionLong => _mintInfo.DescriptionLong; + public string? Name => _mintInfo.Name; + public string? Pubkey => _mintInfo.Pubkey; + public Dictionary? Nuts => _mintInfo.Nuts; + public string? Version => _mintInfo.Version; + public string? Motd => _mintInfo.Motd; +} + +// Supporting classes for different NUT types +public class GenericNut +{ + [JsonPropertyName("supported")] + public bool Supported { get; set; } +} + +public class MintMeltNut +{ + [JsonPropertyName("methods")] + public SwapInfo.SwapMethod[]? Methods { get; set; } + + [JsonPropertyName("disabled")] + public bool? Disabled { get; set; } +} + +public class WebSocketNut +{ + [JsonPropertyName("supported")] + public WebSocketSupport[]? Supported { get; set; } +} + +public class Nut22 +{ + [JsonPropertyName("protected_endpoints")] + public ProtectedEndpointSpec[]? ProtectedEndpoints { get; set; } +} + +public class ProtectedEndpointSpec +{ + [JsonPropertyName("method")] + public string Method { get; set; } = string.Empty; + + [JsonPropertyName("path")] + public string Path { get; set; } = string.Empty; +} + +internal class ProtectedEndpoints +{ + public ConcurrentDictionary Cache { get; set; } = new(); + public ProtectedEndpoint[] ApiReturn { get; set; } = Array.Empty(); +} + +internal class ProtectedEndpoint +{ + public string Method { get; set; } = string.Empty; + public System.Text.RegularExpressions.Regex Regex { get; set; } +} + +public class WebSocketSupportResult +{ + public bool Supported { get; set; } + public WebSocketSupport[]? Params { get; set; } +} + +public class MppSupport : MPPInfo +{ + public bool Supported { get; set; } +} diff --git a/DotNut/Abstractions/MintQuoteBuilder.cs b/DotNut/Abstractions/MintQuoteBuilder.cs new file mode 100644 index 0000000..0c4257a --- /dev/null +++ b/DotNut/Abstractions/MintQuoteBuilder.cs @@ -0,0 +1,275 @@ +using System.Security.Cryptography; +using DotNut.Abstractions.Handlers; +using DotNut.Api; +using DotNut.ApiModels; +using DotNut.ApiModels.Mint.bolt12; + +namespace DotNut.Abstractions; + +class MintQuoteBuilder : IMintQuoteBuilder +{ + private readonly Wallet _wallet; + + private ulong? _amount; + private List? _amounts; + private string _unit = "sat"; + private string? _description; + private List? _outputs; + + private string? _pubkey; + + private KeysetId? _keysetId; + private GetKeysResponse.KeysetItemResponse? _keyset; + + //for p2pk + private P2PkBuilder? _builder; + private bool _shouldBlind = false; + + public MintQuoteBuilder(Wallet wallet) + { + this._wallet = wallet; + } + + public IMintQuoteBuilder WithAmount(ulong amount) + { + this._amount = amount; + return this; + } + + public IMintQuoteBuilder WithUnit(string unit) + { + this._unit = unit; + return this; + } + + public IMintQuoteBuilder WithPubkey(string pubkey) + { + this._pubkey = pubkey; + return this; + } + + public IMintQuoteBuilder WithPubkey(PubKey pubkey) + { + this._pubkey = pubkey.ToString(); + return this; + } + + public IMintQuoteBuilder WithKeyset(KeysetId keysetId) + { + this._keysetId = keysetId; + return this; + } + + public IMintQuoteBuilder WithOutputs(IEnumerable outputs) + { + var os = outputs as List ?? outputs.ToList(); + if (os.Count == 0) + { + throw new ArgumentException("Outputs collection cannot be empty."); + } + if (os.Any(o => o.BlindedMessage.Id != os[0].BlindedMessage.Id)) + { + throw new ArgumentException("Every output must have the same keyset id!"); + } + + this._outputs = os; + return this; + } + + public IMintQuoteBuilder WithP2PkLock(P2PkBuilder p2pkBuilder) + { + this._builder = p2pkBuilder; + return this; + } + + public IMintQuoteBuilder BlindPubkeys(bool withBlinding = true) + { + this._shouldBlind = withBlinding; + return this; + } + + public IMintQuoteBuilder WithHTLCLock(HTLCBuilder htlcBuilder) + { + this._builder = htlcBuilder; + return this; + } + + public IMintQuoteBuilder WithDescription(string description) + { + this._description = description; + return this; + } + + public async Task>> ProcessAsyncBolt11( + CancellationToken ct = default + ) + { + //todo implement info + + await this._wallet._maybeSyncKeys(ct); + if (_amount == null) + { + throw new ArgumentNullException( + nameof(_amount), + "can't create mint quote without amount!" + ); + } + + var api = await this._wallet.GetMintApi(ct); + if (api is null) + { + throw new ArgumentNullException( + nameof(ICashuApi), + "Can't request mint quote without mint API" + ); + } + + this._keysetId ??= + await this._wallet.GetActiveKeysetId(this._unit, ct) + ?? throw new ArgumentException($"Can't get active keyset ID for unit: {_unit}"); + + this._keyset ??= + await this._wallet.GetKeys(this._keysetId, true, false, ct) + ?? throw new ArgumentException($"Cant get keys for keysetId: {_keysetId}"); + + var outputs = await this._createOutputs(); + + var reqBolt11 = new PostMintQuoteBolt11Request() + { + Amount = this._amount.Value, + Unit = this._unit, + Description = this._description, + Pubkey = this._pubkey, + }; + var quoteBolt11 = await api.CreateMintQuote< + PostMintQuoteBolt11Response, + PostMintQuoteBolt11Request + >("bolt11", reqBolt11, ct); + return new MintHandlerBolt11(this._wallet, quoteBolt11, this._keyset, outputs); + } + + public async Task>> ProcessAsyncBolt12( + CancellationToken ct = default + ) + { + await this._wallet._maybeSyncKeys(ct); + + var api = await this._wallet.GetMintApi(ct); + if (api is null) + { + throw new ArgumentNullException( + nameof(ICashuApi), + "Can't request mint quote without mint API" + ); + } + + if (this._pubkey == null) + { + throw new ArgumentNullException( + nameof(_pubkey), + "Can't request bolt12 mint quote without pubkey!" + ); + } + if (this._amount == null) + { + throw new ArgumentNullException( + nameof(_amount), + "Can't create bolt12 mint quote without amount!" + ); + } + + this._keysetId ??= + await this._wallet.GetActiveKeysetId(this._unit, ct) + ?? throw new ArgumentException($"Can't get active keyset ID for unit: {_unit}"); + + if (this._keyset == null) + { + this._keyset = + await this._wallet.GetKeys(this._keysetId, true, false, ct) + ?? throw new ArgumentException($"Cant fetch keys for keysetId: {_keysetId}"); + } + + var outputs = await this._createOutputs(); + + var req = new PostMintQuoteBolt12Request() + { + Amount = this._amount.Value, + Unit = this._unit, + Pubkey = this._pubkey, + Description = this._description, + }; + var mintQuote = await api.CreateMintQuote< + PostMintQuoteBolt12Response, + PostMintQuoteBolt12Request + >("bolt12", req, ct); + return new MintHandlerBolt12(this._wallet, mintQuote, this._keyset, outputs); + } + + // skipped checks for keysetid and keys, since its validated before. make sure to remember about it. + async Task> _createOutputs() + { + var outputs = new List(); + + if (this._outputs != null) + { + if (this._builder is not null) + { + throw new ArgumentException( + "Can't create p2pk outputs if outputs provided. Remove either p2pk builder parameter or outputs." + ); + } + return this._outputs; + } + + if (this._amount is null && this._amounts is null) + { + throw new ArgumentNullException( + nameof(_amount), + "Amount can't be determined. Make sure to include amount, or amounts parameter!" + ); + } + _amounts ??= Utils.SplitToProofsAmounts(_amount.Value, _keyset!.Keys); + + if (this._builder is null) + { + return await _wallet.CreateOutputs(_amounts, this._keysetId!); + } + + if (this._shouldBlind) + { + if (this._builder.SigFlag == "SIG_ALL") + { + var e = new PrivKey(RandomNumberGenerator.GetHexString(64)); + foreach (var amount in _amounts) + { + var builder = _builder.Clone(); + var p2pkOutput = Utils.CreateNut10BlindedOutput( + amount, + this._keysetId!, + builder, + e + ); + outputs.Add(p2pkOutput); + } + + return outputs; + } + + foreach (var amount in _amounts) + { + var builder = _builder.Clone(); + var p2pkOutput = Utils.CreateNut10BlindedOutput(amount, this._keysetId!, builder); + outputs.Add(p2pkOutput); + } + return outputs; + } + + foreach (var amount in _amounts) + { + var builder = _builder.Clone(); + var p2pkOutput = Utils.CreateNut10Output(amount, this._keysetId!, builder); + outputs.Add(p2pkOutput); + } + return outputs; + } +} diff --git a/DotNut/Abstractions/Nut10Helper.cs b/DotNut/Abstractions/Nut10Helper.cs new file mode 100644 index 0000000..9d50a99 --- /dev/null +++ b/DotNut/Abstractions/Nut10Helper.cs @@ -0,0 +1,74 @@ +using System.Text.Json; +using NBitcoin.Secp256k1; + +namespace DotNut.Abstractions; + +internal static class Nut10Helper +{ + public static void MaybeProcessNut10( + List privKeys, + List proofs, + List? outputs = null, + string? htlcPreimage = null, + string? meltQuoteId = null + ) + { + if (privKeys.Count == 0 || proofs.Count == 0) + { + return; + } + + outputs ??= []; + var sigAllHandler = new SigAllHandler + { + Proofs = proofs, + PrivKeys = privKeys, + BlindedMessages = outputs.Select(o => o.BlindedMessage).ToList(), + HTLCPreimage = htlcPreimage, + MeltQuoteId = meltQuoteId, + }; + + if (sigAllHandler.TrySign(out string? witness)) + { + proofs[0].Witness = witness; + return; + } + + var keys = privKeys.Select(p => p.Key).ToArray(); + + foreach (var proof in proofs) + { + HandleWitnessCreation(proof, keys, htlcPreimage); + } + } + + private static void HandleWitnessCreation(Proof proof, ECPrivKey[] keys, string? htlcPreimage) + { + if (proof.Secret is Nut10Secret { ProofSecret: HTLCProofSecret htlc }) + { + // preimage isn't verified after timelock + var preimage = htlcPreimage ?? ""; + if (proof.P2PkE is { } E) + { + var blindwitness = htlc.GenerateBlindWitness(proof, keys, preimage); + proof.Witness = JsonSerializer.Serialize(blindwitness); + return; + } + var witness = htlc.GenerateWitness(proof, keys, preimage); + proof.Witness = JsonSerializer.Serialize(witness); + return; + } + + if (proof.Secret is Nut10Secret { ProofSecret: P2PKProofSecret p2pk }) + { + if (proof.P2PkE is { } E) + { + var blindWitness = p2pk.GenerateBlindWitness(proof, keys); + proof.Witness = JsonSerializer.Serialize(blindWitness); + return; + } + var proofWitness = p2pk.GenerateWitness(proof, keys); + proof.Witness = JsonSerializer.Serialize(proofWitness); + } + } +} diff --git a/DotNut/Abstractions/OutputData.cs b/DotNut/Abstractions/OutputData.cs new file mode 100644 index 0000000..f7902a9 --- /dev/null +++ b/DotNut/Abstractions/OutputData.cs @@ -0,0 +1,10 @@ +namespace DotNut.Abstractions; + +public class OutputData +{ + public BlindedMessage BlindedMessage { get; set; } + public ISecret Secret { get; set; } + public PrivKey BlindingFactor { get; set; } + + public PubKey? P2BkE { get; set; } +} diff --git a/DotNut/ProofSelector.cs b/DotNut/Abstractions/ProofSelector.cs similarity index 83% rename from DotNut/ProofSelector.cs rename to DotNut/Abstractions/ProofSelector.cs index 22f7caf..bbd95ba 100644 --- a/DotNut/ProofSelector.cs +++ b/DotNut/Abstractions/ProofSelector.cs @@ -1,16 +1,10 @@ using System.Diagnostics; -namespace DotNut; +namespace DotNut.Abstractions; -public class SendResponse -{ - public List Keep { get; set; } = new(); - public List Send { get; set; } = new(); -} - -// Borrowed from cashu-ts +// Borrowed from cashu-ts // see https://github.com/cashubtc/cashu-ts/pull/314 -public class ProofSelector +public class ProofSelector : IProofSelector { private class ProofWithFee { @@ -60,8 +54,15 @@ private ulong GetProofFeePPK(Proof proof) return _keysetFees.TryGetValue(proof.Id, out var fee) ? fee : 0; } - public SendResponse SelectProofsToSend(List proofs, ulong amountToSend, bool includeFees = false) + public async Task SelectProofsToSend( + IEnumerable proofs, + ulong amountToSend, + bool includeFees = false, + CancellationToken ct = default + ) { + var proofsToSelectFrom = proofs as List ?? proofs.ToList(); + // Init vars const int MAX_TRIALS = 60; // 40-80 is optimal (per RGLI paper) const double MAX_OVRPCT = 0; // Acceptable close match overage (percent) @@ -69,7 +70,7 @@ public SendResponse SelectProofsToSend(List proofs, ulong amountToSend, b const long MAX_TIMEMS = 1000; // Halt new trials if over time (in ms) const int MAX_P2SWAP = 5000; // Max number of Phase 2 improvement swaps const bool exactMatch = false; // Allows close match (> amountToSend + fee) - + var timer = new Timer(); // start the clock List? bestSubset = null; double bestDelta = double.PositiveInfinity; @@ -79,7 +80,7 @@ public SendResponse SelectProofsToSend(List proofs, ulong amountToSend, b /* * Helper Functions. */ - + // Calculate net amount after fees double SumExFees(ulong amount, ulong feePPK) { @@ -90,10 +91,9 @@ double SumExFees(ulong amount, ulong feePPK) List ShuffleArray(IEnumerable array) { var shuffled = array.ToList(); - var random = new Random(); for (int i = shuffled.Count - 1; i > 0; i--) { - int j = random.Next(i + 1); + int j = Random.Shared.Next(i + 1); (shuffled[i], shuffled[j]) = (shuffled[j], shuffled[i]); } return shuffled; @@ -104,27 +104,28 @@ List ShuffleArray(IEnumerable array) // If lessOrEqual=false, returns the leftmost index where exFee >= value int? BinarySearchIndex(List arr, double value, bool lessOrEqual) { - int left = 0, right = arr.Count - 1; + int left = 0, + right = arr.Count - 1; int? result = null; - + while (left <= right) { int mid = (left + right) / 2; double midValue = arr[mid].ExFee; - + if (lessOrEqual ? midValue <= value : midValue >= value) { result = mid; - if (lessOrEqual) + if (lessOrEqual) left = mid + 1; - else + else right = mid - 1; } else { - if (lessOrEqual) + if (lessOrEqual) right = mid - 1; - else + else left = mid + 1; } } @@ -135,14 +136,15 @@ List ShuffleArray(IEnumerable array) void InsertSorted(List arr, ProofWithFee obj) { double value = obj.ExFee; - int left = 0, right = arr.Count; - + int left = 0, + right = arr.Count; + while (left < right) { int mid = (left + right) / 2; - if (arr[mid].ExFee < value) + if (arr[mid].ExFee < value) left = mid + 1; - else + else right = mid; } arr.Insert(left, obj); @@ -154,7 +156,7 @@ void InsertSorted(List arr, ProofWithFee obj) double CalculateDelta(ulong amount, ulong feePPK) { double netSum = SumExFees(amount, feePPK); - if (netSum < amountToSend) + if (netSum < amountToSend) return double.PositiveInfinity; // no good return amount + feePPK / 1000.0 - amountToSend; } @@ -164,20 +166,22 @@ double CalculateDelta(ulong amount, ulong feePPK) */ ulong totalAmount = 0; ulong totalFeePPK = 0; - var proofWithFees = proofs.Select(p => - { - ulong ppkfee = GetProofFeePPK(p); - double exFee = includeFees ? p.Amount - ppkfee / 1000.0 : p.Amount; - var obj = new ProofWithFee(p, exFee, ppkfee); - - // Sum all economical proofs (filtered below) - if (!includeFees || exFee > 0) + var proofWithFees = proofsToSelectFrom + .Select(p => { - totalAmount += p.Amount; - totalFeePPK += ppkfee; - } - return obj; - }).ToList(); + ulong ppkfee = GetProofFeePPK(p); + double exFee = includeFees ? p.Amount - ppkfee / 1000.0 : p.Amount; + var obj = new ProofWithFee(p, exFee, ppkfee); + + // Sum all economical proofs (filtered below) + if (!includeFees || exFee > 0) + { + totalAmount += p.Amount; + totalFeePPK += ppkfee; + } + return obj; + }) + .ToList(); // Filter uneconomical proofs (totals computed above) var spendableProofs = includeFees @@ -207,7 +211,9 @@ double CalculateDelta(ulong amount, ulong feePPK) var rightIndex = BinarySearchIndex(spendableProofs, nextBiggerExFee, true); if (rightIndex == null) { - throw new InvalidOperationException("Unexpected null rightIndex in binary search"); + throw new InvalidOperationException( + "Unexpected null rightIndex in binary search" + ); } endIndex = rightIndex.Value + 1; } @@ -231,13 +237,14 @@ double CalculateDelta(ulong amount, ulong feePPK) double totalNetSum = SumExFees(totalAmount, totalFeePPK); if (amountToSend <= 0 || amountToSend > totalNetSum) { - return new SendResponse { Keep = proofs, Send = new List() }; + return new SendResponse { Keep = proofsToSelectFrom, Send = new List() }; } // Max acceptable amount for non-exact matches double maxOverAmount = Math.Min( Math.Ceiling(amountToSend * (1 + MAX_OVRPCT / 100)), - Math.Min(amountToSend + MAX_OVRAMT, totalNetSum)); + Math.Min(amountToSend + MAX_OVRAMT, totalNetSum) + ); /* * RGLI algorithm: Runs multiple trials (up to MAX_TRIALS) Each trial starts with randomized @@ -247,27 +254,28 @@ double CalculateDelta(ulong amount, ulong feePPK) */ for (int trial = 0; trial < MAX_TRIALS; trial++) { + ct.ThrowIfCancellationRequested(); // PHASE 1: Randomized Greedy Selection // Add proofs up to amountToSend (after adjusting for fees) // for exact match or the first amount over target otherwise var S = new List(); ulong amount = 0; ulong feePPK = 0; - + foreach (var obj in ShuffleArray(spendableProofs)) { ulong newAmount = amount + obj.Proof.Amount; ulong newFeePPK = feePPK + obj.PpkFee; double netSum = SumExFees(newAmount, newFeePPK); - - if (exactMatch && netSum > amountToSend) + + if (exactMatch && netSum > amountToSend) break; - + S.Add(obj); amount = newAmount; feePPK = newFeePPK; - - if (netSum >= amountToSend) + + if (netSum >= amountToSend) break; } @@ -281,16 +289,18 @@ double CalculateDelta(ulong amount, ulong feePPK) // Using array.Contains() would be way slower: O(n*m) var selectedCs = S.Select(pwf => pwf.Proof.C).ToHashSet(); var others = spendableProofs.Where(obj => !selectedCs.Contains(obj.Proof.C)).ToList(); - + // Generate a random order for accessing the trial subset ('S') var indices = ShuffleArray(Enumerable.Range(0, S.Count)).Take(MAX_P2SWAP).ToList(); - + foreach (int i in indices) { // Exact or acceptable close match solution found? double netSum = SumExFees(amount, feePPK); - if (Math.Abs(netSum - amountToSend) < 0.0001 || - (!exactMatch && netSum >= amountToSend && netSum <= maxOverAmount)) + if ( + Math.Abs(netSum - amountToSend) < 0.0001 + || (!exactMatch && netSum >= amountToSend && netSum <= maxOverAmount) + ) { break; } @@ -329,7 +339,6 @@ double CalculateDelta(ulong amount, ulong feePPK) double delta = CalculateDelta(amount, feePPK); if (delta < bestDelta) { - bestSubset = S.OrderByDescending(a => a.ExFee).ToList(); // copy & sort bestDelta = delta; bestAmount = amount; @@ -344,14 +353,14 @@ double CalculateDelta(ulong amount, ulong feePPK) { var objP = tempS.Last(); tempS.RemoveAt(tempS.Count - 1); - + ulong tempAmount2 = amount - objP.Proof.Amount; ulong tempFeePPK2 = feePPK - objP.PpkFee; double tempDelta = CalculateDelta(tempAmount2, tempFeePPK2); - - if (double.IsPositiveInfinity(tempDelta)) + + if (double.IsPositiveInfinity(tempDelta)) break; - + if (tempDelta < bestDelta) { bestSubset = tempS.ToList(); @@ -368,8 +377,10 @@ double CalculateDelta(ulong amount, ulong feePPK) if (bestSubset != null && !double.IsPositiveInfinity(bestDelta)) { double bestSum = SumExFees(bestAmount, bestFeePPK); - if (Math.Abs(bestSum - amountToSend) < 0.0001 || - (!exactMatch && bestSum >= amountToSend && bestSum <= maxOverAmount)) + if ( + Math.Abs(bestSum - amountToSend) < 0.0001 + || (!exactMatch && bestSum >= amountToSend && bestSum <= maxOverAmount) + ) { break; } @@ -380,7 +391,9 @@ double CalculateDelta(ulong amount, ulong feePPK) { if (exactMatch) { - throw new TimeoutException("Proof selection took too long. Try again with a smaller proof set."); + throw new TimeoutException( + "Proof selection took too long. Try again with a smaller proof set." + ); } else { @@ -394,11 +407,11 @@ double CalculateDelta(ulong amount, ulong feePPK) { var bestProofs = bestSubset.Select(obj => obj.Proof).ToList(); var bestProofCs = bestProofs.Select(p => p.C).ToHashSet(); - var keep = proofs.Where(p => !bestProofCs.Contains(p.C)).ToList(); - + var keep = proofsToSelectFrom.Where(p => !bestProofCs.Contains(p.C)).ToList(); + return new SendResponse { Keep = keep, Send = bestProofs }; } - return new SendResponse { Keep = proofs, Send = new List() }; + return new SendResponse { Keep = proofsToSelectFrom, Send = new List() }; } } diff --git a/DotNut/Abstractions/RestoreBuilder.cs b/DotNut/Abstractions/RestoreBuilder.cs new file mode 100644 index 0000000..f95f92a --- /dev/null +++ b/DotNut/Abstractions/RestoreBuilder.cs @@ -0,0 +1,228 @@ +using DotNut.Api; +using DotNut.ApiModels; +using DotNut.NBitcoin.BIP39; +using DotNut.NUT13; + +namespace DotNut.Abstractions; + +public class RestoreBuilder : IRestoreBuilder +{ + private readonly Wallet _wallet; + private List? _specifiedKeysets; + private const uint BATCH_SIZE = 100; + private const uint EMPTY_BATCHES_ALLOWED = 3; + + public RestoreBuilder(Wallet wallet) + { + this._wallet = wallet; + } + + public IRestoreBuilder FromKeysetIds(IEnumerable keysetIds) + { + this._specifiedKeysets = keysetIds.ToList(); + return this; + } + + public async Task> ProcessAsync(CancellationToken ct = default) + { + var api = await _wallet.GetMintApi(ct); + await _wallet._maybeSyncKeys(ct); + var mnemonic = + _wallet.GetMnemonic() + ?? throw new ArgumentNullException( + nameof(Mnemonic), + "Can't restore wallet without Mnemonic" + ); + + // keyset ids we want to grind our counter on + _specifiedKeysets ??= (await _wallet.GetKeysets(ct: ct)).Select(k => k.Id).ToList(); + if (_specifiedKeysets == null || _specifiedKeysets.Count == 0) + { + throw new InvalidOperationException( + "No keysets available for restoration. Ensure the mint has at least one active keyset or specify keysets explicitly." + ); + } + + var counter = _wallet.GetCounter(); + if (counter == null) + { + throw new ArgumentNullException(nameof(counter), "Counter cannot be null."); + } + + // fetch all batches + List recoveredProofs = new List(); + foreach (var keysetId in _specifiedKeysets) + { + var keysetProofs = await GrindKeyset(keysetId, mnemonic, counter, api, ct); + recoveredProofs.AddRange(keysetProofs); + } + + // if nothing found - return empty collection + if (recoveredProofs.Count == 0) + { + return []; + } + + var freshProofs = new List(); + + // create hash table for every KeysetId : unit. + var allKeysetsUnits = await _wallet.GetKeysetIdsWithUnits(ct); + if (allKeysetsUnits == null) + { + throw new InvalidOperationException("No keysets available for restoration."); + } + var unitsForKeysets = allKeysetsUnits + .SelectMany(unit => + unit.Value.Select(keysetId => new { KeysetId = keysetId, Unit = unit.Key }) + ) + .ToDictionary(x => x.KeysetId, x => x.Unit); + + var activeUnits = await this._wallet.GetActiveKeysetIdsWithUnits(ct); + if (activeUnits == null || !activeUnits.Any()) + { + throw new InvalidOperationException("Could not restore wallet without active keysets"); + } + + foreach (var unitKeyset in activeUnits) + { + var unit = unitKeyset.Key; + var proofsForUnit = recoveredProofs + .Where(p => + unitsForKeysets.TryGetValue(p.Id, out var proofUnit) && proofUnit == unit + ) + .ToList(); + if (proofsForUnit.Count == 0) + { + continue; + } + + // check proofs state: + var unspentProofsForUnit = new List(); + var state = await _wallet.CheckState(proofsForUnit, ct); + for (int i = 0; i < proofsForUnit.Count; i++) + { + if (state.States[i].State != StateResponseItem.TokenState.UNSPENT) + { + continue; + } + unspentProofsForUnit.Add(proofsForUnit[i]); + } + + // swap unspent tokens to single keyset + var proofs = await _wallet + .Swap() + .ForKeyset(unitKeyset.Value) + .WithDLEQVerification() + .FromInputs(unspentProofsForUnit) + .ProcessAsync(ct); + + freshProofs.AddRange(proofs); + } + return freshProofs; + } + + private static List CreateBatch( + Mnemonic mnemonic, + KeysetId keysetId, + int batchNumber + ) + { + if (batchNumber < 0) + { + throw new ArgumentOutOfRangeException(nameof(batchNumber)); + } + var amounts = Enumerable.Repeat((ulong)0, (int)BATCH_SIZE).ToList(); + return mnemonic.DeriveOutputs(amounts, keysetId, (uint)(batchNumber * BATCH_SIZE)); + } + + private async Task> GrindKeyset( + KeysetId keysetId, + Mnemonic mnemonic, + ICounter counter, + ICashuApi api, + CancellationToken ct + ) + { + uint batchNumber = 0; + uint emptyBatchesRemaining = EMPTY_BATCHES_ALLOWED; + uint lastUsedCounter = 0; + List recoveredProofs = new List(); + + // don't care about invalid / non existent source keyset ids. let's fetch what we can + GetKeysResponse.KeysetItemResponse? keyset; + try + { + keyset = await _wallet.GetKeys(keysetId, true, false, ct); + } + catch + { + return []; + } + if (keyset == null) + { + return []; + } + + // proofs for keysetid are considered restored after 3 empty batches. + while (emptyBatchesRemaining > 0) + { + // create batch of 100, and request restore for whole batch + var outputs = CreateBatch(mnemonic, keysetId, (int)batchNumber); + var req = new PostRestoreRequest + { + Outputs = outputs.Select(o => o.BlindedMessage).ToArray(), + }; + var res = await api.Restore(req, ct); + + if (res.Signatures.Length == 0) + { + emptyBatchesRemaining--; + batchNumber++; + continue; + } + + // find last restored index of batch + uint lastUsedIndexInBatch = (uint) + outputs + .Select((o, i) => new { o, i }) + .Where(x => res.Outputs.Any(r => Equals(r.B_, x.o.BlindedMessage.B_))) + .MaxBy(x => x.i)! + .i; + + // set last used counter value for this batch + lastUsedCounter = BATCH_SIZE * batchNumber + lastUsedIndexInBatch; + + // bump batch number after calculating last used counter + batchNumber++; + + // if anything found, reset batches counter + emptyBatchesRemaining = EMPTY_BATCHES_ALLOWED; + + var returnedOutputs = new List(); + foreach (var output in res.Outputs) + { + // there can't be any dupes here + var matchingOutputs = outputs.SingleOrDefault(o => + Equals(o.BlindedMessage.B_, output.B_) + ); + if (matchingOutputs == null) + { + throw new InvalidOperationException("Invalid outputs returned by mint!"); + } + returnedOutputs.Add(matchingOutputs); + } + + var proofs = Utils.ConstructProofsFromPromises( + res.Signatures.ToList(), + returnedOutputs, + keyset.Keys + ); + recoveredProofs.AddRange(proofs); + } + + // 1 is added so we'll be consistent with counter usage. it will be ready for next use + await counter.SetCounter(keysetId, lastUsedCounter + 1, ct); + return recoveredProofs; + } + // in future it may be also usefult to add restore by binary search +} diff --git a/DotNut/Abstractions/SendResponse.cs b/DotNut/Abstractions/SendResponse.cs new file mode 100644 index 0000000..ff92f68 --- /dev/null +++ b/DotNut/Abstractions/SendResponse.cs @@ -0,0 +1,7 @@ +namespace DotNut.Abstractions; + +public class SendResponse +{ + public List Keep { get; set; } = new(); + public List Send { get; set; } = new(); +} diff --git a/DotNut/Abstractions/SwapBuilder.cs b/DotNut/Abstractions/SwapBuilder.cs new file mode 100644 index 0000000..d202cfc --- /dev/null +++ b/DotNut/Abstractions/SwapBuilder.cs @@ -0,0 +1,336 @@ +using System.Security.Cryptography; +using DotNut.ApiModels; + +namespace DotNut.Abstractions; + +/// +/// Receive operation builder implementation +/// +class SwapBuilder : ISwapBuilder +{ + private readonly Wallet _wallet; + + // input + private readonly string? _tokenString; + private readonly CashuToken? _token; + private List? _proofsToSwap; + + private List? _outputs; + private List? _amounts; + private KeysetId? _targetKeysetId; + + private string _unit = "sat"; + private bool _verifyDleq = true; + + private bool _includeFees = true; + + //nut10 stuff + private List? _privKeys; + private P2PkBuilder? _builder; + private string? _htlcPreimage; + private bool _shouldBlind = false; + + public SwapBuilder(Wallet wallet, string tokenString) + { + _wallet = wallet; + _tokenString = tokenString; + } + + public SwapBuilder(Wallet wallet, CashuToken token) + { + _wallet = wallet; + _token = token; + } + + public SwapBuilder(Wallet wallet) + { + _wallet = wallet; + } + + public ISwapBuilder WithUnit(string unit) + { + this._unit = unit; + return this; + } + + public ISwapBuilder FromInputs(IEnumerable proofs) + { + this._proofsToSwap = proofs.DeepCopyList(); + return this; + } + + public ISwapBuilder ForOutputs(IEnumerable outputs) + { + this._outputs = outputs.DeepCopyList(); + return this; + } + + public ISwapBuilder WithDLEQVerification(bool verify = true) + { + _verifyDleq = verify; + return this; + } + + public ISwapBuilder WithFeeCalculation(bool includeFees = true) + { + this._includeFees = includeFees; + return this; + } + + public ISwapBuilder WithAmounts(IEnumerable amounts) + { + _amounts = amounts.ToList(); + return this; + } + + public ISwapBuilder ForKeyset(KeysetId keysetId) + { + _targetKeysetId = keysetId; + return this; + } + + // when proofs were p2pk + public ISwapBuilder WithPrivkeys(IEnumerable privKeys) + { + this._privKeys = privKeys.ToList(); + return this; + } + + public ISwapBuilder ToP2PK(P2PkBuilder p2pkBuilder) + { + this._builder = p2pkBuilder; + return this; + } + + public ISwapBuilder WithHtlcPreimage(string preimage) + { + this._htlcPreimage = preimage; + return this; + } + + public ISwapBuilder ToHTLC(HTLCBuilder htlcBuilder) + { + this._builder = htlcBuilder; + return this; + } + + // P2Bk should be compatible with both p2pk and HTLC. Not implemented in the second one + public ISwapBuilder BlindPubkeys(bool withBlinding = true) + { + this._shouldBlind = withBlinding; + return this; + } + + public async Task> ProcessAsync(CancellationToken ct = default) + { + var mintApi = await _wallet.GetMintApi(ct); + + var swapInputs = _getSwapProofs(); + if (swapInputs == null || swapInputs.Count == 0) + { + throw new ArgumentException("Nothing to swap!"); + } + + // if there's no keysetId specified - let's choose it. + if (_targetKeysetId == null) + { + _targetKeysetId = + await _wallet.GetActiveKeysetId(this._unit, ct) + ?? throw new InvalidOperationException("Could not fetch Keyset ID"); + } + var keysForCurrentId = await _wallet.GetKeys(_targetKeysetId, true, false, ct); + + if (keysForCurrentId == null) + { + throw new InvalidOperationException($"Can't find keys for keyset {_targetKeysetId}"); + } + + if (_verifyDleq) + { + foreach (var proof in swapInputs) + { + if (proof.DLEQ == null) + { + throw new ArgumentNullException( + nameof(proof.DLEQ), + "Can't verify non-existent DLEQ proof!" + ); + } + // proof may be already inactive - make sure to fetch + var keyset = await _wallet.GetKeys(proof.Id, true, false, ct); + if (keyset == null) + { + throw new InvalidOperationException( + $"Can't find keys for keyset id {proof.Id}" + ); + } + + if (!keyset.Keys.TryGetValue(proof.Amount, out var key)) + { + throw new InvalidOperationException( + $"Can't find key for amount {proof.Amount} in keyset {keyset.Id}" + ); + } + var isValid = proof.Verify(key); + if (!isValid) + { + throw new InvalidOperationException( + $"Invalid proof signature for amount {proof.Amount}" + ); + } + } + } + + var fee = 0UL; + if (_includeFees) + { + // returns also non-active keysets. + var keysetsFees = (await _wallet.GetKeysets(false, ct)).ToDictionary( + k => k.Id, + k => k.InputFee ?? 0 + ); + fee = swapInputs.ComputeFee(keysetsFees); + } + + var total = Utils.SumProofs(swapInputs); + + this._amounts ??= this._getAmounts(total, fee, keysForCurrentId.Keys); + + // Swap received proofs to our keyset + var outputs = await this._getOutputs(keysForCurrentId.Keys, ct); + + Nut10Helper.MaybeProcessNut10(_privKeys ?? [], swapInputs, outputs, _htlcPreimage); + swapInputs.ForEach(i => i.StripFingerprints()); + var request = new PostSwapRequest() + { + Inputs = swapInputs.ToArray(), + Outputs = outputs.Select(o => o.BlindedMessage).ToArray(), + }; + + var swapResponse = await mintApi.Swap(request, ct); + + var swappedProofs = Utils.ConstructProofsFromPromises( + swapResponse.Signatures.ToList(), + outputs, + keysForCurrentId.Keys + ); + + return swappedProofs; + } + + private List _getSwapProofs() + { + _proofsToSwap ??= new(); + + if (_tokenString != null) + { + var token = CashuTokenHelper.Decode(this._tokenString, out var v); + ValidateSingleMint(token); + this._proofsToSwap.AddRange(token.Tokens.SelectMany(t => t.Proofs)); + } + + if (_token != null) + { + ValidateSingleMint(_token); + this._proofsToSwap.AddRange(_token.Tokens.SelectMany(t => t.Proofs)); + } + + return _proofsToSwap; + } + + private async Task> _getOutputs(Keyset keys, CancellationToken ct = default) + { + if (this._outputs != null) + { + if (this._builder is not null) + { + throw new ArgumentException( + "Can't create nut10 outputs by builder if outputs provided. Remove either p2pk builder parameter or outputs." + ); + } + return this._outputs; + } + + if (this._amounts is null) + { + throw new ArgumentNullException(nameof(_amounts), "Amounts can't be null."); + } + + var outputs = new List(); + if (this._builder is not null) + { + if (this._shouldBlind) + { + if (this._builder.SigFlag == "SIG_ALL") + { + var e = new PrivKey(RandomNumberGenerator.GetHexString(64)); + foreach (var amount in _amounts) + { + var builder = _builder.Clone(); + outputs.Add( + Utils.CreateNut10BlindedOutput( + amount, + this._targetKeysetId!, + builder, + e + ) + ); + } + return outputs; + } + foreach (var amount in _amounts) + { + var builder = _builder.Clone(); + outputs.Add( + Utils.CreateNut10BlindedOutput(amount, this._targetKeysetId!, builder) + ); + } + return outputs; + } + // skipped checks for keysetid and keys, since its validated before. make sure to remember about it. + foreach (var amount in _amounts) + { + var builder = _builder.Clone(); + outputs.Add(Utils.CreateNut10Output(amount, this._targetKeysetId!, builder)); + } + return outputs; + } + + return await _wallet.CreateOutputs(_amounts, this._targetKeysetId!, ct); + } + + private List _getAmounts(ulong total, ulong fee, Keyset keys) + { + if (_amounts != null) + { + var sum = checked(_amounts.Aggregate(0UL, (acc, val) => acc + val)); + + if (checked(sum + fee) == total) + { + return _amounts; + } + if (sum + fee < total) + { + var underpay = checked(total - fee - sum); + this._amounts.AddRange(Utils.SplitToProofsAmounts(underpay, keys)); + return this._amounts; + } + + throw new ArgumentException( + $"Invalid amounts requested. Sum of amounts: {sum}, total input: {total}, fee:{fee}." + ); + } + + this._amounts = Utils.SplitToProofsAmounts(checked(total - fee), keys); + return this._amounts; + } + + private static void ValidateSingleMint(CashuToken token) + { + var distinctMints = token.Tokens.Select(t => t.Mint).Distinct().ToList(); + if (distinctMints.Count > 1) + { + throw new ArgumentException("Only swap from single mint is allowed"); + } + } +} diff --git a/DotNut/Abstractions/Utils.cs b/DotNut/Abstractions/Utils.cs new file mode 100644 index 0000000..9349cbb --- /dev/null +++ b/DotNut/Abstractions/Utils.cs @@ -0,0 +1,401 @@ +using System.Security.Cryptography; +using System.Text.Json; +using DotNut.NUT13; + +namespace DotNut.Abstractions; + +public static class Utils +{ + /// + /// Function mapping payment amount to keyset supported amounts in order to create swap payload. Always tries to fit the biggest proof. + /// + /// Amount that has to be covered. + /// Mints keyset> + /// List of ulong proof amounts for given keyset + public static List SplitToProofsAmounts(ulong paymentAmount, Keyset keyset) + { + var outputAmounts = new List(); + var possibleValues = keyset.Keys.OrderByDescending(x => x).ToList(); + foreach (var value in possibleValues) + { + while (paymentAmount >= value) + { + outputAmounts.Add(value); + paymentAmount -= value; + } + + if (paymentAmount == 0) + { + break; + } + } + + return outputAmounts; + } + + /// + /// Creates blank outputs (see nut-08) + /// + /// Amount that blank outputs have to cover + /// Active keyset id which will sign outputs + /// Keys for given KeysetId + /// Bip39 mnemonic for Nut13 deterministic secret derivation + /// Nut13 counter, for current keysetId. + /// Blank Outputs + public static List CreateBlankOutputs( + ulong amount, + KeysetId keysetId, + Keyset keys, + NBitcoin.BIP39.Mnemonic? mnemonic = null, + uint? counter = null + ) + { + if (amount == 0) + { + throw new ArgumentException("Cannot create blank outputs zero amount."); + } + + var count = CalculateNumberOfBlankOutputs(amount); + + // Amount is set for 1, they're blank. Mint will automatically set their amount and sign each by pk corresponding to value + var amounts = Enumerable.Repeat((ulong)1, count).ToList(); + return CreateOutputs(amounts, keysetId, keys, mnemonic, counter); + } + + /// + /// Calculates amount of blank outputs needed by mint to return overpaid fees + /// + /// Amount of tokens that has to be covered by mint. + /// Integer amount of blank outputs needed + public static int CalculateNumberOfBlankOutputs(ulong amountToCover) + { + if (amountToCover == 0) + { + return 0; + } + + return Math.Max(Convert.ToInt32(Math.Ceiling(Math.Log2(amountToCover))), 1); + } + + /// + /// Creates outputs (secrets, proof messages and blinding factors). Outputs should have valid amounts. + /// + /// Amounts for each output (e.g. [1,2,4,8] + /// ID of keyset we want to receive the proofs + /// Keyset for given ID + /// + /// + public static List CreateOutputs( + IEnumerable amounts, + KeysetId keysetId, + Keyset keys, + NBitcoin.BIP39.Mnemonic? mnemonic = null, + uint? counter = null + ) + { + var amountsList = amounts as IReadOnlyList ?? amounts.ToList(); + + if (amountsList.Any(a => !keys.Keys.Contains(a))) + throw new ArgumentException("Invalid amounts"); + + var outputs = new List(amountsList.Count); + + if (mnemonic is not null && counter is { } c) + { + for (uint i = 0; i < amountsList.Count; i++) + { + var secret = mnemonic.DeriveSecret(keysetId, c + i); + var r = new PrivKey(mnemonic.DeriveBlindingFactor(keysetId, c + i)); + var B_ = Cashu.ComputeB_(secret.ToCurve(), r); + var output = new OutputData + { + BlindedMessage = new BlindedMessage + { + Amount = amountsList[(int)i], + B_ = B_, + Id = keysetId, + }, + BlindingFactor = r, + Secret = secret, + }; + outputs.Add(output); + } + return outputs; + } + + foreach (var amount in amountsList) + { + var secret = RandomSecret(); + var r = RandomPrivkey(); + var B_ = Cashu.ComputeB_(secret.ToCurve(), r); + var output = new OutputData + { + BlindedMessage = new BlindedMessage + { + Amount = amount, + B_ = B_, + Id = keysetId, + }, + BlindingFactor = r, + Secret = secret, + }; + outputs.Add(output); + } + return outputs; + } + + /// + /// Create P2Pk / HTLC outputs. + /// + /// + /// + /// + /// + public static OutputData CreateNut10Output(ulong amount, KeysetId keysetId, P2PkBuilder builder) + { + // ugliest hack ever + Nut10Secret secret; + if (builder is HTLCBuilder htlc) + { + secret = new Nut10Secret("HTLC", htlc.Build()); + } + else + { + secret = new Nut10Secret("P2PK", builder.Build()); + } + + var r = RandomPrivkey(); + var B_ = Cashu.ComputeB_(secret.ToCurve(), r); + return new OutputData + { + BlindedMessage = new BlindedMessage() + { + Amount = amount, + B_ = B_, + Id = keysetId, + }, + BlindingFactor = r, + Secret = secret, + }; + } + + /// + /// Creates P2Pk / HTLC Blinded Outputs + /// + /// + /// + /// + /// + public static OutputData CreateNut10BlindedOutput( + ulong amount, + KeysetId keysetId, + P2PkBuilder builder + ) + { + Nut10Secret secret; + PubKey E; + if (builder is HTLCBuilder htlc) + { + secret = new Nut10Secret("HTLC", htlc.BuildBlinded(out var e)); + E = e; + } + else + { + secret = new Nut10Secret("P2PK", builder.BuildBlinded(out var e)); + E = e; + } + + var r = RandomPrivkey(); + var B_ = Cashu.ComputeB_(secret.ToCurve(), r); + return new OutputData + { + BlindedMessage = new BlindedMessage() + { + Amount = amount, + B_ = B_, + Id = keysetId, + }, + BlindingFactor = r, + Secret = secret, + P2BkE = E, + }; + } + + /// + /// Creates P2Pk / HTLC Blinded Outputs with specified ephemeral sender keypair. + /// + /// + /// + /// + /// + public static OutputData CreateNut10BlindedOutput( + ulong amount, + KeysetId keysetId, + P2PkBuilder builder, + PrivKey e + ) + { + Nut10Secret secret; + if (builder is HTLCBuilder htlc) + { + secret = new Nut10Secret("HTLC", htlc.BuildBlinded(e)); + } + else + { + secret = new Nut10Secret("P2PK", builder.BuildBlinded(e)); + } + + var r = RandomPrivkey(); + var B_ = Cashu.ComputeB_(secret.ToCurve(), r); + return new OutputData + { + BlindedMessage = new BlindedMessage() + { + Amount = amount, + B_ = B_, + Id = keysetId, + }, + BlindingFactor = r, + Secret = secret, + P2BkE = e.Key.CreatePubKey(), + }; + } + + /// + /// Method creating proofs, from provided promises (blinded signatures) + /// + /// Blinded Signature + /// Blinding factor + /// Yeah, secret + /// Key, corresponding to proof amount + /// Valid proof + public static Proof ConstructProofFromPromise( + BlindSignature promise, + PrivKey r, + ISecret secret, + PubKey amountPubkey, + PubKey? P2PkE = null + ) + { + //unblind signature + var C = Cashu.ComputeC(promise.C_, r, amountPubkey); + + DLEQProof? dleq = null; + + var proof = new Proof + { + Id = promise.Id, + Amount = promise.Amount, + Secret = secret, + C = C, + P2PkE = P2PkE, + }; + + if (promise.DLEQ is null) + { + return proof; + } + + proof.DLEQ = new DLEQProof + { + E = promise.DLEQ.E, + S = promise.DLEQ.S, + R = r.Key.Clone(), + }; + if (!proof.Verify(amountPubkey)) + { + throw new InvalidOperationException($"Could not verify mint signature on proof"); + } + return proof; + } + + public static List ConstructProofsFromPromises( + IEnumerable promises, + IEnumerable outputs, + Keyset keys + ) + { + var bs = promises as IReadOnlyList ?? promises.ToList(); + var os = outputs as IReadOnlyList ?? outputs.ToList(); + if (os.Count < bs.Count) + { + throw new ArgumentException("Outputs must as least equal amount of elements!"); + } + + List proofs = new List(bs.Count); + for (int i = 0; i < bs.Count; i++) + { + if (!keys.TryGetValue(bs[i].Amount, out var key)) + { + throw new ArgumentException( + $"Provided keyset doesn't contain PubKey for amount {bs[i].Amount}" + ); + } + + var proof = ConstructProofFromPromise( + bs[i], + os[i].BlindingFactor, + os[i].Secret, + key, + os[i].P2BkE + ); + proofs.Add(proof); + } + return proofs; + } + + public static ulong SumProofs(List proofs) + { + return proofs.Aggregate(0UL, (current, proof) => checked(current + proof.Amount)); + } + + public static ISecret RandomSecret() + { + var bytes = RandomNumberGenerator.GetBytes(32); + return new StringSecret(Convert.ToHexString(bytes)); + } + + public static PrivKey RandomPrivkey() + { + var bytes = RandomNumberGenerator.GetBytes(32); + return new PrivKey(Convert.ToHexString(bytes)); + } + + /// + /// Should be called before every interaction with mint. Strips info that could fingerprint user. + /// It mustn't be called before sending token to someone - may make it unspendable. + /// + /// Proofs to clean + public static void StripFingerprints(this Proof proof) + { + if (proof.DLEQ != null) + { + proof.DLEQ = null; + } + proof.P2PkE = null; + } + + /// + /// Create deep copy of the object, so original one won't get mutated by reference. + /// + /// Object to clone + /// Object type + /// Deep copy of the object + public static T DeepCopy(this T obj) + where T : class + { + return JsonSerializer.Deserialize(JsonSerializer.Serialize(obj))!; + } + + /// + /// Create deep copy of the list + /// + /// + /// + /// + public static List DeepCopyList(this IEnumerable list) + where T : class + { + return list.Select(item => item.DeepCopy()).ToList(); + } +} diff --git a/DotNut/Abstractions/Wallet.cs b/DotNut/Abstractions/Wallet.cs new file mode 100644 index 0000000..64da6ab --- /dev/null +++ b/DotNut/Abstractions/Wallet.cs @@ -0,0 +1,582 @@ +using DotNut.Api; +using DotNut.ApiModels; +using DotNut.NBitcoin.BIP39; + +namespace DotNut.Abstractions; + +/// +/// Main Cashu Wallet class implementing fluent builder pattern +/// +/// +public class Wallet : IWalletBuilder +{ + private MintInfo? _info; + private IProofSelector? _selector; + private ICashuApi? _mintApi; + private List _keysets = []; + private List _keys = []; + private Dictionary _keysetFees => + _keysets.ToDictionary(k => k.Id, k => k.InputFee ?? 0); + private Mnemonic? _mnemonic; + private ICounter? _counter; + + private IWebsocketService? _wsService; + + //flags + private bool _shouldSyncKeyset = true; + private DateTime _lastSync = DateTime.MinValue; + private TimeSpan? _syncThreshold; // if null sync only once + private bool _shouldBumpCounter = true; + private bool _ownsHttpClient = false; + + /* + * Fluent Builder Methods + */ + public static IWalletBuilder Create() => new Wallet(); + + public IWalletBuilder WithMint(ICashuApi mintApi, bool canDispose = false) + { + _mintApi = mintApi; + _ownsHttpClient = canDispose; + return this; + } + + public IWalletBuilder WithMint(string mintUrl) + { + //add trailing / so mint like https://mint.minibits.cash/Bitcoin will work correctly + var mintUri = new Uri(mintUrl + "/"); + var httpClient = new HttpClient { BaseAddress = mintUri }; + _mintApi = new CashuHttpClient(httpClient, true); + _ownsHttpClient = true; + return this; + } + + public IWalletBuilder WithMint(Uri mintUri) + { + var httpClient = new HttpClient { BaseAddress = mintUri }; + _mintApi = new CashuHttpClient(httpClient, true); + _ownsHttpClient = true; + return this; + } + + public IWalletBuilder WithInfo(MintInfo info) + { + this._info = info; + return this; + } + + public IWalletBuilder WithInfo(GetInfoResponse info) => this.WithInfo(new MintInfo(info)); + + public IWalletBuilder WithKeysets(IEnumerable keysets) + { + this._keysets = keysets.ToList(); + return this; + } + + public IWalletBuilder WithKeysets(GetKeysetsResponse keysets) => + this.WithKeysets(keysets.Keysets.ToList()); + + public IWalletBuilder WithKeys(IEnumerable keys) + { + this._keys = keys.ToList(); + return this; + } + + public IWalletBuilder WithKeys(GetKeysResponse keys) => this.WithKeys(keys.Keysets.ToList()); + + public IWalletBuilder WithKeysetSync(bool syncKeyset = true) + { + this._shouldSyncKeyset = syncKeyset; + return this; + } + + public IWalletBuilder WithKeysetSync(bool syncKeyset, TimeSpan syncThreshold) + { + this._shouldSyncKeyset = syncKeyset; + this._syncThreshold = syncThreshold; + return this; + } + + public IWalletBuilder WithSelector(IProofSelector selector) + { + _selector = selector; + return this; + } + + public IWalletBuilder WithMnemonic(Mnemonic mnemonic) + { + _mnemonic = mnemonic; + return this; + } + + public IWalletBuilder WithMnemonic(string mnemonic) + { + _mnemonic = new Mnemonic(mnemonic); + return this; + } + + public IWalletBuilder WithCounter(ICounter counter) + { + this._counter = counter; + return this; + } + + public IWalletBuilder WithCounter(IDictionary counter) + { + this._counter = new InMemoryCounter(counter); + return this; + } + + public IWalletBuilder ShouldBumpCounter(bool shouldBumpCounter = true) + { + this._shouldBumpCounter = shouldBumpCounter; + return this; + } + + public IWalletBuilder WithWebsocketService(IWebsocketService websocketService) + { + this._wsService = websocketService; + return this; + } + + /* + * Main api methods + */ + public IMintQuoteBuilder CreateMintQuote() + { + _ensureApiConnected(); + return new MintQuoteBuilder(this); + } + + public ISwapBuilder Swap() + { + _ensureApiConnected(); + return new SwapBuilder(this); + } + + public IMeltQuoteBuilder CreateMeltQuote() + { + _ensureApiConnected(); + return new MeltQuoteBuilder(this); + } + + public async Task CheckState( + IEnumerable proofs, + CancellationToken ct = default + ) + { + // no need for striping DLEQ r, or p2pkE, since only Ys are being sent. + return await CheckState(proofs.Select(p => (PubKey)p.Secret.ToCurve()), ct); + } + + public async Task CheckState( + IEnumerable Ys, + CancellationToken ct = default + ) + { + _ensureApiConnected(); + var req = new PostCheckStateRequest() { Ys = Ys.Select(y => y.ToString()).ToArray() }; + return await _mintApi!.CheckState(req, ct); + } + + public IRestoreBuilder Restore() + { + _ensureApiConnected(); + return new RestoreBuilder(this); + } + + /* + * Public Mint utils + */ + + public void InvalidateCache() + { + _lastSync = DateTime.MinValue; + } + + public async Task GetActiveKeysetId(string unit, CancellationToken ct = default) + { + await _maybeSyncKeys(ct); + return _keysets + .OrderBy(k => k.InputFee) + .FirstOrDefault(k => k is { Active: true } && k.Unit == unit, null) + ?.Id; + } + + public async Task>> GetKeysetIdsWithUnits( + CancellationToken ct = default + ) + { + await _maybeSyncKeys(ct); + return _keysets + .GroupBy(k => k.Unit) + .ToDictionary(g => g.Key, g => g.OrderBy(k => k.InputFee).Select(k => k.Id).ToList()); + } + + public async Task> GetActiveKeysetIdsWithUnits( + CancellationToken ct = default + ) + { + await _maybeSyncKeys(ct); + return _keysets + .Where(k => k.Active) + .GroupBy(k => k.Unit) + .ToDictionary(g => g.Key, g => g.OrderBy(k => k.InputFee).First().Id); + } + + public async Task> GetKeys( + bool forceRefresh = false, + CancellationToken ct = default + ) + { + if (forceRefresh) + { + this._keys = await _fetchKeys(ct); + return this._keys; + } + await _maybeSyncKeys(ct); + return this._keys; + } + + public async Task GetKeys( + KeysetId id, + bool allowFetch = true, + bool forceRefresh = false, + CancellationToken ct = default + ) + { + if (forceRefresh) + { + return await _fetchKeys(id, ct); + } + + var localKeyset = this._keys.SingleOrDefault(k => k.Id == id); + if (localKeyset != null) + { + return localKeyset; + } + + if (!allowFetch) + { + return null; + } + + var keyset = await _fetchKeys(id, ct); + if (keyset != null) + { + _keys.Add(keyset); + } + + return keyset; + } + + public async Task> GetKeysets( + bool forceRefresh = false, + CancellationToken ct = default + ) + { + if (forceRefresh) + { + this._keysets = await _fetchKeysets(ct); + return _keysets; + } + await _maybeSyncKeys(ct); + return _keysets; + } + + public async Task GetInfo(bool forceRefresh = false, CancellationToken ct = default) + { + if (forceRefresh) + { + return await _fetchMintInfo(ct); + } + return await _lazyFetchMintInfo(ct); + } + + public async Task> CreateOutputs( + IEnumerable amounts, + KeysetId id, + CancellationToken ct = default + ) + { + var amountsList = amounts as IReadOnlyList ?? amounts.ToList(); + await _maybeSyncKeys(ct); + if (this._keys.Count == 0) + { + throw new ArgumentException( + "No Keys found. Make sure to fetch them!", + nameof(this._keys) + ); + } + var keyset = this._keys.SingleOrDefault(k => k.Id == id); + if (keyset == null) + { + throw new ArgumentNullException(nameof(keyset), $"No matching keys for id {id}"); + } + if (this._mnemonic == null) + { + return Utils.CreateOutputs(amountsList, id, keyset.Keys); + } + + if (this._counter == null) + { + throw new ArgumentNullException( + nameof(ICounter), + "Can't derive outputs without keyset counter" + ); + } + + if (!_shouldBumpCounter) + { + var counterValue = await this._counter.GetCounterForId(id, ct); + return Utils.CreateOutputs(amountsList, id, keyset.Keys, this._mnemonic, counterValue); + } + + var (old, @new) = await this._counter.FetchAndIncrement(id, (uint)amountsList.Count, ct); + return Utils.CreateOutputs(amountsList, id, keyset.Keys, this._mnemonic, old); + } + + public async Task> CreateOutputs( + IEnumerable amounts, + string unit, + CancellationToken ct = default + ) + { + var amountsList = amounts as IReadOnlyList ?? amounts.ToList(); + var keysetId = await this.GetActiveKeysetId(unit, ct); + if (keysetId == null) + { + throw new ArgumentNullException(nameof(keysetId)); + } + return await this.CreateOutputs(amountsList, keysetId, ct); + } + + public async Task SelectProofsToSend( + IEnumerable proofs, + ulong amount, + bool includeFees, + CancellationToken ct = default + ) + { + if (this._selector == null) + { + await _maybeSyncKeys(ct); + if (this._keysetFees.Count == 0) + { + throw new ArgumentException("No keyset fees found", nameof(this._keysetFees)); + } + this._selector = new ProofSelector(this._keysetFees); + } + + return await _selector.SelectProofsToSend(proofs, amount, includeFees, ct); + } + + public async Task GetMintApi(CancellationToken ct = default) + { + _ensureApiConnected(); + return _mintApi!; + } + + public async Task GetSelector(CancellationToken ct = default) + { + if (this._selector == null) + { + await _maybeSyncKeys(ct); + if (this._keysetFees.Count == 0) + { + throw new ArgumentException("No keyset fees found", nameof(this._keysetFees)); + } + this._selector = new ProofSelector(this._keysetFees); + } + return this._selector; + } + + public async Task GetWebsocketService(CancellationToken ct = default) + { + return this._wsService ??= new WebsocketService(); + } + + public Mnemonic? GetMnemonic() => _mnemonic; + + public ICounter? GetCounter() => _counter; + + /* + * Private helpers + */ + + /// + /// Throws exception if api not connected + /// + /// + /// + internal void _ensureApiConnected(string? msg = null) + { + if (_mintApi != null) + { + return; + } + + if (msg is not null) + { + throw new ArgumentNullException(nameof(this._mintApi), msg); + } + + throw new ArgumentNullException(nameof(this._mintApi)); + } + + /// + /// Wrapper for GetKeysets api endpoint. Formats Keysets to list. + /// + /// List of Keysets + /// May be thrown if mint is not set. + private async Task> _fetchKeysets( + CancellationToken ct = default + ) + { + _ensureApiConnected("Can't fetch keysets without mint api!"); + var keysetsRaw = await _mintApi!.GetKeysets(ct); + return keysetsRaw.Keysets.ToList(); + } + + /// + /// Wrapper for GetKeys api endpoint. Validates returned KeysetIds and formats Keys to list. + /// + /// List of Keys (lists :)) + /// May be thrown if mint is not set. + /// May be thrown if mint returns invalid keysetId for at least one Keyset + private async Task> _fetchKeys( + CancellationToken ct = default + ) + { + _ensureApiConnected("Can't fetch keys without mint api!"); + var keysRaw = await _mintApi!.GetKeys(ct); + foreach (var keysetItemResponse in keysRaw.Keysets) + { + var isKeysetIdValid = keysetItemResponse.Keys.VerifyKeysetId( + keysetItemResponse.Id, + keysetItemResponse.Unit, + keysetItemResponse.InputFeePpk, + keysetItemResponse.FinalExpiry + ); + if (!isKeysetIdValid) + { + throw new ArgumentException( + $"Mint provided invalid keysetId. Provided: {keysetItemResponse.Id}, derived: {keysetItemResponse.Keys.GetKeysetId()} " + ); + } + } + return keysRaw.Keysets.ToList(); + } + + /// + /// Wrapper for GetKeys api endpoint. Validates KeysetId and fetches keys for single KeysetId Formats Keys to list. + /// + /// KeysetId we want fetch keys for. + /// Keys + /// May be thrown if mint returns invalid keysetId for at least one Keyset + /// May be thrown if mint is not set. + private async Task _fetchKeys( + KeysetId id, + CancellationToken ct = default + ) + { + _ensureApiConnected("Can't fetch keys without mint api!"); + var keysRaw = (await _mintApi!.GetKeys(id, ct)).Keysets.SingleOrDefault(); + if (keysRaw == null) + { + return null; + } + var isKeysetIdValid = keysRaw.Keys.VerifyKeysetId( + keysRaw.Id, + keysRaw.Unit, + keysRaw.InputFeePpk, + keysRaw.FinalExpiry + ); + if (!isKeysetIdValid) + { + throw new ArgumentException( + $"Mint provided invalid keysetId. Provided: {keysRaw.Id}, derived: {keysRaw.Keys.GetKeysetId()} " + ); + } + return keysRaw; + } + + /// + /// Wrapper for GetInfo api endpoint. Translates Payload to MintInfo. + /// + /// May be thrown if mint is not set. + private async Task _fetchMintInfo(CancellationToken cts = default) + { + _ensureApiConnected("Can't fetch mint info without mint api!"); + var infoRaw = await _mintApi!.GetInfo(cts); + return new MintInfo(infoRaw); + } + + /// + /// Fetches mint info if not present in CashuWallet. + /// + /// + private async Task _lazyFetchMintInfo(CancellationToken cts = default) + { + if (this._info != null) + return this._info; + return await this._fetchMintInfo(cts); + } + + /// + /// Local Keys sync. Will fetch _all_ keys if more than 2 unknown keysets are returned. + /// Doesn't sync fetch non-active keys. If you want to fetch keys for inactive keyset, you will need to use GetKeys. + /// + /// + /// + internal async Task _maybeSyncKeys(CancellationToken cts = default) + { + if (!_shouldSyncKeyset) + { + return; + } + + switch (_syncThreshold) + { + // should sync keysets SINGLE time in the lifespan of object. If already synced - return; + case null when _lastSync != DateTime.MinValue: + // should sync keysets in some timepsan + case { } threshold when _lastSync + threshold >= DateTime.UtcNow: + return; + } + + this._keysets = await _fetchKeysets(cts); + if (_keys.Count == 0) + { + this._keys = await _fetchKeys(cts); // we're fetching all keys here, so no need for additional check. + return; + } + + var knownIds = _keys.Select(key => key.Id).ToHashSet(); + var unknownKeysets = _keysets.Where(k => !knownIds.Contains(k.Id) && k.Active).ToList(); + if (unknownKeysets.Count > 2) // just make a single request. May override stored keys. + { + this._keys = await _fetchKeys(cts); + return; + } + + foreach (var unknownKeyset in unknownKeysets) + { + var keyset = await this._fetchKeys(unknownKeyset.Id, cts); + if (keyset != null) + { + _keys.Add(keyset); + } + } + + _lastSync = DateTime.UtcNow; + } + + public void Dispose() + { + if (_ownsHttpClient) + { + _mintApi?.Dispose(); + } + } +} diff --git a/DotNut/Abstractions/Websockets/NotificationParser.cs b/DotNut/Abstractions/Websockets/NotificationParser.cs new file mode 100644 index 0000000..143e963 --- /dev/null +++ b/DotNut/Abstractions/Websockets/NotificationParser.cs @@ -0,0 +1,51 @@ +using System.Text.Json; +using DotNut.ApiModels; + +namespace DotNut.Abstractions.Websockets; + +public static class NotificationParser +{ + public static object? ParsePayload( + WsNotification notification, + SubscriptionKind subscriptionKind + ) + { + if (notification.Params.Payload == null) + return null; + + var jsonElement = (JsonElement)notification.Params.Payload; + + return subscriptionKind switch + { + SubscriptionKind.Bolt11MintQuote => + jsonElement.Deserialize(), + SubscriptionKind.Bolt11MeltQuote => + jsonElement.Deserialize(), + SubscriptionKind.ProofState => jsonElement.Deserialize(), + _ => notification.Params.Payload, + }; + } + + public static T? ParsePayload(WsNotification notification) + where T : class + { + if (notification.Params.Payload == null) + return null; + + var jsonElement = (JsonElement)notification.Params.Payload; + return jsonElement.Deserialize(); + } + + public static bool IsPayloadOfType(WsNotification notification) + where T : class + { + try + { + return ParsePayload(notification) != null; + } + catch + { + return false; + } + } +} diff --git a/DotNut/Abstractions/Websockets/Subscription.cs b/DotNut/Abstractions/Websockets/Subscription.cs new file mode 100644 index 0000000..f9a8904 --- /dev/null +++ b/DotNut/Abstractions/Websockets/Subscription.cs @@ -0,0 +1,76 @@ +using System.Runtime.CompilerServices; +using System.Threading.Channels; + +namespace DotNut.Abstractions.Websockets; + +public class Subscription : IAsyncDisposable +{ + public string Id { get; init; } = string.Empty; + public string ConnectionId { get; init; } = string.Empty; + public SubscriptionKind Kind { get; init; } + public string[] Filters { get; init; } = Array.Empty(); + public DateTime CreatedAt { get; init; } = DateTime.UtcNow; + public Channel NotificationChannel { get; init; } = + Channel.CreateUnbounded(); + + /// + /// Indicates whether the subscription is still active (channel not completed). + /// + public bool IsActive => !_isClosed; + + private volatile bool _isClosed; + private readonly WeakReference? _serviceRef; + + public Subscription(IWebsocketService? websocketService = null) + { + _serviceRef = + websocketService != null + ? new WeakReference(websocketService) + : null; + } + + /// + /// Reads all notifications as an async stream. Completes when the subscription is closed. + /// + public async IAsyncEnumerable ReadAllAsync( + [EnumeratorCancellation] CancellationToken ct = default + ) + { + await foreach (var msg in NotificationChannel.Reader.ReadAllAsync(ct)) + { + yield return msg; + } + } + + /// + /// Closes the subscription and sends unsubscribe request to the server. + /// + public async Task CloseAsync() + { + if (_isClosed) + return; + _isClosed = true; + + NotificationChannel.Writer.TryComplete(); + if (_serviceRef != null && _serviceRef.TryGetTarget(out var service)) + { + await service.UnsubscribeAsync(Id); + } + } + + /// + /// Internal close - only closes the channel without server notification. + /// Used when connection is already closed or during cleanup. + /// + internal Task CloseInternalAsync() + { + _isClosed = true; + NotificationChannel.Writer.TryComplete(); + return Task.CompletedTask; + } + + public async ValueTask DisposeAsync() + { + await CloseAsync(); + } +} diff --git a/DotNut/Abstractions/Websockets/WebsocketConnection.cs b/DotNut/Abstractions/Websockets/WebsocketConnection.cs new file mode 100644 index 0000000..e52d51d --- /dev/null +++ b/DotNut/Abstractions/Websockets/WebsocketConnection.cs @@ -0,0 +1,53 @@ +using System.Net.WebSockets; + +namespace DotNut.Abstractions.Websockets; + +public class WebsocketConnection : IDisposable +{ + public string Id { get; set; } = string.Empty; + public string MintUrl { get; set; } = string.Empty; + public ClientWebSocket WebSocket { get; set; } = new(); + public WebSocketState State { get; set; } + public DateTime ConnectedAt { get; set; } = DateTime.UtcNow; + public CancellationTokenSource? CancellationTokenSource { get; set; } + + public DateTime? LastPingSent { get; set; } + public DateTime? LastMessageReceived { get; set; } + public int ReconnectAttempts { get; set; } + + public bool Equals(WebsocketConnection? other) + { + if (other is null) + return false; + if (ReferenceEquals(this, other)) + return true; + return string.Equals(MintUrl, other.MintUrl, StringComparison.OrdinalIgnoreCase); + } + + public override bool Equals(object? obj) + { + return obj is WebsocketConnection other && Equals(other); + } + + public override int GetHashCode() + { + return MintUrl?.GetHashCode(StringComparison.OrdinalIgnoreCase) ?? 0; + } + + public static bool operator ==(WebsocketConnection? left, WebsocketConnection? right) + { + return object.Equals(left, right); + } + + public static bool operator !=(WebsocketConnection? left, WebsocketConnection? right) + { + return !object.Equals(left, right); + } + + public void Dispose() + { + CancellationTokenSource?.Cancel(); + CancellationTokenSource?.Dispose(); + WebSocket?.Dispose(); + } +} diff --git a/DotNut/Abstractions/Websockets/WebsocketEnums.cs b/DotNut/Abstractions/Websockets/WebsocketEnums.cs new file mode 100644 index 0000000..cd34e03 --- /dev/null +++ b/DotNut/Abstractions/Websockets/WebsocketEnums.cs @@ -0,0 +1,16 @@ +namespace DotNut.Abstractions.Websockets; + +public enum SubscriptionKind +{ + Bolt11MeltQuote, + Bolt11MintQuote, + Bolt12MeltQuote, + Bolt12MintQuote, + ProofState, +} + +public enum WsRequestMethod +{ + Subscribe, + Unsubscribe, +} diff --git a/DotNut/Abstractions/Websockets/WebsocketEvents.cs b/DotNut/Abstractions/Websockets/WebsocketEvents.cs new file mode 100644 index 0000000..62b8480 --- /dev/null +++ b/DotNut/Abstractions/Websockets/WebsocketEvents.cs @@ -0,0 +1,15 @@ +using System.Net.WebSockets; + +namespace DotNut.Abstractions.Websockets; + +public class NotificationEventArgs : EventArgs +{ + public string ConnectionId { get; set; } = string.Empty; + public WsNotification Notification { get; set; } = new(); +} + +public class ConnectionStateChangedEventArgs : EventArgs +{ + public string ConnectionId { get; set; } = string.Empty; + public WebSocketState State { get; set; } +} diff --git a/DotNut/Abstractions/Websockets/WebsocketModels.cs b/DotNut/Abstractions/Websockets/WebsocketModels.cs new file mode 100644 index 0000000..059fc86 --- /dev/null +++ b/DotNut/Abstractions/Websockets/WebsocketModels.cs @@ -0,0 +1,118 @@ +using System.Text.Json.Serialization; + +namespace DotNut.Abstractions.Websockets; + +public class WsRequest +{ + [JsonPropertyName("jsonrpc")] + public string JsonRpc { get; set; } = "2.0"; + + [JsonPropertyName("method")] + public WsRequestMethod Method { get; set; } + + [JsonPropertyName("params")] + public WsRequestParams Params { get; set; } = new(); + + [JsonPropertyName("id")] + public int Id { get; set; } +} + +public class WsRequestParams +{ + [JsonPropertyName("kind")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public SubscriptionKind? Kind { get; set; } + + [JsonPropertyName("subId")] + public string SubId { get; set; } = string.Empty; + + [JsonPropertyName("filters")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public string[]? Filters { get; set; } +} + +public class WsResponse +{ + [JsonPropertyName("jsonrpc")] + public string JsonRpc { get; } = "2.0"; + + [JsonPropertyName("result")] + public WsResult Result { get; set; } = new(); + + [JsonPropertyName("id")] + public int Id { get; set; } +} + +public class WsResult +{ + [JsonPropertyName("status")] + public string Status { get; } = "OK"; + + [JsonPropertyName("subId")] + public string SubId { get; set; } = string.Empty; +} + +public class WsError +{ + [JsonPropertyName("jsonrpc")] + public string JsonRpc { get; } = "2.0"; + + [JsonPropertyName("error")] + public WsErrorDetails Error { get; set; } = new(); + + [JsonPropertyName("id")] + public int Id { get; set; } +} + +public class WsErrorDetails +{ + [JsonPropertyName("code")] + public int Code { get; set; } + + [JsonPropertyName("message")] + public string Message { get; set; } = string.Empty; +} + +public class WsNotification +{ + [JsonPropertyName("jsonrpc")] + public string JsonRpc { get; } = "2.0"; + + [JsonPropertyName("method")] + public string Method { get; } = "subscribe"; + + [JsonPropertyName("params")] + public WsNotificationParams Params { get; set; } = new(); +} + +public class WsNotificationParams +{ + [JsonPropertyName("subId")] + public string SubId { get; set; } = string.Empty; + + [JsonPropertyName("payload")] + public object? Payload { get; set; } +} + +public abstract record WsMessage +{ + public sealed record Response(WsResponse Value) : WsMessage; + + public sealed record Error(WsError Value) : WsMessage; + + public sealed record Notification(WsNotification Value) : WsMessage; +} + +public abstract record RequestResult +{ + public sealed record Success(string SubId, string Status) : RequestResult; + + public sealed record Failure(int Code, string Message, int RequestId) : RequestResult; +} + +internal class PendingRequest +{ + public required TaskCompletionSource Tcs { get; set; } + public required string SubscriptionId { get; set; } + public DateTime CreatedAt { get; set; } = DateTime.UtcNow; +} diff --git a/DotNut/Abstractions/Websockets/WebsocketService.cs b/DotNut/Abstractions/Websockets/WebsocketService.cs new file mode 100644 index 0000000..3656050 --- /dev/null +++ b/DotNut/Abstractions/Websockets/WebsocketService.cs @@ -0,0 +1,782 @@ +using System.Buffers; +using System.Collections.Concurrent; +using System.Net.WebSockets; +using System.Text; +using System.Text.Json; +using System.Text.Json.Serialization; +using System.Threading.Channels; +using DotNut.Abstractions.Websockets; + +namespace DotNut.Abstractions; + +public class WebsocketService : IWebsocketService +{ + private readonly ConcurrentDictionary _connections = new(); + private readonly ConcurrentDictionary _subscriptions = new(); + private readonly ConcurrentDictionary _pendingRequests = new(); + private readonly ConcurrentDictionary _connectionLocks = new(); + private readonly ConcurrentDictionary _subscriptionInfos = new(); + + private readonly WebsocketServiceOptions _options; + private readonly CancellationTokenSource _disposeCts = new(); + + private int _nextRequestId; + private volatile bool _disposed; + + private static readonly JsonSerializerOptions JsonOptions = new() + { + DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, + Converters = { new JsonStringEnumConverter(JsonNamingPolicy.SnakeCaseLower) }, + }; + + public event EventHandler? ConnectionStateChanged; + + public WebsocketService() + : this(new WebsocketServiceOptions()) { } + + public WebsocketService(WebsocketServiceOptions options) + { + _options = options ?? new WebsocketServiceOptions(); + _ = RunRequestCleanupLoopAsync(_disposeCts.Token); + } + + public async Task ConnectAsync( + string mintUrl, + CancellationToken ct = default + ) + { + var normalized = NormalizeMintUrl(mintUrl); + var connectionId = Guid.NewGuid().ToString(); + var wsUrl = GetWebSocketUrl(mintUrl); + + var clientWebSocket = new ClientWebSocket(); + var connectionCts = CancellationTokenSource.CreateLinkedTokenSource(_disposeCts.Token, ct); + + try + { + await clientWebSocket.ConnectAsync(new Uri(wsUrl), ct); + } + catch + { + connectionCts.Dispose(); + clientWebSocket.Dispose(); + throw; + } + + var connection = new WebsocketConnection + { + Id = connectionId, + MintUrl = normalized, + WebSocket = clientWebSocket, + State = WebSocketState.Open, + CancellationTokenSource = connectionCts, + LastMessageReceived = DateTime.UtcNow, + }; + + _connections[normalized] = connection; + OnConnectionStateChanged(connectionId, WebSocketState.Open); + + _ = RunWithErrorHandlingAsync( + () => ListenForMessagesAsync(connection, connectionCts.Token), + connection + ); + + _ = RunWithErrorHandlingAsync( + () => RunHeartbeatLoopAsync(connection, connectionCts.Token), + connection + ); + + return connection; + } + + public async Task LazyConnectAsync( + string mintUrl, + CancellationToken ct = default + ) + { + var normalized = NormalizeMintUrl(mintUrl); + var connectionLock = _connectionLocks.GetOrAdd(normalized, _ => new SemaphoreSlim(1, 1)); + + await connectionLock.WaitAsync(ct); + try + { + if (_connections.TryGetValue(normalized, out var existing)) + { + if ( + existing is { State: WebSocketState.Open, WebSocket.State: WebSocketState.Open } + ) + { + return existing; + } + + _connections.TryRemove(normalized, out _); + existing.Dispose(); + } + + return await ConnectAsync(mintUrl, ct); + } + finally + { + connectionLock.Release(); + } + } + + public async Task DisconnectAsync(string mintUrl, CancellationToken ct = default) + { + var normalized = NormalizeMintUrl(mintUrl); + + if (!_connections.TryRemove(normalized, out var connection)) + { + return; + } + + try + { + if (connection.State == WebSocketState.Open) + { + using var timeoutCts = CancellationTokenSource.CreateLinkedTokenSource(ct); + timeoutCts.CancelAfter(TimeSpan.FromSeconds(5)); + + await connection.WebSocket.CloseAsync( + WebSocketCloseStatus.NormalClosure, + "Client disconnecting", + timeoutCts.Token + ); + } + } + catch + { + // graceful close failed, continue with cleanup + } + finally + { + connection.State = WebSocketState.Closed; + connection.CancellationTokenSource?.Cancel(); + connection.Dispose(); + + await CleanupConnectionSubscriptionsAsync(connection); + OnConnectionStateChanged(connection.Id, WebSocketState.Closed); + } + } + + public async Task SubscribeAsync( + string mintUrl, + SubscriptionKind kind, + string[] filters, + CancellationToken ct = default + ) + { + var normalized = NormalizeMintUrl(mintUrl); + + if (!_connections.TryGetValue(normalized, out var connection)) + { + throw new InvalidOperationException($"Connection for mint {mintUrl} not found"); + } + + if (connection.State != WebSocketState.Open) + { + throw new InvalidOperationException($"Connection for mint {mintUrl} is not open"); + } + + var subId = Guid.NewGuid().ToString(); + var requestId = GetNextRequestId(); + + var channel = Channel.CreateBounded( + new BoundedChannelOptions(_options.MaxChannelCapacity) + { + FullMode = BoundedChannelFullMode.DropOldest, + SingleReader = false, + } + ); + + var request = new WsRequest + { + JsonRpc = "2.0", + Method = WsRequestMethod.Subscribe, + Params = new WsRequestParams + { + Kind = kind, + SubId = subId, + Filters = filters, + }, + Id = requestId, + }; + + var subscription = new Subscription(this) + { + Id = subId, + ConnectionId = connection.Id, + Kind = kind, + Filters = filters, + CreatedAt = DateTime.UtcNow, + NotificationChannel = channel, + }; + + _subscriptions[subId] = subscription; + _subscriptionInfos[subId] = new SubscriptionInfo + { + MintUrl = normalized, + Kind = kind, + Filters = filters, + }; + + var tcs = new TaskCompletionSource(); + _pendingRequests[requestId] = new PendingRequest + { + Tcs = tcs, + SubscriptionId = subId, + CreatedAt = DateTime.UtcNow, + }; + + try + { + await SendMessageAsync(connection, request, ct); + + using var cts = CancellationTokenSource.CreateLinkedTokenSource(ct); + cts.CancelAfter(TimeSpan.FromSeconds(30)); + + var completedTask = await Task.WhenAny( + tcs.Task, + Task.Delay(Timeout.Infinite, cts.Token) + ) + .ConfigureAwait(false); + + if (completedTask != tcs.Task) + { + _subscriptions.TryRemove(subId, out _); + _subscriptionInfos.TryRemove(subId, out _); + await subscription.CloseInternalAsync(); + throw new TimeoutException("Subscription request timed out"); + } + + var result = await tcs.Task; + + if (result is RequestResult.Failure failure) + { + _subscriptions.TryRemove(subId, out _); + _subscriptionInfos.TryRemove(subId, out _); + await subscription.CloseInternalAsync(); + throw new InvalidOperationException($"Subscription failed: {failure.Message}"); + } + + return subscription; + } + catch + { + subscription.NotificationChannel.Writer.TryComplete(); + _subscriptions.TryRemove(subId, out _); + _subscriptionInfos.TryRemove(subId, out _); + throw; + } + finally + { + _pendingRequests.TryRemove(requestId, out _); + } + } + + public async Task UnsubscribeAsync(string subId, CancellationToken ct = default) + { + if (!_subscriptions.TryRemove(subId, out var subscription)) + return; + + _subscriptionInfos.TryRemove(subId, out _); + subscription.NotificationChannel.Writer.TryComplete(); + + var connection = _connections.Values.FirstOrDefault(c => c.Id == subscription.ConnectionId); + if (connection is null || connection.State != WebSocketState.Open) + { + return; + } + + var requestId = GetNextRequestId(); + var tcs = new TaskCompletionSource(); + _pendingRequests[requestId] = new PendingRequest + { + Tcs = tcs, + SubscriptionId = subId, + CreatedAt = DateTime.UtcNow, + }; + + try + { + var request = new WsRequest + { + JsonRpc = "2.0", + Method = WsRequestMethod.Unsubscribe, + Params = new WsRequestParams { SubId = subId }, + Id = requestId, + }; + + await SendMessageAsync(connection, request, ct); + + using var cts = CancellationTokenSource.CreateLinkedTokenSource(ct); + cts.CancelAfter(TimeSpan.FromSeconds(30)); + + var completed = await Task.WhenAny(tcs.Task, Task.Delay(Timeout.Infinite, cts.Token)) + .ConfigureAwait(false); + + if (completed == tcs.Task) + { + await tcs.Task.ConfigureAwait(false); + } + } + catch + { + // unsubscribe failed, local cleanup already done + } + finally + { + _pendingRequests.TryRemove(requestId, out _); + } + } + + public async ValueTask DisposeAsync() + { + if (_disposed) + return; + + _disposed = true; + + var mintUrls = _connections.Keys.ToList(); + foreach (var mintUrl in mintUrls) + { + try + { + await DisconnectAsync(mintUrl); + } + catch + { + // continue disposing other connections + } + } + + _disposeCts.Cancel(); + + _subscriptions.Clear(); + _connections.Clear(); + _pendingRequests.Clear(); + _subscriptionInfos.Clear(); + + foreach (var semaphore in _connectionLocks.Values) + { + semaphore.Dispose(); + } + _connectionLocks.Clear(); + + _disposeCts.Dispose(); + } + + public WebSocketState GetConnectionState(string mintUrl) + { + var normalized = NormalizeMintUrl(mintUrl); + return _connections.TryGetValue(normalized, out var connection) + ? connection.State + : WebSocketState.None; + } + + public IEnumerable GetSubscriptions(string mintUrl) + { + var normalized = NormalizeMintUrl(mintUrl); + if (!_connections.TryGetValue(normalized, out var connection)) + { + return Enumerable.Empty(); + } + return _subscriptions.Values.Where(s => s.ConnectionId == connection.Id); + } + + public IEnumerable GetConnections() => _connections.Values; + + public bool IsConnected(string mintUrl) + { + var normalized = NormalizeMintUrl(mintUrl); + return _connections.TryGetValue(normalized, out var conn) + && conn.State == WebSocketState.Open; + } + + #region Message Handling + + private async Task ListenForMessagesAsync(WebsocketConnection connection, CancellationToken ct) + { + var buffer = ArrayPool.Shared.Rent(4096); + using var messageBuffer = new MemoryStream(); + + try + { + while (connection.State == WebSocketState.Open && !ct.IsCancellationRequested) + { + var result = await connection.WebSocket.ReceiveAsync( + new ArraySegment(buffer), + ct + ); + + connection.LastMessageReceived = DateTime.UtcNow; + + if (result.MessageType == WebSocketMessageType.Close) + { + connection.State = WebSocketState.Closed; + break; + } + + if (result.MessageType == WebSocketMessageType.Text) + { + messageBuffer.Write(buffer, 0, result.Count); + if (result.EndOfMessage) + { + var message = Encoding.UTF8.GetString( + messageBuffer.GetBuffer(), + 0, + (int)messageBuffer.Length + ); + messageBuffer.SetLength(0); + ProcessMessage(message); + } + } + } + } + catch (OperationCanceledException) + { + // Expected on cancellation + } + catch + { + connection.State = WebSocketState.Aborted; + } + finally + { + ArrayPool.Shared.Return(buffer); + connection.CancellationTokenSource?.Cancel(); + + if (connection.State != WebSocketState.Closed) + { + OnConnectionStateChanged(connection.Id, WebSocketState.Aborted); + } + + await CleanupConnectionSubscriptionsAsync(connection); + + if (_options.AutoReconnect && !_disposed && !_disposeCts.IsCancellationRequested) + { + _ = ReconnectAsync(connection.MintUrl, _disposeCts.Token); + } + } + } + + private void ProcessMessage(string message) + { + try + { + using var doc = JsonDocument.Parse(message); + var root = doc.RootElement; + + if ( + root.TryGetProperty("method", out var methodProp) + && methodProp.GetString() == "subscribe" + ) + { + var notification = JsonSerializer.Deserialize(message, JsonOptions); + if (notification != null) + { + OnNotificationReceived(notification); + } + } + else if (root.TryGetProperty("result", out _)) + { + var response = JsonSerializer.Deserialize(message, JsonOptions); + if (response != null) + { + HandleResponse(response); + } + } + else if (root.TryGetProperty("error", out _)) + { + var error = JsonSerializer.Deserialize(message, JsonOptions); + if (error != null) + { + HandleError(error); + } + } + } + catch + { + // invalid message format, ignore + } + } + + private async Task SendMessageAsync( + WebsocketConnection connection, + T message, + CancellationToken ct + ) + { + var json = JsonSerializer.Serialize(message, JsonOptions); + var bytes = Encoding.UTF8.GetBytes(json); + + await connection.WebSocket.SendAsync( + new ArraySegment(bytes), + WebSocketMessageType.Text, + true, + ct + ); + } + + private void HandleResponse(WsResponse response) + { + if (!_pendingRequests.TryGetValue(response.Id, out var pr)) + return; + + var result = new RequestResult.Success(response.Result.SubId, response.Result.Status); + pr.Tcs.TrySetResult(result); + + if (_subscriptions.TryGetValue(pr.SubscriptionId, out var sub)) + { + sub.NotificationChannel.Writer.TryWrite(new WsMessage.Response(response)); + } + } + + private void HandleError(WsError error) + { + if (!_pendingRequests.TryGetValue(error.Id, out var pr)) + return; + + var result = new RequestResult.Failure(error.Error.Code, error.Error.Message, error.Id); + pr.Tcs.TrySetResult(result); + + if (_subscriptions.TryGetValue(pr.SubscriptionId, out var sub)) + { + sub.NotificationChannel.Writer.TryWrite(new WsMessage.Error(error)); + } + } + + private void OnNotificationReceived(WsNotification notification) + { + if (_subscriptions.TryGetValue(notification.Params.SubId, out var sub)) + { + sub.NotificationChannel.Writer.TryWrite(new WsMessage.Notification(notification)); + } + } + + #endregion + + #region Heartbeat & Reconnect + + private async Task RunHeartbeatLoopAsync(WebsocketConnection connection, CancellationToken ct) + { + while (!ct.IsCancellationRequested && connection.State == WebSocketState.Open) + { + try + { + await Task.Delay(_options.HeartbeatInterval, ct); + + if (connection.State != WebSocketState.Open) + break; + + var lastReceived = connection.LastMessageReceived ?? connection.ConnectedAt; + var timeSinceLastMessage = DateTime.UtcNow - lastReceived; + + if (timeSinceLastMessage > _options.HeartbeatInterval + _options.HeartbeatTimeout) + { + connection.State = WebSocketState.Aborted; + connection.CancellationTokenSource?.Cancel(); + break; + } + + connection.LastPingSent = DateTime.UtcNow; + } + catch (OperationCanceledException) + { + break; + } + } + } + + private async Task ReconnectAsync(string mintUrl, CancellationToken ct) + { + var normalized = NormalizeMintUrl(mintUrl); + var delay = _options.InitialReconnectDelay; + + for ( + int attempt = 1; + attempt <= _options.MaxReconnectAttempts && !ct.IsCancellationRequested; + attempt++ + ) + { + try + { + await Task.Delay(delay, ct); + + var connectionLock = _connectionLocks.GetOrAdd( + normalized, + _ => new SemaphoreSlim(1, 1) + ); + await connectionLock.WaitAsync(ct); + + try + { + if ( + _connections.TryGetValue(normalized, out var existing) + && existing.State == WebSocketState.Open + ) + { + return; // already reconnected + } + + await ConnectAsync(mintUrl, ct); + await ResubscribeAllAsync(normalized, ct); + return; + } + finally + { + connectionLock.Release(); + } + } + catch (OperationCanceledException) + { + return; + } + catch + { + delay = TimeSpan.FromTicks( + Math.Min(delay.Ticks * 2, _options.MaxReconnectDelay.Ticks) + ); + } + } + + OnConnectionStateChanged(normalized, WebSocketState.Closed); + } + + private async Task ResubscribeAllAsync(string mintUrl, CancellationToken ct) + { + var subsToRestore = _subscriptionInfos.Where(kvp => kvp.Value.MintUrl == mintUrl).ToList(); + + foreach (var (subId, info) in subsToRestore) + { + try + { + _subscriptions.TryRemove(subId, out var oldSub); + _subscriptionInfos.TryRemove(subId, out _); + if (oldSub != null) + { + await oldSub.CloseInternalAsync(); + } + + await SubscribeAsync(mintUrl, info.Kind, info.Filters, ct); + } + catch + { + // failed to re-subscribe, continue with others + } + } + } + + #endregion + + #region Cleanup & Utilities + + private async Task CleanupConnectionSubscriptionsAsync(WebsocketConnection connection) + { + var subscriptionsToClose = _subscriptions + .Where(s => s.Value.ConnectionId == connection.Id) + .ToList(); + + foreach (var (subId, sub) in subscriptionsToClose) + { + try + { + await sub.CloseInternalAsync(); + } + catch + { + // continue cleanup + } + _subscriptions.TryRemove(subId, out _); + } + } + + private async Task RunRequestCleanupLoopAsync(CancellationToken ct) + { + while (!ct.IsCancellationRequested) + { + try + { + await Task.Delay(_options.RequestCleanupInterval, ct); + CleanupStaleRequests(); + } + catch (OperationCanceledException) + { + break; + } + } + } + + private void CleanupStaleRequests() + { + var staleThreshold = DateTime.UtcNow - _options.RequestTimeout; + var staleRequests = _pendingRequests + .Where(pr => pr.Value.CreatedAt < staleThreshold) + .Select(pr => pr.Key) + .ToList(); + + foreach (var id in staleRequests) + { + if (_pendingRequests.TryRemove(id, out var pr)) + { + pr.Tcs.TrySetException(new TimeoutException("Request expired")); + } + } + } + + private async Task RunWithErrorHandlingAsync(Func action, WebsocketConnection connection) + { + try + { + await action(); + } + catch (OperationCanceledException) + { + // Expected + } + catch + { + connection.State = WebSocketState.Aborted; + OnConnectionStateChanged(connection.Id, WebSocketState.Aborted); + } + } + + private int GetNextRequestId() => Interlocked.Increment(ref _nextRequestId); + + private void OnConnectionStateChanged(string connectionId, WebSocketState state) + { + ConnectionStateChanged?.Invoke( + this, + new ConnectionStateChangedEventArgs { ConnectionId = connectionId, State = state } + ); + } + + private static string NormalizeMintUrl(string mintUrl) + { + if (!Uri.TryCreate(mintUrl.TrimEnd('/'), UriKind.Absolute, out var uri)) + { + return mintUrl.TrimEnd('/').ToLowerInvariant(); + } + var host = uri.Host.ToLowerInvariant(); + var builder = new UriBuilder(uri) { Host = host }; + return builder.Uri.ToString().TrimEnd('/'); + } + + private string GetWebSocketUrl(string mintUrl) + { + var uri = new Uri(NormalizeMintUrl(mintUrl)); + var scheme = uri.Scheme == "https" ? "wss" : "ws"; + var hostPort = uri.IsDefaultPort ? uri.Host : $"{uri.Host}:{uri.Port}"; + var path = uri.AbsolutePath.TrimEnd('/'); + return $"{scheme}://{hostPort}{path}/v1/ws"; + } + + #endregion + + private class SubscriptionInfo + { + public required string MintUrl { get; init; } + public required SubscriptionKind Kind { get; init; } + public required string[] Filters { get; init; } + } +} diff --git a/DotNut/Abstractions/Websockets/WebsocketServiceExtensions.cs b/DotNut/Abstractions/Websockets/WebsocketServiceExtensions.cs new file mode 100644 index 0000000..111317c --- /dev/null +++ b/DotNut/Abstractions/Websockets/WebsocketServiceExtensions.cs @@ -0,0 +1,122 @@ +namespace DotNut.Abstractions.Websockets; + +public static class WebsocketServiceExtensions +{ + public static async Task SubscribeToMintQuoteAsync( + this IWebsocketService service, + string mintUrl, + string[] quoteIds, + CancellationToken ct = default + ) + { + await service.LazyConnectAsync(mintUrl, ct); + return await service.SubscribeAsync( + mintUrl, + SubscriptionKind.Bolt11MintQuote, + quoteIds, + ct + ); + } + + public static async Task SubscribeToMeltQuoteAsync( + this IWebsocketService service, + string mintUrl, + string[] quoteIds, + CancellationToken ct = default + ) + { + await service.LazyConnectAsync(mintUrl, ct); + return await service.SubscribeAsync( + mintUrl, + SubscriptionKind.Bolt11MeltQuote, + quoteIds, + ct + ); + } + + public static async Task SubscribeToProofStateAsync( + this IWebsocketService service, + string mintUrl, + string[] proofYs, + CancellationToken ct = default + ) + { + await service.LazyConnectAsync(mintUrl, ct); + return await service.SubscribeAsync(mintUrl, SubscriptionKind.ProofState, proofYs, ct); + } + + public static async Task SubscribeToSingleProofStateAsync( + this IWebsocketService service, + string mintUrl, + string proofY, + CancellationToken ct = default + ) + { + await service.LazyConnectAsync(mintUrl, ct); + return await service.SubscribeToProofStateAsync(mintUrl, new[] { proofY }, ct); + } + + public static async Task SubscribeToSingleMintQuoteAsync( + this IWebsocketService service, + string mintUrl, + string quoteId, + CancellationToken ct = default + ) + { + await service.LazyConnectAsync(mintUrl, ct); + return await service.SubscribeToMintQuoteAsync(mintUrl, new[] { quoteId }, ct); + } + + public static async Task SubscribeToSingleMeltQuoteAsync( + this IWebsocketService service, + string mintUrl, + string quoteId, + CancellationToken ct = default + ) + { + await service.LazyConnectAsync(mintUrl, ct); + return await service.SubscribeToMeltQuoteAsync(mintUrl, new[] { quoteId }, ct); + } + + public static bool IsConnectionActive(this IWebsocketService service, string connectionId) + { + var state = service.GetConnectionState(connectionId); + return state == System.Net.WebSockets.WebSocketState.Open; + } + + public static IEnumerable GetSubscriptionsByKind( + this IWebsocketService service, + string connectionId, + SubscriptionKind kind + ) + { + return service.GetSubscriptions(connectionId).Where(s => s.Kind == kind); + } + + public static async Task UnsubscribeAllAsync( + this IWebsocketService service, + string connectionId, + CancellationToken ct = default + ) + { + var subscriptions = service.GetSubscriptions(connectionId).ToList(); + foreach (var subscription in subscriptions) + { + await service.UnsubscribeAsync(subscription.Id, ct); + } + } + + public static async Task UnsubscribeByKindAsync( + this IWebsocketService service, + string connectionId, + SubscriptionKind kind, + CancellationToken ct = default + ) + { + var subscriptions = service.GetSubscriptionsByKind(connectionId, kind).ToList(); + foreach (var subscription in subscriptions) + { + await service.UnsubscribeAsync(subscription.Id, ct); + } + } +} diff --git a/DotNut/Abstractions/Websockets/WebsocketServiceOptions.cs b/DotNut/Abstractions/Websockets/WebsocketServiceOptions.cs new file mode 100644 index 0000000..fbb672b --- /dev/null +++ b/DotNut/Abstractions/Websockets/WebsocketServiceOptions.cs @@ -0,0 +1,61 @@ +namespace DotNut.Abstractions.Websockets; + +/// +/// Configuration options for WebsocketService +/// +public class WebsocketServiceOptions +{ + /// + /// Whether to automatically reconnect when connection is lost. + /// Default: true + /// + public bool AutoReconnect { get; set; } = true; + + /// + /// Maximum number of reconnect attempts before giving up. + /// Default: 10 + /// + public int MaxReconnectAttempts { get; set; } = 10; + + /// + /// Initial delay before first reconnect attempt. + /// Default: 1 second + /// + public TimeSpan InitialReconnectDelay { get; set; } = TimeSpan.FromSeconds(1); + + /// + /// Maximum delay between reconnect attempts (exponential backoff cap). + /// Default: 5 minutes + /// + public TimeSpan MaxReconnectDelay { get; set; } = TimeSpan.FromMinutes(5); + + /// + /// Interval between heartbeat checks. + /// Default: 30 seconds + /// + public TimeSpan HeartbeatInterval { get; set; } = TimeSpan.FromSeconds(30); + + /// + /// Timeout for heartbeat response before considering connection dead. + /// Default: 10 seconds + /// + public TimeSpan HeartbeatTimeout { get; set; } = TimeSpan.FromSeconds(10); + + /// + /// Maximum number of messages in subscription channel before dropping oldest. + /// Default: 1000 + /// + public int MaxChannelCapacity { get; set; } = 1000; + + /// + /// Timeout for pending requests before they are cleaned up. + /// Default: 5 minutes + /// + public TimeSpan RequestTimeout { get; set; } = TimeSpan.FromMinutes(5); + + /// + /// Interval for cleaning up stale pending requests. + /// Default: 1 minute + /// + public TimeSpan RequestCleanupInterval { get; set; } = TimeSpan.FromMinutes(1); +} diff --git a/DotNut/Api/CashuHttpClient.cs b/DotNut/Api/CashuHttpClient.cs index cd156bb..9d98113 100644 --- a/DotNut/Api/CashuHttpClient.cs +++ b/DotNut/Api/CashuHttpClient.cs @@ -9,10 +9,20 @@ namespace DotNut.Api; public class CashuHttpClient : ICashuApi { private readonly HttpClient _httpClient; + private readonly bool _ownsHttpClient; - public CashuHttpClient(HttpClient httpClient) + public CashuHttpClient(HttpClient httpClient, bool ownsHttpClient = false) { + ArgumentNullException.ThrowIfNull(httpClient); + ArgumentNullException.ThrowIfNull(httpClient.BaseAddress); _httpClient = httpClient; + _ownsHttpClient = ownsHttpClient; + } + + public string GetBaseUrl() + { + ArgumentNullException.ThrowIfNull(_httpClient.BaseAddress); + return _httpClient.BaseAddress.AbsoluteUri; } public async Task GetKeys(CancellationToken cancellationToken = default) @@ -27,78 +37,133 @@ public async Task GetKeysets(CancellationToken cancellationT return await HandleResponse(response, cancellationToken); } - public async Task GetKeys(KeysetId keysetId, CancellationToken cancellationToken = default) + public async Task GetKeys( + KeysetId keysetId, + CancellationToken cancellationToken = default + ) { var response = await _httpClient.GetAsync($"v1/keys/{keysetId}", cancellationToken); return await HandleResponse(response, cancellationToken); } - public async Task Swap(PostSwapRequest request, CancellationToken cancellationToken = default) + public async Task Swap( + PostSwapRequest request, + CancellationToken cancellationToken = default + ) { - var response = await _httpClient.PostAsync($"v1/swap", - new StringContent(JsonSerializer.Serialize(request), Encoding.UTF8, "application/json"), cancellationToken); + var response = await _httpClient.PostAsync( + $"v1/swap", + new StringContent(JsonSerializer.Serialize(request), Encoding.UTF8, "application/json"), + cancellationToken + ); return await HandleResponse(response, cancellationToken); } - public async Task CreateMintQuote(string method, TRequest request, CancellationToken - cancellationToken = default) + public async Task CreateMintQuote( + string method, + TRequest request, + CancellationToken cancellationToken = default + ) { - var response = await _httpClient.PostAsync($"v1/mint/quote/{method}", - new StringContent(JsonSerializer.Serialize(request), Encoding.UTF8, "application/json"), cancellationToken); + var response = await _httpClient.PostAsync( + $"v1/mint/quote/{method}", + new StringContent(JsonSerializer.Serialize(request), Encoding.UTF8, "application/json"), + cancellationToken + ); return await HandleResponse(response, cancellationToken); } - public async Task CreateMeltQuote(string method, TRequest request, CancellationToken - cancellationToken = default) + public async Task CreateMeltQuote( + string method, + TRequest request, + CancellationToken cancellationToken = default + ) { - var response = await _httpClient.PostAsync($"v1/melt/quote/{method}", - new StringContent(JsonSerializer.Serialize(request), Encoding.UTF8, "application/json"), cancellationToken); + var response = await _httpClient.PostAsync( + $"v1/melt/quote/{method}", + new StringContent(JsonSerializer.Serialize(request), Encoding.UTF8, "application/json"), + cancellationToken + ); return await HandleResponse(response, cancellationToken); } - public async Task Melt(string method, TRequest request, CancellationToken - cancellationToken = default) + public async Task Melt( + string method, + TRequest request, + CancellationToken cancellationToken = default + ) { - var response = await _httpClient.PostAsync($"v1/melt/{method}", - new StringContent(JsonSerializer.Serialize(request), Encoding.UTF8, "application/json"), cancellationToken); + var response = await _httpClient.PostAsync( + $"v1/melt/{method}", + new StringContent(JsonSerializer.Serialize(request), Encoding.UTF8, "application/json"), + cancellationToken + ); return await HandleResponse(response, cancellationToken); } - public async Task CheckMeltQuote(string method, string quoteId, CancellationToken - cancellationToken = default) + public async Task CheckMeltQuote( + string method, + string quoteId, + CancellationToken cancellationToken = default + ) { - var response = await _httpClient.GetAsync($"v1/melt/quote/{method}/{quoteId}", cancellationToken); + var response = await _httpClient.GetAsync( + $"v1/melt/quote/{method}/{quoteId}", + cancellationToken + ); return await HandleResponse(response, cancellationToken); } - public async Task CheckMintQuote(string method, string quoteId, CancellationToken - cancellationToken = default) + public async Task CheckMintQuote( + string method, + string quoteId, + CancellationToken cancellationToken = default + ) { - var response = await _httpClient.GetAsync($"v1/mint/quote/{method}/{quoteId}", cancellationToken); + var response = await _httpClient.GetAsync( + $"v1/mint/quote/{method}/{quoteId}", + cancellationToken + ); return await HandleResponse(response, cancellationToken); } - public async Task Mint(string method, TRequest request, - CancellationToken cancellationToken = default) + public async Task Mint( + string method, + TRequest request, + CancellationToken cancellationToken = default + ) { - var response = await _httpClient.PostAsync($"v1/mint/{method}", - new StringContent(JsonSerializer.Serialize(request), Encoding.UTF8, "application/json"), cancellationToken); + var response = await _httpClient.PostAsync( + $"v1/mint/{method}", + new StringContent(JsonSerializer.Serialize(request), Encoding.UTF8, "application/json"), + cancellationToken + ); return await HandleResponse(response, cancellationToken); } - public async Task CheckState(PostCheckStateRequest request, - CancellationToken cancellationToken = default) + public async Task CheckState( + PostCheckStateRequest request, + CancellationToken cancellationToken = default + ) { - var response = await _httpClient.PostAsync($"v1/checkstate", - new StringContent(JsonSerializer.Serialize(request), Encoding.UTF8, "application/json"), cancellationToken); + var response = await _httpClient.PostAsync( + $"v1/checkstate", + new StringContent(JsonSerializer.Serialize(request), Encoding.UTF8, "application/json"), + cancellationToken + ); return await HandleResponse(response, cancellationToken); } - public async Task Restore(PostRestoreRequest request, - CancellationToken cancellationToken = default) + public async Task Restore( + PostRestoreRequest request, + CancellationToken cancellationToken = default + ) { - var response = await _httpClient.PostAsync($"v1/restore", - new StringContent(JsonSerializer.Serialize(request), Encoding.UTF8, "application/json"), cancellationToken); + var response = await _httpClient.PostAsync( + $"v1/restore", + new StringContent(JsonSerializer.Serialize(request), Encoding.UTF8, "application/json"), + cancellationToken + ); return await HandleResponse(response, cancellationToken); } @@ -108,12 +173,16 @@ public async Task GetInfo(CancellationToken cancellationToken = return await HandleResponse(response, cancellationToken); } - protected async Task HandleResponse(HttpResponseMessage response, CancellationToken cancellationToken) + protected async Task HandleResponse( + HttpResponseMessage response, + CancellationToken cancellationToken + ) { if (response.StatusCode == HttpStatusCode.BadRequest) { - var error = - await response.Content.ReadFromJsonAsync(cancellationToken: cancellationToken); + var error = await response.Content.ReadFromJsonAsync( + cancellationToken: cancellationToken + ); throw new CashuProtocolException(error); } @@ -131,4 +200,12 @@ protected async Task HandleResponse(HttpResponseMessage response, Cancella return result!; } -} \ No newline at end of file + + public void Dispose() + { + if (_ownsHttpClient) + { + _httpClient.Dispose(); + } + } +} diff --git a/DotNut/Api/CashuProtocolError.cs b/DotNut/Api/CashuProtocolError.cs index 3e4f7aa..edb98c8 100644 --- a/DotNut/Api/CashuProtocolError.cs +++ b/DotNut/Api/CashuProtocolError.cs @@ -4,6 +4,9 @@ namespace DotNut.Api; public class CashuProtocolError { - [JsonPropertyName("detail")] public string Detail { get; set; } - [JsonPropertyName("code")] public int Code { get; set; } -} \ No newline at end of file + [JsonPropertyName("detail")] + public string Detail { get; set; } + + [JsonPropertyName("code")] + public int Code { get; set; } +} diff --git a/DotNut/Api/CashuProtocolException.cs b/DotNut/Api/CashuProtocolException.cs index 86ad641..ab2d229 100644 --- a/DotNut/Api/CashuProtocolException.cs +++ b/DotNut/Api/CashuProtocolException.cs @@ -2,10 +2,11 @@ public class CashuProtocolException : Exception { - public CashuProtocolException(CashuProtocolError error) : base(error.Detail) + public CashuProtocolException(CashuProtocolError error) + : base(error.Detail) { Error = error; } public CashuProtocolError Error { get; } -} \ No newline at end of file +} diff --git a/DotNut/Api/ICashuApi.cs b/DotNut/Api/ICashuApi.cs index e7696b4..fbca063 100644 --- a/DotNut/Api/ICashuApi.cs +++ b/DotNut/Api/ICashuApi.cs @@ -2,27 +2,59 @@ namespace DotNut.Api; -public interface ICashuApi +public interface ICashuApi : IDisposable { + string GetBaseUrl(); Task GetKeys(CancellationToken cancellationToken = default); Task GetKeys(KeysetId keysetId, CancellationToken cancellationToken = default); Task GetKeysets(CancellationToken cancellationToken = default); - Task Swap(PostSwapRequest request, CancellationToken cancellationToken = default); + Task Swap( + PostSwapRequest request, + CancellationToken cancellationToken = default + ); - Task CreateMintQuote(string method, TRequest request, CancellationToken - cancellationToken = default); + Task CreateMintQuote( + string method, + TRequest request, + CancellationToken cancellationToken = default + ); - Task CreateMeltQuote(string method, TRequest request, CancellationToken - cancellationToken = default); + Task CreateMeltQuote( + string method, + TRequest request, + CancellationToken cancellationToken = default + ); - Task Melt(string method, TRequest request, CancellationToken - cancellationToken = default); + Task Melt( + string method, + TRequest request, + CancellationToken cancellationToken = default + ); - Task CheckMintQuote(string method, string quoteId, CancellationToken - cancellationToken = default); + public Task CheckMeltQuote( + string method, + string quoteId, + CancellationToken cancellationToken = default + ); - Task Mint(string method, TRequest request, CancellationToken cancellationToken = default); - Task CheckState(PostCheckStateRequest request, CancellationToken cancellationToken = default); - Task Restore(PostRestoreRequest request, CancellationToken cancellationToken = default); + Task CheckMintQuote( + string method, + string quoteId, + CancellationToken cancellationToken = default + ); + + Task Mint( + string method, + TRequest request, + CancellationToken cancellationToken = default + ); + Task CheckState( + PostCheckStateRequest request, + CancellationToken cancellationToken = default + ); + Task Restore( + PostRestoreRequest request, + CancellationToken cancellationToken = default + ); Task GetInfo(CancellationToken cancellationToken = default); -} \ No newline at end of file +} diff --git a/DotNut/ApiModels/ContactInfo.cs b/DotNut/ApiModels/ContactInfo.cs deleted file mode 100644 index 46ea371..0000000 --- a/DotNut/ApiModels/ContactInfo.cs +++ /dev/null @@ -1,9 +0,0 @@ -using System.Text.Json.Serialization; - -namespace DotNut.ApiModels; - -public class ContactInfo -{ - [JsonPropertyName("method")] public string Method { get; set; } - [JsonPropertyName("info")] public string Info { get; set; } -} \ No newline at end of file diff --git a/DotNut/ApiModels/GetKeysResponse.cs b/DotNut/ApiModels/GetKeysResponse.cs index db21c2b..242a799 100644 --- a/DotNut/ApiModels/GetKeysResponse.cs +++ b/DotNut/ApiModels/GetKeysResponse.cs @@ -4,17 +4,29 @@ namespace DotNut.ApiModels; public class GetKeysResponse { - [JsonPropertyName("keysets")] public KeysetItemResponse[] Keysets { get; set; } + [JsonPropertyName("keysets")] + public KeysetItemResponse[] Keysets { get; set; } public class KeysetItemResponse { - [JsonPropertyName("id")] public KeysetId Id { get; set; } - [JsonPropertyName("unit")] public string Unit { get; set; } - [JsonPropertyName("active")] public bool? Active { get; set; } // nullable until wider adoption + [JsonPropertyName("id")] + public KeysetId Id { get; set; } + + [JsonPropertyName("unit")] + public string Unit { get; set; } + + [JsonPropertyName("active")] + public bool? Active { get; set; } // nullable until wider adoption + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - [JsonPropertyName("input_fee_ppk")] public ulong? InputFeePpk { get; set; } + [JsonPropertyName("input_fee_ppk")] + public ulong? InputFeePpk { get; set; } + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - [JsonPropertyName("final_expiry")] public ulong? FinalExpiry { get; set; } - [JsonPropertyName("keys")] public Keyset Keys { get; set; } + [JsonPropertyName("final_expiry")] + public ulong? FinalExpiry { get; set; } + + [JsonPropertyName("keys")] + public Keyset Keys { get; set; } } -} \ No newline at end of file +} diff --git a/DotNut/ApiModels/GetKeysetsResponse.cs b/DotNut/ApiModels/GetKeysetsResponse.cs index 2760802..d388a3e 100644 --- a/DotNut/ApiModels/GetKeysetsResponse.cs +++ b/DotNut/ApiModels/GetKeysetsResponse.cs @@ -4,16 +4,26 @@ namespace DotNut.ApiModels; public class GetKeysetsResponse { - [JsonPropertyName("keysets")] public KeysetItemResponse[] Keysets { get; set; } + [JsonPropertyName("keysets")] + public KeysetItemResponse[] Keysets { get; set; } public class KeysetItemResponse { - [JsonPropertyName("id")] public KeysetId Id { get; set; } - [JsonPropertyName("unit")] public string Unit { get; set; } - [JsonPropertyName("active")] public bool Active { get; set; } + [JsonPropertyName("id")] + public KeysetId Id { get; set; } + + [JsonPropertyName("unit")] + public string Unit { get; set; } + + [JsonPropertyName("active")] + public bool Active { get; set; } + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - [JsonPropertyName("input_fee_ppk")] public ulong? InputFee { get; set; } + [JsonPropertyName("input_fee_ppk")] + public ulong? InputFee { get; set; } + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - [JsonPropertyName("final_expiry")] public ulong? FinalExpiry { get; set; } + [JsonPropertyName("final_expiry")] + public ulong? FinalExpiry { get; set; } } -} \ No newline at end of file +} diff --git a/DotNut/ApiModels/Info/ContactInfo.cs b/DotNut/ApiModels/Info/ContactInfo.cs new file mode 100644 index 0000000..63573f3 --- /dev/null +++ b/DotNut/ApiModels/Info/ContactInfo.cs @@ -0,0 +1,12 @@ +using System.Text.Json.Serialization; + +namespace DotNut.ApiModels; + +public class ContactInfo +{ + [JsonPropertyName("method")] + public string Method { get; set; } + + [JsonPropertyName("info")] + public string Info { get; set; } +} diff --git a/DotNut/ApiModels/GetInfoResponse.cs b/DotNut/ApiModels/Info/GetInfoResponse.cs similarity index 96% rename from DotNut/ApiModels/GetInfoResponse.cs rename to DotNut/ApiModels/Info/GetInfoResponse.cs index 4e7b4ea..73c3286 100644 --- a/DotNut/ApiModels/GetInfoResponse.cs +++ b/DotNut/ApiModels/Info/GetInfoResponse.cs @@ -33,7 +33,7 @@ public class GetInfoResponse [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] [JsonPropertyName("motd")] public string? Motd { get; set; } - + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] [JsonPropertyName("icon_url")] public string? IconUrl { get; set; } @@ -41,17 +41,17 @@ public class GetInfoResponse [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] [JsonPropertyName("urls")] public string[]? Urls { get; set; } - + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] [JsonConverter(typeof(UnixDateTimeOffsetConverter))] [JsonPropertyName("time")] public DateTimeOffset? Time { get; set; } - + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] [JsonPropertyName("tos_url")] public string? TosUrl { get; set; } [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] [JsonPropertyName("nuts")] - public Dictionary? Nuts { get; set; } -} \ No newline at end of file + public Dictionary? Nuts { get; set; } +} diff --git a/DotNut/ApiModels/Info/MPPInfo.cs b/DotNut/ApiModels/Info/MPPInfo.cs new file mode 100644 index 0000000..7394f0c --- /dev/null +++ b/DotNut/ApiModels/Info/MPPInfo.cs @@ -0,0 +1,19 @@ +using System.Text.Json.Serialization; + +namespace DotNut.ApiModels.Info; + +public class MPPInfo +{ + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] + [JsonPropertyName("methods")] + public MPPMethod[]? Methods { get; set; } + + public class MPPMethod + { + [JsonPropertyName("method")] + public string Method { get; set; } + + [JsonPropertyName("unit")] + public string Unit { get; set; } + } +} diff --git a/DotNut/ApiModels/Info/SwapInfo.cs b/DotNut/ApiModels/Info/SwapInfo.cs new file mode 100644 index 0000000..7c3707f --- /dev/null +++ b/DotNut/ApiModels/Info/SwapInfo.cs @@ -0,0 +1,37 @@ +using System.Text.Json.Serialization; + +namespace DotNut.ApiModels; + +public class SwapInfo +{ + [JsonPropertyName("methods")] + public SwapMethod[] Methods { get; set; } + + [JsonPropertyName("disabled")] + public bool Disabled { get; set; } + + public class SwapMethod + { + [JsonPropertyName("method")] + public string Method { get; set; } + + [JsonPropertyName("unit")] + public string Unit { get; set; } + + [JsonPropertyName("min_amount")] + public ulong MinAmount { get; set; } + + [JsonPropertyName("max_amount")] + public ulong MaxAmount { get; set; } + + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] + [JsonPropertyName("options")] + public SwapOptions? Options { get; set; } + + public class SwapOptions + { + [JsonPropertyName("description")] + public bool? Description { get; set; } + } + } +} diff --git a/DotNut/ApiModels/Info/WebSocketSupport.cs b/DotNut/ApiModels/Info/WebSocketSupport.cs new file mode 100644 index 0000000..fed139a --- /dev/null +++ b/DotNut/ApiModels/Info/WebSocketSupport.cs @@ -0,0 +1,15 @@ +using System.Text.Json.Serialization; + +namespace DotNut.ApiModels; + +public class WebSocketSupport +{ + [JsonPropertyName("method")] + public string Method { get; set; } + + [JsonPropertyName("unit")] + public string Unit { get; set; } + + [JsonPropertyName("commands")] + public string[] Commands { get; set; } +} diff --git a/DotNut/ApiModels/Melt/MeltQuoteRequestOptions.cs b/DotNut/ApiModels/Melt/MeltQuoteRequestOptions.cs new file mode 100644 index 0000000..1895f2e --- /dev/null +++ b/DotNut/ApiModels/Melt/MeltQuoteRequestOptions.cs @@ -0,0 +1,16 @@ +using System.Text.Json.Serialization; + +namespace DotNut.ApiModels.Melt; + +public class MeltQuoteRequestOptions +{ + [JsonPropertyName("amountless")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public AmountlessMeltQuoteOptions? Amountless { get; set; } +} + +public class AmountlessMeltQuoteOptions +{ + [JsonPropertyName("amount_msat")] + public ulong AmountMsat { get; set; } +} diff --git a/DotNut/ApiModels/Melt/PostMeltRequest.cs b/DotNut/ApiModels/Melt/PostMeltRequest.cs index a5cd262..706f5ca 100644 --- a/DotNut/ApiModels/Melt/PostMeltRequest.cs +++ b/DotNut/ApiModels/Melt/PostMeltRequest.cs @@ -4,14 +4,13 @@ namespace DotNut.ApiModels; public class PostMeltRequest { - [JsonPropertyName("quote")] public string Quote { get; set; } - + [JsonPropertyName("inputs")] public Proof[] Inputs { get; set; } - + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] [JsonPropertyName("outputs")] public BlindedMessage[]? Outputs { get; set; } -} \ No newline at end of file +} diff --git a/DotNut/ApiModels/Melt/bolt11/PostMeltQuoteBolt11Request.cs b/DotNut/ApiModels/Melt/bolt11/PostMeltQuoteBolt11Request.cs index 6c57f5d..a9358ca 100644 --- a/DotNut/ApiModels/Melt/bolt11/PostMeltQuoteBolt11Request.cs +++ b/DotNut/ApiModels/Melt/bolt11/PostMeltQuoteBolt11Request.cs @@ -1,13 +1,17 @@ using System.Text.Json.Serialization; +using DotNut.ApiModels.Melt; namespace DotNut.ApiModels; public class PostMeltQuoteBolt11Request { - - [JsonPropertyName("request")] + [JsonPropertyName("request")] public string Request { get; set; } - [JsonPropertyName("unit")] + [JsonPropertyName("unit")] public string Unit { get; set; } -} \ No newline at end of file + + [JsonPropertyName("options")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public MeltQuoteRequestOptions? Options { get; set; } +} diff --git a/DotNut/ApiModels/Melt/bolt11/PostMeltQuoteBolt11Response.cs b/DotNut/ApiModels/Melt/bolt11/PostMeltQuoteBolt11Response.cs index 8beffbb..ecf0bd0 100644 --- a/DotNut/ApiModels/Melt/bolt11/PostMeltQuoteBolt11Response.cs +++ b/DotNut/ApiModels/Melt/bolt11/PostMeltQuoteBolt11Response.cs @@ -4,26 +4,26 @@ namespace DotNut.ApiModels; public class PostMeltQuoteBolt11Response { - [JsonPropertyName("quote")] + [JsonPropertyName("quote")] public string Quote { get; set; } - - [JsonPropertyName("amount")] + + [JsonPropertyName("amount")] public ulong Amount { get; set; } - - [JsonPropertyName("fee_reserve")] + + [JsonPropertyName("fee_reserve")] public int FeeReserve { get; set; } - + [JsonPropertyName("state")] - public string State {get; set;} - + public string State { get; set; } + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] [JsonPropertyName("expiry")] - public int? Expiry {get; set;} - + public int? Expiry { get; set; } + [JsonPropertyName("payment_preimage")] - public string? PaymentPreimage {get; set;} - + public string? PaymentPreimage { get; set; } + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] [JsonPropertyName("change")] public BlindSignature[]? Change { get; set; } -} \ No newline at end of file +} diff --git a/DotNut/ApiModels/Melt/bolt12/PostMeltQuoteBolt12Request.cs b/DotNut/ApiModels/Melt/bolt12/PostMeltQuoteBolt12Request.cs index f5b36a2..ed202f4 100644 --- a/DotNut/ApiModels/Melt/bolt12/PostMeltQuoteBolt12Request.cs +++ b/DotNut/ApiModels/Melt/bolt12/PostMeltQuoteBolt12Request.cs @@ -1,4 +1,5 @@ using System.Text.Json; +using System.Text.Json.Nodes; using System.Text.Json.Serialization; namespace DotNut.ApiModels.Melt.bolt12; @@ -7,10 +8,11 @@ public class PostMeltQuoteBolt12Request { [JsonPropertyName("request")] public string Request { get; set; } - + [JsonPropertyName("unit")] public string Unit { get; set; } - - public JsonDocument? Options { get; set; } -} + [JsonPropertyName("options")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public MeltQuoteRequestOptions? Options { get; set; } +} diff --git a/DotNut/ApiModels/Melt/bolt12/PostMeltQuoteBolt12Response.cs b/DotNut/ApiModels/Melt/bolt12/PostMeltQuoteBolt12Response.cs index e84ff34..14a4a38 100644 --- a/DotNut/ApiModels/Melt/bolt12/PostMeltQuoteBolt12Response.cs +++ b/DotNut/ApiModels/Melt/bolt12/PostMeltQuoteBolt12Response.cs @@ -4,19 +4,32 @@ namespace DotNut.ApiModels.Melt.bolt12; public class PostMeltQuoteBolt12Response { - [JsonPropertyName("quote")] public string Quote { get; set; } + [JsonPropertyName("quote")] + public string Quote { get; set; } - [JsonPropertyName("request")] public string Request { get; set; } + [JsonPropertyName("request")] + public string Request { get; set; } - [JsonPropertyName("amount")] public ulong Amount { get; set; } + [JsonPropertyName("amount")] + public ulong Amount { get; set; } - [JsonPropertyName("fee_reserve")] public ulong FeeReserve { get; set; } + [JsonPropertyName("unit")] + public string Unit { get; set; } + + [JsonPropertyName("fee_reserve")] + public ulong FeeReserve { get; set; } + + [JsonPropertyName("state")] + public string State { get; set; } + + [JsonPropertyName("expiry")] + public int Expiry { get; set; } - [JsonPropertyName("state")] public string State { get; set; } - - [JsonPropertyName("expiry")] public int Expiry { get; set; } - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - [JsonPropertyName("payment_preimage")] public string PaymentPreimage { get; set; } + [JsonPropertyName("payment_preimage")] + public string? PaymentPreimage { get; set; } -} \ No newline at end of file + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("change")] + public BlindSignature[]? Change { get; set; } +} diff --git a/DotNut/ApiModels/Mint/PostMintRequest.cs b/DotNut/ApiModels/Mint/PostMintRequest.cs index 9767af1..0622927 100644 --- a/DotNut/ApiModels/Mint/PostMintRequest.cs +++ b/DotNut/ApiModels/Mint/PostMintRequest.cs @@ -9,4 +9,8 @@ public class PostMintRequest [JsonPropertyName("outputs")] public BlindedMessage[] Outputs { get; set; } -} \ No newline at end of file + + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("signature")] + public string? Signature { get; set; } +} diff --git a/DotNut/ApiModels/Mint/PostMintResponse.cs b/DotNut/ApiModels/Mint/PostMintResponse.cs index 609fa6a..5aa0e8c 100644 --- a/DotNut/ApiModels/Mint/PostMintResponse.cs +++ b/DotNut/ApiModels/Mint/PostMintResponse.cs @@ -6,4 +6,4 @@ public class PostMintResponse { [JsonPropertyName("signatures")] public BlindSignature[] Signatures { get; set; } -} \ No newline at end of file +} diff --git a/DotNut/ApiModels/Mint/bolt11/PostMintQuoteBolt11Request.cs b/DotNut/ApiModels/Mint/bolt11/PostMintQuoteBolt11Request.cs index 53b91ce..95021ad 100644 --- a/DotNut/ApiModels/Mint/bolt11/PostMintQuoteBolt11Request.cs +++ b/DotNut/ApiModels/Mint/bolt11/PostMintQuoteBolt11Request.cs @@ -1,18 +1,20 @@ -using System.Diagnostics; using System.Text.Json.Serialization; namespace DotNut.ApiModels; public class PostMintQuoteBolt11Request { - - [JsonPropertyName("amount")] - public ulong Amount {get; set;} - - [JsonPropertyName("unit")] - public string Unit {get; set;} - - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] - [JsonPropertyName("description")] - public string? Description {get; set;} -} \ No newline at end of file + [JsonPropertyName("amount")] + public ulong Amount { get; set; } + + [JsonPropertyName("unit")] + public string Unit { get; set; } + + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("pubkey")] + public string? Pubkey { get; set; } + + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("description")] + public string? Description { get; set; } +} diff --git a/DotNut/ApiModels/Mint/bolt11/PostMintQuoteBolt11Response.cs b/DotNut/ApiModels/Mint/bolt11/PostMintQuoteBolt11Response.cs index 8980825..c7f579d 100644 --- a/DotNut/ApiModels/Mint/bolt11/PostMintQuoteBolt11Response.cs +++ b/DotNut/ApiModels/Mint/bolt11/PostMintQuoteBolt11Response.cs @@ -4,25 +4,29 @@ namespace DotNut.ApiModels; public class PostMintQuoteBolt11Response { - [JsonPropertyName("quote")] + [JsonPropertyName("quote")] public string Quote { get; set; } - - [JsonPropertyName("request")] + + [JsonPropertyName("request")] public string Request { get; set; } - - [JsonPropertyName("state")] + + [JsonPropertyName("state")] public string State { get; set; } - + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - [JsonPropertyName("expiry")] + [JsonPropertyName("expiry")] public int? Expiry { get; set; } - + // 'amount' and 'unit' were recently added to the spec in PostMintQuoteBolt11Response, so they are optional for now [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] [JsonPropertyName("amount")] public ulong? Amount { get; set; } - + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] [JsonPropertyName("unit")] - public string? Unit {get; set;} -} \ No newline at end of file + public string? Unit { get; set; } + + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("pubkey")] + public string? PubKey { get; set; } +} diff --git a/DotNut/ApiModels/Mint/bolt12/PostMintQuoteBolt12Request.cs b/DotNut/ApiModels/Mint/bolt12/PostMintQuoteBolt12Request.cs index d00c3d1..40428e2 100644 --- a/DotNut/ApiModels/Mint/bolt12/PostMintQuoteBolt12Request.cs +++ b/DotNut/ApiModels/Mint/bolt12/PostMintQuoteBolt12Request.cs @@ -7,15 +7,14 @@ public class PostMintQuoteBolt12Request [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] [JsonPropertyName("amount")] public ulong? Amount { get; set; } - + [JsonPropertyName("unit")] - public string Unit {get; set;} - + public string Unit { get; set; } + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] [JsonPropertyName("description")] public string? Description { get; set; } - + [JsonPropertyName("pubkey")] public string Pubkey { get; set; } - -} \ No newline at end of file +} diff --git a/DotNut/ApiModels/Mint/bolt12/PostMintQuoteBolt12Response.cs b/DotNut/ApiModels/Mint/bolt12/PostMintQuoteBolt12Response.cs index cbb8a6b..ea1663b 100644 --- a/DotNut/ApiModels/Mint/bolt12/PostMintQuoteBolt12Response.cs +++ b/DotNut/ApiModels/Mint/bolt12/PostMintQuoteBolt12Response.cs @@ -6,27 +6,27 @@ public class PostMintQuoteBolt12Response { [JsonPropertyName("quote")] public string Quote { get; set; } - + [JsonPropertyName("request")] - public string Request {get; set;} - + public string Request { get; set; } + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] [JsonPropertyName("amount")] public ulong? Amount { get; set; } - + [JsonPropertyName("unit")] - public string Unit {get; set;} - + public string Unit { get; set; } + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] [JsonPropertyName("expiry")] - public int? Expiry {get; set;} - + public int? Expiry { get; set; } + [JsonPropertyName("pubkey")] - public string Pubkey {get; set;} - + public string Pubkey { get; set; } + [JsonPropertyName("amount_paid")] - public ulong AmountPaid {get; set;} - + public ulong AmountPaid { get; set; } + [JsonPropertyName("amount_issued")] - public ulong AmountIssued {get; set;} -} \ No newline at end of file + public ulong AmountIssued { get; set; } +} diff --git a/DotNut/ApiModels/PostCheckStateRequest.cs b/DotNut/ApiModels/PostCheckStateRequest.cs index 3421fd2..2860710 100644 --- a/DotNut/ApiModels/PostCheckStateRequest.cs +++ b/DotNut/ApiModels/PostCheckStateRequest.cs @@ -6,4 +6,4 @@ public class PostCheckStateRequest { [JsonPropertyName("Ys")] public string[] Ys { get; set; } -} \ No newline at end of file +} diff --git a/DotNut/ApiModels/PostCheckStateResponse.cs b/DotNut/ApiModels/PostCheckStateResponse.cs index 475881b..a197fdf 100644 --- a/DotNut/ApiModels/PostCheckStateResponse.cs +++ b/DotNut/ApiModels/PostCheckStateResponse.cs @@ -4,7 +4,6 @@ namespace DotNut.ApiModels; public class PostCheckStateResponse { - [JsonPropertyName("states")] public StateResponseItem[] States { get; set; } -} \ No newline at end of file +} diff --git a/DotNut/ApiModels/PostRestoreRequest.cs b/DotNut/ApiModels/PostRestoreRequest.cs index e46e872..d0d8c6b 100644 --- a/DotNut/ApiModels/PostRestoreRequest.cs +++ b/DotNut/ApiModels/PostRestoreRequest.cs @@ -6,4 +6,4 @@ public class PostRestoreRequest { [JsonPropertyName("outputs")] public BlindedMessage[] Outputs { get; set; } -} \ No newline at end of file +} diff --git a/DotNut/ApiModels/PostRestoreResponse.cs b/DotNut/ApiModels/PostRestoreResponse.cs index 7af6dfa..6b05a00 100644 --- a/DotNut/ApiModels/PostRestoreResponse.cs +++ b/DotNut/ApiModels/PostRestoreResponse.cs @@ -6,6 +6,7 @@ public class PostRestoreResponse { [JsonPropertyName("outputs")] public BlindedMessage[] Outputs { get; set; } + [JsonPropertyName("signatures")] public BlindSignature[] Signatures { get; set; } -} \ No newline at end of file +} diff --git a/DotNut/ApiModels/PostSwapRequest.cs b/DotNut/ApiModels/PostSwapRequest.cs index a018dce..cffafee 100644 --- a/DotNut/ApiModels/PostSwapRequest.cs +++ b/DotNut/ApiModels/PostSwapRequest.cs @@ -4,6 +4,9 @@ namespace DotNut.ApiModels; public class PostSwapRequest { - [JsonPropertyName("inputs")] public Proof[] Inputs { get; set; } - [JsonPropertyName("outputs")] public BlindedMessage[] Outputs { get; set; } -} \ No newline at end of file + [JsonPropertyName("inputs")] + public Proof[] Inputs { get; set; } + + [JsonPropertyName("outputs")] + public BlindedMessage[] Outputs { get; set; } +} diff --git a/DotNut/ApiModels/PostSwapResponse.cs b/DotNut/ApiModels/PostSwapResponse.cs index 5bb3caf..2103035 100644 --- a/DotNut/ApiModels/PostSwapResponse.cs +++ b/DotNut/ApiModels/PostSwapResponse.cs @@ -4,5 +4,6 @@ namespace DotNut.ApiModels; public class PostSwapResponse { - [JsonPropertyName("signatures")] public BlindSignature[] Signatures { get; set; } -} \ No newline at end of file + [JsonPropertyName("signatures")] + public BlindSignature[] Signatures { get; set; } +} diff --git a/DotNut/ApiModels/StateResponseItem.cs b/DotNut/ApiModels/StateResponseItem.cs index 5d0a05c..c670f97 100644 --- a/DotNut/ApiModels/StateResponseItem.cs +++ b/DotNut/ApiModels/StateResponseItem.cs @@ -4,8 +4,8 @@ namespace DotNut.ApiModels; public class StateResponseItem { - public string Y { get; set; } + [JsonConverter(typeof(JsonStringEnumConverter))] public TokenState State { get; set; } public string? Witness { get; set; } @@ -14,6 +14,6 @@ public enum TokenState { UNSPENT, PENDING, - SPENT + SPENT, } -} \ No newline at end of file +} diff --git a/DotNut/BlindedMessage.cs b/DotNut/BlindedMessage.cs deleted file mode 100644 index b6fbb45..0000000 --- a/DotNut/BlindedMessage.cs +++ /dev/null @@ -1,11 +0,0 @@ -using System.Text.Json.Serialization; - -namespace DotNut; - -public class BlindedMessage -{ - [JsonPropertyName("amount")] public ulong Amount { get; set; } - [JsonPropertyName("id")] public KeysetId Id { get; set; } - [JsonPropertyName("B_")] public PubKey B_ { get; set; } - [JsonPropertyName("witness")][JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public string? Witness { get; set; } -} \ No newline at end of file diff --git a/DotNut/CashuToken.cs b/DotNut/CashuToken.cs deleted file mode 100644 index 4efb1cb..0000000 --- a/DotNut/CashuToken.cs +++ /dev/null @@ -1,22 +0,0 @@ -using System.Text.Json.Serialization; - -namespace DotNut; - -public class CashuToken -{ - public class Token - { - [JsonPropertyName("mint")] public string Mint { get; set; } - [JsonPropertyName("proofs")] public List Proofs { get; set; } - } - - [JsonPropertyName("token")] public List Tokens { get; set; } - - [JsonPropertyName("unit")] - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - public string? Unit { get; set; } - - [JsonPropertyName("memo")] - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - public string? Memo { get; set; } -} \ No newline at end of file diff --git a/DotNut/DLEQ.cs b/DotNut/DLEQ.cs deleted file mode 100644 index 9c7b973..0000000 --- a/DotNut/DLEQ.cs +++ /dev/null @@ -1,9 +0,0 @@ -using System.Text.Json.Serialization; - -namespace DotNut; - -public class DLEQ -{ - [JsonPropertyName("e")] public PrivKey E { get; set; } - [JsonPropertyName("s")] public PrivKey S { get; set; } -} \ No newline at end of file diff --git a/DotNut/DLEQProof.cs b/DotNut/DLEQProof.cs deleted file mode 100644 index a569e12..0000000 --- a/DotNut/DLEQProof.cs +++ /dev/null @@ -1,8 +0,0 @@ -using System.Text.Json.Serialization; - -namespace DotNut; - -public class DLEQProof: DLEQ -{ - [JsonPropertyName("r")] public PrivKey R { get; set; } -} \ No newline at end of file diff --git a/DotNut/DotNut.csproj b/DotNut/DotNut.csproj index 2710efb..1eec5fc 100644 --- a/DotNut/DotNut.csproj +++ b/DotNut/DotNut.csproj @@ -1,27 +1,24 @@  + + net8.0 + enable + enable + true + DotNut + Kukks + A full C# native implementation of the Cashu protocol + MIT + https://github.com/Kukks/DotNut + 2.0.0 + https://github.com/Kukks/DotNut + git + bitcoin cashu ecash secp256k1 + https://github.com/Kukks/DotNut/blob/master/LICENSE + - - net8.0 - enable - enable - true - DotNut - Kukks - A full C# native implementation of the Cashu protocol - MIT - https://github.com/Kukks/DotNut - 1.0.6 - https://github.com/Kukks/DotNut - git - bitcoin cashu ecash secp256k1 - https://github.com/Kukks/DotNut/blob/master/LICENSE - - - - - - - - - + + + + + diff --git a/DotNut/Encoding/Base64UrlSafe.cs b/DotNut/Encoding/Base64UrlSafe.cs index dfa0e44..1d0e9fa 100644 --- a/DotNut/Encoding/Base64UrlSafe.cs +++ b/DotNut/Encoding/Base64UrlSafe.cs @@ -2,13 +2,17 @@ public static class Base64UrlSafe { - static readonly char[] padding = {'='}; + static readonly char[] padding = { '=' }; //(base64 encoding with / replaced by _ and + by -) public static string Encode(byte[] data) { - return System.Convert.ToBase64String(data) - .TrimEnd(padding).Replace('+', '-').Replace('/', '_').TrimEnd(padding); + return System + .Convert.ToBase64String(data) + .TrimEnd(padding) + .Replace('+', '-') + .Replace('/', '_') + .TrimEnd(padding); } public static byte[] Decode(string base64) @@ -26,4 +30,4 @@ public static byte[] Decode(string base64) return System.Convert.FromBase64String(incoming); } -} \ No newline at end of file +} diff --git a/DotNut/Encoding/CashuTokenHelper.cs b/DotNut/Encoding/CashuTokenHelper.cs index e7de7a5..e69b290 100644 --- a/DotNut/Encoding/CashuTokenHelper.cs +++ b/DotNut/Encoding/CashuTokenHelper.cs @@ -26,7 +26,7 @@ public static string Encode(this CashuToken token, string version = "B", bool ma foreach (var token1 in token.Tokens) { if (token1.Mint.EndsWith("/")) - { + { token1.Mint = token1.Mint.TrimEnd('/'); } foreach (var proof in token1.Proofs) @@ -34,7 +34,7 @@ public static string Encode(this CashuToken token, string version = "B", bool ma proof.Id = MaybeShortenId(proof.Id); } } - + var encoded = encoder.Encode(token); var result = $"{CashuPrefix}{version}{encoded}"; @@ -47,7 +47,11 @@ public static string Encode(this CashuToken token, string version = "B", bool ma return result; } - public static CashuToken Decode(string token, out string? version, List? keysetIds = null) + public static CashuToken Decode( + string token, + out string? version, + List? keysetIds = null + ) { version = null; if (Uri.IsWellFormedUriString(token, UriKind.Absolute)) @@ -70,7 +74,7 @@ public static CashuToken Decode(string token, out string? version, List MapShortKeysetIds(List proofs, List? keysetIds = null) + + private static List MapShortKeysetIds( + List proofs, + List? keysetIds = null + ) { - if (proofs.Count == 0 || proofs.All(p => p.Id.GetVersion() != 0x01 || p.Id.ToString().Length != 16)) + if ( + proofs.Count == 0 + || proofs.All(p => p.Id.GetVersion() != 0x01 || p.Id.ToString().Length != 16) + ) { return proofs; } if (keysetIds is null) { - throw new ArgumentNullException(nameof(keysetIds), - "Encountered short keyset IDs but no keysets were provided for mapping."); + throw new ArgumentNullException( + nameof(keysetIds), + "Encountered short keyset IDs but no keysets were provided for mapping." + ); } - - return proofs.Select(proof => - { - if (proof.Id.GetVersion() != 0x01) - return proof; - - var proofShortId = proof.Id.ToString(); - var match = keysetIds.FirstOrDefault(k=> k.ToString().StartsWith(proofShortId, StringComparison.OrdinalIgnoreCase)); - - if (match is null) - throw new Exception($"Couldn't map short keyset ID {proof.Id} to any known keysets of the current Mint"); - return new Proof + return proofs + .Select(proof => { - Amount = proof.Amount, - Secret = proof.Secret, - C = proof.C, - Witness = proof.Witness, - DLEQ = proof.DLEQ, - Id = match - }; - }).ToList(); + if (proof.Id.GetVersion() != 0x01) + return proof; + + var proofShortId = proof.Id.ToString(); + var match = keysetIds.FirstOrDefault(k => + k.ToString().StartsWith(proofShortId, StringComparison.OrdinalIgnoreCase) + ); + + if (match is null) + throw new Exception( + $"Couldn't map short keyset ID {proof.Id} to any known keysets of the current Mint" + ); + + return new Proof + { + Amount = proof.Amount, + Secret = proof.Secret, + C = proof.C, + Witness = proof.Witness, + DLEQ = proof.DLEQ, + Id = match, + }; + }) + .ToList(); } -} \ No newline at end of file +} diff --git a/DotNut/Encoding/CashuTokenV3Encoder.cs b/DotNut/Encoding/CashuTokenV3Encoder.cs index 8685ef4..2f94d89 100644 --- a/DotNut/Encoding/CashuTokenV3Encoder.cs +++ b/DotNut/Encoding/CashuTokenV3Encoder.cs @@ -16,4 +16,4 @@ public CashuToken Decode(string token) var json = Encoding.UTF8.GetString(Base64UrlSafe.Decode(token)); return JsonSerializer.Deserialize(json)!; } -} \ No newline at end of file +} diff --git a/DotNut/Encoding/CashuTokenV4Encoder.cs b/DotNut/Encoding/CashuTokenV4Encoder.cs index 480cfd4..47b7b85 100644 --- a/DotNut/Encoding/CashuTokenV4Encoder.cs +++ b/DotNut/Encoding/CashuTokenV4Encoder.cs @@ -26,24 +26,32 @@ public CBORObject ToCBORObject(CashuToken token) if (mints.Distinct().Count() != 1) throw new FormatException("All proofs must have the same mint in v4 tokens"); var proofSets = CBORObject.NewArray(); - foreach (var proofSet in token.Tokens.SelectMany(token1 => token1.Proofs).GroupBy(proof => proof.Id)) + foreach ( + var proofSet in token + .Tokens.SelectMany(token1 => token1.Proofs) + .GroupBy(proof => proof.Id) + ) { var proofSetItem = CBORObject.NewOrderedMap(); proofSetItem.Add("i", Convert.FromHexString(proofSet.Key.ToString())); var proofSetItemArray = CBORObject.NewArray(); foreach (var proof in proofSet) { - var proofItem = CBORObject.NewOrderedMap() + var proofItem = CBORObject + .NewOrderedMap() .Add("a", proof.Amount) .Add("s", Encoding.UTF8.GetString(proof.Secret.GetBytes())) .Add("c", proof.C.Key.ToBytes()); if (proof.DLEQ is not null) { - proofItem.Add("d", CBORObject - .NewOrderedMap() - .Add("e", proof.DLEQ.E.Key.ToBytes()) - .Add("s", proof.DLEQ.S.Key.ToBytes()) - .Add("r", proof.DLEQ.R.Key.ToBytes())); + proofItem.Add( + "d", + CBORObject + .NewOrderedMap() + .Add("e", proof.DLEQ.E.Key.ToBytes()) + .Add("s", proof.DLEQ.S.Key.ToBytes()) + .Add("r", proof.DLEQ.R.Key.ToBytes()) + ); } if (proof.Witness is not null) @@ -53,7 +61,7 @@ public CBORObject ToCBORObject(CashuToken token) if (proof.P2PkE?.Key is not null) { - proofItem.Add("pe", Convert.FromHexString(proof.P2PkE.Key.ToString())); + proofItem.Add("pe", proof.P2PkE.Key.ToBytes()); } proofSetItemArray.Add(proofItem); @@ -64,13 +72,10 @@ public CBORObject ToCBORObject(CashuToken token) } var cbor = CBORObject.NewOrderedMap(); - if (token.Memo is not null) cbor.Add("d", token.Memo); - cbor.Add("t", proofSets) - .Add("m", mints.First()) - .Add("u", token.Unit!); + cbor.Add("t", proofSets).Add("m", mints.First()).Add("u", token.Unit!); return cbor; } @@ -85,33 +90,42 @@ public CashuToken FromCBORObject(CBORObject obj) new CashuToken.Token() { Mint = obj["m"].AsString(), - Proofs = obj["t"].Values.SelectMany(proofSet => - { - var id = new KeysetId(Convert.ToHexString(proofSet["i"].GetByteString()).ToLowerInvariant()); - - return proofSet["p"].Values.Select(proof => new Proof() + Proofs = obj["t"] + .Values.SelectMany(proofSet => { - Amount = proof["a"].ToObject(), - Secret = JsonSerializer.Deserialize(proof["s"].ToJSONString())!, - C = ECPubKey.Create(proof["c"].GetByteString()), - Witness = proof.GetOrDefault("w", null)?.AsString(), - DLEQ = proof.GetOrDefault("d", null) is { } cborDLEQ - ? new DLEQProof + var id = new KeysetId( + Convert + .ToHexString(proofSet["i"].GetByteString()) + .ToLowerInvariant() + ); + + return proofSet["p"] + .Values.Select(proof => new Proof() { - E = ECPrivKey.Create(cborDLEQ["e"].GetByteString()), - S = ECPrivKey.Create(cborDLEQ["s"].GetByteString()), - R = ECPrivKey.Create(cborDLEQ["r"].GetByteString()) - } - : null, - Id = id, - - P2PkE = proof.GetOrDefault("pe", null) is { } p2pkE? - ECPubKey.Create(p2pkE.GetByteString()) : null - - }); - }).ToList() - } - ] + Amount = proof["a"].ToObject(), + Secret = JsonSerializer.Deserialize( + proof["s"].ToJSONString() + )!, + C = ECPubKey.Create(proof["c"].GetByteString()), + Witness = proof.GetOrDefault("w", null)?.AsString(), + DLEQ = proof.GetOrDefault("d", null) is { } cborDLEQ + ? new DLEQProof + { + E = ECPrivKey.Create(cborDLEQ["e"].GetByteString()), + S = ECPrivKey.Create(cborDLEQ["s"].GetByteString()), + R = ECPrivKey.Create(cborDLEQ["r"].GetByteString()), + } + : null, + Id = id, + + P2PkE = proof.GetOrDefault("pe", null) is { } p2pkE + ? (PubKey?)ECPubKey.Create(p2pkE.GetByteString()) + : null, + }); + }) + .ToList(), + }, + ], }; } -} \ No newline at end of file +} diff --git a/DotNut/Encoding/ConvertUtils.cs b/DotNut/Encoding/ConvertUtils.cs index de38e7e..cacc3a7 100644 --- a/DotNut/Encoding/ConvertUtils.cs +++ b/DotNut/Encoding/ConvertUtils.cs @@ -13,4 +13,4 @@ public static ECPrivKey ToPrivKey(this string hex) { return ECPrivKey.Create(global::System.Convert.FromHexString(hex)); } -} \ No newline at end of file +} diff --git a/DotNut/Encoding/ICashuTokenEncoder.cs b/DotNut/Encoding/ICashuTokenEncoder.cs index 6898820..93c7d61 100644 --- a/DotNut/Encoding/ICashuTokenEncoder.cs +++ b/DotNut/Encoding/ICashuTokenEncoder.cs @@ -4,5 +4,4 @@ public interface ICashuTokenEncoder { string Encode(CashuToken token); CashuToken Decode(string token); - -} \ No newline at end of file +} diff --git a/DotNut/PaymentRequestBech32Encoder.cs b/DotNut/Encoding/PaymentRequestBech32Encoder.cs similarity index 92% rename from DotNut/PaymentRequestBech32Encoder.cs rename to DotNut/Encoding/PaymentRequestBech32Encoder.cs index af50844..0aeacea 100644 --- a/DotNut/PaymentRequestBech32Encoder.cs +++ b/DotNut/Encoding/PaymentRequestBech32Encoder.cs @@ -20,7 +20,7 @@ private enum TlvTag : byte Mint = 0x05, Description = 0x06, Transport = 0x07, - Nut10 = 0x08 + Nut10 = 0x08, } public static string Encode(PaymentRequest paymentRequest) @@ -29,12 +29,15 @@ public static string Encode(PaymentRequest paymentRequest) EncodeTLV(writer, paymentRequest); var tlvBytes = writer.WrittenSpan; - Span words = tlvBytes.Length * 2 > 1024 - ? new byte[tlvBytes.Length * 2] - : stackalloc byte[tlvBytes.Length * 2]; + Span words = + tlvBytes.Length * 2 > 1024 + ? new byte[tlvBytes.Length * 2] + : stackalloc byte[tlvBytes.Length * 2]; var wordsLen = ConvertBits(tlvBytes, words, 8, 5, true); - return Encoder.EncodeRaw(words[..wordsLen].ToArray(), Bech32EncodingType.BECH32M).ToUpperInvariant(); + return Encoder + .EncodeRaw(words[..wordsLen].ToArray(), Bech32EncodingType.BECH32M) + .ToUpperInvariant(); } public static PaymentRequest Decode(string creqb) @@ -117,7 +120,7 @@ private static void EncodeNut10(IBufferWriter writer, Nut10LockingConditio { "P2PK" => (byte)0x00, "HTLC" => (byte)0x01, - _ => throw new ArgumentException("Unknown nut10 kind!") + _ => throw new ArgumentException("Unknown nut10 kind!"), }; WriteTlv(writer, 0x01, [kindByte]); WriteTlvUtf8(writer, 0x02, nut10.Data); @@ -128,7 +131,10 @@ private static void EncodeNut10(IBufferWriter writer, Nut10LockingConditio } } - private static void EncodeTransport(IBufferWriter writer, PaymentRequestTransport transport) + private static void EncodeTransport( + IBufferWriter writer, + PaymentRequestTransport transport + ) { switch (transport.Type.ToLowerInvariant()) { @@ -152,7 +158,6 @@ private static void EncodeTransport(IBufferWriter writer, PaymentRequestTr WriteTagTuple(writer, 0x03, ["r", relay]); } - foreach (var tag in transport.Tags ?? []) { WriteTagTuple(writer, 0x03, tag.ToArray()); @@ -164,7 +169,11 @@ private static void EncodeTransport(IBufferWriter writer, PaymentRequestTr } } - private static void WriteTagTuple(IBufferWriter writer, byte tag, ReadOnlySpan tuple) + private static void WriteTagTuple( + IBufferWriter writer, + byte tag, + ReadOnlySpan tuple + ) { // Calculate total size for the tuple data var totalLen = 0; @@ -196,8 +205,8 @@ private static void WriteTagTuple(IBufferWriter writer, byte tag, ReadOnly writer.Advance(3 + totalLen); } - private static void WriteTlv(IBufferWriter writer, TlvTag tag, ReadOnlySpan data) - => WriteTlv(writer, (byte)tag, data); + private static void WriteTlv(IBufferWriter writer, TlvTag tag, ReadOnlySpan data) => + WriteTlv(writer, (byte)tag, data); private static void WriteTlv(IBufferWriter writer, byte tag, ReadOnlySpan data) { @@ -211,8 +220,8 @@ private static void WriteTlv(IBufferWriter writer, byte tag, ReadOnlySpan< writer.Advance(3 + data.Length); } - private static void WriteTlvUtf8(IBufferWriter writer, TlvTag tag, string value) - => WriteTlvUtf8(writer, (byte)tag, value); + private static void WriteTlvUtf8(IBufferWriter writer, TlvTag tag, string value) => + WriteTlvUtf8(writer, (byte)tag, value); private static void WriteTlvUtf8(IBufferWriter writer, byte tag, string value) { @@ -251,7 +260,10 @@ private static PaymentRequest DecodeTLV(ReadOnlySpan data) pr.Amount = BinaryPrimitives.ReadUInt64BigEndian(value); break; case 0x03: - pr.Unit = value.Length == 1 && value[0] == 0x00 ? "sat" : Encoding.UTF8.GetString(value); + pr.Unit = + value.Length == 1 && value[0] == 0x00 + ? "sat" + : Encoding.UTF8.GetString(value); break; case 0x04: pr.OneTimeUse = value.Length == 1 && value[0] == 0x01; @@ -301,7 +313,7 @@ private static PaymentRequestTransport DecodeTransport(ReadOnlySpan data) { 0x00 => "nostr", 0x01 => "post", - _ => throw new FormatException("Unknown transport kind") + _ => throw new FormatException("Unknown transport kind"), }; break; case 0x02: @@ -363,7 +375,7 @@ private static Nut10LockingCondition DecodeNut10(ReadOnlySpan data) { 0x00 => "P2PK", 0x01 => "HTLC", - _ => throw new FormatException("Unknown nut10 kind") + _ => throw new FormatException("Unknown nut10 kind"), }; break; case 0x02: @@ -453,7 +465,9 @@ private static (byte[] Pubkey, string[] Relays) DecodeNprofile(string nprofile) { case 0x00: if (length != 32) - throw new FormatException($"Invalid pubkey length: expected 32 bytes, got {length}"); + throw new FormatException( + $"Invalid pubkey length: expected 32 bytes, got {length}" + ); pubkey = tlvData.AsSpan(offset, 32).ToArray(); break; case 0x01: @@ -509,8 +523,8 @@ private static string EncodeNprofile(byte[] pubkey, string[] relays) return encoder.EncodeRaw(words, Bech32EncodingType.BECH32); } - private static byte[] ConvertBits(byte[] data, int fromBits, int toBits, bool pad) - => ConvertBits(data.AsSpan(), fromBits, toBits, pad); + private static byte[] ConvertBits(byte[] data, int fromBits, int toBits, bool pad) => + ConvertBits(data.AsSpan(), fromBits, toBits, pad); private static byte[] ConvertBits(ReadOnlySpan data, int fromBits, int toBits, bool pad) { @@ -521,7 +535,13 @@ private static byte[] ConvertBits(ReadOnlySpan data, int fromBits, int toB return output[..written].ToArray(); } - private static int ConvertBits(ReadOnlySpan data, Span output, int fromBits, int toBits, bool pad) + private static int ConvertBits( + ReadOnlySpan data, + Span output, + int fromBits, + int toBits, + bool pad + ) { var acc = 0; var bits = 0; diff --git a/DotNut/PaymentRequestEncoder.cs b/DotNut/Encoding/PaymentRequestEncoder.cs similarity index 68% rename from DotNut/PaymentRequestEncoder.cs rename to DotNut/Encoding/PaymentRequestEncoder.cs index 5fde53b..0eed2eb 100644 --- a/DotNut/PaymentRequestEncoder.cs +++ b/DotNut/Encoding/PaymentRequestEncoder.cs @@ -1,4 +1,4 @@ -using PeterO.Cbor; +using PeterO.Cbor; namespace DotNut; @@ -24,7 +24,8 @@ public CBORObject ToCBORObject(PaymentRequest paymentRequest) var transports = CBORObject.NewArray(); foreach (var transport in paymentRequest.Transports) { - var transportItem = CBORObject.NewMap() + var transportItem = CBORObject + .NewMap() .Add("t", transport.Type) .Add("a", transport.Target); if (transport.Tags is not null) @@ -46,6 +47,8 @@ public CBORObject ToCBORObject(PaymentRequest paymentRequest) transports.Add(transportItem); } + cbor.Add("t", transports); + if (paymentRequest.Nut10 is not null) { var nut10Obj = CBORObject.NewMap(); @@ -69,7 +72,6 @@ public CBORObject ToCBORObject(PaymentRequest paymentRequest) cbor.Add("nut10", nut10Obj); } - cbor.Add("t", transports); return cbor; } @@ -100,33 +102,38 @@ public PaymentRequest FromCBORObject(CBORObject obj) paymentRequest.Memo = value.AsString(); break; case "t": - paymentRequest.Transports = value.Values.Select(v => - { - var transport = new PaymentRequestTransport(); - foreach (var transportKey in v.Keys) + paymentRequest.Transports = value + .Values.Select(v => { - var transportValue = v[transportKey]; - switch (transportKey.AsString()) + var transport = new PaymentRequestTransport(); + foreach (var transportKey in v.Keys) { - case "t": - transport.Type = transportValue.AsString(); - break; - case "a": - transport.Target = transportValue.AsString(); - break; - case "g": - transport.Tags = transportValue.Values - .Where(tag => tag.Type == CBORType.Array) - .Select(tag => - new Tag(tag.Values.Select(cborObject => cborObject.AsString()).ToArray()) - ) - .ToArray(); - break; + var transportValue = v[transportKey]; + switch (transportKey.AsString()) + { + case "t": + transport.Type = transportValue.AsString(); + break; + case "a": + transport.Target = transportValue.AsString(); + break; + case "g": + transport.Tags = transportValue + .Values.Where(tag => tag.Type == CBORType.Array) + .Select(tag => new Tag( + tag.Values.Select(cborObject => + cborObject.AsString() + ) + .ToArray() + )) + .ToArray(); + break; + } } - } - return transport; - }).ToArray(); + return transport; + }) + .ToArray(); break; case "nut10": var lockingCondition = new Nut10LockingCondition(); @@ -142,11 +149,12 @@ public PaymentRequest FromCBORObject(CBORObject obj) lockingCondition.Data = nut10Value.AsString(); break; case "t": - lockingCondition.Tags = nut10Value.Values - .Where(tag => tag.Type == CBORType.Array) - .Select(tag => - new Tag(tag.Values.Select(cborObject => cborObject.AsString()).ToArray()) - ) + lockingCondition.Tags = nut10Value + .Values.Where(tag => tag.Type == CBORType.Array) + .Select(tag => new Tag( + tag.Values.Select(cborObject => cborObject.AsString()) + .ToArray() + )) .ToArray(); break; } @@ -155,7 +163,6 @@ public PaymentRequest FromCBORObject(CBORObject obj) break; } } - return paymentRequest; } -} \ No newline at end of file +} diff --git a/DotNut/FeeHelper.cs b/DotNut/FeeHelper.cs deleted file mode 100644 index f82757f..0000000 --- a/DotNut/FeeHelper.cs +++ /dev/null @@ -1,21 +0,0 @@ -using DotNut.ApiModels; - -namespace DotNut; - -public static class FeeHelper -{ - - public static ulong ComputeFee(this IEnumerable proofsToSpend, Dictionary keysetFees) - { - ulong sum = 0; - foreach (var proof in proofsToSpend) - { - if (keysetFees.TryGetValue(proof.Id, out var fee)) - { - sum += fee; - } - } - - return (sum + 999) / 1000; - } -} \ No newline at end of file diff --git a/DotNut/HTLCBuilder.cs b/DotNut/HTLCBuilder.cs deleted file mode 100644 index f236a71..0000000 --- a/DotNut/HTLCBuilder.cs +++ /dev/null @@ -1,82 +0,0 @@ -using NBitcoin.Secp256k1; - -namespace DotNut; - -public class HTLCBuilder : P2PKBuilder -{ - public string HashLock { get; set; } - - /* - * ugly hack to reuse P2PKBuilder for HTLCs. - * P2PKBuilder expects a pubkey in `data` field, but we need to store a hashlock instead - * - * we inject a dummy pubkey so the loader doesn’t break, then remove it after load/build. - */ - private static readonly PubKey _dummy = - "020000000000000000000000000000000000000000000000000000000000000001".ToPubKey(); - - public static HTLCBuilder Load(HTLCProofSecret proofSecret) - { - var hashLock = proofSecret.Data; - if (hashLock.Length != 64) // hex string - { - throw new ArgumentException("HashLock must be 32 bytes (64 chars hex)", nameof(HashLock)); - } - var tempProof = new P2PKProofSecret - { - Data = _dummy.ToString(), - Nonce = proofSecret.Nonce, - Tags = proofSecret.Tags - }; - - var innerbuilder = P2PKBuilder.Load(tempProof); - innerbuilder.Pubkeys = innerbuilder.Pubkeys.Except([_dummy.Key]).ToArray(); - return new HTLCBuilder() - { - HashLock = hashLock, - Lock = innerbuilder.Lock, - Pubkeys = innerbuilder.Pubkeys, - RefundPubkeys = innerbuilder.RefundPubkeys, - SignatureThreshold = innerbuilder.SignatureThreshold, - SigFlag = innerbuilder.SigFlag, - Nonce = innerbuilder.Nonce - }; - - } - - public new HTLCProofSecret Build() - { - if (HashLock.Length != 64) - { - throw new ArgumentException("HashLock must be 32 bytes (64 chars hex)", nameof(HashLock)); - } - var innerBuilder = new P2PKBuilder() - { - Lock = Lock, - Pubkeys = Pubkeys.ToArray(), - RefundPubkeys = RefundPubkeys, - SignatureThreshold = SignatureThreshold, - SigFlag = SigFlag, - Nonce = Nonce - }; - innerBuilder.Pubkeys = innerBuilder.Pubkeys.Prepend(_dummy.Key).ToArray(); - - var p2pkProof = innerBuilder.Build(); - return new HTLCProofSecret() - { - Data = HashLock, - Nonce = p2pkProof.Nonce, - Tags = p2pkProof.Tags - }; - } - - public new HTLCProofSecret BuildBlinded(KeysetId keysetId, out ECPubKey p2pkE) - { - throw new NotImplementedException(); - } - - public HTLCProofSecret BuildBlinded(KeysetId keysetId, ECPrivKey p2pke) - { - throw new NotImplementedException(); - } -} \ No newline at end of file diff --git a/DotNut/HTLCProofSecret.cs b/DotNut/HTLCProofSecret.cs deleted file mode 100644 index 1e646ca..0000000 --- a/DotNut/HTLCProofSecret.cs +++ /dev/null @@ -1,211 +0,0 @@ -using System.Text; -using System.Text.Json.Serialization; -using NBitcoin.Secp256k1; -using SHA256 = System.Security.Cryptography.SHA256; - -namespace DotNut; - -public class HTLCProofSecret : P2PKProofSecret -{ - public const string Key = "HTLC"; - - [JsonIgnore] public HTLCBuilder Builder => HTLCBuilder.Load(this); - - public override ECPubKey[] GetAllowedPubkeys(out int requiredSignatures) - { - var builder = Builder; - requiredSignatures = builder.SignatureThreshold; - return builder.Pubkeys; - } - - public override ECPubKey[] GetAllowedRefundPubkeys(out int? requiredSignatures) - { - var builder = Builder; - if (builder.Lock.HasValue && builder.Lock.Value.ToUnixTimeSeconds() < DateTimeOffset.Now.ToUnixTimeSeconds()) - { - if (builder.RefundPubkeys == null) - { - requiredSignatures = 0; // proof is spendable without any signature - return []; - } - requiredSignatures = builder.RefundSignatureThreshold ?? 1; - return [..builder.RefundPubkeys??[]]; - } - - requiredSignatures = null; // there's no refund condition :/ - return []; - } - - - - public HTLCWitness GenerateWitness(Proof proof, ECPrivKey[] keys, string preimage) - { - return GenerateWitness(proof.Secret.GetBytes(), keys, Convert.FromHexString(preimage)); - } - - public HTLCWitness GenerateWitness(BlindedMessage blindedMessage, ECPrivKey[] keys, string preimage) - { - return GenerateWitness(blindedMessage.B_.Key.ToBytes(), keys, Convert.FromHexString(preimage)); - } - - public HTLCWitness GenerateWitness(byte[] msg, ECPrivKey[] keys, byte[] preimage) - { - var hash = SHA256.HashData(msg); - return GenerateWitness(ECPrivKey.Create(hash), keys, preimage); - } - - public HTLCWitness GenerateWitness(ECPrivKey hash, ECPrivKey[] keys, byte[] preimage) - { - if (!VerifyPreimage(preimage)) - throw new InvalidOperationException("Invalid preimage"); - var p2pkhWitness = base.GenerateWitness(hash, keys); - return new HTLCWitness() - { - Signatures = p2pkhWitness.Signatures, - Preimage = Convert.ToHexString(preimage) - }; - } - - - - public HTLCWitness GenerateBlindWitness(Proof proof, ECPrivKey[] keys, byte[] preimage, KeysetId keysetId) - { - throw new NotImplementedException(); - } - public HTLCWitness GenerateBlindWitness(Proof proof, ECPrivKey[] keys, byte[] preimage, KeysetId keysetId, - ECPubKey P2PkE) - { - throw new NotImplementedException(); - } - - public HTLCWitness GenerateBlindWitness(BlindedMessage message, ECPrivKey[] keys, byte[] preimage, KeysetId keysetId, ECPubKey P2PkE) - { - throw new NotImplementedException(); - } - - public HTLCWitness GenerateBlindWitness(byte[] msg, ECPrivKey[] keys, byte[] preimage, KeysetId keysetId, - ECPubKey P2PkE) - { - throw new NotImplementedException(); - } - - public HTLCWitness GenerateBlindWitness(ECPrivKey hash, ECPrivKey[] keys, byte[] preimage, KeysetId keysetId, - ECPubKey P2PkE) - { - throw new NotImplementedException(); - } - - - - public bool VerifyPreimage(string preimage) - { - return Convert.FromHexString(Builder.HashLock).SequenceEqual(SHA256.HashData(Convert.FromHexString(preimage))); - } - - public bool VerifyPreimage(byte[] preimage) - { - return Convert.FromHexString(Builder.HashLock).SequenceEqual(SHA256.HashData(preimage)); - } - - public bool VerifyWitness(string message, HTLCWitness witness) - { - var hash = SHA256.HashData(Encoding.UTF8.GetBytes(message)); - return VerifyWitnessHash(hash, witness); - } - - public bool VerifyWitness(ISecret secret, HTLCWitness witness) - { - if (secret is not Nut10Secret {ProofSecret: HTLCProofSecret}) - { - return false; - } - - return VerifyWitness(secret.GetBytes(), witness); - } - - - [Obsolete("Use GenerateWitness(Proof proof, ECPrivKey[] keys, string preimage)")] - public override P2PKWitness GenerateWitness(Proof proof, ECPrivKey[] keys) - { - throw new InvalidOperationException("Use GenerateWitness(Proof proof, ECPrivKey[] keys, string preimage)"); - } - - [Obsolete("Use GenerateWitness(byte[] msg, ECPrivKey[] keys, byte[] preimage)")] - public override P2PKWitness GenerateWitness(BlindedMessage message, ECPrivKey[] keys) - { - throw new InvalidOperationException("Use GenerateWitness(BlindedMessage message, ECPrivKey[] keys, string preimage)"); - } - - [Obsolete("Use GenerateWitness(byte[] msg, ECPrivKey[] keys, byte[] preimage)")] - public override P2PKWitness GenerateWitness(byte[] msg, ECPrivKey[] keys) - { - throw new InvalidOperationException("Use GenerateWitness(byte[] msg, ECPrivKey[] keys, byte[] preimage)"); - } - - - [Obsolete("Use GenerateBlindWitness(Proof proof, ECPrivKey[] keys, byte[] preimage, KeysetId keysetId)")] - public override P2PKWitness GenerateBlindWitness(Proof proof, ECPrivKey[] keys, KeysetId keysetId) - { - throw new InvalidOperationException(); - } - - [Obsolete("Use GenerateBlindWitness(Proof proof, ECPrivKey[] keys, byte[] preimage, KeysetId keysetId, ECPubKey P2PkE)")] - public override P2PKWitness GenerateBlindWitness(Proof proof, ECPrivKey[] keys, KeysetId keysetId, ECPubKey P2PkE) - { - throw new InvalidOperationException("Use GenerateBlindWitness(Proof proof, ECPrivKey[] keys, byte[] preimage, KeysetId keysetId, ECPubKey P2PkE)"); - } - - [Obsolete("Use GenerateBlindWitness(BlindedMessage message, ECPrivKey[] keys, byte[] preimage, KeysetId keysetId, ECPubKey P2PkE)")] - public override P2PKWitness GenerateBlindWitness(BlindedMessage message, ECPrivKey[] keys, KeysetId keysetId, - ECPubKey P2PkE) - { - throw new InvalidOperationException("Use GenerateBlindWitness(BlindedMessage message, ECPrivKey[] keys, byte[] preimage, KeysetId keysetId, ECPubKey P2PkE)"); - } - - [Obsolete("Use GenerateBlindWitness(byte[] msg, ECPrivKey[] keys, byte[] preimage, KeysetId keysetId, ECPubKey P2PkE)")] - public override P2PKWitness GenerateBlindWitness(byte[] msg, ECPrivKey[] keys, KeysetId keysetId, ECPubKey P2PkE) - { - throw new InvalidOperationException("Use GenerateBlindWitness(byte[] msg, ECPrivKey[] keys, byte[] preimage, KeysetId keysetId, ECPubKey P2PkE)"); - } - - [Obsolete("Use GenerateBlindWitness(ECPrivKey hash, ECPrivKey[] keys, byte[] preimage, KeysetId keysetId, ECPubKey P2PkE)")] - public override P2PKWitness GenerateBlindWitness(ECPrivKey hash, ECPrivKey[] keys, KeysetId keysetId, - ECPubKey P2PkE) - { - throw new InvalidOperationException("Use GenerateBlindWitness(ECPrivKey hash, ECPrivKey[] keys, byte[] preimage, KeysetId keysetId, ECPubKey P2PkE)"); - } - - - public override P2PKWitness GenerateWitness(ECPrivKey hash, ECPrivKey[] keys) - { - return base.GenerateWitness(hash, keys); - } - - public override bool VerifyWitness(string message, P2PKWitness witness) - { - return base.VerifyWitness(message, witness); - } - - public override bool VerifyWitness(ISecret secret, P2PKWitness witness) - { - return base.VerifyWitness(secret, witness); - } - - public override bool VerifyWitness(byte[] message, P2PKWitness witness) - { - return base.VerifyWitness(message, witness); - } - public override bool VerifyWitnessHash(byte[] hash, P2PKWitness witness) - { - if (witness is not HTLCWitness htlcWitness) - { - return false; - } - if (!VerifyPreimage(htlcWitness.Preimage)) - { - return false; - } - - return base.VerifyWitnessHash(hash, witness); - } -} \ No newline at end of file diff --git a/DotNut/JsonConverters/KeysetIdJsonConverter.cs b/DotNut/JsonConverters/KeysetIdJsonConverter.cs index 465e377..56dd0e8 100644 --- a/DotNut/JsonConverters/KeysetIdJsonConverter.cs +++ b/DotNut/JsonConverters/KeysetIdJsonConverter.cs @@ -5,16 +5,22 @@ namespace DotNut.JsonConverters; public class KeysetIdJsonConverter : JsonConverter { - public override KeysetId? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + public override KeysetId? Read( + ref Utf8JsonReader reader, + Type typeToConvert, + JsonSerializerOptions options + ) { if (reader.TokenType == JsonTokenType.Null) { return null; } - if (reader.TokenType != JsonTokenType.String || - reader.GetString() is not { } str || - string.IsNullOrEmpty(str)) + if ( + reader.TokenType != JsonTokenType.String + || reader.GetString() is not { } str + || string.IsNullOrEmpty(str) + ) { throw new JsonException("Expected string"); } @@ -22,7 +28,11 @@ public class KeysetIdJsonConverter : JsonConverter return new KeysetId(str); } - public override void Write(Utf8JsonWriter writer, KeysetId? value, JsonSerializerOptions options) + public override void Write( + Utf8JsonWriter writer, + KeysetId? value, + JsonSerializerOptions options + ) { if (value is null) { @@ -32,4 +42,4 @@ public override void Write(Utf8JsonWriter writer, KeysetId? value, JsonSerialize writer.WriteStringValue(value.ToString()); } -} \ No newline at end of file +} diff --git a/DotNut/JsonConverters/KeysetJsonConverter.cs b/DotNut/JsonConverters/KeysetJsonConverter.cs index d15c997..5b7b4eb 100644 --- a/DotNut/JsonConverters/KeysetJsonConverter.cs +++ b/DotNut/JsonConverters/KeysetJsonConverter.cs @@ -5,7 +5,11 @@ namespace DotNut.JsonConverters; public class KeysetJsonConverter : JsonConverter { - public override Keyset? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + public override Keyset? Read( + ref Utf8JsonReader reader, + Type typeToConvert, + JsonSerializerOptions options + ) { if (reader.TokenType == JsonTokenType.Null) { @@ -21,7 +25,6 @@ public class KeysetJsonConverter : JsonConverter { if (reader.TokenType == JsonTokenType.EndObject) { - return keyset; } @@ -30,7 +33,7 @@ public class KeysetJsonConverter : JsonConverter { amount = reader.GetUInt64(); } - else if (reader.TokenType is JsonTokenType.String or JsonTokenType.PropertyName) + else if (reader.TokenType is JsonTokenType.String or JsonTokenType.PropertyName) { var str = reader.GetString(); if (string.IsNullOrEmpty(str)) @@ -42,10 +45,9 @@ public class KeysetJsonConverter : JsonConverter throw new JsonException("Expected number or string"); } - reader.Read(); var pubkey = JsonSerializer.Deserialize(ref reader, options); - if(pubkey is null || pubkey.Key.ToBytes().Length != 33) + if (pubkey is null || pubkey.Key.ToBytes().Length != 33) throw new JsonException("Invalid public key (not compressed?)"); keyset.Add(amount, pubkey); } @@ -70,4 +72,4 @@ public override void Write(Utf8JsonWriter writer, Keyset? value, JsonSerializerO writer.WriteEndObject(); } -} \ No newline at end of file +} diff --git a/DotNut/JsonConverters/Nut10SecretJsonConverter.cs b/DotNut/JsonConverters/Nut10SecretJsonConverter.cs index e38a874..7dbc211 100644 --- a/DotNut/JsonConverters/Nut10SecretJsonConverter.cs +++ b/DotNut/JsonConverters/Nut10SecretJsonConverter.cs @@ -5,20 +5,24 @@ namespace DotNut.JsonConverters; public class Nut10SecretJsonConverter : JsonConverter { - public override Nut10Secret? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + public override Nut10Secret? Read( + ref Utf8JsonReader reader, + Type typeToConvert, + JsonSerializerOptions options + ) { - if(reader.TokenType == JsonTokenType.Null) + if (reader.TokenType == JsonTokenType.Null) return null; if (reader.TokenType != JsonTokenType.StartArray) { throw new JsonException("Expected array"); } reader.Read(); - if(reader.TokenType != JsonTokenType.String) + if (reader.TokenType != JsonTokenType.String) throw new JsonException("Expected string"); var key = reader.GetString(); reader.Read(); - + Nut10ProofSecret? proofSecret; switch (key) { @@ -32,7 +36,7 @@ public class Nut10SecretJsonConverter : JsonConverter default: throw new JsonException("Unknown secret type"); } - if(proofSecret is null) + if (proofSecret is null) throw new JsonException("Invalid proof secret"); reader.Read(); if (reader.TokenType != JsonTokenType.EndArray) @@ -40,22 +44,24 @@ public class Nut10SecretJsonConverter : JsonConverter throw new JsonException("Expected end array"); } - return new Nut10Secret(key, proofSecret); - - + return new Nut10Secret(key, proofSecret); } - public override void Write(Utf8JsonWriter writer, Nut10Secret? value, JsonSerializerOptions options) + public override void Write( + Utf8JsonWriter writer, + Nut10Secret? value, + JsonSerializerOptions options + ) { if (value is null) { writer.WriteNullValue(); return; } - + writer.WriteStartArray(); JsonSerializer.Serialize(writer, value.Key, options); JsonSerializer.Serialize(writer, value.ProofSecret, options); writer.WriteEndArray(); } -} \ No newline at end of file +} diff --git a/DotNut/JsonConverters/PrivKeyJsonConverter.cs b/DotNut/JsonConverters/PrivKeyJsonConverter.cs index 6e57bf1..062a524 100644 --- a/DotNut/JsonConverters/PrivKeyJsonConverter.cs +++ b/DotNut/JsonConverters/PrivKeyJsonConverter.cs @@ -5,16 +5,22 @@ namespace DotNut.JsonConverters; public class PrivKeyJsonConverter : JsonConverter { - public override PrivKey? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + public override PrivKey? Read( + ref Utf8JsonReader reader, + Type typeToConvert, + JsonSerializerOptions options + ) { if (reader.TokenType == JsonTokenType.Null) { return null; } - if (reader.TokenType != JsonTokenType.String || - reader.GetString() is not { } str || - string.IsNullOrEmpty(str)) + if ( + reader.TokenType != JsonTokenType.String + || reader.GetString() is not { } str + || string.IsNullOrEmpty(str) + ) { throw new JsonException("Expected string"); } @@ -32,4 +38,4 @@ public override void Write(Utf8JsonWriter writer, PrivKey? value, JsonSerializer writer.WriteStringValue(value.ToString()); } -} \ No newline at end of file +} diff --git a/DotNut/JsonConverters/PubKeyJsonConverter.cs b/DotNut/JsonConverters/PubKeyJsonConverter.cs index c3413ae..30e270d 100644 --- a/DotNut/JsonConverters/PubKeyJsonConverter.cs +++ b/DotNut/JsonConverters/PubKeyJsonConverter.cs @@ -5,16 +5,22 @@ namespace DotNut.JsonConverters; public class PubKeyJsonConverter : JsonConverter { - public override PubKey? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + public override PubKey? Read( + ref Utf8JsonReader reader, + Type typeToConvert, + JsonSerializerOptions options + ) { if (reader.TokenType == JsonTokenType.Null) { return null; } - if (reader.TokenType != JsonTokenType.String || - reader.GetString() is not { } str || - string.IsNullOrEmpty(str)) + if ( + reader.TokenType != JsonTokenType.String + || reader.GetString() is not { } str + || string.IsNullOrEmpty(str) + ) { throw new JsonException("Expected string"); } @@ -32,4 +38,4 @@ public override void Write(Utf8JsonWriter writer, PubKey? value, JsonSerializerO writer.WriteStringValue(value.ToString()); } -} \ No newline at end of file +} diff --git a/DotNut/JsonConverters/SecretJsonConverter.cs b/DotNut/JsonConverters/SecretJsonConverter.cs index 7576b38..6a99ec1 100644 --- a/DotNut/JsonConverters/SecretJsonConverter.cs +++ b/DotNut/JsonConverters/SecretJsonConverter.cs @@ -5,7 +5,11 @@ namespace DotNut.JsonConverters; public class SecretJsonConverter : JsonConverter { - public override ISecret? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + public override ISecret? Read( + ref Utf8JsonReader reader, + Type typeToConvert, + JsonSerializerOptions options + ) { if (reader.TokenType == JsonTokenType.Null) { @@ -14,7 +18,7 @@ public class SecretJsonConverter : JsonConverter if (reader.TokenType == JsonTokenType.StartArray && reader.CurrentDepth == 0) { - //we are converting a nut10 secret directly + //we are converting a nut10 secret directly return JsonSerializer.Deserialize(ref reader, options); } if (reader.TokenType != JsonTokenType.String) @@ -33,7 +37,6 @@ public class SecretJsonConverter : JsonConverter } catch (Exception e) { - return new StringSecret(str); } } @@ -53,4 +56,4 @@ public override void Write(Utf8JsonWriter writer, ISecret? value, JsonSerializer break; } } -} \ No newline at end of file +} diff --git a/DotNut/JsonConverters/UnixDateTimeOffsetConverter.cs b/DotNut/JsonConverters/UnixDateTimeOffsetConverter.cs index 0ecfbec..30460b8 100644 --- a/DotNut/JsonConverters/UnixDateTimeOffsetConverter.cs +++ b/DotNut/JsonConverters/UnixDateTimeOffsetConverter.cs @@ -5,16 +5,26 @@ namespace DotNut.JsonConverters; public class UnixDateTimeOffsetConverter : JsonConverter { - public override DateTimeOffset Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + public override DateTimeOffset Read( + ref Utf8JsonReader reader, + Type typeToConvert, + JsonSerializerOptions options + ) { - var val = reader.TokenType == JsonTokenType.Number? reader.GetInt64() : long.Parse(reader.GetString()!); - + var val = + reader.TokenType == JsonTokenType.Number + ? reader.GetInt64() + : long.Parse(reader.GetString()!); return DateTimeOffset.FromUnixTimeSeconds(val); } - public override void Write(Utf8JsonWriter writer, DateTimeOffset value, JsonSerializerOptions options) + public override void Write( + Utf8JsonWriter writer, + DateTimeOffset value, + JsonSerializerOptions options + ) { writer.WriteNumberValue(value.ToUnixTimeSeconds()); } -} \ No newline at end of file +} diff --git a/DotNut/MeltMethodSetting.cs b/DotNut/MeltMethodSetting.cs deleted file mode 100644 index a89630b..0000000 --- a/DotNut/MeltMethodSetting.cs +++ /dev/null @@ -1,11 +0,0 @@ -using System.Text.Json.Serialization; - -namespace DotNut; - -public class MeltMethodSetting -{ - [JsonPropertyName("method")] public string Method { get; set; } - [JsonPropertyName("unit")] public string Unit { get; set; } - [JsonPropertyName("min_amount")] public ulong? Min { get; set; } - [JsonPropertyName("max_amount")] public ulong? Max { get; set; } -} \ No newline at end of file diff --git a/DotNut/MintMethodSetting.cs b/DotNut/MintMethodSetting.cs deleted file mode 100644 index 291c5a2..0000000 --- a/DotNut/MintMethodSetting.cs +++ /dev/null @@ -1,14 +0,0 @@ -using System.Text.Json; -using System.Text.Json.Serialization; - -namespace DotNut; - -public class MintMethodSetting -{ - [JsonPropertyName("method")] public string Method { get; set; } - [JsonPropertyName("unit")] public string Unit { get; set; } - [JsonPropertyName("min_amount")] public ulong? Min { get; set; } - [JsonPropertyName("max_amount")] public ulong? Max { get; set; } - [JsonPropertyName("options")] public JsonDocument? Options { get; set; } -} - diff --git a/DotNut/MultipathPaymentSetting.cs b/DotNut/MultipathPaymentSetting.cs deleted file mode 100644 index 90134a3..0000000 --- a/DotNut/MultipathPaymentSetting.cs +++ /dev/null @@ -1,10 +0,0 @@ -using System.Text.Json.Serialization; - -namespace DotNut; - -public class MultipathPaymentSetting -{ - [JsonPropertyName("method")] public string Method { get; set; } - [JsonPropertyName("unit")] public List Unit { get; set; } - [JsonPropertyName("mpp")] public bool MultiPathPayments { get; set; } -} \ No newline at end of file diff --git a/DotNut/NBitcoin/BIP39/HardcodedWordlistSource.cs b/DotNut/NBitcoin/BIP39/HardcodedWordlistSource.cs index 87b06dc..7110338 100644 --- a/DotNut/NBitcoin/BIP39/HardcodedWordlistSource.cs +++ b/DotNut/NBitcoin/BIP39/HardcodedWordlistSource.cs @@ -7,22 +7,38 @@ public class HardcodedWordlistSource : IWordlistSource static HardcodedWordlistSource() { var dico = new Dictionary(); - dico.Add("chinese_simplified", - "的\n一\n是\n在\n不\n了\n有\n和\n人\n这\n中\n大\n为\n上\n个\n国\n我\n以\n要\n他\n时\n来\n用\n们\n生\n到\n作\n地\n于\n出\n就\n分\n对\n成\n会\n可\n主\n发\n年\n动\n同\n工\n也\n能\n下\n过\n子\n说\n产\n种\n面\n而\n方\n后\n多\n定\n行\n学\n法\n所\n民\n得\n经\n十\n三\n之\n进\n着\n等\n部\n度\n家\n电\n力\n里\n如\n水\n化\n高\n自\n二\n理\n起\n小\n物\n现\n实\n加\n量\n都\n两\n体\n制\n机\n当\n使\n点\n从\n业\n本\n去\n把\n性\n好\n应\n开\n它\n合\n还\n因\n由\n其\n些\n然\n前\n外\n天\n政\n四\n日\n那\n社\n义\n事\n平\n形\n相\n全\n表\n间\n样\n与\n关\n各\n重\n新\n线\n内\n数\n正\n心\n反\n你\n明\n看\n原\n又\n么\n利\n比\n或\n但\n质\n气\n第\n向\n道\n命\n此\n变\n条\n只\n没\n结\n解\n问\n意\n建\n月\n公\n无\n系\n军\n很\n情\n者\n最\n立\n代\n想\n已\n通\n并\n提\n直\n题\n党\n程\n展\n五\n果\n料\n象\n员\n革\n位\n入\n常\n文\n总\n次\n品\n式\n活\n设\n及\n管\n特\n件\n长\n求\n老\n头\n基\n资\n边\n流\n路\n级\n少\n图\n山\n统\n接\n知\n较\n将\n组\n见\n计\n别\n她\n手\n角\n期\n根\n论\n运\n农\n指\n几\n九\n区\n强\n放\n决\n西\n被\n干\n做\n必\n战\n先\n回\n则\n任\n取\n据\n处\n队\n南\n给\n色\n光\n门\n即\n保\n治\n北\n造\n百\n规\n热\n领\n七\n海\n口\n东\n导\n器\n压\n志\n世\n金\n增\n争\n济\n阶\n油\n思\n术\n极\n交\n受\n联\n什\n认\n六\n共\n权\n收\n证\n改\n清\n美\n再\n采\n转\n更\n单\n风\n切\n打\n白\n教\n速\n花\n带\n安\n场\n身\n车\n例\n真\n务\n具\n万\n每\n目\n至\n达\n走\n积\n示\n议\n声\n报\n斗\n完\n类\n八\n离\n华\n名\n确\n才\n科\n张\n信\n马\n节\n话\n米\n整\n空\n元\n况\n今\n集\n温\n传\n土\n许\n步\n群\n广\n石\n记\n需\n段\n研\n界\n拉\n林\n律\n叫\n且\n究\n观\n越\n织\n装\n影\n算\n低\n持\n音\n众\n书\n布\n复\n容\n儿\n须\n际\n商\n非\n验\n连\n断\n深\n难\n近\n矿\n千\n周\n委\n素\n技\n备\n半\n办\n青\n省\n列\n习\n响\n约\n支\n般\n史\n感\n劳\n便\n团\n往\n酸\n历\n市\n克\n何\n除\n消\n构\n府\n称\n太\n准\n精\n值\n号\n率\n族\n维\n划\n选\n标\n写\n存\n候\n毛\n亲\n快\n效\n斯\n院\n查\n江\n型\n眼\n王\n按\n格\n养\n易\n置\n派\n层\n片\n始\n却\n专\n状\n育\n厂\n京\n识\n适\n属\n圆\n包\n火\n住\n调\n满\n县\n局\n照\n参\n红\n细\n引\n听\n该\n铁\n价\n严\n首\n底\n液\n官\n德\n随\n病\n苏\n失\n尔\n死\n讲\n配\n女\n黄\n推\n显\n谈\n罪\n神\n艺\n呢\n席\n含\n企\n望\n密\n批\n营\n项\n防\n举\n球\n英\n氧\n势\n告\n李\n台\n落\n木\n帮\n轮\n破\n亚\n师\n围\n注\n远\n字\n材\n排\n供\n河\n态\n封\n另\n施\n减\n树\n溶\n怎\n止\n案\n言\n士\n均\n武\n固\n叶\n鱼\n波\n视\n仅\n费\n紧\n爱\n左\n章\n早\n朝\n害\n续\n轻\n服\n试\n食\n充\n兵\n源\n判\n护\n司\n足\n某\n练\n差\n致\n板\n田\n降\n黑\n犯\n负\n击\n范\n继\n兴\n似\n余\n坚\n曲\n输\n修\n故\n城\n夫\n够\n送\n笔\n船\n占\n右\n财\n吃\n富\n春\n职\n觉\n汉\n画\n功\n巴\n跟\n虽\n杂\n飞\n检\n吸\n助\n升\n阳\n互\n初\n创\n抗\n考\n投\n坏\n策\n古\n径\n换\n未\n跑\n留\n钢\n曾\n端\n责\n站\n简\n述\n钱\n副\n尽\n帝\n射\n草\n冲\n承\n独\n令\n限\n阿\n宣\n环\n双\n请\n超\n微\n让\n控\n州\n良\n轴\n找\n否\n纪\n益\n依\n优\n顶\n础\n载\n倒\n房\n突\n坐\n粉\n敌\n略\n客\n袁\n冷\n胜\n绝\n析\n块\n剂\n测\n丝\n协\n诉\n念\n陈\n仍\n罗\n盐\n友\n洋\n错\n苦\n夜\n刑\n移\n频\n逐\n靠\n混\n母\n短\n皮\n终\n聚\n汽\n村\n云\n哪\n既\n距\n卫\n停\n烈\n央\n察\n烧\n迅\n境\n若\n印\n洲\n刻\n括\n激\n孔\n搞\n甚\n室\n待\n核\n校\n散\n侵\n吧\n甲\n游\n久\n菜\n味\n旧\n模\n湖\n货\n损\n预\n阻\n毫\n普\n稳\n乙\n妈\n植\n息\n扩\n银\n语\n挥\n酒\n守\n拿\n序\n纸\n医\n缺\n雨\n吗\n针\n刘\n啊\n急\n唱\n误\n训\n愿\n审\n附\n获\n茶\n鲜\n粮\n斤\n孩\n脱\n硫\n肥\n善\n龙\n演\n父\n渐\n血\n欢\n械\n掌\n歌\n沙\n刚\n攻\n谓\n盾\n讨\n晚\n粒\n乱\n燃\n矛\n乎\n杀\n药\n宁\n鲁\n贵\n钟\n煤\n读\n班\n伯\n香\n介\n迫\n句\n丰\n培\n握\n兰\n担\n弦\n蛋\n沉\n假\n穿\n执\n答\n乐\n谁\n顺\n烟\n缩\n征\n脸\n喜\n松\n脚\n困\n异\n免\n背\n星\n福\n买\n染\n井\n概\n慢\n怕\n磁\n倍\n祖\n皇\n促\n静\n补\n评\n翻\n肉\n践\n尼\n衣\n宽\n扬\n棉\n希\n伤\n操\n垂\n秋\n宜\n氢\n套\n督\n振\n架\n亮\n末\n宪\n庆\n编\n牛\n触\n映\n雷\n销\n诗\n座\n居\n抓\n裂\n胞\n呼\n娘\n景\n威\n绿\n晶\n厚\n盟\n衡\n鸡\n孙\n延\n危\n胶\n屋\n乡\n临\n陆\n顾\n掉\n呀\n灯\n岁\n措\n束\n耐\n剧\n玉\n赵\n跳\n哥\n季\n课\n凯\n胡\n额\n款\n绍\n卷\n齐\n伟\n蒸\n殖\n永\n宗\n苗\n川\n炉\n岩\n弱\n零\n杨\n奏\n沿\n露\n杆\n探\n滑\n镇\n饭\n浓\n航\n怀\n赶\n库\n夺\n伊\n灵\n税\n途\n灭\n赛\n归\n召\n鼓\n播\n盘\n裁\n险\n康\n唯\n录\n菌\n纯\n借\n糖\n盖\n横\n符\n私\n努\n堂\n域\n枪\n润\n幅\n哈\n竟\n熟\n虫\n泽\n脑\n壤\n碳\n欧\n遍\n侧\n寨\n敢\n彻\n虑\n斜\n薄\n庭\n纳\n弹\n饲\n伸\n折\n麦\n湿\n暗\n荷\n瓦\n塞\n床\n筑\n恶\n户\n访\n塔\n奇\n透\n梁\n刀\n旋\n迹\n卡\n氯\n遇\n份\n毒\n泥\n退\n洗\n摆\n灰\n彩\n卖\n耗\n夏\n择\n忙\n铜\n献\n硬\n予\n繁\n圈\n雪\n函\n亦\n抽\n篇\n阵\n阴\n丁\n尺\n追\n堆\n雄\n迎\n泛\n爸\n楼\n避\n谋\n吨\n野\n猪\n旗\n累\n偏\n典\n馆\n索\n秦\n脂\n潮\n爷\n豆\n忽\n托\n惊\n塑\n遗\n愈\n朱\n替\n纤\n粗\n倾\n尚\n痛\n楚\n谢\n奋\n购\n磨\n君\n池\n旁\n碎\n骨\n监\n捕\n弟\n暴\n割\n贯\n殊\n释\n词\n亡\n壁\n顿\n宝\n午\n尘\n闻\n揭\n炮\n残\n冬\n桥\n妇\n警\n综\n招\n吴\n付\n浮\n遭\n徐\n您\n摇\n谷\n赞\n箱\n隔\n订\n男\n吹\n园\n纷\n唐\n败\n宋\n玻\n巨\n耕\n坦\n荣\n闭\n湾\n键\n凡\n驻\n锅\n救\n恩\n剥\n凝\n碱\n齿\n截\n炼\n麻\n纺\n禁\n废\n盛\n版\n缓\n净\n睛\n昌\n婚\n涉\n筒\n嘴\n插\n岸\n朗\n庄\n街\n藏\n姑\n贸\n腐\n奴\n啦\n惯\n乘\n伙\n恢\n匀\n纱\n扎\n辩\n耳\n彪\n臣\n亿\n璃\n抵\n脉\n秀\n萨\n俄\n网\n舞\n店\n喷\n纵\n寸\n汗\n挂\n洪\n贺\n闪\n柬\n爆\n烯\n津\n稻\n墙\n软\n勇\n像\n滚\n厘\n蒙\n芳\n肯\n坡\n柱\n荡\n腿\n仪\n旅\n尾\n轧\n冰\n贡\n登\n黎\n削\n钻\n勒\n逃\n障\n氨\n郭\n峰\n币\n港\n伏\n轨\n亩\n毕\n擦\n莫\n刺\n浪\n秘\n援\n株\n健\n售\n股\n岛\n甘\n泡\n睡\n童\n铸\n汤\n阀\n休\n汇\n舍\n牧\n绕\n炸\n哲\n磷\n绩\n朋\n淡\n尖\n启\n陷\n柴\n呈\n徒\n颜\n泪\n稍\n忘\n泵\n蓝\n拖\n洞\n授\n镜\n辛\n壮\n锋\n贫\n虚\n弯\n摩\n泰\n幼\n廷\n尊\n窗\n纲\n弄\n隶\n疑\n氏\n宫\n姐\n震\n瑞\n怪\n尤\n琴\n循\n描\n膜\n违\n夹\n腰\n缘\n珠\n穷\n森\n枝\n竹\n沟\n催\n绳\n忆\n邦\n剩\n幸\n浆\n栏\n拥\n牙\n贮\n礼\n滤\n钠\n纹\n罢\n拍\n咱\n喊\n袖\n埃\n勤\n罚\n焦\n潜\n伍\n墨\n欲\n缝\n姓\n刊\n饱\n仿\n奖\n铝\n鬼\n丽\n跨\n默\n挖\n链\n扫\n喝\n袋\n炭\n污\n幕\n诸\n弧\n励\n梅\n奶\n洁\n灾\n舟\n鉴\n苯\n讼\n抱\n毁\n懂\n寒\n智\n埔\n寄\n届\n跃\n渡\n挑\n丹\n艰\n贝\n碰\n拔\n爹\n戴\n码\n梦\n芽\n熔\n赤\n渔\n哭\n敬\n颗\n奔\n铅\n仲\n虎\n稀\n妹\n乏\n珍\n申\n桌\n遵\n允\n隆\n螺\n仓\n魏\n锐\n晓\n氮\n兼\n隐\n碍\n赫\n拨\n忠\n肃\n缸\n牵\n抢\n博\n巧\n壳\n兄\n杜\n讯\n诚\n碧\n祥\n柯\n页\n巡\n矩\n悲\n灌\n龄\n伦\n票\n寻\n桂\n铺\n圣\n恐\n恰\n郑\n趣\n抬\n荒\n腾\n贴\n柔\n滴\n猛\n阔\n辆\n妻\n填\n撤\n储\n签\n闹\n扰\n紫\n砂\n递\n戏\n吊\n陶\n伐\n喂\n疗\n瓶\n婆\n抚\n臂\n摸\n忍\n虾\n蜡\n邻\n胸\n巩\n挤\n偶\n弃\n槽\n劲\n乳\n邓\n吉\n仁\n烂\n砖\n租\n乌\n舰\n伴\n瓜\n浅\n丙\n暂\n燥\n橡\n柳\n迷\n暖\n牌\n秧\n胆\n详\n簧\n踏\n瓷\n谱\n呆\n宾\n糊\n洛\n辉\n愤\n竞\n隙\n怒\n粘\n乃\n绪\n肩\n籍\n敏\n涂\n熙\n皆\n侦\n悬\n掘\n享\n纠\n醒\n狂\n锁\n淀\n恨\n牲\n霸\n爬\n赏\n逆\n玩\n陵\n祝\n秒\n浙\n貌\n役\n彼\n悉\n鸭\n趋\n凤\n晨\n畜\n辈\n秩\n卵\n署\n梯\n炎\n滩\n棋\n驱\n筛\n峡\n冒\n啥\n寿\n译\n浸\n泉\n帽\n迟\n硅\n疆\n贷\n漏\n稿\n冠\n嫩\n胁\n芯\n牢\n叛\n蚀\n奥\n鸣\n岭\n羊\n凭\n串\n塘\n绘\n酵\n融\n盆\n锡\n庙\n筹\n冻\n辅\n摄\n袭\n筋\n拒\n僚\n旱\n钾\n鸟\n漆\n沈\n眉\n疏\n添\n棒\n穗\n硝\n韩\n逼\n扭\n侨\n凉\n挺\n碗\n栽\n炒\n杯\n患\n馏\n劝\n豪\n辽\n勃\n鸿\n旦\n吏\n拜\n狗\n埋\n辊\n掩\n饮\n搬\n骂\n辞\n勾\n扣\n估\n蒋\n绒\n雾\n丈\n朵\n姆\n拟\n宇\n辑\n陕\n雕\n偿\n蓄\n崇\n剪\n倡\n厅\n咬\n驶\n薯\n刷\n斥\n番\n赋\n奉\n佛\n浇\n漫\n曼\n扇\n钙\n桃\n扶\n仔\n返\n俗\n亏\n腔\n鞋\n棱\n覆\n框\n悄\n叔\n撞\n骗\n勘\n旺\n沸\n孤\n吐\n孟\n渠\n屈\n疾\n妙\n惜\n仰\n狠\n胀\n谐\n抛\n霉\n桑\n岗\n嘛\n衰\n盗\n渗\n脏\n赖\n涌\n甜\n曹\n阅\n肌\n哩\n厉\n烃\n纬\n毅\n昨\n伪\n症\n煮\n叹\n钉\n搭\n茎\n笼\n酷\n偷\n弓\n锥\n恒\n杰\n坑\n鼻\n翼\n纶\n叙\n狱\n逮\n罐\n络\n棚\n抑\n膨\n蔬\n寺\n骤\n穆\n冶\n枯\n册\n尸\n凸\n绅\n坯\n牺\n焰\n轰\n欣\n晋\n瘦\n御\n锭\n锦\n丧\n旬\n锻\n垄\n搜\n扑\n邀\n亭\n酯\n迈\n舒\n脆\n酶\n闲\n忧\n酚\n顽\n羽\n涨\n卸\n仗\n陪\n辟\n惩\n杭\n姚\n肚\n捉\n飘\n漂\n昆\n欺\n吾\n郎\n烷\n汁\n呵\n饰\n萧\n雅\n邮\n迁\n燕\n撒\n姻\n赴\n宴\n烦\n债\n帐\n斑\n铃\n旨\n醇\n董\n饼\n雏\n姿\n拌\n傅\n腹\n妥\n揉\n贤\n拆\n歪\n葡\n胺\n丢\n浩\n徽\n昂\n垫\n挡\n览\n贪\n慰\n缴\n汪\n慌\n冯\n诺\n姜\n谊\n凶\n劣\n诬\n耀\n昏\n躺\n盈\n骑\n乔\n溪\n丛\n卢\n抹\n闷\n咨\n刮\n驾\n缆\n悟\n摘\n铒\n掷\n颇\n幻\n柄\n惠\n惨\n佳\n仇\n腊\n窝\n涤\n剑\n瞧\n堡\n泼\n葱\n罩\n霍\n捞\n胎\n苍\n滨\n俩\n捅\n湘\n砍\n霞\n邵\n萄\n疯\n淮\n遂\n熊\n粪\n烘\n宿\n档\n戈\n驳\n嫂\n裕\n徙\n箭\n捐\n肠\n撑\n晒\n辨\n殿\n莲\n摊\n搅\n酱\n屏\n疫\n哀\n蔡\n堵\n沫\n皱\n畅\n叠\n阁\n莱\n敲\n辖\n钩\n痕\n坝\n巷\n饿\n祸\n丘\n玄\n溜\n曰\n逻\n彭\n尝\n卿\n妨\n艇\n吞\n韦\n怨\n矮\n歇\n"); - dico.Add("chinese_traditional", - "的\n一\n是\n在\n不\n了\n有\n和\n人\n這\n中\n大\n為\n上\n個\n國\n我\n以\n要\n他\n時\n來\n用\n們\n生\n到\n作\n地\n於\n出\n就\n分\n對\n成\n會\n可\n主\n發\n年\n動\n同\n工\n也\n能\n下\n過\n子\n說\n產\n種\n面\n而\n方\n後\n多\n定\n行\n學\n法\n所\n民\n得\n經\n十\n三\n之\n進\n著\n等\n部\n度\n家\n電\n力\n裡\n如\n水\n化\n高\n自\n二\n理\n起\n小\n物\n現\n實\n加\n量\n都\n兩\n體\n制\n機\n當\n使\n點\n從\n業\n本\n去\n把\n性\n好\n應\n開\n它\n合\n還\n因\n由\n其\n些\n然\n前\n外\n天\n政\n四\n日\n那\n社\n義\n事\n平\n形\n相\n全\n表\n間\n樣\n與\n關\n各\n重\n新\n線\n內\n數\n正\n心\n反\n你\n明\n看\n原\n又\n麼\n利\n比\n或\n但\n質\n氣\n第\n向\n道\n命\n此\n變\n條\n只\n沒\n結\n解\n問\n意\n建\n月\n公\n無\n系\n軍\n很\n情\n者\n最\n立\n代\n想\n已\n通\n並\n提\n直\n題\n黨\n程\n展\n五\n果\n料\n象\n員\n革\n位\n入\n常\n文\n總\n次\n品\n式\n活\n設\n及\n管\n特\n件\n長\n求\n老\n頭\n基\n資\n邊\n流\n路\n級\n少\n圖\n山\n統\n接\n知\n較\n將\n組\n見\n計\n別\n她\n手\n角\n期\n根\n論\n運\n農\n指\n幾\n九\n區\n強\n放\n決\n西\n被\n幹\n做\n必\n戰\n先\n回\n則\n任\n取\n據\n處\n隊\n南\n給\n色\n光\n門\n即\n保\n治\n北\n造\n百\n規\n熱\n領\n七\n海\n口\n東\n導\n器\n壓\n志\n世\n金\n增\n爭\n濟\n階\n油\n思\n術\n極\n交\n受\n聯\n什\n認\n六\n共\n權\n收\n證\n改\n清\n美\n再\n採\n轉\n更\n單\n風\n切\n打\n白\n教\n速\n花\n帶\n安\n場\n身\n車\n例\n真\n務\n具\n萬\n每\n目\n至\n達\n走\n積\n示\n議\n聲\n報\n鬥\n完\n類\n八\n離\n華\n名\n確\n才\n科\n張\n信\n馬\n節\n話\n米\n整\n空\n元\n況\n今\n集\n溫\n傳\n土\n許\n步\n群\n廣\n石\n記\n需\n段\n研\n界\n拉\n林\n律\n叫\n且\n究\n觀\n越\n織\n裝\n影\n算\n低\n持\n音\n眾\n書\n布\n复\n容\n兒\n須\n際\n商\n非\n驗\n連\n斷\n深\n難\n近\n礦\n千\n週\n委\n素\n技\n備\n半\n辦\n青\n省\n列\n習\n響\n約\n支\n般\n史\n感\n勞\n便\n團\n往\n酸\n歷\n市\n克\n何\n除\n消\n構\n府\n稱\n太\n準\n精\n值\n號\n率\n族\n維\n劃\n選\n標\n寫\n存\n候\n毛\n親\n快\n效\n斯\n院\n查\n江\n型\n眼\n王\n按\n格\n養\n易\n置\n派\n層\n片\n始\n卻\n專\n狀\n育\n廠\n京\n識\n適\n屬\n圓\n包\n火\n住\n調\n滿\n縣\n局\n照\n參\n紅\n細\n引\n聽\n該\n鐵\n價\n嚴\n首\n底\n液\n官\n德\n隨\n病\n蘇\n失\n爾\n死\n講\n配\n女\n黃\n推\n顯\n談\n罪\n神\n藝\n呢\n席\n含\n企\n望\n密\n批\n營\n項\n防\n舉\n球\n英\n氧\n勢\n告\n李\n台\n落\n木\n幫\n輪\n破\n亞\n師\n圍\n注\n遠\n字\n材\n排\n供\n河\n態\n封\n另\n施\n減\n樹\n溶\n怎\n止\n案\n言\n士\n均\n武\n固\n葉\n魚\n波\n視\n僅\n費\n緊\n愛\n左\n章\n早\n朝\n害\n續\n輕\n服\n試\n食\n充\n兵\n源\n判\n護\n司\n足\n某\n練\n差\n致\n板\n田\n降\n黑\n犯\n負\n擊\n范\n繼\n興\n似\n餘\n堅\n曲\n輸\n修\n故\n城\n夫\n夠\n送\n筆\n船\n佔\n右\n財\n吃\n富\n春\n職\n覺\n漢\n畫\n功\n巴\n跟\n雖\n雜\n飛\n檢\n吸\n助\n昇\n陽\n互\n初\n創\n抗\n考\n投\n壞\n策\n古\n徑\n換\n未\n跑\n留\n鋼\n曾\n端\n責\n站\n簡\n述\n錢\n副\n盡\n帝\n射\n草\n衝\n承\n獨\n令\n限\n阿\n宣\n環\n雙\n請\n超\n微\n讓\n控\n州\n良\n軸\n找\n否\n紀\n益\n依\n優\n頂\n礎\n載\n倒\n房\n突\n坐\n粉\n敵\n略\n客\n袁\n冷\n勝\n絕\n析\n塊\n劑\n測\n絲\n協\n訴\n念\n陳\n仍\n羅\n鹽\n友\n洋\n錯\n苦\n夜\n刑\n移\n頻\n逐\n靠\n混\n母\n短\n皮\n終\n聚\n汽\n村\n雲\n哪\n既\n距\n衛\n停\n烈\n央\n察\n燒\n迅\n境\n若\n印\n洲\n刻\n括\n激\n孔\n搞\n甚\n室\n待\n核\n校\n散\n侵\n吧\n甲\n遊\n久\n菜\n味\n舊\n模\n湖\n貨\n損\n預\n阻\n毫\n普\n穩\n乙\n媽\n植\n息\n擴\n銀\n語\n揮\n酒\n守\n拿\n序\n紙\n醫\n缺\n雨\n嗎\n針\n劉\n啊\n急\n唱\n誤\n訓\n願\n審\n附\n獲\n茶\n鮮\n糧\n斤\n孩\n脫\n硫\n肥\n善\n龍\n演\n父\n漸\n血\n歡\n械\n掌\n歌\n沙\n剛\n攻\n謂\n盾\n討\n晚\n粒\n亂\n燃\n矛\n乎\n殺\n藥\n寧\n魯\n貴\n鐘\n煤\n讀\n班\n伯\n香\n介\n迫\n句\n豐\n培\n握\n蘭\n擔\n弦\n蛋\n沉\n假\n穿\n執\n答\n樂\n誰\n順\n煙\n縮\n徵\n臉\n喜\n松\n腳\n困\n異\n免\n背\n星\n福\n買\n染\n井\n概\n慢\n怕\n磁\n倍\n祖\n皇\n促\n靜\n補\n評\n翻\n肉\n踐\n尼\n衣\n寬\n揚\n棉\n希\n傷\n操\n垂\n秋\n宜\n氫\n套\n督\n振\n架\n亮\n末\n憲\n慶\n編\n牛\n觸\n映\n雷\n銷\n詩\n座\n居\n抓\n裂\n胞\n呼\n娘\n景\n威\n綠\n晶\n厚\n盟\n衡\n雞\n孫\n延\n危\n膠\n屋\n鄉\n臨\n陸\n顧\n掉\n呀\n燈\n歲\n措\n束\n耐\n劇\n玉\n趙\n跳\n哥\n季\n課\n凱\n胡\n額\n款\n紹\n卷\n齊\n偉\n蒸\n殖\n永\n宗\n苗\n川\n爐\n岩\n弱\n零\n楊\n奏\n沿\n露\n桿\n探\n滑\n鎮\n飯\n濃\n航\n懷\n趕\n庫\n奪\n伊\n靈\n稅\n途\n滅\n賽\n歸\n召\n鼓\n播\n盤\n裁\n險\n康\n唯\n錄\n菌\n純\n借\n糖\n蓋\n橫\n符\n私\n努\n堂\n域\n槍\n潤\n幅\n哈\n竟\n熟\n蟲\n澤\n腦\n壤\n碳\n歐\n遍\n側\n寨\n敢\n徹\n慮\n斜\n薄\n庭\n納\n彈\n飼\n伸\n折\n麥\n濕\n暗\n荷\n瓦\n塞\n床\n築\n惡\n戶\n訪\n塔\n奇\n透\n梁\n刀\n旋\n跡\n卡\n氯\n遇\n份\n毒\n泥\n退\n洗\n擺\n灰\n彩\n賣\n耗\n夏\n擇\n忙\n銅\n獻\n硬\n予\n繁\n圈\n雪\n函\n亦\n抽\n篇\n陣\n陰\n丁\n尺\n追\n堆\n雄\n迎\n泛\n爸\n樓\n避\n謀\n噸\n野\n豬\n旗\n累\n偏\n典\n館\n索\n秦\n脂\n潮\n爺\n豆\n忽\n托\n驚\n塑\n遺\n愈\n朱\n替\n纖\n粗\n傾\n尚\n痛\n楚\n謝\n奮\n購\n磨\n君\n池\n旁\n碎\n骨\n監\n捕\n弟\n暴\n割\n貫\n殊\n釋\n詞\n亡\n壁\n頓\n寶\n午\n塵\n聞\n揭\n炮\n殘\n冬\n橋\n婦\n警\n綜\n招\n吳\n付\n浮\n遭\n徐\n您\n搖\n谷\n贊\n箱\n隔\n訂\n男\n吹\n園\n紛\n唐\n敗\n宋\n玻\n巨\n耕\n坦\n榮\n閉\n灣\n鍵\n凡\n駐\n鍋\n救\n恩\n剝\n凝\n鹼\n齒\n截\n煉\n麻\n紡\n禁\n廢\n盛\n版\n緩\n淨\n睛\n昌\n婚\n涉\n筒\n嘴\n插\n岸\n朗\n莊\n街\n藏\n姑\n貿\n腐\n奴\n啦\n慣\n乘\n夥\n恢\n勻\n紗\n扎\n辯\n耳\n彪\n臣\n億\n璃\n抵\n脈\n秀\n薩\n俄\n網\n舞\n店\n噴\n縱\n寸\n汗\n掛\n洪\n賀\n閃\n柬\n爆\n烯\n津\n稻\n牆\n軟\n勇\n像\n滾\n厘\n蒙\n芳\n肯\n坡\n柱\n盪\n腿\n儀\n旅\n尾\n軋\n冰\n貢\n登\n黎\n削\n鑽\n勒\n逃\n障\n氨\n郭\n峰\n幣\n港\n伏\n軌\n畝\n畢\n擦\n莫\n刺\n浪\n秘\n援\n株\n健\n售\n股\n島\n甘\n泡\n睡\n童\n鑄\n湯\n閥\n休\n匯\n舍\n牧\n繞\n炸\n哲\n磷\n績\n朋\n淡\n尖\n啟\n陷\n柴\n呈\n徒\n顏\n淚\n稍\n忘\n泵\n藍\n拖\n洞\n授\n鏡\n辛\n壯\n鋒\n貧\n虛\n彎\n摩\n泰\n幼\n廷\n尊\n窗\n綱\n弄\n隸\n疑\n氏\n宮\n姐\n震\n瑞\n怪\n尤\n琴\n循\n描\n膜\n違\n夾\n腰\n緣\n珠\n窮\n森\n枝\n竹\n溝\n催\n繩\n憶\n邦\n剩\n幸\n漿\n欄\n擁\n牙\n貯\n禮\n濾\n鈉\n紋\n罷\n拍\n咱\n喊\n袖\n埃\n勤\n罰\n焦\n潛\n伍\n墨\n欲\n縫\n姓\n刊\n飽\n仿\n獎\n鋁\n鬼\n麗\n跨\n默\n挖\n鏈\n掃\n喝\n袋\n炭\n污\n幕\n諸\n弧\n勵\n梅\n奶\n潔\n災\n舟\n鑑\n苯\n訟\n抱\n毀\n懂\n寒\n智\n埔\n寄\n屆\n躍\n渡\n挑\n丹\n艱\n貝\n碰\n拔\n爹\n戴\n碼\n夢\n芽\n熔\n赤\n漁\n哭\n敬\n顆\n奔\n鉛\n仲\n虎\n稀\n妹\n乏\n珍\n申\n桌\n遵\n允\n隆\n螺\n倉\n魏\n銳\n曉\n氮\n兼\n隱\n礙\n赫\n撥\n忠\n肅\n缸\n牽\n搶\n博\n巧\n殼\n兄\n杜\n訊\n誠\n碧\n祥\n柯\n頁\n巡\n矩\n悲\n灌\n齡\n倫\n票\n尋\n桂\n鋪\n聖\n恐\n恰\n鄭\n趣\n抬\n荒\n騰\n貼\n柔\n滴\n猛\n闊\n輛\n妻\n填\n撤\n儲\n簽\n鬧\n擾\n紫\n砂\n遞\n戲\n吊\n陶\n伐\n餵\n療\n瓶\n婆\n撫\n臂\n摸\n忍\n蝦\n蠟\n鄰\n胸\n鞏\n擠\n偶\n棄\n槽\n勁\n乳\n鄧\n吉\n仁\n爛\n磚\n租\n烏\n艦\n伴\n瓜\n淺\n丙\n暫\n燥\n橡\n柳\n迷\n暖\n牌\n秧\n膽\n詳\n簧\n踏\n瓷\n譜\n呆\n賓\n糊\n洛\n輝\n憤\n競\n隙\n怒\n粘\n乃\n緒\n肩\n籍\n敏\n塗\n熙\n皆\n偵\n懸\n掘\n享\n糾\n醒\n狂\n鎖\n淀\n恨\n牲\n霸\n爬\n賞\n逆\n玩\n陵\n祝\n秒\n浙\n貌\n役\n彼\n悉\n鴨\n趨\n鳳\n晨\n畜\n輩\n秩\n卵\n署\n梯\n炎\n灘\n棋\n驅\n篩\n峽\n冒\n啥\n壽\n譯\n浸\n泉\n帽\n遲\n矽\n疆\n貸\n漏\n稿\n冠\n嫩\n脅\n芯\n牢\n叛\n蝕\n奧\n鳴\n嶺\n羊\n憑\n串\n塘\n繪\n酵\n融\n盆\n錫\n廟\n籌\n凍\n輔\n攝\n襲\n筋\n拒\n僚\n旱\n鉀\n鳥\n漆\n沈\n眉\n疏\n添\n棒\n穗\n硝\n韓\n逼\n扭\n僑\n涼\n挺\n碗\n栽\n炒\n杯\n患\n餾\n勸\n豪\n遼\n勃\n鴻\n旦\n吏\n拜\n狗\n埋\n輥\n掩\n飲\n搬\n罵\n辭\n勾\n扣\n估\n蔣\n絨\n霧\n丈\n朵\n姆\n擬\n宇\n輯\n陝\n雕\n償\n蓄\n崇\n剪\n倡\n廳\n咬\n駛\n薯\n刷\n斥\n番\n賦\n奉\n佛\n澆\n漫\n曼\n扇\n鈣\n桃\n扶\n仔\n返\n俗\n虧\n腔\n鞋\n棱\n覆\n框\n悄\n叔\n撞\n騙\n勘\n旺\n沸\n孤\n吐\n孟\n渠\n屈\n疾\n妙\n惜\n仰\n狠\n脹\n諧\n拋\n黴\n桑\n崗\n嘛\n衰\n盜\n滲\n臟\n賴\n湧\n甜\n曹\n閱\n肌\n哩\n厲\n烴\n緯\n毅\n昨\n偽\n症\n煮\n嘆\n釘\n搭\n莖\n籠\n酷\n偷\n弓\n錐\n恆\n傑\n坑\n鼻\n翼\n綸\n敘\n獄\n逮\n罐\n絡\n棚\n抑\n膨\n蔬\n寺\n驟\n穆\n冶\n枯\n冊\n屍\n凸\n紳\n坯\n犧\n焰\n轟\n欣\n晉\n瘦\n禦\n錠\n錦\n喪\n旬\n鍛\n壟\n搜\n撲\n邀\n亭\n酯\n邁\n舒\n脆\n酶\n閒\n憂\n酚\n頑\n羽\n漲\n卸\n仗\n陪\n闢\n懲\n杭\n姚\n肚\n捉\n飄\n漂\n昆\n欺\n吾\n郎\n烷\n汁\n呵\n飾\n蕭\n雅\n郵\n遷\n燕\n撒\n姻\n赴\n宴\n煩\n債\n帳\n斑\n鈴\n旨\n醇\n董\n餅\n雛\n姿\n拌\n傅\n腹\n妥\n揉\n賢\n拆\n歪\n葡\n胺\n丟\n浩\n徽\n昂\n墊\n擋\n覽\n貪\n慰\n繳\n汪\n慌\n馮\n諾\n姜\n誼\n兇\n劣\n誣\n耀\n昏\n躺\n盈\n騎\n喬\n溪\n叢\n盧\n抹\n悶\n諮\n刮\n駕\n纜\n悟\n摘\n鉺\n擲\n頗\n幻\n柄\n惠\n慘\n佳\n仇\n臘\n窩\n滌\n劍\n瞧\n堡\n潑\n蔥\n罩\n霍\n撈\n胎\n蒼\n濱\n倆\n捅\n湘\n砍\n霞\n邵\n萄\n瘋\n淮\n遂\n熊\n糞\n烘\n宿\n檔\n戈\n駁\n嫂\n裕\n徙\n箭\n捐\n腸\n撐\n曬\n辨\n殿\n蓮\n攤\n攪\n醬\n屏\n疫\n哀\n蔡\n堵\n沫\n皺\n暢\n疊\n閣\n萊\n敲\n轄\n鉤\n痕\n壩\n巷\n餓\n禍\n丘\n玄\n溜\n曰\n邏\n彭\n嘗\n卿\n妨\n艇\n吞\n韋\n怨\n矮\n歇\n"); - dico.Add("english", - "abandon\nability\nable\nabout\nabove\nabsent\nabsorb\nabstract\nabsurd\nabuse\naccess\naccident\naccount\naccuse\nachieve\nacid\nacoustic\nacquire\nacross\nact\naction\nactor\nactress\nactual\nadapt\nadd\naddict\naddress\nadjust\nadmit\nadult\nadvance\nadvice\naerobic\naffair\nafford\nafraid\nagain\nage\nagent\nagree\nahead\naim\nair\nairport\naisle\nalarm\nalbum\nalcohol\nalert\nalien\nall\nalley\nallow\nalmost\nalone\nalpha\nalready\nalso\nalter\nalways\namateur\namazing\namong\namount\namused\nanalyst\nanchor\nancient\nanger\nangle\nangry\nanimal\nankle\nannounce\nannual\nanother\nanswer\nantenna\nantique\nanxiety\nany\napart\napology\nappear\napple\napprove\napril\narch\narctic\narea\narena\nargue\narm\narmed\narmor\narmy\naround\narrange\narrest\narrive\narrow\nart\nartefact\nartist\nartwork\nask\naspect\nassault\nasset\nassist\nassume\nasthma\nathlete\natom\nattack\nattend\nattitude\nattract\nauction\naudit\naugust\naunt\nauthor\nauto\nautumn\naverage\navocado\navoid\nawake\naware\naway\nawesome\nawful\nawkward\naxis\nbaby\nbachelor\nbacon\nbadge\nbag\nbalance\nbalcony\nball\nbamboo\nbanana\nbanner\nbar\nbarely\nbargain\nbarrel\nbase\nbasic\nbasket\nbattle\nbeach\nbean\nbeauty\nbecause\nbecome\nbeef\nbefore\nbegin\nbehave\nbehind\nbelieve\nbelow\nbelt\nbench\nbenefit\nbest\nbetray\nbetter\nbetween\nbeyond\nbicycle\nbid\nbike\nbind\nbiology\nbird\nbirth\nbitter\nblack\nblade\nblame\nblanket\nblast\nbleak\nbless\nblind\nblood\nblossom\nblouse\nblue\nblur\nblush\nboard\nboat\nbody\nboil\nbomb\nbone\nbonus\nbook\nboost\nborder\nboring\nborrow\nboss\nbottom\nbounce\nbox\nboy\nbracket\nbrain\nbrand\nbrass\nbrave\nbread\nbreeze\nbrick\nbridge\nbrief\nbright\nbring\nbrisk\nbroccoli\nbroken\nbronze\nbroom\nbrother\nbrown\nbrush\nbubble\nbuddy\nbudget\nbuffalo\nbuild\nbulb\nbulk\nbullet\nbundle\nbunker\nburden\nburger\nburst\nbus\nbusiness\nbusy\nbutter\nbuyer\nbuzz\ncabbage\ncabin\ncable\ncactus\ncage\ncake\ncall\ncalm\ncamera\ncamp\ncan\ncanal\ncancel\ncandy\ncannon\ncanoe\ncanvas\ncanyon\ncapable\ncapital\ncaptain\ncar\ncarbon\ncard\ncargo\ncarpet\ncarry\ncart\ncase\ncash\ncasino\ncastle\ncasual\ncat\ncatalog\ncatch\ncategory\ncattle\ncaught\ncause\ncaution\ncave\nceiling\ncelery\ncement\ncensus\ncentury\ncereal\ncertain\nchair\nchalk\nchampion\nchange\nchaos\nchapter\ncharge\nchase\nchat\ncheap\ncheck\ncheese\nchef\ncherry\nchest\nchicken\nchief\nchild\nchimney\nchoice\nchoose\nchronic\nchuckle\nchunk\nchurn\ncigar\ncinnamon\ncircle\ncitizen\ncity\ncivil\nclaim\nclap\nclarify\nclaw\nclay\nclean\nclerk\nclever\nclick\nclient\ncliff\nclimb\nclinic\nclip\nclock\nclog\nclose\ncloth\ncloud\nclown\nclub\nclump\ncluster\nclutch\ncoach\ncoast\ncoconut\ncode\ncoffee\ncoil\ncoin\ncollect\ncolor\ncolumn\ncombine\ncome\ncomfort\ncomic\ncommon\ncompany\nconcert\nconduct\nconfirm\ncongress\nconnect\nconsider\ncontrol\nconvince\ncook\ncool\ncopper\ncopy\ncoral\ncore\ncorn\ncorrect\ncost\ncotton\ncouch\ncountry\ncouple\ncourse\ncousin\ncover\ncoyote\ncrack\ncradle\ncraft\ncram\ncrane\ncrash\ncrater\ncrawl\ncrazy\ncream\ncredit\ncreek\ncrew\ncricket\ncrime\ncrisp\ncritic\ncrop\ncross\ncrouch\ncrowd\ncrucial\ncruel\ncruise\ncrumble\ncrunch\ncrush\ncry\ncrystal\ncube\nculture\ncup\ncupboard\ncurious\ncurrent\ncurtain\ncurve\ncushion\ncustom\ncute\ncycle\ndad\ndamage\ndamp\ndance\ndanger\ndaring\ndash\ndaughter\ndawn\nday\ndeal\ndebate\ndebris\ndecade\ndecember\ndecide\ndecline\ndecorate\ndecrease\ndeer\ndefense\ndefine\ndefy\ndegree\ndelay\ndeliver\ndemand\ndemise\ndenial\ndentist\ndeny\ndepart\ndepend\ndeposit\ndepth\ndeputy\nderive\ndescribe\ndesert\ndesign\ndesk\ndespair\ndestroy\ndetail\ndetect\ndevelop\ndevice\ndevote\ndiagram\ndial\ndiamond\ndiary\ndice\ndiesel\ndiet\ndiffer\ndigital\ndignity\ndilemma\ndinner\ndinosaur\ndirect\ndirt\ndisagree\ndiscover\ndisease\ndish\ndismiss\ndisorder\ndisplay\ndistance\ndivert\ndivide\ndivorce\ndizzy\ndoctor\ndocument\ndog\ndoll\ndolphin\ndomain\ndonate\ndonkey\ndonor\ndoor\ndose\ndouble\ndove\ndraft\ndragon\ndrama\ndrastic\ndraw\ndream\ndress\ndrift\ndrill\ndrink\ndrip\ndrive\ndrop\ndrum\ndry\nduck\ndumb\ndune\nduring\ndust\ndutch\nduty\ndwarf\ndynamic\neager\neagle\nearly\nearn\nearth\neasily\neast\neasy\necho\necology\neconomy\nedge\nedit\neducate\neffort\negg\neight\neither\nelbow\nelder\nelectric\nelegant\nelement\nelephant\nelevator\nelite\nelse\nembark\nembody\nembrace\nemerge\nemotion\nemploy\nempower\nempty\nenable\nenact\nend\nendless\nendorse\nenemy\nenergy\nenforce\nengage\nengine\nenhance\nenjoy\nenlist\nenough\nenrich\nenroll\nensure\nenter\nentire\nentry\nenvelope\nepisode\nequal\nequip\nera\nerase\nerode\nerosion\nerror\nerupt\nescape\nessay\nessence\nestate\neternal\nethics\nevidence\nevil\nevoke\nevolve\nexact\nexample\nexcess\nexchange\nexcite\nexclude\nexcuse\nexecute\nexercise\nexhaust\nexhibit\nexile\nexist\nexit\nexotic\nexpand\nexpect\nexpire\nexplain\nexpose\nexpress\nextend\nextra\neye\neyebrow\nfabric\nface\nfaculty\nfade\nfaint\nfaith\nfall\nfalse\nfame\nfamily\nfamous\nfan\nfancy\nfantasy\nfarm\nfashion\nfat\nfatal\nfather\nfatigue\nfault\nfavorite\nfeature\nfebruary\nfederal\nfee\nfeed\nfeel\nfemale\nfence\nfestival\nfetch\nfever\nfew\nfiber\nfiction\nfield\nfigure\nfile\nfilm\nfilter\nfinal\nfind\nfine\nfinger\nfinish\nfire\nfirm\nfirst\nfiscal\nfish\nfit\nfitness\nfix\nflag\nflame\nflash\nflat\nflavor\nflee\nflight\nflip\nfloat\nflock\nfloor\nflower\nfluid\nflush\nfly\nfoam\nfocus\nfog\nfoil\nfold\nfollow\nfood\nfoot\nforce\nforest\nforget\nfork\nfortune\nforum\nforward\nfossil\nfoster\nfound\nfox\nfragile\nframe\nfrequent\nfresh\nfriend\nfringe\nfrog\nfront\nfrost\nfrown\nfrozen\nfruit\nfuel\nfun\nfunny\nfurnace\nfury\nfuture\ngadget\ngain\ngalaxy\ngallery\ngame\ngap\ngarage\ngarbage\ngarden\ngarlic\ngarment\ngas\ngasp\ngate\ngather\ngauge\ngaze\ngeneral\ngenius\ngenre\ngentle\ngenuine\ngesture\nghost\ngiant\ngift\ngiggle\nginger\ngiraffe\ngirl\ngive\nglad\nglance\nglare\nglass\nglide\nglimpse\nglobe\ngloom\nglory\nglove\nglow\nglue\ngoat\ngoddess\ngold\ngood\ngoose\ngorilla\ngospel\ngossip\ngovern\ngown\ngrab\ngrace\ngrain\ngrant\ngrape\ngrass\ngravity\ngreat\ngreen\ngrid\ngrief\ngrit\ngrocery\ngroup\ngrow\ngrunt\nguard\nguess\nguide\nguilt\nguitar\ngun\ngym\nhabit\nhair\nhalf\nhammer\nhamster\nhand\nhappy\nharbor\nhard\nharsh\nharvest\nhat\nhave\nhawk\nhazard\nhead\nhealth\nheart\nheavy\nhedgehog\nheight\nhello\nhelmet\nhelp\nhen\nhero\nhidden\nhigh\nhill\nhint\nhip\nhire\nhistory\nhobby\nhockey\nhold\nhole\nholiday\nhollow\nhome\nhoney\nhood\nhope\nhorn\nhorror\nhorse\nhospital\nhost\nhotel\nhour\nhover\nhub\nhuge\nhuman\nhumble\nhumor\nhundred\nhungry\nhunt\nhurdle\nhurry\nhurt\nhusband\nhybrid\nice\nicon\nidea\nidentify\nidle\nignore\nill\nillegal\nillness\nimage\nimitate\nimmense\nimmune\nimpact\nimpose\nimprove\nimpulse\ninch\ninclude\nincome\nincrease\nindex\nindicate\nindoor\nindustry\ninfant\ninflict\ninform\ninhale\ninherit\ninitial\ninject\ninjury\ninmate\ninner\ninnocent\ninput\ninquiry\ninsane\ninsect\ninside\ninspire\ninstall\nintact\ninterest\ninto\ninvest\ninvite\ninvolve\niron\nisland\nisolate\nissue\nitem\nivory\njacket\njaguar\njar\njazz\njealous\njeans\njelly\njewel\njob\njoin\njoke\njourney\njoy\njudge\njuice\njump\njungle\njunior\njunk\njust\nkangaroo\nkeen\nkeep\nketchup\nkey\nkick\nkid\nkidney\nkind\nkingdom\nkiss\nkit\nkitchen\nkite\nkitten\nkiwi\nknee\nknife\nknock\nknow\nlab\nlabel\nlabor\nladder\nlady\nlake\nlamp\nlanguage\nlaptop\nlarge\nlater\nlatin\nlaugh\nlaundry\nlava\nlaw\nlawn\nlawsuit\nlayer\nlazy\nleader\nleaf\nlearn\nleave\nlecture\nleft\nleg\nlegal\nlegend\nleisure\nlemon\nlend\nlength\nlens\nleopard\nlesson\nletter\nlevel\nliar\nliberty\nlibrary\nlicense\nlife\nlift\nlight\nlike\nlimb\nlimit\nlink\nlion\nliquid\nlist\nlittle\nlive\nlizard\nload\nloan\nlobster\nlocal\nlock\nlogic\nlonely\nlong\nloop\nlottery\nloud\nlounge\nlove\nloyal\nlucky\nluggage\nlumber\nlunar\nlunch\nluxury\nlyrics\nmachine\nmad\nmagic\nmagnet\nmaid\nmail\nmain\nmajor\nmake\nmammal\nman\nmanage\nmandate\nmango\nmansion\nmanual\nmaple\nmarble\nmarch\nmargin\nmarine\nmarket\nmarriage\nmask\nmass\nmaster\nmatch\nmaterial\nmath\nmatrix\nmatter\nmaximum\nmaze\nmeadow\nmean\nmeasure\nmeat\nmechanic\nmedal\nmedia\nmelody\nmelt\nmember\nmemory\nmention\nmenu\nmercy\nmerge\nmerit\nmerry\nmesh\nmessage\nmetal\nmethod\nmiddle\nmidnight\nmilk\nmillion\nmimic\nmind\nminimum\nminor\nminute\nmiracle\nmirror\nmisery\nmiss\nmistake\nmix\nmixed\nmixture\nmobile\nmodel\nmodify\nmom\nmoment\nmonitor\nmonkey\nmonster\nmonth\nmoon\nmoral\nmore\nmorning\nmosquito\nmother\nmotion\nmotor\nmountain\nmouse\nmove\nmovie\nmuch\nmuffin\nmule\nmultiply\nmuscle\nmuseum\nmushroom\nmusic\nmust\nmutual\nmyself\nmystery\nmyth\nnaive\nname\nnapkin\nnarrow\nnasty\nnation\nnature\nnear\nneck\nneed\nnegative\nneglect\nneither\nnephew\nnerve\nnest\nnet\nnetwork\nneutral\nnever\nnews\nnext\nnice\nnight\nnoble\nnoise\nnominee\nnoodle\nnormal\nnorth\nnose\nnotable\nnote\nnothing\nnotice\nnovel\nnow\nnuclear\nnumber\nnurse\nnut\noak\nobey\nobject\noblige\nobscure\nobserve\nobtain\nobvious\noccur\nocean\noctober\nodor\noff\noffer\noffice\noften\noil\nokay\nold\nolive\nolympic\nomit\nonce\none\nonion\nonline\nonly\nopen\nopera\nopinion\noppose\noption\norange\norbit\norchard\norder\nordinary\norgan\norient\noriginal\norphan\nostrich\nother\noutdoor\nouter\noutput\noutside\noval\noven\nover\nown\nowner\noxygen\noyster\nozone\npact\npaddle\npage\npair\npalace\npalm\npanda\npanel\npanic\npanther\npaper\nparade\nparent\npark\nparrot\nparty\npass\npatch\npath\npatient\npatrol\npattern\npause\npave\npayment\npeace\npeanut\npear\npeasant\npelican\npen\npenalty\npencil\npeople\npepper\nperfect\npermit\nperson\npet\nphone\nphoto\nphrase\nphysical\npiano\npicnic\npicture\npiece\npig\npigeon\npill\npilot\npink\npioneer\npipe\npistol\npitch\npizza\nplace\nplanet\nplastic\nplate\nplay\nplease\npledge\npluck\nplug\nplunge\npoem\npoet\npoint\npolar\npole\npolice\npond\npony\npool\npopular\nportion\nposition\npossible\npost\npotato\npottery\npoverty\npowder\npower\npractice\npraise\npredict\nprefer\nprepare\npresent\npretty\nprevent\nprice\npride\nprimary\nprint\npriority\nprison\nprivate\nprize\nproblem\nprocess\nproduce\nprofit\nprogram\nproject\npromote\nproof\nproperty\nprosper\nprotect\nproud\nprovide\npublic\npudding\npull\npulp\npulse\npumpkin\npunch\npupil\npuppy\npurchase\npurity\npurpose\npurse\npush\nput\npuzzle\npyramid\nquality\nquantum\nquarter\nquestion\nquick\nquit\nquiz\nquote\nrabbit\nraccoon\nrace\nrack\nradar\nradio\nrail\nrain\nraise\nrally\nramp\nranch\nrandom\nrange\nrapid\nrare\nrate\nrather\nraven\nraw\nrazor\nready\nreal\nreason\nrebel\nrebuild\nrecall\nreceive\nrecipe\nrecord\nrecycle\nreduce\nreflect\nreform\nrefuse\nregion\nregret\nregular\nreject\nrelax\nrelease\nrelief\nrely\nremain\nremember\nremind\nremove\nrender\nrenew\nrent\nreopen\nrepair\nrepeat\nreplace\nreport\nrequire\nrescue\nresemble\nresist\nresource\nresponse\nresult\nretire\nretreat\nreturn\nreunion\nreveal\nreview\nreward\nrhythm\nrib\nribbon\nrice\nrich\nride\nridge\nrifle\nright\nrigid\nring\nriot\nripple\nrisk\nritual\nrival\nriver\nroad\nroast\nrobot\nrobust\nrocket\nromance\nroof\nrookie\nroom\nrose\nrotate\nrough\nround\nroute\nroyal\nrubber\nrude\nrug\nrule\nrun\nrunway\nrural\nsad\nsaddle\nsadness\nsafe\nsail\nsalad\nsalmon\nsalon\nsalt\nsalute\nsame\nsample\nsand\nsatisfy\nsatoshi\nsauce\nsausage\nsave\nsay\nscale\nscan\nscare\nscatter\nscene\nscheme\nschool\nscience\nscissors\nscorpion\nscout\nscrap\nscreen\nscript\nscrub\nsea\nsearch\nseason\nseat\nsecond\nsecret\nsection\nsecurity\nseed\nseek\nsegment\nselect\nsell\nseminar\nsenior\nsense\nsentence\nseries\nservice\nsession\nsettle\nsetup\nseven\nshadow\nshaft\nshallow\nshare\nshed\nshell\nsheriff\nshield\nshift\nshine\nship\nshiver\nshock\nshoe\nshoot\nshop\nshort\nshoulder\nshove\nshrimp\nshrug\nshuffle\nshy\nsibling\nsick\nside\nsiege\nsight\nsign\nsilent\nsilk\nsilly\nsilver\nsimilar\nsimple\nsince\nsing\nsiren\nsister\nsituate\nsix\nsize\nskate\nsketch\nski\nskill\nskin\nskirt\nskull\nslab\nslam\nsleep\nslender\nslice\nslide\nslight\nslim\nslogan\nslot\nslow\nslush\nsmall\nsmart\nsmile\nsmoke\nsmooth\nsnack\nsnake\nsnap\nsniff\nsnow\nsoap\nsoccer\nsocial\nsock\nsoda\nsoft\nsolar\nsoldier\nsolid\nsolution\nsolve\nsomeone\nsong\nsoon\nsorry\nsort\nsoul\nsound\nsoup\nsource\nsouth\nspace\nspare\nspatial\nspawn\nspeak\nspecial\nspeed\nspell\nspend\nsphere\nspice\nspider\nspike\nspin\nspirit\nsplit\nspoil\nsponsor\nspoon\nsport\nspot\nspray\nspread\nspring\nspy\nsquare\nsqueeze\nsquirrel\nstable\nstadium\nstaff\nstage\nstairs\nstamp\nstand\nstart\nstate\nstay\nsteak\nsteel\nstem\nstep\nstereo\nstick\nstill\nsting\nstock\nstomach\nstone\nstool\nstory\nstove\nstrategy\nstreet\nstrike\nstrong\nstruggle\nstudent\nstuff\nstumble\nstyle\nsubject\nsubmit\nsubway\nsuccess\nsuch\nsudden\nsuffer\nsugar\nsuggest\nsuit\nsummer\nsun\nsunny\nsunset\nsuper\nsupply\nsupreme\nsure\nsurface\nsurge\nsurprise\nsurround\nsurvey\nsuspect\nsustain\nswallow\nswamp\nswap\nswarm\nswear\nsweet\nswift\nswim\nswing\nswitch\nsword\nsymbol\nsymptom\nsyrup\nsystem\ntable\ntackle\ntag\ntail\ntalent\ntalk\ntank\ntape\ntarget\ntask\ntaste\ntattoo\ntaxi\nteach\nteam\ntell\nten\ntenant\ntennis\ntent\nterm\ntest\ntext\nthank\nthat\ntheme\nthen\ntheory\nthere\nthey\nthing\nthis\nthought\nthree\nthrive\nthrow\nthumb\nthunder\nticket\ntide\ntiger\ntilt\ntimber\ntime\ntiny\ntip\ntired\ntissue\ntitle\ntoast\ntobacco\ntoday\ntoddler\ntoe\ntogether\ntoilet\ntoken\ntomato\ntomorrow\ntone\ntongue\ntonight\ntool\ntooth\ntop\ntopic\ntopple\ntorch\ntornado\ntortoise\ntoss\ntotal\ntourist\ntoward\ntower\ntown\ntoy\ntrack\ntrade\ntraffic\ntragic\ntrain\ntransfer\ntrap\ntrash\ntravel\ntray\ntreat\ntree\ntrend\ntrial\ntribe\ntrick\ntrigger\ntrim\ntrip\ntrophy\ntrouble\ntruck\ntrue\ntruly\ntrumpet\ntrust\ntruth\ntry\ntube\ntuition\ntumble\ntuna\ntunnel\nturkey\nturn\nturtle\ntwelve\ntwenty\ntwice\ntwin\ntwist\ntwo\ntype\ntypical\nugly\numbrella\nunable\nunaware\nuncle\nuncover\nunder\nundo\nunfair\nunfold\nunhappy\nuniform\nunique\nunit\nuniverse\nunknown\nunlock\nuntil\nunusual\nunveil\nupdate\nupgrade\nuphold\nupon\nupper\nupset\nurban\nurge\nusage\nuse\nused\nuseful\nuseless\nusual\nutility\nvacant\nvacuum\nvague\nvalid\nvalley\nvalve\nvan\nvanish\nvapor\nvarious\nvast\nvault\nvehicle\nvelvet\nvendor\nventure\nvenue\nverb\nverify\nversion\nvery\nvessel\nveteran\nviable\nvibrant\nvicious\nvictory\nvideo\nview\nvillage\nvintage\nviolin\nvirtual\nvirus\nvisa\nvisit\nvisual\nvital\nvivid\nvocal\nvoice\nvoid\nvolcano\nvolume\nvote\nvoyage\nwage\nwagon\nwait\nwalk\nwall\nwalnut\nwant\nwarfare\nwarm\nwarrior\nwash\nwasp\nwaste\nwater\nwave\nway\nwealth\nweapon\nwear\nweasel\nweather\nweb\nwedding\nweekend\nweird\nwelcome\nwest\nwet\nwhale\nwhat\nwheat\nwheel\nwhen\nwhere\nwhip\nwhisper\nwide\nwidth\nwife\nwild\nwill\nwin\nwindow\nwine\nwing\nwink\nwinner\nwinter\nwire\nwisdom\nwise\nwish\nwitness\nwolf\nwoman\nwonder\nwood\nwool\nword\nwork\nworld\nworry\nworth\nwrap\nwreck\nwrestle\nwrist\nwrite\nwrong\nyard\nyear\nyellow\nyou\nyoung\nyouth\nzebra\nzero\nzone\nzoo\n"); - dico.Add("japanese", - "あいこくしん\nあいさつ\nあいだ\nあおぞら\nあかちゃん\nあきる\nあけがた\nあける\nあこがれる\nあさい\nあさひ\nあしあと\nあじわう\nあずかる\nあずき\nあそぶ\nあたえる\nあたためる\nあたりまえ\nあたる\nあつい\nあつかう\nあっしゅく\nあつまり\nあつめる\nあてな\nあてはまる\nあひる\nあぶら\nあぶる\nあふれる\nあまい\nあまど\nあまやかす\nあまり\nあみもの\nあめりか\nあやまる\nあゆむ\nあらいぐま\nあらし\nあらすじ\nあらためる\nあらゆる\nあらわす\nありがとう\nあわせる\nあわてる\nあんい\nあんがい\nあんこ\nあんぜん\nあんてい\nあんない\nあんまり\nいいだす\nいおん\nいがい\nいがく\nいきおい\nいきなり\nいきもの\nいきる\nいくじ\nいくぶん\nいけばな\nいけん\nいこう\nいこく\nいこつ\nいさましい\nいさん\nいしき\nいじゅう\nいじょう\nいじわる\nいずみ\nいずれ\nいせい\nいせえび\nいせかい\nいせき\nいぜん\nいそうろう\nいそがしい\nいだい\nいだく\nいたずら\nいたみ\nいたりあ\nいちおう\nいちじ\nいちど\nいちば\nいちぶ\nいちりゅう\nいつか\nいっしゅん\nいっせい\nいっそう\nいったん\nいっち\nいってい\nいっぽう\nいてざ\nいてん\nいどう\nいとこ\nいない\nいなか\nいねむり\nいのち\nいのる\nいはつ\nいばる\nいはん\nいびき\nいひん\nいふく\nいへん\nいほう\nいみん\nいもうと\nいもたれ\nいもり\nいやがる\nいやす\nいよかん\nいよく\nいらい\nいらすと\nいりぐち\nいりょう\nいれい\nいれもの\nいれる\nいろえんぴつ\nいわい\nいわう\nいわかん\nいわば\nいわゆる\nいんげんまめ\nいんさつ\nいんしょう\nいんよう\nうえき\nうえる\nうおざ\nうがい\nうかぶ\nうかべる\nうきわ\nうくらいな\nうくれれ\nうけたまわる\nうけつけ\nうけとる\nうけもつ\nうける\nうごかす\nうごく\nうこん\nうさぎ\nうしなう\nうしろがみ\nうすい\nうすぎ\nうすぐらい\nうすめる\nうせつ\nうちあわせ\nうちがわ\nうちき\nうちゅう\nうっかり\nうつくしい\nうったえる\nうつる\nうどん\nうなぎ\nうなじ\nうなずく\nうなる\nうねる\nうのう\nうぶげ\nうぶごえ\nうまれる\nうめる\nうもう\nうやまう\nうよく\nうらがえす\nうらぐち\nうらない\nうりあげ\nうりきれ\nうるさい\nうれしい\nうれゆき\nうれる\nうろこ\nうわき\nうわさ\nうんこう\nうんちん\nうんてん\nうんどう\nえいえん\nえいが\nえいきょう\nえいご\nえいせい\nえいぶん\nえいよう\nえいわ\nえおり\nえがお\nえがく\nえきたい\nえくせる\nえしゃく\nえすて\nえつらん\nえのぐ\nえほうまき\nえほん\nえまき\nえもじ\nえもの\nえらい\nえらぶ\nえりあ\nえんえん\nえんかい\nえんぎ\nえんげき\nえんしゅう\nえんぜつ\nえんそく\nえんちょう\nえんとつ\nおいかける\nおいこす\nおいしい\nおいつく\nおうえん\nおうさま\nおうじ\nおうせつ\nおうたい\nおうふく\nおうべい\nおうよう\nおえる\nおおい\nおおう\nおおどおり\nおおや\nおおよそ\nおかえり\nおかず\nおがむ\nおかわり\nおぎなう\nおきる\nおくさま\nおくじょう\nおくりがな\nおくる\nおくれる\nおこす\nおこなう\nおこる\nおさえる\nおさない\nおさめる\nおしいれ\nおしえる\nおじぎ\nおじさん\nおしゃれ\nおそらく\nおそわる\nおたがい\nおたく\nおだやか\nおちつく\nおっと\nおつり\nおでかけ\nおとしもの\nおとなしい\nおどり\nおどろかす\nおばさん\nおまいり\nおめでとう\nおもいで\nおもう\nおもたい\nおもちゃ\nおやつ\nおやゆび\nおよぼす\nおらんだ\nおろす\nおんがく\nおんけい\nおんしゃ\nおんせん\nおんだん\nおんちゅう\nおんどけい\nかあつ\nかいが\nがいき\nがいけん\nがいこう\nかいさつ\nかいしゃ\nかいすいよく\nかいぜん\nかいぞうど\nかいつう\nかいてん\nかいとう\nかいふく\nがいへき\nかいほう\nかいよう\nがいらい\nかいわ\nかえる\nかおり\nかかえる\nかがく\nかがし\nかがみ\nかくご\nかくとく\nかざる\nがぞう\nかたい\nかたち\nがちょう\nがっきゅう\nがっこう\nがっさん\nがっしょう\nかなざわし\nかのう\nがはく\nかぶか\nかほう\nかほご\nかまう\nかまぼこ\nかめれおん\nかゆい\nかようび\nからい\nかるい\nかろう\nかわく\nかわら\nがんか\nかんけい\nかんこう\nかんしゃ\nかんそう\nかんたん\nかんち\nがんばる\nきあい\nきあつ\nきいろ\nぎいん\nきうい\nきうん\nきえる\nきおう\nきおく\nきおち\nきおん\nきかい\nきかく\nきかんしゃ\nききて\nきくばり\nきくらげ\nきけんせい\nきこう\nきこえる\nきこく\nきさい\nきさく\nきさま\nきさらぎ\nぎじかがく\nぎしき\nぎじたいけん\nぎじにってい\nぎじゅつしゃ\nきすう\nきせい\nきせき\nきせつ\nきそう\nきぞく\nきぞん\nきたえる\nきちょう\nきつえん\nぎっちり\nきつつき\nきつね\nきてい\nきどう\nきどく\nきない\nきなが\nきなこ\nきぬごし\nきねん\nきのう\nきのした\nきはく\nきびしい\nきひん\nきふく\nきぶん\nきぼう\nきほん\nきまる\nきみつ\nきむずかしい\nきめる\nきもだめし\nきもち\nきもの\nきゃく\nきやく\nぎゅうにく\nきよう\nきょうりゅう\nきらい\nきらく\nきりん\nきれい\nきれつ\nきろく\nぎろん\nきわめる\nぎんいろ\nきんかくじ\nきんじょ\nきんようび\nぐあい\nくいず\nくうかん\nくうき\nくうぐん\nくうこう\nぐうせい\nくうそう\nぐうたら\nくうふく\nくうぼ\nくかん\nくきょう\nくげん\nぐこう\nくさい\nくさき\nくさばな\nくさる\nくしゃみ\nくしょう\nくすのき\nくすりゆび\nくせげ\nくせん\nぐたいてき\nくださる\nくたびれる\nくちこみ\nくちさき\nくつした\nぐっすり\nくつろぐ\nくとうてん\nくどく\nくなん\nくねくね\nくのう\nくふう\nくみあわせ\nくみたてる\nくめる\nくやくしょ\nくらす\nくらべる\nくるま\nくれる\nくろう\nくわしい\nぐんかん\nぐんしょく\nぐんたい\nぐんて\nけあな\nけいかく\nけいけん\nけいこ\nけいさつ\nげいじゅつ\nけいたい\nげいのうじん\nけいれき\nけいろ\nけおとす\nけおりもの\nげきか\nげきげん\nげきだん\nげきちん\nげきとつ\nげきは\nげきやく\nげこう\nげこくじょう\nげざい\nけさき\nげざん\nけしき\nけしごむ\nけしょう\nげすと\nけたば\nけちゃっぷ\nけちらす\nけつあつ\nけつい\nけつえき\nけっこん\nけつじょ\nけっせき\nけってい\nけつまつ\nげつようび\nげつれい\nけつろん\nげどく\nけとばす\nけとる\nけなげ\nけなす\nけなみ\nけぬき\nげねつ\nけねん\nけはい\nげひん\nけぶかい\nげぼく\nけまり\nけみかる\nけむし\nけむり\nけもの\nけらい\nけろけろ\nけわしい\nけんい\nけんえつ\nけんお\nけんか\nげんき\nけんげん\nけんこう\nけんさく\nけんしゅう\nけんすう\nげんそう\nけんちく\nけんてい\nけんとう\nけんない\nけんにん\nげんぶつ\nけんま\nけんみん\nけんめい\nけんらん\nけんり\nこあくま\nこいぬ\nこいびと\nごうい\nこうえん\nこうおん\nこうかん\nごうきゅう\nごうけい\nこうこう\nこうさい\nこうじ\nこうすい\nごうせい\nこうそく\nこうたい\nこうちゃ\nこうつう\nこうてい\nこうどう\nこうない\nこうはい\nごうほう\nごうまん\nこうもく\nこうりつ\nこえる\nこおり\nごかい\nごがつ\nごかん\nこくご\nこくさい\nこくとう\nこくない\nこくはく\nこぐま\nこけい\nこける\nここのか\nこころ\nこさめ\nこしつ\nこすう\nこせい\nこせき\nこぜん\nこそだて\nこたい\nこたえる\nこたつ\nこちょう\nこっか\nこつこつ\nこつばん\nこつぶ\nこてい\nこてん\nことがら\nことし\nことば\nことり\nこなごな\nこねこね\nこのまま\nこのみ\nこのよ\nごはん\nこひつじ\nこふう\nこふん\nこぼれる\nごまあぶら\nこまかい\nごますり\nこまつな\nこまる\nこむぎこ\nこもじ\nこもち\nこもの\nこもん\nこやく\nこやま\nこゆう\nこゆび\nこよい\nこよう\nこりる\nこれくしょん\nころっけ\nこわもて\nこわれる\nこんいん\nこんかい\nこんき\nこんしゅう\nこんすい\nこんだて\nこんとん\nこんなん\nこんびに\nこんぽん\nこんまけ\nこんや\nこんれい\nこんわく\nざいえき\nさいかい\nさいきん\nざいげん\nざいこ\nさいしょ\nさいせい\nざいたく\nざいちゅう\nさいてき\nざいりょう\nさうな\nさかいし\nさがす\nさかな\nさかみち\nさがる\nさぎょう\nさくし\nさくひん\nさくら\nさこく\nさこつ\nさずかる\nざせき\nさたん\nさつえい\nざつおん\nざっか\nざつがく\nさっきょく\nざっし\nさつじん\nざっそう\nさつたば\nさつまいも\nさてい\nさといも\nさとう\nさとおや\nさとし\nさとる\nさのう\nさばく\nさびしい\nさべつ\nさほう\nさほど\nさます\nさみしい\nさみだれ\nさむけ\nさめる\nさやえんどう\nさゆう\nさよう\nさよく\nさらだ\nざるそば\nさわやか\nさわる\nさんいん\nさんか\nさんきゃく\nさんこう\nさんさい\nざんしょ\nさんすう\nさんせい\nさんそ\nさんち\nさんま\nさんみ\nさんらん\nしあい\nしあげ\nしあさって\nしあわせ\nしいく\nしいん\nしうち\nしえい\nしおけ\nしかい\nしかく\nじかん\nしごと\nしすう\nじだい\nしたうけ\nしたぎ\nしたて\nしたみ\nしちょう\nしちりん\nしっかり\nしつじ\nしつもん\nしてい\nしてき\nしてつ\nじてん\nじどう\nしなぎれ\nしなもの\nしなん\nしねま\nしねん\nしのぐ\nしのぶ\nしはい\nしばかり\nしはつ\nしはらい\nしはん\nしひょう\nしふく\nじぶん\nしへい\nしほう\nしほん\nしまう\nしまる\nしみん\nしむける\nじむしょ\nしめい\nしめる\nしもん\nしゃいん\nしゃうん\nしゃおん\nじゃがいも\nしやくしょ\nしゃくほう\nしゃけん\nしゃこ\nしゃざい\nしゃしん\nしゃせん\nしゃそう\nしゃたい\nしゃちょう\nしゃっきん\nじゃま\nしゃりん\nしゃれい\nじゆう\nじゅうしょ\nしゅくはく\nじゅしん\nしゅっせき\nしゅみ\nしゅらば\nじゅんばん\nしょうかい\nしょくたく\nしょっけん\nしょどう\nしょもつ\nしらせる\nしらべる\nしんか\nしんこう\nじんじゃ\nしんせいじ\nしんちく\nしんりん\nすあげ\nすあし\nすあな\nずあん\nすいえい\nすいか\nすいとう\nずいぶん\nすいようび\nすうがく\nすうじつ\nすうせん\nすおどり\nすきま\nすくう\nすくない\nすける\nすごい\nすこし\nずさん\nすずしい\nすすむ\nすすめる\nすっかり\nずっしり\nずっと\nすてき\nすてる\nすねる\nすのこ\nすはだ\nすばらしい\nずひょう\nずぶぬれ\nすぶり\nすふれ\nすべて\nすべる\nずほう\nすぼん\nすまい\nすめし\nすもう\nすやき\nすらすら\nするめ\nすれちがう\nすろっと\nすわる\nすんぜん\nすんぽう\nせあぶら\nせいかつ\nせいげん\nせいじ\nせいよう\nせおう\nせかいかん\nせきにん\nせきむ\nせきゆ\nせきらんうん\nせけん\nせこう\nせすじ\nせたい\nせたけ\nせっかく\nせっきゃく\nぜっく\nせっけん\nせっこつ\nせっさたくま\nせつぞく\nせつだん\nせつでん\nせっぱん\nせつび\nせつぶん\nせつめい\nせつりつ\nせなか\nせのび\nせはば\nせびろ\nせぼね\nせまい\nせまる\nせめる\nせもたれ\nせりふ\nぜんあく\nせんい\nせんえい\nせんか\nせんきょ\nせんく\nせんげん\nぜんご\nせんさい\nせんしゅ\nせんすい\nせんせい\nせんぞ\nせんたく\nせんちょう\nせんてい\nせんとう\nせんぬき\nせんねん\nせんぱい\nぜんぶ\nぜんぽう\nせんむ\nせんめんじょ\nせんもん\nせんやく\nせんゆう\nせんよう\nぜんら\nぜんりゃく\nせんれい\nせんろ\nそあく\nそいとげる\nそいね\nそうがんきょう\nそうき\nそうご\nそうしん\nそうだん\nそうなん\nそうび\nそうめん\nそうり\nそえもの\nそえん\nそがい\nそげき\nそこう\nそこそこ\nそざい\nそしな\nそせい\nそせん\nそそぐ\nそだてる\nそつう\nそつえん\nそっかん\nそつぎょう\nそっけつ\nそっこう\nそっせん\nそっと\nそとがわ\nそとづら\nそなえる\nそなた\nそふぼ\nそぼく\nそぼろ\nそまつ\nそまる\nそむく\nそむりえ\nそめる\nそもそも\nそよかぜ\nそらまめ\nそろう\nそんかい\nそんけい\nそんざい\nそんしつ\nそんぞく\nそんちょう\nぞんび\nぞんぶん\nそんみん\nたあい\nたいいん\nたいうん\nたいえき\nたいおう\nだいがく\nたいき\nたいぐう\nたいけん\nたいこ\nたいざい\nだいじょうぶ\nだいすき\nたいせつ\nたいそう\nだいたい\nたいちょう\nたいてい\nだいどころ\nたいない\nたいねつ\nたいのう\nたいはん\nだいひょう\nたいふう\nたいへん\nたいほ\nたいまつばな\nたいみんぐ\nたいむ\nたいめん\nたいやき\nたいよう\nたいら\nたいりょく\nたいる\nたいわん\nたうえ\nたえる\nたおす\nたおる\nたおれる\nたかい\nたかね\nたきび\nたくさん\nたこく\nたこやき\nたさい\nたしざん\nだじゃれ\nたすける\nたずさわる\nたそがれ\nたたかう\nたたく\nただしい\nたたみ\nたちばな\nだっかい\nだっきゃく\nだっこ\nだっしゅつ\nだったい\nたてる\nたとえる\nたなばた\nたにん\nたぬき\nたのしみ\nたはつ\nたぶん\nたべる\nたぼう\nたまご\nたまる\nだむる\nためいき\nためす\nためる\nたもつ\nたやすい\nたよる\nたらす\nたりきほんがん\nたりょう\nたりる\nたると\nたれる\nたれんと\nたろっと\nたわむれる\nだんあつ\nたんい\nたんおん\nたんか\nたんき\nたんけん\nたんご\nたんさん\nたんじょうび\nだんせい\nたんそく\nたんたい\nだんち\nたんてい\nたんとう\nだんな\nたんにん\nだんねつ\nたんのう\nたんぴん\nだんぼう\nたんまつ\nたんめい\nだんれつ\nだんろ\nだんわ\nちあい\nちあん\nちいき\nちいさい\nちえん\nちかい\nちから\nちきゅう\nちきん\nちけいず\nちけん\nちこく\nちさい\nちしき\nちしりょう\nちせい\nちそう\nちたい\nちたん\nちちおや\nちつじょ\nちてき\nちてん\nちぬき\nちぬり\nちのう\nちひょう\nちへいせん\nちほう\nちまた\nちみつ\nちみどろ\nちめいど\nちゃんこなべ\nちゅうい\nちゆりょく\nちょうし\nちょさくけん\nちらし\nちらみ\nちりがみ\nちりょう\nちるど\nちわわ\nちんたい\nちんもく\nついか\nついたち\nつうか\nつうじょう\nつうはん\nつうわ\nつかう\nつかれる\nつくね\nつくる\nつけね\nつける\nつごう\nつたえる\nつづく\nつつじ\nつつむ\nつとめる\nつながる\nつなみ\nつねづね\nつのる\nつぶす\nつまらない\nつまる\nつみき\nつめたい\nつもり\nつもる\nつよい\nつるぼ\nつるみく\nつわもの\nつわり\nてあし\nてあて\nてあみ\nていおん\nていか\nていき\nていけい\nていこく\nていさつ\nていし\nていせい\nていたい\nていど\nていねい\nていひょう\nていへん\nていぼう\nてうち\nておくれ\nてきとう\nてくび\nでこぼこ\nてさぎょう\nてさげ\nてすり\nてそう\nてちがい\nてちょう\nてつがく\nてつづき\nでっぱ\nてつぼう\nてつや\nでぬかえ\nてぬき\nてぬぐい\nてのひら\nてはい\nてぶくろ\nてふだ\nてほどき\nてほん\nてまえ\nてまきずし\nてみじか\nてみやげ\nてらす\nてれび\nてわけ\nてわたし\nでんあつ\nてんいん\nてんかい\nてんき\nてんぐ\nてんけん\nてんごく\nてんさい\nてんし\nてんすう\nでんち\nてんてき\nてんとう\nてんない\nてんぷら\nてんぼうだい\nてんめつ\nてんらんかい\nでんりょく\nでんわ\nどあい\nといれ\nどうかん\nとうきゅう\nどうぐ\nとうし\nとうむぎ\nとおい\nとおか\nとおく\nとおす\nとおる\nとかい\nとかす\nときおり\nときどき\nとくい\nとくしゅう\nとくてん\nとくに\nとくべつ\nとけい\nとける\nとこや\nとさか\nとしょかん\nとそう\nとたん\nとちゅう\nとっきゅう\nとっくん\nとつぜん\nとつにゅう\nとどける\nととのえる\nとない\nとなえる\nとなり\nとのさま\nとばす\nどぶがわ\nとほう\nとまる\nとめる\nともだち\nともる\nどようび\nとらえる\nとんかつ\nどんぶり\nないかく\nないこう\nないしょ\nないす\nないせん\nないそう\nなおす\nながい\nなくす\nなげる\nなこうど\nなさけ\nなたでここ\nなっとう\nなつやすみ\nななおし\nなにごと\nなにもの\nなにわ\nなのか\nなふだ\nなまいき\nなまえ\nなまみ\nなみだ\nなめらか\nなめる\nなやむ\nならう\nならび\nならぶ\nなれる\nなわとび\nなわばり\nにあう\nにいがた\nにうけ\nにおい\nにかい\nにがて\nにきび\nにくしみ\nにくまん\nにげる\nにさんかたんそ\nにしき\nにせもの\nにちじょう\nにちようび\nにっか\nにっき\nにっけい\nにっこう\nにっさん\nにっしょく\nにっすう\nにっせき\nにってい\nになう\nにほん\nにまめ\nにもつ\nにやり\nにゅういん\nにりんしゃ\nにわとり\nにんい\nにんか\nにんき\nにんげん\nにんしき\nにんずう\nにんそう\nにんたい\nにんち\nにんてい\nにんにく\nにんぷ\nにんまり\nにんむ\nにんめい\nにんよう\nぬいくぎ\nぬかす\nぬぐいとる\nぬぐう\nぬくもり\nぬすむ\nぬまえび\nぬめり\nぬらす\nぬんちゃく\nねあげ\nねいき\nねいる\nねいろ\nねぐせ\nねくたい\nねくら\nねこぜ\nねこむ\nねさげ\nねすごす\nねそべる\nねだん\nねつい\nねっしん\nねつぞう\nねったいぎょ\nねぶそく\nねふだ\nねぼう\nねほりはほり\nねまき\nねまわし\nねみみ\nねむい\nねむたい\nねもと\nねらう\nねわざ\nねんいり\nねんおし\nねんかん\nねんきん\nねんぐ\nねんざ\nねんし\nねんちゃく\nねんど\nねんぴ\nねんぶつ\nねんまつ\nねんりょう\nねんれい\nのいず\nのおづま\nのがす\nのきなみ\nのこぎり\nのこす\nのこる\nのせる\nのぞく\nのぞむ\nのたまう\nのちほど\nのっく\nのばす\nのはら\nのべる\nのぼる\nのみもの\nのやま\nのらいぬ\nのらねこ\nのりもの\nのりゆき\nのれん\nのんき\nばあい\nはあく\nばあさん\nばいか\nばいく\nはいけん\nはいご\nはいしん\nはいすい\nはいせん\nはいそう\nはいち\nばいばい\nはいれつ\nはえる\nはおる\nはかい\nばかり\nはかる\nはくしゅ\nはけん\nはこぶ\nはさみ\nはさん\nはしご\nばしょ\nはしる\nはせる\nぱそこん\nはそん\nはたん\nはちみつ\nはつおん\nはっかく\nはづき\nはっきり\nはっくつ\nはっけん\nはっこう\nはっさん\nはっしん\nはったつ\nはっちゅう\nはってん\nはっぴょう\nはっぽう\nはなす\nはなび\nはにかむ\nはぶらし\nはみがき\nはむかう\nはめつ\nはやい\nはやし\nはらう\nはろうぃん\nはわい\nはんい\nはんえい\nはんおん\nはんかく\nはんきょう\nばんぐみ\nはんこ\nはんしゃ\nはんすう\nはんだん\nぱんち\nぱんつ\nはんてい\nはんとし\nはんのう\nはんぱ\nはんぶん\nはんぺん\nはんぼうき\nはんめい\nはんらん\nはんろん\nひいき\nひうん\nひえる\nひかく\nひかり\nひかる\nひかん\nひくい\nひけつ\nひこうき\nひこく\nひさい\nひさしぶり\nひさん\nびじゅつかん\nひしょ\nひそか\nひそむ\nひたむき\nひだり\nひたる\nひつぎ\nひっこし\nひっし\nひつじゅひん\nひっす\nひつぜん\nぴったり\nぴっちり\nひつよう\nひてい\nひとごみ\nひなまつり\nひなん\nひねる\nひはん\nひびく\nひひょう\nひほう\nひまわり\nひまん\nひみつ\nひめい\nひめじし\nひやけ\nひやす\nひよう\nびょうき\nひらがな\nひらく\nひりつ\nひりょう\nひるま\nひるやすみ\nひれい\nひろい\nひろう\nひろき\nひろゆき\nひんかく\nひんけつ\nひんこん\nひんしゅ\nひんそう\nぴんち\nひんぱん\nびんぼう\nふあん\nふいうち\nふうけい\nふうせん\nぷうたろう\nふうとう\nふうふ\nふえる\nふおん\nふかい\nふきん\nふくざつ\nふくぶくろ\nふこう\nふさい\nふしぎ\nふじみ\nふすま\nふせい\nふせぐ\nふそく\nぶたにく\nふたん\nふちょう\nふつう\nふつか\nふっかつ\nふっき\nふっこく\nぶどう\nふとる\nふとん\nふのう\nふはい\nふひょう\nふへん\nふまん\nふみん\nふめつ\nふめん\nふよう\nふりこ\nふりる\nふるい\nふんいき\nぶんがく\nぶんぐ\nふんしつ\nぶんせき\nふんそう\nぶんぽう\nへいあん\nへいおん\nへいがい\nへいき\nへいげん\nへいこう\nへいさ\nへいしゃ\nへいせつ\nへいそ\nへいたく\nへいてん\nへいねつ\nへいわ\nへきが\nへこむ\nべにいろ\nべにしょうが\nへらす\nへんかん\nべんきょう\nべんごし\nへんさい\nへんたい\nべんり\nほあん\nほいく\nぼうぎょ\nほうこく\nほうそう\nほうほう\nほうもん\nほうりつ\nほえる\nほおん\nほかん\nほきょう\nぼきん\nほくろ\nほけつ\nほけん\nほこう\nほこる\nほしい\nほしつ\nほしゅ\nほしょう\nほせい\nほそい\nほそく\nほたて\nほたる\nぽちぶくろ\nほっきょく\nほっさ\nほったん\nほとんど\nほめる\nほんい\nほんき\nほんけ\nほんしつ\nほんやく\nまいにち\nまかい\nまかせる\nまがる\nまける\nまこと\nまさつ\nまじめ\nますく\nまぜる\nまつり\nまとめ\nまなぶ\nまぬけ\nまねく\nまほう\nまもる\nまゆげ\nまよう\nまろやか\nまわす\nまわり\nまわる\nまんが\nまんきつ\nまんぞく\nまんなか\nみいら\nみうち\nみえる\nみがく\nみかた\nみかん\nみけん\nみこん\nみじかい\nみすい\nみすえる\nみせる\nみっか\nみつかる\nみつける\nみてい\nみとめる\nみなと\nみなみかさい\nみねらる\nみのう\nみのがす\nみほん\nみもと\nみやげ\nみらい\nみりょく\nみわく\nみんか\nみんぞく\nむいか\nむえき\nむえん\nむかい\nむかう\nむかえ\nむかし\nむぎちゃ\nむける\nむげん\nむさぼる\nむしあつい\nむしば\nむじゅん\nむしろ\nむすう\nむすこ\nむすぶ\nむすめ\nむせる\nむせん\nむちゅう\nむなしい\nむのう\nむやみ\nむよう\nむらさき\nむりょう\nむろん\nめいあん\nめいうん\nめいえん\nめいかく\nめいきょく\nめいさい\nめいし\nめいそう\nめいぶつ\nめいれい\nめいわく\nめぐまれる\nめざす\nめした\nめずらしい\nめだつ\nめまい\nめやす\nめんきょ\nめんせき\nめんどう\nもうしあげる\nもうどうけん\nもえる\nもくし\nもくてき\nもくようび\nもちろん\nもどる\nもらう\nもんく\nもんだい\nやおや\nやける\nやさい\nやさしい\nやすい\nやすたろう\nやすみ\nやせる\nやそう\nやたい\nやちん\nやっと\nやっぱり\nやぶる\nやめる\nややこしい\nやよい\nやわらかい\nゆうき\nゆうびんきょく\nゆうべ\nゆうめい\nゆけつ\nゆしゅつ\nゆせん\nゆそう\nゆたか\nゆちゃく\nゆでる\nゆにゅう\nゆびわ\nゆらい\nゆれる\nようい\nようか\nようきゅう\nようじ\nようす\nようちえん\nよかぜ\nよかん\nよきん\nよくせい\nよくぼう\nよけい\nよごれる\nよさん\nよしゅう\nよそう\nよそく\nよっか\nよてい\nよどがわく\nよねつ\nよやく\nよゆう\nよろこぶ\nよろしい\nらいう\nらくがき\nらくご\nらくさつ\nらくだ\nらしんばん\nらせん\nらぞく\nらたい\nらっか\nられつ\nりえき\nりかい\nりきさく\nりきせつ\nりくぐん\nりくつ\nりけん\nりこう\nりせい\nりそう\nりそく\nりてん\nりねん\nりゆう\nりゅうがく\nりよう\nりょうり\nりょかん\nりょくちゃ\nりょこう\nりりく\nりれき\nりろん\nりんご\nるいけい\nるいさい\nるいじ\nるいせき\nるすばん\nるりがわら\nれいかん\nれいぎ\nれいせい\nれいぞうこ\nれいとう\nれいぼう\nれきし\nれきだい\nれんあい\nれんけい\nれんこん\nれんさい\nれんしゅう\nれんぞく\nれんらく\nろうか\nろうご\nろうじん\nろうそく\nろくが\nろこつ\nろじうら\nろしゅつ\nろせん\nろてん\nろめん\nろれつ\nろんぎ\nろんぱ\nろんぶん\nろんり\nわかす\nわかめ\nわかやま\nわかれる\nわしつ\nわじまし\nわすれもの\nわらう\nわれる\n"); - dico.Add("spanish", - "ábaco\nabdomen\nabeja\nabierto\nabogado\nabono\naborto\nabrazo\nabrir\nabuelo\nabuso\nacabar\nacademia\nacceso\nacción\naceite\nacelga\nacento\naceptar\nácido\naclarar\nacné\nacoger\nacoso\nactivo\nacto\nactriz\nactuar\nacudir\nacuerdo\nacusar\nadicto\nadmitir\nadoptar\nadorno\naduana\nadulto\naéreo\nafectar\nafición\nafinar\nafirmar\nágil\nagitar\nagonía\nagosto\nagotar\nagregar\nagrio\nagua\nagudo\náguila\naguja\nahogo\nahorro\naire\naislar\najedrez\najeno\najuste\nalacrán\nalambre\nalarma\nalba\nálbum\nalcalde\naldea\nalegre\nalejar\nalerta\naleta\nalfiler\nalga\nalgodón\naliado\naliento\nalivio\nalma\nalmeja\nalmíbar\naltar\nalteza\naltivo\nalto\naltura\nalumno\nalzar\namable\namante\namapola\namargo\namasar\námbar\námbito\nameno\namigo\namistad\namor\namparo\namplio\nancho\nanciano\nancla\nandar\nandén\nanemia\nángulo\nanillo\nánimo\nanís\nanotar\nantena\nantiguo\nantojo\nanual\nanular\nanuncio\nañadir\nañejo\naño\napagar\naparato\napetito\napio\naplicar\napodo\naporte\napoyo\naprender\naprobar\napuesta\napuro\narado\naraña\narar\nárbitro\nárbol\narbusto\narchivo\narco\narder\nardilla\narduo\nárea\nárido\naries\narmonía\narnés\naroma\narpa\narpón\narreglo\narroz\narruga\narte\nartista\nasa\nasado\nasalto\nascenso\nasegurar\naseo\nasesor\nasiento\nasilo\nasistir\nasno\nasombro\náspero\nastilla\nastro\nastuto\nasumir\nasunto\natajo\nataque\natar\natento\nateo\nático\natleta\nátomo\natraer\natroz\natún\naudaz\naudio\nauge\naula\naumento\nausente\nautor\naval\navance\navaro\nave\navellana\navena\navestruz\navión\naviso\nayer\nayuda\nayuno\nazafrán\nazar\nazote\nazúcar\nazufre\nazul\nbaba\nbabor\nbache\nbahía\nbaile\nbajar\nbalanza\nbalcón\nbalde\nbambú\nbanco\nbanda\nbaño\nbarba\nbarco\nbarniz\nbarro\nbáscula\nbastón\nbasura\nbatalla\nbatería\nbatir\nbatuta\nbaúl\nbazar\nbebé\nbebida\nbello\nbesar\nbeso\nbestia\nbicho\nbien\nbingo\nblanco\nbloque\nblusa\nboa\nbobina\nbobo\nboca\nbocina\nboda\nbodega\nboina\nbola\nbolero\nbolsa\nbomba\nbondad\nbonito\nbono\nbonsái\nborde\nborrar\nbosque\nbote\nbotín\nbóveda\nbozal\nbravo\nbrazo\nbrecha\nbreve\nbrillo\nbrinco\nbrisa\nbroca\nbroma\nbronce\nbrote\nbruja\nbrusco\nbruto\nbuceo\nbucle\nbueno\nbuey\nbufanda\nbufón\nbúho\nbuitre\nbulto\nburbuja\nburla\nburro\nbuscar\nbutaca\nbuzón\ncaballo\ncabeza\ncabina\ncabra\ncacao\ncadáver\ncadena\ncaer\ncafé\ncaída\ncaimán\ncaja\ncajón\ncal\ncalamar\ncalcio\ncaldo\ncalidad\ncalle\ncalma\ncalor\ncalvo\ncama\ncambio\ncamello\ncamino\ncampo\ncáncer\ncandil\ncanela\ncanguro\ncanica\ncanto\ncaña\ncañón\ncaoba\ncaos\ncapaz\ncapitán\ncapote\ncaptar\ncapucha\ncara\ncarbón\ncárcel\ncareta\ncarga\ncariño\ncarne\ncarpeta\ncarro\ncarta\ncasa\ncasco\ncasero\ncaspa\ncastor\ncatorce\ncatre\ncaudal\ncausa\ncazo\ncebolla\nceder\ncedro\ncelda\ncélebre\nceloso\ncélula\ncemento\nceniza\ncentro\ncerca\ncerdo\ncereza\ncero\ncerrar\ncerteza\ncésped\ncetro\nchacal\nchaleco\nchampú\nchancla\nchapa\ncharla\nchico\nchiste\nchivo\nchoque\nchoza\nchuleta\nchupar\nciclón\nciego\ncielo\ncien\ncierto\ncifra\ncigarro\ncima\ncinco\ncine\ncinta\nciprés\ncirco\nciruela\ncisne\ncita\nciudad\nclamor\nclan\nclaro\nclase\nclave\ncliente\nclima\nclínica\ncobre\ncocción\ncochino\ncocina\ncoco\ncódigo\ncodo\ncofre\ncoger\ncohete\ncojín\ncojo\ncola\ncolcha\ncolegio\ncolgar\ncolina\ncollar\ncolmo\ncolumna\ncombate\ncomer\ncomida\ncómodo\ncompra\nconde\nconejo\nconga\nconocer\nconsejo\ncontar\ncopa\ncopia\ncorazón\ncorbata\ncorcho\ncordón\ncorona\ncorrer\ncoser\ncosmos\ncosta\ncráneo\ncráter\ncrear\ncrecer\ncreído\ncrema\ncría\ncrimen\ncripta\ncrisis\ncromo\ncrónica\ncroqueta\ncrudo\ncruz\ncuadro\ncuarto\ncuatro\ncubo\ncubrir\ncuchara\ncuello\ncuento\ncuerda\ncuesta\ncueva\ncuidar\nculebra\nculpa\nculto\ncumbre\ncumplir\ncuna\ncuneta\ncuota\ncupón\ncúpula\ncurar\ncurioso\ncurso\ncurva\ncutis\ndama\ndanza\ndar\ndardo\ndátil\ndeber\ndébil\ndécada\ndecir\ndedo\ndefensa\ndefinir\ndejar\ndelfín\ndelgado\ndelito\ndemora\ndenso\ndental\ndeporte\nderecho\nderrota\ndesayuno\ndeseo\ndesfile\ndesnudo\ndestino\ndesvío\ndetalle\ndetener\ndeuda\ndía\ndiablo\ndiadema\ndiamante\ndiana\ndiario\ndibujo\ndictar\ndiente\ndieta\ndiez\ndifícil\ndigno\ndilema\ndiluir\ndinero\ndirecto\ndirigir\ndisco\ndiseño\ndisfraz\ndiva\ndivino\ndoble\ndoce\ndolor\ndomingo\ndon\ndonar\ndorado\ndormir\ndorso\ndos\ndosis\ndragón\ndroga\nducha\nduda\nduelo\ndueño\ndulce\ndúo\nduque\ndurar\ndureza\nduro\nébano\nebrio\nechar\neco\necuador\nedad\nedición\nedificio\neditor\neducar\nefecto\neficaz\neje\nejemplo\nelefante\nelegir\nelemento\nelevar\nelipse\nélite\nelixir\nelogio\neludir\nembudo\nemitir\nemoción\nempate\nempeño\nempleo\nempresa\nenano\nencargo\nenchufe\nencía\nenemigo\nenero\nenfado\nenfermo\nengaño\nenigma\nenlace\nenorme\nenredo\nensayo\nenseñar\nentero\nentrar\nenvase\nenvío\népoca\nequipo\nerizo\nescala\nescena\nescolar\nescribir\nescudo\nesencia\nesfera\nesfuerzo\nespada\nespejo\nespía\nesposa\nespuma\nesquí\nestar\neste\nestilo\nestufa\netapa\neterno\nética\netnia\nevadir\nevaluar\nevento\nevitar\nexacto\nexamen\nexceso\nexcusa\nexento\nexigir\nexilio\nexistir\néxito\nexperto\nexplicar\nexponer\nextremo\nfábrica\nfábula\nfachada\nfácil\nfactor\nfaena\nfaja\nfalda\nfallo\nfalso\nfaltar\nfama\nfamilia\nfamoso\nfaraón\nfarmacia\nfarol\nfarsa\nfase\nfatiga\nfauna\nfavor\nfax\nfebrero\nfecha\nfeliz\nfeo\nferia\nferoz\nfértil\nfervor\nfestín\nfiable\nfianza\nfiar\nfibra\nficción\nficha\nfideo\nfiebre\nfiel\nfiera\nfiesta\nfigura\nfijar\nfijo\nfila\nfilete\nfilial\nfiltro\nfin\nfinca\nfingir\nfinito\nfirma\nflaco\nflauta\nflecha\nflor\nflota\nfluir\nflujo\nflúor\nfobia\nfoca\nfogata\nfogón\nfolio\nfolleto\nfondo\nforma\nforro\nfortuna\nforzar\nfosa\nfoto\nfracaso\nfrágil\nfranja\nfrase\nfraude\nfreír\nfreno\nfresa\nfrío\nfrito\nfruta\nfuego\nfuente\nfuerza\nfuga\nfumar\nfunción\nfunda\nfurgón\nfuria\nfusil\nfútbol\nfuturo\ngacela\ngafas\ngaita\ngajo\ngala\ngalería\ngallo\ngamba\nganar\ngancho\nganga\nganso\ngaraje\ngarza\ngasolina\ngastar\ngato\ngavilán\ngemelo\ngemir\ngen\ngénero\ngenio\ngente\ngeranio\ngerente\ngermen\ngesto\ngigante\ngimnasio\ngirar\ngiro\nglaciar\nglobo\ngloria\ngol\ngolfo\ngoloso\ngolpe\ngoma\ngordo\ngorila\ngorra\ngota\ngoteo\ngozar\ngrada\ngráfico\ngrano\ngrasa\ngratis\ngrave\ngrieta\ngrillo\ngripe\ngris\ngrito\ngrosor\ngrúa\ngrueso\ngrumo\ngrupo\nguante\nguapo\nguardia\nguerra\nguía\nguiño\nguion\nguiso\nguitarra\ngusano\ngustar\nhaber\nhábil\nhablar\nhacer\nhacha\nhada\nhallar\nhamaca\nharina\nhaz\nhazaña\nhebilla\nhebra\nhecho\nhelado\nhelio\nhembra\nherir\nhermano\nhéroe\nhervir\nhielo\nhierro\nhígado\nhigiene\nhijo\nhimno\nhistoria\nhocico\nhogar\nhoguera\nhoja\nhombre\nhongo\nhonor\nhonra\nhora\nhormiga\nhorno\nhostil\nhoyo\nhueco\nhuelga\nhuerta\nhueso\nhuevo\nhuida\nhuir\nhumano\nhúmedo\nhumilde\nhumo\nhundir\nhuracán\nhurto\nicono\nideal\nidioma\nídolo\niglesia\niglú\nigual\nilegal\nilusión\nimagen\nimán\nimitar\nimpar\nimperio\nimponer\nimpulso\nincapaz\níndice\ninerte\ninfiel\ninforme\ningenio\ninicio\ninmenso\ninmune\ninnato\ninsecto\ninstante\ninterés\níntimo\nintuir\ninútil\ninvierno\nira\niris\nironía\nisla\nislote\njabalí\njabón\njamón\njarabe\njardín\njarra\njaula\njazmín\njefe\njeringa\njinete\njornada\njoroba\njoven\njoya\njuerga\njueves\njuez\njugador\njugo\njuguete\njuicio\njunco\njungla\njunio\njuntar\njúpiter\njurar\njusto\njuvenil\njuzgar\nkilo\nkoala\nlabio\nlacio\nlacra\nlado\nladrón\nlagarto\nlágrima\nlaguna\nlaico\nlamer\nlámina\nlámpara\nlana\nlancha\nlangosta\nlanza\nlápiz\nlargo\nlarva\nlástima\nlata\nlátex\nlatir\nlaurel\nlavar\nlazo\nleal\nlección\nleche\nlector\nleer\nlegión\nlegumbre\nlejano\nlengua\nlento\nleña\nleón\nleopardo\nlesión\nletal\nletra\nleve\nleyenda\nlibertad\nlibro\nlicor\nlíder\nlidiar\nlienzo\nliga\nligero\nlima\nlímite\nlimón\nlimpio\nlince\nlindo\nlínea\nlingote\nlino\nlinterna\nlíquido\nliso\nlista\nlitera\nlitio\nlitro\nllaga\nllama\nllanto\nllave\nllegar\nllenar\nllevar\nllorar\nllover\nlluvia\nlobo\nloción\nloco\nlocura\nlógica\nlogro\nlombriz\nlomo\nlonja\nlote\nlucha\nlucir\nlugar\nlujo\nluna\nlunes\nlupa\nlustro\nluto\nluz\nmaceta\nmacho\nmadera\nmadre\nmaduro\nmaestro\nmafia\nmagia\nmago\nmaíz\nmaldad\nmaleta\nmalla\nmalo\nmamá\nmambo\nmamut\nmanco\nmando\nmanejar\nmanga\nmaniquí\nmanjar\nmano\nmanso\nmanta\nmañana\nmapa\nmáquina\nmar\nmarco\nmarea\nmarfil\nmargen\nmarido\nmármol\nmarrón\nmartes\nmarzo\nmasa\nmáscara\nmasivo\nmatar\nmateria\nmatiz\nmatriz\nmáximo\nmayor\nmazorca\nmecha\nmedalla\nmedio\nmédula\nmejilla\nmejor\nmelena\nmelón\nmemoria\nmenor\nmensaje\nmente\nmenú\nmercado\nmerengue\nmérito\nmes\nmesón\nmeta\nmeter\nmétodo\nmetro\nmezcla\nmiedo\nmiel\nmiembro\nmiga\nmil\nmilagro\nmilitar\nmillón\nmimo\nmina\nminero\nmínimo\nminuto\nmiope\nmirar\nmisa\nmiseria\nmisil\nmismo\nmitad\nmito\nmochila\nmoción\nmoda\nmodelo\nmoho\nmojar\nmolde\nmoler\nmolino\nmomento\nmomia\nmonarca\nmoneda\nmonja\nmonto\nmoño\nmorada\nmorder\nmoreno\nmorir\nmorro\nmorsa\nmortal\nmosca\nmostrar\nmotivo\nmover\nmóvil\nmozo\nmucho\nmudar\nmueble\nmuela\nmuerte\nmuestra\nmugre\nmujer\nmula\nmuleta\nmulta\nmundo\nmuñeca\nmural\nmuro\nmúsculo\nmuseo\nmusgo\nmúsica\nmuslo\nnácar\nnación\nnadar\nnaipe\nnaranja\nnariz\nnarrar\nnasal\nnatal\nnativo\nnatural\nnáusea\nnaval\nnave\nnavidad\nnecio\nnéctar\nnegar\nnegocio\nnegro\nneón\nnervio\nneto\nneutro\nnevar\nnevera\nnicho\nnido\nniebla\nnieto\nniñez\nniño\nnítido\nnivel\nnobleza\nnoche\nnómina\nnoria\nnorma\nnorte\nnota\nnoticia\nnovato\nnovela\nnovio\nnube\nnuca\nnúcleo\nnudillo\nnudo\nnuera\nnueve\nnuez\nnulo\nnúmero\nnutria\noasis\nobeso\nobispo\nobjeto\nobra\nobrero\nobservar\nobtener\nobvio\noca\nocaso\nocéano\nochenta\nocho\nocio\nocre\noctavo\noctubre\noculto\nocupar\nocurrir\nodiar\nodio\nodisea\noeste\nofensa\noferta\noficio\nofrecer\nogro\noído\noír\nojo\nola\noleada\nolfato\nolivo\nolla\nolmo\nolor\nolvido\nombligo\nonda\nonza\nopaco\nopción\nópera\nopinar\noponer\noptar\nóptica\nopuesto\noración\norador\noral\nórbita\norca\norden\noreja\nórgano\norgía\norgullo\noriente\norigen\norilla\noro\norquesta\noruga\nosadía\noscuro\nosezno\noso\nostra\notoño\notro\noveja\nóvulo\nóxido\noxígeno\noyente\nozono\npacto\npadre\npaella\npágina\npago\npaís\npájaro\npalabra\npalco\npaleta\npálido\npalma\npaloma\npalpar\npan\npanal\npánico\npantera\npañuelo\npapá\npapel\npapilla\npaquete\nparar\nparcela\npared\nparir\nparo\npárpado\nparque\npárrafo\nparte\npasar\npaseo\npasión\npaso\npasta\npata\npatio\npatria\npausa\npauta\npavo\npayaso\npeatón\npecado\npecera\npecho\npedal\npedir\npegar\npeine\npelar\npeldaño\npelea\npeligro\npellejo\npelo\npeluca\npena\npensar\npeñón\npeón\npeor\npepino\npequeño\npera\npercha\nperder\npereza\nperfil\nperico\nperla\npermiso\nperro\npersona\npesa\npesca\npésimo\npestaña\npétalo\npetróleo\npez\npezuña\npicar\npichón\npie\npiedra\npierna\npieza\npijama\npilar\npiloto\npimienta\npino\npintor\npinza\npiña\npiojo\npipa\npirata\npisar\npiscina\npiso\npista\npitón\npizca\nplaca\nplan\nplata\nplaya\nplaza\npleito\npleno\nplomo\npluma\nplural\npobre\npoco\npoder\npodio\npoema\npoesía\npoeta\npolen\npolicía\npollo\npolvo\npomada\npomelo\npomo\npompa\nponer\nporción\nportal\nposada\nposeer\nposible\nposte\npotencia\npotro\npozo\nprado\nprecoz\npregunta\npremio\nprensa\npreso\nprevio\nprimo\npríncipe\nprisión\nprivar\nproa\nprobar\nproceso\nproducto\nproeza\nprofesor\nprograma\nprole\npromesa\npronto\npropio\npróximo\nprueba\npúblico\npuchero\npudor\npueblo\npuerta\npuesto\npulga\npulir\npulmón\npulpo\npulso\npuma\npunto\npuñal\npuño\npupa\npupila\npuré\nquedar\nqueja\nquemar\nquerer\nqueso\nquieto\nquímica\nquince\nquitar\nrábano\nrabia\nrabo\nración\nradical\nraíz\nrama\nrampa\nrancho\nrango\nrapaz\nrápido\nrapto\nrasgo\nraspa\nrato\nrayo\nraza\nrazón\nreacción\nrealidad\nrebaño\nrebote\nrecaer\nreceta\nrechazo\nrecoger\nrecreo\nrecto\nrecurso\nred\nredondo\nreducir\nreflejo\nreforma\nrefrán\nrefugio\nregalo\nregir\nregla\nregreso\nrehén\nreino\nreír\nreja\nrelato\nrelevo\nrelieve\nrelleno\nreloj\nremar\nremedio\nremo\nrencor\nrendir\nrenta\nreparto\nrepetir\nreposo\nreptil\nres\nrescate\nresina\nrespeto\nresto\nresumen\nretiro\nretorno\nretrato\nreunir\nrevés\nrevista\nrey\nrezar\nrico\nriego\nrienda\nriesgo\nrifa\nrígido\nrigor\nrincón\nriñón\nrío\nriqueza\nrisa\nritmo\nrito\nrizo\nroble\nroce\nrociar\nrodar\nrodeo\nrodilla\nroer\nrojizo\nrojo\nromero\nromper\nron\nronco\nronda\nropa\nropero\nrosa\nrosca\nrostro\nrotar\nrubí\nrubor\nrudo\nrueda\nrugir\nruido\nruina\nruleta\nrulo\nrumbo\nrumor\nruptura\nruta\nrutina\nsábado\nsaber\nsabio\nsable\nsacar\nsagaz\nsagrado\nsala\nsaldo\nsalero\nsalir\nsalmón\nsalón\nsalsa\nsalto\nsalud\nsalvar\nsamba\nsanción\nsandía\nsanear\nsangre\nsanidad\nsano\nsanto\nsapo\nsaque\nsardina\nsartén\nsastre\nsatán\nsauna\nsaxofón\nsección\nseco\nsecreto\nsecta\nsed\nseguir\nseis\nsello\nselva\nsemana\nsemilla\nsenda\nsensor\nseñal\nseñor\nseparar\nsepia\nsequía\nser\nserie\nsermón\nservir\nsesenta\nsesión\nseta\nsetenta\nsevero\nsexo\nsexto\nsidra\nsiesta\nsiete\nsiglo\nsigno\nsílaba\nsilbar\nsilencio\nsilla\nsímbolo\nsimio\nsirena\nsistema\nsitio\nsituar\nsobre\nsocio\nsodio\nsol\nsolapa\nsoldado\nsoledad\nsólido\nsoltar\nsolución\nsombra\nsondeo\nsonido\nsonoro\nsonrisa\nsopa\nsoplar\nsoporte\nsordo\nsorpresa\nsorteo\nsostén\nsótano\nsuave\nsubir\nsuceso\nsudor\nsuegra\nsuelo\nsueño\nsuerte\nsufrir\nsujeto\nsultán\nsumar\nsuperar\nsuplir\nsuponer\nsupremo\nsur\nsurco\nsureño\nsurgir\nsusto\nsutil\ntabaco\ntabique\ntabla\ntabú\ntaco\ntacto\ntajo\ntalar\ntalco\ntalento\ntalla\ntalón\ntamaño\ntambor\ntango\ntanque\ntapa\ntapete\ntapia\ntapón\ntaquilla\ntarde\ntarea\ntarifa\ntarjeta\ntarot\ntarro\ntarta\ntatuaje\ntauro\ntaza\ntazón\nteatro\ntecho\ntecla\ntécnica\ntejado\ntejer\ntejido\ntela\nteléfono\ntema\ntemor\ntemplo\ntenaz\ntender\ntener\ntenis\ntenso\nteoría\nterapia\nterco\ntérmino\nternura\nterror\ntesis\ntesoro\ntestigo\ntetera\ntexto\ntez\ntibio\ntiburón\ntiempo\ntienda\ntierra\ntieso\ntigre\ntijera\ntilde\ntimbre\ntímido\ntimo\ntinta\ntío\ntípico\ntipo\ntira\ntirón\ntitán\ntítere\ntítulo\ntiza\ntoalla\ntobillo\ntocar\ntocino\ntodo\ntoga\ntoldo\ntomar\ntono\ntonto\ntopar\ntope\ntoque\ntórax\ntorero\ntormenta\ntorneo\ntoro\ntorpedo\ntorre\ntorso\ntortuga\ntos\ntosco\ntoser\ntóxico\ntrabajo\ntractor\ntraer\ntráfico\ntrago\ntraje\ntramo\ntrance\ntrato\ntrauma\ntrazar\ntrébol\ntregua\ntreinta\ntren\ntrepar\ntres\ntribu\ntrigo\ntripa\ntriste\ntriunfo\ntrofeo\ntrompa\ntronco\ntropa\ntrote\ntrozo\ntruco\ntrueno\ntrufa\ntubería\ntubo\ntuerto\ntumba\ntumor\ntúnel\ntúnica\nturbina\nturismo\nturno\ntutor\nubicar\núlcera\numbral\nunidad\nunir\nuniverso\nuno\nuntar\nuña\nurbano\nurbe\nurgente\nurna\nusar\nusuario\nútil\nutopía\nuva\nvaca\nvacío\nvacuna\nvagar\nvago\nvaina\nvajilla\nvale\nválido\nvalle\nvalor\nválvula\nvampiro\nvara\nvariar\nvarón\nvaso\nvecino\nvector\nvehículo\nveinte\nvejez\nvela\nvelero\nveloz\nvena\nvencer\nvenda\nveneno\nvengar\nvenir\nventa\nvenus\nver\nverano\nverbo\nverde\nvereda\nverja\nverso\nverter\nvía\nviaje\nvibrar\nvicio\nvíctima\nvida\nvídeo\nvidrio\nviejo\nviernes\nvigor\nvil\nvilla\nvinagre\nvino\nviñedo\nviolín\nviral\nvirgo\nvirtud\nvisor\nvíspera\nvista\nvitamina\nviudo\nvivaz\nvivero\nvivir\nvivo\nvolcán\nvolumen\nvolver\nvoraz\nvotar\nvoto\nvoz\nvuelo\nvulgar\nyacer\nyate\nyegua\nyema\nyerno\nyeso\nyodo\nyoga\nyogur\nzafiro\nzanja\nzapato\nzarza\nzona\nzorro\nzumo\nzurdo\n"); - dico.Add("french", - "abaisser\nabandon\nabdiquer\nabeille\nabolir\naborder\naboutir\naboyer\nabrasif\nabreuver\nabriter\nabroger\nabrupt\nabsence\nabsolu\nabsurde\nabusif\nabyssal\nacadémie\nacajou\nacarien\naccabler\naccepter\nacclamer\naccolade\naccroche\naccuser\nacerbe\nachat\nacheter\naciduler\nacier\nacompte\nacquérir\nacronyme\nacteur\nactif\nactuel\nadepte\nadéquat\nadhésif\nadjectif\nadjuger\nadmettre\nadmirer\nadopter\nadorer\nadoucir\nadresse\nadroit\nadulte\nadverbe\naérer\naéronef\naffaire\naffecter\naffiche\naffreux\naffubler\nagacer\nagencer\nagile\nagiter\nagrafer\nagréable\nagrume\naider\naiguille\nailier\naimable\naisance\najouter\najuster\nalarmer\nalchimie\nalerte\nalgèbre\nalgue\naliéner\naliment\nalléger\nalliage\nallouer\nallumer\nalourdir\nalpaga\naltesse\nalvéole\namateur\nambigu\nambre\naménager\namertume\namidon\namiral\namorcer\namour\namovible\namphibie\nampleur\namusant\nanalyse\nanaphore\nanarchie\nanatomie\nancien\nanéantir\nangle\nangoisse\nanguleux\nanimal\nannexer\nannonce\nannuel\nanodin\nanomalie\nanonyme\nanormal\nantenne\nantidote\nanxieux\napaiser\napéritif\naplanir\napologie\nappareil\nappeler\napporter\nappuyer\naquarium\naqueduc\narbitre\narbuste\nardeur\nardoise\nargent\narlequin\narmature\narmement\narmoire\narmure\narpenter\narracher\narriver\narroser\narsenic\nartériel\narticle\naspect\nasphalte\naspirer\nassaut\nasservir\nassiette\nassocier\nassurer\nasticot\nastre\nastuce\natelier\natome\natrium\natroce\nattaque\nattentif\nattirer\nattraper\naubaine\nauberge\naudace\naudible\naugurer\naurore\nautomne\nautruche\navaler\navancer\navarice\navenir\naverse\naveugle\naviateur\navide\navion\naviser\navoine\navouer\navril\naxial\naxiome\nbadge\nbafouer\nbagage\nbaguette\nbaignade\nbalancer\nbalcon\nbaleine\nbalisage\nbambin\nbancaire\nbandage\nbanlieue\nbannière\nbanquier\nbarbier\nbaril\nbaron\nbarque\nbarrage\nbassin\nbastion\nbataille\nbateau\nbatterie\nbaudrier\nbavarder\nbelette\nbélier\nbelote\nbénéfice\nberceau\nberger\nberline\nbermuda\nbesace\nbesogne\nbétail\nbeurre\nbiberon\nbicycle\nbidule\nbijou\nbilan\nbilingue\nbillard\nbinaire\nbiologie\nbiopsie\nbiotype\nbiscuit\nbison\nbistouri\nbitume\nbizarre\nblafard\nblague\nblanchir\nblessant\nblinder\nblond\nbloquer\nblouson\nbobard\nbobine\nboire\nboiser\nbolide\nbonbon\nbondir\nbonheur\nbonifier\nbonus\nbordure\nborne\nbotte\nboucle\nboueux\nbougie\nboulon\nbouquin\nbourse\nboussole\nboutique\nboxeur\nbranche\nbrasier\nbrave\nbrebis\nbrèche\nbreuvage\nbricoler\nbrigade\nbrillant\nbrioche\nbrique\nbrochure\nbroder\nbronzer\nbrousse\nbroyeur\nbrume\nbrusque\nbrutal\nbruyant\nbuffle\nbuisson\nbulletin\nbureau\nburin\nbustier\nbutiner\nbutoir\nbuvable\nbuvette\ncabanon\ncabine\ncachette\ncadeau\ncadre\ncaféine\ncaillou\ncaisson\ncalculer\ncalepin\ncalibre\ncalmer\ncalomnie\ncalvaire\ncamarade\ncaméra\ncamion\ncampagne\ncanal\ncaneton\ncanon\ncantine\ncanular\ncapable\ncaporal\ncaprice\ncapsule\ncapter\ncapuche\ncarabine\ncarbone\ncaresser\ncaribou\ncarnage\ncarotte\ncarreau\ncarton\ncascade\ncasier\ncasque\ncassure\ncauser\ncaution\ncavalier\ncaverne\ncaviar\ncédille\nceinture\ncéleste\ncellule\ncendrier\ncensurer\ncentral\ncercle\ncérébral\ncerise\ncerner\ncerveau\ncesser\nchagrin\nchaise\nchaleur\nchambre\nchance\nchapitre\ncharbon\nchasseur\nchaton\nchausson\nchavirer\nchemise\nchenille\nchéquier\nchercher\ncheval\nchien\nchiffre\nchignon\nchimère\nchiot\nchlorure\nchocolat\nchoisir\nchose\nchouette\nchrome\nchute\ncigare\ncigogne\ncimenter\ncinéma\ncintrer\ncirculer\ncirer\ncirque\nciterne\ncitoyen\ncitron\ncivil\nclairon\nclameur\nclaquer\nclasse\nclavier\nclient\ncligner\nclimat\nclivage\ncloche\nclonage\ncloporte\ncobalt\ncobra\ncocasse\ncocotier\ncoder\ncodifier\ncoffre\ncogner\ncohésion\ncoiffer\ncoincer\ncolère\ncolibri\ncolline\ncolmater\ncolonel\ncombat\ncomédie\ncommande\ncompact\nconcert\nconduire\nconfier\ncongeler\nconnoter\nconsonne\ncontact\nconvexe\ncopain\ncopie\ncorail\ncorbeau\ncordage\ncorniche\ncorpus\ncorrect\ncortège\ncosmique\ncostume\ncoton\ncoude\ncoupure\ncourage\ncouteau\ncouvrir\ncoyote\ncrabe\ncrainte\ncravate\ncrayon\ncréature\ncréditer\ncrémeux\ncreuser\ncrevette\ncribler\ncrier\ncristal\ncritère\ncroire\ncroquer\ncrotale\ncrucial\ncruel\ncrypter\ncubique\ncueillir\ncuillère\ncuisine\ncuivre\nculminer\ncultiver\ncumuler\ncupide\ncuratif\ncurseur\ncyanure\ncycle\ncylindre\ncynique\ndaigner\ndamier\ndanger\ndanseur\ndauphin\ndébattre\ndébiter\ndéborder\ndébrider\ndébutant\ndécaler\ndécembre\ndéchirer\ndécider\ndéclarer\ndécorer\ndécrire\ndécupler\ndédale\ndéductif\ndéesse\ndéfensif\ndéfiler\ndéfrayer\ndégager\ndégivrer\ndéglutir\ndégrafer\ndéjeuner\ndélice\ndéloger\ndemander\ndemeurer\ndémolir\ndénicher\ndénouer\ndentelle\ndénuder\ndépart\ndépenser\ndéphaser\ndéplacer\ndéposer\ndéranger\ndérober\ndésastre\ndescente\ndésert\ndésigner\ndésobéir\ndessiner\ndestrier\ndétacher\ndétester\ndétourer\ndétresse\ndevancer\ndevenir\ndeviner\ndevoir\ndiable\ndialogue\ndiamant\ndicter\ndifférer\ndigérer\ndigital\ndigne\ndiluer\ndimanche\ndiminuer\ndioxyde\ndirectif\ndiriger\ndiscuter\ndisposer\ndissiper\ndistance\ndivertir\ndiviser\ndocile\ndocteur\ndogme\ndoigt\ndomaine\ndomicile\ndompter\ndonateur\ndonjon\ndonner\ndopamine\ndortoir\ndorure\ndosage\ndoseur\ndossier\ndotation\ndouanier\ndouble\ndouceur\ndouter\ndoyen\ndragon\ndraper\ndresser\ndribbler\ndroiture\nduperie\nduplexe\ndurable\ndurcir\ndynastie\néblouir\nécarter\nécharpe\néchelle\néclairer\néclipse\néclore\nécluse\nécole\néconomie\nécorce\nécouter\nécraser\nécrémer\nécrivain\nécrou\nécume\nécureuil\nédifier\néduquer\neffacer\neffectif\neffigie\neffort\neffrayer\neffusion\négaliser\négarer\néjecter\nélaborer\nélargir\nélectron\nélégant\néléphant\nélève\néligible\nélitisme\néloge\nélucider\néluder\nemballer\nembellir\nembryon\némeraude\némission\nemmener\némotion\némouvoir\nempereur\nemployer\nemporter\nemprise\némulsion\nencadrer\nenchère\nenclave\nencoche\nendiguer\nendosser\nendroit\nenduire\nénergie\nenfance\nenfermer\nenfouir\nengager\nengin\nenglober\nénigme\nenjamber\nenjeu\nenlever\nennemi\nennuyeux\nenrichir\nenrobage\nenseigne\nentasser\nentendre\nentier\nentourer\nentraver\nénumérer\nenvahir\nenviable\nenvoyer\nenzyme\néolien\népaissir\népargne\népatant\népaule\népicerie\népidémie\népier\népilogue\népine\népisode\népitaphe\népoque\népreuve\néprouver\népuisant\néquerre\néquipe\nériger\nérosion\nerreur\néruption\nescalier\nespadon\nespèce\nespiègle\nespoir\nesprit\nesquiver\nessayer\nessence\nessieu\nessorer\nestime\nestomac\nestrade\nétagère\nétaler\nétanche\nétatique\néteindre\nétendoir\néternel\néthanol\néthique\nethnie\nétirer\nétoffer\nétoile\nétonnant\nétourdir\nétrange\nétroit\nétude\neuphorie\névaluer\névasion\néventail\névidence\néviter\névolutif\névoquer\nexact\nexagérer\nexaucer\nexceller\nexcitant\nexclusif\nexcuse\nexécuter\nexemple\nexercer\nexhaler\nexhorter\nexigence\nexiler\nexister\nexotique\nexpédier\nexplorer\nexposer\nexprimer\nexquis\nextensif\nextraire\nexulter\nfable\nfabuleux\nfacette\nfacile\nfacture\nfaiblir\nfalaise\nfameux\nfamille\nfarceur\nfarfelu\nfarine\nfarouche\nfasciner\nfatal\nfatigue\nfaucon\nfautif\nfaveur\nfavori\nfébrile\nféconder\nfédérer\nfélin\nfemme\nfémur\nfendoir\nféodal\nfermer\nféroce\nferveur\nfestival\nfeuille\nfeutre\nfévrier\nfiasco\nficeler\nfictif\nfidèle\nfigure\nfilature\nfiletage\nfilière\nfilleul\nfilmer\nfilou\nfiltrer\nfinancer\nfinir\nfiole\nfirme\nfissure\nfixer\nflairer\nflamme\nflasque\nflatteur\nfléau\nflèche\nfleur\nflexion\nflocon\nflore\nfluctuer\nfluide\nfluvial\nfolie\nfonderie\nfongible\nfontaine\nforcer\nforgeron\nformuler\nfortune\nfossile\nfoudre\nfougère\nfouiller\nfoulure\nfourmi\nfragile\nfraise\nfranchir\nfrapper\nfrayeur\nfrégate\nfreiner\nfrelon\nfrémir\nfrénésie\nfrère\nfriable\nfriction\nfrisson\nfrivole\nfroid\nfromage\nfrontal\nfrotter\nfruit\nfugitif\nfuite\nfureur\nfurieux\nfurtif\nfusion\nfutur\ngagner\ngalaxie\ngalerie\ngambader\ngarantir\ngardien\ngarnir\ngarrigue\ngazelle\ngazon\ngéant\ngélatine\ngélule\ngendarme\ngénéral\ngénie\ngenou\ngentil\ngéologie\ngéomètre\ngéranium\ngerme\ngestuel\ngeyser\ngibier\ngicler\ngirafe\ngivre\nglace\nglaive\nglisser\nglobe\ngloire\nglorieux\ngolfeur\ngomme\ngonfler\ngorge\ngorille\ngoudron\ngouffre\ngoulot\ngoupille\ngourmand\ngoutte\ngraduel\ngraffiti\ngraine\ngrand\ngrappin\ngratuit\ngravir\ngrenat\ngriffure\ngriller\ngrimper\ngrogner\ngronder\ngrotte\ngroupe\ngruger\ngrutier\ngruyère\nguépard\nguerrier\nguide\nguimauve\nguitare\ngustatif\ngymnaste\ngyrostat\nhabitude\nhachoir\nhalte\nhameau\nhangar\nhanneton\nharicot\nharmonie\nharpon\nhasard\nhélium\nhématome\nherbe\nhérisson\nhermine\nhéron\nhésiter\nheureux\nhiberner\nhibou\nhilarant\nhistoire\nhiver\nhomard\nhommage\nhomogène\nhonneur\nhonorer\nhonteux\nhorde\nhorizon\nhorloge\nhormone\nhorrible\nhouleux\nhousse\nhublot\nhuileux\nhumain\nhumble\nhumide\nhumour\nhurler\nhydromel\nhygiène\nhymne\nhypnose\nidylle\nignorer\niguane\nillicite\nillusion\nimage\nimbiber\nimiter\nimmense\nimmobile\nimmuable\nimpact\nimpérial\nimplorer\nimposer\nimprimer\nimputer\nincarner\nincendie\nincident\nincliner\nincolore\nindexer\nindice\ninductif\ninédit\nineptie\ninexact\ninfini\ninfliger\ninformer\ninfusion\ningérer\ninhaler\ninhiber\ninjecter\ninjure\ninnocent\ninoculer\ninonder\ninscrire\ninsecte\ninsigne\ninsolite\ninspirer\ninstinct\ninsulter\nintact\nintense\nintime\nintrigue\nintuitif\ninutile\ninvasion\ninventer\ninviter\ninvoquer\nironique\nirradier\nirréel\nirriter\nisoler\nivoire\nivresse\njaguar\njaillir\njambe\njanvier\njardin\njauger\njaune\njavelot\njetable\njeton\njeudi\njeunesse\njoindre\njoncher\njongler\njoueur\njouissif\njournal\njovial\njoyau\njoyeux\njubiler\njugement\njunior\njupon\njuriste\njustice\njuteux\njuvénile\nkayak\nkimono\nkiosque\nlabel\nlabial\nlabourer\nlacérer\nlactose\nlagune\nlaine\nlaisser\nlaitier\nlambeau\nlamelle\nlampe\nlanceur\nlangage\nlanterne\nlapin\nlargeur\nlarme\nlaurier\nlavabo\nlavoir\nlecture\nlégal\nléger\nlégume\nlessive\nlettre\nlevier\nlexique\nlézard\nliasse\nlibérer\nlibre\nlicence\nlicorne\nliège\nlièvre\nligature\nligoter\nligue\nlimer\nlimite\nlimonade\nlimpide\nlinéaire\nlingot\nlionceau\nliquide\nlisière\nlister\nlithium\nlitige\nlittoral\nlivreur\nlogique\nlointain\nloisir\nlombric\nloterie\nlouer\nlourd\nloutre\nlouve\nloyal\nlubie\nlucide\nlucratif\nlueur\nlugubre\nluisant\nlumière\nlunaire\nlundi\nluron\nlutter\nluxueux\nmachine\nmagasin\nmagenta\nmagique\nmaigre\nmaillon\nmaintien\nmairie\nmaison\nmajorer\nmalaxer\nmaléfice\nmalheur\nmalice\nmallette\nmammouth\nmandater\nmaniable\nmanquant\nmanteau\nmanuel\nmarathon\nmarbre\nmarchand\nmardi\nmaritime\nmarqueur\nmarron\nmarteler\nmascotte\nmassif\nmatériel\nmatière\nmatraque\nmaudire\nmaussade\nmauve\nmaximal\nméchant\nméconnu\nmédaille\nmédecin\nméditer\nméduse\nmeilleur\nmélange\nmélodie\nmembre\nmémoire\nmenacer\nmener\nmenhir\nmensonge\nmentor\nmercredi\nmérite\nmerle\nmessager\nmesure\nmétal\nmétéore\nméthode\nmétier\nmeuble\nmiauler\nmicrobe\nmiette\nmignon\nmigrer\nmilieu\nmillion\nmimique\nmince\nminéral\nminimal\nminorer\nminute\nmiracle\nmiroiter\nmissile\nmixte\nmobile\nmoderne\nmoelleux\nmondial\nmoniteur\nmonnaie\nmonotone\nmonstre\nmontagne\nmonument\nmoqueur\nmorceau\nmorsure\nmortier\nmoteur\nmotif\nmouche\nmoufle\nmoulin\nmousson\nmouton\nmouvant\nmultiple\nmunition\nmuraille\nmurène\nmurmure\nmuscle\nmuséum\nmusicien\nmutation\nmuter\nmutuel\nmyriade\nmyrtille\nmystère\nmythique\nnageur\nnappe\nnarquois\nnarrer\nnatation\nnation\nnature\nnaufrage\nnautique\nnavire\nnébuleux\nnectar\nnéfaste\nnégation\nnégliger\nnégocier\nneige\nnerveux\nnettoyer\nneurone\nneutron\nneveu\nniche\nnickel\nnitrate\nniveau\nnoble\nnocif\nnocturne\nnoirceur\nnoisette\nnomade\nnombreux\nnommer\nnormatif\nnotable\nnotifier\nnotoire\nnourrir\nnouveau\nnovateur\nnovembre\nnovice\nnuage\nnuancer\nnuire\nnuisible\nnuméro\nnuptial\nnuque\nnutritif\nobéir\nobjectif\nobliger\nobscur\nobserver\nobstacle\nobtenir\nobturer\noccasion\noccuper\nocéan\noctobre\noctroyer\noctupler\noculaire\nodeur\nodorant\noffenser\nofficier\noffrir\nogive\noiseau\noisillon\nolfactif\nolivier\nombrage\nomettre\nonctueux\nonduler\nonéreux\nonirique\nopale\nopaque\nopérer\nopinion\nopportun\nopprimer\nopter\noptique\norageux\norange\norbite\nordonner\noreille\norgane\norgueil\norifice\nornement\norque\nortie\nosciller\nosmose\nossature\notarie\nouragan\nourson\noutil\noutrager\nouvrage\novation\noxyde\noxygène\nozone\npaisible\npalace\npalmarès\npalourde\npalper\npanache\npanda\npangolin\npaniquer\npanneau\npanorama\npantalon\npapaye\npapier\npapoter\npapyrus\nparadoxe\nparcelle\nparesse\nparfumer\nparler\nparole\nparrain\nparsemer\npartager\nparure\nparvenir\npassion\npastèque\npaternel\npatience\npatron\npavillon\npavoiser\npayer\npaysage\npeigne\npeintre\npelage\npélican\npelle\npelouse\npeluche\npendule\npénétrer\npénible\npensif\npénurie\npépite\npéplum\nperdrix\nperforer\npériode\npermuter\nperplexe\npersil\nperte\npeser\npétale\npetit\npétrir\npeuple\npharaon\nphobie\nphoque\nphoton\nphrase\nphysique\npiano\npictural\npièce\npierre\npieuvre\npilote\npinceau\npipette\npiquer\npirogue\npiscine\npiston\npivoter\npixel\npizza\nplacard\nplafond\nplaisir\nplaner\nplaque\nplastron\nplateau\npleurer\nplexus\npliage\nplomb\nplonger\npluie\nplumage\npochette\npoésie\npoète\npointe\npoirier\npoisson\npoivre\npolaire\npolicier\npollen\npolygone\npommade\npompier\nponctuel\npondérer\nponey\nportique\nposition\nposséder\nposture\npotager\npoteau\npotion\npouce\npoulain\npoumon\npourpre\npoussin\npouvoir\nprairie\npratique\nprécieux\nprédire\npréfixe\nprélude\nprénom\nprésence\nprétexte\nprévoir\nprimitif\nprince\nprison\npriver\nproblème\nprocéder\nprodige\nprofond\nprogrès\nproie\nprojeter\nprologue\npromener\npropre\nprospère\nprotéger\nprouesse\nproverbe\nprudence\npruneau\npsychose\npublic\npuceron\npuiser\npulpe\npulsar\npunaise\npunitif\npupitre\npurifier\npuzzle\npyramide\nquasar\nquerelle\nquestion\nquiétude\nquitter\nquotient\nracine\nraconter\nradieux\nragondin\nraideur\nraisin\nralentir\nrallonge\nramasser\nrapide\nrasage\nratisser\nravager\nravin\nrayonner\nréactif\nréagir\nréaliser\nréanimer\nrecevoir\nréciter\nréclamer\nrécolter\nrecruter\nreculer\nrecycler\nrédiger\nredouter\nrefaire\nréflexe\nréformer\nrefrain\nrefuge\nrégalien\nrégion\nréglage\nrégulier\nréitérer\nrejeter\nrejouer\nrelatif\nrelever\nrelief\nremarque\nremède\nremise\nremonter\nremplir\nremuer\nrenard\nrenfort\nrenifler\nrenoncer\nrentrer\nrenvoi\nreplier\nreporter\nreprise\nreptile\nrequin\nréserve\nrésineux\nrésoudre\nrespect\nrester\nrésultat\nrétablir\nretenir\nréticule\nretomber\nretracer\nréunion\nréussir\nrevanche\nrevivre\nrévolte\nrévulsif\nrichesse\nrideau\nrieur\nrigide\nrigoler\nrincer\nriposter\nrisible\nrisque\nrituel\nrival\nrivière\nrocheux\nromance\nrompre\nronce\nrondin\nroseau\nrosier\nrotatif\nrotor\nrotule\nrouge\nrouille\nrouleau\nroutine\nroyaume\nruban\nrubis\nruche\nruelle\nrugueux\nruiner\nruisseau\nruser\nrustique\nrythme\nsabler\nsaboter\nsabre\nsacoche\nsafari\nsagesse\nsaisir\nsalade\nsalive\nsalon\nsaluer\nsamedi\nsanction\nsanglier\nsarcasme\nsardine\nsaturer\nsaugrenu\nsaumon\nsauter\nsauvage\nsavant\nsavonner\nscalpel\nscandale\nscélérat\nscénario\nsceptre\nschéma\nscience\nscinder\nscore\nscrutin\nsculpter\nséance\nsécable\nsécher\nsecouer\nsécréter\nsédatif\nséduire\nseigneur\nséjour\nsélectif\nsemaine\nsembler\nsemence\nséminal\nsénateur\nsensible\nsentence\nséparer\nséquence\nserein\nsergent\nsérieux\nserrure\nsérum\nservice\nsésame\nsévir\nsevrage\nsextuple\nsidéral\nsiècle\nsiéger\nsiffler\nsigle\nsignal\nsilence\nsilicium\nsimple\nsincère\nsinistre\nsiphon\nsirop\nsismique\nsituer\nskier\nsocial\nsocle\nsodium\nsoigneux\nsoldat\nsoleil\nsolitude\nsoluble\nsombre\nsommeil\nsomnoler\nsonde\nsongeur\nsonnette\nsonore\nsorcier\nsortir\nsosie\nsottise\nsoucieux\nsoudure\nsouffle\nsoulever\nsoupape\nsource\nsoutirer\nsouvenir\nspacieux\nspatial\nspécial\nsphère\nspiral\nstable\nstation\nsternum\nstimulus\nstipuler\nstrict\nstudieux\nstupeur\nstyliste\nsublime\nsubstrat\nsubtil\nsubvenir\nsuccès\nsucre\nsuffixe\nsuggérer\nsuiveur\nsulfate\nsuperbe\nsupplier\nsurface\nsuricate\nsurmener\nsurprise\nsursaut\nsurvie\nsuspect\nsyllabe\nsymbole\nsymétrie\nsynapse\nsyntaxe\nsystème\ntabac\ntablier\ntactile\ntailler\ntalent\ntalisman\ntalonner\ntambour\ntamiser\ntangible\ntapis\ntaquiner\ntarder\ntarif\ntartine\ntasse\ntatami\ntatouage\ntaupe\ntaureau\ntaxer\ntémoin\ntemporel\ntenaille\ntendre\nteneur\ntenir\ntension\nterminer\nterne\nterrible\ntétine\ntexte\nthème\nthéorie\nthérapie\nthorax\ntibia\ntiède\ntimide\ntirelire\ntiroir\ntissu\ntitane\ntitre\ntituber\ntoboggan\ntolérant\ntomate\ntonique\ntonneau\ntoponyme\ntorche\ntordre\ntornade\ntorpille\ntorrent\ntorse\ntortue\ntotem\ntoucher\ntournage\ntousser\ntoxine\ntraction\ntrafic\ntragique\ntrahir\ntrain\ntrancher\ntravail\ntrèfle\ntremper\ntrésor\ntreuil\ntriage\ntribunal\ntricoter\ntrilogie\ntriomphe\ntripler\ntriturer\ntrivial\ntrombone\ntronc\ntropical\ntroupeau\ntuile\ntulipe\ntumulte\ntunnel\nturbine\ntuteur\ntutoyer\ntuyau\ntympan\ntyphon\ntypique\ntyran\nubuesque\nultime\nultrason\nunanime\nunifier\nunion\nunique\nunitaire\nunivers\nuranium\nurbain\nurticant\nusage\nusine\nusuel\nusure\nutile\nutopie\nvacarme\nvaccin\nvagabond\nvague\nvaillant\nvaincre\nvaisseau\nvalable\nvalise\nvallon\nvalve\nvampire\nvanille\nvapeur\nvarier\nvaseux\nvassal\nvaste\nvecteur\nvedette\nvégétal\nvéhicule\nveinard\nvéloce\nvendredi\nvénérer\nvenger\nvenimeux\nventouse\nverdure\nvérin\nvernir\nverrou\nverser\nvertu\nveston\nvétéran\nvétuste\nvexant\nvexer\nviaduc\nviande\nvictoire\nvidange\nvidéo\nvignette\nvigueur\nvilain\nvillage\nvinaigre\nviolon\nvipère\nvirement\nvirtuose\nvirus\nvisage\nviseur\nvision\nvisqueux\nvisuel\nvital\nvitesse\nviticole\nvitrine\nvivace\nvivipare\nvocation\nvoguer\nvoile\nvoisin\nvoiture\nvolaille\nvolcan\nvoltiger\nvolume\nvorace\nvortex\nvoter\nvouloir\nvoyage\nvoyelle\nwagon\nxénon\nyacht\nzèbre\nzénith\nzeste\nzoologie"); - dico.Add("portuguese_brazil", - "abacate\nabaixo\nabalar\nabater\nabduzir\nabelha\naberto\nabismo\nabotoar\nabranger\nabreviar\nabrigar\nabrupto\nabsinto\nabsoluto\nabsurdo\nabutre\nacabado\nacalmar\nacampar\nacanhar\nacaso\naceitar\nacelerar\nacenar\nacervo\nacessar\nacetona\nachatar\nacidez\nacima\nacionado\nacirrar\naclamar\naclive\nacolhida\nacomodar\nacoplar\nacordar\nacumular\nacusador\nadaptar\nadega\nadentro\nadepto\nadequar\naderente\nadesivo\nadeus\nadiante\naditivo\nadjetivo\nadjunto\nadmirar\nadorar\nadquirir\nadubo\nadverso\nadvogado\naeronave\nafastar\naferir\nafetivo\nafinador\nafivelar\naflito\nafluente\nafrontar\nagachar\nagarrar\nagasalho\nagenciar\nagilizar\nagiota\nagitado\nagora\nagradar\nagreste\nagrupar\naguardar\nagulha\najoelhar\najudar\najustar\nalameda\nalarme\nalastrar\nalavanca\nalbergue\nalbino\nalcatra\naldeia\nalecrim\nalegria\nalertar\nalface\nalfinete\nalgum\nalheio\naliar\nalicate\nalienar\nalinhar\naliviar\nalmofada\nalocar\nalpiste\nalterar\naltitude\nalucinar\nalugar\naluno\nalusivo\nalvo\namaciar\namador\namarelo\namassar\nambas\nambiente\nameixa\namenizar\namido\namistoso\namizade\namolador\namontoar\namoroso\namostra\namparar\nampliar\nampola\nanagrama\nanalisar\nanarquia\nanatomia\nandaime\nanel\nanexo\nangular\nanimar\nanjo\nanomalia\nanotado\nansioso\nanterior\nanuidade\nanunciar\nanzol\napagador\napalpar\napanhado\napego\napelido\napertada\napesar\napetite\napito\naplauso\naplicada\napoio\napontar\naposta\naprendiz\naprovar\naquecer\narame\naranha\narara\narcada\nardente\nareia\narejar\narenito\naresta\nargiloso\nargola\narma\narquivo\narraial\narrebate\narriscar\narroba\narrumar\narsenal\narterial\nartigo\narvoredo\nasfaltar\nasilado\naspirar\nassador\nassinar\nassoalho\nassunto\nastral\natacado\natadura\natalho\natarefar\natear\natender\naterro\nateu\natingir\natirador\nativo\natoleiro\natracar\natrevido\natriz\natual\natum\nauditor\naumentar\naura\naurora\nautismo\nautoria\nautuar\navaliar\navante\navaria\navental\navesso\naviador\navisar\navulso\naxila\nazarar\nazedo\nazeite\nazulejo\nbabar\nbabosa\nbacalhau\nbacharel\nbacia\nbagagem\nbaiano\nbailar\nbaioneta\nbairro\nbaixista\nbajular\nbaleia\nbaliza\nbalsa\nbanal\nbandeira\nbanho\nbanir\nbanquete\nbarato\nbarbado\nbaronesa\nbarraca\nbarulho\nbaseado\nbastante\nbatata\nbatedor\nbatida\nbatom\nbatucar\nbaunilha\nbeber\nbeijo\nbeirada\nbeisebol\nbeldade\nbeleza\nbelga\nbeliscar\nbendito\nbengala\nbenzer\nberimbau\nberlinda\nberro\nbesouro\nbexiga\nbezerro\nbico\nbicudo\nbienal\nbifocal\nbifurcar\nbigorna\nbilhete\nbimestre\nbimotor\nbiologia\nbiombo\nbiosfera\nbipolar\nbirrento\nbiscoito\nbisneto\nbispo\nbissexto\nbitola\nbizarro\nblindado\nbloco\nbloquear\nboato\nbobagem\nbocado\nbocejo\nbochecha\nboicotar\nbolada\nboletim\nbolha\nbolo\nbombeiro\nbonde\nboneco\nbonita\nborbulha\nborda\nboreal\nborracha\nbovino\nboxeador\nbranco\nbrasa\nbraveza\nbreu\nbriga\nbrilho\nbrincar\nbroa\nbrochura\nbronzear\nbroto\nbruxo\nbucha\nbudismo\nbufar\nbule\nburaco\nbusca\nbusto\nbuzina\ncabana\ncabelo\ncabide\ncabo\ncabrito\ncacau\ncacetada\ncachorro\ncacique\ncadastro\ncadeado\ncafezal\ncaiaque\ncaipira\ncaixote\ncajado\ncaju\ncalafrio\ncalcular\ncaldeira\ncalibrar\ncalmante\ncalota\ncamada\ncambista\ncamisa\ncamomila\ncampanha\ncamuflar\ncanavial\ncancelar\ncaneta\ncanguru\ncanhoto\ncanivete\ncanoa\ncansado\ncantar\ncanudo\ncapacho\ncapela\ncapinar\ncapotar\ncapricho\ncaptador\ncapuz\ncaracol\ncarbono\ncardeal\ncareca\ncarimbar\ncarneiro\ncarpete\ncarreira\ncartaz\ncarvalho\ncasaco\ncasca\ncasebre\ncastelo\ncasulo\ncatarata\ncativar\ncaule\ncausador\ncautelar\ncavalo\ncaverna\ncebola\ncedilha\ncegonha\ncelebrar\ncelular\ncenoura\ncenso\ncenteio\ncercar\ncerrado\ncerteiro\ncerveja\ncetim\ncevada\nchacota\nchaleira\nchamado\nchapada\ncharme\nchatice\nchave\nchefe\nchegada\ncheiro\ncheque\nchicote\nchifre\nchinelo\nchocalho\nchover\nchumbo\nchutar\nchuva\ncicatriz\nciclone\ncidade\ncidreira\nciente\ncigana\ncimento\ncinto\ncinza\nciranda\ncircuito\ncirurgia\ncitar\nclareza\nclero\nclicar\nclone\nclube\ncoado\ncoagir\ncobaia\ncobertor\ncobrar\ncocada\ncoelho\ncoentro\ncoeso\ncogumelo\ncoibir\ncoifa\ncoiote\ncolar\ncoleira\ncolher\ncolidir\ncolmeia\ncolono\ncoluna\ncomando\ncombinar\ncomentar\ncomitiva\ncomover\ncomplexo\ncomum\nconcha\ncondor\nconectar\nconfuso\ncongelar\nconhecer\nconjugar\nconsumir\ncontrato\nconvite\ncooperar\ncopeiro\ncopiador\ncopo\ncoquetel\ncoragem\ncordial\ncorneta\ncoronha\ncorporal\ncorreio\ncortejo\ncoruja\ncorvo\ncosseno\ncostela\ncotonete\ncouro\ncouve\ncovil\ncozinha\ncratera\ncravo\ncreche\ncredor\ncreme\ncrer\ncrespo\ncriada\ncriminal\ncrioulo\ncrise\ncriticar\ncrosta\ncrua\ncruzeiro\ncubano\ncueca\ncuidado\ncujo\nculatra\nculminar\nculpar\ncultura\ncumprir\ncunhado\ncupido\ncurativo\ncurral\ncursar\ncurto\ncuspir\ncustear\ncutelo\ndamasco\ndatar\ndebater\ndebitar\ndeboche\ndebulhar\ndecalque\ndecimal\ndeclive\ndecote\ndecretar\ndedal\ndedicado\ndeduzir\ndefesa\ndefumar\ndegelo\ndegrau\ndegustar\ndeitado\ndeixar\ndelator\ndelegado\ndelinear\ndelonga\ndemanda\ndemitir\ndemolido\ndentista\ndepenado\ndepilar\ndepois\ndepressa\ndepurar\nderiva\nderramar\ndesafio\ndesbotar\ndescanso\ndesenho\ndesfiado\ndesgaste\ndesigual\ndeslize\ndesmamar\ndesova\ndespesa\ndestaque\ndesviar\ndetalhar\ndetentor\ndetonar\ndetrito\ndeusa\ndever\ndevido\ndevotado\ndezena\ndiagrama\ndialeto\ndidata\ndifuso\ndigitar\ndilatado\ndiluente\ndiminuir\ndinastia\ndinheiro\ndiocese\ndireto\ndiscreta\ndisfarce\ndisparo\ndisquete\ndissipar\ndistante\nditador\ndiurno\ndiverso\ndivisor\ndivulgar\ndizer\ndobrador\ndolorido\ndomador\ndominado\ndonativo\ndonzela\ndormente\ndorsal\ndosagem\ndourado\ndoutor\ndrenagem\ndrible\ndrogaria\nduelar\nduende\ndueto\nduplo\nduquesa\ndurante\nduvidoso\neclodir\necoar\necologia\nedificar\nedital\neducado\nefeito\nefetivar\nejetar\nelaborar\neleger\neleitor\nelenco\nelevador\neliminar\nelogiar\nembargo\nembolado\nembrulho\nembutido\nemenda\nemergir\nemissor\nempatia\nempenho\nempinado\nempolgar\nemprego\nempurrar\nemulador\nencaixe\nencenado\nenchente\nencontro\nendeusar\nendossar\nenfaixar\nenfeite\nenfim\nengajado\nengenho\nenglobar\nengomado\nengraxar\nenguia\nenjoar\nenlatar\nenquanto\nenraizar\nenrolado\nenrugar\nensaio\nenseada\nensino\nensopado\nentanto\nenteado\nentidade\nentortar\nentrada\nentulho\nenvergar\nenviado\nenvolver\nenxame\nenxerto\nenxofre\nenxuto\nepiderme\nequipar\nereto\nerguido\nerrata\nerva\nervilha\nesbanjar\nesbelto\nescama\nescola\nescrita\nescuta\nesfinge\nesfolar\nesfregar\nesfumado\nesgrima\nesmalte\nespanto\nespelho\nespiga\nesponja\nespreita\nespumar\nesquerda\nestaca\nesteira\nesticar\nestofado\nestrela\nestudo\nesvaziar\netanol\netiqueta\neuforia\neuropeu\nevacuar\nevaporar\nevasivo\neventual\nevidente\nevoluir\nexagero\nexalar\nexaminar\nexato\nexausto\nexcesso\nexcitar\nexclamar\nexecutar\nexemplo\nexibir\nexigente\nexonerar\nexpandir\nexpelir\nexpirar\nexplanar\nexposto\nexpresso\nexpulsar\nexterno\nextinto\nextrato\nfabricar\nfabuloso\nfaceta\nfacial\nfada\nfadiga\nfaixa\nfalar\nfalta\nfamiliar\nfandango\nfanfarra\nfantoche\nfardado\nfarelo\nfarinha\nfarofa\nfarpa\nfartura\nfatia\nfator\nfavorita\nfaxina\nfazenda\nfechado\nfeijoada\nfeirante\nfelino\nfeminino\nfenda\nfeno\nfera\nferiado\nferrugem\nferver\nfestejar\nfetal\nfeudal\nfiapo\nfibrose\nficar\nficheiro\nfigurado\nfileira\nfilho\nfilme\nfiltrar\nfirmeza\nfisgada\nfissura\nfita\nfivela\nfixador\nfixo\nflacidez\nflamingo\nflanela\nflechada\nflora\nflutuar\nfluxo\nfocal\nfocinho\nfofocar\nfogo\nfoguete\nfoice\nfolgado\nfolheto\nforjar\nformiga\nforno\nforte\nfosco\nfossa\nfragata\nfralda\nfrango\nfrasco\nfraterno\nfreira\nfrente\nfretar\nfrieza\nfriso\nfritura\nfronha\nfrustrar\nfruteira\nfugir\nfulano\nfuligem\nfundar\nfungo\nfunil\nfurador\nfurioso\nfutebol\ngabarito\ngabinete\ngado\ngaiato\ngaiola\ngaivota\ngalega\ngalho\ngalinha\ngalocha\nganhar\ngaragem\ngarfo\ngargalo\ngarimpo\ngaroupa\ngarrafa\ngasoduto\ngasto\ngata\ngatilho\ngaveta\ngazela\ngelado\ngeleia\ngelo\ngemada\ngemer\ngemido\ngeneroso\ngengiva\ngenial\ngenoma\ngenro\ngeologia\ngerador\ngerminar\ngesso\ngestor\nginasta\ngincana\ngingado\ngirafa\ngirino\nglacial\nglicose\nglobal\nglorioso\ngoela\ngoiaba\ngolfe\ngolpear\ngordura\ngorjeta\ngorro\ngostoso\ngoteira\ngovernar\ngracejo\ngradual\ngrafite\ngralha\ngrampo\ngranada\ngratuito\ngraveto\ngraxa\ngrego\ngrelhar\ngreve\ngrilo\ngrisalho\ngritaria\ngrosso\ngrotesco\ngrudado\ngrunhido\ngruta\nguache\nguarani\nguaxinim\nguerrear\nguiar\nguincho\nguisado\ngula\nguloso\nguru\nhabitar\nharmonia\nhaste\nhaver\nhectare\nherdar\nheresia\nhesitar\nhiato\nhibernar\nhidratar\nhiena\nhino\nhipismo\nhipnose\nhipoteca\nhoje\nholofote\nhomem\nhonesto\nhonrado\nhormonal\nhospedar\nhumorado\niate\nideia\nidoso\nignorado\nigreja\niguana\nileso\nilha\niludido\niluminar\nilustrar\nimagem\nimediato\nimenso\nimersivo\niminente\nimitador\nimortal\nimpacto\nimpedir\nimplante\nimpor\nimprensa\nimpune\nimunizar\ninalador\ninapto\ninativo\nincenso\ninchar\nincidir\nincluir\nincolor\nindeciso\nindireto\nindutor\nineficaz\ninerente\ninfantil\ninfestar\ninfinito\ninflamar\ninformal\ninfrator\ningerir\ninibido\ninicial\ninimigo\ninjetar\ninocente\ninodoro\ninovador\ninox\ninquieto\ninscrito\ninseto\ninsistir\ninspetor\ninstalar\ninsulto\nintacto\nintegral\nintimar\nintocado\nintriga\ninvasor\ninverno\ninvicto\ninvocar\niogurte\niraniano\nironizar\nirreal\nirritado\nisca\nisento\nisolado\nisqueiro\nitaliano\njaneiro\njangada\njanta\njararaca\njardim\njarro\njasmim\njato\njavali\njazida\njejum\njoaninha\njoelhada\njogador\njoia\njornal\njorrar\njovem\njuba\njudeu\njudoca\njuiz\njulgador\njulho\njurado\njurista\njuro\njusta\nlabareda\nlaboral\nlacre\nlactante\nladrilho\nlagarta\nlagoa\nlaje\nlamber\nlamentar\nlaminar\nlampejo\nlanche\nlapidar\nlapso\nlaranja\nlareira\nlargura\nlasanha\nlastro\nlateral\nlatido\nlavanda\nlavoura\nlavrador\nlaxante\nlazer\nlealdade\nlebre\nlegado\nlegendar\nlegista\nleigo\nleiloar\nleitura\nlembrete\nleme\nlenhador\nlentilha\nleoa\nlesma\nleste\nletivo\nletreiro\nlevar\nleveza\nlevitar\nliberal\nlibido\nliderar\nligar\nligeiro\nlimitar\nlimoeiro\nlimpador\nlinda\nlinear\nlinhagem\nliquidez\nlistagem\nlisura\nlitoral\nlivro\nlixa\nlixeira\nlocador\nlocutor\nlojista\nlombo\nlona\nlonge\nlontra\nlorde\nlotado\nloteria\nloucura\nlousa\nlouvar\nluar\nlucidez\nlucro\nluneta\nlustre\nlutador\nluva\nmacaco\nmacete\nmachado\nmacio\nmadeira\nmadrinha\nmagnata\nmagreza\nmaior\nmais\nmalandro\nmalha\nmalote\nmaluco\nmamilo\nmamoeiro\nmamute\nmanada\nmancha\nmandato\nmanequim\nmanhoso\nmanivela\nmanobrar\nmansa\nmanter\nmanusear\nmapeado\nmaquinar\nmarcador\nmaresia\nmarfim\nmargem\nmarinho\nmarmita\nmaroto\nmarquise\nmarreco\nmartelo\nmarujo\nmascote\nmasmorra\nmassagem\nmastigar\nmatagal\nmaterno\nmatinal\nmatutar\nmaxilar\nmedalha\nmedida\nmedusa\nmegafone\nmeiga\nmelancia\nmelhor\nmembro\nmemorial\nmenino\nmenos\nmensagem\nmental\nmerecer\nmergulho\nmesada\nmesclar\nmesmo\nmesquita\nmestre\nmetade\nmeteoro\nmetragem\nmexer\nmexicano\nmicro\nmigalha\nmigrar\nmilagre\nmilenar\nmilhar\nmimado\nminerar\nminhoca\nministro\nminoria\nmiolo\nmirante\nmirtilo\nmisturar\nmocidade\nmoderno\nmodular\nmoeda\nmoer\nmoinho\nmoita\nmoldura\nmoleza\nmolho\nmolinete\nmolusco\nmontanha\nmoqueca\nmorango\nmorcego\nmordomo\nmorena\nmosaico\nmosquete\nmostarda\nmotel\nmotim\nmoto\nmotriz\nmuda\nmuito\nmulata\nmulher\nmultar\nmundial\nmunido\nmuralha\nmurcho\nmuscular\nmuseu\nmusical\nnacional\nnadador\nnaja\nnamoro\nnarina\nnarrado\nnascer\nnativa\nnatureza\nnavalha\nnavegar\nnavio\nneblina\nnebuloso\nnegativa\nnegociar\nnegrito\nnervoso\nneta\nneural\nnevasca\nnevoeiro\nninar\nninho\nnitidez\nnivelar\nnobreza\nnoite\nnoiva\nnomear\nnominal\nnordeste\nnortear\nnotar\nnoticiar\nnoturno\nnovelo\nnovilho\nnovo\nnublado\nnudez\nnumeral\nnupcial\nnutrir\nnuvem\nobcecado\nobedecer\nobjetivo\nobrigado\nobscuro\nobstetra\nobter\nobturar\nocidente\nocioso\nocorrer\noculista\nocupado\nofegante\nofensiva\noferenda\noficina\nofuscado\nogiva\nolaria\noleoso\nolhar\noliveira\nombro\nomelete\nomisso\nomitir\nondulado\noneroso\nontem\nopcional\noperador\noponente\noportuno\noposto\norar\norbitar\nordem\nordinal\norfanato\norgasmo\norgulho\noriental\norigem\noriundo\norla\nortodoxo\norvalho\noscilar\nossada\nosso\nostentar\notimismo\nousadia\noutono\noutubro\nouvido\novelha\novular\noxidar\noxigenar\npacato\npaciente\npacote\npactuar\npadaria\npadrinho\npagar\npagode\npainel\npairar\npaisagem\npalavra\npalestra\npalheta\npalito\npalmada\npalpitar\npancada\npanela\npanfleto\npanqueca\npantanal\npapagaio\npapelada\npapiro\nparafina\nparcial\npardal\nparede\npartida\npasmo\npassado\npastel\npatamar\npatente\npatinar\npatrono\npaulada\npausar\npeculiar\npedalar\npedestre\npediatra\npedra\npegada\npeitoral\npeixe\npele\npelicano\npenca\npendurar\npeneira\npenhasco\npensador\npente\nperceber\nperfeito\npergunta\nperito\npermitir\nperna\nperplexo\npersiana\npertence\nperuca\npescado\npesquisa\npessoa\npetiscar\npiada\npicado\npiedade\npigmento\npilastra\npilhado\npilotar\npimenta\npincel\npinguim\npinha\npinote\npintar\npioneiro\npipoca\npiquete\npiranha\npires\npirueta\npiscar\npistola\npitanga\npivete\nplanta\nplaqueta\nplatina\nplebeu\nplumagem\npluvial\npneu\npoda\npoeira\npoetisa\npolegada\npoliciar\npoluente\npolvilho\npomar\npomba\nponderar\npontaria\npopuloso\nporta\npossuir\npostal\npote\npoupar\npouso\npovoar\npraia\nprancha\nprato\npraxe\nprece\npredador\nprefeito\npremiar\nprensar\npreparar\npresilha\npretexto\nprevenir\nprezar\nprimata\nprincesa\nprisma\nprivado\nprocesso\nproduto\nprofeta\nproibido\nprojeto\nprometer\npropagar\nprosa\nprotetor\nprovador\npublicar\npudim\npular\npulmonar\npulseira\npunhal\npunir\npupilo\npureza\npuxador\nquadra\nquantia\nquarto\nquase\nquebrar\nqueda\nqueijo\nquente\nquerido\nquimono\nquina\nquiosque\nrabanada\nrabisco\nrachar\nracionar\nradial\nraiar\nrainha\nraio\nraiva\nrajada\nralado\nramal\nranger\nranhura\nrapadura\nrapel\nrapidez\nraposa\nraquete\nraridade\nrasante\nrascunho\nrasgar\nraspador\nrasteira\nrasurar\nratazana\nratoeira\nrealeza\nreanimar\nreaver\nrebaixar\nrebelde\nrebolar\nrecado\nrecente\nrecheio\nrecibo\nrecordar\nrecrutar\nrecuar\nrede\nredimir\nredonda\nreduzida\nreenvio\nrefinar\nrefletir\nrefogar\nrefresco\nrefugiar\nregalia\nregime\nregra\nreinado\nreitor\nrejeitar\nrelativo\nremador\nremendo\nremorso\nrenovado\nreparo\nrepelir\nrepleto\nrepolho\nrepresa\nrepudiar\nrequerer\nresenha\nresfriar\nresgatar\nresidir\nresolver\nrespeito\nressaca\nrestante\nresumir\nretalho\nreter\nretirar\nretomada\nretratar\nrevelar\nrevisor\nrevolta\nriacho\nrica\nrigidez\nrigoroso\nrimar\nringue\nrisada\nrisco\nrisonho\nrobalo\nrochedo\nrodada\nrodeio\nrodovia\nroedor\nroleta\nromano\nroncar\nrosado\nroseira\nrosto\nrota\nroteiro\nrotina\nrotular\nrouco\nroupa\nroxo\nrubro\nrugido\nrugoso\nruivo\nrumo\nrupestre\nrusso\nsabor\nsaciar\nsacola\nsacudir\nsadio\nsafira\nsaga\nsagrada\nsaibro\nsalada\nsaleiro\nsalgado\nsaliva\nsalpicar\nsalsicha\nsaltar\nsalvador\nsambar\nsamurai\nsanar\nsanfona\nsangue\nsanidade\nsapato\nsarda\nsargento\nsarjeta\nsaturar\nsaudade\nsaxofone\nsazonal\nsecar\nsecular\nseda\nsedento\nsediado\nsedoso\nsedutor\nsegmento\nsegredo\nsegundo\nseiva\nseleto\nselvagem\nsemanal\nsemente\nsenador\nsenhor\nsensual\nsentado\nseparado\nsereia\nseringa\nserra\nservo\nsetembro\nsetor\nsigilo\nsilhueta\nsilicone\nsimetria\nsimpatia\nsimular\nsinal\nsincero\nsingular\nsinopse\nsintonia\nsirene\nsiri\nsituado\nsoberano\nsobra\nsocorro\nsogro\nsoja\nsolda\nsoletrar\nsolteiro\nsombrio\nsonata\nsondar\nsonegar\nsonhador\nsono\nsoprano\nsoquete\nsorrir\nsorteio\nsossego\nsotaque\nsoterrar\nsovado\nsozinho\nsuavizar\nsubida\nsubmerso\nsubsolo\nsubtrair\nsucata\nsucesso\nsuco\nsudeste\nsufixo\nsugador\nsugerir\nsujeito\nsulfato\nsumir\nsuor\nsuperior\nsuplicar\nsuposto\nsuprimir\nsurdina\nsurfista\nsurpresa\nsurreal\nsurtir\nsuspiro\nsustento\ntabela\ntablete\ntabuada\ntacho\ntagarela\ntalher\ntalo\ntalvez\ntamanho\ntamborim\ntampa\ntangente\ntanto\ntapar\ntapioca\ntardio\ntarefa\ntarja\ntarraxa\ntatuagem\ntaurino\ntaxativo\ntaxista\nteatral\ntecer\ntecido\nteclado\ntedioso\nteia\nteimar\ntelefone\ntelhado\ntempero\ntenente\ntensor\ntentar\ntermal\nterno\nterreno\ntese\ntesoura\ntestado\nteto\ntextura\ntexugo\ntiara\ntigela\ntijolo\ntimbrar\ntimidez\ntingido\ntinteiro\ntiragem\ntitular\ntoalha\ntocha\ntolerar\ntolice\ntomada\ntomilho\ntonel\ntontura\ntopete\ntora\ntorcido\ntorneio\ntorque\ntorrada\ntorto\ntostar\ntouca\ntoupeira\ntoxina\ntrabalho\ntracejar\ntradutor\ntrafegar\ntrajeto\ntrama\ntrancar\ntrapo\ntraseiro\ntratador\ntravar\ntreino\ntremer\ntrepidar\ntrevo\ntriagem\ntribo\ntriciclo\ntridente\ntrilogia\ntrindade\ntriplo\ntriturar\ntriunfal\ntrocar\ntrombeta\ntrova\ntrunfo\ntruque\ntubular\ntucano\ntudo\ntulipa\ntupi\nturbo\nturma\nturquesa\ntutelar\ntutorial\nuivar\numbigo\nunha\nunidade\nuniforme\nurologia\nurso\nurtiga\nurubu\nusado\nusina\nusufruir\nvacina\nvadiar\nvagaroso\nvaidoso\nvala\nvalente\nvalidade\nvalores\nvantagem\nvaqueiro\nvaranda\nvareta\nvarrer\nvascular\nvasilha\nvassoura\nvazar\nvazio\nveado\nvedar\nvegetar\nveicular\nveleiro\nvelhice\nveludo\nvencedor\nvendaval\nvenerar\nventre\nverbal\nverdade\nvereador\nvergonha\nvermelho\nverniz\nversar\nvertente\nvespa\nvestido\nvetorial\nviaduto\nviagem\nviajar\nviatura\nvibrador\nvideira\nvidraria\nviela\nviga\nvigente\nvigiar\nvigorar\nvilarejo\nvinco\nvinheta\nvinil\nvioleta\nvirada\nvirtude\nvisitar\nvisto\nvitral\nviveiro\nvizinho\nvoador\nvoar\nvogal\nvolante\nvoleibol\nvoltagem\nvolumoso\nvontade\nvulto\nvuvuzela\nxadrez\nxarope\nxeque\nxeretar\nxerife\nxingar\nzangado\nzarpar\nzebu\nzelador\nzombar\nzoologia\nzumbido"); - dico.Add("czech", - "abdikace\nabeceda\nadresa\nagrese\nakce\naktovka\nalej\nalkohol\namputace\nananas\nandulka\nanekdota\nanketa\nantika\nanulovat\narcha\narogance\nasfalt\nasistent\naspirace\nastma\nastronom\natlas\natletika\natol\nautobus\nazyl\nbabka\nbachor\nbacil\nbaculka\nbadatel\nbageta\nbagr\nbahno\nbakterie\nbalada\nbaletka\nbalkon\nbalonek\nbalvan\nbalza\nbambus\nbankomat\nbarbar\nbaret\nbarman\nbaroko\nbarva\nbaterka\nbatoh\nbavlna\nbazalka\nbazilika\nbazuka\nbedna\nberan\nbeseda\nbestie\nbeton\nbezinka\nbezmoc\nbeztak\nbicykl\nbidlo\nbiftek\nbikiny\nbilance\nbiograf\nbiolog\nbitva\nbizon\nblahobyt\nblatouch\nblecha\nbledule\nblesk\nblikat\nblizna\nblokovat\nbloudit\nblud\nbobek\nbobr\nbodlina\nbodnout\nbohatost\nbojkot\nbojovat\nbokorys\nbolest\nborec\nborovice\nbota\nboubel\nbouchat\nbouda\nboule\nbourat\nboxer\nbradavka\nbrambora\nbranka\nbratr\nbrepta\nbriketa\nbrko\nbrloh\nbronz\nbroskev\nbrunetka\nbrusinka\nbrzda\nbrzy\nbublina\nbubnovat\nbuchta\nbuditel\nbudka\nbudova\nbufet\nbujarost\nbukvice\nbuldok\nbulva\nbunda\nbunkr\nburza\nbutik\nbuvol\nbuzola\nbydlet\nbylina\nbytovka\nbzukot\ncapart\ncarevna\ncedr\ncedule\ncejch\ncejn\ncela\nceler\ncelkem\ncelnice\ncenina\ncennost\ncenovka\ncentrum\ncenzor\ncestopis\ncetka\nchalupa\nchapadlo\ncharita\nchata\nchechtat\nchemie\nchichot\nchirurg\nchlad\nchleba\nchlubit\nchmel\nchmura\nchobot\nchochol\nchodba\ncholera\nchomout\nchopit\nchoroba\nchov\nchrapot\nchrlit\nchrt\nchrup\nchtivost\nchudina\nchutnat\nchvat\nchvilka\nchvost\nchyba\nchystat\nchytit\ncibule\ncigareta\ncihelna\ncihla\ncinkot\ncirkus\ncisterna\ncitace\ncitrus\ncizinec\ncizost\nclona\ncokoliv\ncouvat\nctitel\nctnost\ncudnost\ncuketa\ncukr\ncupot\ncvaknout\ncval\ncvik\ncvrkot\ncyklista\ndaleko\ndareba\ndatel\ndatum\ndcera\ndebata\ndechovka\ndecibel\ndeficit\ndeflace\ndekl\ndekret\ndemokrat\ndeprese\nderby\ndeska\ndetektiv\ndikobraz\ndiktovat\ndioda\ndiplom\ndisk\ndisplej\ndivadlo\ndivoch\ndlaha\ndlouho\ndluhopis\ndnes\ndobro\ndobytek\ndocent\ndochutit\ndodnes\ndohled\ndohoda\ndohra\ndojem\ndojnice\ndoklad\ndokola\ndoktor\ndokument\ndolar\ndoleva\ndolina\ndoma\ndominant\ndomluvit\ndomov\ndonutit\ndopad\ndopis\ndoplnit\ndoposud\ndoprovod\ndopustit\ndorazit\ndorost\ndort\ndosah\ndoslov\ndostatek\ndosud\ndosyta\ndotaz\ndotek\ndotknout\ndoufat\ndoutnat\ndovozce\ndozadu\ndoznat\ndozorce\ndrahota\ndrak\ndramatik\ndravec\ndraze\ndrdol\ndrobnost\ndrogerie\ndrozd\ndrsnost\ndrtit\ndrzost\nduben\nduchovno\ndudek\nduha\nduhovka\ndusit\ndusno\ndutost\ndvojice\ndvorec\ndynamit\nekolog\nekonomie\nelektron\nelipsa\nemail\nemise\nemoce\nempatie\nepizoda\nepocha\nepopej\nepos\nesej\nesence\neskorta\neskymo\netiketa\neuforie\nevoluce\nexekuce\nexkurze\nexpedice\nexploze\nexport\nextrakt\nfacka\nfajfka\nfakulta\nfanatik\nfantazie\nfarmacie\nfavorit\nfazole\nfederace\nfejeton\nfenka\nfialka\nfigurant\nfilozof\nfiltr\nfinance\nfinta\nfixace\nfjord\nflanel\nflirt\nflotila\nfond\nfosfor\nfotbal\nfotka\nfoton\nfrakce\nfreska\nfronta\nfukar\nfunkce\nfyzika\ngaleje\ngarant\ngenetika\ngeolog\ngilotina\nglazura\nglejt\ngolem\ngolfista\ngotika\ngraf\ngramofon\ngranule\ngrep\ngril\ngrog\ngroteska\nguma\nhadice\nhadr\nhala\nhalenka\nhanba\nhanopis\nharfa\nharpuna\nhavran\nhebkost\nhejkal\nhejno\nhejtman\nhektar\nhelma\nhematom\nherec\nherna\nheslo\nhezky\nhistorik\nhladovka\nhlasivky\nhlava\nhledat\nhlen\nhlodavec\nhloh\nhloupost\nhltat\nhlubina\nhluchota\nhmat\nhmota\nhmyz\nhnis\nhnojivo\nhnout\nhoblina\nhoboj\nhoch\nhodiny\nhodlat\nhodnota\nhodovat\nhojnost\nhokej\nholinka\nholka\nholub\nhomole\nhonitba\nhonorace\nhoral\nhorda\nhorizont\nhorko\nhorlivec\nhormon\nhornina\nhoroskop\nhorstvo\nhospoda\nhostina\nhotovost\nhouba\nhouf\nhoupat\nhouska\nhovor\nhradba\nhranice\nhravost\nhrazda\nhrbolek\nhrdina\nhrdlo\nhrdost\nhrnek\nhrobka\nhromada\nhrot\nhrouda\nhrozen\nhrstka\nhrubost\nhryzat\nhubenost\nhubnout\nhudba\nhukot\nhumr\nhusita\nhustota\nhvozd\nhybnost\nhydrant\nhygiena\nhymna\nhysterik\nidylka\nihned\nikona\niluze\nimunita\ninfekce\ninflace\ninkaso\ninovace\ninspekce\ninternet\ninvalida\ninvestor\ninzerce\nironie\njablko\njachta\njahoda\njakmile\njakost\njalovec\njantar\njarmark\njaro\njasan\njasno\njatka\njavor\njazyk\njedinec\njedle\njednatel\njehlan\njekot\njelen\njelito\njemnost\njenom\njepice\njeseter\njevit\njezdec\njezero\njinak\njindy\njinoch\njiskra\njistota\njitrnice\njizva\njmenovat\njogurt\njurta\nkabaret\nkabel\nkabinet\nkachna\nkadet\nkadidlo\nkahan\nkajak\nkajuta\nkakao\nkaktus\nkalamita\nkalhoty\nkalibr\nkalnost\nkamera\nkamkoliv\nkamna\nkanibal\nkanoe\nkantor\nkapalina\nkapela\nkapitola\nkapka\nkaple\nkapota\nkapr\nkapusta\nkapybara\nkaramel\nkarotka\nkarton\nkasa\nkatalog\nkatedra\nkauce\nkauza\nkavalec\nkazajka\nkazeta\nkazivost\nkdekoliv\nkdesi\nkedluben\nkemp\nkeramika\nkino\nklacek\nkladivo\nklam\nklapot\nklasika\nklaun\nklec\nklenba\nklepat\nklesnout\nklid\nklima\nklisna\nklobouk\nklokan\nklopa\nkloub\nklubovna\nklusat\nkluzkost\nkmen\nkmitat\nkmotr\nkniha\nknot\nkoalice\nkoberec\nkobka\nkobliha\nkobyla\nkocour\nkohout\nkojenec\nkokos\nkoktejl\nkolaps\nkoleda\nkolize\nkolo\nkomando\nkometa\nkomik\nkomnata\nkomora\nkompas\nkomunita\nkonat\nkoncept\nkondice\nkonec\nkonfese\nkongres\nkonina\nkonkurs\nkontakt\nkonzerva\nkopanec\nkopie\nkopnout\nkoprovka\nkorbel\nkorektor\nkormidlo\nkoroptev\nkorpus\nkoruna\nkoryto\nkorzet\nkosatec\nkostka\nkotel\nkotleta\nkotoul\nkoukat\nkoupelna\nkousek\nkouzlo\nkovboj\nkoza\nkozoroh\nkrabice\nkrach\nkrajina\nkralovat\nkrasopis\nkravata\nkredit\nkrejcar\nkresba\nkreveta\nkriket\nkritik\nkrize\nkrkavec\nkrmelec\nkrmivo\nkrocan\nkrok\nkronika\nkropit\nkroupa\nkrovka\nkrtek\nkruhadlo\nkrupice\nkrutost\nkrvinka\nkrychle\nkrypta\nkrystal\nkryt\nkudlanka\nkufr\nkujnost\nkukla\nkulajda\nkulich\nkulka\nkulomet\nkultura\nkuna\nkupodivu\nkurt\nkurzor\nkutil\nkvalita\nkvasinka\nkvestor\nkynolog\nkyselina\nkytara\nkytice\nkytka\nkytovec\nkyvadlo\nlabrador\nlachtan\nladnost\nlaik\nlakomec\nlamela\nlampa\nlanovka\nlasice\nlaso\nlastura\nlatinka\nlavina\nlebka\nleckdy\nleden\nlednice\nledovka\nledvina\nlegenda\nlegie\nlegrace\nlehce\nlehkost\nlehnout\nlektvar\nlenochod\nlentilka\nlepenka\nlepidlo\nletadlo\nletec\nletmo\nletokruh\nlevhart\nlevitace\nlevobok\nlibra\nlichotka\nlidojed\nlidskost\nlihovina\nlijavec\nlilek\nlimetka\nlinie\nlinka\nlinoleum\nlistopad\nlitina\nlitovat\nlobista\nlodivod\nlogika\nlogoped\nlokalita\nloket\nlomcovat\nlopata\nlopuch\nlord\nlosos\nlotr\nloudal\nlouh\nlouka\nlouskat\nlovec\nlstivost\nlucerna\nlucifer\nlump\nlusk\nlustrace\nlvice\nlyra\nlyrika\nlysina\nmadam\nmadlo\nmagistr\nmahagon\nmajetek\nmajitel\nmajorita\nmakak\nmakovice\nmakrela\nmalba\nmalina\nmalovat\nmalvice\nmaminka\nmandle\nmanko\nmarnost\nmasakr\nmaskot\nmasopust\nmatice\nmatrika\nmaturita\nmazanec\nmazivo\nmazlit\nmazurka\nmdloba\nmechanik\nmeditace\nmedovina\nmelasa\nmeloun\nmentolka\nmetla\nmetoda\nmetr\nmezera\nmigrace\nmihnout\nmihule\nmikina\nmikrofon\nmilenec\nmilimetr\nmilost\nmimika\nmincovna\nminibar\nminomet\nminulost\nmiska\nmistr\nmixovat\nmladost\nmlha\nmlhovina\nmlok\nmlsat\nmluvit\nmnich\nmnohem\nmobil\nmocnost\nmodelka\nmodlitba\nmohyla\nmokro\nmolekula\nmomentka\nmonarcha\nmonokl\nmonstrum\nmontovat\nmonzun\nmosaz\nmoskyt\nmost\nmotivace\nmotorka\nmotyka\nmoucha\nmoudrost\nmozaika\nmozek\nmozol\nmramor\nmravenec\nmrkev\nmrtvola\nmrzet\nmrzutost\nmstitel\nmudrc\nmuflon\nmulat\nmumie\nmunice\nmuset\nmutace\nmuzeum\nmuzikant\nmyslivec\nmzda\nnabourat\nnachytat\nnadace\nnadbytek\nnadhoz\nnadobro\nnadpis\nnahlas\nnahnat\nnahodile\nnahradit\nnaivita\nnajednou\nnajisto\nnajmout\nnaklonit\nnakonec\nnakrmit\nnalevo\nnamazat\nnamluvit\nnanometr\nnaoko\nnaopak\nnaostro\nnapadat\nnapevno\nnaplnit\nnapnout\nnaposled\nnaprosto\nnarodit\nnaruby\nnarychlo\nnasadit\nnasekat\nnaslepo\nnastat\nnatolik\nnavenek\nnavrch\nnavzdory\nnazvat\nnebe\nnechat\nnecky\nnedaleko\nnedbat\nneduh\nnegace\nnehet\nnehoda\nnejen\nnejprve\nneklid\nnelibost\nnemilost\nnemoc\nneochota\nneonka\nnepokoj\nnerost\nnerv\nnesmysl\nnesoulad\nnetvor\nneuron\nnevina\nnezvykle\nnicota\nnijak\nnikam\nnikdy\nnikl\nnikterak\nnitro\nnocleh\nnohavice\nnominace\nnora\nnorek\nnositel\nnosnost\nnouze\nnoviny\nnovota\nnozdra\nnuda\nnudle\nnuget\nnutit\nnutnost\nnutrie\nnymfa\nobal\nobarvit\nobava\nobdiv\nobec\nobehnat\nobejmout\nobezita\nobhajoba\nobilnice\nobjasnit\nobjekt\nobklopit\noblast\noblek\nobliba\nobloha\nobluda\nobnos\nobohatit\nobojek\nobout\nobrazec\nobrna\nobruba\nobrys\nobsah\nobsluha\nobstarat\nobuv\nobvaz\nobvinit\nobvod\nobvykle\nobyvatel\nobzor\nocas\nocel\nocenit\nochladit\nochota\nochrana\nocitnout\nodboj\nodbyt\nodchod\nodcizit\nodebrat\nodeslat\nodevzdat\nodezva\nodhadce\nodhodit\nodjet\nodjinud\nodkaz\nodkoupit\nodliv\nodluka\nodmlka\nodolnost\nodpad\nodpis\nodplout\nodpor\nodpustit\nodpykat\nodrazka\nodsoudit\nodstup\nodsun\nodtok\nodtud\nodvaha\nodveta\nodvolat\nodvracet\nodznak\nofina\nofsajd\nohlas\nohnisko\nohrada\nohrozit\nohryzek\nokap\nokenice\noklika\nokno\nokouzlit\nokovy\nokrasa\nokres\nokrsek\nokruh\nokupant\nokurka\nokusit\nolejnina\nolizovat\nomak\nomeleta\nomezit\nomladina\nomlouvat\nomluva\nomyl\nonehdy\nopakovat\nopasek\noperace\nopice\nopilost\nopisovat\nopora\nopozice\nopravdu\noproti\norbital\norchestr\norgie\norlice\norloj\nortel\nosada\noschnout\nosika\nosivo\noslava\noslepit\noslnit\noslovit\nosnova\nosoba\nosolit\nospalec\nosten\nostraha\nostuda\nostych\nosvojit\noteplit\notisk\notop\notrhat\notrlost\notrok\notruby\notvor\novanout\novar\noves\novlivnit\novoce\noxid\nozdoba\npachatel\npacient\npadouch\npahorek\npakt\npalanda\npalec\npalivo\npaluba\npamflet\npamlsek\npanenka\npanika\npanna\npanovat\npanstvo\npantofle\npaprika\nparketa\nparodie\nparta\nparuka\nparyba\npaseka\npasivita\npastelka\npatent\npatrona\npavouk\npazneht\npazourek\npecka\npedagog\npejsek\npeklo\npeloton\npenalta\npendrek\npenze\nperiskop\npero\npestrost\npetarda\npetice\npetrolej\npevnina\npexeso\npianista\npiha\npijavice\npikle\npiknik\npilina\npilnost\npilulka\npinzeta\npipeta\npisatel\npistole\npitevna\npivnice\npivovar\nplacenta\nplakat\nplamen\nplaneta\nplastika\nplatit\nplavidlo\nplaz\nplech\nplemeno\nplenta\nples\npletivo\nplevel\nplivat\nplnit\nplno\nplocha\nplodina\nplomba\nplout\npluk\nplyn\npobavit\npobyt\npochod\npocit\npoctivec\npodat\npodcenit\npodepsat\npodhled\npodivit\npodklad\npodmanit\npodnik\npodoba\npodpora\npodraz\npodstata\npodvod\npodzim\npoezie\npohanka\npohnutka\npohovor\npohroma\npohyb\npointa\npojistka\npojmout\npokazit\npokles\npokoj\npokrok\npokuta\npokyn\npoledne\npolibek\npolknout\npoloha\npolynom\npomalu\npominout\npomlka\npomoc\npomsta\npomyslet\nponechat\nponorka\nponurost\npopadat\npopel\npopisek\npoplach\npoprosit\npopsat\npopud\nporadce\nporce\nporod\nporucha\nporyv\nposadit\nposed\nposila\nposkok\nposlanec\nposoudit\npospolu\npostava\nposudek\nposyp\npotah\npotkan\npotlesk\npotomek\npotrava\npotupa\npotvora\npoukaz\npouto\npouzdro\npovaha\npovidla\npovlak\npovoz\npovrch\npovstat\npovyk\npovzdech\npozdrav\npozemek\npoznatek\npozor\npozvat\npracovat\nprahory\npraktika\nprales\npraotec\npraporek\nprase\npravda\nprincip\nprkno\nprobudit\nprocento\nprodej\nprofese\nprohra\nprojekt\nprolomit\npromile\npronikat\npropad\nprorok\nprosba\nproton\nproutek\nprovaz\nprskavka\nprsten\nprudkost\nprut\nprvek\nprvohory\npsanec\npsovod\npstruh\nptactvo\npuberta\npuch\npudl\npukavec\npuklina\npukrle\npult\npumpa\npunc\npupen\npusa\npusinka\npustina\nputovat\nputyka\npyramida\npysk\npytel\nracek\nrachot\nradiace\nradnice\nradon\nraft\nragby\nraketa\nrakovina\nrameno\nrampouch\nrande\nrarach\nrarita\nrasovna\nrastr\nratolest\nrazance\nrazidlo\nreagovat\nreakce\nrecept\nredaktor\nreferent\nreflex\nrejnok\nreklama\nrekord\nrekrut\nrektor\nreputace\nrevize\nrevma\nrevolver\nrezerva\nriskovat\nriziko\nrobotika\nrodokmen\nrohovka\nrokle\nrokoko\nromaneto\nropovod\nropucha\nrorejs\nrosol\nrostlina\nrotmistr\nrotoped\nrotunda\nroubenka\nroucho\nroup\nroura\nrovina\nrovnice\nrozbor\nrozchod\nrozdat\nrozeznat\nrozhodce\nrozinka\nrozjezd\nrozkaz\nrozloha\nrozmar\nrozpad\nrozruch\nrozsah\nroztok\nrozum\nrozvod\nrubrika\nruchadlo\nrukavice\nrukopis\nryba\nrybolov\nrychlost\nrydlo\nrypadlo\nrytina\nryzost\nsadista\nsahat\nsako\nsamec\nsamizdat\nsamota\nsanitka\nsardinka\nsasanka\nsatelit\nsazba\nsazenice\nsbor\nschovat\nsebranka\nsecese\nsedadlo\nsediment\nsedlo\nsehnat\nsejmout\nsekera\nsekta\nsekunda\nsekvoje\nsemeno\nseno\nservis\nsesadit\nseshora\nseskok\nseslat\nsestra\nsesuv\nsesypat\nsetba\nsetina\nsetkat\nsetnout\nsetrvat\nsever\nseznam\nshoda\nshrnout\nsifon\nsilnice\nsirka\nsirotek\nsirup\nsituace\nskafandr\nskalisko\nskanzen\nskaut\nskeptik\nskica\nskladba\nsklenice\nsklo\nskluz\nskoba\nskokan\nskoro\nskripta\nskrz\nskupina\nskvost\nskvrna\nslabika\nsladidlo\nslanina\nslast\nslavnost\nsledovat\nslepec\nsleva\nslezina\nslib\nslina\nsliznice\nslon\nsloupek\nslovo\nsluch\nsluha\nslunce\nslupka\nslza\nsmaragd\nsmetana\nsmilstvo\nsmlouva\nsmog\nsmrad\nsmrk\nsmrtka\nsmutek\nsmysl\nsnad\nsnaha\nsnob\nsobota\nsocha\nsodovka\nsokol\nsopka\nsotva\nsouboj\nsoucit\nsoudce\nsouhlas\nsoulad\nsoumrak\nsouprava\nsoused\nsoutok\nsouviset\nspalovna\nspasitel\nspis\nsplav\nspodek\nspojenec\nspolu\nsponzor\nspornost\nspousta\nsprcha\nspustit\nsranda\nsraz\nsrdce\nsrna\nsrnec\nsrovnat\nsrpen\nsrst\nsrub\nstanice\nstarosta\nstatika\nstavba\nstehno\nstezka\nstodola\nstolek\nstopa\nstorno\nstoupat\nstrach\nstres\nstrhnout\nstrom\nstruna\nstudna\nstupnice\nstvol\nstyk\nsubjekt\nsubtropy\nsuchar\nsudost\nsukno\nsundat\nsunout\nsurikata\nsurovina\nsvah\nsvalstvo\nsvetr\nsvatba\nsvazek\nsvisle\nsvitek\nsvoboda\nsvodidlo\nsvorka\nsvrab\nsykavka\nsykot\nsynek\nsynovec\nsypat\nsypkost\nsyrovost\nsysel\nsytost\ntabletka\ntabule\ntahoun\ntajemno\ntajfun\ntajga\ntajit\ntajnost\ntaktika\ntamhle\ntampon\ntancovat\ntanec\ntanker\ntapeta\ntavenina\ntazatel\ntechnika\ntehdy\ntekutina\ntelefon\ntemnota\ntendence\ntenista\ntenor\nteplota\ntepna\nteprve\nterapie\ntermoska\ntextil\nticho\ntiskopis\ntitulek\ntkadlec\ntkanina\ntlapka\ntleskat\ntlukot\ntlupa\ntmel\ntoaleta\ntopinka\ntopol\ntorzo\ntouha\ntoulec\ntradice\ntraktor\ntramp\ntrasa\ntraverza\ntrefit\ntrest\ntrezor\ntrhavina\ntrhlina\ntrochu\ntrojice\ntroska\ntrouba\ntrpce\ntrpitel\ntrpkost\ntrubec\ntruchlit\ntruhlice\ntrus\ntrvat\ntudy\ntuhnout\ntuhost\ntundra\nturista\nturnaj\ntuzemsko\ntvaroh\ntvorba\ntvrdost\ntvrz\ntygr\ntykev\nubohost\nuboze\nubrat\nubrousek\nubrus\nubytovna\nucho\nuctivost\nudivit\nuhradit\nujednat\nujistit\nujmout\nukazatel\nuklidnit\nuklonit\nukotvit\nukrojit\nulice\nulita\nulovit\numyvadlo\nunavit\nuniforma\nuniknout\nupadnout\nuplatnit\nuplynout\nupoutat\nupravit\nuran\nurazit\nusednout\nusilovat\nusmrtit\nusnadnit\nusnout\nusoudit\nustlat\nustrnout\nutahovat\nutkat\nutlumit\nutonout\nutopenec\nutrousit\nuvalit\nuvolnit\nuvozovka\nuzdravit\nuzel\nuzenina\nuzlina\nuznat\nvagon\nvalcha\nvaloun\nvana\nvandal\nvanilka\nvaran\nvarhany\nvarovat\nvcelku\nvchod\nvdova\nvedro\nvegetace\nvejce\nvelbloud\nveletrh\nvelitel\nvelmoc\nvelryba\nvenkov\nveranda\nverze\nveselka\nveskrze\nvesnice\nvespodu\nvesta\nveterina\nveverka\nvibrace\nvichr\nvideohra\nvidina\nvidle\nvila\nvinice\nviset\nvitalita\nvize\nvizitka\nvjezd\nvklad\nvkus\nvlajka\nvlak\nvlasec\nvlevo\nvlhkost\nvliv\nvlnovka\nvloupat\nvnucovat\nvnuk\nvoda\nvodivost\nvodoznak\nvodstvo\nvojensky\nvojna\nvojsko\nvolant\nvolba\nvolit\nvolno\nvoskovka\nvozidlo\nvozovna\nvpravo\nvrabec\nvracet\nvrah\nvrata\nvrba\nvrcholek\nvrhat\nvrstva\nvrtule\nvsadit\nvstoupit\nvstup\nvtip\nvybavit\nvybrat\nvychovat\nvydat\nvydra\nvyfotit\nvyhledat\nvyhnout\nvyhodit\nvyhradit\nvyhubit\nvyjasnit\nvyjet\nvyjmout\nvyklopit\nvykonat\nvylekat\nvymazat\nvymezit\nvymizet\nvymyslet\nvynechat\nvynikat\nvynutit\nvypadat\nvyplatit\nvypravit\nvypustit\nvyrazit\nvyrovnat\nvyrvat\nvyslovit\nvysoko\nvystavit\nvysunout\nvysypat\nvytasit\nvytesat\nvytratit\nvyvinout\nvyvolat\nvyvrhel\nvyzdobit\nvyznat\nvzadu\nvzbudit\nvzchopit\nvzdor\nvzduch\nvzdychat\nvzestup\nvzhledem\nvzkaz\nvzlykat\nvznik\nvzorek\nvzpoura\nvztah\nvztek\nxylofon\nzabrat\nzabydlet\nzachovat\nzadarmo\nzadusit\nzafoukat\nzahltit\nzahodit\nzahrada\nzahynout\nzajatec\nzajet\nzajistit\nzaklepat\nzakoupit\nzalepit\nzamezit\nzamotat\nzamyslet\nzanechat\nzanikat\nzaplatit\nzapojit\nzapsat\nzarazit\nzastavit\nzasunout\nzatajit\nzatemnit\nzatknout\nzaujmout\nzavalit\nzavelet\nzavinit\nzavolat\nzavrtat\nzazvonit\nzbavit\nzbrusu\nzbudovat\nzbytek\nzdaleka\nzdarma\nzdatnost\nzdivo\nzdobit\nzdroj\nzdvih\nzdymadlo\nzelenina\nzeman\nzemina\nzeptat\nzezadu\nzezdola\nzhatit\nzhltnout\nzhluboka\nzhotovit\nzhruba\nzima\nzimnice\nzjemnit\nzklamat\nzkoumat\nzkratka\nzkumavka\nzlato\nzlehka\nzloba\nzlom\nzlost\nzlozvyk\nzmapovat\nzmar\nzmatek\nzmije\nzmizet\nzmocnit\nzmodrat\nzmrzlina\nzmutovat\nznak\nznalost\nznamenat\nznovu\nzobrazit\nzotavit\nzoubek\nzoufale\nzplodit\nzpomalit\nzprava\nzprostit\nzprudka\nzprvu\nzrada\nzranit\nzrcadlo\nzrnitost\nzrno\nzrovna\nzrychlit\nzrzavost\nzticha\nztratit\nzubovina\nzubr\nzvednout\nzvenku\nzvesela\nzvon\nzvrat\nzvukovod\nzvyk"); + dico.Add( + "chinese_simplified", + "的\n一\n是\n在\n不\n了\n有\n和\n人\n这\n中\n大\n为\n上\n个\n国\n我\n以\n要\n他\n时\n来\n用\n们\n生\n到\n作\n地\n于\n出\n就\n分\n对\n成\n会\n可\n主\n发\n年\n动\n同\n工\n也\n能\n下\n过\n子\n说\n产\n种\n面\n而\n方\n后\n多\n定\n行\n学\n法\n所\n民\n得\n经\n十\n三\n之\n进\n着\n等\n部\n度\n家\n电\n力\n里\n如\n水\n化\n高\n自\n二\n理\n起\n小\n物\n现\n实\n加\n量\n都\n两\n体\n制\n机\n当\n使\n点\n从\n业\n本\n去\n把\n性\n好\n应\n开\n它\n合\n还\n因\n由\n其\n些\n然\n前\n外\n天\n政\n四\n日\n那\n社\n义\n事\n平\n形\n相\n全\n表\n间\n样\n与\n关\n各\n重\n新\n线\n内\n数\n正\n心\n反\n你\n明\n看\n原\n又\n么\n利\n比\n或\n但\n质\n气\n第\n向\n道\n命\n此\n变\n条\n只\n没\n结\n解\n问\n意\n建\n月\n公\n无\n系\n军\n很\n情\n者\n最\n立\n代\n想\n已\n通\n并\n提\n直\n题\n党\n程\n展\n五\n果\n料\n象\n员\n革\n位\n入\n常\n文\n总\n次\n品\n式\n活\n设\n及\n管\n特\n件\n长\n求\n老\n头\n基\n资\n边\n流\n路\n级\n少\n图\n山\n统\n接\n知\n较\n将\n组\n见\n计\n别\n她\n手\n角\n期\n根\n论\n运\n农\n指\n几\n九\n区\n强\n放\n决\n西\n被\n干\n做\n必\n战\n先\n回\n则\n任\n取\n据\n处\n队\n南\n给\n色\n光\n门\n即\n保\n治\n北\n造\n百\n规\n热\n领\n七\n海\n口\n东\n导\n器\n压\n志\n世\n金\n增\n争\n济\n阶\n油\n思\n术\n极\n交\n受\n联\n什\n认\n六\n共\n权\n收\n证\n改\n清\n美\n再\n采\n转\n更\n单\n风\n切\n打\n白\n教\n速\n花\n带\n安\n场\n身\n车\n例\n真\n务\n具\n万\n每\n目\n至\n达\n走\n积\n示\n议\n声\n报\n斗\n完\n类\n八\n离\n华\n名\n确\n才\n科\n张\n信\n马\n节\n话\n米\n整\n空\n元\n况\n今\n集\n温\n传\n土\n许\n步\n群\n广\n石\n记\n需\n段\n研\n界\n拉\n林\n律\n叫\n且\n究\n观\n越\n织\n装\n影\n算\n低\n持\n音\n众\n书\n布\n复\n容\n儿\n须\n际\n商\n非\n验\n连\n断\n深\n难\n近\n矿\n千\n周\n委\n素\n技\n备\n半\n办\n青\n省\n列\n习\n响\n约\n支\n般\n史\n感\n劳\n便\n团\n往\n酸\n历\n市\n克\n何\n除\n消\n构\n府\n称\n太\n准\n精\n值\n号\n率\n族\n维\n划\n选\n标\n写\n存\n候\n毛\n亲\n快\n效\n斯\n院\n查\n江\n型\n眼\n王\n按\n格\n养\n易\n置\n派\n层\n片\n始\n却\n专\n状\n育\n厂\n京\n识\n适\n属\n圆\n包\n火\n住\n调\n满\n县\n局\n照\n参\n红\n细\n引\n听\n该\n铁\n价\n严\n首\n底\n液\n官\n德\n随\n病\n苏\n失\n尔\n死\n讲\n配\n女\n黄\n推\n显\n谈\n罪\n神\n艺\n呢\n席\n含\n企\n望\n密\n批\n营\n项\n防\n举\n球\n英\n氧\n势\n告\n李\n台\n落\n木\n帮\n轮\n破\n亚\n师\n围\n注\n远\n字\n材\n排\n供\n河\n态\n封\n另\n施\n减\n树\n溶\n怎\n止\n案\n言\n士\n均\n武\n固\n叶\n鱼\n波\n视\n仅\n费\n紧\n爱\n左\n章\n早\n朝\n害\n续\n轻\n服\n试\n食\n充\n兵\n源\n判\n护\n司\n足\n某\n练\n差\n致\n板\n田\n降\n黑\n犯\n负\n击\n范\n继\n兴\n似\n余\n坚\n曲\n输\n修\n故\n城\n夫\n够\n送\n笔\n船\n占\n右\n财\n吃\n富\n春\n职\n觉\n汉\n画\n功\n巴\n跟\n虽\n杂\n飞\n检\n吸\n助\n升\n阳\n互\n初\n创\n抗\n考\n投\n坏\n策\n古\n径\n换\n未\n跑\n留\n钢\n曾\n端\n责\n站\n简\n述\n钱\n副\n尽\n帝\n射\n草\n冲\n承\n独\n令\n限\n阿\n宣\n环\n双\n请\n超\n微\n让\n控\n州\n良\n轴\n找\n否\n纪\n益\n依\n优\n顶\n础\n载\n倒\n房\n突\n坐\n粉\n敌\n略\n客\n袁\n冷\n胜\n绝\n析\n块\n剂\n测\n丝\n协\n诉\n念\n陈\n仍\n罗\n盐\n友\n洋\n错\n苦\n夜\n刑\n移\n频\n逐\n靠\n混\n母\n短\n皮\n终\n聚\n汽\n村\n云\n哪\n既\n距\n卫\n停\n烈\n央\n察\n烧\n迅\n境\n若\n印\n洲\n刻\n括\n激\n孔\n搞\n甚\n室\n待\n核\n校\n散\n侵\n吧\n甲\n游\n久\n菜\n味\n旧\n模\n湖\n货\n损\n预\n阻\n毫\n普\n稳\n乙\n妈\n植\n息\n扩\n银\n语\n挥\n酒\n守\n拿\n序\n纸\n医\n缺\n雨\n吗\n针\n刘\n啊\n急\n唱\n误\n训\n愿\n审\n附\n获\n茶\n鲜\n粮\n斤\n孩\n脱\n硫\n肥\n善\n龙\n演\n父\n渐\n血\n欢\n械\n掌\n歌\n沙\n刚\n攻\n谓\n盾\n讨\n晚\n粒\n乱\n燃\n矛\n乎\n杀\n药\n宁\n鲁\n贵\n钟\n煤\n读\n班\n伯\n香\n介\n迫\n句\n丰\n培\n握\n兰\n担\n弦\n蛋\n沉\n假\n穿\n执\n答\n乐\n谁\n顺\n烟\n缩\n征\n脸\n喜\n松\n脚\n困\n异\n免\n背\n星\n福\n买\n染\n井\n概\n慢\n怕\n磁\n倍\n祖\n皇\n促\n静\n补\n评\n翻\n肉\n践\n尼\n衣\n宽\n扬\n棉\n希\n伤\n操\n垂\n秋\n宜\n氢\n套\n督\n振\n架\n亮\n末\n宪\n庆\n编\n牛\n触\n映\n雷\n销\n诗\n座\n居\n抓\n裂\n胞\n呼\n娘\n景\n威\n绿\n晶\n厚\n盟\n衡\n鸡\n孙\n延\n危\n胶\n屋\n乡\n临\n陆\n顾\n掉\n呀\n灯\n岁\n措\n束\n耐\n剧\n玉\n赵\n跳\n哥\n季\n课\n凯\n胡\n额\n款\n绍\n卷\n齐\n伟\n蒸\n殖\n永\n宗\n苗\n川\n炉\n岩\n弱\n零\n杨\n奏\n沿\n露\n杆\n探\n滑\n镇\n饭\n浓\n航\n怀\n赶\n库\n夺\n伊\n灵\n税\n途\n灭\n赛\n归\n召\n鼓\n播\n盘\n裁\n险\n康\n唯\n录\n菌\n纯\n借\n糖\n盖\n横\n符\n私\n努\n堂\n域\n枪\n润\n幅\n哈\n竟\n熟\n虫\n泽\n脑\n壤\n碳\n欧\n遍\n侧\n寨\n敢\n彻\n虑\n斜\n薄\n庭\n纳\n弹\n饲\n伸\n折\n麦\n湿\n暗\n荷\n瓦\n塞\n床\n筑\n恶\n户\n访\n塔\n奇\n透\n梁\n刀\n旋\n迹\n卡\n氯\n遇\n份\n毒\n泥\n退\n洗\n摆\n灰\n彩\n卖\n耗\n夏\n择\n忙\n铜\n献\n硬\n予\n繁\n圈\n雪\n函\n亦\n抽\n篇\n阵\n阴\n丁\n尺\n追\n堆\n雄\n迎\n泛\n爸\n楼\n避\n谋\n吨\n野\n猪\n旗\n累\n偏\n典\n馆\n索\n秦\n脂\n潮\n爷\n豆\n忽\n托\n惊\n塑\n遗\n愈\n朱\n替\n纤\n粗\n倾\n尚\n痛\n楚\n谢\n奋\n购\n磨\n君\n池\n旁\n碎\n骨\n监\n捕\n弟\n暴\n割\n贯\n殊\n释\n词\n亡\n壁\n顿\n宝\n午\n尘\n闻\n揭\n炮\n残\n冬\n桥\n妇\n警\n综\n招\n吴\n付\n浮\n遭\n徐\n您\n摇\n谷\n赞\n箱\n隔\n订\n男\n吹\n园\n纷\n唐\n败\n宋\n玻\n巨\n耕\n坦\n荣\n闭\n湾\n键\n凡\n驻\n锅\n救\n恩\n剥\n凝\n碱\n齿\n截\n炼\n麻\n纺\n禁\n废\n盛\n版\n缓\n净\n睛\n昌\n婚\n涉\n筒\n嘴\n插\n岸\n朗\n庄\n街\n藏\n姑\n贸\n腐\n奴\n啦\n惯\n乘\n伙\n恢\n匀\n纱\n扎\n辩\n耳\n彪\n臣\n亿\n璃\n抵\n脉\n秀\n萨\n俄\n网\n舞\n店\n喷\n纵\n寸\n汗\n挂\n洪\n贺\n闪\n柬\n爆\n烯\n津\n稻\n墙\n软\n勇\n像\n滚\n厘\n蒙\n芳\n肯\n坡\n柱\n荡\n腿\n仪\n旅\n尾\n轧\n冰\n贡\n登\n黎\n削\n钻\n勒\n逃\n障\n氨\n郭\n峰\n币\n港\n伏\n轨\n亩\n毕\n擦\n莫\n刺\n浪\n秘\n援\n株\n健\n售\n股\n岛\n甘\n泡\n睡\n童\n铸\n汤\n阀\n休\n汇\n舍\n牧\n绕\n炸\n哲\n磷\n绩\n朋\n淡\n尖\n启\n陷\n柴\n呈\n徒\n颜\n泪\n稍\n忘\n泵\n蓝\n拖\n洞\n授\n镜\n辛\n壮\n锋\n贫\n虚\n弯\n摩\n泰\n幼\n廷\n尊\n窗\n纲\n弄\n隶\n疑\n氏\n宫\n姐\n震\n瑞\n怪\n尤\n琴\n循\n描\n膜\n违\n夹\n腰\n缘\n珠\n穷\n森\n枝\n竹\n沟\n催\n绳\n忆\n邦\n剩\n幸\n浆\n栏\n拥\n牙\n贮\n礼\n滤\n钠\n纹\n罢\n拍\n咱\n喊\n袖\n埃\n勤\n罚\n焦\n潜\n伍\n墨\n欲\n缝\n姓\n刊\n饱\n仿\n奖\n铝\n鬼\n丽\n跨\n默\n挖\n链\n扫\n喝\n袋\n炭\n污\n幕\n诸\n弧\n励\n梅\n奶\n洁\n灾\n舟\n鉴\n苯\n讼\n抱\n毁\n懂\n寒\n智\n埔\n寄\n届\n跃\n渡\n挑\n丹\n艰\n贝\n碰\n拔\n爹\n戴\n码\n梦\n芽\n熔\n赤\n渔\n哭\n敬\n颗\n奔\n铅\n仲\n虎\n稀\n妹\n乏\n珍\n申\n桌\n遵\n允\n隆\n螺\n仓\n魏\n锐\n晓\n氮\n兼\n隐\n碍\n赫\n拨\n忠\n肃\n缸\n牵\n抢\n博\n巧\n壳\n兄\n杜\n讯\n诚\n碧\n祥\n柯\n页\n巡\n矩\n悲\n灌\n龄\n伦\n票\n寻\n桂\n铺\n圣\n恐\n恰\n郑\n趣\n抬\n荒\n腾\n贴\n柔\n滴\n猛\n阔\n辆\n妻\n填\n撤\n储\n签\n闹\n扰\n紫\n砂\n递\n戏\n吊\n陶\n伐\n喂\n疗\n瓶\n婆\n抚\n臂\n摸\n忍\n虾\n蜡\n邻\n胸\n巩\n挤\n偶\n弃\n槽\n劲\n乳\n邓\n吉\n仁\n烂\n砖\n租\n乌\n舰\n伴\n瓜\n浅\n丙\n暂\n燥\n橡\n柳\n迷\n暖\n牌\n秧\n胆\n详\n簧\n踏\n瓷\n谱\n呆\n宾\n糊\n洛\n辉\n愤\n竞\n隙\n怒\n粘\n乃\n绪\n肩\n籍\n敏\n涂\n熙\n皆\n侦\n悬\n掘\n享\n纠\n醒\n狂\n锁\n淀\n恨\n牲\n霸\n爬\n赏\n逆\n玩\n陵\n祝\n秒\n浙\n貌\n役\n彼\n悉\n鸭\n趋\n凤\n晨\n畜\n辈\n秩\n卵\n署\n梯\n炎\n滩\n棋\n驱\n筛\n峡\n冒\n啥\n寿\n译\n浸\n泉\n帽\n迟\n硅\n疆\n贷\n漏\n稿\n冠\n嫩\n胁\n芯\n牢\n叛\n蚀\n奥\n鸣\n岭\n羊\n凭\n串\n塘\n绘\n酵\n融\n盆\n锡\n庙\n筹\n冻\n辅\n摄\n袭\n筋\n拒\n僚\n旱\n钾\n鸟\n漆\n沈\n眉\n疏\n添\n棒\n穗\n硝\n韩\n逼\n扭\n侨\n凉\n挺\n碗\n栽\n炒\n杯\n患\n馏\n劝\n豪\n辽\n勃\n鸿\n旦\n吏\n拜\n狗\n埋\n辊\n掩\n饮\n搬\n骂\n辞\n勾\n扣\n估\n蒋\n绒\n雾\n丈\n朵\n姆\n拟\n宇\n辑\n陕\n雕\n偿\n蓄\n崇\n剪\n倡\n厅\n咬\n驶\n薯\n刷\n斥\n番\n赋\n奉\n佛\n浇\n漫\n曼\n扇\n钙\n桃\n扶\n仔\n返\n俗\n亏\n腔\n鞋\n棱\n覆\n框\n悄\n叔\n撞\n骗\n勘\n旺\n沸\n孤\n吐\n孟\n渠\n屈\n疾\n妙\n惜\n仰\n狠\n胀\n谐\n抛\n霉\n桑\n岗\n嘛\n衰\n盗\n渗\n脏\n赖\n涌\n甜\n曹\n阅\n肌\n哩\n厉\n烃\n纬\n毅\n昨\n伪\n症\n煮\n叹\n钉\n搭\n茎\n笼\n酷\n偷\n弓\n锥\n恒\n杰\n坑\n鼻\n翼\n纶\n叙\n狱\n逮\n罐\n络\n棚\n抑\n膨\n蔬\n寺\n骤\n穆\n冶\n枯\n册\n尸\n凸\n绅\n坯\n牺\n焰\n轰\n欣\n晋\n瘦\n御\n锭\n锦\n丧\n旬\n锻\n垄\n搜\n扑\n邀\n亭\n酯\n迈\n舒\n脆\n酶\n闲\n忧\n酚\n顽\n羽\n涨\n卸\n仗\n陪\n辟\n惩\n杭\n姚\n肚\n捉\n飘\n漂\n昆\n欺\n吾\n郎\n烷\n汁\n呵\n饰\n萧\n雅\n邮\n迁\n燕\n撒\n姻\n赴\n宴\n烦\n债\n帐\n斑\n铃\n旨\n醇\n董\n饼\n雏\n姿\n拌\n傅\n腹\n妥\n揉\n贤\n拆\n歪\n葡\n胺\n丢\n浩\n徽\n昂\n垫\n挡\n览\n贪\n慰\n缴\n汪\n慌\n冯\n诺\n姜\n谊\n凶\n劣\n诬\n耀\n昏\n躺\n盈\n骑\n乔\n溪\n丛\n卢\n抹\n闷\n咨\n刮\n驾\n缆\n悟\n摘\n铒\n掷\n颇\n幻\n柄\n惠\n惨\n佳\n仇\n腊\n窝\n涤\n剑\n瞧\n堡\n泼\n葱\n罩\n霍\n捞\n胎\n苍\n滨\n俩\n捅\n湘\n砍\n霞\n邵\n萄\n疯\n淮\n遂\n熊\n粪\n烘\n宿\n档\n戈\n驳\n嫂\n裕\n徙\n箭\n捐\n肠\n撑\n晒\n辨\n殿\n莲\n摊\n搅\n酱\n屏\n疫\n哀\n蔡\n堵\n沫\n皱\n畅\n叠\n阁\n莱\n敲\n辖\n钩\n痕\n坝\n巷\n饿\n祸\n丘\n玄\n溜\n曰\n逻\n彭\n尝\n卿\n妨\n艇\n吞\n韦\n怨\n矮\n歇\n" + ); + dico.Add( + "chinese_traditional", + "的\n一\n是\n在\n不\n了\n有\n和\n人\n這\n中\n大\n為\n上\n個\n國\n我\n以\n要\n他\n時\n來\n用\n們\n生\n到\n作\n地\n於\n出\n就\n分\n對\n成\n會\n可\n主\n發\n年\n動\n同\n工\n也\n能\n下\n過\n子\n說\n產\n種\n面\n而\n方\n後\n多\n定\n行\n學\n法\n所\n民\n得\n經\n十\n三\n之\n進\n著\n等\n部\n度\n家\n電\n力\n裡\n如\n水\n化\n高\n自\n二\n理\n起\n小\n物\n現\n實\n加\n量\n都\n兩\n體\n制\n機\n當\n使\n點\n從\n業\n本\n去\n把\n性\n好\n應\n開\n它\n合\n還\n因\n由\n其\n些\n然\n前\n外\n天\n政\n四\n日\n那\n社\n義\n事\n平\n形\n相\n全\n表\n間\n樣\n與\n關\n各\n重\n新\n線\n內\n數\n正\n心\n反\n你\n明\n看\n原\n又\n麼\n利\n比\n或\n但\n質\n氣\n第\n向\n道\n命\n此\n變\n條\n只\n沒\n結\n解\n問\n意\n建\n月\n公\n無\n系\n軍\n很\n情\n者\n最\n立\n代\n想\n已\n通\n並\n提\n直\n題\n黨\n程\n展\n五\n果\n料\n象\n員\n革\n位\n入\n常\n文\n總\n次\n品\n式\n活\n設\n及\n管\n特\n件\n長\n求\n老\n頭\n基\n資\n邊\n流\n路\n級\n少\n圖\n山\n統\n接\n知\n較\n將\n組\n見\n計\n別\n她\n手\n角\n期\n根\n論\n運\n農\n指\n幾\n九\n區\n強\n放\n決\n西\n被\n幹\n做\n必\n戰\n先\n回\n則\n任\n取\n據\n處\n隊\n南\n給\n色\n光\n門\n即\n保\n治\n北\n造\n百\n規\n熱\n領\n七\n海\n口\n東\n導\n器\n壓\n志\n世\n金\n增\n爭\n濟\n階\n油\n思\n術\n極\n交\n受\n聯\n什\n認\n六\n共\n權\n收\n證\n改\n清\n美\n再\n採\n轉\n更\n單\n風\n切\n打\n白\n教\n速\n花\n帶\n安\n場\n身\n車\n例\n真\n務\n具\n萬\n每\n目\n至\n達\n走\n積\n示\n議\n聲\n報\n鬥\n完\n類\n八\n離\n華\n名\n確\n才\n科\n張\n信\n馬\n節\n話\n米\n整\n空\n元\n況\n今\n集\n溫\n傳\n土\n許\n步\n群\n廣\n石\n記\n需\n段\n研\n界\n拉\n林\n律\n叫\n且\n究\n觀\n越\n織\n裝\n影\n算\n低\n持\n音\n眾\n書\n布\n复\n容\n兒\n須\n際\n商\n非\n驗\n連\n斷\n深\n難\n近\n礦\n千\n週\n委\n素\n技\n備\n半\n辦\n青\n省\n列\n習\n響\n約\n支\n般\n史\n感\n勞\n便\n團\n往\n酸\n歷\n市\n克\n何\n除\n消\n構\n府\n稱\n太\n準\n精\n值\n號\n率\n族\n維\n劃\n選\n標\n寫\n存\n候\n毛\n親\n快\n效\n斯\n院\n查\n江\n型\n眼\n王\n按\n格\n養\n易\n置\n派\n層\n片\n始\n卻\n專\n狀\n育\n廠\n京\n識\n適\n屬\n圓\n包\n火\n住\n調\n滿\n縣\n局\n照\n參\n紅\n細\n引\n聽\n該\n鐵\n價\n嚴\n首\n底\n液\n官\n德\n隨\n病\n蘇\n失\n爾\n死\n講\n配\n女\n黃\n推\n顯\n談\n罪\n神\n藝\n呢\n席\n含\n企\n望\n密\n批\n營\n項\n防\n舉\n球\n英\n氧\n勢\n告\n李\n台\n落\n木\n幫\n輪\n破\n亞\n師\n圍\n注\n遠\n字\n材\n排\n供\n河\n態\n封\n另\n施\n減\n樹\n溶\n怎\n止\n案\n言\n士\n均\n武\n固\n葉\n魚\n波\n視\n僅\n費\n緊\n愛\n左\n章\n早\n朝\n害\n續\n輕\n服\n試\n食\n充\n兵\n源\n判\n護\n司\n足\n某\n練\n差\n致\n板\n田\n降\n黑\n犯\n負\n擊\n范\n繼\n興\n似\n餘\n堅\n曲\n輸\n修\n故\n城\n夫\n夠\n送\n筆\n船\n佔\n右\n財\n吃\n富\n春\n職\n覺\n漢\n畫\n功\n巴\n跟\n雖\n雜\n飛\n檢\n吸\n助\n昇\n陽\n互\n初\n創\n抗\n考\n投\n壞\n策\n古\n徑\n換\n未\n跑\n留\n鋼\n曾\n端\n責\n站\n簡\n述\n錢\n副\n盡\n帝\n射\n草\n衝\n承\n獨\n令\n限\n阿\n宣\n環\n雙\n請\n超\n微\n讓\n控\n州\n良\n軸\n找\n否\n紀\n益\n依\n優\n頂\n礎\n載\n倒\n房\n突\n坐\n粉\n敵\n略\n客\n袁\n冷\n勝\n絕\n析\n塊\n劑\n測\n絲\n協\n訴\n念\n陳\n仍\n羅\n鹽\n友\n洋\n錯\n苦\n夜\n刑\n移\n頻\n逐\n靠\n混\n母\n短\n皮\n終\n聚\n汽\n村\n雲\n哪\n既\n距\n衛\n停\n烈\n央\n察\n燒\n迅\n境\n若\n印\n洲\n刻\n括\n激\n孔\n搞\n甚\n室\n待\n核\n校\n散\n侵\n吧\n甲\n遊\n久\n菜\n味\n舊\n模\n湖\n貨\n損\n預\n阻\n毫\n普\n穩\n乙\n媽\n植\n息\n擴\n銀\n語\n揮\n酒\n守\n拿\n序\n紙\n醫\n缺\n雨\n嗎\n針\n劉\n啊\n急\n唱\n誤\n訓\n願\n審\n附\n獲\n茶\n鮮\n糧\n斤\n孩\n脫\n硫\n肥\n善\n龍\n演\n父\n漸\n血\n歡\n械\n掌\n歌\n沙\n剛\n攻\n謂\n盾\n討\n晚\n粒\n亂\n燃\n矛\n乎\n殺\n藥\n寧\n魯\n貴\n鐘\n煤\n讀\n班\n伯\n香\n介\n迫\n句\n豐\n培\n握\n蘭\n擔\n弦\n蛋\n沉\n假\n穿\n執\n答\n樂\n誰\n順\n煙\n縮\n徵\n臉\n喜\n松\n腳\n困\n異\n免\n背\n星\n福\n買\n染\n井\n概\n慢\n怕\n磁\n倍\n祖\n皇\n促\n靜\n補\n評\n翻\n肉\n踐\n尼\n衣\n寬\n揚\n棉\n希\n傷\n操\n垂\n秋\n宜\n氫\n套\n督\n振\n架\n亮\n末\n憲\n慶\n編\n牛\n觸\n映\n雷\n銷\n詩\n座\n居\n抓\n裂\n胞\n呼\n娘\n景\n威\n綠\n晶\n厚\n盟\n衡\n雞\n孫\n延\n危\n膠\n屋\n鄉\n臨\n陸\n顧\n掉\n呀\n燈\n歲\n措\n束\n耐\n劇\n玉\n趙\n跳\n哥\n季\n課\n凱\n胡\n額\n款\n紹\n卷\n齊\n偉\n蒸\n殖\n永\n宗\n苗\n川\n爐\n岩\n弱\n零\n楊\n奏\n沿\n露\n桿\n探\n滑\n鎮\n飯\n濃\n航\n懷\n趕\n庫\n奪\n伊\n靈\n稅\n途\n滅\n賽\n歸\n召\n鼓\n播\n盤\n裁\n險\n康\n唯\n錄\n菌\n純\n借\n糖\n蓋\n橫\n符\n私\n努\n堂\n域\n槍\n潤\n幅\n哈\n竟\n熟\n蟲\n澤\n腦\n壤\n碳\n歐\n遍\n側\n寨\n敢\n徹\n慮\n斜\n薄\n庭\n納\n彈\n飼\n伸\n折\n麥\n濕\n暗\n荷\n瓦\n塞\n床\n築\n惡\n戶\n訪\n塔\n奇\n透\n梁\n刀\n旋\n跡\n卡\n氯\n遇\n份\n毒\n泥\n退\n洗\n擺\n灰\n彩\n賣\n耗\n夏\n擇\n忙\n銅\n獻\n硬\n予\n繁\n圈\n雪\n函\n亦\n抽\n篇\n陣\n陰\n丁\n尺\n追\n堆\n雄\n迎\n泛\n爸\n樓\n避\n謀\n噸\n野\n豬\n旗\n累\n偏\n典\n館\n索\n秦\n脂\n潮\n爺\n豆\n忽\n托\n驚\n塑\n遺\n愈\n朱\n替\n纖\n粗\n傾\n尚\n痛\n楚\n謝\n奮\n購\n磨\n君\n池\n旁\n碎\n骨\n監\n捕\n弟\n暴\n割\n貫\n殊\n釋\n詞\n亡\n壁\n頓\n寶\n午\n塵\n聞\n揭\n炮\n殘\n冬\n橋\n婦\n警\n綜\n招\n吳\n付\n浮\n遭\n徐\n您\n搖\n谷\n贊\n箱\n隔\n訂\n男\n吹\n園\n紛\n唐\n敗\n宋\n玻\n巨\n耕\n坦\n榮\n閉\n灣\n鍵\n凡\n駐\n鍋\n救\n恩\n剝\n凝\n鹼\n齒\n截\n煉\n麻\n紡\n禁\n廢\n盛\n版\n緩\n淨\n睛\n昌\n婚\n涉\n筒\n嘴\n插\n岸\n朗\n莊\n街\n藏\n姑\n貿\n腐\n奴\n啦\n慣\n乘\n夥\n恢\n勻\n紗\n扎\n辯\n耳\n彪\n臣\n億\n璃\n抵\n脈\n秀\n薩\n俄\n網\n舞\n店\n噴\n縱\n寸\n汗\n掛\n洪\n賀\n閃\n柬\n爆\n烯\n津\n稻\n牆\n軟\n勇\n像\n滾\n厘\n蒙\n芳\n肯\n坡\n柱\n盪\n腿\n儀\n旅\n尾\n軋\n冰\n貢\n登\n黎\n削\n鑽\n勒\n逃\n障\n氨\n郭\n峰\n幣\n港\n伏\n軌\n畝\n畢\n擦\n莫\n刺\n浪\n秘\n援\n株\n健\n售\n股\n島\n甘\n泡\n睡\n童\n鑄\n湯\n閥\n休\n匯\n舍\n牧\n繞\n炸\n哲\n磷\n績\n朋\n淡\n尖\n啟\n陷\n柴\n呈\n徒\n顏\n淚\n稍\n忘\n泵\n藍\n拖\n洞\n授\n鏡\n辛\n壯\n鋒\n貧\n虛\n彎\n摩\n泰\n幼\n廷\n尊\n窗\n綱\n弄\n隸\n疑\n氏\n宮\n姐\n震\n瑞\n怪\n尤\n琴\n循\n描\n膜\n違\n夾\n腰\n緣\n珠\n窮\n森\n枝\n竹\n溝\n催\n繩\n憶\n邦\n剩\n幸\n漿\n欄\n擁\n牙\n貯\n禮\n濾\n鈉\n紋\n罷\n拍\n咱\n喊\n袖\n埃\n勤\n罰\n焦\n潛\n伍\n墨\n欲\n縫\n姓\n刊\n飽\n仿\n獎\n鋁\n鬼\n麗\n跨\n默\n挖\n鏈\n掃\n喝\n袋\n炭\n污\n幕\n諸\n弧\n勵\n梅\n奶\n潔\n災\n舟\n鑑\n苯\n訟\n抱\n毀\n懂\n寒\n智\n埔\n寄\n屆\n躍\n渡\n挑\n丹\n艱\n貝\n碰\n拔\n爹\n戴\n碼\n夢\n芽\n熔\n赤\n漁\n哭\n敬\n顆\n奔\n鉛\n仲\n虎\n稀\n妹\n乏\n珍\n申\n桌\n遵\n允\n隆\n螺\n倉\n魏\n銳\n曉\n氮\n兼\n隱\n礙\n赫\n撥\n忠\n肅\n缸\n牽\n搶\n博\n巧\n殼\n兄\n杜\n訊\n誠\n碧\n祥\n柯\n頁\n巡\n矩\n悲\n灌\n齡\n倫\n票\n尋\n桂\n鋪\n聖\n恐\n恰\n鄭\n趣\n抬\n荒\n騰\n貼\n柔\n滴\n猛\n闊\n輛\n妻\n填\n撤\n儲\n簽\n鬧\n擾\n紫\n砂\n遞\n戲\n吊\n陶\n伐\n餵\n療\n瓶\n婆\n撫\n臂\n摸\n忍\n蝦\n蠟\n鄰\n胸\n鞏\n擠\n偶\n棄\n槽\n勁\n乳\n鄧\n吉\n仁\n爛\n磚\n租\n烏\n艦\n伴\n瓜\n淺\n丙\n暫\n燥\n橡\n柳\n迷\n暖\n牌\n秧\n膽\n詳\n簧\n踏\n瓷\n譜\n呆\n賓\n糊\n洛\n輝\n憤\n競\n隙\n怒\n粘\n乃\n緒\n肩\n籍\n敏\n塗\n熙\n皆\n偵\n懸\n掘\n享\n糾\n醒\n狂\n鎖\n淀\n恨\n牲\n霸\n爬\n賞\n逆\n玩\n陵\n祝\n秒\n浙\n貌\n役\n彼\n悉\n鴨\n趨\n鳳\n晨\n畜\n輩\n秩\n卵\n署\n梯\n炎\n灘\n棋\n驅\n篩\n峽\n冒\n啥\n壽\n譯\n浸\n泉\n帽\n遲\n矽\n疆\n貸\n漏\n稿\n冠\n嫩\n脅\n芯\n牢\n叛\n蝕\n奧\n鳴\n嶺\n羊\n憑\n串\n塘\n繪\n酵\n融\n盆\n錫\n廟\n籌\n凍\n輔\n攝\n襲\n筋\n拒\n僚\n旱\n鉀\n鳥\n漆\n沈\n眉\n疏\n添\n棒\n穗\n硝\n韓\n逼\n扭\n僑\n涼\n挺\n碗\n栽\n炒\n杯\n患\n餾\n勸\n豪\n遼\n勃\n鴻\n旦\n吏\n拜\n狗\n埋\n輥\n掩\n飲\n搬\n罵\n辭\n勾\n扣\n估\n蔣\n絨\n霧\n丈\n朵\n姆\n擬\n宇\n輯\n陝\n雕\n償\n蓄\n崇\n剪\n倡\n廳\n咬\n駛\n薯\n刷\n斥\n番\n賦\n奉\n佛\n澆\n漫\n曼\n扇\n鈣\n桃\n扶\n仔\n返\n俗\n虧\n腔\n鞋\n棱\n覆\n框\n悄\n叔\n撞\n騙\n勘\n旺\n沸\n孤\n吐\n孟\n渠\n屈\n疾\n妙\n惜\n仰\n狠\n脹\n諧\n拋\n黴\n桑\n崗\n嘛\n衰\n盜\n滲\n臟\n賴\n湧\n甜\n曹\n閱\n肌\n哩\n厲\n烴\n緯\n毅\n昨\n偽\n症\n煮\n嘆\n釘\n搭\n莖\n籠\n酷\n偷\n弓\n錐\n恆\n傑\n坑\n鼻\n翼\n綸\n敘\n獄\n逮\n罐\n絡\n棚\n抑\n膨\n蔬\n寺\n驟\n穆\n冶\n枯\n冊\n屍\n凸\n紳\n坯\n犧\n焰\n轟\n欣\n晉\n瘦\n禦\n錠\n錦\n喪\n旬\n鍛\n壟\n搜\n撲\n邀\n亭\n酯\n邁\n舒\n脆\n酶\n閒\n憂\n酚\n頑\n羽\n漲\n卸\n仗\n陪\n闢\n懲\n杭\n姚\n肚\n捉\n飄\n漂\n昆\n欺\n吾\n郎\n烷\n汁\n呵\n飾\n蕭\n雅\n郵\n遷\n燕\n撒\n姻\n赴\n宴\n煩\n債\n帳\n斑\n鈴\n旨\n醇\n董\n餅\n雛\n姿\n拌\n傅\n腹\n妥\n揉\n賢\n拆\n歪\n葡\n胺\n丟\n浩\n徽\n昂\n墊\n擋\n覽\n貪\n慰\n繳\n汪\n慌\n馮\n諾\n姜\n誼\n兇\n劣\n誣\n耀\n昏\n躺\n盈\n騎\n喬\n溪\n叢\n盧\n抹\n悶\n諮\n刮\n駕\n纜\n悟\n摘\n鉺\n擲\n頗\n幻\n柄\n惠\n慘\n佳\n仇\n臘\n窩\n滌\n劍\n瞧\n堡\n潑\n蔥\n罩\n霍\n撈\n胎\n蒼\n濱\n倆\n捅\n湘\n砍\n霞\n邵\n萄\n瘋\n淮\n遂\n熊\n糞\n烘\n宿\n檔\n戈\n駁\n嫂\n裕\n徙\n箭\n捐\n腸\n撐\n曬\n辨\n殿\n蓮\n攤\n攪\n醬\n屏\n疫\n哀\n蔡\n堵\n沫\n皺\n暢\n疊\n閣\n萊\n敲\n轄\n鉤\n痕\n壩\n巷\n餓\n禍\n丘\n玄\n溜\n曰\n邏\n彭\n嘗\n卿\n妨\n艇\n吞\n韋\n怨\n矮\n歇\n" + ); + dico.Add( + "english", + "abandon\nability\nable\nabout\nabove\nabsent\nabsorb\nabstract\nabsurd\nabuse\naccess\naccident\naccount\naccuse\nachieve\nacid\nacoustic\nacquire\nacross\nact\naction\nactor\nactress\nactual\nadapt\nadd\naddict\naddress\nadjust\nadmit\nadult\nadvance\nadvice\naerobic\naffair\nafford\nafraid\nagain\nage\nagent\nagree\nahead\naim\nair\nairport\naisle\nalarm\nalbum\nalcohol\nalert\nalien\nall\nalley\nallow\nalmost\nalone\nalpha\nalready\nalso\nalter\nalways\namateur\namazing\namong\namount\namused\nanalyst\nanchor\nancient\nanger\nangle\nangry\nanimal\nankle\nannounce\nannual\nanother\nanswer\nantenna\nantique\nanxiety\nany\napart\napology\nappear\napple\napprove\napril\narch\narctic\narea\narena\nargue\narm\narmed\narmor\narmy\naround\narrange\narrest\narrive\narrow\nart\nartefact\nartist\nartwork\nask\naspect\nassault\nasset\nassist\nassume\nasthma\nathlete\natom\nattack\nattend\nattitude\nattract\nauction\naudit\naugust\naunt\nauthor\nauto\nautumn\naverage\navocado\navoid\nawake\naware\naway\nawesome\nawful\nawkward\naxis\nbaby\nbachelor\nbacon\nbadge\nbag\nbalance\nbalcony\nball\nbamboo\nbanana\nbanner\nbar\nbarely\nbargain\nbarrel\nbase\nbasic\nbasket\nbattle\nbeach\nbean\nbeauty\nbecause\nbecome\nbeef\nbefore\nbegin\nbehave\nbehind\nbelieve\nbelow\nbelt\nbench\nbenefit\nbest\nbetray\nbetter\nbetween\nbeyond\nbicycle\nbid\nbike\nbind\nbiology\nbird\nbirth\nbitter\nblack\nblade\nblame\nblanket\nblast\nbleak\nbless\nblind\nblood\nblossom\nblouse\nblue\nblur\nblush\nboard\nboat\nbody\nboil\nbomb\nbone\nbonus\nbook\nboost\nborder\nboring\nborrow\nboss\nbottom\nbounce\nbox\nboy\nbracket\nbrain\nbrand\nbrass\nbrave\nbread\nbreeze\nbrick\nbridge\nbrief\nbright\nbring\nbrisk\nbroccoli\nbroken\nbronze\nbroom\nbrother\nbrown\nbrush\nbubble\nbuddy\nbudget\nbuffalo\nbuild\nbulb\nbulk\nbullet\nbundle\nbunker\nburden\nburger\nburst\nbus\nbusiness\nbusy\nbutter\nbuyer\nbuzz\ncabbage\ncabin\ncable\ncactus\ncage\ncake\ncall\ncalm\ncamera\ncamp\ncan\ncanal\ncancel\ncandy\ncannon\ncanoe\ncanvas\ncanyon\ncapable\ncapital\ncaptain\ncar\ncarbon\ncard\ncargo\ncarpet\ncarry\ncart\ncase\ncash\ncasino\ncastle\ncasual\ncat\ncatalog\ncatch\ncategory\ncattle\ncaught\ncause\ncaution\ncave\nceiling\ncelery\ncement\ncensus\ncentury\ncereal\ncertain\nchair\nchalk\nchampion\nchange\nchaos\nchapter\ncharge\nchase\nchat\ncheap\ncheck\ncheese\nchef\ncherry\nchest\nchicken\nchief\nchild\nchimney\nchoice\nchoose\nchronic\nchuckle\nchunk\nchurn\ncigar\ncinnamon\ncircle\ncitizen\ncity\ncivil\nclaim\nclap\nclarify\nclaw\nclay\nclean\nclerk\nclever\nclick\nclient\ncliff\nclimb\nclinic\nclip\nclock\nclog\nclose\ncloth\ncloud\nclown\nclub\nclump\ncluster\nclutch\ncoach\ncoast\ncoconut\ncode\ncoffee\ncoil\ncoin\ncollect\ncolor\ncolumn\ncombine\ncome\ncomfort\ncomic\ncommon\ncompany\nconcert\nconduct\nconfirm\ncongress\nconnect\nconsider\ncontrol\nconvince\ncook\ncool\ncopper\ncopy\ncoral\ncore\ncorn\ncorrect\ncost\ncotton\ncouch\ncountry\ncouple\ncourse\ncousin\ncover\ncoyote\ncrack\ncradle\ncraft\ncram\ncrane\ncrash\ncrater\ncrawl\ncrazy\ncream\ncredit\ncreek\ncrew\ncricket\ncrime\ncrisp\ncritic\ncrop\ncross\ncrouch\ncrowd\ncrucial\ncruel\ncruise\ncrumble\ncrunch\ncrush\ncry\ncrystal\ncube\nculture\ncup\ncupboard\ncurious\ncurrent\ncurtain\ncurve\ncushion\ncustom\ncute\ncycle\ndad\ndamage\ndamp\ndance\ndanger\ndaring\ndash\ndaughter\ndawn\nday\ndeal\ndebate\ndebris\ndecade\ndecember\ndecide\ndecline\ndecorate\ndecrease\ndeer\ndefense\ndefine\ndefy\ndegree\ndelay\ndeliver\ndemand\ndemise\ndenial\ndentist\ndeny\ndepart\ndepend\ndeposit\ndepth\ndeputy\nderive\ndescribe\ndesert\ndesign\ndesk\ndespair\ndestroy\ndetail\ndetect\ndevelop\ndevice\ndevote\ndiagram\ndial\ndiamond\ndiary\ndice\ndiesel\ndiet\ndiffer\ndigital\ndignity\ndilemma\ndinner\ndinosaur\ndirect\ndirt\ndisagree\ndiscover\ndisease\ndish\ndismiss\ndisorder\ndisplay\ndistance\ndivert\ndivide\ndivorce\ndizzy\ndoctor\ndocument\ndog\ndoll\ndolphin\ndomain\ndonate\ndonkey\ndonor\ndoor\ndose\ndouble\ndove\ndraft\ndragon\ndrama\ndrastic\ndraw\ndream\ndress\ndrift\ndrill\ndrink\ndrip\ndrive\ndrop\ndrum\ndry\nduck\ndumb\ndune\nduring\ndust\ndutch\nduty\ndwarf\ndynamic\neager\neagle\nearly\nearn\nearth\neasily\neast\neasy\necho\necology\neconomy\nedge\nedit\neducate\neffort\negg\neight\neither\nelbow\nelder\nelectric\nelegant\nelement\nelephant\nelevator\nelite\nelse\nembark\nembody\nembrace\nemerge\nemotion\nemploy\nempower\nempty\nenable\nenact\nend\nendless\nendorse\nenemy\nenergy\nenforce\nengage\nengine\nenhance\nenjoy\nenlist\nenough\nenrich\nenroll\nensure\nenter\nentire\nentry\nenvelope\nepisode\nequal\nequip\nera\nerase\nerode\nerosion\nerror\nerupt\nescape\nessay\nessence\nestate\neternal\nethics\nevidence\nevil\nevoke\nevolve\nexact\nexample\nexcess\nexchange\nexcite\nexclude\nexcuse\nexecute\nexercise\nexhaust\nexhibit\nexile\nexist\nexit\nexotic\nexpand\nexpect\nexpire\nexplain\nexpose\nexpress\nextend\nextra\neye\neyebrow\nfabric\nface\nfaculty\nfade\nfaint\nfaith\nfall\nfalse\nfame\nfamily\nfamous\nfan\nfancy\nfantasy\nfarm\nfashion\nfat\nfatal\nfather\nfatigue\nfault\nfavorite\nfeature\nfebruary\nfederal\nfee\nfeed\nfeel\nfemale\nfence\nfestival\nfetch\nfever\nfew\nfiber\nfiction\nfield\nfigure\nfile\nfilm\nfilter\nfinal\nfind\nfine\nfinger\nfinish\nfire\nfirm\nfirst\nfiscal\nfish\nfit\nfitness\nfix\nflag\nflame\nflash\nflat\nflavor\nflee\nflight\nflip\nfloat\nflock\nfloor\nflower\nfluid\nflush\nfly\nfoam\nfocus\nfog\nfoil\nfold\nfollow\nfood\nfoot\nforce\nforest\nforget\nfork\nfortune\nforum\nforward\nfossil\nfoster\nfound\nfox\nfragile\nframe\nfrequent\nfresh\nfriend\nfringe\nfrog\nfront\nfrost\nfrown\nfrozen\nfruit\nfuel\nfun\nfunny\nfurnace\nfury\nfuture\ngadget\ngain\ngalaxy\ngallery\ngame\ngap\ngarage\ngarbage\ngarden\ngarlic\ngarment\ngas\ngasp\ngate\ngather\ngauge\ngaze\ngeneral\ngenius\ngenre\ngentle\ngenuine\ngesture\nghost\ngiant\ngift\ngiggle\nginger\ngiraffe\ngirl\ngive\nglad\nglance\nglare\nglass\nglide\nglimpse\nglobe\ngloom\nglory\nglove\nglow\nglue\ngoat\ngoddess\ngold\ngood\ngoose\ngorilla\ngospel\ngossip\ngovern\ngown\ngrab\ngrace\ngrain\ngrant\ngrape\ngrass\ngravity\ngreat\ngreen\ngrid\ngrief\ngrit\ngrocery\ngroup\ngrow\ngrunt\nguard\nguess\nguide\nguilt\nguitar\ngun\ngym\nhabit\nhair\nhalf\nhammer\nhamster\nhand\nhappy\nharbor\nhard\nharsh\nharvest\nhat\nhave\nhawk\nhazard\nhead\nhealth\nheart\nheavy\nhedgehog\nheight\nhello\nhelmet\nhelp\nhen\nhero\nhidden\nhigh\nhill\nhint\nhip\nhire\nhistory\nhobby\nhockey\nhold\nhole\nholiday\nhollow\nhome\nhoney\nhood\nhope\nhorn\nhorror\nhorse\nhospital\nhost\nhotel\nhour\nhover\nhub\nhuge\nhuman\nhumble\nhumor\nhundred\nhungry\nhunt\nhurdle\nhurry\nhurt\nhusband\nhybrid\nice\nicon\nidea\nidentify\nidle\nignore\nill\nillegal\nillness\nimage\nimitate\nimmense\nimmune\nimpact\nimpose\nimprove\nimpulse\ninch\ninclude\nincome\nincrease\nindex\nindicate\nindoor\nindustry\ninfant\ninflict\ninform\ninhale\ninherit\ninitial\ninject\ninjury\ninmate\ninner\ninnocent\ninput\ninquiry\ninsane\ninsect\ninside\ninspire\ninstall\nintact\ninterest\ninto\ninvest\ninvite\ninvolve\niron\nisland\nisolate\nissue\nitem\nivory\njacket\njaguar\njar\njazz\njealous\njeans\njelly\njewel\njob\njoin\njoke\njourney\njoy\njudge\njuice\njump\njungle\njunior\njunk\njust\nkangaroo\nkeen\nkeep\nketchup\nkey\nkick\nkid\nkidney\nkind\nkingdom\nkiss\nkit\nkitchen\nkite\nkitten\nkiwi\nknee\nknife\nknock\nknow\nlab\nlabel\nlabor\nladder\nlady\nlake\nlamp\nlanguage\nlaptop\nlarge\nlater\nlatin\nlaugh\nlaundry\nlava\nlaw\nlawn\nlawsuit\nlayer\nlazy\nleader\nleaf\nlearn\nleave\nlecture\nleft\nleg\nlegal\nlegend\nleisure\nlemon\nlend\nlength\nlens\nleopard\nlesson\nletter\nlevel\nliar\nliberty\nlibrary\nlicense\nlife\nlift\nlight\nlike\nlimb\nlimit\nlink\nlion\nliquid\nlist\nlittle\nlive\nlizard\nload\nloan\nlobster\nlocal\nlock\nlogic\nlonely\nlong\nloop\nlottery\nloud\nlounge\nlove\nloyal\nlucky\nluggage\nlumber\nlunar\nlunch\nluxury\nlyrics\nmachine\nmad\nmagic\nmagnet\nmaid\nmail\nmain\nmajor\nmake\nmammal\nman\nmanage\nmandate\nmango\nmansion\nmanual\nmaple\nmarble\nmarch\nmargin\nmarine\nmarket\nmarriage\nmask\nmass\nmaster\nmatch\nmaterial\nmath\nmatrix\nmatter\nmaximum\nmaze\nmeadow\nmean\nmeasure\nmeat\nmechanic\nmedal\nmedia\nmelody\nmelt\nmember\nmemory\nmention\nmenu\nmercy\nmerge\nmerit\nmerry\nmesh\nmessage\nmetal\nmethod\nmiddle\nmidnight\nmilk\nmillion\nmimic\nmind\nminimum\nminor\nminute\nmiracle\nmirror\nmisery\nmiss\nmistake\nmix\nmixed\nmixture\nmobile\nmodel\nmodify\nmom\nmoment\nmonitor\nmonkey\nmonster\nmonth\nmoon\nmoral\nmore\nmorning\nmosquito\nmother\nmotion\nmotor\nmountain\nmouse\nmove\nmovie\nmuch\nmuffin\nmule\nmultiply\nmuscle\nmuseum\nmushroom\nmusic\nmust\nmutual\nmyself\nmystery\nmyth\nnaive\nname\nnapkin\nnarrow\nnasty\nnation\nnature\nnear\nneck\nneed\nnegative\nneglect\nneither\nnephew\nnerve\nnest\nnet\nnetwork\nneutral\nnever\nnews\nnext\nnice\nnight\nnoble\nnoise\nnominee\nnoodle\nnormal\nnorth\nnose\nnotable\nnote\nnothing\nnotice\nnovel\nnow\nnuclear\nnumber\nnurse\nnut\noak\nobey\nobject\noblige\nobscure\nobserve\nobtain\nobvious\noccur\nocean\noctober\nodor\noff\noffer\noffice\noften\noil\nokay\nold\nolive\nolympic\nomit\nonce\none\nonion\nonline\nonly\nopen\nopera\nopinion\noppose\noption\norange\norbit\norchard\norder\nordinary\norgan\norient\noriginal\norphan\nostrich\nother\noutdoor\nouter\noutput\noutside\noval\noven\nover\nown\nowner\noxygen\noyster\nozone\npact\npaddle\npage\npair\npalace\npalm\npanda\npanel\npanic\npanther\npaper\nparade\nparent\npark\nparrot\nparty\npass\npatch\npath\npatient\npatrol\npattern\npause\npave\npayment\npeace\npeanut\npear\npeasant\npelican\npen\npenalty\npencil\npeople\npepper\nperfect\npermit\nperson\npet\nphone\nphoto\nphrase\nphysical\npiano\npicnic\npicture\npiece\npig\npigeon\npill\npilot\npink\npioneer\npipe\npistol\npitch\npizza\nplace\nplanet\nplastic\nplate\nplay\nplease\npledge\npluck\nplug\nplunge\npoem\npoet\npoint\npolar\npole\npolice\npond\npony\npool\npopular\nportion\nposition\npossible\npost\npotato\npottery\npoverty\npowder\npower\npractice\npraise\npredict\nprefer\nprepare\npresent\npretty\nprevent\nprice\npride\nprimary\nprint\npriority\nprison\nprivate\nprize\nproblem\nprocess\nproduce\nprofit\nprogram\nproject\npromote\nproof\nproperty\nprosper\nprotect\nproud\nprovide\npublic\npudding\npull\npulp\npulse\npumpkin\npunch\npupil\npuppy\npurchase\npurity\npurpose\npurse\npush\nput\npuzzle\npyramid\nquality\nquantum\nquarter\nquestion\nquick\nquit\nquiz\nquote\nrabbit\nraccoon\nrace\nrack\nradar\nradio\nrail\nrain\nraise\nrally\nramp\nranch\nrandom\nrange\nrapid\nrare\nrate\nrather\nraven\nraw\nrazor\nready\nreal\nreason\nrebel\nrebuild\nrecall\nreceive\nrecipe\nrecord\nrecycle\nreduce\nreflect\nreform\nrefuse\nregion\nregret\nregular\nreject\nrelax\nrelease\nrelief\nrely\nremain\nremember\nremind\nremove\nrender\nrenew\nrent\nreopen\nrepair\nrepeat\nreplace\nreport\nrequire\nrescue\nresemble\nresist\nresource\nresponse\nresult\nretire\nretreat\nreturn\nreunion\nreveal\nreview\nreward\nrhythm\nrib\nribbon\nrice\nrich\nride\nridge\nrifle\nright\nrigid\nring\nriot\nripple\nrisk\nritual\nrival\nriver\nroad\nroast\nrobot\nrobust\nrocket\nromance\nroof\nrookie\nroom\nrose\nrotate\nrough\nround\nroute\nroyal\nrubber\nrude\nrug\nrule\nrun\nrunway\nrural\nsad\nsaddle\nsadness\nsafe\nsail\nsalad\nsalmon\nsalon\nsalt\nsalute\nsame\nsample\nsand\nsatisfy\nsatoshi\nsauce\nsausage\nsave\nsay\nscale\nscan\nscare\nscatter\nscene\nscheme\nschool\nscience\nscissors\nscorpion\nscout\nscrap\nscreen\nscript\nscrub\nsea\nsearch\nseason\nseat\nsecond\nsecret\nsection\nsecurity\nseed\nseek\nsegment\nselect\nsell\nseminar\nsenior\nsense\nsentence\nseries\nservice\nsession\nsettle\nsetup\nseven\nshadow\nshaft\nshallow\nshare\nshed\nshell\nsheriff\nshield\nshift\nshine\nship\nshiver\nshock\nshoe\nshoot\nshop\nshort\nshoulder\nshove\nshrimp\nshrug\nshuffle\nshy\nsibling\nsick\nside\nsiege\nsight\nsign\nsilent\nsilk\nsilly\nsilver\nsimilar\nsimple\nsince\nsing\nsiren\nsister\nsituate\nsix\nsize\nskate\nsketch\nski\nskill\nskin\nskirt\nskull\nslab\nslam\nsleep\nslender\nslice\nslide\nslight\nslim\nslogan\nslot\nslow\nslush\nsmall\nsmart\nsmile\nsmoke\nsmooth\nsnack\nsnake\nsnap\nsniff\nsnow\nsoap\nsoccer\nsocial\nsock\nsoda\nsoft\nsolar\nsoldier\nsolid\nsolution\nsolve\nsomeone\nsong\nsoon\nsorry\nsort\nsoul\nsound\nsoup\nsource\nsouth\nspace\nspare\nspatial\nspawn\nspeak\nspecial\nspeed\nspell\nspend\nsphere\nspice\nspider\nspike\nspin\nspirit\nsplit\nspoil\nsponsor\nspoon\nsport\nspot\nspray\nspread\nspring\nspy\nsquare\nsqueeze\nsquirrel\nstable\nstadium\nstaff\nstage\nstairs\nstamp\nstand\nstart\nstate\nstay\nsteak\nsteel\nstem\nstep\nstereo\nstick\nstill\nsting\nstock\nstomach\nstone\nstool\nstory\nstove\nstrategy\nstreet\nstrike\nstrong\nstruggle\nstudent\nstuff\nstumble\nstyle\nsubject\nsubmit\nsubway\nsuccess\nsuch\nsudden\nsuffer\nsugar\nsuggest\nsuit\nsummer\nsun\nsunny\nsunset\nsuper\nsupply\nsupreme\nsure\nsurface\nsurge\nsurprise\nsurround\nsurvey\nsuspect\nsustain\nswallow\nswamp\nswap\nswarm\nswear\nsweet\nswift\nswim\nswing\nswitch\nsword\nsymbol\nsymptom\nsyrup\nsystem\ntable\ntackle\ntag\ntail\ntalent\ntalk\ntank\ntape\ntarget\ntask\ntaste\ntattoo\ntaxi\nteach\nteam\ntell\nten\ntenant\ntennis\ntent\nterm\ntest\ntext\nthank\nthat\ntheme\nthen\ntheory\nthere\nthey\nthing\nthis\nthought\nthree\nthrive\nthrow\nthumb\nthunder\nticket\ntide\ntiger\ntilt\ntimber\ntime\ntiny\ntip\ntired\ntissue\ntitle\ntoast\ntobacco\ntoday\ntoddler\ntoe\ntogether\ntoilet\ntoken\ntomato\ntomorrow\ntone\ntongue\ntonight\ntool\ntooth\ntop\ntopic\ntopple\ntorch\ntornado\ntortoise\ntoss\ntotal\ntourist\ntoward\ntower\ntown\ntoy\ntrack\ntrade\ntraffic\ntragic\ntrain\ntransfer\ntrap\ntrash\ntravel\ntray\ntreat\ntree\ntrend\ntrial\ntribe\ntrick\ntrigger\ntrim\ntrip\ntrophy\ntrouble\ntruck\ntrue\ntruly\ntrumpet\ntrust\ntruth\ntry\ntube\ntuition\ntumble\ntuna\ntunnel\nturkey\nturn\nturtle\ntwelve\ntwenty\ntwice\ntwin\ntwist\ntwo\ntype\ntypical\nugly\numbrella\nunable\nunaware\nuncle\nuncover\nunder\nundo\nunfair\nunfold\nunhappy\nuniform\nunique\nunit\nuniverse\nunknown\nunlock\nuntil\nunusual\nunveil\nupdate\nupgrade\nuphold\nupon\nupper\nupset\nurban\nurge\nusage\nuse\nused\nuseful\nuseless\nusual\nutility\nvacant\nvacuum\nvague\nvalid\nvalley\nvalve\nvan\nvanish\nvapor\nvarious\nvast\nvault\nvehicle\nvelvet\nvendor\nventure\nvenue\nverb\nverify\nversion\nvery\nvessel\nveteran\nviable\nvibrant\nvicious\nvictory\nvideo\nview\nvillage\nvintage\nviolin\nvirtual\nvirus\nvisa\nvisit\nvisual\nvital\nvivid\nvocal\nvoice\nvoid\nvolcano\nvolume\nvote\nvoyage\nwage\nwagon\nwait\nwalk\nwall\nwalnut\nwant\nwarfare\nwarm\nwarrior\nwash\nwasp\nwaste\nwater\nwave\nway\nwealth\nweapon\nwear\nweasel\nweather\nweb\nwedding\nweekend\nweird\nwelcome\nwest\nwet\nwhale\nwhat\nwheat\nwheel\nwhen\nwhere\nwhip\nwhisper\nwide\nwidth\nwife\nwild\nwill\nwin\nwindow\nwine\nwing\nwink\nwinner\nwinter\nwire\nwisdom\nwise\nwish\nwitness\nwolf\nwoman\nwonder\nwood\nwool\nword\nwork\nworld\nworry\nworth\nwrap\nwreck\nwrestle\nwrist\nwrite\nwrong\nyard\nyear\nyellow\nyou\nyoung\nyouth\nzebra\nzero\nzone\nzoo\n" + ); + dico.Add( + "japanese", + "あいこくしん\nあいさつ\nあいだ\nあおぞら\nあかちゃん\nあきる\nあけがた\nあける\nあこがれる\nあさい\nあさひ\nあしあと\nあじわう\nあずかる\nあずき\nあそぶ\nあたえる\nあたためる\nあたりまえ\nあたる\nあつい\nあつかう\nあっしゅく\nあつまり\nあつめる\nあてな\nあてはまる\nあひる\nあぶら\nあぶる\nあふれる\nあまい\nあまど\nあまやかす\nあまり\nあみもの\nあめりか\nあやまる\nあゆむ\nあらいぐま\nあらし\nあらすじ\nあらためる\nあらゆる\nあらわす\nありがとう\nあわせる\nあわてる\nあんい\nあんがい\nあんこ\nあんぜん\nあんてい\nあんない\nあんまり\nいいだす\nいおん\nいがい\nいがく\nいきおい\nいきなり\nいきもの\nいきる\nいくじ\nいくぶん\nいけばな\nいけん\nいこう\nいこく\nいこつ\nいさましい\nいさん\nいしき\nいじゅう\nいじょう\nいじわる\nいずみ\nいずれ\nいせい\nいせえび\nいせかい\nいせき\nいぜん\nいそうろう\nいそがしい\nいだい\nいだく\nいたずら\nいたみ\nいたりあ\nいちおう\nいちじ\nいちど\nいちば\nいちぶ\nいちりゅう\nいつか\nいっしゅん\nいっせい\nいっそう\nいったん\nいっち\nいってい\nいっぽう\nいてざ\nいてん\nいどう\nいとこ\nいない\nいなか\nいねむり\nいのち\nいのる\nいはつ\nいばる\nいはん\nいびき\nいひん\nいふく\nいへん\nいほう\nいみん\nいもうと\nいもたれ\nいもり\nいやがる\nいやす\nいよかん\nいよく\nいらい\nいらすと\nいりぐち\nいりょう\nいれい\nいれもの\nいれる\nいろえんぴつ\nいわい\nいわう\nいわかん\nいわば\nいわゆる\nいんげんまめ\nいんさつ\nいんしょう\nいんよう\nうえき\nうえる\nうおざ\nうがい\nうかぶ\nうかべる\nうきわ\nうくらいな\nうくれれ\nうけたまわる\nうけつけ\nうけとる\nうけもつ\nうける\nうごかす\nうごく\nうこん\nうさぎ\nうしなう\nうしろがみ\nうすい\nうすぎ\nうすぐらい\nうすめる\nうせつ\nうちあわせ\nうちがわ\nうちき\nうちゅう\nうっかり\nうつくしい\nうったえる\nうつる\nうどん\nうなぎ\nうなじ\nうなずく\nうなる\nうねる\nうのう\nうぶげ\nうぶごえ\nうまれる\nうめる\nうもう\nうやまう\nうよく\nうらがえす\nうらぐち\nうらない\nうりあげ\nうりきれ\nうるさい\nうれしい\nうれゆき\nうれる\nうろこ\nうわき\nうわさ\nうんこう\nうんちん\nうんてん\nうんどう\nえいえん\nえいが\nえいきょう\nえいご\nえいせい\nえいぶん\nえいよう\nえいわ\nえおり\nえがお\nえがく\nえきたい\nえくせる\nえしゃく\nえすて\nえつらん\nえのぐ\nえほうまき\nえほん\nえまき\nえもじ\nえもの\nえらい\nえらぶ\nえりあ\nえんえん\nえんかい\nえんぎ\nえんげき\nえんしゅう\nえんぜつ\nえんそく\nえんちょう\nえんとつ\nおいかける\nおいこす\nおいしい\nおいつく\nおうえん\nおうさま\nおうじ\nおうせつ\nおうたい\nおうふく\nおうべい\nおうよう\nおえる\nおおい\nおおう\nおおどおり\nおおや\nおおよそ\nおかえり\nおかず\nおがむ\nおかわり\nおぎなう\nおきる\nおくさま\nおくじょう\nおくりがな\nおくる\nおくれる\nおこす\nおこなう\nおこる\nおさえる\nおさない\nおさめる\nおしいれ\nおしえる\nおじぎ\nおじさん\nおしゃれ\nおそらく\nおそわる\nおたがい\nおたく\nおだやか\nおちつく\nおっと\nおつり\nおでかけ\nおとしもの\nおとなしい\nおどり\nおどろかす\nおばさん\nおまいり\nおめでとう\nおもいで\nおもう\nおもたい\nおもちゃ\nおやつ\nおやゆび\nおよぼす\nおらんだ\nおろす\nおんがく\nおんけい\nおんしゃ\nおんせん\nおんだん\nおんちゅう\nおんどけい\nかあつ\nかいが\nがいき\nがいけん\nがいこう\nかいさつ\nかいしゃ\nかいすいよく\nかいぜん\nかいぞうど\nかいつう\nかいてん\nかいとう\nかいふく\nがいへき\nかいほう\nかいよう\nがいらい\nかいわ\nかえる\nかおり\nかかえる\nかがく\nかがし\nかがみ\nかくご\nかくとく\nかざる\nがぞう\nかたい\nかたち\nがちょう\nがっきゅう\nがっこう\nがっさん\nがっしょう\nかなざわし\nかのう\nがはく\nかぶか\nかほう\nかほご\nかまう\nかまぼこ\nかめれおん\nかゆい\nかようび\nからい\nかるい\nかろう\nかわく\nかわら\nがんか\nかんけい\nかんこう\nかんしゃ\nかんそう\nかんたん\nかんち\nがんばる\nきあい\nきあつ\nきいろ\nぎいん\nきうい\nきうん\nきえる\nきおう\nきおく\nきおち\nきおん\nきかい\nきかく\nきかんしゃ\nききて\nきくばり\nきくらげ\nきけんせい\nきこう\nきこえる\nきこく\nきさい\nきさく\nきさま\nきさらぎ\nぎじかがく\nぎしき\nぎじたいけん\nぎじにってい\nぎじゅつしゃ\nきすう\nきせい\nきせき\nきせつ\nきそう\nきぞく\nきぞん\nきたえる\nきちょう\nきつえん\nぎっちり\nきつつき\nきつね\nきてい\nきどう\nきどく\nきない\nきなが\nきなこ\nきぬごし\nきねん\nきのう\nきのした\nきはく\nきびしい\nきひん\nきふく\nきぶん\nきぼう\nきほん\nきまる\nきみつ\nきむずかしい\nきめる\nきもだめし\nきもち\nきもの\nきゃく\nきやく\nぎゅうにく\nきよう\nきょうりゅう\nきらい\nきらく\nきりん\nきれい\nきれつ\nきろく\nぎろん\nきわめる\nぎんいろ\nきんかくじ\nきんじょ\nきんようび\nぐあい\nくいず\nくうかん\nくうき\nくうぐん\nくうこう\nぐうせい\nくうそう\nぐうたら\nくうふく\nくうぼ\nくかん\nくきょう\nくげん\nぐこう\nくさい\nくさき\nくさばな\nくさる\nくしゃみ\nくしょう\nくすのき\nくすりゆび\nくせげ\nくせん\nぐたいてき\nくださる\nくたびれる\nくちこみ\nくちさき\nくつした\nぐっすり\nくつろぐ\nくとうてん\nくどく\nくなん\nくねくね\nくのう\nくふう\nくみあわせ\nくみたてる\nくめる\nくやくしょ\nくらす\nくらべる\nくるま\nくれる\nくろう\nくわしい\nぐんかん\nぐんしょく\nぐんたい\nぐんて\nけあな\nけいかく\nけいけん\nけいこ\nけいさつ\nげいじゅつ\nけいたい\nげいのうじん\nけいれき\nけいろ\nけおとす\nけおりもの\nげきか\nげきげん\nげきだん\nげきちん\nげきとつ\nげきは\nげきやく\nげこう\nげこくじょう\nげざい\nけさき\nげざん\nけしき\nけしごむ\nけしょう\nげすと\nけたば\nけちゃっぷ\nけちらす\nけつあつ\nけつい\nけつえき\nけっこん\nけつじょ\nけっせき\nけってい\nけつまつ\nげつようび\nげつれい\nけつろん\nげどく\nけとばす\nけとる\nけなげ\nけなす\nけなみ\nけぬき\nげねつ\nけねん\nけはい\nげひん\nけぶかい\nげぼく\nけまり\nけみかる\nけむし\nけむり\nけもの\nけらい\nけろけろ\nけわしい\nけんい\nけんえつ\nけんお\nけんか\nげんき\nけんげん\nけんこう\nけんさく\nけんしゅう\nけんすう\nげんそう\nけんちく\nけんてい\nけんとう\nけんない\nけんにん\nげんぶつ\nけんま\nけんみん\nけんめい\nけんらん\nけんり\nこあくま\nこいぬ\nこいびと\nごうい\nこうえん\nこうおん\nこうかん\nごうきゅう\nごうけい\nこうこう\nこうさい\nこうじ\nこうすい\nごうせい\nこうそく\nこうたい\nこうちゃ\nこうつう\nこうてい\nこうどう\nこうない\nこうはい\nごうほう\nごうまん\nこうもく\nこうりつ\nこえる\nこおり\nごかい\nごがつ\nごかん\nこくご\nこくさい\nこくとう\nこくない\nこくはく\nこぐま\nこけい\nこける\nここのか\nこころ\nこさめ\nこしつ\nこすう\nこせい\nこせき\nこぜん\nこそだて\nこたい\nこたえる\nこたつ\nこちょう\nこっか\nこつこつ\nこつばん\nこつぶ\nこてい\nこてん\nことがら\nことし\nことば\nことり\nこなごな\nこねこね\nこのまま\nこのみ\nこのよ\nごはん\nこひつじ\nこふう\nこふん\nこぼれる\nごまあぶら\nこまかい\nごますり\nこまつな\nこまる\nこむぎこ\nこもじ\nこもち\nこもの\nこもん\nこやく\nこやま\nこゆう\nこゆび\nこよい\nこよう\nこりる\nこれくしょん\nころっけ\nこわもて\nこわれる\nこんいん\nこんかい\nこんき\nこんしゅう\nこんすい\nこんだて\nこんとん\nこんなん\nこんびに\nこんぽん\nこんまけ\nこんや\nこんれい\nこんわく\nざいえき\nさいかい\nさいきん\nざいげん\nざいこ\nさいしょ\nさいせい\nざいたく\nざいちゅう\nさいてき\nざいりょう\nさうな\nさかいし\nさがす\nさかな\nさかみち\nさがる\nさぎょう\nさくし\nさくひん\nさくら\nさこく\nさこつ\nさずかる\nざせき\nさたん\nさつえい\nざつおん\nざっか\nざつがく\nさっきょく\nざっし\nさつじん\nざっそう\nさつたば\nさつまいも\nさてい\nさといも\nさとう\nさとおや\nさとし\nさとる\nさのう\nさばく\nさびしい\nさべつ\nさほう\nさほど\nさます\nさみしい\nさみだれ\nさむけ\nさめる\nさやえんどう\nさゆう\nさよう\nさよく\nさらだ\nざるそば\nさわやか\nさわる\nさんいん\nさんか\nさんきゃく\nさんこう\nさんさい\nざんしょ\nさんすう\nさんせい\nさんそ\nさんち\nさんま\nさんみ\nさんらん\nしあい\nしあげ\nしあさって\nしあわせ\nしいく\nしいん\nしうち\nしえい\nしおけ\nしかい\nしかく\nじかん\nしごと\nしすう\nじだい\nしたうけ\nしたぎ\nしたて\nしたみ\nしちょう\nしちりん\nしっかり\nしつじ\nしつもん\nしてい\nしてき\nしてつ\nじてん\nじどう\nしなぎれ\nしなもの\nしなん\nしねま\nしねん\nしのぐ\nしのぶ\nしはい\nしばかり\nしはつ\nしはらい\nしはん\nしひょう\nしふく\nじぶん\nしへい\nしほう\nしほん\nしまう\nしまる\nしみん\nしむける\nじむしょ\nしめい\nしめる\nしもん\nしゃいん\nしゃうん\nしゃおん\nじゃがいも\nしやくしょ\nしゃくほう\nしゃけん\nしゃこ\nしゃざい\nしゃしん\nしゃせん\nしゃそう\nしゃたい\nしゃちょう\nしゃっきん\nじゃま\nしゃりん\nしゃれい\nじゆう\nじゅうしょ\nしゅくはく\nじゅしん\nしゅっせき\nしゅみ\nしゅらば\nじゅんばん\nしょうかい\nしょくたく\nしょっけん\nしょどう\nしょもつ\nしらせる\nしらべる\nしんか\nしんこう\nじんじゃ\nしんせいじ\nしんちく\nしんりん\nすあげ\nすあし\nすあな\nずあん\nすいえい\nすいか\nすいとう\nずいぶん\nすいようび\nすうがく\nすうじつ\nすうせん\nすおどり\nすきま\nすくう\nすくない\nすける\nすごい\nすこし\nずさん\nすずしい\nすすむ\nすすめる\nすっかり\nずっしり\nずっと\nすてき\nすてる\nすねる\nすのこ\nすはだ\nすばらしい\nずひょう\nずぶぬれ\nすぶり\nすふれ\nすべて\nすべる\nずほう\nすぼん\nすまい\nすめし\nすもう\nすやき\nすらすら\nするめ\nすれちがう\nすろっと\nすわる\nすんぜん\nすんぽう\nせあぶら\nせいかつ\nせいげん\nせいじ\nせいよう\nせおう\nせかいかん\nせきにん\nせきむ\nせきゆ\nせきらんうん\nせけん\nせこう\nせすじ\nせたい\nせたけ\nせっかく\nせっきゃく\nぜっく\nせっけん\nせっこつ\nせっさたくま\nせつぞく\nせつだん\nせつでん\nせっぱん\nせつび\nせつぶん\nせつめい\nせつりつ\nせなか\nせのび\nせはば\nせびろ\nせぼね\nせまい\nせまる\nせめる\nせもたれ\nせりふ\nぜんあく\nせんい\nせんえい\nせんか\nせんきょ\nせんく\nせんげん\nぜんご\nせんさい\nせんしゅ\nせんすい\nせんせい\nせんぞ\nせんたく\nせんちょう\nせんてい\nせんとう\nせんぬき\nせんねん\nせんぱい\nぜんぶ\nぜんぽう\nせんむ\nせんめんじょ\nせんもん\nせんやく\nせんゆう\nせんよう\nぜんら\nぜんりゃく\nせんれい\nせんろ\nそあく\nそいとげる\nそいね\nそうがんきょう\nそうき\nそうご\nそうしん\nそうだん\nそうなん\nそうび\nそうめん\nそうり\nそえもの\nそえん\nそがい\nそげき\nそこう\nそこそこ\nそざい\nそしな\nそせい\nそせん\nそそぐ\nそだてる\nそつう\nそつえん\nそっかん\nそつぎょう\nそっけつ\nそっこう\nそっせん\nそっと\nそとがわ\nそとづら\nそなえる\nそなた\nそふぼ\nそぼく\nそぼろ\nそまつ\nそまる\nそむく\nそむりえ\nそめる\nそもそも\nそよかぜ\nそらまめ\nそろう\nそんかい\nそんけい\nそんざい\nそんしつ\nそんぞく\nそんちょう\nぞんび\nぞんぶん\nそんみん\nたあい\nたいいん\nたいうん\nたいえき\nたいおう\nだいがく\nたいき\nたいぐう\nたいけん\nたいこ\nたいざい\nだいじょうぶ\nだいすき\nたいせつ\nたいそう\nだいたい\nたいちょう\nたいてい\nだいどころ\nたいない\nたいねつ\nたいのう\nたいはん\nだいひょう\nたいふう\nたいへん\nたいほ\nたいまつばな\nたいみんぐ\nたいむ\nたいめん\nたいやき\nたいよう\nたいら\nたいりょく\nたいる\nたいわん\nたうえ\nたえる\nたおす\nたおる\nたおれる\nたかい\nたかね\nたきび\nたくさん\nたこく\nたこやき\nたさい\nたしざん\nだじゃれ\nたすける\nたずさわる\nたそがれ\nたたかう\nたたく\nただしい\nたたみ\nたちばな\nだっかい\nだっきゃく\nだっこ\nだっしゅつ\nだったい\nたてる\nたとえる\nたなばた\nたにん\nたぬき\nたのしみ\nたはつ\nたぶん\nたべる\nたぼう\nたまご\nたまる\nだむる\nためいき\nためす\nためる\nたもつ\nたやすい\nたよる\nたらす\nたりきほんがん\nたりょう\nたりる\nたると\nたれる\nたれんと\nたろっと\nたわむれる\nだんあつ\nたんい\nたんおん\nたんか\nたんき\nたんけん\nたんご\nたんさん\nたんじょうび\nだんせい\nたんそく\nたんたい\nだんち\nたんてい\nたんとう\nだんな\nたんにん\nだんねつ\nたんのう\nたんぴん\nだんぼう\nたんまつ\nたんめい\nだんれつ\nだんろ\nだんわ\nちあい\nちあん\nちいき\nちいさい\nちえん\nちかい\nちから\nちきゅう\nちきん\nちけいず\nちけん\nちこく\nちさい\nちしき\nちしりょう\nちせい\nちそう\nちたい\nちたん\nちちおや\nちつじょ\nちてき\nちてん\nちぬき\nちぬり\nちのう\nちひょう\nちへいせん\nちほう\nちまた\nちみつ\nちみどろ\nちめいど\nちゃんこなべ\nちゅうい\nちゆりょく\nちょうし\nちょさくけん\nちらし\nちらみ\nちりがみ\nちりょう\nちるど\nちわわ\nちんたい\nちんもく\nついか\nついたち\nつうか\nつうじょう\nつうはん\nつうわ\nつかう\nつかれる\nつくね\nつくる\nつけね\nつける\nつごう\nつたえる\nつづく\nつつじ\nつつむ\nつとめる\nつながる\nつなみ\nつねづね\nつのる\nつぶす\nつまらない\nつまる\nつみき\nつめたい\nつもり\nつもる\nつよい\nつるぼ\nつるみく\nつわもの\nつわり\nてあし\nてあて\nてあみ\nていおん\nていか\nていき\nていけい\nていこく\nていさつ\nていし\nていせい\nていたい\nていど\nていねい\nていひょう\nていへん\nていぼう\nてうち\nておくれ\nてきとう\nてくび\nでこぼこ\nてさぎょう\nてさげ\nてすり\nてそう\nてちがい\nてちょう\nてつがく\nてつづき\nでっぱ\nてつぼう\nてつや\nでぬかえ\nてぬき\nてぬぐい\nてのひら\nてはい\nてぶくろ\nてふだ\nてほどき\nてほん\nてまえ\nてまきずし\nてみじか\nてみやげ\nてらす\nてれび\nてわけ\nてわたし\nでんあつ\nてんいん\nてんかい\nてんき\nてんぐ\nてんけん\nてんごく\nてんさい\nてんし\nてんすう\nでんち\nてんてき\nてんとう\nてんない\nてんぷら\nてんぼうだい\nてんめつ\nてんらんかい\nでんりょく\nでんわ\nどあい\nといれ\nどうかん\nとうきゅう\nどうぐ\nとうし\nとうむぎ\nとおい\nとおか\nとおく\nとおす\nとおる\nとかい\nとかす\nときおり\nときどき\nとくい\nとくしゅう\nとくてん\nとくに\nとくべつ\nとけい\nとける\nとこや\nとさか\nとしょかん\nとそう\nとたん\nとちゅう\nとっきゅう\nとっくん\nとつぜん\nとつにゅう\nとどける\nととのえる\nとない\nとなえる\nとなり\nとのさま\nとばす\nどぶがわ\nとほう\nとまる\nとめる\nともだち\nともる\nどようび\nとらえる\nとんかつ\nどんぶり\nないかく\nないこう\nないしょ\nないす\nないせん\nないそう\nなおす\nながい\nなくす\nなげる\nなこうど\nなさけ\nなたでここ\nなっとう\nなつやすみ\nななおし\nなにごと\nなにもの\nなにわ\nなのか\nなふだ\nなまいき\nなまえ\nなまみ\nなみだ\nなめらか\nなめる\nなやむ\nならう\nならび\nならぶ\nなれる\nなわとび\nなわばり\nにあう\nにいがた\nにうけ\nにおい\nにかい\nにがて\nにきび\nにくしみ\nにくまん\nにげる\nにさんかたんそ\nにしき\nにせもの\nにちじょう\nにちようび\nにっか\nにっき\nにっけい\nにっこう\nにっさん\nにっしょく\nにっすう\nにっせき\nにってい\nになう\nにほん\nにまめ\nにもつ\nにやり\nにゅういん\nにりんしゃ\nにわとり\nにんい\nにんか\nにんき\nにんげん\nにんしき\nにんずう\nにんそう\nにんたい\nにんち\nにんてい\nにんにく\nにんぷ\nにんまり\nにんむ\nにんめい\nにんよう\nぬいくぎ\nぬかす\nぬぐいとる\nぬぐう\nぬくもり\nぬすむ\nぬまえび\nぬめり\nぬらす\nぬんちゃく\nねあげ\nねいき\nねいる\nねいろ\nねぐせ\nねくたい\nねくら\nねこぜ\nねこむ\nねさげ\nねすごす\nねそべる\nねだん\nねつい\nねっしん\nねつぞう\nねったいぎょ\nねぶそく\nねふだ\nねぼう\nねほりはほり\nねまき\nねまわし\nねみみ\nねむい\nねむたい\nねもと\nねらう\nねわざ\nねんいり\nねんおし\nねんかん\nねんきん\nねんぐ\nねんざ\nねんし\nねんちゃく\nねんど\nねんぴ\nねんぶつ\nねんまつ\nねんりょう\nねんれい\nのいず\nのおづま\nのがす\nのきなみ\nのこぎり\nのこす\nのこる\nのせる\nのぞく\nのぞむ\nのたまう\nのちほど\nのっく\nのばす\nのはら\nのべる\nのぼる\nのみもの\nのやま\nのらいぬ\nのらねこ\nのりもの\nのりゆき\nのれん\nのんき\nばあい\nはあく\nばあさん\nばいか\nばいく\nはいけん\nはいご\nはいしん\nはいすい\nはいせん\nはいそう\nはいち\nばいばい\nはいれつ\nはえる\nはおる\nはかい\nばかり\nはかる\nはくしゅ\nはけん\nはこぶ\nはさみ\nはさん\nはしご\nばしょ\nはしる\nはせる\nぱそこん\nはそん\nはたん\nはちみつ\nはつおん\nはっかく\nはづき\nはっきり\nはっくつ\nはっけん\nはっこう\nはっさん\nはっしん\nはったつ\nはっちゅう\nはってん\nはっぴょう\nはっぽう\nはなす\nはなび\nはにかむ\nはぶらし\nはみがき\nはむかう\nはめつ\nはやい\nはやし\nはらう\nはろうぃん\nはわい\nはんい\nはんえい\nはんおん\nはんかく\nはんきょう\nばんぐみ\nはんこ\nはんしゃ\nはんすう\nはんだん\nぱんち\nぱんつ\nはんてい\nはんとし\nはんのう\nはんぱ\nはんぶん\nはんぺん\nはんぼうき\nはんめい\nはんらん\nはんろん\nひいき\nひうん\nひえる\nひかく\nひかり\nひかる\nひかん\nひくい\nひけつ\nひこうき\nひこく\nひさい\nひさしぶり\nひさん\nびじゅつかん\nひしょ\nひそか\nひそむ\nひたむき\nひだり\nひたる\nひつぎ\nひっこし\nひっし\nひつじゅひん\nひっす\nひつぜん\nぴったり\nぴっちり\nひつよう\nひてい\nひとごみ\nひなまつり\nひなん\nひねる\nひはん\nひびく\nひひょう\nひほう\nひまわり\nひまん\nひみつ\nひめい\nひめじし\nひやけ\nひやす\nひよう\nびょうき\nひらがな\nひらく\nひりつ\nひりょう\nひるま\nひるやすみ\nひれい\nひろい\nひろう\nひろき\nひろゆき\nひんかく\nひんけつ\nひんこん\nひんしゅ\nひんそう\nぴんち\nひんぱん\nびんぼう\nふあん\nふいうち\nふうけい\nふうせん\nぷうたろう\nふうとう\nふうふ\nふえる\nふおん\nふかい\nふきん\nふくざつ\nふくぶくろ\nふこう\nふさい\nふしぎ\nふじみ\nふすま\nふせい\nふせぐ\nふそく\nぶたにく\nふたん\nふちょう\nふつう\nふつか\nふっかつ\nふっき\nふっこく\nぶどう\nふとる\nふとん\nふのう\nふはい\nふひょう\nふへん\nふまん\nふみん\nふめつ\nふめん\nふよう\nふりこ\nふりる\nふるい\nふんいき\nぶんがく\nぶんぐ\nふんしつ\nぶんせき\nふんそう\nぶんぽう\nへいあん\nへいおん\nへいがい\nへいき\nへいげん\nへいこう\nへいさ\nへいしゃ\nへいせつ\nへいそ\nへいたく\nへいてん\nへいねつ\nへいわ\nへきが\nへこむ\nべにいろ\nべにしょうが\nへらす\nへんかん\nべんきょう\nべんごし\nへんさい\nへんたい\nべんり\nほあん\nほいく\nぼうぎょ\nほうこく\nほうそう\nほうほう\nほうもん\nほうりつ\nほえる\nほおん\nほかん\nほきょう\nぼきん\nほくろ\nほけつ\nほけん\nほこう\nほこる\nほしい\nほしつ\nほしゅ\nほしょう\nほせい\nほそい\nほそく\nほたて\nほたる\nぽちぶくろ\nほっきょく\nほっさ\nほったん\nほとんど\nほめる\nほんい\nほんき\nほんけ\nほんしつ\nほんやく\nまいにち\nまかい\nまかせる\nまがる\nまける\nまこと\nまさつ\nまじめ\nますく\nまぜる\nまつり\nまとめ\nまなぶ\nまぬけ\nまねく\nまほう\nまもる\nまゆげ\nまよう\nまろやか\nまわす\nまわり\nまわる\nまんが\nまんきつ\nまんぞく\nまんなか\nみいら\nみうち\nみえる\nみがく\nみかた\nみかん\nみけん\nみこん\nみじかい\nみすい\nみすえる\nみせる\nみっか\nみつかる\nみつける\nみてい\nみとめる\nみなと\nみなみかさい\nみねらる\nみのう\nみのがす\nみほん\nみもと\nみやげ\nみらい\nみりょく\nみわく\nみんか\nみんぞく\nむいか\nむえき\nむえん\nむかい\nむかう\nむかえ\nむかし\nむぎちゃ\nむける\nむげん\nむさぼる\nむしあつい\nむしば\nむじゅん\nむしろ\nむすう\nむすこ\nむすぶ\nむすめ\nむせる\nむせん\nむちゅう\nむなしい\nむのう\nむやみ\nむよう\nむらさき\nむりょう\nむろん\nめいあん\nめいうん\nめいえん\nめいかく\nめいきょく\nめいさい\nめいし\nめいそう\nめいぶつ\nめいれい\nめいわく\nめぐまれる\nめざす\nめした\nめずらしい\nめだつ\nめまい\nめやす\nめんきょ\nめんせき\nめんどう\nもうしあげる\nもうどうけん\nもえる\nもくし\nもくてき\nもくようび\nもちろん\nもどる\nもらう\nもんく\nもんだい\nやおや\nやける\nやさい\nやさしい\nやすい\nやすたろう\nやすみ\nやせる\nやそう\nやたい\nやちん\nやっと\nやっぱり\nやぶる\nやめる\nややこしい\nやよい\nやわらかい\nゆうき\nゆうびんきょく\nゆうべ\nゆうめい\nゆけつ\nゆしゅつ\nゆせん\nゆそう\nゆたか\nゆちゃく\nゆでる\nゆにゅう\nゆびわ\nゆらい\nゆれる\nようい\nようか\nようきゅう\nようじ\nようす\nようちえん\nよかぜ\nよかん\nよきん\nよくせい\nよくぼう\nよけい\nよごれる\nよさん\nよしゅう\nよそう\nよそく\nよっか\nよてい\nよどがわく\nよねつ\nよやく\nよゆう\nよろこぶ\nよろしい\nらいう\nらくがき\nらくご\nらくさつ\nらくだ\nらしんばん\nらせん\nらぞく\nらたい\nらっか\nられつ\nりえき\nりかい\nりきさく\nりきせつ\nりくぐん\nりくつ\nりけん\nりこう\nりせい\nりそう\nりそく\nりてん\nりねん\nりゆう\nりゅうがく\nりよう\nりょうり\nりょかん\nりょくちゃ\nりょこう\nりりく\nりれき\nりろん\nりんご\nるいけい\nるいさい\nるいじ\nるいせき\nるすばん\nるりがわら\nれいかん\nれいぎ\nれいせい\nれいぞうこ\nれいとう\nれいぼう\nれきし\nれきだい\nれんあい\nれんけい\nれんこん\nれんさい\nれんしゅう\nれんぞく\nれんらく\nろうか\nろうご\nろうじん\nろうそく\nろくが\nろこつ\nろじうら\nろしゅつ\nろせん\nろてん\nろめん\nろれつ\nろんぎ\nろんぱ\nろんぶん\nろんり\nわかす\nわかめ\nわかやま\nわかれる\nわしつ\nわじまし\nわすれもの\nわらう\nわれる\n" + ); + dico.Add( + "spanish", + "ábaco\nabdomen\nabeja\nabierto\nabogado\nabono\naborto\nabrazo\nabrir\nabuelo\nabuso\nacabar\nacademia\nacceso\nacción\naceite\nacelga\nacento\naceptar\nácido\naclarar\nacné\nacoger\nacoso\nactivo\nacto\nactriz\nactuar\nacudir\nacuerdo\nacusar\nadicto\nadmitir\nadoptar\nadorno\naduana\nadulto\naéreo\nafectar\nafición\nafinar\nafirmar\nágil\nagitar\nagonía\nagosto\nagotar\nagregar\nagrio\nagua\nagudo\náguila\naguja\nahogo\nahorro\naire\naislar\najedrez\najeno\najuste\nalacrán\nalambre\nalarma\nalba\nálbum\nalcalde\naldea\nalegre\nalejar\nalerta\naleta\nalfiler\nalga\nalgodón\naliado\naliento\nalivio\nalma\nalmeja\nalmíbar\naltar\nalteza\naltivo\nalto\naltura\nalumno\nalzar\namable\namante\namapola\namargo\namasar\námbar\námbito\nameno\namigo\namistad\namor\namparo\namplio\nancho\nanciano\nancla\nandar\nandén\nanemia\nángulo\nanillo\nánimo\nanís\nanotar\nantena\nantiguo\nantojo\nanual\nanular\nanuncio\nañadir\nañejo\naño\napagar\naparato\napetito\napio\naplicar\napodo\naporte\napoyo\naprender\naprobar\napuesta\napuro\narado\naraña\narar\nárbitro\nárbol\narbusto\narchivo\narco\narder\nardilla\narduo\nárea\nárido\naries\narmonía\narnés\naroma\narpa\narpón\narreglo\narroz\narruga\narte\nartista\nasa\nasado\nasalto\nascenso\nasegurar\naseo\nasesor\nasiento\nasilo\nasistir\nasno\nasombro\náspero\nastilla\nastro\nastuto\nasumir\nasunto\natajo\nataque\natar\natento\nateo\nático\natleta\nátomo\natraer\natroz\natún\naudaz\naudio\nauge\naula\naumento\nausente\nautor\naval\navance\navaro\nave\navellana\navena\navestruz\navión\naviso\nayer\nayuda\nayuno\nazafrán\nazar\nazote\nazúcar\nazufre\nazul\nbaba\nbabor\nbache\nbahía\nbaile\nbajar\nbalanza\nbalcón\nbalde\nbambú\nbanco\nbanda\nbaño\nbarba\nbarco\nbarniz\nbarro\nbáscula\nbastón\nbasura\nbatalla\nbatería\nbatir\nbatuta\nbaúl\nbazar\nbebé\nbebida\nbello\nbesar\nbeso\nbestia\nbicho\nbien\nbingo\nblanco\nbloque\nblusa\nboa\nbobina\nbobo\nboca\nbocina\nboda\nbodega\nboina\nbola\nbolero\nbolsa\nbomba\nbondad\nbonito\nbono\nbonsái\nborde\nborrar\nbosque\nbote\nbotín\nbóveda\nbozal\nbravo\nbrazo\nbrecha\nbreve\nbrillo\nbrinco\nbrisa\nbroca\nbroma\nbronce\nbrote\nbruja\nbrusco\nbruto\nbuceo\nbucle\nbueno\nbuey\nbufanda\nbufón\nbúho\nbuitre\nbulto\nburbuja\nburla\nburro\nbuscar\nbutaca\nbuzón\ncaballo\ncabeza\ncabina\ncabra\ncacao\ncadáver\ncadena\ncaer\ncafé\ncaída\ncaimán\ncaja\ncajón\ncal\ncalamar\ncalcio\ncaldo\ncalidad\ncalle\ncalma\ncalor\ncalvo\ncama\ncambio\ncamello\ncamino\ncampo\ncáncer\ncandil\ncanela\ncanguro\ncanica\ncanto\ncaña\ncañón\ncaoba\ncaos\ncapaz\ncapitán\ncapote\ncaptar\ncapucha\ncara\ncarbón\ncárcel\ncareta\ncarga\ncariño\ncarne\ncarpeta\ncarro\ncarta\ncasa\ncasco\ncasero\ncaspa\ncastor\ncatorce\ncatre\ncaudal\ncausa\ncazo\ncebolla\nceder\ncedro\ncelda\ncélebre\nceloso\ncélula\ncemento\nceniza\ncentro\ncerca\ncerdo\ncereza\ncero\ncerrar\ncerteza\ncésped\ncetro\nchacal\nchaleco\nchampú\nchancla\nchapa\ncharla\nchico\nchiste\nchivo\nchoque\nchoza\nchuleta\nchupar\nciclón\nciego\ncielo\ncien\ncierto\ncifra\ncigarro\ncima\ncinco\ncine\ncinta\nciprés\ncirco\nciruela\ncisne\ncita\nciudad\nclamor\nclan\nclaro\nclase\nclave\ncliente\nclima\nclínica\ncobre\ncocción\ncochino\ncocina\ncoco\ncódigo\ncodo\ncofre\ncoger\ncohete\ncojín\ncojo\ncola\ncolcha\ncolegio\ncolgar\ncolina\ncollar\ncolmo\ncolumna\ncombate\ncomer\ncomida\ncómodo\ncompra\nconde\nconejo\nconga\nconocer\nconsejo\ncontar\ncopa\ncopia\ncorazón\ncorbata\ncorcho\ncordón\ncorona\ncorrer\ncoser\ncosmos\ncosta\ncráneo\ncráter\ncrear\ncrecer\ncreído\ncrema\ncría\ncrimen\ncripta\ncrisis\ncromo\ncrónica\ncroqueta\ncrudo\ncruz\ncuadro\ncuarto\ncuatro\ncubo\ncubrir\ncuchara\ncuello\ncuento\ncuerda\ncuesta\ncueva\ncuidar\nculebra\nculpa\nculto\ncumbre\ncumplir\ncuna\ncuneta\ncuota\ncupón\ncúpula\ncurar\ncurioso\ncurso\ncurva\ncutis\ndama\ndanza\ndar\ndardo\ndátil\ndeber\ndébil\ndécada\ndecir\ndedo\ndefensa\ndefinir\ndejar\ndelfín\ndelgado\ndelito\ndemora\ndenso\ndental\ndeporte\nderecho\nderrota\ndesayuno\ndeseo\ndesfile\ndesnudo\ndestino\ndesvío\ndetalle\ndetener\ndeuda\ndía\ndiablo\ndiadema\ndiamante\ndiana\ndiario\ndibujo\ndictar\ndiente\ndieta\ndiez\ndifícil\ndigno\ndilema\ndiluir\ndinero\ndirecto\ndirigir\ndisco\ndiseño\ndisfraz\ndiva\ndivino\ndoble\ndoce\ndolor\ndomingo\ndon\ndonar\ndorado\ndormir\ndorso\ndos\ndosis\ndragón\ndroga\nducha\nduda\nduelo\ndueño\ndulce\ndúo\nduque\ndurar\ndureza\nduro\nébano\nebrio\nechar\neco\necuador\nedad\nedición\nedificio\neditor\neducar\nefecto\neficaz\neje\nejemplo\nelefante\nelegir\nelemento\nelevar\nelipse\nélite\nelixir\nelogio\neludir\nembudo\nemitir\nemoción\nempate\nempeño\nempleo\nempresa\nenano\nencargo\nenchufe\nencía\nenemigo\nenero\nenfado\nenfermo\nengaño\nenigma\nenlace\nenorme\nenredo\nensayo\nenseñar\nentero\nentrar\nenvase\nenvío\népoca\nequipo\nerizo\nescala\nescena\nescolar\nescribir\nescudo\nesencia\nesfera\nesfuerzo\nespada\nespejo\nespía\nesposa\nespuma\nesquí\nestar\neste\nestilo\nestufa\netapa\neterno\nética\netnia\nevadir\nevaluar\nevento\nevitar\nexacto\nexamen\nexceso\nexcusa\nexento\nexigir\nexilio\nexistir\néxito\nexperto\nexplicar\nexponer\nextremo\nfábrica\nfábula\nfachada\nfácil\nfactor\nfaena\nfaja\nfalda\nfallo\nfalso\nfaltar\nfama\nfamilia\nfamoso\nfaraón\nfarmacia\nfarol\nfarsa\nfase\nfatiga\nfauna\nfavor\nfax\nfebrero\nfecha\nfeliz\nfeo\nferia\nferoz\nfértil\nfervor\nfestín\nfiable\nfianza\nfiar\nfibra\nficción\nficha\nfideo\nfiebre\nfiel\nfiera\nfiesta\nfigura\nfijar\nfijo\nfila\nfilete\nfilial\nfiltro\nfin\nfinca\nfingir\nfinito\nfirma\nflaco\nflauta\nflecha\nflor\nflota\nfluir\nflujo\nflúor\nfobia\nfoca\nfogata\nfogón\nfolio\nfolleto\nfondo\nforma\nforro\nfortuna\nforzar\nfosa\nfoto\nfracaso\nfrágil\nfranja\nfrase\nfraude\nfreír\nfreno\nfresa\nfrío\nfrito\nfruta\nfuego\nfuente\nfuerza\nfuga\nfumar\nfunción\nfunda\nfurgón\nfuria\nfusil\nfútbol\nfuturo\ngacela\ngafas\ngaita\ngajo\ngala\ngalería\ngallo\ngamba\nganar\ngancho\nganga\nganso\ngaraje\ngarza\ngasolina\ngastar\ngato\ngavilán\ngemelo\ngemir\ngen\ngénero\ngenio\ngente\ngeranio\ngerente\ngermen\ngesto\ngigante\ngimnasio\ngirar\ngiro\nglaciar\nglobo\ngloria\ngol\ngolfo\ngoloso\ngolpe\ngoma\ngordo\ngorila\ngorra\ngota\ngoteo\ngozar\ngrada\ngráfico\ngrano\ngrasa\ngratis\ngrave\ngrieta\ngrillo\ngripe\ngris\ngrito\ngrosor\ngrúa\ngrueso\ngrumo\ngrupo\nguante\nguapo\nguardia\nguerra\nguía\nguiño\nguion\nguiso\nguitarra\ngusano\ngustar\nhaber\nhábil\nhablar\nhacer\nhacha\nhada\nhallar\nhamaca\nharina\nhaz\nhazaña\nhebilla\nhebra\nhecho\nhelado\nhelio\nhembra\nherir\nhermano\nhéroe\nhervir\nhielo\nhierro\nhígado\nhigiene\nhijo\nhimno\nhistoria\nhocico\nhogar\nhoguera\nhoja\nhombre\nhongo\nhonor\nhonra\nhora\nhormiga\nhorno\nhostil\nhoyo\nhueco\nhuelga\nhuerta\nhueso\nhuevo\nhuida\nhuir\nhumano\nhúmedo\nhumilde\nhumo\nhundir\nhuracán\nhurto\nicono\nideal\nidioma\nídolo\niglesia\niglú\nigual\nilegal\nilusión\nimagen\nimán\nimitar\nimpar\nimperio\nimponer\nimpulso\nincapaz\níndice\ninerte\ninfiel\ninforme\ningenio\ninicio\ninmenso\ninmune\ninnato\ninsecto\ninstante\ninterés\níntimo\nintuir\ninútil\ninvierno\nira\niris\nironía\nisla\nislote\njabalí\njabón\njamón\njarabe\njardín\njarra\njaula\njazmín\njefe\njeringa\njinete\njornada\njoroba\njoven\njoya\njuerga\njueves\njuez\njugador\njugo\njuguete\njuicio\njunco\njungla\njunio\njuntar\njúpiter\njurar\njusto\njuvenil\njuzgar\nkilo\nkoala\nlabio\nlacio\nlacra\nlado\nladrón\nlagarto\nlágrima\nlaguna\nlaico\nlamer\nlámina\nlámpara\nlana\nlancha\nlangosta\nlanza\nlápiz\nlargo\nlarva\nlástima\nlata\nlátex\nlatir\nlaurel\nlavar\nlazo\nleal\nlección\nleche\nlector\nleer\nlegión\nlegumbre\nlejano\nlengua\nlento\nleña\nleón\nleopardo\nlesión\nletal\nletra\nleve\nleyenda\nlibertad\nlibro\nlicor\nlíder\nlidiar\nlienzo\nliga\nligero\nlima\nlímite\nlimón\nlimpio\nlince\nlindo\nlínea\nlingote\nlino\nlinterna\nlíquido\nliso\nlista\nlitera\nlitio\nlitro\nllaga\nllama\nllanto\nllave\nllegar\nllenar\nllevar\nllorar\nllover\nlluvia\nlobo\nloción\nloco\nlocura\nlógica\nlogro\nlombriz\nlomo\nlonja\nlote\nlucha\nlucir\nlugar\nlujo\nluna\nlunes\nlupa\nlustro\nluto\nluz\nmaceta\nmacho\nmadera\nmadre\nmaduro\nmaestro\nmafia\nmagia\nmago\nmaíz\nmaldad\nmaleta\nmalla\nmalo\nmamá\nmambo\nmamut\nmanco\nmando\nmanejar\nmanga\nmaniquí\nmanjar\nmano\nmanso\nmanta\nmañana\nmapa\nmáquina\nmar\nmarco\nmarea\nmarfil\nmargen\nmarido\nmármol\nmarrón\nmartes\nmarzo\nmasa\nmáscara\nmasivo\nmatar\nmateria\nmatiz\nmatriz\nmáximo\nmayor\nmazorca\nmecha\nmedalla\nmedio\nmédula\nmejilla\nmejor\nmelena\nmelón\nmemoria\nmenor\nmensaje\nmente\nmenú\nmercado\nmerengue\nmérito\nmes\nmesón\nmeta\nmeter\nmétodo\nmetro\nmezcla\nmiedo\nmiel\nmiembro\nmiga\nmil\nmilagro\nmilitar\nmillón\nmimo\nmina\nminero\nmínimo\nminuto\nmiope\nmirar\nmisa\nmiseria\nmisil\nmismo\nmitad\nmito\nmochila\nmoción\nmoda\nmodelo\nmoho\nmojar\nmolde\nmoler\nmolino\nmomento\nmomia\nmonarca\nmoneda\nmonja\nmonto\nmoño\nmorada\nmorder\nmoreno\nmorir\nmorro\nmorsa\nmortal\nmosca\nmostrar\nmotivo\nmover\nmóvil\nmozo\nmucho\nmudar\nmueble\nmuela\nmuerte\nmuestra\nmugre\nmujer\nmula\nmuleta\nmulta\nmundo\nmuñeca\nmural\nmuro\nmúsculo\nmuseo\nmusgo\nmúsica\nmuslo\nnácar\nnación\nnadar\nnaipe\nnaranja\nnariz\nnarrar\nnasal\nnatal\nnativo\nnatural\nnáusea\nnaval\nnave\nnavidad\nnecio\nnéctar\nnegar\nnegocio\nnegro\nneón\nnervio\nneto\nneutro\nnevar\nnevera\nnicho\nnido\nniebla\nnieto\nniñez\nniño\nnítido\nnivel\nnobleza\nnoche\nnómina\nnoria\nnorma\nnorte\nnota\nnoticia\nnovato\nnovela\nnovio\nnube\nnuca\nnúcleo\nnudillo\nnudo\nnuera\nnueve\nnuez\nnulo\nnúmero\nnutria\noasis\nobeso\nobispo\nobjeto\nobra\nobrero\nobservar\nobtener\nobvio\noca\nocaso\nocéano\nochenta\nocho\nocio\nocre\noctavo\noctubre\noculto\nocupar\nocurrir\nodiar\nodio\nodisea\noeste\nofensa\noferta\noficio\nofrecer\nogro\noído\noír\nojo\nola\noleada\nolfato\nolivo\nolla\nolmo\nolor\nolvido\nombligo\nonda\nonza\nopaco\nopción\nópera\nopinar\noponer\noptar\nóptica\nopuesto\noración\norador\noral\nórbita\norca\norden\noreja\nórgano\norgía\norgullo\noriente\norigen\norilla\noro\norquesta\noruga\nosadía\noscuro\nosezno\noso\nostra\notoño\notro\noveja\nóvulo\nóxido\noxígeno\noyente\nozono\npacto\npadre\npaella\npágina\npago\npaís\npájaro\npalabra\npalco\npaleta\npálido\npalma\npaloma\npalpar\npan\npanal\npánico\npantera\npañuelo\npapá\npapel\npapilla\npaquete\nparar\nparcela\npared\nparir\nparo\npárpado\nparque\npárrafo\nparte\npasar\npaseo\npasión\npaso\npasta\npata\npatio\npatria\npausa\npauta\npavo\npayaso\npeatón\npecado\npecera\npecho\npedal\npedir\npegar\npeine\npelar\npeldaño\npelea\npeligro\npellejo\npelo\npeluca\npena\npensar\npeñón\npeón\npeor\npepino\npequeño\npera\npercha\nperder\npereza\nperfil\nperico\nperla\npermiso\nperro\npersona\npesa\npesca\npésimo\npestaña\npétalo\npetróleo\npez\npezuña\npicar\npichón\npie\npiedra\npierna\npieza\npijama\npilar\npiloto\npimienta\npino\npintor\npinza\npiña\npiojo\npipa\npirata\npisar\npiscina\npiso\npista\npitón\npizca\nplaca\nplan\nplata\nplaya\nplaza\npleito\npleno\nplomo\npluma\nplural\npobre\npoco\npoder\npodio\npoema\npoesía\npoeta\npolen\npolicía\npollo\npolvo\npomada\npomelo\npomo\npompa\nponer\nporción\nportal\nposada\nposeer\nposible\nposte\npotencia\npotro\npozo\nprado\nprecoz\npregunta\npremio\nprensa\npreso\nprevio\nprimo\npríncipe\nprisión\nprivar\nproa\nprobar\nproceso\nproducto\nproeza\nprofesor\nprograma\nprole\npromesa\npronto\npropio\npróximo\nprueba\npúblico\npuchero\npudor\npueblo\npuerta\npuesto\npulga\npulir\npulmón\npulpo\npulso\npuma\npunto\npuñal\npuño\npupa\npupila\npuré\nquedar\nqueja\nquemar\nquerer\nqueso\nquieto\nquímica\nquince\nquitar\nrábano\nrabia\nrabo\nración\nradical\nraíz\nrama\nrampa\nrancho\nrango\nrapaz\nrápido\nrapto\nrasgo\nraspa\nrato\nrayo\nraza\nrazón\nreacción\nrealidad\nrebaño\nrebote\nrecaer\nreceta\nrechazo\nrecoger\nrecreo\nrecto\nrecurso\nred\nredondo\nreducir\nreflejo\nreforma\nrefrán\nrefugio\nregalo\nregir\nregla\nregreso\nrehén\nreino\nreír\nreja\nrelato\nrelevo\nrelieve\nrelleno\nreloj\nremar\nremedio\nremo\nrencor\nrendir\nrenta\nreparto\nrepetir\nreposo\nreptil\nres\nrescate\nresina\nrespeto\nresto\nresumen\nretiro\nretorno\nretrato\nreunir\nrevés\nrevista\nrey\nrezar\nrico\nriego\nrienda\nriesgo\nrifa\nrígido\nrigor\nrincón\nriñón\nrío\nriqueza\nrisa\nritmo\nrito\nrizo\nroble\nroce\nrociar\nrodar\nrodeo\nrodilla\nroer\nrojizo\nrojo\nromero\nromper\nron\nronco\nronda\nropa\nropero\nrosa\nrosca\nrostro\nrotar\nrubí\nrubor\nrudo\nrueda\nrugir\nruido\nruina\nruleta\nrulo\nrumbo\nrumor\nruptura\nruta\nrutina\nsábado\nsaber\nsabio\nsable\nsacar\nsagaz\nsagrado\nsala\nsaldo\nsalero\nsalir\nsalmón\nsalón\nsalsa\nsalto\nsalud\nsalvar\nsamba\nsanción\nsandía\nsanear\nsangre\nsanidad\nsano\nsanto\nsapo\nsaque\nsardina\nsartén\nsastre\nsatán\nsauna\nsaxofón\nsección\nseco\nsecreto\nsecta\nsed\nseguir\nseis\nsello\nselva\nsemana\nsemilla\nsenda\nsensor\nseñal\nseñor\nseparar\nsepia\nsequía\nser\nserie\nsermón\nservir\nsesenta\nsesión\nseta\nsetenta\nsevero\nsexo\nsexto\nsidra\nsiesta\nsiete\nsiglo\nsigno\nsílaba\nsilbar\nsilencio\nsilla\nsímbolo\nsimio\nsirena\nsistema\nsitio\nsituar\nsobre\nsocio\nsodio\nsol\nsolapa\nsoldado\nsoledad\nsólido\nsoltar\nsolución\nsombra\nsondeo\nsonido\nsonoro\nsonrisa\nsopa\nsoplar\nsoporte\nsordo\nsorpresa\nsorteo\nsostén\nsótano\nsuave\nsubir\nsuceso\nsudor\nsuegra\nsuelo\nsueño\nsuerte\nsufrir\nsujeto\nsultán\nsumar\nsuperar\nsuplir\nsuponer\nsupremo\nsur\nsurco\nsureño\nsurgir\nsusto\nsutil\ntabaco\ntabique\ntabla\ntabú\ntaco\ntacto\ntajo\ntalar\ntalco\ntalento\ntalla\ntalón\ntamaño\ntambor\ntango\ntanque\ntapa\ntapete\ntapia\ntapón\ntaquilla\ntarde\ntarea\ntarifa\ntarjeta\ntarot\ntarro\ntarta\ntatuaje\ntauro\ntaza\ntazón\nteatro\ntecho\ntecla\ntécnica\ntejado\ntejer\ntejido\ntela\nteléfono\ntema\ntemor\ntemplo\ntenaz\ntender\ntener\ntenis\ntenso\nteoría\nterapia\nterco\ntérmino\nternura\nterror\ntesis\ntesoro\ntestigo\ntetera\ntexto\ntez\ntibio\ntiburón\ntiempo\ntienda\ntierra\ntieso\ntigre\ntijera\ntilde\ntimbre\ntímido\ntimo\ntinta\ntío\ntípico\ntipo\ntira\ntirón\ntitán\ntítere\ntítulo\ntiza\ntoalla\ntobillo\ntocar\ntocino\ntodo\ntoga\ntoldo\ntomar\ntono\ntonto\ntopar\ntope\ntoque\ntórax\ntorero\ntormenta\ntorneo\ntoro\ntorpedo\ntorre\ntorso\ntortuga\ntos\ntosco\ntoser\ntóxico\ntrabajo\ntractor\ntraer\ntráfico\ntrago\ntraje\ntramo\ntrance\ntrato\ntrauma\ntrazar\ntrébol\ntregua\ntreinta\ntren\ntrepar\ntres\ntribu\ntrigo\ntripa\ntriste\ntriunfo\ntrofeo\ntrompa\ntronco\ntropa\ntrote\ntrozo\ntruco\ntrueno\ntrufa\ntubería\ntubo\ntuerto\ntumba\ntumor\ntúnel\ntúnica\nturbina\nturismo\nturno\ntutor\nubicar\núlcera\numbral\nunidad\nunir\nuniverso\nuno\nuntar\nuña\nurbano\nurbe\nurgente\nurna\nusar\nusuario\nútil\nutopía\nuva\nvaca\nvacío\nvacuna\nvagar\nvago\nvaina\nvajilla\nvale\nválido\nvalle\nvalor\nválvula\nvampiro\nvara\nvariar\nvarón\nvaso\nvecino\nvector\nvehículo\nveinte\nvejez\nvela\nvelero\nveloz\nvena\nvencer\nvenda\nveneno\nvengar\nvenir\nventa\nvenus\nver\nverano\nverbo\nverde\nvereda\nverja\nverso\nverter\nvía\nviaje\nvibrar\nvicio\nvíctima\nvida\nvídeo\nvidrio\nviejo\nviernes\nvigor\nvil\nvilla\nvinagre\nvino\nviñedo\nviolín\nviral\nvirgo\nvirtud\nvisor\nvíspera\nvista\nvitamina\nviudo\nvivaz\nvivero\nvivir\nvivo\nvolcán\nvolumen\nvolver\nvoraz\nvotar\nvoto\nvoz\nvuelo\nvulgar\nyacer\nyate\nyegua\nyema\nyerno\nyeso\nyodo\nyoga\nyogur\nzafiro\nzanja\nzapato\nzarza\nzona\nzorro\nzumo\nzurdo\n" + ); + dico.Add( + "french", + "abaisser\nabandon\nabdiquer\nabeille\nabolir\naborder\naboutir\naboyer\nabrasif\nabreuver\nabriter\nabroger\nabrupt\nabsence\nabsolu\nabsurde\nabusif\nabyssal\nacadémie\nacajou\nacarien\naccabler\naccepter\nacclamer\naccolade\naccroche\naccuser\nacerbe\nachat\nacheter\naciduler\nacier\nacompte\nacquérir\nacronyme\nacteur\nactif\nactuel\nadepte\nadéquat\nadhésif\nadjectif\nadjuger\nadmettre\nadmirer\nadopter\nadorer\nadoucir\nadresse\nadroit\nadulte\nadverbe\naérer\naéronef\naffaire\naffecter\naffiche\naffreux\naffubler\nagacer\nagencer\nagile\nagiter\nagrafer\nagréable\nagrume\naider\naiguille\nailier\naimable\naisance\najouter\najuster\nalarmer\nalchimie\nalerte\nalgèbre\nalgue\naliéner\naliment\nalléger\nalliage\nallouer\nallumer\nalourdir\nalpaga\naltesse\nalvéole\namateur\nambigu\nambre\naménager\namertume\namidon\namiral\namorcer\namour\namovible\namphibie\nampleur\namusant\nanalyse\nanaphore\nanarchie\nanatomie\nancien\nanéantir\nangle\nangoisse\nanguleux\nanimal\nannexer\nannonce\nannuel\nanodin\nanomalie\nanonyme\nanormal\nantenne\nantidote\nanxieux\napaiser\napéritif\naplanir\napologie\nappareil\nappeler\napporter\nappuyer\naquarium\naqueduc\narbitre\narbuste\nardeur\nardoise\nargent\narlequin\narmature\narmement\narmoire\narmure\narpenter\narracher\narriver\narroser\narsenic\nartériel\narticle\naspect\nasphalte\naspirer\nassaut\nasservir\nassiette\nassocier\nassurer\nasticot\nastre\nastuce\natelier\natome\natrium\natroce\nattaque\nattentif\nattirer\nattraper\naubaine\nauberge\naudace\naudible\naugurer\naurore\nautomne\nautruche\navaler\navancer\navarice\navenir\naverse\naveugle\naviateur\navide\navion\naviser\navoine\navouer\navril\naxial\naxiome\nbadge\nbafouer\nbagage\nbaguette\nbaignade\nbalancer\nbalcon\nbaleine\nbalisage\nbambin\nbancaire\nbandage\nbanlieue\nbannière\nbanquier\nbarbier\nbaril\nbaron\nbarque\nbarrage\nbassin\nbastion\nbataille\nbateau\nbatterie\nbaudrier\nbavarder\nbelette\nbélier\nbelote\nbénéfice\nberceau\nberger\nberline\nbermuda\nbesace\nbesogne\nbétail\nbeurre\nbiberon\nbicycle\nbidule\nbijou\nbilan\nbilingue\nbillard\nbinaire\nbiologie\nbiopsie\nbiotype\nbiscuit\nbison\nbistouri\nbitume\nbizarre\nblafard\nblague\nblanchir\nblessant\nblinder\nblond\nbloquer\nblouson\nbobard\nbobine\nboire\nboiser\nbolide\nbonbon\nbondir\nbonheur\nbonifier\nbonus\nbordure\nborne\nbotte\nboucle\nboueux\nbougie\nboulon\nbouquin\nbourse\nboussole\nboutique\nboxeur\nbranche\nbrasier\nbrave\nbrebis\nbrèche\nbreuvage\nbricoler\nbrigade\nbrillant\nbrioche\nbrique\nbrochure\nbroder\nbronzer\nbrousse\nbroyeur\nbrume\nbrusque\nbrutal\nbruyant\nbuffle\nbuisson\nbulletin\nbureau\nburin\nbustier\nbutiner\nbutoir\nbuvable\nbuvette\ncabanon\ncabine\ncachette\ncadeau\ncadre\ncaféine\ncaillou\ncaisson\ncalculer\ncalepin\ncalibre\ncalmer\ncalomnie\ncalvaire\ncamarade\ncaméra\ncamion\ncampagne\ncanal\ncaneton\ncanon\ncantine\ncanular\ncapable\ncaporal\ncaprice\ncapsule\ncapter\ncapuche\ncarabine\ncarbone\ncaresser\ncaribou\ncarnage\ncarotte\ncarreau\ncarton\ncascade\ncasier\ncasque\ncassure\ncauser\ncaution\ncavalier\ncaverne\ncaviar\ncédille\nceinture\ncéleste\ncellule\ncendrier\ncensurer\ncentral\ncercle\ncérébral\ncerise\ncerner\ncerveau\ncesser\nchagrin\nchaise\nchaleur\nchambre\nchance\nchapitre\ncharbon\nchasseur\nchaton\nchausson\nchavirer\nchemise\nchenille\nchéquier\nchercher\ncheval\nchien\nchiffre\nchignon\nchimère\nchiot\nchlorure\nchocolat\nchoisir\nchose\nchouette\nchrome\nchute\ncigare\ncigogne\ncimenter\ncinéma\ncintrer\ncirculer\ncirer\ncirque\nciterne\ncitoyen\ncitron\ncivil\nclairon\nclameur\nclaquer\nclasse\nclavier\nclient\ncligner\nclimat\nclivage\ncloche\nclonage\ncloporte\ncobalt\ncobra\ncocasse\ncocotier\ncoder\ncodifier\ncoffre\ncogner\ncohésion\ncoiffer\ncoincer\ncolère\ncolibri\ncolline\ncolmater\ncolonel\ncombat\ncomédie\ncommande\ncompact\nconcert\nconduire\nconfier\ncongeler\nconnoter\nconsonne\ncontact\nconvexe\ncopain\ncopie\ncorail\ncorbeau\ncordage\ncorniche\ncorpus\ncorrect\ncortège\ncosmique\ncostume\ncoton\ncoude\ncoupure\ncourage\ncouteau\ncouvrir\ncoyote\ncrabe\ncrainte\ncravate\ncrayon\ncréature\ncréditer\ncrémeux\ncreuser\ncrevette\ncribler\ncrier\ncristal\ncritère\ncroire\ncroquer\ncrotale\ncrucial\ncruel\ncrypter\ncubique\ncueillir\ncuillère\ncuisine\ncuivre\nculminer\ncultiver\ncumuler\ncupide\ncuratif\ncurseur\ncyanure\ncycle\ncylindre\ncynique\ndaigner\ndamier\ndanger\ndanseur\ndauphin\ndébattre\ndébiter\ndéborder\ndébrider\ndébutant\ndécaler\ndécembre\ndéchirer\ndécider\ndéclarer\ndécorer\ndécrire\ndécupler\ndédale\ndéductif\ndéesse\ndéfensif\ndéfiler\ndéfrayer\ndégager\ndégivrer\ndéglutir\ndégrafer\ndéjeuner\ndélice\ndéloger\ndemander\ndemeurer\ndémolir\ndénicher\ndénouer\ndentelle\ndénuder\ndépart\ndépenser\ndéphaser\ndéplacer\ndéposer\ndéranger\ndérober\ndésastre\ndescente\ndésert\ndésigner\ndésobéir\ndessiner\ndestrier\ndétacher\ndétester\ndétourer\ndétresse\ndevancer\ndevenir\ndeviner\ndevoir\ndiable\ndialogue\ndiamant\ndicter\ndifférer\ndigérer\ndigital\ndigne\ndiluer\ndimanche\ndiminuer\ndioxyde\ndirectif\ndiriger\ndiscuter\ndisposer\ndissiper\ndistance\ndivertir\ndiviser\ndocile\ndocteur\ndogme\ndoigt\ndomaine\ndomicile\ndompter\ndonateur\ndonjon\ndonner\ndopamine\ndortoir\ndorure\ndosage\ndoseur\ndossier\ndotation\ndouanier\ndouble\ndouceur\ndouter\ndoyen\ndragon\ndraper\ndresser\ndribbler\ndroiture\nduperie\nduplexe\ndurable\ndurcir\ndynastie\néblouir\nécarter\nécharpe\néchelle\néclairer\néclipse\néclore\nécluse\nécole\néconomie\nécorce\nécouter\nécraser\nécrémer\nécrivain\nécrou\nécume\nécureuil\nédifier\néduquer\neffacer\neffectif\neffigie\neffort\neffrayer\neffusion\négaliser\négarer\néjecter\nélaborer\nélargir\nélectron\nélégant\néléphant\nélève\néligible\nélitisme\néloge\nélucider\néluder\nemballer\nembellir\nembryon\némeraude\némission\nemmener\némotion\némouvoir\nempereur\nemployer\nemporter\nemprise\némulsion\nencadrer\nenchère\nenclave\nencoche\nendiguer\nendosser\nendroit\nenduire\nénergie\nenfance\nenfermer\nenfouir\nengager\nengin\nenglober\nénigme\nenjamber\nenjeu\nenlever\nennemi\nennuyeux\nenrichir\nenrobage\nenseigne\nentasser\nentendre\nentier\nentourer\nentraver\nénumérer\nenvahir\nenviable\nenvoyer\nenzyme\néolien\népaissir\népargne\népatant\népaule\népicerie\népidémie\népier\népilogue\népine\népisode\népitaphe\népoque\népreuve\néprouver\népuisant\néquerre\néquipe\nériger\nérosion\nerreur\néruption\nescalier\nespadon\nespèce\nespiègle\nespoir\nesprit\nesquiver\nessayer\nessence\nessieu\nessorer\nestime\nestomac\nestrade\nétagère\nétaler\nétanche\nétatique\néteindre\nétendoir\néternel\néthanol\néthique\nethnie\nétirer\nétoffer\nétoile\nétonnant\nétourdir\nétrange\nétroit\nétude\neuphorie\névaluer\névasion\néventail\névidence\néviter\névolutif\névoquer\nexact\nexagérer\nexaucer\nexceller\nexcitant\nexclusif\nexcuse\nexécuter\nexemple\nexercer\nexhaler\nexhorter\nexigence\nexiler\nexister\nexotique\nexpédier\nexplorer\nexposer\nexprimer\nexquis\nextensif\nextraire\nexulter\nfable\nfabuleux\nfacette\nfacile\nfacture\nfaiblir\nfalaise\nfameux\nfamille\nfarceur\nfarfelu\nfarine\nfarouche\nfasciner\nfatal\nfatigue\nfaucon\nfautif\nfaveur\nfavori\nfébrile\nféconder\nfédérer\nfélin\nfemme\nfémur\nfendoir\nféodal\nfermer\nféroce\nferveur\nfestival\nfeuille\nfeutre\nfévrier\nfiasco\nficeler\nfictif\nfidèle\nfigure\nfilature\nfiletage\nfilière\nfilleul\nfilmer\nfilou\nfiltrer\nfinancer\nfinir\nfiole\nfirme\nfissure\nfixer\nflairer\nflamme\nflasque\nflatteur\nfléau\nflèche\nfleur\nflexion\nflocon\nflore\nfluctuer\nfluide\nfluvial\nfolie\nfonderie\nfongible\nfontaine\nforcer\nforgeron\nformuler\nfortune\nfossile\nfoudre\nfougère\nfouiller\nfoulure\nfourmi\nfragile\nfraise\nfranchir\nfrapper\nfrayeur\nfrégate\nfreiner\nfrelon\nfrémir\nfrénésie\nfrère\nfriable\nfriction\nfrisson\nfrivole\nfroid\nfromage\nfrontal\nfrotter\nfruit\nfugitif\nfuite\nfureur\nfurieux\nfurtif\nfusion\nfutur\ngagner\ngalaxie\ngalerie\ngambader\ngarantir\ngardien\ngarnir\ngarrigue\ngazelle\ngazon\ngéant\ngélatine\ngélule\ngendarme\ngénéral\ngénie\ngenou\ngentil\ngéologie\ngéomètre\ngéranium\ngerme\ngestuel\ngeyser\ngibier\ngicler\ngirafe\ngivre\nglace\nglaive\nglisser\nglobe\ngloire\nglorieux\ngolfeur\ngomme\ngonfler\ngorge\ngorille\ngoudron\ngouffre\ngoulot\ngoupille\ngourmand\ngoutte\ngraduel\ngraffiti\ngraine\ngrand\ngrappin\ngratuit\ngravir\ngrenat\ngriffure\ngriller\ngrimper\ngrogner\ngronder\ngrotte\ngroupe\ngruger\ngrutier\ngruyère\nguépard\nguerrier\nguide\nguimauve\nguitare\ngustatif\ngymnaste\ngyrostat\nhabitude\nhachoir\nhalte\nhameau\nhangar\nhanneton\nharicot\nharmonie\nharpon\nhasard\nhélium\nhématome\nherbe\nhérisson\nhermine\nhéron\nhésiter\nheureux\nhiberner\nhibou\nhilarant\nhistoire\nhiver\nhomard\nhommage\nhomogène\nhonneur\nhonorer\nhonteux\nhorde\nhorizon\nhorloge\nhormone\nhorrible\nhouleux\nhousse\nhublot\nhuileux\nhumain\nhumble\nhumide\nhumour\nhurler\nhydromel\nhygiène\nhymne\nhypnose\nidylle\nignorer\niguane\nillicite\nillusion\nimage\nimbiber\nimiter\nimmense\nimmobile\nimmuable\nimpact\nimpérial\nimplorer\nimposer\nimprimer\nimputer\nincarner\nincendie\nincident\nincliner\nincolore\nindexer\nindice\ninductif\ninédit\nineptie\ninexact\ninfini\ninfliger\ninformer\ninfusion\ningérer\ninhaler\ninhiber\ninjecter\ninjure\ninnocent\ninoculer\ninonder\ninscrire\ninsecte\ninsigne\ninsolite\ninspirer\ninstinct\ninsulter\nintact\nintense\nintime\nintrigue\nintuitif\ninutile\ninvasion\ninventer\ninviter\ninvoquer\nironique\nirradier\nirréel\nirriter\nisoler\nivoire\nivresse\njaguar\njaillir\njambe\njanvier\njardin\njauger\njaune\njavelot\njetable\njeton\njeudi\njeunesse\njoindre\njoncher\njongler\njoueur\njouissif\njournal\njovial\njoyau\njoyeux\njubiler\njugement\njunior\njupon\njuriste\njustice\njuteux\njuvénile\nkayak\nkimono\nkiosque\nlabel\nlabial\nlabourer\nlacérer\nlactose\nlagune\nlaine\nlaisser\nlaitier\nlambeau\nlamelle\nlampe\nlanceur\nlangage\nlanterne\nlapin\nlargeur\nlarme\nlaurier\nlavabo\nlavoir\nlecture\nlégal\nléger\nlégume\nlessive\nlettre\nlevier\nlexique\nlézard\nliasse\nlibérer\nlibre\nlicence\nlicorne\nliège\nlièvre\nligature\nligoter\nligue\nlimer\nlimite\nlimonade\nlimpide\nlinéaire\nlingot\nlionceau\nliquide\nlisière\nlister\nlithium\nlitige\nlittoral\nlivreur\nlogique\nlointain\nloisir\nlombric\nloterie\nlouer\nlourd\nloutre\nlouve\nloyal\nlubie\nlucide\nlucratif\nlueur\nlugubre\nluisant\nlumière\nlunaire\nlundi\nluron\nlutter\nluxueux\nmachine\nmagasin\nmagenta\nmagique\nmaigre\nmaillon\nmaintien\nmairie\nmaison\nmajorer\nmalaxer\nmaléfice\nmalheur\nmalice\nmallette\nmammouth\nmandater\nmaniable\nmanquant\nmanteau\nmanuel\nmarathon\nmarbre\nmarchand\nmardi\nmaritime\nmarqueur\nmarron\nmarteler\nmascotte\nmassif\nmatériel\nmatière\nmatraque\nmaudire\nmaussade\nmauve\nmaximal\nméchant\nméconnu\nmédaille\nmédecin\nméditer\nméduse\nmeilleur\nmélange\nmélodie\nmembre\nmémoire\nmenacer\nmener\nmenhir\nmensonge\nmentor\nmercredi\nmérite\nmerle\nmessager\nmesure\nmétal\nmétéore\nméthode\nmétier\nmeuble\nmiauler\nmicrobe\nmiette\nmignon\nmigrer\nmilieu\nmillion\nmimique\nmince\nminéral\nminimal\nminorer\nminute\nmiracle\nmiroiter\nmissile\nmixte\nmobile\nmoderne\nmoelleux\nmondial\nmoniteur\nmonnaie\nmonotone\nmonstre\nmontagne\nmonument\nmoqueur\nmorceau\nmorsure\nmortier\nmoteur\nmotif\nmouche\nmoufle\nmoulin\nmousson\nmouton\nmouvant\nmultiple\nmunition\nmuraille\nmurène\nmurmure\nmuscle\nmuséum\nmusicien\nmutation\nmuter\nmutuel\nmyriade\nmyrtille\nmystère\nmythique\nnageur\nnappe\nnarquois\nnarrer\nnatation\nnation\nnature\nnaufrage\nnautique\nnavire\nnébuleux\nnectar\nnéfaste\nnégation\nnégliger\nnégocier\nneige\nnerveux\nnettoyer\nneurone\nneutron\nneveu\nniche\nnickel\nnitrate\nniveau\nnoble\nnocif\nnocturne\nnoirceur\nnoisette\nnomade\nnombreux\nnommer\nnormatif\nnotable\nnotifier\nnotoire\nnourrir\nnouveau\nnovateur\nnovembre\nnovice\nnuage\nnuancer\nnuire\nnuisible\nnuméro\nnuptial\nnuque\nnutritif\nobéir\nobjectif\nobliger\nobscur\nobserver\nobstacle\nobtenir\nobturer\noccasion\noccuper\nocéan\noctobre\noctroyer\noctupler\noculaire\nodeur\nodorant\noffenser\nofficier\noffrir\nogive\noiseau\noisillon\nolfactif\nolivier\nombrage\nomettre\nonctueux\nonduler\nonéreux\nonirique\nopale\nopaque\nopérer\nopinion\nopportun\nopprimer\nopter\noptique\norageux\norange\norbite\nordonner\noreille\norgane\norgueil\norifice\nornement\norque\nortie\nosciller\nosmose\nossature\notarie\nouragan\nourson\noutil\noutrager\nouvrage\novation\noxyde\noxygène\nozone\npaisible\npalace\npalmarès\npalourde\npalper\npanache\npanda\npangolin\npaniquer\npanneau\npanorama\npantalon\npapaye\npapier\npapoter\npapyrus\nparadoxe\nparcelle\nparesse\nparfumer\nparler\nparole\nparrain\nparsemer\npartager\nparure\nparvenir\npassion\npastèque\npaternel\npatience\npatron\npavillon\npavoiser\npayer\npaysage\npeigne\npeintre\npelage\npélican\npelle\npelouse\npeluche\npendule\npénétrer\npénible\npensif\npénurie\npépite\npéplum\nperdrix\nperforer\npériode\npermuter\nperplexe\npersil\nperte\npeser\npétale\npetit\npétrir\npeuple\npharaon\nphobie\nphoque\nphoton\nphrase\nphysique\npiano\npictural\npièce\npierre\npieuvre\npilote\npinceau\npipette\npiquer\npirogue\npiscine\npiston\npivoter\npixel\npizza\nplacard\nplafond\nplaisir\nplaner\nplaque\nplastron\nplateau\npleurer\nplexus\npliage\nplomb\nplonger\npluie\nplumage\npochette\npoésie\npoète\npointe\npoirier\npoisson\npoivre\npolaire\npolicier\npollen\npolygone\npommade\npompier\nponctuel\npondérer\nponey\nportique\nposition\nposséder\nposture\npotager\npoteau\npotion\npouce\npoulain\npoumon\npourpre\npoussin\npouvoir\nprairie\npratique\nprécieux\nprédire\npréfixe\nprélude\nprénom\nprésence\nprétexte\nprévoir\nprimitif\nprince\nprison\npriver\nproblème\nprocéder\nprodige\nprofond\nprogrès\nproie\nprojeter\nprologue\npromener\npropre\nprospère\nprotéger\nprouesse\nproverbe\nprudence\npruneau\npsychose\npublic\npuceron\npuiser\npulpe\npulsar\npunaise\npunitif\npupitre\npurifier\npuzzle\npyramide\nquasar\nquerelle\nquestion\nquiétude\nquitter\nquotient\nracine\nraconter\nradieux\nragondin\nraideur\nraisin\nralentir\nrallonge\nramasser\nrapide\nrasage\nratisser\nravager\nravin\nrayonner\nréactif\nréagir\nréaliser\nréanimer\nrecevoir\nréciter\nréclamer\nrécolter\nrecruter\nreculer\nrecycler\nrédiger\nredouter\nrefaire\nréflexe\nréformer\nrefrain\nrefuge\nrégalien\nrégion\nréglage\nrégulier\nréitérer\nrejeter\nrejouer\nrelatif\nrelever\nrelief\nremarque\nremède\nremise\nremonter\nremplir\nremuer\nrenard\nrenfort\nrenifler\nrenoncer\nrentrer\nrenvoi\nreplier\nreporter\nreprise\nreptile\nrequin\nréserve\nrésineux\nrésoudre\nrespect\nrester\nrésultat\nrétablir\nretenir\nréticule\nretomber\nretracer\nréunion\nréussir\nrevanche\nrevivre\nrévolte\nrévulsif\nrichesse\nrideau\nrieur\nrigide\nrigoler\nrincer\nriposter\nrisible\nrisque\nrituel\nrival\nrivière\nrocheux\nromance\nrompre\nronce\nrondin\nroseau\nrosier\nrotatif\nrotor\nrotule\nrouge\nrouille\nrouleau\nroutine\nroyaume\nruban\nrubis\nruche\nruelle\nrugueux\nruiner\nruisseau\nruser\nrustique\nrythme\nsabler\nsaboter\nsabre\nsacoche\nsafari\nsagesse\nsaisir\nsalade\nsalive\nsalon\nsaluer\nsamedi\nsanction\nsanglier\nsarcasme\nsardine\nsaturer\nsaugrenu\nsaumon\nsauter\nsauvage\nsavant\nsavonner\nscalpel\nscandale\nscélérat\nscénario\nsceptre\nschéma\nscience\nscinder\nscore\nscrutin\nsculpter\nséance\nsécable\nsécher\nsecouer\nsécréter\nsédatif\nséduire\nseigneur\nséjour\nsélectif\nsemaine\nsembler\nsemence\nséminal\nsénateur\nsensible\nsentence\nséparer\nséquence\nserein\nsergent\nsérieux\nserrure\nsérum\nservice\nsésame\nsévir\nsevrage\nsextuple\nsidéral\nsiècle\nsiéger\nsiffler\nsigle\nsignal\nsilence\nsilicium\nsimple\nsincère\nsinistre\nsiphon\nsirop\nsismique\nsituer\nskier\nsocial\nsocle\nsodium\nsoigneux\nsoldat\nsoleil\nsolitude\nsoluble\nsombre\nsommeil\nsomnoler\nsonde\nsongeur\nsonnette\nsonore\nsorcier\nsortir\nsosie\nsottise\nsoucieux\nsoudure\nsouffle\nsoulever\nsoupape\nsource\nsoutirer\nsouvenir\nspacieux\nspatial\nspécial\nsphère\nspiral\nstable\nstation\nsternum\nstimulus\nstipuler\nstrict\nstudieux\nstupeur\nstyliste\nsublime\nsubstrat\nsubtil\nsubvenir\nsuccès\nsucre\nsuffixe\nsuggérer\nsuiveur\nsulfate\nsuperbe\nsupplier\nsurface\nsuricate\nsurmener\nsurprise\nsursaut\nsurvie\nsuspect\nsyllabe\nsymbole\nsymétrie\nsynapse\nsyntaxe\nsystème\ntabac\ntablier\ntactile\ntailler\ntalent\ntalisman\ntalonner\ntambour\ntamiser\ntangible\ntapis\ntaquiner\ntarder\ntarif\ntartine\ntasse\ntatami\ntatouage\ntaupe\ntaureau\ntaxer\ntémoin\ntemporel\ntenaille\ntendre\nteneur\ntenir\ntension\nterminer\nterne\nterrible\ntétine\ntexte\nthème\nthéorie\nthérapie\nthorax\ntibia\ntiède\ntimide\ntirelire\ntiroir\ntissu\ntitane\ntitre\ntituber\ntoboggan\ntolérant\ntomate\ntonique\ntonneau\ntoponyme\ntorche\ntordre\ntornade\ntorpille\ntorrent\ntorse\ntortue\ntotem\ntoucher\ntournage\ntousser\ntoxine\ntraction\ntrafic\ntragique\ntrahir\ntrain\ntrancher\ntravail\ntrèfle\ntremper\ntrésor\ntreuil\ntriage\ntribunal\ntricoter\ntrilogie\ntriomphe\ntripler\ntriturer\ntrivial\ntrombone\ntronc\ntropical\ntroupeau\ntuile\ntulipe\ntumulte\ntunnel\nturbine\ntuteur\ntutoyer\ntuyau\ntympan\ntyphon\ntypique\ntyran\nubuesque\nultime\nultrason\nunanime\nunifier\nunion\nunique\nunitaire\nunivers\nuranium\nurbain\nurticant\nusage\nusine\nusuel\nusure\nutile\nutopie\nvacarme\nvaccin\nvagabond\nvague\nvaillant\nvaincre\nvaisseau\nvalable\nvalise\nvallon\nvalve\nvampire\nvanille\nvapeur\nvarier\nvaseux\nvassal\nvaste\nvecteur\nvedette\nvégétal\nvéhicule\nveinard\nvéloce\nvendredi\nvénérer\nvenger\nvenimeux\nventouse\nverdure\nvérin\nvernir\nverrou\nverser\nvertu\nveston\nvétéran\nvétuste\nvexant\nvexer\nviaduc\nviande\nvictoire\nvidange\nvidéo\nvignette\nvigueur\nvilain\nvillage\nvinaigre\nviolon\nvipère\nvirement\nvirtuose\nvirus\nvisage\nviseur\nvision\nvisqueux\nvisuel\nvital\nvitesse\nviticole\nvitrine\nvivace\nvivipare\nvocation\nvoguer\nvoile\nvoisin\nvoiture\nvolaille\nvolcan\nvoltiger\nvolume\nvorace\nvortex\nvoter\nvouloir\nvoyage\nvoyelle\nwagon\nxénon\nyacht\nzèbre\nzénith\nzeste\nzoologie" + ); + dico.Add( + "portuguese_brazil", + "abacate\nabaixo\nabalar\nabater\nabduzir\nabelha\naberto\nabismo\nabotoar\nabranger\nabreviar\nabrigar\nabrupto\nabsinto\nabsoluto\nabsurdo\nabutre\nacabado\nacalmar\nacampar\nacanhar\nacaso\naceitar\nacelerar\nacenar\nacervo\nacessar\nacetona\nachatar\nacidez\nacima\nacionado\nacirrar\naclamar\naclive\nacolhida\nacomodar\nacoplar\nacordar\nacumular\nacusador\nadaptar\nadega\nadentro\nadepto\nadequar\naderente\nadesivo\nadeus\nadiante\naditivo\nadjetivo\nadjunto\nadmirar\nadorar\nadquirir\nadubo\nadverso\nadvogado\naeronave\nafastar\naferir\nafetivo\nafinador\nafivelar\naflito\nafluente\nafrontar\nagachar\nagarrar\nagasalho\nagenciar\nagilizar\nagiota\nagitado\nagora\nagradar\nagreste\nagrupar\naguardar\nagulha\najoelhar\najudar\najustar\nalameda\nalarme\nalastrar\nalavanca\nalbergue\nalbino\nalcatra\naldeia\nalecrim\nalegria\nalertar\nalface\nalfinete\nalgum\nalheio\naliar\nalicate\nalienar\nalinhar\naliviar\nalmofada\nalocar\nalpiste\nalterar\naltitude\nalucinar\nalugar\naluno\nalusivo\nalvo\namaciar\namador\namarelo\namassar\nambas\nambiente\nameixa\namenizar\namido\namistoso\namizade\namolador\namontoar\namoroso\namostra\namparar\nampliar\nampola\nanagrama\nanalisar\nanarquia\nanatomia\nandaime\nanel\nanexo\nangular\nanimar\nanjo\nanomalia\nanotado\nansioso\nanterior\nanuidade\nanunciar\nanzol\napagador\napalpar\napanhado\napego\napelido\napertada\napesar\napetite\napito\naplauso\naplicada\napoio\napontar\naposta\naprendiz\naprovar\naquecer\narame\naranha\narara\narcada\nardente\nareia\narejar\narenito\naresta\nargiloso\nargola\narma\narquivo\narraial\narrebate\narriscar\narroba\narrumar\narsenal\narterial\nartigo\narvoredo\nasfaltar\nasilado\naspirar\nassador\nassinar\nassoalho\nassunto\nastral\natacado\natadura\natalho\natarefar\natear\natender\naterro\nateu\natingir\natirador\nativo\natoleiro\natracar\natrevido\natriz\natual\natum\nauditor\naumentar\naura\naurora\nautismo\nautoria\nautuar\navaliar\navante\navaria\navental\navesso\naviador\navisar\navulso\naxila\nazarar\nazedo\nazeite\nazulejo\nbabar\nbabosa\nbacalhau\nbacharel\nbacia\nbagagem\nbaiano\nbailar\nbaioneta\nbairro\nbaixista\nbajular\nbaleia\nbaliza\nbalsa\nbanal\nbandeira\nbanho\nbanir\nbanquete\nbarato\nbarbado\nbaronesa\nbarraca\nbarulho\nbaseado\nbastante\nbatata\nbatedor\nbatida\nbatom\nbatucar\nbaunilha\nbeber\nbeijo\nbeirada\nbeisebol\nbeldade\nbeleza\nbelga\nbeliscar\nbendito\nbengala\nbenzer\nberimbau\nberlinda\nberro\nbesouro\nbexiga\nbezerro\nbico\nbicudo\nbienal\nbifocal\nbifurcar\nbigorna\nbilhete\nbimestre\nbimotor\nbiologia\nbiombo\nbiosfera\nbipolar\nbirrento\nbiscoito\nbisneto\nbispo\nbissexto\nbitola\nbizarro\nblindado\nbloco\nbloquear\nboato\nbobagem\nbocado\nbocejo\nbochecha\nboicotar\nbolada\nboletim\nbolha\nbolo\nbombeiro\nbonde\nboneco\nbonita\nborbulha\nborda\nboreal\nborracha\nbovino\nboxeador\nbranco\nbrasa\nbraveza\nbreu\nbriga\nbrilho\nbrincar\nbroa\nbrochura\nbronzear\nbroto\nbruxo\nbucha\nbudismo\nbufar\nbule\nburaco\nbusca\nbusto\nbuzina\ncabana\ncabelo\ncabide\ncabo\ncabrito\ncacau\ncacetada\ncachorro\ncacique\ncadastro\ncadeado\ncafezal\ncaiaque\ncaipira\ncaixote\ncajado\ncaju\ncalafrio\ncalcular\ncaldeira\ncalibrar\ncalmante\ncalota\ncamada\ncambista\ncamisa\ncamomila\ncampanha\ncamuflar\ncanavial\ncancelar\ncaneta\ncanguru\ncanhoto\ncanivete\ncanoa\ncansado\ncantar\ncanudo\ncapacho\ncapela\ncapinar\ncapotar\ncapricho\ncaptador\ncapuz\ncaracol\ncarbono\ncardeal\ncareca\ncarimbar\ncarneiro\ncarpete\ncarreira\ncartaz\ncarvalho\ncasaco\ncasca\ncasebre\ncastelo\ncasulo\ncatarata\ncativar\ncaule\ncausador\ncautelar\ncavalo\ncaverna\ncebola\ncedilha\ncegonha\ncelebrar\ncelular\ncenoura\ncenso\ncenteio\ncercar\ncerrado\ncerteiro\ncerveja\ncetim\ncevada\nchacota\nchaleira\nchamado\nchapada\ncharme\nchatice\nchave\nchefe\nchegada\ncheiro\ncheque\nchicote\nchifre\nchinelo\nchocalho\nchover\nchumbo\nchutar\nchuva\ncicatriz\nciclone\ncidade\ncidreira\nciente\ncigana\ncimento\ncinto\ncinza\nciranda\ncircuito\ncirurgia\ncitar\nclareza\nclero\nclicar\nclone\nclube\ncoado\ncoagir\ncobaia\ncobertor\ncobrar\ncocada\ncoelho\ncoentro\ncoeso\ncogumelo\ncoibir\ncoifa\ncoiote\ncolar\ncoleira\ncolher\ncolidir\ncolmeia\ncolono\ncoluna\ncomando\ncombinar\ncomentar\ncomitiva\ncomover\ncomplexo\ncomum\nconcha\ncondor\nconectar\nconfuso\ncongelar\nconhecer\nconjugar\nconsumir\ncontrato\nconvite\ncooperar\ncopeiro\ncopiador\ncopo\ncoquetel\ncoragem\ncordial\ncorneta\ncoronha\ncorporal\ncorreio\ncortejo\ncoruja\ncorvo\ncosseno\ncostela\ncotonete\ncouro\ncouve\ncovil\ncozinha\ncratera\ncravo\ncreche\ncredor\ncreme\ncrer\ncrespo\ncriada\ncriminal\ncrioulo\ncrise\ncriticar\ncrosta\ncrua\ncruzeiro\ncubano\ncueca\ncuidado\ncujo\nculatra\nculminar\nculpar\ncultura\ncumprir\ncunhado\ncupido\ncurativo\ncurral\ncursar\ncurto\ncuspir\ncustear\ncutelo\ndamasco\ndatar\ndebater\ndebitar\ndeboche\ndebulhar\ndecalque\ndecimal\ndeclive\ndecote\ndecretar\ndedal\ndedicado\ndeduzir\ndefesa\ndefumar\ndegelo\ndegrau\ndegustar\ndeitado\ndeixar\ndelator\ndelegado\ndelinear\ndelonga\ndemanda\ndemitir\ndemolido\ndentista\ndepenado\ndepilar\ndepois\ndepressa\ndepurar\nderiva\nderramar\ndesafio\ndesbotar\ndescanso\ndesenho\ndesfiado\ndesgaste\ndesigual\ndeslize\ndesmamar\ndesova\ndespesa\ndestaque\ndesviar\ndetalhar\ndetentor\ndetonar\ndetrito\ndeusa\ndever\ndevido\ndevotado\ndezena\ndiagrama\ndialeto\ndidata\ndifuso\ndigitar\ndilatado\ndiluente\ndiminuir\ndinastia\ndinheiro\ndiocese\ndireto\ndiscreta\ndisfarce\ndisparo\ndisquete\ndissipar\ndistante\nditador\ndiurno\ndiverso\ndivisor\ndivulgar\ndizer\ndobrador\ndolorido\ndomador\ndominado\ndonativo\ndonzela\ndormente\ndorsal\ndosagem\ndourado\ndoutor\ndrenagem\ndrible\ndrogaria\nduelar\nduende\ndueto\nduplo\nduquesa\ndurante\nduvidoso\neclodir\necoar\necologia\nedificar\nedital\neducado\nefeito\nefetivar\nejetar\nelaborar\neleger\neleitor\nelenco\nelevador\neliminar\nelogiar\nembargo\nembolado\nembrulho\nembutido\nemenda\nemergir\nemissor\nempatia\nempenho\nempinado\nempolgar\nemprego\nempurrar\nemulador\nencaixe\nencenado\nenchente\nencontro\nendeusar\nendossar\nenfaixar\nenfeite\nenfim\nengajado\nengenho\nenglobar\nengomado\nengraxar\nenguia\nenjoar\nenlatar\nenquanto\nenraizar\nenrolado\nenrugar\nensaio\nenseada\nensino\nensopado\nentanto\nenteado\nentidade\nentortar\nentrada\nentulho\nenvergar\nenviado\nenvolver\nenxame\nenxerto\nenxofre\nenxuto\nepiderme\nequipar\nereto\nerguido\nerrata\nerva\nervilha\nesbanjar\nesbelto\nescama\nescola\nescrita\nescuta\nesfinge\nesfolar\nesfregar\nesfumado\nesgrima\nesmalte\nespanto\nespelho\nespiga\nesponja\nespreita\nespumar\nesquerda\nestaca\nesteira\nesticar\nestofado\nestrela\nestudo\nesvaziar\netanol\netiqueta\neuforia\neuropeu\nevacuar\nevaporar\nevasivo\neventual\nevidente\nevoluir\nexagero\nexalar\nexaminar\nexato\nexausto\nexcesso\nexcitar\nexclamar\nexecutar\nexemplo\nexibir\nexigente\nexonerar\nexpandir\nexpelir\nexpirar\nexplanar\nexposto\nexpresso\nexpulsar\nexterno\nextinto\nextrato\nfabricar\nfabuloso\nfaceta\nfacial\nfada\nfadiga\nfaixa\nfalar\nfalta\nfamiliar\nfandango\nfanfarra\nfantoche\nfardado\nfarelo\nfarinha\nfarofa\nfarpa\nfartura\nfatia\nfator\nfavorita\nfaxina\nfazenda\nfechado\nfeijoada\nfeirante\nfelino\nfeminino\nfenda\nfeno\nfera\nferiado\nferrugem\nferver\nfestejar\nfetal\nfeudal\nfiapo\nfibrose\nficar\nficheiro\nfigurado\nfileira\nfilho\nfilme\nfiltrar\nfirmeza\nfisgada\nfissura\nfita\nfivela\nfixador\nfixo\nflacidez\nflamingo\nflanela\nflechada\nflora\nflutuar\nfluxo\nfocal\nfocinho\nfofocar\nfogo\nfoguete\nfoice\nfolgado\nfolheto\nforjar\nformiga\nforno\nforte\nfosco\nfossa\nfragata\nfralda\nfrango\nfrasco\nfraterno\nfreira\nfrente\nfretar\nfrieza\nfriso\nfritura\nfronha\nfrustrar\nfruteira\nfugir\nfulano\nfuligem\nfundar\nfungo\nfunil\nfurador\nfurioso\nfutebol\ngabarito\ngabinete\ngado\ngaiato\ngaiola\ngaivota\ngalega\ngalho\ngalinha\ngalocha\nganhar\ngaragem\ngarfo\ngargalo\ngarimpo\ngaroupa\ngarrafa\ngasoduto\ngasto\ngata\ngatilho\ngaveta\ngazela\ngelado\ngeleia\ngelo\ngemada\ngemer\ngemido\ngeneroso\ngengiva\ngenial\ngenoma\ngenro\ngeologia\ngerador\ngerminar\ngesso\ngestor\nginasta\ngincana\ngingado\ngirafa\ngirino\nglacial\nglicose\nglobal\nglorioso\ngoela\ngoiaba\ngolfe\ngolpear\ngordura\ngorjeta\ngorro\ngostoso\ngoteira\ngovernar\ngracejo\ngradual\ngrafite\ngralha\ngrampo\ngranada\ngratuito\ngraveto\ngraxa\ngrego\ngrelhar\ngreve\ngrilo\ngrisalho\ngritaria\ngrosso\ngrotesco\ngrudado\ngrunhido\ngruta\nguache\nguarani\nguaxinim\nguerrear\nguiar\nguincho\nguisado\ngula\nguloso\nguru\nhabitar\nharmonia\nhaste\nhaver\nhectare\nherdar\nheresia\nhesitar\nhiato\nhibernar\nhidratar\nhiena\nhino\nhipismo\nhipnose\nhipoteca\nhoje\nholofote\nhomem\nhonesto\nhonrado\nhormonal\nhospedar\nhumorado\niate\nideia\nidoso\nignorado\nigreja\niguana\nileso\nilha\niludido\niluminar\nilustrar\nimagem\nimediato\nimenso\nimersivo\niminente\nimitador\nimortal\nimpacto\nimpedir\nimplante\nimpor\nimprensa\nimpune\nimunizar\ninalador\ninapto\ninativo\nincenso\ninchar\nincidir\nincluir\nincolor\nindeciso\nindireto\nindutor\nineficaz\ninerente\ninfantil\ninfestar\ninfinito\ninflamar\ninformal\ninfrator\ningerir\ninibido\ninicial\ninimigo\ninjetar\ninocente\ninodoro\ninovador\ninox\ninquieto\ninscrito\ninseto\ninsistir\ninspetor\ninstalar\ninsulto\nintacto\nintegral\nintimar\nintocado\nintriga\ninvasor\ninverno\ninvicto\ninvocar\niogurte\niraniano\nironizar\nirreal\nirritado\nisca\nisento\nisolado\nisqueiro\nitaliano\njaneiro\njangada\njanta\njararaca\njardim\njarro\njasmim\njato\njavali\njazida\njejum\njoaninha\njoelhada\njogador\njoia\njornal\njorrar\njovem\njuba\njudeu\njudoca\njuiz\njulgador\njulho\njurado\njurista\njuro\njusta\nlabareda\nlaboral\nlacre\nlactante\nladrilho\nlagarta\nlagoa\nlaje\nlamber\nlamentar\nlaminar\nlampejo\nlanche\nlapidar\nlapso\nlaranja\nlareira\nlargura\nlasanha\nlastro\nlateral\nlatido\nlavanda\nlavoura\nlavrador\nlaxante\nlazer\nlealdade\nlebre\nlegado\nlegendar\nlegista\nleigo\nleiloar\nleitura\nlembrete\nleme\nlenhador\nlentilha\nleoa\nlesma\nleste\nletivo\nletreiro\nlevar\nleveza\nlevitar\nliberal\nlibido\nliderar\nligar\nligeiro\nlimitar\nlimoeiro\nlimpador\nlinda\nlinear\nlinhagem\nliquidez\nlistagem\nlisura\nlitoral\nlivro\nlixa\nlixeira\nlocador\nlocutor\nlojista\nlombo\nlona\nlonge\nlontra\nlorde\nlotado\nloteria\nloucura\nlousa\nlouvar\nluar\nlucidez\nlucro\nluneta\nlustre\nlutador\nluva\nmacaco\nmacete\nmachado\nmacio\nmadeira\nmadrinha\nmagnata\nmagreza\nmaior\nmais\nmalandro\nmalha\nmalote\nmaluco\nmamilo\nmamoeiro\nmamute\nmanada\nmancha\nmandato\nmanequim\nmanhoso\nmanivela\nmanobrar\nmansa\nmanter\nmanusear\nmapeado\nmaquinar\nmarcador\nmaresia\nmarfim\nmargem\nmarinho\nmarmita\nmaroto\nmarquise\nmarreco\nmartelo\nmarujo\nmascote\nmasmorra\nmassagem\nmastigar\nmatagal\nmaterno\nmatinal\nmatutar\nmaxilar\nmedalha\nmedida\nmedusa\nmegafone\nmeiga\nmelancia\nmelhor\nmembro\nmemorial\nmenino\nmenos\nmensagem\nmental\nmerecer\nmergulho\nmesada\nmesclar\nmesmo\nmesquita\nmestre\nmetade\nmeteoro\nmetragem\nmexer\nmexicano\nmicro\nmigalha\nmigrar\nmilagre\nmilenar\nmilhar\nmimado\nminerar\nminhoca\nministro\nminoria\nmiolo\nmirante\nmirtilo\nmisturar\nmocidade\nmoderno\nmodular\nmoeda\nmoer\nmoinho\nmoita\nmoldura\nmoleza\nmolho\nmolinete\nmolusco\nmontanha\nmoqueca\nmorango\nmorcego\nmordomo\nmorena\nmosaico\nmosquete\nmostarda\nmotel\nmotim\nmoto\nmotriz\nmuda\nmuito\nmulata\nmulher\nmultar\nmundial\nmunido\nmuralha\nmurcho\nmuscular\nmuseu\nmusical\nnacional\nnadador\nnaja\nnamoro\nnarina\nnarrado\nnascer\nnativa\nnatureza\nnavalha\nnavegar\nnavio\nneblina\nnebuloso\nnegativa\nnegociar\nnegrito\nnervoso\nneta\nneural\nnevasca\nnevoeiro\nninar\nninho\nnitidez\nnivelar\nnobreza\nnoite\nnoiva\nnomear\nnominal\nnordeste\nnortear\nnotar\nnoticiar\nnoturno\nnovelo\nnovilho\nnovo\nnublado\nnudez\nnumeral\nnupcial\nnutrir\nnuvem\nobcecado\nobedecer\nobjetivo\nobrigado\nobscuro\nobstetra\nobter\nobturar\nocidente\nocioso\nocorrer\noculista\nocupado\nofegante\nofensiva\noferenda\noficina\nofuscado\nogiva\nolaria\noleoso\nolhar\noliveira\nombro\nomelete\nomisso\nomitir\nondulado\noneroso\nontem\nopcional\noperador\noponente\noportuno\noposto\norar\norbitar\nordem\nordinal\norfanato\norgasmo\norgulho\noriental\norigem\noriundo\norla\nortodoxo\norvalho\noscilar\nossada\nosso\nostentar\notimismo\nousadia\noutono\noutubro\nouvido\novelha\novular\noxidar\noxigenar\npacato\npaciente\npacote\npactuar\npadaria\npadrinho\npagar\npagode\npainel\npairar\npaisagem\npalavra\npalestra\npalheta\npalito\npalmada\npalpitar\npancada\npanela\npanfleto\npanqueca\npantanal\npapagaio\npapelada\npapiro\nparafina\nparcial\npardal\nparede\npartida\npasmo\npassado\npastel\npatamar\npatente\npatinar\npatrono\npaulada\npausar\npeculiar\npedalar\npedestre\npediatra\npedra\npegada\npeitoral\npeixe\npele\npelicano\npenca\npendurar\npeneira\npenhasco\npensador\npente\nperceber\nperfeito\npergunta\nperito\npermitir\nperna\nperplexo\npersiana\npertence\nperuca\npescado\npesquisa\npessoa\npetiscar\npiada\npicado\npiedade\npigmento\npilastra\npilhado\npilotar\npimenta\npincel\npinguim\npinha\npinote\npintar\npioneiro\npipoca\npiquete\npiranha\npires\npirueta\npiscar\npistola\npitanga\npivete\nplanta\nplaqueta\nplatina\nplebeu\nplumagem\npluvial\npneu\npoda\npoeira\npoetisa\npolegada\npoliciar\npoluente\npolvilho\npomar\npomba\nponderar\npontaria\npopuloso\nporta\npossuir\npostal\npote\npoupar\npouso\npovoar\npraia\nprancha\nprato\npraxe\nprece\npredador\nprefeito\npremiar\nprensar\npreparar\npresilha\npretexto\nprevenir\nprezar\nprimata\nprincesa\nprisma\nprivado\nprocesso\nproduto\nprofeta\nproibido\nprojeto\nprometer\npropagar\nprosa\nprotetor\nprovador\npublicar\npudim\npular\npulmonar\npulseira\npunhal\npunir\npupilo\npureza\npuxador\nquadra\nquantia\nquarto\nquase\nquebrar\nqueda\nqueijo\nquente\nquerido\nquimono\nquina\nquiosque\nrabanada\nrabisco\nrachar\nracionar\nradial\nraiar\nrainha\nraio\nraiva\nrajada\nralado\nramal\nranger\nranhura\nrapadura\nrapel\nrapidez\nraposa\nraquete\nraridade\nrasante\nrascunho\nrasgar\nraspador\nrasteira\nrasurar\nratazana\nratoeira\nrealeza\nreanimar\nreaver\nrebaixar\nrebelde\nrebolar\nrecado\nrecente\nrecheio\nrecibo\nrecordar\nrecrutar\nrecuar\nrede\nredimir\nredonda\nreduzida\nreenvio\nrefinar\nrefletir\nrefogar\nrefresco\nrefugiar\nregalia\nregime\nregra\nreinado\nreitor\nrejeitar\nrelativo\nremador\nremendo\nremorso\nrenovado\nreparo\nrepelir\nrepleto\nrepolho\nrepresa\nrepudiar\nrequerer\nresenha\nresfriar\nresgatar\nresidir\nresolver\nrespeito\nressaca\nrestante\nresumir\nretalho\nreter\nretirar\nretomada\nretratar\nrevelar\nrevisor\nrevolta\nriacho\nrica\nrigidez\nrigoroso\nrimar\nringue\nrisada\nrisco\nrisonho\nrobalo\nrochedo\nrodada\nrodeio\nrodovia\nroedor\nroleta\nromano\nroncar\nrosado\nroseira\nrosto\nrota\nroteiro\nrotina\nrotular\nrouco\nroupa\nroxo\nrubro\nrugido\nrugoso\nruivo\nrumo\nrupestre\nrusso\nsabor\nsaciar\nsacola\nsacudir\nsadio\nsafira\nsaga\nsagrada\nsaibro\nsalada\nsaleiro\nsalgado\nsaliva\nsalpicar\nsalsicha\nsaltar\nsalvador\nsambar\nsamurai\nsanar\nsanfona\nsangue\nsanidade\nsapato\nsarda\nsargento\nsarjeta\nsaturar\nsaudade\nsaxofone\nsazonal\nsecar\nsecular\nseda\nsedento\nsediado\nsedoso\nsedutor\nsegmento\nsegredo\nsegundo\nseiva\nseleto\nselvagem\nsemanal\nsemente\nsenador\nsenhor\nsensual\nsentado\nseparado\nsereia\nseringa\nserra\nservo\nsetembro\nsetor\nsigilo\nsilhueta\nsilicone\nsimetria\nsimpatia\nsimular\nsinal\nsincero\nsingular\nsinopse\nsintonia\nsirene\nsiri\nsituado\nsoberano\nsobra\nsocorro\nsogro\nsoja\nsolda\nsoletrar\nsolteiro\nsombrio\nsonata\nsondar\nsonegar\nsonhador\nsono\nsoprano\nsoquete\nsorrir\nsorteio\nsossego\nsotaque\nsoterrar\nsovado\nsozinho\nsuavizar\nsubida\nsubmerso\nsubsolo\nsubtrair\nsucata\nsucesso\nsuco\nsudeste\nsufixo\nsugador\nsugerir\nsujeito\nsulfato\nsumir\nsuor\nsuperior\nsuplicar\nsuposto\nsuprimir\nsurdina\nsurfista\nsurpresa\nsurreal\nsurtir\nsuspiro\nsustento\ntabela\ntablete\ntabuada\ntacho\ntagarela\ntalher\ntalo\ntalvez\ntamanho\ntamborim\ntampa\ntangente\ntanto\ntapar\ntapioca\ntardio\ntarefa\ntarja\ntarraxa\ntatuagem\ntaurino\ntaxativo\ntaxista\nteatral\ntecer\ntecido\nteclado\ntedioso\nteia\nteimar\ntelefone\ntelhado\ntempero\ntenente\ntensor\ntentar\ntermal\nterno\nterreno\ntese\ntesoura\ntestado\nteto\ntextura\ntexugo\ntiara\ntigela\ntijolo\ntimbrar\ntimidez\ntingido\ntinteiro\ntiragem\ntitular\ntoalha\ntocha\ntolerar\ntolice\ntomada\ntomilho\ntonel\ntontura\ntopete\ntora\ntorcido\ntorneio\ntorque\ntorrada\ntorto\ntostar\ntouca\ntoupeira\ntoxina\ntrabalho\ntracejar\ntradutor\ntrafegar\ntrajeto\ntrama\ntrancar\ntrapo\ntraseiro\ntratador\ntravar\ntreino\ntremer\ntrepidar\ntrevo\ntriagem\ntribo\ntriciclo\ntridente\ntrilogia\ntrindade\ntriplo\ntriturar\ntriunfal\ntrocar\ntrombeta\ntrova\ntrunfo\ntruque\ntubular\ntucano\ntudo\ntulipa\ntupi\nturbo\nturma\nturquesa\ntutelar\ntutorial\nuivar\numbigo\nunha\nunidade\nuniforme\nurologia\nurso\nurtiga\nurubu\nusado\nusina\nusufruir\nvacina\nvadiar\nvagaroso\nvaidoso\nvala\nvalente\nvalidade\nvalores\nvantagem\nvaqueiro\nvaranda\nvareta\nvarrer\nvascular\nvasilha\nvassoura\nvazar\nvazio\nveado\nvedar\nvegetar\nveicular\nveleiro\nvelhice\nveludo\nvencedor\nvendaval\nvenerar\nventre\nverbal\nverdade\nvereador\nvergonha\nvermelho\nverniz\nversar\nvertente\nvespa\nvestido\nvetorial\nviaduto\nviagem\nviajar\nviatura\nvibrador\nvideira\nvidraria\nviela\nviga\nvigente\nvigiar\nvigorar\nvilarejo\nvinco\nvinheta\nvinil\nvioleta\nvirada\nvirtude\nvisitar\nvisto\nvitral\nviveiro\nvizinho\nvoador\nvoar\nvogal\nvolante\nvoleibol\nvoltagem\nvolumoso\nvontade\nvulto\nvuvuzela\nxadrez\nxarope\nxeque\nxeretar\nxerife\nxingar\nzangado\nzarpar\nzebu\nzelador\nzombar\nzoologia\nzumbido" + ); + dico.Add( + "czech", + "abdikace\nabeceda\nadresa\nagrese\nakce\naktovka\nalej\nalkohol\namputace\nananas\nandulka\nanekdota\nanketa\nantika\nanulovat\narcha\narogance\nasfalt\nasistent\naspirace\nastma\nastronom\natlas\natletika\natol\nautobus\nazyl\nbabka\nbachor\nbacil\nbaculka\nbadatel\nbageta\nbagr\nbahno\nbakterie\nbalada\nbaletka\nbalkon\nbalonek\nbalvan\nbalza\nbambus\nbankomat\nbarbar\nbaret\nbarman\nbaroko\nbarva\nbaterka\nbatoh\nbavlna\nbazalka\nbazilika\nbazuka\nbedna\nberan\nbeseda\nbestie\nbeton\nbezinka\nbezmoc\nbeztak\nbicykl\nbidlo\nbiftek\nbikiny\nbilance\nbiograf\nbiolog\nbitva\nbizon\nblahobyt\nblatouch\nblecha\nbledule\nblesk\nblikat\nblizna\nblokovat\nbloudit\nblud\nbobek\nbobr\nbodlina\nbodnout\nbohatost\nbojkot\nbojovat\nbokorys\nbolest\nborec\nborovice\nbota\nboubel\nbouchat\nbouda\nboule\nbourat\nboxer\nbradavka\nbrambora\nbranka\nbratr\nbrepta\nbriketa\nbrko\nbrloh\nbronz\nbroskev\nbrunetka\nbrusinka\nbrzda\nbrzy\nbublina\nbubnovat\nbuchta\nbuditel\nbudka\nbudova\nbufet\nbujarost\nbukvice\nbuldok\nbulva\nbunda\nbunkr\nburza\nbutik\nbuvol\nbuzola\nbydlet\nbylina\nbytovka\nbzukot\ncapart\ncarevna\ncedr\ncedule\ncejch\ncejn\ncela\nceler\ncelkem\ncelnice\ncenina\ncennost\ncenovka\ncentrum\ncenzor\ncestopis\ncetka\nchalupa\nchapadlo\ncharita\nchata\nchechtat\nchemie\nchichot\nchirurg\nchlad\nchleba\nchlubit\nchmel\nchmura\nchobot\nchochol\nchodba\ncholera\nchomout\nchopit\nchoroba\nchov\nchrapot\nchrlit\nchrt\nchrup\nchtivost\nchudina\nchutnat\nchvat\nchvilka\nchvost\nchyba\nchystat\nchytit\ncibule\ncigareta\ncihelna\ncihla\ncinkot\ncirkus\ncisterna\ncitace\ncitrus\ncizinec\ncizost\nclona\ncokoliv\ncouvat\nctitel\nctnost\ncudnost\ncuketa\ncukr\ncupot\ncvaknout\ncval\ncvik\ncvrkot\ncyklista\ndaleko\ndareba\ndatel\ndatum\ndcera\ndebata\ndechovka\ndecibel\ndeficit\ndeflace\ndekl\ndekret\ndemokrat\ndeprese\nderby\ndeska\ndetektiv\ndikobraz\ndiktovat\ndioda\ndiplom\ndisk\ndisplej\ndivadlo\ndivoch\ndlaha\ndlouho\ndluhopis\ndnes\ndobro\ndobytek\ndocent\ndochutit\ndodnes\ndohled\ndohoda\ndohra\ndojem\ndojnice\ndoklad\ndokola\ndoktor\ndokument\ndolar\ndoleva\ndolina\ndoma\ndominant\ndomluvit\ndomov\ndonutit\ndopad\ndopis\ndoplnit\ndoposud\ndoprovod\ndopustit\ndorazit\ndorost\ndort\ndosah\ndoslov\ndostatek\ndosud\ndosyta\ndotaz\ndotek\ndotknout\ndoufat\ndoutnat\ndovozce\ndozadu\ndoznat\ndozorce\ndrahota\ndrak\ndramatik\ndravec\ndraze\ndrdol\ndrobnost\ndrogerie\ndrozd\ndrsnost\ndrtit\ndrzost\nduben\nduchovno\ndudek\nduha\nduhovka\ndusit\ndusno\ndutost\ndvojice\ndvorec\ndynamit\nekolog\nekonomie\nelektron\nelipsa\nemail\nemise\nemoce\nempatie\nepizoda\nepocha\nepopej\nepos\nesej\nesence\neskorta\neskymo\netiketa\neuforie\nevoluce\nexekuce\nexkurze\nexpedice\nexploze\nexport\nextrakt\nfacka\nfajfka\nfakulta\nfanatik\nfantazie\nfarmacie\nfavorit\nfazole\nfederace\nfejeton\nfenka\nfialka\nfigurant\nfilozof\nfiltr\nfinance\nfinta\nfixace\nfjord\nflanel\nflirt\nflotila\nfond\nfosfor\nfotbal\nfotka\nfoton\nfrakce\nfreska\nfronta\nfukar\nfunkce\nfyzika\ngaleje\ngarant\ngenetika\ngeolog\ngilotina\nglazura\nglejt\ngolem\ngolfista\ngotika\ngraf\ngramofon\ngranule\ngrep\ngril\ngrog\ngroteska\nguma\nhadice\nhadr\nhala\nhalenka\nhanba\nhanopis\nharfa\nharpuna\nhavran\nhebkost\nhejkal\nhejno\nhejtman\nhektar\nhelma\nhematom\nherec\nherna\nheslo\nhezky\nhistorik\nhladovka\nhlasivky\nhlava\nhledat\nhlen\nhlodavec\nhloh\nhloupost\nhltat\nhlubina\nhluchota\nhmat\nhmota\nhmyz\nhnis\nhnojivo\nhnout\nhoblina\nhoboj\nhoch\nhodiny\nhodlat\nhodnota\nhodovat\nhojnost\nhokej\nholinka\nholka\nholub\nhomole\nhonitba\nhonorace\nhoral\nhorda\nhorizont\nhorko\nhorlivec\nhormon\nhornina\nhoroskop\nhorstvo\nhospoda\nhostina\nhotovost\nhouba\nhouf\nhoupat\nhouska\nhovor\nhradba\nhranice\nhravost\nhrazda\nhrbolek\nhrdina\nhrdlo\nhrdost\nhrnek\nhrobka\nhromada\nhrot\nhrouda\nhrozen\nhrstka\nhrubost\nhryzat\nhubenost\nhubnout\nhudba\nhukot\nhumr\nhusita\nhustota\nhvozd\nhybnost\nhydrant\nhygiena\nhymna\nhysterik\nidylka\nihned\nikona\niluze\nimunita\ninfekce\ninflace\ninkaso\ninovace\ninspekce\ninternet\ninvalida\ninvestor\ninzerce\nironie\njablko\njachta\njahoda\njakmile\njakost\njalovec\njantar\njarmark\njaro\njasan\njasno\njatka\njavor\njazyk\njedinec\njedle\njednatel\njehlan\njekot\njelen\njelito\njemnost\njenom\njepice\njeseter\njevit\njezdec\njezero\njinak\njindy\njinoch\njiskra\njistota\njitrnice\njizva\njmenovat\njogurt\njurta\nkabaret\nkabel\nkabinet\nkachna\nkadet\nkadidlo\nkahan\nkajak\nkajuta\nkakao\nkaktus\nkalamita\nkalhoty\nkalibr\nkalnost\nkamera\nkamkoliv\nkamna\nkanibal\nkanoe\nkantor\nkapalina\nkapela\nkapitola\nkapka\nkaple\nkapota\nkapr\nkapusta\nkapybara\nkaramel\nkarotka\nkarton\nkasa\nkatalog\nkatedra\nkauce\nkauza\nkavalec\nkazajka\nkazeta\nkazivost\nkdekoliv\nkdesi\nkedluben\nkemp\nkeramika\nkino\nklacek\nkladivo\nklam\nklapot\nklasika\nklaun\nklec\nklenba\nklepat\nklesnout\nklid\nklima\nklisna\nklobouk\nklokan\nklopa\nkloub\nklubovna\nklusat\nkluzkost\nkmen\nkmitat\nkmotr\nkniha\nknot\nkoalice\nkoberec\nkobka\nkobliha\nkobyla\nkocour\nkohout\nkojenec\nkokos\nkoktejl\nkolaps\nkoleda\nkolize\nkolo\nkomando\nkometa\nkomik\nkomnata\nkomora\nkompas\nkomunita\nkonat\nkoncept\nkondice\nkonec\nkonfese\nkongres\nkonina\nkonkurs\nkontakt\nkonzerva\nkopanec\nkopie\nkopnout\nkoprovka\nkorbel\nkorektor\nkormidlo\nkoroptev\nkorpus\nkoruna\nkoryto\nkorzet\nkosatec\nkostka\nkotel\nkotleta\nkotoul\nkoukat\nkoupelna\nkousek\nkouzlo\nkovboj\nkoza\nkozoroh\nkrabice\nkrach\nkrajina\nkralovat\nkrasopis\nkravata\nkredit\nkrejcar\nkresba\nkreveta\nkriket\nkritik\nkrize\nkrkavec\nkrmelec\nkrmivo\nkrocan\nkrok\nkronika\nkropit\nkroupa\nkrovka\nkrtek\nkruhadlo\nkrupice\nkrutost\nkrvinka\nkrychle\nkrypta\nkrystal\nkryt\nkudlanka\nkufr\nkujnost\nkukla\nkulajda\nkulich\nkulka\nkulomet\nkultura\nkuna\nkupodivu\nkurt\nkurzor\nkutil\nkvalita\nkvasinka\nkvestor\nkynolog\nkyselina\nkytara\nkytice\nkytka\nkytovec\nkyvadlo\nlabrador\nlachtan\nladnost\nlaik\nlakomec\nlamela\nlampa\nlanovka\nlasice\nlaso\nlastura\nlatinka\nlavina\nlebka\nleckdy\nleden\nlednice\nledovka\nledvina\nlegenda\nlegie\nlegrace\nlehce\nlehkost\nlehnout\nlektvar\nlenochod\nlentilka\nlepenka\nlepidlo\nletadlo\nletec\nletmo\nletokruh\nlevhart\nlevitace\nlevobok\nlibra\nlichotka\nlidojed\nlidskost\nlihovina\nlijavec\nlilek\nlimetka\nlinie\nlinka\nlinoleum\nlistopad\nlitina\nlitovat\nlobista\nlodivod\nlogika\nlogoped\nlokalita\nloket\nlomcovat\nlopata\nlopuch\nlord\nlosos\nlotr\nloudal\nlouh\nlouka\nlouskat\nlovec\nlstivost\nlucerna\nlucifer\nlump\nlusk\nlustrace\nlvice\nlyra\nlyrika\nlysina\nmadam\nmadlo\nmagistr\nmahagon\nmajetek\nmajitel\nmajorita\nmakak\nmakovice\nmakrela\nmalba\nmalina\nmalovat\nmalvice\nmaminka\nmandle\nmanko\nmarnost\nmasakr\nmaskot\nmasopust\nmatice\nmatrika\nmaturita\nmazanec\nmazivo\nmazlit\nmazurka\nmdloba\nmechanik\nmeditace\nmedovina\nmelasa\nmeloun\nmentolka\nmetla\nmetoda\nmetr\nmezera\nmigrace\nmihnout\nmihule\nmikina\nmikrofon\nmilenec\nmilimetr\nmilost\nmimika\nmincovna\nminibar\nminomet\nminulost\nmiska\nmistr\nmixovat\nmladost\nmlha\nmlhovina\nmlok\nmlsat\nmluvit\nmnich\nmnohem\nmobil\nmocnost\nmodelka\nmodlitba\nmohyla\nmokro\nmolekula\nmomentka\nmonarcha\nmonokl\nmonstrum\nmontovat\nmonzun\nmosaz\nmoskyt\nmost\nmotivace\nmotorka\nmotyka\nmoucha\nmoudrost\nmozaika\nmozek\nmozol\nmramor\nmravenec\nmrkev\nmrtvola\nmrzet\nmrzutost\nmstitel\nmudrc\nmuflon\nmulat\nmumie\nmunice\nmuset\nmutace\nmuzeum\nmuzikant\nmyslivec\nmzda\nnabourat\nnachytat\nnadace\nnadbytek\nnadhoz\nnadobro\nnadpis\nnahlas\nnahnat\nnahodile\nnahradit\nnaivita\nnajednou\nnajisto\nnajmout\nnaklonit\nnakonec\nnakrmit\nnalevo\nnamazat\nnamluvit\nnanometr\nnaoko\nnaopak\nnaostro\nnapadat\nnapevno\nnaplnit\nnapnout\nnaposled\nnaprosto\nnarodit\nnaruby\nnarychlo\nnasadit\nnasekat\nnaslepo\nnastat\nnatolik\nnavenek\nnavrch\nnavzdory\nnazvat\nnebe\nnechat\nnecky\nnedaleko\nnedbat\nneduh\nnegace\nnehet\nnehoda\nnejen\nnejprve\nneklid\nnelibost\nnemilost\nnemoc\nneochota\nneonka\nnepokoj\nnerost\nnerv\nnesmysl\nnesoulad\nnetvor\nneuron\nnevina\nnezvykle\nnicota\nnijak\nnikam\nnikdy\nnikl\nnikterak\nnitro\nnocleh\nnohavice\nnominace\nnora\nnorek\nnositel\nnosnost\nnouze\nnoviny\nnovota\nnozdra\nnuda\nnudle\nnuget\nnutit\nnutnost\nnutrie\nnymfa\nobal\nobarvit\nobava\nobdiv\nobec\nobehnat\nobejmout\nobezita\nobhajoba\nobilnice\nobjasnit\nobjekt\nobklopit\noblast\noblek\nobliba\nobloha\nobluda\nobnos\nobohatit\nobojek\nobout\nobrazec\nobrna\nobruba\nobrys\nobsah\nobsluha\nobstarat\nobuv\nobvaz\nobvinit\nobvod\nobvykle\nobyvatel\nobzor\nocas\nocel\nocenit\nochladit\nochota\nochrana\nocitnout\nodboj\nodbyt\nodchod\nodcizit\nodebrat\nodeslat\nodevzdat\nodezva\nodhadce\nodhodit\nodjet\nodjinud\nodkaz\nodkoupit\nodliv\nodluka\nodmlka\nodolnost\nodpad\nodpis\nodplout\nodpor\nodpustit\nodpykat\nodrazka\nodsoudit\nodstup\nodsun\nodtok\nodtud\nodvaha\nodveta\nodvolat\nodvracet\nodznak\nofina\nofsajd\nohlas\nohnisko\nohrada\nohrozit\nohryzek\nokap\nokenice\noklika\nokno\nokouzlit\nokovy\nokrasa\nokres\nokrsek\nokruh\nokupant\nokurka\nokusit\nolejnina\nolizovat\nomak\nomeleta\nomezit\nomladina\nomlouvat\nomluva\nomyl\nonehdy\nopakovat\nopasek\noperace\nopice\nopilost\nopisovat\nopora\nopozice\nopravdu\noproti\norbital\norchestr\norgie\norlice\norloj\nortel\nosada\noschnout\nosika\nosivo\noslava\noslepit\noslnit\noslovit\nosnova\nosoba\nosolit\nospalec\nosten\nostraha\nostuda\nostych\nosvojit\noteplit\notisk\notop\notrhat\notrlost\notrok\notruby\notvor\novanout\novar\noves\novlivnit\novoce\noxid\nozdoba\npachatel\npacient\npadouch\npahorek\npakt\npalanda\npalec\npalivo\npaluba\npamflet\npamlsek\npanenka\npanika\npanna\npanovat\npanstvo\npantofle\npaprika\nparketa\nparodie\nparta\nparuka\nparyba\npaseka\npasivita\npastelka\npatent\npatrona\npavouk\npazneht\npazourek\npecka\npedagog\npejsek\npeklo\npeloton\npenalta\npendrek\npenze\nperiskop\npero\npestrost\npetarda\npetice\npetrolej\npevnina\npexeso\npianista\npiha\npijavice\npikle\npiknik\npilina\npilnost\npilulka\npinzeta\npipeta\npisatel\npistole\npitevna\npivnice\npivovar\nplacenta\nplakat\nplamen\nplaneta\nplastika\nplatit\nplavidlo\nplaz\nplech\nplemeno\nplenta\nples\npletivo\nplevel\nplivat\nplnit\nplno\nplocha\nplodina\nplomba\nplout\npluk\nplyn\npobavit\npobyt\npochod\npocit\npoctivec\npodat\npodcenit\npodepsat\npodhled\npodivit\npodklad\npodmanit\npodnik\npodoba\npodpora\npodraz\npodstata\npodvod\npodzim\npoezie\npohanka\npohnutka\npohovor\npohroma\npohyb\npointa\npojistka\npojmout\npokazit\npokles\npokoj\npokrok\npokuta\npokyn\npoledne\npolibek\npolknout\npoloha\npolynom\npomalu\npominout\npomlka\npomoc\npomsta\npomyslet\nponechat\nponorka\nponurost\npopadat\npopel\npopisek\npoplach\npoprosit\npopsat\npopud\nporadce\nporce\nporod\nporucha\nporyv\nposadit\nposed\nposila\nposkok\nposlanec\nposoudit\npospolu\npostava\nposudek\nposyp\npotah\npotkan\npotlesk\npotomek\npotrava\npotupa\npotvora\npoukaz\npouto\npouzdro\npovaha\npovidla\npovlak\npovoz\npovrch\npovstat\npovyk\npovzdech\npozdrav\npozemek\npoznatek\npozor\npozvat\npracovat\nprahory\npraktika\nprales\npraotec\npraporek\nprase\npravda\nprincip\nprkno\nprobudit\nprocento\nprodej\nprofese\nprohra\nprojekt\nprolomit\npromile\npronikat\npropad\nprorok\nprosba\nproton\nproutek\nprovaz\nprskavka\nprsten\nprudkost\nprut\nprvek\nprvohory\npsanec\npsovod\npstruh\nptactvo\npuberta\npuch\npudl\npukavec\npuklina\npukrle\npult\npumpa\npunc\npupen\npusa\npusinka\npustina\nputovat\nputyka\npyramida\npysk\npytel\nracek\nrachot\nradiace\nradnice\nradon\nraft\nragby\nraketa\nrakovina\nrameno\nrampouch\nrande\nrarach\nrarita\nrasovna\nrastr\nratolest\nrazance\nrazidlo\nreagovat\nreakce\nrecept\nredaktor\nreferent\nreflex\nrejnok\nreklama\nrekord\nrekrut\nrektor\nreputace\nrevize\nrevma\nrevolver\nrezerva\nriskovat\nriziko\nrobotika\nrodokmen\nrohovka\nrokle\nrokoko\nromaneto\nropovod\nropucha\nrorejs\nrosol\nrostlina\nrotmistr\nrotoped\nrotunda\nroubenka\nroucho\nroup\nroura\nrovina\nrovnice\nrozbor\nrozchod\nrozdat\nrozeznat\nrozhodce\nrozinka\nrozjezd\nrozkaz\nrozloha\nrozmar\nrozpad\nrozruch\nrozsah\nroztok\nrozum\nrozvod\nrubrika\nruchadlo\nrukavice\nrukopis\nryba\nrybolov\nrychlost\nrydlo\nrypadlo\nrytina\nryzost\nsadista\nsahat\nsako\nsamec\nsamizdat\nsamota\nsanitka\nsardinka\nsasanka\nsatelit\nsazba\nsazenice\nsbor\nschovat\nsebranka\nsecese\nsedadlo\nsediment\nsedlo\nsehnat\nsejmout\nsekera\nsekta\nsekunda\nsekvoje\nsemeno\nseno\nservis\nsesadit\nseshora\nseskok\nseslat\nsestra\nsesuv\nsesypat\nsetba\nsetina\nsetkat\nsetnout\nsetrvat\nsever\nseznam\nshoda\nshrnout\nsifon\nsilnice\nsirka\nsirotek\nsirup\nsituace\nskafandr\nskalisko\nskanzen\nskaut\nskeptik\nskica\nskladba\nsklenice\nsklo\nskluz\nskoba\nskokan\nskoro\nskripta\nskrz\nskupina\nskvost\nskvrna\nslabika\nsladidlo\nslanina\nslast\nslavnost\nsledovat\nslepec\nsleva\nslezina\nslib\nslina\nsliznice\nslon\nsloupek\nslovo\nsluch\nsluha\nslunce\nslupka\nslza\nsmaragd\nsmetana\nsmilstvo\nsmlouva\nsmog\nsmrad\nsmrk\nsmrtka\nsmutek\nsmysl\nsnad\nsnaha\nsnob\nsobota\nsocha\nsodovka\nsokol\nsopka\nsotva\nsouboj\nsoucit\nsoudce\nsouhlas\nsoulad\nsoumrak\nsouprava\nsoused\nsoutok\nsouviset\nspalovna\nspasitel\nspis\nsplav\nspodek\nspojenec\nspolu\nsponzor\nspornost\nspousta\nsprcha\nspustit\nsranda\nsraz\nsrdce\nsrna\nsrnec\nsrovnat\nsrpen\nsrst\nsrub\nstanice\nstarosta\nstatika\nstavba\nstehno\nstezka\nstodola\nstolek\nstopa\nstorno\nstoupat\nstrach\nstres\nstrhnout\nstrom\nstruna\nstudna\nstupnice\nstvol\nstyk\nsubjekt\nsubtropy\nsuchar\nsudost\nsukno\nsundat\nsunout\nsurikata\nsurovina\nsvah\nsvalstvo\nsvetr\nsvatba\nsvazek\nsvisle\nsvitek\nsvoboda\nsvodidlo\nsvorka\nsvrab\nsykavka\nsykot\nsynek\nsynovec\nsypat\nsypkost\nsyrovost\nsysel\nsytost\ntabletka\ntabule\ntahoun\ntajemno\ntajfun\ntajga\ntajit\ntajnost\ntaktika\ntamhle\ntampon\ntancovat\ntanec\ntanker\ntapeta\ntavenina\ntazatel\ntechnika\ntehdy\ntekutina\ntelefon\ntemnota\ntendence\ntenista\ntenor\nteplota\ntepna\nteprve\nterapie\ntermoska\ntextil\nticho\ntiskopis\ntitulek\ntkadlec\ntkanina\ntlapka\ntleskat\ntlukot\ntlupa\ntmel\ntoaleta\ntopinka\ntopol\ntorzo\ntouha\ntoulec\ntradice\ntraktor\ntramp\ntrasa\ntraverza\ntrefit\ntrest\ntrezor\ntrhavina\ntrhlina\ntrochu\ntrojice\ntroska\ntrouba\ntrpce\ntrpitel\ntrpkost\ntrubec\ntruchlit\ntruhlice\ntrus\ntrvat\ntudy\ntuhnout\ntuhost\ntundra\nturista\nturnaj\ntuzemsko\ntvaroh\ntvorba\ntvrdost\ntvrz\ntygr\ntykev\nubohost\nuboze\nubrat\nubrousek\nubrus\nubytovna\nucho\nuctivost\nudivit\nuhradit\nujednat\nujistit\nujmout\nukazatel\nuklidnit\nuklonit\nukotvit\nukrojit\nulice\nulita\nulovit\numyvadlo\nunavit\nuniforma\nuniknout\nupadnout\nuplatnit\nuplynout\nupoutat\nupravit\nuran\nurazit\nusednout\nusilovat\nusmrtit\nusnadnit\nusnout\nusoudit\nustlat\nustrnout\nutahovat\nutkat\nutlumit\nutonout\nutopenec\nutrousit\nuvalit\nuvolnit\nuvozovka\nuzdravit\nuzel\nuzenina\nuzlina\nuznat\nvagon\nvalcha\nvaloun\nvana\nvandal\nvanilka\nvaran\nvarhany\nvarovat\nvcelku\nvchod\nvdova\nvedro\nvegetace\nvejce\nvelbloud\nveletrh\nvelitel\nvelmoc\nvelryba\nvenkov\nveranda\nverze\nveselka\nveskrze\nvesnice\nvespodu\nvesta\nveterina\nveverka\nvibrace\nvichr\nvideohra\nvidina\nvidle\nvila\nvinice\nviset\nvitalita\nvize\nvizitka\nvjezd\nvklad\nvkus\nvlajka\nvlak\nvlasec\nvlevo\nvlhkost\nvliv\nvlnovka\nvloupat\nvnucovat\nvnuk\nvoda\nvodivost\nvodoznak\nvodstvo\nvojensky\nvojna\nvojsko\nvolant\nvolba\nvolit\nvolno\nvoskovka\nvozidlo\nvozovna\nvpravo\nvrabec\nvracet\nvrah\nvrata\nvrba\nvrcholek\nvrhat\nvrstva\nvrtule\nvsadit\nvstoupit\nvstup\nvtip\nvybavit\nvybrat\nvychovat\nvydat\nvydra\nvyfotit\nvyhledat\nvyhnout\nvyhodit\nvyhradit\nvyhubit\nvyjasnit\nvyjet\nvyjmout\nvyklopit\nvykonat\nvylekat\nvymazat\nvymezit\nvymizet\nvymyslet\nvynechat\nvynikat\nvynutit\nvypadat\nvyplatit\nvypravit\nvypustit\nvyrazit\nvyrovnat\nvyrvat\nvyslovit\nvysoko\nvystavit\nvysunout\nvysypat\nvytasit\nvytesat\nvytratit\nvyvinout\nvyvolat\nvyvrhel\nvyzdobit\nvyznat\nvzadu\nvzbudit\nvzchopit\nvzdor\nvzduch\nvzdychat\nvzestup\nvzhledem\nvzkaz\nvzlykat\nvznik\nvzorek\nvzpoura\nvztah\nvztek\nxylofon\nzabrat\nzabydlet\nzachovat\nzadarmo\nzadusit\nzafoukat\nzahltit\nzahodit\nzahrada\nzahynout\nzajatec\nzajet\nzajistit\nzaklepat\nzakoupit\nzalepit\nzamezit\nzamotat\nzamyslet\nzanechat\nzanikat\nzaplatit\nzapojit\nzapsat\nzarazit\nzastavit\nzasunout\nzatajit\nzatemnit\nzatknout\nzaujmout\nzavalit\nzavelet\nzavinit\nzavolat\nzavrtat\nzazvonit\nzbavit\nzbrusu\nzbudovat\nzbytek\nzdaleka\nzdarma\nzdatnost\nzdivo\nzdobit\nzdroj\nzdvih\nzdymadlo\nzelenina\nzeman\nzemina\nzeptat\nzezadu\nzezdola\nzhatit\nzhltnout\nzhluboka\nzhotovit\nzhruba\nzima\nzimnice\nzjemnit\nzklamat\nzkoumat\nzkratka\nzkumavka\nzlato\nzlehka\nzloba\nzlom\nzlost\nzlozvyk\nzmapovat\nzmar\nzmatek\nzmije\nzmizet\nzmocnit\nzmodrat\nzmrzlina\nzmutovat\nznak\nznalost\nznamenat\nznovu\nzobrazit\nzotavit\nzoubek\nzoufale\nzplodit\nzpomalit\nzprava\nzprostit\nzprudka\nzprvu\nzrada\nzranit\nzrcadlo\nzrnitost\nzrno\nzrovna\nzrychlit\nzrzavost\nzticha\nztratit\nzubovina\nzubr\nzvednout\nzvenku\nzvesela\nzvon\nzvrat\nzvukovod\nzvyk" + ); _WordLists = dico; } @@ -33,10 +49,14 @@ static HardcodedWordlistSource() { if (!_WordLists.TryGetValue(name, out var list)) return null; - return Task.FromResult(new Wordlist(list.Split(["\n"], StringSplitOptions.RemoveEmptyEntries), - name == "japanese" ? ' ' : ' ', name - )); + return Task.FromResult( + new Wordlist( + list.Split(["\n"], StringSplitOptions.RemoveEmptyEntries), + name == "japanese" ? ' ' : ' ', + name + ) + ); } #endregion -} \ No newline at end of file +} diff --git a/DotNut/NBitcoin/BIP39/IWordlistSource.cs b/DotNut/NBitcoin/BIP39/IWordlistSource.cs index a40557c..6b51336 100644 --- a/DotNut/NBitcoin/BIP39/IWordlistSource.cs +++ b/DotNut/NBitcoin/BIP39/IWordlistSource.cs @@ -4,4 +4,4 @@ public interface IWordlistSource { Task? Load(string name); } -} \ No newline at end of file +} diff --git a/DotNut/NBitcoin/BIP39/KDTable.cs b/DotNut/NBitcoin/BIP39/KDTable.cs index 85926ea..05d3f4b 100644 --- a/DotNut/NBitcoin/BIP39/KDTable.cs +++ b/DotNut/NBitcoin/BIP39/KDTable.cs @@ -2,71 +2,74 @@ namespace DotNut.NBitcoin.BIP39 { - class KDTable - { - public static string NormalizeKD(string str) - { - StringBuilder builder = new StringBuilder(str.Length); - foreach (char c in str.ToCharArray()) - { - if (!Supported(c)) - { - throw new PlatformNotSupportedException("the input string can't be normalized on this platform"); - } - Substitute(c, builder); - } - return builder.ToString(); - } + class KDTable + { + public static string NormalizeKD(string str) + { + StringBuilder builder = new StringBuilder(str.Length); + foreach (char c in str.ToCharArray()) + { + if (!Supported(c)) + { + throw new PlatformNotSupportedException( + "the input string can't be normalized on this platform" + ); + } + Substitute(c, builder); + } + return builder.ToString(); + } - private static void Substitute(char c, StringBuilder builder) - { - for (int i = 0; i < _SubstitutionTable.Length; i++) - { - var substituedChar = _SubstitutionTable[i]; - if (substituedChar == c) - { - Substitute(i, builder); - return; - } - if (substituedChar > c) - break; - while (_SubstitutionTable[i] != '\n') - i++; - } - builder.Append(c); - } + private static void Substitute(char c, StringBuilder builder) + { + for (int i = 0; i < _SubstitutionTable.Length; i++) + { + var substituedChar = _SubstitutionTable[i]; + if (substituedChar == c) + { + Substitute(i, builder); + return; + } + if (substituedChar > c) + break; + while (_SubstitutionTable[i] != '\n') + i++; + } + builder.Append(c); + } - private static void Substitute(int pos, StringBuilder builder) - { - for (int i = pos + 1; i < _SubstitutionTable.Length; i++) - { - if (_SubstitutionTable[i] == '\n') - break; - builder.Append(_SubstitutionTable[i]); - } - } + private static void Substitute(int pos, StringBuilder builder) + { + for (int i = pos + 1; i < _SubstitutionTable.Length; i++) + { + if (_SubstitutionTable[i] == '\n') + break; + builder.Append(_SubstitutionTable[i]); + } + } - private static bool Supported(char c) - { - return _SupportedChars.Any(r => r[0] <= c && c <= r[1]); - } + private static bool Supported(char c) + { + return _SupportedChars.Any(r => r[0] <= c && c <= r[1]); + } - - static int[][] _SupportedChars = new int[][]{ -new[]{0,1000}, -new[]{12352,12447}, -new[]{12448,12543}, -new[]{19968,40959}, -new[]{13312,19967}, -new[]{131072,173791}, -new[]{63744,64255}, -new[]{194560,195103}, -new[]{13056,13311}, -new[]{12288,12351}, -new[]{65280,65535}, -new[]{8192,8303}, -new[]{8352,8399}, -}; - const string _SubstitutionTable = "  \n¨ ̈\nªa\n¯ ̄\n²2\n³3\n´ ́\nµμ\n¸ ̧\n¹1\nºo\n¼1⁄4\n½1⁄2\n¾3⁄4\nÀÀ\nÁÁ\nÂÂ\nÃÃ\nÄÄ\nÅÅ\nÇÇ\nÈÈ\nÉÉ\nÊÊ\nËË\nÌÌ\nÍÍ\nÎÎ\nÏÏ\nÑÑ\nÒÒ\nÓÓ\nÔÔ\nÕÕ\nÖÖ\nÙÙ\nÚÚ\nÛÛ\nÜÜ\nÝÝ\nàà\náá\nââ\nãã\nää\nåå\nçç\nèè\néé\nêê\nëë\nìì\níí\nîî\nïï\nññ\nòò\nóó\nôô\nõõ\nöö\nùù\núú\nûû\nüü\nýý\nÿÿ\nĀĀ\nāā\nĂĂ\năă\nĄĄ\nąą\nĆĆ\nćć\nĈĈ\nĉĉ\nĊĊ\nċċ\nČČ\nčč\nĎĎ\nďď\nĒĒ\nēē\nĔĔ\nĕĕ\nĖĖ\nėė\nĘĘ\nęę\nĚĚ\něě\nĜĜ\nĝĝ\nĞĞ\nğğ\nĠĠ\nġġ\nĢĢ\nģģ\nĤĤ\nĥĥ\nĨĨ\nĩĩ\nĪĪ\nīī\nĬĬ\nĭĭ\nĮĮ\nįį\nİİ\nIJIJ\nijij\nĴĴ\nĵĵ\nĶĶ\nķķ\nĹĹ\nĺĺ\nĻĻ\nļļ\nĽĽ\nľľ\nĿL·\nŀl·\nŃŃ\nńń\nŅŅ\nņņ\nŇŇ\nňň\nʼnʼn\nŌŌ\nōō\nŎŎ\nŏŏ\nŐŐ\nőő\nŔŔ\nŕŕ\nŖŖ\nŗŗ\nŘŘ\nřř\nŚŚ\nśś\nŜŜ\nŝŝ\nŞŞ\nşş\nŠŠ\nšš\nŢŢ\nţţ\nŤŤ\nťť\nŨŨ\nũũ\nŪŪ\nūū\nŬŬ\nŭŭ\nŮŮ\nůů\nŰŰ\nűű\nŲŲ\nųų\nŴŴ\nŵŵ\nŶŶ\nŷŷ\nŸŸ\nŹŹ\nźź\nŻŻ\nżż\nŽŽ\nžž\nſs\nƠƠ\nơơ\nƯƯ\nưư\nDŽDŽ\nDžDž\ndždž\nLJLJ\nLjLj\nljlj\nNJNJ\nNjNj\nnjnj\nǍǍ\nǎǎ\nǏǏ\nǐǐ\nǑǑ\nǒǒ\nǓǓ\nǔǔ\nǕǕ\nǖǖ\nǗǗ\nǘǘ\nǙǙ\nǚǚ\nǛǛ\nǜǜ\nǞǞ\nǟǟ\nǠǠ\nǡǡ\nǢǢ\nǣǣ\nǦǦ\nǧǧ\nǨǨ\nǩǩ\nǪǪ\nǫǫ\nǬǬ\nǭǭ\nǮǮ\nǯǯ\nǰǰ\nDZDZ\nDzDz\ndzdz\nǴǴ\nǵǵ\nǸǸ\nǹǹ\nǺǺ\nǻǻ\nǼǼ\nǽǽ\nǾǾ\nǿǿ\nȀȀ\nȁȁ\nȂȂ\nȃȃ\nȄȄ\nȅȅ\nȆȆ\nȇȇ\nȈȈ\nȉȉ\nȊȊ\nȋȋ\nȌȌ\nȍȍ\nȎȎ\nȏȏ\nȐȐ\nȑȑ\nȒȒ\nȓȓ\nȔȔ\nȕȕ\nȖȖ\nȗȗ\nȘȘ\nșș\nȚȚ\nțț\nȞȞ\nȟȟ\nȦȦ\nȧȧ\nȨȨ\nȩȩ\nȪȪ\nȫȫ\nȬȬ\nȭȭ\nȮȮ\nȯȯ\nȰȰ\nȱȱ\nȲȲ\nȳȳ\nʰh\nʱɦ\nʲj\nʳr\nʴɹ\nʵɻ\nʶʁ\nʷw\nʸy\n˘ ̆\n˙ ̇\n˚ ̊\n˛ ̨\n˜ ̃\n˝ ̋\nˠɣ\nˡl\nˢs\nˣx\nˤʕ\ǹ̀\ń́\n̓̓\n̈́̈́\nʹʹ\nͺ ͅ\n;;\n΄ ́\n΅ ̈́\nΆΆ\n··\nΈΈ\nΉΉ\nΊΊ\nΌΌ\nΎΎ\nΏΏ\nΐΐ\nΪΪ\nΫΫ\nάά\nέέ\nήή\nίί\nΰΰ\nϊϊ\nϋϋ\nόό\nύύ\nώώ\nϐβ\nϑθ\nϒΥ\nϓΎ\nϔΫ\nϕφ\nϖπ\nϰκ\nϱρ\nϲς\nϴΘ\nϵε\nϹΣ\nЀЀ\nЁЁ\nЃЃ\nЇЇ\nЌЌ\nЍЍ\nЎЎ\nЙЙ\nйй\nѐѐ\nёё\nѓѓ\nїї\nќќ\nѝѝ\nўў\nѶѶ\nѷѷ\nӁӁ\nӂӂ\nӐӐ\nӑӑ\nӒӒ\nӓӓ\nӖӖ\nӗӗ\nӚӚ\nӛӛ\nӜӜ\nӝӝ\nӞӞ\nӟӟ\nӢӢ\nӣӣ\nӤӤ\nӥӥ\nӦӦ\nӧӧ\nӪӪ\nӫӫ\nӬӬ\nӭӭ\nӮӮ\nӯӯ\nӰӰ\nӱӱ\nӲӲ\nӳӳ\nӴӴ\nӵӵ\nӸӸ\nӹӹ\nևեւ\nآآ\nأأ\nؤؤ\nإإ\nئئ\nٵاٴ\nٶوٴ\nٷۇٴ\nٸيٴ\nۀۀ\nۂۂ\nۓۓ\nऩऩ\nऱऱ\nऴऴ\nक़क़\nख़ख़\nग़ग़\nज़ज़\nड़ड़\nढ़ढ़\nफ़फ़\nय़य़\nোো\nৌৌ\nড়ড়\nঢ়ঢ়\nয়য়\nਲ਼ਲ਼\nਸ਼ਸ਼\nਖ਼ਖ਼\nਗ਼ਗ਼\nਜ਼ਜ਼\nਫ਼ਫ਼\nୈୈ\nୋୋ\nୌୌ\nଡ଼ଡ଼\nଢ଼ଢ଼\nஔஔ\nொொ\nோோ\nௌௌ\nైై\nೀೀ\nೇೇ\nೈೈ\nೊೊ\nೋೋ\nൊൊ\nോോ\nൌൌ\nේේ\nොො\nෝෝ\nෞෞ\nำํา\nຳໍາ\nໜຫນ\nໝຫມ\n༌་\nགྷགྷ\nཌྷཌྷ\nདྷདྷ\nབྷབྷ\nཛྷཛྷ\nཀྵཀྵ\nཱཱིི\nཱཱུུ\nྲྀྲྀ\nཷྲཱྀ\nླྀླྀ\nཹླཱྀ\nཱཱྀྀ\nྒྷྒྷ\nྜྷྜྷ\nྡྷྡྷ\nྦྷྦྷ\nྫྷྫྷ\nྐྵྐྵ\nဦဦ\nჼნ\nᬆᬆ\nᬈᬈ\nᬊᬊ\nᬌᬌ\nᬎᬎ\nᬒᬒ\nᬻᬻ\nᬽᬽ\nᭀᭀ\nᭁᭁ\nᭃᭃ\nᴬA\nᴭÆ\nᴮB\nᴰD\nᴱE\nᴲƎ\nᴳG\nᴴH\nᴵI\nᴶJ\nᴷK\nᴸL\nᴹM\nᴺN\nᴼO\nᴽȢ\nᴾP\nᴿR\nᵀT\nᵁU\nᵂW\nᵃa\nᵄɐ\nᵅɑ\nᵆᴂ\nᵇb\nᵈd\nᵉe\nᵊə\nᵋɛ\nᵌɜ\nᵍg\nᵏk\nᵐm\nᵑŋ\nᵒo\nᵓɔ\nᵔᴖ\nᵕᴗ\nᵖp\nᵗt\nᵘu\nᵙᴝ\nᵚɯ\nᵛv\nᵜᴥ\nᵝβ\nᵞγ\nᵟδ\nᵠφ\nᵡχ\nᵢi\nᵣr\nᵤu\nᵥv\nᵦβ\nᵧγ\nᵨρ\nᵩφ\nᵪχ\nᵸн\nᶛɒ\nᶜc\nᶝɕ\nᶞð\nᶟɜ\nᶠf\nᶡɟ\nᶢɡ\nᶣɥ\nᶤɨ\nᶥɩ\nᶦɪ\nᶧᵻ\nᶨʝ\nᶩɭ\nᶪᶅ\nᶫʟ\nᶬɱ\nᶭɰ\nᶮɲ\nᶯɳ\nᶰɴ\nᶱɵ\nᶲɸ\nᶳʂ\nᶴʃ\nᶵƫ\nᶶʉ\nᶷʊ\nᶸᴜ\nᶹʋ\nᶺʌ\nᶻz\nᶼʐ\nᶽʑ\nᶾʒ\nᶿθ\nḀḀ\nḁḁ\nḂḂ\nḃḃ\nḄḄ\nḅḅ\nḆḆ\nḇḇ\nḈḈ\nḉḉ\nḊḊ\nḋḋ\nḌḌ\nḍḍ\nḎḎ\nḏḏ\nḐḐ\nḑḑ\nḒḒ\nḓḓ\nḔḔ\nḕḕ\nḖḖ\nḗḗ\nḘḘ\nḙḙ\nḚḚ\nḛḛ\nḜḜ\nḝḝ\nḞḞ\nḟḟ\nḠḠ\nḡḡ\nḢḢ\nḣḣ\nḤḤ\nḥḥ\nḦḦ\nḧḧ\nḨḨ\nḩḩ\nḪḪ\nḫḫ\nḬḬ\nḭḭ\nḮḮ\nḯḯ\nḰḰ\nḱḱ\nḲḲ\nḳḳ\nḴḴ\nḵḵ\nḶḶ\nḷḷ\nḸḸ\nḹḹ\nḺḺ\nḻḻ\nḼḼ\nḽḽ\nḾḾ\nḿḿ\nṀṀ\nṁṁ\nṂṂ\nṃṃ\nṄṄ\nṅṅ\nṆṆ\nṇṇ\nṈṈ\nṉṉ\nṊṊ\nṋṋ\nṌṌ\nṍṍ\nṎṎ\nṏṏ\nṐṐ\nṑṑ\nṒṒ\nṓṓ\nṔṔ\nṕṕ\nṖṖ\nṗṗ\nṘṘ\nṙṙ\nṚṚ\nṛṛ\nṜṜ\nṝṝ\nṞṞ\nṟṟ\nṠṠ\nṡṡ\nṢṢ\nṣṣ\nṤṤ\nṥṥ\nṦṦ\nṧṧ\nṨṨ\nṩṩ\nṪṪ\nṫṫ\nṬṬ\nṭṭ\nṮṮ\nṯṯ\nṰṰ\nṱṱ\nṲṲ\nṳṳ\nṴṴ\nṵṵ\nṶṶ\nṷṷ\nṸṸ\nṹṹ\nṺṺ\nṻṻ\nṼṼ\nṽṽ\nṾṾ\nṿṿ\nẀẀ\nẁẁ\nẂẂ\nẃẃ\nẄẄ\nẅẅ\nẆẆ\nẇẇ\nẈẈ\nẉẉ\nẊẊ\nẋẋ\nẌẌ\nẍẍ\nẎẎ\nẏẏ\nẐẐ\nẑẑ\nẒẒ\nẓẓ\nẔẔ\nẕẕ\nẖẖ\nẗẗ\nẘẘ\nẙẙ\nẚaʾ\nẛṡ\nẠẠ\nạạ\nẢẢ\nảả\nẤẤ\nấấ\nẦẦ\nầầ\nẨẨ\nẩẩ\nẪẪ\nẫẫ\nẬẬ\nậậ\nẮẮ\nắắ\nẰẰ\nằằ\nẲẲ\nẳẳ\nẴẴ\nẵẵ\nẶẶ\nặặ\nẸẸ\nẹẹ\nẺẺ\nẻẻ\nẼẼ\nẽẽ\nẾẾ\nếế\nỀỀ\nềề\nỂỂ\nểể\nỄỄ\nễễ\nỆỆ\nệệ\nỈỈ\nỉỉ\nỊỊ\nịị\nỌỌ\nọọ\nỎỎ\nỏỏ\nỐỐ\nốố\nỒỒ\nồồ\nỔỔ\nổổ\nỖỖ\nỗỗ\nỘỘ\nộộ\nỚỚ\nớớ\nỜỜ\nờờ\nỞỞ\nởở\nỠỠ\nỡỡ\nỢỢ\nợợ\nỤỤ\nụụ\nỦỦ\nủủ\nỨỨ\nứứ\nỪỪ\nừừ\nỬỬ\nửử\nỮỮ\nữữ\nỰỰ\nựự\nỲỲ\nỳỳ\nỴỴ\nỵỵ\nỶỶ\nỷỷ\nỸỸ\nỹỹ\nἀἀ\nἁἁ\nἂἂ\nἃἃ\nἄἄ\nἅἅ\nἆἆ\nἇἇ\nἈἈ\nἉἉ\nἊἊ\nἋἋ\nἌἌ\nἍἍ\nἎἎ\nἏἏ\nἐἐ\nἑἑ\nἒἒ\nἓἓ\nἔἔ\nἕἕ\nἘἘ\nἙἙ\nἚἚ\nἛἛ\nἜἜ\nἝἝ\nἠἠ\nἡἡ\nἢἢ\nἣἣ\nἤἤ\nἥἥ\nἦἦ\nἧἧ\nἨἨ\nἩἩ\nἪἪ\nἫἫ\nἬἬ\nἭἭ\nἮἮ\nἯἯ\nἰἰ\nἱἱ\nἲἲ\nἳἳ\nἴἴ\nἵἵ\nἶἶ\nἷἷ\nἸἸ\nἹἹ\nἺἺ\nἻἻ\nἼἼ\nἽἽ\nἾἾ\nἿἿ\nὀὀ\nὁὁ\nὂὂ\nὃὃ\nὄὄ\nὅὅ\nὈὈ\nὉὉ\nὊὊ\nὋὋ\nὌὌ\nὍὍ\nὐὐ\nὑὑ\nὒὒ\nὓὓ\nὔὔ\nὕὕ\nὖὖ\nὗὗ\nὙὙ\nὛὛ\nὝὝ\nὟὟ\nὠὠ\nὡὡ\nὢὢ\nὣὣ\nὤὤ\nὥὥ\nὦὦ\nὧὧ\nὨὨ\nὩὩ\nὪὪ\nὫὫ\nὬὬ\nὭὭ\nὮὮ\nὯὯ\nὰὰ\nάά\nὲὲ\nέέ\nὴὴ\nήή\nὶὶ\nίί\nὸὸ\nόό\nὺὺ\nύύ\nὼὼ\nώώ\nᾀᾀ\nᾁᾁ\nᾂᾂ\nᾃᾃ\nᾄᾄ\nᾅᾅ\nᾆᾆ\nᾇᾇ\nᾈᾈ\nᾉᾉ\nᾊᾊ\nᾋᾋ\nᾌᾌ\nᾍᾍ\nᾎᾎ\nᾏᾏ\nᾐᾐ\nᾑᾑ\nᾒᾒ\nᾓᾓ\nᾔᾔ\nᾕᾕ\nᾖᾖ\nᾗᾗ\nᾘᾘ\nᾙᾙ\nᾚᾚ\nᾛᾛ\nᾜᾜ\nᾝᾝ\nᾞᾞ\nᾟᾟ\nᾠᾠ\nᾡᾡ\nᾢᾢ\nᾣᾣ\nᾤᾤ\nᾥᾥ\nᾦᾦ\nᾧᾧ\nᾨᾨ\nᾩᾩ\nᾪᾪ\nᾫᾫ\nᾬᾬ\nᾭᾭ\nᾮᾮ\nᾯᾯ\nᾰᾰ\nᾱᾱ\nᾲᾲ\nᾳᾳ\nᾴᾴ\nᾶᾶ\nᾷᾷ\nᾸᾸ\nᾹᾹ\nᾺᾺ\nΆΆ\nᾼᾼ\n᾽ ̓\nιι\n᾿ ̓\n῀ ͂\n῁ ̈͂\nῂῂ\nῃῃ\nῄῄ\nῆῆ\nῇῇ\nῈῈ\nΈΈ\nῊῊ\nΉΉ\nῌῌ\n῍ ̓̀\n῎ ̓́\n῏ ̓͂\nῐῐ\nῑῑ\nῒῒ\nΐΐ\nῖῖ\nῗῗ\nῘῘ\nῙῙ\nῚῚ\nΊΊ\n῝ ̔̀\n῞ ̔́\n῟ ̔͂\nῠῠ\nῡῡ\nῢῢ\nΰΰ\nῤῤ\nῥῥ\nῦῦ\nῧῧ\nῨῨ\nῩῩ\nῪῪ\nΎΎ\nῬῬ\n῭ ̈̀\n΅ ̈́\n``\nῲῲ\nῳῳ\nῴῴ\nῶῶ\nῷῷ\nῸῸ\nΌΌ\nῺῺ\nΏΏ\nῼῼ\n´ ́\n῾ ̔\n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n‑‐\n‗ ̳\n․.\n‥..\n…...\n  \n″′′\n‴′′′\n‶‵‵\n‷‵‵‵\n‼!!\n‾ ̅\n⁇??\n⁈?!\n⁉!?\n⁗′′′′\n  \n⁰0\nⁱi\n⁴4\n⁵5\n⁶6\n⁷7\n⁸8\n⁹9\n⁺+\n⁻−\n⁼=\n⁽(\n⁾)\nⁿn\n₀0\n₁1\n₂2\n₃3\n₄4\n₅5\n₆6\n₇7\n₈8\n₉9\n₊+\n₋−\n₌=\n₍(\n₎)\nₐa\nₑe\nₒo\nₓx\nₔə\n₨Rs\n℀a/c\n℁a/s\nℂC\n℃°C\n℅c/o\n℆c/u\nℇƐ\n℉°F\nℊg\nℋH\nℌH\nℍH\nℎh\nℏħ\nℐI\nℑI\nℒL\nℓl\nℕN\n№No\nℙP\nℚQ\nℛR\nℜR\nℝR\n℠SM\n℡TEL\n™TM\nℤZ\nΩΩ\nℨZ\nKK\nÅÅ\nℬB\nℭC\nℯe\nℰE\nℱF\nℳM\nℴo\nℵא\nℶב\nℷג\nℸד\nℹi\n℻FAX\nℼπ\nℽγ\nℾΓ\nℿΠ\n⅀∑\nⅅD\nⅆd\nⅇe\nⅈi\nⅉj\n⅓1⁄3\n⅔2⁄3\n⅕1⁄5\n⅖2⁄5\n⅗3⁄5\n⅘4⁄5\n⅙1⁄6\n⅚5⁄6\n⅛1⁄8\n⅜3⁄8\n⅝5⁄8\n⅞7⁄8\n⅟1⁄\nⅠI\nⅡII\nⅢIII\nⅣIV\nⅤV\nⅥVI\nⅦVII\nⅧVIII\nⅨIX\nⅩX\nⅪXI\nⅫXII\nⅬL\nⅭC\nⅮD\nⅯM\nⅰi\nⅱii\nⅲiii\nⅳiv\nⅴv\nⅵvi\nⅶvii\nⅷviii\nⅸix\nⅹx\nⅺxi\nⅻxii\nⅼl\nⅽc\nⅾd\nⅿm\n↚↚\n↛↛\n↮↮\n⇍⇍\n⇎⇎\n⇏⇏\n∄∄\n∉∉\n∌∌\n∤∤\n∦∦\n∬∫∫\n∭∫∫∫\n∯∮∮\n∰∮∮∮\n≁≁\n≄≄\n≇≇\n≉≉\n≠≠\n≢≢\n≭≭\n≮≮\n≯≯\n≰≰\n≱≱\n≴≴\n≵≵\n≸≸\n≹≹\n⊀⊀\n⊁⊁\n⊄⊄\n⊅⊅\n⊈⊈\n⊉⊉\n⊬⊬\n⊭⊭\n⊮⊮\n⊯⊯\n⋠⋠\n⋡⋡\n⋢⋢\n⋣⋣\n⋪⋪\n⋫⋫\n⋬⋬\n⋭⋭\n〈〈\n〉〉\n①1\n②2\n③3\n④4\n⑤5\n⑥6\n⑦7\n⑧8\n⑨9\n⑩10\n⑪11\n⑫12\n⑬13\n⑭14\n⑮15\n⑯16\n⑰17\n⑱18\n⑲19\n⑳20\n⑴(1)\n⑵(2)\n⑶(3)\n⑷(4)\n⑸(5)\n⑹(6)\n⑺(7)\n⑻(8)\n⑼(9)\n⑽(10)\n⑾(11)\n⑿(12)\n⒀(13)\n⒁(14)\n⒂(15)\n⒃(16)\n⒄(17)\n⒅(18)\n⒆(19)\n⒇(20)\n⒈1.\n⒉2.\n⒊3.\n⒋4.\n⒌5.\n⒍6.\n⒎7.\n⒏8.\n⒐9.\n⒑10.\n⒒11.\n⒓12.\n⒔13.\n⒕14.\n⒖15.\n⒗16.\n⒘17.\n⒙18.\n⒚19.\n⒛20.\n⒜(a)\n⒝(b)\n⒞(c)\n⒟(d)\n⒠(e)\n⒡(f)\n⒢(g)\n⒣(h)\n⒤(i)\n⒥(j)\n⒦(k)\n⒧(l)\n⒨(m)\n⒩(n)\n⒪(o)\n⒫(p)\n⒬(q)\n⒭(r)\n⒮(s)\n⒯(t)\n⒰(u)\n⒱(v)\n⒲(w)\n⒳(x)\n⒴(y)\n⒵(z)\nⒶA\nⒷB\nⒸC\nⒹD\nⒺE\nⒻF\nⒼG\nⒽH\nⒾI\nⒿJ\nⓀK\nⓁL\nⓂM\nⓃN\nⓄO\nⓅP\nⓆQ\nⓇR\nⓈS\nⓉT\nⓊU\nⓋV\nⓌW\nⓍX\nⓎY\nⓏZ\nⓐa\nⓑb\nⓒc\nⓓd\nⓔe\nⓕf\nⓖg\nⓗh\nⓘi\nⓙj\nⓚk\nⓛl\nⓜm\nⓝn\nⓞo\nⓟp\nⓠq\nⓡr\nⓢs\nⓣt\nⓤu\nⓥv\nⓦw\nⓧx\nⓨy\nⓩz\n⓪0\n⨌∫∫∫∫\n⩴::=\n⩵==\n⩶===\n⫝̸⫝̸\nⱼj\nⱽV\nⵯⵡ\n⺟母\n⻳龟\n⼀一\n⼁丨\n⼂丶\n⼃丿\n⼄乙\n⼅亅\n⼆二\n⼇亠\n⼈人\n⼉儿\n⼊入\n⼋八\n⼌冂\n⼍冖\n⼎冫\n⼏几\n⼐凵\n⼑刀\n⼒力\n⼓勹\n⼔匕\n⼕匚\n⼖匸\n⼗十\n⼘卜\n⼙卩\n⼚厂\n⼛厶\n⼜又\n⼝口\n⼞囗\n⼟土\n⼠士\n⼡夂\n⼢夊\n⼣夕\n⼤大\n⼥女\n⼦子\n⼧宀\n⼨寸\n⼩小\n⼪尢\n⼫尸\n⼬屮\n⼭山\n⼮巛\n⼯工\n⼰己\n⼱巾\n⼲干\n⼳幺\n⼴广\n⼵廴\n⼶廾\n⼷弋\n⼸弓\n⼹彐\n⼺彡\n⼻彳\n⼼心\n⼽戈\n⼾戶\n⼿手\n⽀支\n⽁攴\n⽂文\n⽃斗\n⽄斤\n⽅方\n⽆无\n⽇日\n⽈曰\n⽉月\n⽊木\n⽋欠\n⽌止\n⽍歹\n⽎殳\n⽏毋\n⽐比\n⽑毛\n⽒氏\n⽓气\n⽔水\n⽕火\n⽖爪\n⽗父\n⽘爻\n⽙爿\n⽚片\n⽛牙\n⽜牛\n⽝犬\n⽞玄\n⽟玉\n⽠瓜\n⽡瓦\n⽢甘\n⽣生\n⽤用\n⽥田\n⽦疋\n⽧疒\n⽨癶\n⽩白\n⽪皮\n⽫皿\n⽬目\n⽭矛\n⽮矢\n⽯石\n⽰示\n⽱禸\n⽲禾\n⽳穴\n⽴立\n⽵竹\n⽶米\n⽷糸\n⽸缶\n⽹网\n⽺羊\n⽻羽\n⽼老\n⽽而\n⽾耒\n⽿耳\n⾀聿\n⾁肉\n⾂臣\n⾃自\n⾄至\n⾅臼\n⾆舌\n⾇舛\n⾈舟\n⾉艮\n⾊色\n⾋艸\n⾌虍\n⾍虫\n⾎血\n⾏行\n⾐衣\n⾑襾\n⾒見\n⾓角\n⾔言\n⾕谷\n⾖豆\n⾗豕\n⾘豸\n⾙貝\n⾚赤\n⾛走\n⾜足\n⾝身\n⾞車\n⾟辛\n⾠辰\n⾡辵\n⾢邑\n⾣酉\n⾤釆\n⾥里\n⾦金\n⾧長\n⾨門\n⾩阜\n⾪隶\n⾫隹\n⾬雨\n⾭靑\n⾮非\n⾯面\n⾰革\n⾱韋\n⾲韭\n⾳音\n⾴頁\n⾵風\n⾶飛\n⾷食\n⾸首\n⾹香\n⾺馬\n⾻骨\n⾼高\n⾽髟\n⾾鬥\n⾿鬯\n⿀鬲\n⿁鬼\n⿂魚\n⿃鳥\n⿄鹵\n⿅鹿\n⿆麥\n⿇麻\n⿈黃\n⿉黍\n⿊黑\n⿋黹\n⿌黽\n⿍鼎\n⿎鼓\n⿏鼠\n⿐鼻\n⿑齊\n⿒齒\n⿓龍\n⿔龜\n⿕龠\n  \n〶〒\n〸十\n〹卄\n〺卅\nがが\nぎぎ\nぐぐ\nげげ\nごご\nざざ\nじじ\nずず\nぜぜ\nぞぞ\nだだ\nぢぢ\nづづ\nでで\nどど\nばば\nぱぱ\nびび\nぴぴ\nぶぶ\nぷぷ\nべべ\nぺぺ\nぼぼ\nぽぽ\nゔゔ\n゛ ゙\n゜ ゚\nゞゞ\nゟより\nガガ\nギギ\nググ\nゲゲ\nゴゴ\nザザ\nジジ\nズズ\nゼゼ\nゾゾ\nダダ\nヂヂ\nヅヅ\nデデ\nドド\nババ\nパパ\nビビ\nピピ\nブブ\nププ\nベベ\nペペ\nボボ\nポポ\nヴヴ\nヷヷ\nヸヸ\nヹヹ\nヺヺ\nヾヾ\nヿコト\nㄱᄀ\nㄲᄁ\nㄳᆪ\nㄴᄂ\nㄵᆬ\nㄶᆭ\nㄷᄃ\nㄸᄄ\nㄹᄅ\nㄺᆰ\nㄻᆱ\nㄼᆲ\nㄽᆳ\nㄾᆴ\nㄿᆵ\nㅀᄚ\nㅁᄆ\nㅂᄇ\nㅃᄈ\nㅄᄡ\nㅅᄉ\nㅆᄊ\nㅇᄋ\nㅈᄌ\nㅉᄍ\nㅊᄎ\nㅋᄏ\nㅌᄐ\nㅍᄑ\nㅎᄒ\nㅏᅡ\nㅐᅢ\nㅑᅣ\nㅒᅤ\nㅓᅥ\nㅔᅦ\nㅕᅧ\nㅖᅨ\nㅗᅩ\nㅘᅪ\nㅙᅫ\nㅚᅬ\nㅛᅭ\nㅜᅮ\nㅝᅯ\nㅞᅰ\nㅟᅱ\nㅠᅲ\nㅡᅳ\nㅢᅴ\nㅣᅵ\nㅤᅠ\nㅥᄔ\nㅦᄕ\nㅧᇇ\nㅨᇈ\nㅩᇌ\nㅪᇎ\nㅫᇓ\nㅬᇗ\nㅭᇙ\nㅮᄜ\nㅯᇝ\nㅰᇟ\nㅱᄝ\nㅲᄞ\nㅳᄠ\nㅴᄢ\nㅵᄣ\nㅶᄧ\nㅷᄩ\nㅸᄫ\nㅹᄬ\nㅺᄭ\nㅻᄮ\nㅼᄯ\nㅽᄲ\nㅾᄶ\nㅿᅀ\nㆀᅇ\nㆁᅌ\nㆂᇱ\nㆃᇲ\nㆄᅗ\nㆅᅘ\nㆆᅙ\nㆇᆄ\nㆈᆅ\nㆉᆈ\nㆊᆑ\nㆋᆒ\nㆌᆔ\nㆍᆞ\nㆎᆡ\n㆒一\n㆓二\n㆔三\n㆕四\n㆖上\n㆗中\n㆘下\n㆙甲\n㆚乙\n㆛丙\n㆜丁\n㆝天\n㆞地\n㆟人\n㈀(ᄀ)\n㈁(ᄂ)\n㈂(ᄃ)\n㈃(ᄅ)\n㈄(ᄆ)\n㈅(ᄇ)\n㈆(ᄉ)\n㈇(ᄋ)\n㈈(ᄌ)\n㈉(ᄎ)\n㈊(ᄏ)\n㈋(ᄐ)\n㈌(ᄑ)\n㈍(ᄒ)\n㈎(가)\n㈏(나)\n㈐(다)\n㈑(라)\n㈒(마)\n㈓(바)\n㈔(사)\n㈕(아)\n㈖(자)\n㈗(차)\n㈘(카)\n㈙(타)\n㈚(파)\n㈛(하)\n㈜(주)\n㈝(오전)\n㈞(오후)\n㈠(一)\n㈡(二)\n㈢(三)\n㈣(四)\n㈤(五)\n㈥(六)\n㈦(七)\n㈧(八)\n㈨(九)\n㈩(十)\n㈪(月)\n㈫(火)\n㈬(水)\n㈭(木)\n㈮(金)\n㈯(土)\n㈰(日)\n㈱(株)\n㈲(有)\n㈳(社)\n㈴(名)\n㈵(特)\n㈶(財)\n㈷(祝)\n㈸(労)\n㈹(代)\n㈺(呼)\n㈻(学)\n㈼(監)\n㈽(企)\n㈾(資)\n㈿(協)\n㉀(祭)\n㉁(休)\n㉂(自)\n㉃(至)\n㉐PTE\n㉑21\n㉒22\n㉓23\n㉔24\n㉕25\n㉖26\n㉗27\n㉘28\n㉙29\n㉚30\n㉛31\n㉜32\n㉝33\n㉞34\n㉟35\n㉠ᄀ\n㉡ᄂ\n㉢ᄃ\n㉣ᄅ\n㉤ᄆ\n㉥ᄇ\n㉦ᄉ\n㉧ᄋ\n㉨ᄌ\n㉩ᄎ\n㉪ᄏ\n㉫ᄐ\n㉬ᄑ\n㉭ᄒ\n㉮가\n㉯나\n㉰다\n㉱라\n㉲마\n㉳바\n㉴사\n㉵아\n㉶자\n㉷차\n㉸카\n㉹타\n㉺파\n㉻하\n㉼참고\n㉽주의\n㉾우\n㊀一\n㊁二\n㊂三\n㊃四\n㊄五\n㊅六\n㊆七\n㊇八\n㊈九\n㊉十\n㊊月\n㊋火\n㊌水\n㊍木\n㊎金\n㊏土\n㊐日\n㊑株\n㊒有\n㊓社\n㊔名\n㊕特\n㊖財\n㊗祝\n㊘労\n㊙秘\n㊚男\n㊛女\n㊜適\n㊝優\n㊞印\n㊟注\n㊠項\n㊡休\n㊢写\n㊣正\n㊤上\n㊥中\n㊦下\n㊧左\n㊨右\n㊩医\n㊪宗\n㊫学\n㊬監\n㊭企\n㊮資\n㊯協\n㊰夜\n㊱36\n㊲37\n㊳38\n㊴39\n㊵40\n㊶41\n㊷42\n㊸43\n㊹44\n㊺45\n㊻46\n㊼47\n㊽48\n㊾49\n㊿50\n㋀1月\n㋁2月\n㋂3月\n㋃4月\n㋄5月\n㋅6月\n㋆7月\n㋇8月\n㋈9月\n㋉10月\n㋊11月\n㋋12月\n㋌Hg\n㋍erg\n㋎eV\n㋏LTD\n㋐ア\n㋑イ\n㋒ウ\n㋓エ\n㋔オ\n㋕カ\n㋖キ\n㋗ク\n㋘ケ\n㋙コ\n㋚サ\n㋛シ\n㋜ス\n㋝セ\n㋞ソ\n㋟タ\n㋠チ\n㋡ツ\n㋢テ\n㋣ト\n㋤ナ\n㋥ニ\n㋦ヌ\n㋧ネ\n㋨ノ\n㋩ハ\n㋪ヒ\n㋫フ\n㋬ヘ\n㋭ホ\n㋮マ\n㋯ミ\n㋰ム\n㋱メ\n㋲モ\n㋳ヤ\n㋴ユ\n㋵ヨ\n㋶ラ\n㋷リ\n㋸ル\n㋹レ\n㋺ロ\n㋻ワ\n㋼ヰ\n㋽ヱ\n㋾ヲ\n㌀アパート\n㌁アルファ\n㌂アンペア\n㌃アール\n㌄イニング\n㌅インチ\n㌆ウォン\n㌇エスクード\n㌈エーカー\n㌉オンス\n㌊オーム\n㌋カイリ\n㌌カラット\n㌍カロリー\n㌎ガロン\n㌏ガンマ\n㌐ギガ\n㌑ギニー\n㌒キュリー\n㌓ギルダー\n㌔キロ\n㌕キログラム\n㌖キロメートル\n㌗キロワット\n㌘グラム\n㌙グラムトン\n㌚クルゼイロ\n㌛クローネ\n㌜ケース\n㌝コルナ\n㌞コーポ\n㌟サイクル\n㌠サンチーム\n㌡シリング\n㌢センチ\n㌣セント\n㌤ダース\n㌥デシ\n㌦ドル\n㌧トン\n㌨ナノ\n㌩ノット\n㌪ハイツ\n㌫パーセント\n㌬パーツ\n㌭バーレル\n㌮ピアストル\n㌯ピクル\n㌰ピコ\n㌱ビル\n㌲ファラッド\n㌳フィート\n㌴ブッシェル\n㌵フラン\n㌶ヘクタール\n㌷ペソ\n㌸ペニヒ\n㌹ヘルツ\n㌺ペンス\n㌻ページ\n㌼ベータ\n㌽ポイント\n㌾ボルト\n㌿ホン\n㍀ポンド\n㍁ホール\n㍂ホーン\n㍃マイクロ\n㍄マイル\n㍅マッハ\n㍆マルク\n㍇マンション\n㍈ミクロン\n㍉ミリ\n㍊ミリバール\n㍋メガ\n㍌メガトン\n㍍メートル\n㍎ヤード\n㍏ヤール\n㍐ユアン\n㍑リットル\n㍒リラ\n㍓ルピー\n㍔ルーブル\n㍕レム\n㍖レントゲン\n㍗ワット\n㍘0点\n㍙1点\n㍚2点\n㍛3点\n㍜4点\n㍝5点\n㍞6点\n㍟7点\n㍠8点\n㍡9点\n㍢10点\n㍣11点\n㍤12点\n㍥13点\n㍦14点\n㍧15点\n㍨16点\n㍩17点\n㍪18点\n㍫19点\n㍬20点\n㍭21点\n㍮22点\n㍯23点\n㍰24点\n㍱hPa\n㍲da\n㍳AU\n㍴bar\n㍵oV\n㍶pc\n㍷dm\n㍸dm2\n㍹dm3\n㍺IU\n㍻平成\n㍼昭和\n㍽大正\n㍾明治\n㍿株式会社\n㎀pA\n㎁nA\n㎂μA\n㎃mA\n㎄kA\n㎅KB\n㎆MB\n㎇GB\n㎈cal\n㎉kcal\n㎊pF\n㎋nF\n㎌μF\n㎍μg\n㎎mg\n㎏kg\n㎐Hz\n㎑kHz\n㎒MHz\n㎓GHz\n㎔THz\n㎕μl\n㎖ml\n㎗dl\n㎘kl\n㎙fm\n㎚nm\n㎛μm\n㎜mm\n㎝cm\n㎞km\n㎟mm2\n㎠cm2\n㎡m2\n㎢km2\n㎣mm3\n㎤cm3\n㎥m3\n㎦km3\n㎧m∕s\n㎨m∕s2\n㎩Pa\n㎪kPa\n㎫MPa\n㎬GPa\n㎭rad\n㎮rad∕s\n㎯rad∕s2\n㎰ps\n㎱ns\n㎲μs\n㎳ms\n㎴pV\n㎵nV\n㎶μV\n㎷mV\n㎸kV\n㎹MV\n㎺pW\n㎻nW\n㎼μW\n㎽mW\n㎾kW\n㎿MW\n㏀kΩ\n㏁MΩ\n㏂a.m.\n㏃Bq\n㏄cc\n㏅cd\n㏆C∕kg\n㏇Co.\n㏈dB\n㏉Gy\n㏊ha\n㏋HP\n㏌in\n㏍KK\n㏎KM\n㏏kt\n㏐lm\n㏑ln\n㏒log\n㏓lx\n㏔mb\n㏕mil\n㏖mol\n㏗PH\n㏘p.m.\n㏙PPM\n㏚PR\n㏛sr\n㏜Sv\n㏝Wb\n㏞V∕m\n㏟A∕m\n㏠1日\n㏡2日\n㏢3日\n㏣4日\n㏤5日\n㏥6日\n㏦7日\n㏧8日\n㏨9日\n㏩10日\n㏪11日\n㏫12日\n㏬13日\n㏭14日\n㏮15日\n㏯16日\n㏰17日\n㏱18日\n㏲19日\n㏳20日\n㏴21日\n㏵22日\n㏶23日\n㏷24日\n㏸25日\n㏹26日\n㏺27日\n㏻28日\n㏼29日\n㏽30日\n㏾31日\n㏿gal\n豈豈\n更更\n車車\n賈賈\n滑滑\n串串\n句句\n龜龜\n龜龜\n契契\n金金\n喇喇\n奈奈\n懶懶\n癩癩\n羅羅\n蘿蘿\n螺螺\n裸裸\n邏邏\n樂樂\n洛洛\n烙烙\n珞珞\n落落\n酪酪\n駱駱\n亂亂\n卵卵\n欄欄\n爛爛\n蘭蘭\n鸞鸞\n嵐嵐\n濫濫\n藍藍\n襤襤\n拉拉\n臘臘\n蠟蠟\n廊廊\n朗朗\n浪浪\n狼狼\n郎郎\n來來\n冷冷\n勞勞\n擄擄\n櫓櫓\n爐爐\n盧盧\n老老\n蘆蘆\n虜虜\n路路\n露露\n魯魯\n鷺鷺\n碌碌\n祿祿\n綠綠\n菉菉\n錄錄\n鹿鹿\n論論\n壟壟\n弄弄\n籠籠\n聾聾\n牢牢\n磊磊\n賂賂\n雷雷\n壘壘\n屢屢\n樓樓\n淚淚\n漏漏\n累累\n縷縷\n陋陋\n勒勒\n肋肋\n凜凜\n凌凌\n稜稜\n綾綾\n菱菱\n陵陵\n讀讀\n拏拏\n樂樂\n諾諾\n丹丹\n寧寧\n怒怒\n率率\n異異\n北北\n磻磻\n便便\n復復\n不不\n泌泌\n數數\n索索\n參參\n塞塞\n省省\n葉葉\n說說\n殺殺\n辰辰\n沈沈\n拾拾\n若若\n掠掠\n略略\n亮亮\n兩兩\n凉凉\n梁梁\n糧糧\n良良\n諒諒\n量量\n勵勵\n呂呂\n女女\n廬廬\n旅旅\n濾濾\n礪礪\n閭閭\n驪驪\n麗麗\n黎黎\n力力\n曆曆\n歷歷\n轢轢\n年年\n憐憐\n戀戀\n撚撚\n漣漣\n煉煉\n璉璉\n秊秊\n練練\n聯聯\n輦輦\n蓮蓮\n連連\n鍊鍊\n列列\n劣劣\n咽咽\n烈烈\n裂裂\n說說\n廉廉\n念念\n捻捻\n殮殮\n簾簾\n獵獵\n令令\n囹囹\n寧寧\n嶺嶺\n怜怜\n玲玲\n瑩瑩\n羚羚\n聆聆\n鈴鈴\n零零\n靈靈\n領領\n例例\n禮禮\n醴醴\n隸隸\n惡惡\n了了\n僚僚\n寮寮\n尿尿\n料料\n樂樂\n燎燎\n療療\n蓼蓼\n遼遼\n龍龍\n暈暈\n阮阮\n劉劉\n杻杻\n柳柳\n流流\n溜溜\n琉琉\n留留\n硫硫\n紐紐\n類類\n六六\n戮戮\n陸陸\n倫倫\n崙崙\n淪淪\n輪輪\n律律\n慄慄\n栗栗\n率率\n隆隆\n利利\n吏吏\n履履\n易易\n李李\n梨梨\n泥泥\n理理\n痢痢\n罹罹\n裏裏\n裡裡\n里里\n離離\n匿匿\n溺溺\n吝吝\n燐燐\n璘璘\n藺藺\n隣隣\n鱗鱗\n麟麟\n林林\n淋淋\n臨臨\n立立\n笠笠\n粒粒\n狀狀\n炙炙\n識識\n什什\n茶茶\n刺刺\n切切\n度度\n拓拓\n糖糖\n宅宅\n洞洞\n暴暴\n輻輻\n行行\n降降\n見見\n廓廓\n兀兀\n嗀嗀\n塚塚\n晴晴\n凞凞\n猪猪\n益益\n礼礼\n神神\n祥祥\n福福\n靖靖\n精精\n羽羽\n蘒蘒\n諸諸\n逸逸\n都都\n飯飯\n飼飼\n館館\n鶴鶴\n侮侮\n僧僧\n免免\n勉勉\n勤勤\n卑卑\n喝喝\n嘆嘆\n器器\n塀塀\n墨墨\n層層\n屮屮\n悔悔\n慨慨\n憎憎\n懲懲\n敏敏\n既既\n暑暑\n梅梅\n海海\n渚渚\n漢漢\n煮煮\n爫爫\n琢琢\n碑碑\n社社\n祉祉\n祈祈\n祐祐\n祖祖\n祝祝\n禍禍\n禎禎\n穀穀\n突突\n節節\n練練\n縉縉\n繁繁\n署署\n者者\n臭臭\n艹艹\n艹艹\n著著\n褐褐\n視視\n謁謁\n謹謹\n賓賓\n贈贈\n辶辶\n逸逸\n難難\n響響\n頻頻\n並並\n况况\n全全\n侀侀\n充充\n冀冀\n勇勇\n勺勺\n喝喝\n啕啕\n喙喙\n嗢嗢\n塚塚\n墳墳\n奄奄\n奔奔\n婢婢\n嬨嬨\n廒廒\n廙廙\n彩彩\n徭徭\n惘惘\n慎慎\n愈愈\n憎憎\n慠慠\n懲懲\n戴戴\n揄揄\n搜搜\n摒摒\n敖敖\n晴晴\n朗朗\n望望\n杖杖\n歹歹\n殺殺\n流流\n滛滛\n滋滋\n漢漢\n瀞瀞\n煮煮\n瞧瞧\n爵爵\n犯犯\n猪猪\n瑱瑱\n甆甆\n画画\n瘝瘝\n瘟瘟\n益益\n盛盛\n直直\n睊睊\n着着\n磌磌\n窱窱\n節節\n类类\n絛絛\n練練\n缾缾\n者者\n荒荒\n華華\n蝹蝹\n襁襁\n覆覆\n視視\n調調\n諸諸\n請請\n謁謁\n諾諾\n諭諭\n謹謹\n變變\n贈贈\n輸輸\n遲遲\n醙醙\n鉶鉶\n陼陼\n難難\n靖靖\n韛韛\n響響\n頋頋\n頻頻\n鬒鬒\n龜龜\n𢡊𢡊\n𢡄𢡄\n𣏕𣏕\n㮝㮝\n䀘䀘\n䀹䀹\n𥉉𥉉\n𥳐𥳐\n𧻓𧻓\n齃齃\n龎龎\n!!\n"\"\n##\n$$\n%%\n&&\n''\n((\n))\n**\n++\n,,\n--\n..\n//\n00\n11\n22\n33\n44\n55\n66\n77\n88\n99\n::\n;;\n<<\n==\n>>\n??\n@@\nAA\nBB\nCC\nDD\nEE\nFF\nGG\nHH\nII\nJJ\nKK\nLL\nMM\nNN\nOO\nPP\nQQ\nRR\nSS\nTT\nUU\nVV\nWW\nXX\nYY\nZZ\n[[\n\\\n]]\n^^\n__\n``\naa\nbb\ncc\ndd\nee\nff\ngg\nhh\nii\njj\nkk\nll\nmm\nnn\noo\npp\nqq\nrr\nss\ntt\nuu\nvv\nww\nxx\nyy\nzz\n{{\n||\n}}\n~~\n⦅⦅\n⦆⦆\n。。\n「「\n」」\n、、\n・・\nヲヲ\nァァ\nィィ\nゥゥ\nェェ\nォォ\nャャ\nュュ\nョョ\nッッ\nーー\nアア\nイイ\nウウ\nエエ\nオオ\nカカ\nキキ\nクク\nケケ\nココ\nササ\nシシ\nスス\nセセ\nソソ\nタタ\nチチ\nツツ\nテテ\nトト\nナナ\nニニ\nヌヌ\nネネ\nノノ\nハハ\nヒヒ\nフフ\nヘヘ\nホホ\nママ\nミミ\nムム\nメメ\nモモ\nヤヤ\nユユ\nヨヨ\nララ\nリリ\nルル\nレレ\nロロ\nワワ\nンン\n゙゙\n゚゚\nᅠᅠ\nᄀᄀ\nᄁᄁ\nᆪᆪ\nᄂᄂ\nᆬᆬ\nᆭᆭ\nᄃᄃ\nᄄᄄ\nᄅᄅ\nᆰᆰ\nᆱᆱ\nᆲᆲ\nᆳᆳ\nᆴᆴ\nᆵᆵ\nᄚᄚ\nᄆᄆ\nᄇᄇ\nᄈᄈ\nᄡᄡ\nᄉᄉ\nᄊᄊ\nᄋᄋ\nᄌᄌ\nᄍᄍ\nᄎᄎ\nᄏᄏ\nᄐᄐ\nᄑᄑ\nᄒᄒ\nᅡᅡ\nᅢᅢ\nᅣᅣ\nᅤᅤ\nᅥᅥ\nᅦᅦ\nᅧᅧ\nᅨᅨ\nᅩᅩ\nᅪᅪ\nᅫᅫ\nᅬᅬ\nᅭᅭ\nᅮᅮ\nᅯᅯ\nᅰᅰ\nᅱᅱ\nᅲᅲ\nᅳᅳ\nᅴᅴ\nᅵᅵ\n¢¢\n££\n¬¬\n ̄ ̄\n¦¦\n¥¥\n₩₩\n││\n←←\n↑↑\n→→\n↓↓\n■■\n○○\n"; - } + static int[][] _SupportedChars = new int[][] + { + new[] { 0, 1000 }, + new[] { 12352, 12447 }, + new[] { 12448, 12543 }, + new[] { 19968, 40959 }, + new[] { 13312, 19967 }, + new[] { 131072, 173791 }, + new[] { 63744, 64255 }, + new[] { 194560, 195103 }, + new[] { 13056, 13311 }, + new[] { 12288, 12351 }, + new[] { 65280, 65535 }, + new[] { 8192, 8303 }, + new[] { 8352, 8399 }, + }; + const string _SubstitutionTable = + "  \n¨ ̈\nªa\n¯ ̄\n²2\n³3\n´ ́\nµμ\n¸ ̧\n¹1\nºo\n¼1⁄4\n½1⁄2\n¾3⁄4\nÀÀ\nÁÁ\nÂÂ\nÃÃ\nÄÄ\nÅÅ\nÇÇ\nÈÈ\nÉÉ\nÊÊ\nËË\nÌÌ\nÍÍ\nÎÎ\nÏÏ\nÑÑ\nÒÒ\nÓÓ\nÔÔ\nÕÕ\nÖÖ\nÙÙ\nÚÚ\nÛÛ\nÜÜ\nÝÝ\nàà\náá\nââ\nãã\nää\nåå\nçç\nèè\néé\nêê\nëë\nìì\níí\nîî\nïï\nññ\nòò\nóó\nôô\nõõ\nöö\nùù\núú\nûû\nüü\nýý\nÿÿ\nĀĀ\nāā\nĂĂ\năă\nĄĄ\nąą\nĆĆ\nćć\nĈĈ\nĉĉ\nĊĊ\nċċ\nČČ\nčč\nĎĎ\nďď\nĒĒ\nēē\nĔĔ\nĕĕ\nĖĖ\nėė\nĘĘ\nęę\nĚĚ\něě\nĜĜ\nĝĝ\nĞĞ\nğğ\nĠĠ\nġġ\nĢĢ\nģģ\nĤĤ\nĥĥ\nĨĨ\nĩĩ\nĪĪ\nīī\nĬĬ\nĭĭ\nĮĮ\nįį\nİİ\nIJIJ\nijij\nĴĴ\nĵĵ\nĶĶ\nķķ\nĹĹ\nĺĺ\nĻĻ\nļļ\nĽĽ\nľľ\nĿL·\nŀl·\nŃŃ\nńń\nŅŅ\nņņ\nŇŇ\nňň\nʼnʼn\nŌŌ\nōō\nŎŎ\nŏŏ\nŐŐ\nőő\nŔŔ\nŕŕ\nŖŖ\nŗŗ\nŘŘ\nřř\nŚŚ\nśś\nŜŜ\nŝŝ\nŞŞ\nşş\nŠŠ\nšš\nŢŢ\nţţ\nŤŤ\nťť\nŨŨ\nũũ\nŪŪ\nūū\nŬŬ\nŭŭ\nŮŮ\nůů\nŰŰ\nűű\nŲŲ\nųų\nŴŴ\nŵŵ\nŶŶ\nŷŷ\nŸŸ\nŹŹ\nźź\nŻŻ\nżż\nŽŽ\nžž\nſs\nƠƠ\nơơ\nƯƯ\nưư\nDŽDŽ\nDžDž\ndždž\nLJLJ\nLjLj\nljlj\nNJNJ\nNjNj\nnjnj\nǍǍ\nǎǎ\nǏǏ\nǐǐ\nǑǑ\nǒǒ\nǓǓ\nǔǔ\nǕǕ\nǖǖ\nǗǗ\nǘǘ\nǙǙ\nǚǚ\nǛǛ\nǜǜ\nǞǞ\nǟǟ\nǠǠ\nǡǡ\nǢǢ\nǣǣ\nǦǦ\nǧǧ\nǨǨ\nǩǩ\nǪǪ\nǫǫ\nǬǬ\nǭǭ\nǮǮ\nǯǯ\nǰǰ\nDZDZ\nDzDz\ndzdz\nǴǴ\nǵǵ\nǸǸ\nǹǹ\nǺǺ\nǻǻ\nǼǼ\nǽǽ\nǾǾ\nǿǿ\nȀȀ\nȁȁ\nȂȂ\nȃȃ\nȄȄ\nȅȅ\nȆȆ\nȇȇ\nȈȈ\nȉȉ\nȊȊ\nȋȋ\nȌȌ\nȍȍ\nȎȎ\nȏȏ\nȐȐ\nȑȑ\nȒȒ\nȓȓ\nȔȔ\nȕȕ\nȖȖ\nȗȗ\nȘȘ\nșș\nȚȚ\nțț\nȞȞ\nȟȟ\nȦȦ\nȧȧ\nȨȨ\nȩȩ\nȪȪ\nȫȫ\nȬȬ\nȭȭ\nȮȮ\nȯȯ\nȰȰ\nȱȱ\nȲȲ\nȳȳ\nʰh\nʱɦ\nʲj\nʳr\nʴɹ\nʵɻ\nʶʁ\nʷw\nʸy\n˘ ̆\n˙ ̇\n˚ ̊\n˛ ̨\n˜ ̃\n˝ ̋\nˠɣ\nˡl\nˢs\nˣx\nˤʕ\ǹ̀\ń́\n̓̓\n̈́̈́\nʹʹ\nͺ ͅ\n;;\n΄ ́\n΅ ̈́\nΆΆ\n··\nΈΈ\nΉΉ\nΊΊ\nΌΌ\nΎΎ\nΏΏ\nΐΐ\nΪΪ\nΫΫ\nάά\nέέ\nήή\nίί\nΰΰ\nϊϊ\nϋϋ\nόό\nύύ\nώώ\nϐβ\nϑθ\nϒΥ\nϓΎ\nϔΫ\nϕφ\nϖπ\nϰκ\nϱρ\nϲς\nϴΘ\nϵε\nϹΣ\nЀЀ\nЁЁ\nЃЃ\nЇЇ\nЌЌ\nЍЍ\nЎЎ\nЙЙ\nйй\nѐѐ\nёё\nѓѓ\nїї\nќќ\nѝѝ\nўў\nѶѶ\nѷѷ\nӁӁ\nӂӂ\nӐӐ\nӑӑ\nӒӒ\nӓӓ\nӖӖ\nӗӗ\nӚӚ\nӛӛ\nӜӜ\nӝӝ\nӞӞ\nӟӟ\nӢӢ\nӣӣ\nӤӤ\nӥӥ\nӦӦ\nӧӧ\nӪӪ\nӫӫ\nӬӬ\nӭӭ\nӮӮ\nӯӯ\nӰӰ\nӱӱ\nӲӲ\nӳӳ\nӴӴ\nӵӵ\nӸӸ\nӹӹ\nևեւ\nآآ\nأأ\nؤؤ\nإإ\nئئ\nٵاٴ\nٶوٴ\nٷۇٴ\nٸيٴ\nۀۀ\nۂۂ\nۓۓ\nऩऩ\nऱऱ\nऴऴ\nक़क़\nख़ख़\nग़ग़\nज़ज़\nड़ड़\nढ़ढ़\nफ़फ़\nय़य़\nোো\nৌৌ\nড়ড়\nঢ়ঢ়\nয়য়\nਲ਼ਲ਼\nਸ਼ਸ਼\nਖ਼ਖ਼\nਗ਼ਗ਼\nਜ਼ਜ਼\nਫ਼ਫ਼\nୈୈ\nୋୋ\nୌୌ\nଡ଼ଡ଼\nଢ଼ଢ଼\nஔஔ\nொொ\nோோ\nௌௌ\nైై\nೀೀ\nೇೇ\nೈೈ\nೊೊ\nೋೋ\nൊൊ\nോോ\nൌൌ\nේේ\nොො\nෝෝ\nෞෞ\nำํา\nຳໍາ\nໜຫນ\nໝຫມ\n༌་\nགྷགྷ\nཌྷཌྷ\nདྷདྷ\nབྷབྷ\nཛྷཛྷ\nཀྵཀྵ\nཱཱིི\nཱཱུུ\nྲྀྲྀ\nཷྲཱྀ\nླྀླྀ\nཹླཱྀ\nཱཱྀྀ\nྒྷྒྷ\nྜྷྜྷ\nྡྷྡྷ\nྦྷྦྷ\nྫྷྫྷ\nྐྵྐྵ\nဦဦ\nჼნ\nᬆᬆ\nᬈᬈ\nᬊᬊ\nᬌᬌ\nᬎᬎ\nᬒᬒ\nᬻᬻ\nᬽᬽ\nᭀᭀ\nᭁᭁ\nᭃᭃ\nᴬA\nᴭÆ\nᴮB\nᴰD\nᴱE\nᴲƎ\nᴳG\nᴴH\nᴵI\nᴶJ\nᴷK\nᴸL\nᴹM\nᴺN\nᴼO\nᴽȢ\nᴾP\nᴿR\nᵀT\nᵁU\nᵂW\nᵃa\nᵄɐ\nᵅɑ\nᵆᴂ\nᵇb\nᵈd\nᵉe\nᵊə\nᵋɛ\nᵌɜ\nᵍg\nᵏk\nᵐm\nᵑŋ\nᵒo\nᵓɔ\nᵔᴖ\nᵕᴗ\nᵖp\nᵗt\nᵘu\nᵙᴝ\nᵚɯ\nᵛv\nᵜᴥ\nᵝβ\nᵞγ\nᵟδ\nᵠφ\nᵡχ\nᵢi\nᵣr\nᵤu\nᵥv\nᵦβ\nᵧγ\nᵨρ\nᵩφ\nᵪχ\nᵸн\nᶛɒ\nᶜc\nᶝɕ\nᶞð\nᶟɜ\nᶠf\nᶡɟ\nᶢɡ\nᶣɥ\nᶤɨ\nᶥɩ\nᶦɪ\nᶧᵻ\nᶨʝ\nᶩɭ\nᶪᶅ\nᶫʟ\nᶬɱ\nᶭɰ\nᶮɲ\nᶯɳ\nᶰɴ\nᶱɵ\nᶲɸ\nᶳʂ\nᶴʃ\nᶵƫ\nᶶʉ\nᶷʊ\nᶸᴜ\nᶹʋ\nᶺʌ\nᶻz\nᶼʐ\nᶽʑ\nᶾʒ\nᶿθ\nḀḀ\nḁḁ\nḂḂ\nḃḃ\nḄḄ\nḅḅ\nḆḆ\nḇḇ\nḈḈ\nḉḉ\nḊḊ\nḋḋ\nḌḌ\nḍḍ\nḎḎ\nḏḏ\nḐḐ\nḑḑ\nḒḒ\nḓḓ\nḔḔ\nḕḕ\nḖḖ\nḗḗ\nḘḘ\nḙḙ\nḚḚ\nḛḛ\nḜḜ\nḝḝ\nḞḞ\nḟḟ\nḠḠ\nḡḡ\nḢḢ\nḣḣ\nḤḤ\nḥḥ\nḦḦ\nḧḧ\nḨḨ\nḩḩ\nḪḪ\nḫḫ\nḬḬ\nḭḭ\nḮḮ\nḯḯ\nḰḰ\nḱḱ\nḲḲ\nḳḳ\nḴḴ\nḵḵ\nḶḶ\nḷḷ\nḸḸ\nḹḹ\nḺḺ\nḻḻ\nḼḼ\nḽḽ\nḾḾ\nḿḿ\nṀṀ\nṁṁ\nṂṂ\nṃṃ\nṄṄ\nṅṅ\nṆṆ\nṇṇ\nṈṈ\nṉṉ\nṊṊ\nṋṋ\nṌṌ\nṍṍ\nṎṎ\nṏṏ\nṐṐ\nṑṑ\nṒṒ\nṓṓ\nṔṔ\nṕṕ\nṖṖ\nṗṗ\nṘṘ\nṙṙ\nṚṚ\nṛṛ\nṜṜ\nṝṝ\nṞṞ\nṟṟ\nṠṠ\nṡṡ\nṢṢ\nṣṣ\nṤṤ\nṥṥ\nṦṦ\nṧṧ\nṨṨ\nṩṩ\nṪṪ\nṫṫ\nṬṬ\nṭṭ\nṮṮ\nṯṯ\nṰṰ\nṱṱ\nṲṲ\nṳṳ\nṴṴ\nṵṵ\nṶṶ\nṷṷ\nṸṸ\nṹṹ\nṺṺ\nṻṻ\nṼṼ\nṽṽ\nṾṾ\nṿṿ\nẀẀ\nẁẁ\nẂẂ\nẃẃ\nẄẄ\nẅẅ\nẆẆ\nẇẇ\nẈẈ\nẉẉ\nẊẊ\nẋẋ\nẌẌ\nẍẍ\nẎẎ\nẏẏ\nẐẐ\nẑẑ\nẒẒ\nẓẓ\nẔẔ\nẕẕ\nẖẖ\nẗẗ\nẘẘ\nẙẙ\nẚaʾ\nẛṡ\nẠẠ\nạạ\nẢẢ\nảả\nẤẤ\nấấ\nẦẦ\nầầ\nẨẨ\nẩẩ\nẪẪ\nẫẫ\nẬẬ\nậậ\nẮẮ\nắắ\nẰẰ\nằằ\nẲẲ\nẳẳ\nẴẴ\nẵẵ\nẶẶ\nặặ\nẸẸ\nẹẹ\nẺẺ\nẻẻ\nẼẼ\nẽẽ\nẾẾ\nếế\nỀỀ\nềề\nỂỂ\nểể\nỄỄ\nễễ\nỆỆ\nệệ\nỈỈ\nỉỉ\nỊỊ\nịị\nỌỌ\nọọ\nỎỎ\nỏỏ\nỐỐ\nốố\nỒỒ\nồồ\nỔỔ\nổổ\nỖỖ\nỗỗ\nỘỘ\nộộ\nỚỚ\nớớ\nỜỜ\nờờ\nỞỞ\nởở\nỠỠ\nỡỡ\nỢỢ\nợợ\nỤỤ\nụụ\nỦỦ\nủủ\nỨỨ\nứứ\nỪỪ\nừừ\nỬỬ\nửử\nỮỮ\nữữ\nỰỰ\nựự\nỲỲ\nỳỳ\nỴỴ\nỵỵ\nỶỶ\nỷỷ\nỸỸ\nỹỹ\nἀἀ\nἁἁ\nἂἂ\nἃἃ\nἄἄ\nἅἅ\nἆἆ\nἇἇ\nἈἈ\nἉἉ\nἊἊ\nἋἋ\nἌἌ\nἍἍ\nἎἎ\nἏἏ\nἐἐ\nἑἑ\nἒἒ\nἓἓ\nἔἔ\nἕἕ\nἘἘ\nἙἙ\nἚἚ\nἛἛ\nἜἜ\nἝἝ\nἠἠ\nἡἡ\nἢἢ\nἣἣ\nἤἤ\nἥἥ\nἦἦ\nἧἧ\nἨἨ\nἩἩ\nἪἪ\nἫἫ\nἬἬ\nἭἭ\nἮἮ\nἯἯ\nἰἰ\nἱἱ\nἲἲ\nἳἳ\nἴἴ\nἵἵ\nἶἶ\nἷἷ\nἸἸ\nἹἹ\nἺἺ\nἻἻ\nἼἼ\nἽἽ\nἾἾ\nἿἿ\nὀὀ\nὁὁ\nὂὂ\nὃὃ\nὄὄ\nὅὅ\nὈὈ\nὉὉ\nὊὊ\nὋὋ\nὌὌ\nὍὍ\nὐὐ\nὑὑ\nὒὒ\nὓὓ\nὔὔ\nὕὕ\nὖὖ\nὗὗ\nὙὙ\nὛὛ\nὝὝ\nὟὟ\nὠὠ\nὡὡ\nὢὢ\nὣὣ\nὤὤ\nὥὥ\nὦὦ\nὧὧ\nὨὨ\nὩὩ\nὪὪ\nὫὫ\nὬὬ\nὭὭ\nὮὮ\nὯὯ\nὰὰ\nάά\nὲὲ\nέέ\nὴὴ\nήή\nὶὶ\nίί\nὸὸ\nόό\nὺὺ\nύύ\nὼὼ\nώώ\nᾀᾀ\nᾁᾁ\nᾂᾂ\nᾃᾃ\nᾄᾄ\nᾅᾅ\nᾆᾆ\nᾇᾇ\nᾈᾈ\nᾉᾉ\nᾊᾊ\nᾋᾋ\nᾌᾌ\nᾍᾍ\nᾎᾎ\nᾏᾏ\nᾐᾐ\nᾑᾑ\nᾒᾒ\nᾓᾓ\nᾔᾔ\nᾕᾕ\nᾖᾖ\nᾗᾗ\nᾘᾘ\nᾙᾙ\nᾚᾚ\nᾛᾛ\nᾜᾜ\nᾝᾝ\nᾞᾞ\nᾟᾟ\nᾠᾠ\nᾡᾡ\nᾢᾢ\nᾣᾣ\nᾤᾤ\nᾥᾥ\nᾦᾦ\nᾧᾧ\nᾨᾨ\nᾩᾩ\nᾪᾪ\nᾫᾫ\nᾬᾬ\nᾭᾭ\nᾮᾮ\nᾯᾯ\nᾰᾰ\nᾱᾱ\nᾲᾲ\nᾳᾳ\nᾴᾴ\nᾶᾶ\nᾷᾷ\nᾸᾸ\nᾹᾹ\nᾺᾺ\nΆΆ\nᾼᾼ\n᾽ ̓\nιι\n᾿ ̓\n῀ ͂\n῁ ̈͂\nῂῂ\nῃῃ\nῄῄ\nῆῆ\nῇῇ\nῈῈ\nΈΈ\nῊῊ\nΉΉ\nῌῌ\n῍ ̓̀\n῎ ̓́\n῏ ̓͂\nῐῐ\nῑῑ\nῒῒ\nΐΐ\nῖῖ\nῗῗ\nῘῘ\nῙῙ\nῚῚ\nΊΊ\n῝ ̔̀\n῞ ̔́\n῟ ̔͂\nῠῠ\nῡῡ\nῢῢ\nΰΰ\nῤῤ\nῥῥ\nῦῦ\nῧῧ\nῨῨ\nῩῩ\nῪῪ\nΎΎ\nῬῬ\n῭ ̈̀\n΅ ̈́\n``\nῲῲ\nῳῳ\nῴῴ\nῶῶ\nῷῷ\nῸῸ\nΌΌ\nῺῺ\nΏΏ\nῼῼ\n´ ́\n῾ ̔\n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n‑‐\n‗ ̳\n․.\n‥..\n…...\n  \n″′′\n‴′′′\n‶‵‵\n‷‵‵‵\n‼!!\n‾ ̅\n⁇??\n⁈?!\n⁉!?\n⁗′′′′\n  \n⁰0\nⁱi\n⁴4\n⁵5\n⁶6\n⁷7\n⁸8\n⁹9\n⁺+\n⁻−\n⁼=\n⁽(\n⁾)\nⁿn\n₀0\n₁1\n₂2\n₃3\n₄4\n₅5\n₆6\n₇7\n₈8\n₉9\n₊+\n₋−\n₌=\n₍(\n₎)\nₐa\nₑe\nₒo\nₓx\nₔə\n₨Rs\n℀a/c\n℁a/s\nℂC\n℃°C\n℅c/o\n℆c/u\nℇƐ\n℉°F\nℊg\nℋH\nℌH\nℍH\nℎh\nℏħ\nℐI\nℑI\nℒL\nℓl\nℕN\n№No\nℙP\nℚQ\nℛR\nℜR\nℝR\n℠SM\n℡TEL\n™TM\nℤZ\nΩΩ\nℨZ\nKK\nÅÅ\nℬB\nℭC\nℯe\nℰE\nℱF\nℳM\nℴo\nℵא\nℶב\nℷג\nℸד\nℹi\n℻FAX\nℼπ\nℽγ\nℾΓ\nℿΠ\n⅀∑\nⅅD\nⅆd\nⅇe\nⅈi\nⅉj\n⅓1⁄3\n⅔2⁄3\n⅕1⁄5\n⅖2⁄5\n⅗3⁄5\n⅘4⁄5\n⅙1⁄6\n⅚5⁄6\n⅛1⁄8\n⅜3⁄8\n⅝5⁄8\n⅞7⁄8\n⅟1⁄\nⅠI\nⅡII\nⅢIII\nⅣIV\nⅤV\nⅥVI\nⅦVII\nⅧVIII\nⅨIX\nⅩX\nⅪXI\nⅫXII\nⅬL\nⅭC\nⅮD\nⅯM\nⅰi\nⅱii\nⅲiii\nⅳiv\nⅴv\nⅵvi\nⅶvii\nⅷviii\nⅸix\nⅹx\nⅺxi\nⅻxii\nⅼl\nⅽc\nⅾd\nⅿm\n↚↚\n↛↛\n↮↮\n⇍⇍\n⇎⇎\n⇏⇏\n∄∄\n∉∉\n∌∌\n∤∤\n∦∦\n∬∫∫\n∭∫∫∫\n∯∮∮\n∰∮∮∮\n≁≁\n≄≄\n≇≇\n≉≉\n≠≠\n≢≢\n≭≭\n≮≮\n≯≯\n≰≰\n≱≱\n≴≴\n≵≵\n≸≸\n≹≹\n⊀⊀\n⊁⊁\n⊄⊄\n⊅⊅\n⊈⊈\n⊉⊉\n⊬⊬\n⊭⊭\n⊮⊮\n⊯⊯\n⋠⋠\n⋡⋡\n⋢⋢\n⋣⋣\n⋪⋪\n⋫⋫\n⋬⋬\n⋭⋭\n〈〈\n〉〉\n①1\n②2\n③3\n④4\n⑤5\n⑥6\n⑦7\n⑧8\n⑨9\n⑩10\n⑪11\n⑫12\n⑬13\n⑭14\n⑮15\n⑯16\n⑰17\n⑱18\n⑲19\n⑳20\n⑴(1)\n⑵(2)\n⑶(3)\n⑷(4)\n⑸(5)\n⑹(6)\n⑺(7)\n⑻(8)\n⑼(9)\n⑽(10)\n⑾(11)\n⑿(12)\n⒀(13)\n⒁(14)\n⒂(15)\n⒃(16)\n⒄(17)\n⒅(18)\n⒆(19)\n⒇(20)\n⒈1.\n⒉2.\n⒊3.\n⒋4.\n⒌5.\n⒍6.\n⒎7.\n⒏8.\n⒐9.\n⒑10.\n⒒11.\n⒓12.\n⒔13.\n⒕14.\n⒖15.\n⒗16.\n⒘17.\n⒙18.\n⒚19.\n⒛20.\n⒜(a)\n⒝(b)\n⒞(c)\n⒟(d)\n⒠(e)\n⒡(f)\n⒢(g)\n⒣(h)\n⒤(i)\n⒥(j)\n⒦(k)\n⒧(l)\n⒨(m)\n⒩(n)\n⒪(o)\n⒫(p)\n⒬(q)\n⒭(r)\n⒮(s)\n⒯(t)\n⒰(u)\n⒱(v)\n⒲(w)\n⒳(x)\n⒴(y)\n⒵(z)\nⒶA\nⒷB\nⒸC\nⒹD\nⒺE\nⒻF\nⒼG\nⒽH\nⒾI\nⒿJ\nⓀK\nⓁL\nⓂM\nⓃN\nⓄO\nⓅP\nⓆQ\nⓇR\nⓈS\nⓉT\nⓊU\nⓋV\nⓌW\nⓍX\nⓎY\nⓏZ\nⓐa\nⓑb\nⓒc\nⓓd\nⓔe\nⓕf\nⓖg\nⓗh\nⓘi\nⓙj\nⓚk\nⓛl\nⓜm\nⓝn\nⓞo\nⓟp\nⓠq\nⓡr\nⓢs\nⓣt\nⓤu\nⓥv\nⓦw\nⓧx\nⓨy\nⓩz\n⓪0\n⨌∫∫∫∫\n⩴::=\n⩵==\n⩶===\n⫝̸⫝̸\nⱼj\nⱽV\nⵯⵡ\n⺟母\n⻳龟\n⼀一\n⼁丨\n⼂丶\n⼃丿\n⼄乙\n⼅亅\n⼆二\n⼇亠\n⼈人\n⼉儿\n⼊入\n⼋八\n⼌冂\n⼍冖\n⼎冫\n⼏几\n⼐凵\n⼑刀\n⼒力\n⼓勹\n⼔匕\n⼕匚\n⼖匸\n⼗十\n⼘卜\n⼙卩\n⼚厂\n⼛厶\n⼜又\n⼝口\n⼞囗\n⼟土\n⼠士\n⼡夂\n⼢夊\n⼣夕\n⼤大\n⼥女\n⼦子\n⼧宀\n⼨寸\n⼩小\n⼪尢\n⼫尸\n⼬屮\n⼭山\n⼮巛\n⼯工\n⼰己\n⼱巾\n⼲干\n⼳幺\n⼴广\n⼵廴\n⼶廾\n⼷弋\n⼸弓\n⼹彐\n⼺彡\n⼻彳\n⼼心\n⼽戈\n⼾戶\n⼿手\n⽀支\n⽁攴\n⽂文\n⽃斗\n⽄斤\n⽅方\n⽆无\n⽇日\n⽈曰\n⽉月\n⽊木\n⽋欠\n⽌止\n⽍歹\n⽎殳\n⽏毋\n⽐比\n⽑毛\n⽒氏\n⽓气\n⽔水\n⽕火\n⽖爪\n⽗父\n⽘爻\n⽙爿\n⽚片\n⽛牙\n⽜牛\n⽝犬\n⽞玄\n⽟玉\n⽠瓜\n⽡瓦\n⽢甘\n⽣生\n⽤用\n⽥田\n⽦疋\n⽧疒\n⽨癶\n⽩白\n⽪皮\n⽫皿\n⽬目\n⽭矛\n⽮矢\n⽯石\n⽰示\n⽱禸\n⽲禾\n⽳穴\n⽴立\n⽵竹\n⽶米\n⽷糸\n⽸缶\n⽹网\n⽺羊\n⽻羽\n⽼老\n⽽而\n⽾耒\n⽿耳\n⾀聿\n⾁肉\n⾂臣\n⾃自\n⾄至\n⾅臼\n⾆舌\n⾇舛\n⾈舟\n⾉艮\n⾊色\n⾋艸\n⾌虍\n⾍虫\n⾎血\n⾏行\n⾐衣\n⾑襾\n⾒見\n⾓角\n⾔言\n⾕谷\n⾖豆\n⾗豕\n⾘豸\n⾙貝\n⾚赤\n⾛走\n⾜足\n⾝身\n⾞車\n⾟辛\n⾠辰\n⾡辵\n⾢邑\n⾣酉\n⾤釆\n⾥里\n⾦金\n⾧長\n⾨門\n⾩阜\n⾪隶\n⾫隹\n⾬雨\n⾭靑\n⾮非\n⾯面\n⾰革\n⾱韋\n⾲韭\n⾳音\n⾴頁\n⾵風\n⾶飛\n⾷食\n⾸首\n⾹香\n⾺馬\n⾻骨\n⾼高\n⾽髟\n⾾鬥\n⾿鬯\n⿀鬲\n⿁鬼\n⿂魚\n⿃鳥\n⿄鹵\n⿅鹿\n⿆麥\n⿇麻\n⿈黃\n⿉黍\n⿊黑\n⿋黹\n⿌黽\n⿍鼎\n⿎鼓\n⿏鼠\n⿐鼻\n⿑齊\n⿒齒\n⿓龍\n⿔龜\n⿕龠\n  \n〶〒\n〸十\n〹卄\n〺卅\nがが\nぎぎ\nぐぐ\nげげ\nごご\nざざ\nじじ\nずず\nぜぜ\nぞぞ\nだだ\nぢぢ\nづづ\nでで\nどど\nばば\nぱぱ\nびび\nぴぴ\nぶぶ\nぷぷ\nべべ\nぺぺ\nぼぼ\nぽぽ\nゔゔ\n゛ ゙\n゜ ゚\nゞゞ\nゟより\nガガ\nギギ\nググ\nゲゲ\nゴゴ\nザザ\nジジ\nズズ\nゼゼ\nゾゾ\nダダ\nヂヂ\nヅヅ\nデデ\nドド\nババ\nパパ\nビビ\nピピ\nブブ\nププ\nベベ\nペペ\nボボ\nポポ\nヴヴ\nヷヷ\nヸヸ\nヹヹ\nヺヺ\nヾヾ\nヿコト\nㄱᄀ\nㄲᄁ\nㄳᆪ\nㄴᄂ\nㄵᆬ\nㄶᆭ\nㄷᄃ\nㄸᄄ\nㄹᄅ\nㄺᆰ\nㄻᆱ\nㄼᆲ\nㄽᆳ\nㄾᆴ\nㄿᆵ\nㅀᄚ\nㅁᄆ\nㅂᄇ\nㅃᄈ\nㅄᄡ\nㅅᄉ\nㅆᄊ\nㅇᄋ\nㅈᄌ\nㅉᄍ\nㅊᄎ\nㅋᄏ\nㅌᄐ\nㅍᄑ\nㅎᄒ\nㅏᅡ\nㅐᅢ\nㅑᅣ\nㅒᅤ\nㅓᅥ\nㅔᅦ\nㅕᅧ\nㅖᅨ\nㅗᅩ\nㅘᅪ\nㅙᅫ\nㅚᅬ\nㅛᅭ\nㅜᅮ\nㅝᅯ\nㅞᅰ\nㅟᅱ\nㅠᅲ\nㅡᅳ\nㅢᅴ\nㅣᅵ\nㅤᅠ\nㅥᄔ\nㅦᄕ\nㅧᇇ\nㅨᇈ\nㅩᇌ\nㅪᇎ\nㅫᇓ\nㅬᇗ\nㅭᇙ\nㅮᄜ\nㅯᇝ\nㅰᇟ\nㅱᄝ\nㅲᄞ\nㅳᄠ\nㅴᄢ\nㅵᄣ\nㅶᄧ\nㅷᄩ\nㅸᄫ\nㅹᄬ\nㅺᄭ\nㅻᄮ\nㅼᄯ\nㅽᄲ\nㅾᄶ\nㅿᅀ\nㆀᅇ\nㆁᅌ\nㆂᇱ\nㆃᇲ\nㆄᅗ\nㆅᅘ\nㆆᅙ\nㆇᆄ\nㆈᆅ\nㆉᆈ\nㆊᆑ\nㆋᆒ\nㆌᆔ\nㆍᆞ\nㆎᆡ\n㆒一\n㆓二\n㆔三\n㆕四\n㆖上\n㆗中\n㆘下\n㆙甲\n㆚乙\n㆛丙\n㆜丁\n㆝天\n㆞地\n㆟人\n㈀(ᄀ)\n㈁(ᄂ)\n㈂(ᄃ)\n㈃(ᄅ)\n㈄(ᄆ)\n㈅(ᄇ)\n㈆(ᄉ)\n㈇(ᄋ)\n㈈(ᄌ)\n㈉(ᄎ)\n㈊(ᄏ)\n㈋(ᄐ)\n㈌(ᄑ)\n㈍(ᄒ)\n㈎(가)\n㈏(나)\n㈐(다)\n㈑(라)\n㈒(마)\n㈓(바)\n㈔(사)\n㈕(아)\n㈖(자)\n㈗(차)\n㈘(카)\n㈙(타)\n㈚(파)\n㈛(하)\n㈜(주)\n㈝(오전)\n㈞(오후)\n㈠(一)\n㈡(二)\n㈢(三)\n㈣(四)\n㈤(五)\n㈥(六)\n㈦(七)\n㈧(八)\n㈨(九)\n㈩(十)\n㈪(月)\n㈫(火)\n㈬(水)\n㈭(木)\n㈮(金)\n㈯(土)\n㈰(日)\n㈱(株)\n㈲(有)\n㈳(社)\n㈴(名)\n㈵(特)\n㈶(財)\n㈷(祝)\n㈸(労)\n㈹(代)\n㈺(呼)\n㈻(学)\n㈼(監)\n㈽(企)\n㈾(資)\n㈿(協)\n㉀(祭)\n㉁(休)\n㉂(自)\n㉃(至)\n㉐PTE\n㉑21\n㉒22\n㉓23\n㉔24\n㉕25\n㉖26\n㉗27\n㉘28\n㉙29\n㉚30\n㉛31\n㉜32\n㉝33\n㉞34\n㉟35\n㉠ᄀ\n㉡ᄂ\n㉢ᄃ\n㉣ᄅ\n㉤ᄆ\n㉥ᄇ\n㉦ᄉ\n㉧ᄋ\n㉨ᄌ\n㉩ᄎ\n㉪ᄏ\n㉫ᄐ\n㉬ᄑ\n㉭ᄒ\n㉮가\n㉯나\n㉰다\n㉱라\n㉲마\n㉳바\n㉴사\n㉵아\n㉶자\n㉷차\n㉸카\n㉹타\n㉺파\n㉻하\n㉼참고\n㉽주의\n㉾우\n㊀一\n㊁二\n㊂三\n㊃四\n㊄五\n㊅六\n㊆七\n㊇八\n㊈九\n㊉十\n㊊月\n㊋火\n㊌水\n㊍木\n㊎金\n㊏土\n㊐日\n㊑株\n㊒有\n㊓社\n㊔名\n㊕特\n㊖財\n㊗祝\n㊘労\n㊙秘\n㊚男\n㊛女\n㊜適\n㊝優\n㊞印\n㊟注\n㊠項\n㊡休\n㊢写\n㊣正\n㊤上\n㊥中\n㊦下\n㊧左\n㊨右\n㊩医\n㊪宗\n㊫学\n㊬監\n㊭企\n㊮資\n㊯協\n㊰夜\n㊱36\n㊲37\n㊳38\n㊴39\n㊵40\n㊶41\n㊷42\n㊸43\n㊹44\n㊺45\n㊻46\n㊼47\n㊽48\n㊾49\n㊿50\n㋀1月\n㋁2月\n㋂3月\n㋃4月\n㋄5月\n㋅6月\n㋆7月\n㋇8月\n㋈9月\n㋉10月\n㋊11月\n㋋12月\n㋌Hg\n㋍erg\n㋎eV\n㋏LTD\n㋐ア\n㋑イ\n㋒ウ\n㋓エ\n㋔オ\n㋕カ\n㋖キ\n㋗ク\n㋘ケ\n㋙コ\n㋚サ\n㋛シ\n㋜ス\n㋝セ\n㋞ソ\n㋟タ\n㋠チ\n㋡ツ\n㋢テ\n㋣ト\n㋤ナ\n㋥ニ\n㋦ヌ\n㋧ネ\n㋨ノ\n㋩ハ\n㋪ヒ\n㋫フ\n㋬ヘ\n㋭ホ\n㋮マ\n㋯ミ\n㋰ム\n㋱メ\n㋲モ\n㋳ヤ\n㋴ユ\n㋵ヨ\n㋶ラ\n㋷リ\n㋸ル\n㋹レ\n㋺ロ\n㋻ワ\n㋼ヰ\n㋽ヱ\n㋾ヲ\n㌀アパート\n㌁アルファ\n㌂アンペア\n㌃アール\n㌄イニング\n㌅インチ\n㌆ウォン\n㌇エスクード\n㌈エーカー\n㌉オンス\n㌊オーム\n㌋カイリ\n㌌カラット\n㌍カロリー\n㌎ガロン\n㌏ガンマ\n㌐ギガ\n㌑ギニー\n㌒キュリー\n㌓ギルダー\n㌔キロ\n㌕キログラム\n㌖キロメートル\n㌗キロワット\n㌘グラム\n㌙グラムトン\n㌚クルゼイロ\n㌛クローネ\n㌜ケース\n㌝コルナ\n㌞コーポ\n㌟サイクル\n㌠サンチーム\n㌡シリング\n㌢センチ\n㌣セント\n㌤ダース\n㌥デシ\n㌦ドル\n㌧トン\n㌨ナノ\n㌩ノット\n㌪ハイツ\n㌫パーセント\n㌬パーツ\n㌭バーレル\n㌮ピアストル\n㌯ピクル\n㌰ピコ\n㌱ビル\n㌲ファラッド\n㌳フィート\n㌴ブッシェル\n㌵フラン\n㌶ヘクタール\n㌷ペソ\n㌸ペニヒ\n㌹ヘルツ\n㌺ペンス\n㌻ページ\n㌼ベータ\n㌽ポイント\n㌾ボルト\n㌿ホン\n㍀ポンド\n㍁ホール\n㍂ホーン\n㍃マイクロ\n㍄マイル\n㍅マッハ\n㍆マルク\n㍇マンション\n㍈ミクロン\n㍉ミリ\n㍊ミリバール\n㍋メガ\n㍌メガトン\n㍍メートル\n㍎ヤード\n㍏ヤール\n㍐ユアン\n㍑リットル\n㍒リラ\n㍓ルピー\n㍔ルーブル\n㍕レム\n㍖レントゲン\n㍗ワット\n㍘0点\n㍙1点\n㍚2点\n㍛3点\n㍜4点\n㍝5点\n㍞6点\n㍟7点\n㍠8点\n㍡9点\n㍢10点\n㍣11点\n㍤12点\n㍥13点\n㍦14点\n㍧15点\n㍨16点\n㍩17点\n㍪18点\n㍫19点\n㍬20点\n㍭21点\n㍮22点\n㍯23点\n㍰24点\n㍱hPa\n㍲da\n㍳AU\n㍴bar\n㍵oV\n㍶pc\n㍷dm\n㍸dm2\n㍹dm3\n㍺IU\n㍻平成\n㍼昭和\n㍽大正\n㍾明治\n㍿株式会社\n㎀pA\n㎁nA\n㎂μA\n㎃mA\n㎄kA\n㎅KB\n㎆MB\n㎇GB\n㎈cal\n㎉kcal\n㎊pF\n㎋nF\n㎌μF\n㎍μg\n㎎mg\n㎏kg\n㎐Hz\n㎑kHz\n㎒MHz\n㎓GHz\n㎔THz\n㎕μl\n㎖ml\n㎗dl\n㎘kl\n㎙fm\n㎚nm\n㎛μm\n㎜mm\n㎝cm\n㎞km\n㎟mm2\n㎠cm2\n㎡m2\n㎢km2\n㎣mm3\n㎤cm3\n㎥m3\n㎦km3\n㎧m∕s\n㎨m∕s2\n㎩Pa\n㎪kPa\n㎫MPa\n㎬GPa\n㎭rad\n㎮rad∕s\n㎯rad∕s2\n㎰ps\n㎱ns\n㎲μs\n㎳ms\n㎴pV\n㎵nV\n㎶μV\n㎷mV\n㎸kV\n㎹MV\n㎺pW\n㎻nW\n㎼μW\n㎽mW\n㎾kW\n㎿MW\n㏀kΩ\n㏁MΩ\n㏂a.m.\n㏃Bq\n㏄cc\n㏅cd\n㏆C∕kg\n㏇Co.\n㏈dB\n㏉Gy\n㏊ha\n㏋HP\n㏌in\n㏍KK\n㏎KM\n㏏kt\n㏐lm\n㏑ln\n㏒log\n㏓lx\n㏔mb\n㏕mil\n㏖mol\n㏗PH\n㏘p.m.\n㏙PPM\n㏚PR\n㏛sr\n㏜Sv\n㏝Wb\n㏞V∕m\n㏟A∕m\n㏠1日\n㏡2日\n㏢3日\n㏣4日\n㏤5日\n㏥6日\n㏦7日\n㏧8日\n㏨9日\n㏩10日\n㏪11日\n㏫12日\n㏬13日\n㏭14日\n㏮15日\n㏯16日\n㏰17日\n㏱18日\n㏲19日\n㏳20日\n㏴21日\n㏵22日\n㏶23日\n㏷24日\n㏸25日\n㏹26日\n㏺27日\n㏻28日\n㏼29日\n㏽30日\n㏾31日\n㏿gal\n豈豈\n更更\n車車\n賈賈\n滑滑\n串串\n句句\n龜龜\n龜龜\n契契\n金金\n喇喇\n奈奈\n懶懶\n癩癩\n羅羅\n蘿蘿\n螺螺\n裸裸\n邏邏\n樂樂\n洛洛\n烙烙\n珞珞\n落落\n酪酪\n駱駱\n亂亂\n卵卵\n欄欄\n爛爛\n蘭蘭\n鸞鸞\n嵐嵐\n濫濫\n藍藍\n襤襤\n拉拉\n臘臘\n蠟蠟\n廊廊\n朗朗\n浪浪\n狼狼\n郎郎\n來來\n冷冷\n勞勞\n擄擄\n櫓櫓\n爐爐\n盧盧\n老老\n蘆蘆\n虜虜\n路路\n露露\n魯魯\n鷺鷺\n碌碌\n祿祿\n綠綠\n菉菉\n錄錄\n鹿鹿\n論論\n壟壟\n弄弄\n籠籠\n聾聾\n牢牢\n磊磊\n賂賂\n雷雷\n壘壘\n屢屢\n樓樓\n淚淚\n漏漏\n累累\n縷縷\n陋陋\n勒勒\n肋肋\n凜凜\n凌凌\n稜稜\n綾綾\n菱菱\n陵陵\n讀讀\n拏拏\n樂樂\n諾諾\n丹丹\n寧寧\n怒怒\n率率\n異異\n北北\n磻磻\n便便\n復復\n不不\n泌泌\n數數\n索索\n參參\n塞塞\n省省\n葉葉\n說說\n殺殺\n辰辰\n沈沈\n拾拾\n若若\n掠掠\n略略\n亮亮\n兩兩\n凉凉\n梁梁\n糧糧\n良良\n諒諒\n量量\n勵勵\n呂呂\n女女\n廬廬\n旅旅\n濾濾\n礪礪\n閭閭\n驪驪\n麗麗\n黎黎\n力力\n曆曆\n歷歷\n轢轢\n年年\n憐憐\n戀戀\n撚撚\n漣漣\n煉煉\n璉璉\n秊秊\n練練\n聯聯\n輦輦\n蓮蓮\n連連\n鍊鍊\n列列\n劣劣\n咽咽\n烈烈\n裂裂\n說說\n廉廉\n念念\n捻捻\n殮殮\n簾簾\n獵獵\n令令\n囹囹\n寧寧\n嶺嶺\n怜怜\n玲玲\n瑩瑩\n羚羚\n聆聆\n鈴鈴\n零零\n靈靈\n領領\n例例\n禮禮\n醴醴\n隸隸\n惡惡\n了了\n僚僚\n寮寮\n尿尿\n料料\n樂樂\n燎燎\n療療\n蓼蓼\n遼遼\n龍龍\n暈暈\n阮阮\n劉劉\n杻杻\n柳柳\n流流\n溜溜\n琉琉\n留留\n硫硫\n紐紐\n類類\n六六\n戮戮\n陸陸\n倫倫\n崙崙\n淪淪\n輪輪\n律律\n慄慄\n栗栗\n率率\n隆隆\n利利\n吏吏\n履履\n易易\n李李\n梨梨\n泥泥\n理理\n痢痢\n罹罹\n裏裏\n裡裡\n里里\n離離\n匿匿\n溺溺\n吝吝\n燐燐\n璘璘\n藺藺\n隣隣\n鱗鱗\n麟麟\n林林\n淋淋\n臨臨\n立立\n笠笠\n粒粒\n狀狀\n炙炙\n識識\n什什\n茶茶\n刺刺\n切切\n度度\n拓拓\n糖糖\n宅宅\n洞洞\n暴暴\n輻輻\n行行\n降降\n見見\n廓廓\n兀兀\n嗀嗀\n塚塚\n晴晴\n凞凞\n猪猪\n益益\n礼礼\n神神\n祥祥\n福福\n靖靖\n精精\n羽羽\n蘒蘒\n諸諸\n逸逸\n都都\n飯飯\n飼飼\n館館\n鶴鶴\n侮侮\n僧僧\n免免\n勉勉\n勤勤\n卑卑\n喝喝\n嘆嘆\n器器\n塀塀\n墨墨\n層層\n屮屮\n悔悔\n慨慨\n憎憎\n懲懲\n敏敏\n既既\n暑暑\n梅梅\n海海\n渚渚\n漢漢\n煮煮\n爫爫\n琢琢\n碑碑\n社社\n祉祉\n祈祈\n祐祐\n祖祖\n祝祝\n禍禍\n禎禎\n穀穀\n突突\n節節\n練練\n縉縉\n繁繁\n署署\n者者\n臭臭\n艹艹\n艹艹\n著著\n褐褐\n視視\n謁謁\n謹謹\n賓賓\n贈贈\n辶辶\n逸逸\n難難\n響響\n頻頻\n並並\n况况\n全全\n侀侀\n充充\n冀冀\n勇勇\n勺勺\n喝喝\n啕啕\n喙喙\n嗢嗢\n塚塚\n墳墳\n奄奄\n奔奔\n婢婢\n嬨嬨\n廒廒\n廙廙\n彩彩\n徭徭\n惘惘\n慎慎\n愈愈\n憎憎\n慠慠\n懲懲\n戴戴\n揄揄\n搜搜\n摒摒\n敖敖\n晴晴\n朗朗\n望望\n杖杖\n歹歹\n殺殺\n流流\n滛滛\n滋滋\n漢漢\n瀞瀞\n煮煮\n瞧瞧\n爵爵\n犯犯\n猪猪\n瑱瑱\n甆甆\n画画\n瘝瘝\n瘟瘟\n益益\n盛盛\n直直\n睊睊\n着着\n磌磌\n窱窱\n節節\n类类\n絛絛\n練練\n缾缾\n者者\n荒荒\n華華\n蝹蝹\n襁襁\n覆覆\n視視\n調調\n諸諸\n請請\n謁謁\n諾諾\n諭諭\n謹謹\n變變\n贈贈\n輸輸\n遲遲\n醙醙\n鉶鉶\n陼陼\n難難\n靖靖\n韛韛\n響響\n頋頋\n頻頻\n鬒鬒\n龜龜\n𢡊𢡊\n𢡄𢡄\n𣏕𣏕\n㮝㮝\n䀘䀘\n䀹䀹\n𥉉𥉉\n𥳐𥳐\n𧻓𧻓\n齃齃\n龎龎\n!!\n"\"\n##\n$$\n%%\n&&\n''\n((\n))\n**\n++\n,,\n--\n..\n//\n00\n11\n22\n33\n44\n55\n66\n77\n88\n99\n::\n;;\n<<\n==\n>>\n??\n@@\nAA\nBB\nCC\nDD\nEE\nFF\nGG\nHH\nII\nJJ\nKK\nLL\nMM\nNN\nOO\nPP\nQQ\nRR\nSS\nTT\nUU\nVV\nWW\nXX\nYY\nZZ\n[[\n\\\n]]\n^^\n__\n``\naa\nbb\ncc\ndd\nee\nff\ngg\nhh\nii\njj\nkk\nll\nmm\nnn\noo\npp\nqq\nrr\nss\ntt\nuu\nvv\nww\nxx\nyy\nzz\n{{\n||\n}}\n~~\n⦅⦅\n⦆⦆\n。。\n「「\n」」\n、、\n・・\nヲヲ\nァァ\nィィ\nゥゥ\nェェ\nォォ\nャャ\nュュ\nョョ\nッッ\nーー\nアア\nイイ\nウウ\nエエ\nオオ\nカカ\nキキ\nクク\nケケ\nココ\nササ\nシシ\nスス\nセセ\nソソ\nタタ\nチチ\nツツ\nテテ\nトト\nナナ\nニニ\nヌヌ\nネネ\nノノ\nハハ\nヒヒ\nフフ\nヘヘ\nホホ\nママ\nミミ\nムム\nメメ\nモモ\nヤヤ\nユユ\nヨヨ\nララ\nリリ\nルル\nレレ\nロロ\nワワ\nンン\n゙゙\n゚゚\nᅠᅠ\nᄀᄀ\nᄁᄁ\nᆪᆪ\nᄂᄂ\nᆬᆬ\nᆭᆭ\nᄃᄃ\nᄄᄄ\nᄅᄅ\nᆰᆰ\nᆱᆱ\nᆲᆲ\nᆳᆳ\nᆴᆴ\nᆵᆵ\nᄚᄚ\nᄆᄆ\nᄇᄇ\nᄈᄈ\nᄡᄡ\nᄉᄉ\nᄊᄊ\nᄋᄋ\nᄌᄌ\nᄍᄍ\nᄎᄎ\nᄏᄏ\nᄐᄐ\nᄑᄑ\nᄒᄒ\nᅡᅡ\nᅢᅢ\nᅣᅣ\nᅤᅤ\nᅥᅥ\nᅦᅦ\nᅧᅧ\nᅨᅨ\nᅩᅩ\nᅪᅪ\nᅫᅫ\nᅬᅬ\nᅭᅭ\nᅮᅮ\nᅯᅯ\nᅰᅰ\nᅱᅱ\nᅲᅲ\nᅳᅳ\nᅴᅴ\nᅵᅵ\n¢¢\n££\n¬¬\n ̄ ̄\n¦¦\n¥¥\n₩₩\n││\n←←\n↑↑\n→→\n↓↓\n■■\n○○\n"; + } } diff --git a/DotNut/NBitcoin/BIP39/Language.cs b/DotNut/NBitcoin/BIP39/Language.cs index 6a9a762..a5b90ed 100644 --- a/DotNut/NBitcoin/BIP39/Language.cs +++ b/DotNut/NBitcoin/BIP39/Language.cs @@ -1,15 +1,15 @@ namespace DotNut.NBitcoin.BIP39 { - public enum Language - { - English, - Japanese, - Spanish, - ChineseSimplified, - ChineseTraditional, - French, - PortugueseBrazil, - Czech, - Unknown - }; + public enum Language + { + English, + Japanese, + Spanish, + ChineseSimplified, + ChineseTraditional, + French, + PortugueseBrazil, + Czech, + Unknown, + }; } diff --git a/DotNut/NBitcoin/BIP39/Mnemonic.cs b/DotNut/NBitcoin/BIP39/Mnemonic.cs index aa8493f..f1944e4 100644 --- a/DotNut/NBitcoin/BIP39/Mnemonic.cs +++ b/DotNut/NBitcoin/BIP39/Mnemonic.cs @@ -4,208 +4,207 @@ namespace DotNut.NBitcoin.BIP39 { - /// - /// A .NET implementation of the Bitcoin Improvement Proposal - 39 (BIP39) - /// BIP39 specification used as reference located here: https://github.com/bitcoin/bips/blob/master/bip-0039.mediawiki - /// Made by thashiznets@yahoo.com.au - /// v1.0.1.1 - /// I ♥ Bitcoin :) - /// Bitcoin:1ETQjMkR1NNh4jwLuN5LxY7bMsHC9PUPSV - /// - public class Mnemonic - { - public Mnemonic(string mnemonic, Wordlist wordlist = null) - { - if (mnemonic == null) - throw new ArgumentNullException(nameof(mnemonic)); - _Mnemonic = mnemonic.Trim(); - if (wordlist == null) - wordlist = Wordlist.AutoDetect(mnemonic) ?? Wordlist.English; - var words = mnemonic.Split((char[])null, StringSplitOptions.RemoveEmptyEntries); - _Mnemonic = string.Join(wordlist.Space.ToString(), words); - //if the sentence is not at least 12 characters or cleanly divisible by 3, it is bad! - if (!CorrectWordCount(words.Length)) - { - throw new FormatException("Word count should be 12,15,18,21 or 24"); - } - _Words = words; - _WordList = wordlist; - _Indices = wordlist.ToIndices(words); - } - - /// - /// Generate a mnemonic - /// - /// - /// - public Mnemonic(Wordlist wordList, byte[] entropy = null) - { - wordList = wordList ?? Wordlist.English; - _WordList = wordList; - if (entropy == null) - entropy = RandomNumberGenerator.GetBytes(32); - - var i = Array.IndexOf(entArray, entropy.Length * 8); - if (i == -1) - throw new ArgumentException("The length for entropy should be " + String.Join(",", entArray) + " bits", "entropy"); - - int cs = csArray[i]; - byte[] checksum = SHA256.HashData(entropy); - BitWriter entcsResult = new BitWriter(); - - entcsResult.Write(entropy); - entcsResult.Write(checksum, cs); - _Indices = entcsResult.ToIntegers(); - _Words = _WordList.GetWords(_Indices); - _Mnemonic = _WordList.GetSentence(_Indices); - } - - public Mnemonic(Wordlist wordList, WordCount wordCount) - : this(wordList, GenerateEntropy(wordCount)) - { - - } - - private static byte[] GenerateEntropy(WordCount wordCount) - { - var ms = (int)wordCount; - if (!CorrectWordCount(ms)) - throw new ArgumentException("Word count should be 12,15,18,21 or 24", "wordCount"); - int i = Array.IndexOf(msArray, (int)wordCount); - return RandomNumberGenerator.GetBytes(entArray[i] / 8); - } - - static readonly int[] msArray = new[] { 12, 15, 18, 21, 24 }; - static readonly int[] csArray = new[] { 4, 5, 6, 7, 8 }; - static readonly int[] entArray = new[] { 128, 160, 192, 224, 256 }; - - bool? _IsValidChecksum; - public bool IsValidChecksum - { - get - { - if (_IsValidChecksum == null) - { - int i = Array.IndexOf(msArray, _Indices.Length); - int cs = csArray[i]; - int ent = entArray[i]; - - BitWriter writer = new BitWriter(); - var bits = Wordlist.ToBits(_Indices); - writer.Write(bits, ent); - var entropy = writer.ToBytes(); - var checksum = SHA256.HashData(entropy as byte[]); - - writer.Write(checksum, cs); - var expectedIndices = writer.ToIntegers(); - _IsValidChecksum = expectedIndices.SequenceEqual(_Indices); - } - return _IsValidChecksum.Value; - } - } - - private static bool CorrectWordCount(int ms) - { - return msArray.Any(_ => _ == ms); - } - - private readonly Wordlist _WordList; - public Wordlist WordList - { - get - { - return _WordList; - } - } - - private readonly int[] _Indices; - public int[] Indices - { - get - { - return _Indices; - } - } - private readonly string[] _Words; - public string[] Words - { - get - { - return _Words; - } - } - - static Encoding NoBOMUTF8 = new UTF8Encoding(false); - public byte[] DeriveSeed(string passphrase = null) - { - passphrase = passphrase ?? ""; - var salt = Concat(NoBOMUTF8.GetBytes("mnemonic"), Normalize(passphrase)); - var bytes = Normalize(_Mnemonic); - - using Rfc2898DeriveBytes derive = new Rfc2898DeriveBytes(bytes, salt, 2048, HashAlgorithmName.SHA512); - return derive.GetBytes(64); - } - - internal static byte[] Normalize(string str) - { - return NoBOMUTF8.GetBytes(NormalizeString(str)); - } - - internal static string NormalizeString(string word) - { - if (!SupportOsNormalization()) - { - return KDTable.NormalizeKD(word); - } - else - { - return word.Normalize(NormalizationForm.FormKD); - } - - } - - static bool? _SupportOSNormalization; - internal static bool SupportOsNormalization() - { - if (_SupportOSNormalization == null) - { - var notNormalized = "あおぞら"; - var normalized = "あおぞら"; - if (notNormalized.Equals(normalized, StringComparison.Ordinal)) - { - _SupportOSNormalization = false; - } - else - { - try - { - _SupportOSNormalization = notNormalized.Normalize(NormalizationForm.FormKD).Equals(normalized, StringComparison.Ordinal); - } - catch { _SupportOSNormalization = false; } - } - } - return _SupportOSNormalization.Value; - } - - - static Byte[] Concat(Byte[] source1, Byte[] source2) - { - //Most efficient way to merge two arrays this according to http://stackoverflow.com/questions/415291/best-way-to-combine-two-or-more-byte-arrays-in-c-sharp - Byte[] buffer = new Byte[source1.Length + source2.Length]; - System.Buffer.BlockCopy(source1, 0, buffer, 0, source1.Length); - System.Buffer.BlockCopy(source2, 0, buffer, source1.Length, source2.Length); - - return buffer; - } - - - string _Mnemonic; - public override string ToString() - { - return _Mnemonic; - } - - - } + /// + /// A .NET implementation of the Bitcoin Improvement Proposal - 39 (BIP39) + /// BIP39 specification used as reference located here: https://github.com/bitcoin/bips/blob/master/bip-0039.mediawiki + /// Made by thashiznets@yahoo.com.au + /// v1.0.1.1 + /// I ♥ Bitcoin :) + /// Bitcoin:1ETQjMkR1NNh4jwLuN5LxY7bMsHC9PUPSV + /// + public class Mnemonic + { + public Mnemonic(string mnemonic, Wordlist wordlist = null) + { + if (mnemonic == null) + throw new ArgumentNullException(nameof(mnemonic)); + _Mnemonic = mnemonic.Trim(); + if (wordlist == null) + wordlist = Wordlist.AutoDetect(mnemonic) ?? Wordlist.English; + var words = mnemonic.Split((char[])null, StringSplitOptions.RemoveEmptyEntries); + _Mnemonic = string.Join(wordlist.Space.ToString(), words); + //if the sentence is not at least 12 characters or cleanly divisible by 3, it is bad! + if (!CorrectWordCount(words.Length)) + { + throw new FormatException("Word count should be 12,15,18,21 or 24"); + } + _Words = words; + _WordList = wordlist; + _Indices = wordlist.ToIndices(words); + } + + /// + /// Generate a mnemonic + /// + /// + /// + public Mnemonic(Wordlist wordList, byte[] entropy = null) + { + wordList = wordList ?? Wordlist.English; + _WordList = wordList; + if (entropy == null) + entropy = RandomNumberGenerator.GetBytes(32); + + var i = Array.IndexOf(entArray, entropy.Length * 8); + if (i == -1) + throw new ArgumentException( + "The length for entropy should be " + String.Join(",", entArray) + " bits", + "entropy" + ); + + int cs = csArray[i]; + byte[] checksum = SHA256.HashData(entropy); + BitWriter entcsResult = new BitWriter(); + + entcsResult.Write(entropy); + entcsResult.Write(checksum, cs); + _Indices = entcsResult.ToIntegers(); + _Words = _WordList.GetWords(_Indices); + _Mnemonic = _WordList.GetSentence(_Indices); + } + + public Mnemonic(Wordlist wordList, WordCount wordCount) + : this(wordList, GenerateEntropy(wordCount)) { } + + private static byte[] GenerateEntropy(WordCount wordCount) + { + var ms = (int)wordCount; + if (!CorrectWordCount(ms)) + throw new ArgumentException("Word count should be 12,15,18,21 or 24", "wordCount"); + int i = Array.IndexOf(msArray, (int)wordCount); + return RandomNumberGenerator.GetBytes(entArray[i] / 8); + } + + static readonly int[] msArray = new[] { 12, 15, 18, 21, 24 }; + static readonly int[] csArray = new[] { 4, 5, 6, 7, 8 }; + static readonly int[] entArray = new[] { 128, 160, 192, 224, 256 }; + + bool? _IsValidChecksum; + public bool IsValidChecksum + { + get + { + if (_IsValidChecksum == null) + { + int i = Array.IndexOf(msArray, _Indices.Length); + int cs = csArray[i]; + int ent = entArray[i]; + + BitWriter writer = new BitWriter(); + var bits = Wordlist.ToBits(_Indices); + writer.Write(bits, ent); + var entropy = writer.ToBytes(); + var checksum = SHA256.HashData(entropy as byte[]); + + writer.Write(checksum, cs); + var expectedIndices = writer.ToIntegers(); + _IsValidChecksum = expectedIndices.SequenceEqual(_Indices); + } + return _IsValidChecksum.Value; + } + } + + private static bool CorrectWordCount(int ms) + { + return msArray.Any(_ => _ == ms); + } + + private readonly Wordlist _WordList; + public Wordlist WordList + { + get { return _WordList; } + } + + private readonly int[] _Indices; + public int[] Indices + { + get { return _Indices; } + } + private readonly string[] _Words; + public string[] Words + { + get { return _Words; } + } + + static Encoding NoBOMUTF8 = new UTF8Encoding(false); + + public byte[] DeriveSeed(string passphrase = null) + { + passphrase = passphrase ?? ""; + var salt = Concat(NoBOMUTF8.GetBytes("mnemonic"), Normalize(passphrase)); + var bytes = Normalize(_Mnemonic); + + using Rfc2898DeriveBytes derive = new Rfc2898DeriveBytes( + bytes, + salt, + 2048, + HashAlgorithmName.SHA512 + ); + return derive.GetBytes(64); + } + + internal static byte[] Normalize(string str) + { + return NoBOMUTF8.GetBytes(NormalizeString(str)); + } + + internal static string NormalizeString(string word) + { + if (!SupportOsNormalization()) + { + return KDTable.NormalizeKD(word); + } + else + { + return word.Normalize(NormalizationForm.FormKD); + } + } + + static bool? _SupportOSNormalization; + + internal static bool SupportOsNormalization() + { + if (_SupportOSNormalization == null) + { + var notNormalized = "あおぞら"; + var normalized = "あおぞら"; + if (notNormalized.Equals(normalized, StringComparison.Ordinal)) + { + _SupportOSNormalization = false; + } + else + { + try + { + _SupportOSNormalization = notNormalized + .Normalize(NormalizationForm.FormKD) + .Equals(normalized, StringComparison.Ordinal); + } + catch + { + _SupportOSNormalization = false; + } + } + } + return _SupportOSNormalization.Value; + } + + static Byte[] Concat(Byte[] source1, Byte[] source2) + { + //Most efficient way to merge two arrays this according to http://stackoverflow.com/questions/415291/best-way-to-combine-two-or-more-byte-arrays-in-c-sharp + Byte[] buffer = new Byte[source1.Length + source2.Length]; + System.Buffer.BlockCopy(source1, 0, buffer, 0, source1.Length); + System.Buffer.BlockCopy(source2, 0, buffer, source1.Length, source2.Length); + + return buffer; + } + + string _Mnemonic; + + public override string ToString() + { + return _Mnemonic; + } + } } #pragma warning restore CS0618 // Type or member is obsolete diff --git a/DotNut/NBitcoin/BIP39/WordCount.cs b/DotNut/NBitcoin/BIP39/WordCount.cs index 5f03dae..aa9a044 100644 --- a/DotNut/NBitcoin/BIP39/WordCount.cs +++ b/DotNut/NBitcoin/BIP39/WordCount.cs @@ -6,5 +6,5 @@ public enum WordCount : int Fifteen = 15, Eighteen = 18, TwentyOne = 21, - TwentyFour = 24 -} \ No newline at end of file + TwentyFour = 24, +} diff --git a/DotNut/NBitcoin/BIP39/Wordlist.cs b/DotNut/NBitcoin/BIP39/Wordlist.cs index 61b4258..3dc4b6e 100644 --- a/DotNut/NBitcoin/BIP39/Wordlist.cs +++ b/DotNut/NBitcoin/BIP39/Wordlist.cs @@ -3,451 +3,439 @@ namespace DotNut.NBitcoin.BIP39 { - public class Wordlist - { - static Wordlist() - { - WordlistSource = new HardcodedWordlistSource(); - } - private static Wordlist _Japanese; - public static Wordlist Japanese - { - get - { - if (_Japanese == null) - _Japanese = LoadWordList(Language.Japanese).Result; - return _Japanese; - } - } - - private static Wordlist _ChineseSimplified; - public static Wordlist ChineseSimplified - { - get - { - if (_ChineseSimplified == null) - _ChineseSimplified = LoadWordList(Language.ChineseSimplified).Result; - return _ChineseSimplified; - } - } - - private static Wordlist _ChineseTraditional; - public static Wordlist ChineseTraditional - { - get - { - if (_ChineseTraditional == null) - _ChineseTraditional = LoadWordList(Language.ChineseTraditional).Result; - return _ChineseTraditional; - } - } - - private static Wordlist _Spanish; - public static Wordlist Spanish - { - get - { - if (_Spanish == null) - _Spanish = LoadWordList(Language.Spanish).Result; - return _Spanish; - } - } - - private static Wordlist _English; - public static Wordlist English - { - get - { - if (_English == null) - _English = LoadWordList(Language.English).Result; - return _English; - } - } - - private static Wordlist _French; - public static Wordlist French - { - get - { - if (_French == null) - _French = LoadWordList(Language.French).Result; - return _French; - } - } - - private static Wordlist _PortugueseBrazil; - public static Wordlist PortugueseBrazil - { - get - { - if (_PortugueseBrazil == null) - _PortugueseBrazil = LoadWordList(Language.PortugueseBrazil).Result; - return _PortugueseBrazil; - } - } - - private static Wordlist _Czech; - public static Wordlist Czech - { - get - { - if (_Czech == null) - _Czech = LoadWordList(Language.Czech).Result; - return _Czech; - } - } - - public static Task LoadWordList(Language language) - { - string name = GetLanguageFileName(language); - return LoadWordList(name); - } - - internal static string GetLanguageFileName(Language language) - { - string name = null; - switch (language) - { - case Language.ChineseTraditional: - name = "chinese_traditional"; - break; - case Language.ChineseSimplified: - name = "chinese_simplified"; - break; - case Language.English: - name = "english"; - break; - case Language.Japanese: - name = "japanese"; - break; - case Language.Spanish: - name = "spanish"; - break; - case Language.French: - name = "french"; - break; - case Language.PortugueseBrazil: - name = "portuguese_brazil"; - break; - case Language.Czech: - name = "czech"; - break; - default: - throw new NotSupportedException(language.ToString()); - } - return name; - } - - static Dictionary _LoadedLists = new Dictionary(); - public static async Task LoadWordList(string name) - { - if (name == null) - throw new ArgumentNullException(nameof(name)); - Wordlist result = null; - lock (_LoadedLists) - { - _LoadedLists.TryGetValue(name, out result); - } - if (result != null) - return await Task.FromResult(result).ConfigureAwait(false); - - - if (WordlistSource == null) - throw new InvalidOperationException("Wordlist.WordlistSource is not set, impossible to fetch word list."); - result = await WordlistSource.Load(name).ConfigureAwait(false); - if (result != null) - lock (_LoadedLists) - { - _LoadedLists.Remove(name); - _LoadedLists.Add(name, result); - } - return result; - } - - public static IWordlistSource WordlistSource - { - get; - set; - } - - private String[] _words; - - /// - /// Constructor used by inheritence only - /// - /// The words to be used in the wordlist - public Wordlist(String[] words, char space, string name) - { - _words = words - .Select(w => Mnemonic.NormalizeString(w)) - .ToArray(); - _Space = space; - _Name = name; - } - - private readonly string _Name; - public string Name - { - get - { - return _Name; - } - } - private readonly char _Space; - public char Space - { - get - { - return _Space; - } - } - - /// - /// Method to determine if word exists in word list, great for auto language detection - /// - /// The word to check for existence - /// Exists (true/false) - public bool WordExists(string word, out int index) - { - word = Mnemonic.NormalizeString(word); - if (_words.Contains(word)) - { - index = Array.IndexOf(_words, word); - return true; - } - - //index -1 means word is not in wordlist - index = -1; - return false; - } - - /// - /// Returns a string containing the word at the specified index of the wordlist - /// - /// Index of word to return - /// Word - public string GetWordAtIndex(int index) - { - return _words[index]; - } - - /// - /// The number of all the words in the wordlist - /// - public int WordCount - { - get - { - return _words.Length; - } - } - - - public static Task AutoDetectAsync(string sentence) - { - return LoadWordList(AutoDetectLanguage(sentence)); - } - public static Wordlist AutoDetect(string sentence) - { - return LoadWordList(AutoDetectLanguage(sentence)).Result; - } - public static Language AutoDetectLanguage(string[] words) - { - List languageCount = new List(new int[] { 0, 0, 0, 0, 0, 0, 0, 0 }); - int index; - - foreach (string s in words) - { - if (Wordlist.English.WordExists(s, out index)) - { - //english is at 0 - languageCount[0]++; - } - - if (Wordlist.Japanese.WordExists(s, out index)) - { - //japanese is at 1 - languageCount[1]++; - } - - if (Wordlist.Spanish.WordExists(s, out index)) - { - //spanish is at 2 - languageCount[2]++; - } - - if (Wordlist.ChineseSimplified.WordExists(s, out index)) - { - //chinese simplified is at 3 - languageCount[3]++; - } - - if (Wordlist.ChineseTraditional.WordExists(s, out index) && !Wordlist.ChineseSimplified.WordExists(s, out index)) - { - //chinese traditional is at 4 - languageCount[4]++; - } - if (Wordlist.French.WordExists(s, out index)) - { - languageCount[5]++; - } - - if (Wordlist.PortugueseBrazil.WordExists(s, out index)) - { - //portuguese_brazil is at 6 - languageCount[6]++; - } - - if (Wordlist.Czech.WordExists(s, out index)) - { - //czech is at 7 - languageCount[7]++; - } - } - - //no hits found for any language unknown - if (languageCount.Max() == 0) - { - return Language.Unknown; - } - - if (languageCount.IndexOf(languageCount.Max()) == 0) - { - return Language.English; - } - else if (languageCount.IndexOf(languageCount.Max()) == 1) - { - return Language.Japanese; - } - else if (languageCount.IndexOf(languageCount.Max()) == 2) - { - return Language.Spanish; - } - else if (languageCount.IndexOf(languageCount.Max()) == 3) - { - if (languageCount[4] > 0) - { - //has traditional characters so not simplified but instead traditional - return Language.ChineseTraditional; - } - - return Language.ChineseSimplified; - } - else if (languageCount.IndexOf(languageCount.Max()) == 4) - { - return Language.ChineseTraditional; - } - else if (languageCount.IndexOf(languageCount.Max()) == 5) - { - return Language.French; - } - else if (languageCount.IndexOf(languageCount.Max()) == 6) - { - return Language.PortugueseBrazil; - } - else if (languageCount.IndexOf(languageCount.Max()) == 7) - { - return Language.Czech; - } - return Language.Unknown; - } - public static Language AutoDetectLanguage(string sentence) - { - string[] words = sentence.Split(new char[] { ' ', ' ' }); //normal space and JP space - - return AutoDetectLanguage(words); - } - - public string[] Split(string mnemonic) - { - return mnemonic.Split(new char[] { Space }, StringSplitOptions.RemoveEmptyEntries); - } - - public override string ToString() - { - return _Name; - } - - public ReadOnlyCollection GetWords() - { - return new ReadOnlyCollection(_words); - } - - public string[] GetWords(int[] indices) - { - return - indices - .Select(i => GetWordAtIndex(i)) - .ToArray(); - } - - public string GetSentence(int[] indices) - { - return String.Join(Space.ToString(), GetWords(indices)); - - } - - public int[] ToIndices(string[] words) - { - var indices = new int[words.Length]; - for (int i = 0; i < words.Length; i++) - { - int idx = -1; - - if (!WordExists(words[i], out idx)) - { - throw new FormatException("Word " + words[i] + " is not in the wordlist for this language, cannot continue to rebuild entropy from wordlist"); - } - indices[i] = idx; - } - return indices; - } - - public int[] ToIndices(string sentence) - { - return ToIndices(Split(sentence)); - } - - public static BitArray ToBits(int[] values) - { - if (values.Any(v => v >= 2048)) - throw new ArgumentException("values should be between 0 and 2048", "values"); - BitArray result = new BitArray(values.Length * 11); - int i = 0; - foreach (var val in values) - { - for (int p = 0; p < 11; p++) - { - var v = (val & (1 << (10 - p))) != 0; - result.Set(i, v); - i++; - } - } - return result; - } - public static int[] ToIntegers(BitArray bits) - { - return - bits - .OfType() - .Select((v, i) => new - { - Group = i / 11, - Value = v ? 1 << (10 - (i % 11)) : 0 - }) - .GroupBy(_ => _.Group, _ => _.Value) - .Select(g => g.Sum()) - .ToArray(); - } - - public BitArray ToBits(string sentence) - { - return ToBits(ToIndices(sentence)); - } - - public string[] GetWords(string sentence) - { - return ToIndices(sentence).Select(i => GetWordAtIndex(i)).ToArray(); - } - } + public class Wordlist + { + static Wordlist() + { + WordlistSource = new HardcodedWordlistSource(); + } + + private static Wordlist _Japanese; + public static Wordlist Japanese + { + get + { + if (_Japanese == null) + _Japanese = LoadWordList(Language.Japanese).Result; + return _Japanese; + } + } + + private static Wordlist _ChineseSimplified; + public static Wordlist ChineseSimplified + { + get + { + if (_ChineseSimplified == null) + _ChineseSimplified = LoadWordList(Language.ChineseSimplified).Result; + return _ChineseSimplified; + } + } + + private static Wordlist _ChineseTraditional; + public static Wordlist ChineseTraditional + { + get + { + if (_ChineseTraditional == null) + _ChineseTraditional = LoadWordList(Language.ChineseTraditional).Result; + return _ChineseTraditional; + } + } + + private static Wordlist _Spanish; + public static Wordlist Spanish + { + get + { + if (_Spanish == null) + _Spanish = LoadWordList(Language.Spanish).Result; + return _Spanish; + } + } + + private static Wordlist _English; + public static Wordlist English + { + get + { + if (_English == null) + _English = LoadWordList(Language.English).Result; + return _English; + } + } + + private static Wordlist _French; + public static Wordlist French + { + get + { + if (_French == null) + _French = LoadWordList(Language.French).Result; + return _French; + } + } + + private static Wordlist _PortugueseBrazil; + public static Wordlist PortugueseBrazil + { + get + { + if (_PortugueseBrazil == null) + _PortugueseBrazil = LoadWordList(Language.PortugueseBrazil).Result; + return _PortugueseBrazil; + } + } + + private static Wordlist _Czech; + public static Wordlist Czech + { + get + { + if (_Czech == null) + _Czech = LoadWordList(Language.Czech).Result; + return _Czech; + } + } + + public static Task LoadWordList(Language language) + { + string name = GetLanguageFileName(language); + return LoadWordList(name); + } + + internal static string GetLanguageFileName(Language language) + { + string name = null; + switch (language) + { + case Language.ChineseTraditional: + name = "chinese_traditional"; + break; + case Language.ChineseSimplified: + name = "chinese_simplified"; + break; + case Language.English: + name = "english"; + break; + case Language.Japanese: + name = "japanese"; + break; + case Language.Spanish: + name = "spanish"; + break; + case Language.French: + name = "french"; + break; + case Language.PortugueseBrazil: + name = "portuguese_brazil"; + break; + case Language.Czech: + name = "czech"; + break; + default: + throw new NotSupportedException(language.ToString()); + } + return name; + } + + static Dictionary _LoadedLists = new Dictionary(); + + public static async Task LoadWordList(string name) + { + if (name == null) + throw new ArgumentNullException(nameof(name)); + Wordlist result = null; + lock (_LoadedLists) + { + _LoadedLists.TryGetValue(name, out result); + } + if (result != null) + return await Task.FromResult(result).ConfigureAwait(false); + + if (WordlistSource == null) + throw new InvalidOperationException( + "Wordlist.WordlistSource is not set, impossible to fetch word list." + ); + result = await WordlistSource.Load(name).ConfigureAwait(false); + if (result != null) + lock (_LoadedLists) + { + _LoadedLists.Remove(name); + _LoadedLists.Add(name, result); + } + return result; + } + + public static IWordlistSource WordlistSource { get; set; } + + private String[] _words; + + /// + /// Constructor used by inheritence only + /// + /// The words to be used in the wordlist + public Wordlist(String[] words, char space, string name) + { + _words = words.Select(w => Mnemonic.NormalizeString(w)).ToArray(); + _Space = space; + _Name = name; + } + + private readonly string _Name; + public string Name + { + get { return _Name; } + } + private readonly char _Space; + public char Space + { + get { return _Space; } + } + + /// + /// Method to determine if word exists in word list, great for auto language detection + /// + /// The word to check for existence + /// Exists (true/false) + public bool WordExists(string word, out int index) + { + word = Mnemonic.NormalizeString(word); + if (_words.Contains(word)) + { + index = Array.IndexOf(_words, word); + return true; + } + + //index -1 means word is not in wordlist + index = -1; + return false; + } + + /// + /// Returns a string containing the word at the specified index of the wordlist + /// + /// Index of word to return + /// Word + public string GetWordAtIndex(int index) + { + return _words[index]; + } + + /// + /// The number of all the words in the wordlist + /// + public int WordCount + { + get { return _words.Length; } + } + + public static Task AutoDetectAsync(string sentence) + { + return LoadWordList(AutoDetectLanguage(sentence)); + } + + public static Wordlist AutoDetect(string sentence) + { + return LoadWordList(AutoDetectLanguage(sentence)).Result; + } + + public static Language AutoDetectLanguage(string[] words) + { + List languageCount = new List(new int[] { 0, 0, 0, 0, 0, 0, 0, 0 }); + int index; + + foreach (string s in words) + { + if (Wordlist.English.WordExists(s, out index)) + { + //english is at 0 + languageCount[0]++; + } + + if (Wordlist.Japanese.WordExists(s, out index)) + { + //japanese is at 1 + languageCount[1]++; + } + + if (Wordlist.Spanish.WordExists(s, out index)) + { + //spanish is at 2 + languageCount[2]++; + } + + if (Wordlist.ChineseSimplified.WordExists(s, out index)) + { + //chinese simplified is at 3 + languageCount[3]++; + } + + if ( + Wordlist.ChineseTraditional.WordExists(s, out index) + && !Wordlist.ChineseSimplified.WordExists(s, out index) + ) + { + //chinese traditional is at 4 + languageCount[4]++; + } + if (Wordlist.French.WordExists(s, out index)) + { + languageCount[5]++; + } + + if (Wordlist.PortugueseBrazil.WordExists(s, out index)) + { + //portuguese_brazil is at 6 + languageCount[6]++; + } + + if (Wordlist.Czech.WordExists(s, out index)) + { + //czech is at 7 + languageCount[7]++; + } + } + + //no hits found for any language unknown + if (languageCount.Max() == 0) + { + return Language.Unknown; + } + + if (languageCount.IndexOf(languageCount.Max()) == 0) + { + return Language.English; + } + else if (languageCount.IndexOf(languageCount.Max()) == 1) + { + return Language.Japanese; + } + else if (languageCount.IndexOf(languageCount.Max()) == 2) + { + return Language.Spanish; + } + else if (languageCount.IndexOf(languageCount.Max()) == 3) + { + if (languageCount[4] > 0) + { + //has traditional characters so not simplified but instead traditional + return Language.ChineseTraditional; + } + + return Language.ChineseSimplified; + } + else if (languageCount.IndexOf(languageCount.Max()) == 4) + { + return Language.ChineseTraditional; + } + else if (languageCount.IndexOf(languageCount.Max()) == 5) + { + return Language.French; + } + else if (languageCount.IndexOf(languageCount.Max()) == 6) + { + return Language.PortugueseBrazil; + } + else if (languageCount.IndexOf(languageCount.Max()) == 7) + { + return Language.Czech; + } + return Language.Unknown; + } + + public static Language AutoDetectLanguage(string sentence) + { + string[] words = sentence.Split(new char[] { ' ', ' ' }); //normal space and JP space + + return AutoDetectLanguage(words); + } + + public string[] Split(string mnemonic) + { + return mnemonic.Split(new char[] { Space }, StringSplitOptions.RemoveEmptyEntries); + } + + public override string ToString() + { + return _Name; + } + + public ReadOnlyCollection GetWords() + { + return new ReadOnlyCollection(_words); + } + + public string[] GetWords(int[] indices) + { + return indices.Select(i => GetWordAtIndex(i)).ToArray(); + } + + public string GetSentence(int[] indices) + { + return String.Join(Space.ToString(), GetWords(indices)); + } + + public int[] ToIndices(string[] words) + { + var indices = new int[words.Length]; + for (int i = 0; i < words.Length; i++) + { + int idx = -1; + + if (!WordExists(words[i], out idx)) + { + throw new FormatException( + "Word " + + words[i] + + " is not in the wordlist for this language, cannot continue to rebuild entropy from wordlist" + ); + } + indices[i] = idx; + } + return indices; + } + + public int[] ToIndices(string sentence) + { + return ToIndices(Split(sentence)); + } + + public static BitArray ToBits(int[] values) + { + if (values.Any(v => v >= 2048)) + throw new ArgumentException("values should be between 0 and 2048", "values"); + BitArray result = new BitArray(values.Length * 11); + int i = 0; + foreach (var val in values) + { + for (int p = 0; p < 11; p++) + { + var v = (val & (1 << (10 - p))) != 0; + result.Set(i, v); + i++; + } + } + return result; + } + + public static int[] ToIntegers(BitArray bits) + { + return bits.OfType() + .Select((v, i) => new { Group = i / 11, Value = v ? 1 << (10 - (i % 11)) : 0 }) + .GroupBy(_ => _.Group, _ => _.Value) + .Select(g => g.Sum()) + .ToArray(); + } + + public BitArray ToBits(string sentence) + { + return ToBits(ToIndices(sentence)); + } + + public string[] GetWords(string sentence) + { + return ToIndices(sentence).Select(i => GetWordAtIndex(i)).ToArray(); + } + } } diff --git a/DotNut/NBitcoin/Bech32/Bech32Encoder.cs b/DotNut/NBitcoin/Bech32/Bech32Encoder.cs index 0e6b1c1..1f6f5d9 100644 --- a/DotNut/NBitcoin/Bech32/Bech32Encoder.cs +++ b/DotNut/NBitcoin/Bech32/Bech32Encoder.cs @@ -4,709 +4,2691 @@ namespace DotNut.NBitcoin.Bech32 { - public abstract class DataEncoder - { - // char.IsWhiteSpace fits well but it match other whitespaces - // characters too and also works for unicode characters. - public static bool IsSpace(char c) - { - switch (c) - { - case ' ': - case '\t': - case '\n': - case '\v': - case '\f': - case '\r': - return true; - } - return false; - } - - internal DataEncoder() - { - } - - public string EncodeData(byte[] data) - { - return EncodeData(data, 0, data.Length); - } - - public abstract string EncodeData(byte[] data, int offset, int count); - - public virtual string EncodeData(ReadOnlySpan data) - { - return this.EncodeData(data.ToArray()); - } - - public abstract byte[] DecodeData(string encoded); - } - - public class ASCIIEncoder : DataEncoder - { - //Do not using Encoding.ASCII (not portable) - public override byte[] DecodeData(string encoded) - { - if (String.IsNullOrEmpty(encoded)) - return new byte[0]; - Span r = encoded.Length is int v && v > 256 ? new byte[v] : stackalloc byte[v]; - for (int i = 0; i < r.Length; i++) - { - r[i] = (byte)encoded[i]; - } - return r.ToArray(); - } - - public void DecodeData(string encoded, Span output) - { - var l = encoded.Length; - for (int i = 0; i < l; i++) - { - output[i] = (byte)encoded[i]; - } - } - - public override string EncodeData(byte[] data, int offset, int count) - { - return new String(data.Skip(offset).Take(count).Select(o => (char)o).ToArray()).Replace("\0", ""); - } - } - - public static class Encoders - { - static readonly ASCIIEncoder _ASCII = new ASCIIEncoder(); - public static DataEncoder ASCII - { - get - { - return _ASCII; - } - } - - public static Bech32Encoder Bech32(string hrp) - { - return new Bech32Encoder(hrp); - } - public static Bech32Encoder Bech32(byte[] hrp) - { - return new Bech32Encoder(hrp); - } - } - - public class Bech32FormatException : FormatException - { - public Bech32FormatException(string message, int[] indexes) : base(message) - { - if (indexes == null) - throw new ArgumentNullException(nameof(indexes)); - ErrorIndexes = indexes; - Array.Sort(ErrorIndexes); - } - public int[] ErrorIndexes - { - get; internal set; - } - } - - public class Bech32EncodingType - { - static Bech32EncodingType() - { - BECH32 = new Bech32EncodingType(1); - BECH32M = new Bech32EncodingType(0x2bc830a3); - All = new Bech32EncodingType[] { BECH32, BECH32M }; - } - public readonly static Bech32EncodingType BECH32; - public readonly static Bech32EncodingType BECH32M; - public readonly static Bech32EncodingType[] All; - public Bech32EncodingType(int encodingConstant) - { - EncodingConstant = encodingConstant; - } - public int EncodingConstant { get; } - } - - public class Bech32Encoder - { - - readonly static int[] GF1024_EXP = new int[] { - 1, 303, 635, 446, 997, 640, 121, 142, 959, 420, 350, 438, 166, 39, 543, - 335, 831, 691, 117, 632, 719, 97, 107, 374, 558, 797, 54, 150, 858, 877, - 724, 1013, 294, 23, 354, 61, 164, 633, 992, 538, 469, 659, 174, 868, 184, - 809, 766, 563, 866, 851, 257, 520, 45, 770, 535, 524, 408, 213, 436, 760, - 472, 330, 933, 799, 616, 361, 15, 391, 756, 814, 58, 608, 554, 680, 993, - 821, 942, 813, 843, 484, 193, 935, 321, 919, 572, 741, 423, 559, 562, - 589, 296, 191, 493, 685, 891, 665, 435, 60, 395, 2, 606, 511, 853, 746, - 32, 219, 284, 631, 840, 661, 837, 332, 78, 311, 670, 887, 111, 195, 505, - 190, 194, 214, 709, 380, 819, 69, 261, 957, 1018, 161, 739, 588, 7, 708, - 83, 328, 507, 736, 317, 899, 47, 348, 1000, 345, 882, 245, 367, 996, 943, - 514, 304, 90, 804, 295, 312, 793, 387, 833, 249, 921, 660, 618, 823, 496, - 722, 30, 782, 225, 892, 93, 480, 372, 112, 738, 867, 636, 890, 950, 968, - 386, 622, 642, 551, 369, 234, 846, 382, 365, 442, 592, 343, 986, 122, - 1023, 59, 847, 81, 790, 4, 437, 983, 931, 244, 64, 415, 529, 487, 944, - 35, 938, 664, 156, 583, 53, 999, 222, 390, 987, 341, 388, 389, 170, 721, - 879, 138, 522, 627, 765, 322, 230, 440, 14, 168, 143, 656, 991, 224, 595, - 550, 94, 657, 752, 667, 1005, 451, 734, 744, 638, 292, 585, 157, 872, - 590, 601, 827, 774, 930, 475, 571, 33, 500, 871, 969, 173, 21, 828, 450, - 1009, 147, 960, 705, 201, 228, 998, 497, 1021, 613, 688, 772, 508, 36, - 366, 715, 468, 956, 725, 730, 861, 425, 647, 701, 221, 759, 95, 958, 139, - 805, 8, 835, 679, 614, 449, 128, 791, 299, 974, 617, 70, 628, 57, 273, - 430, 67, 750, 405, 780, 703, 643, 776, 778, 340, 171, 1022, 276, 308, - 495, 243, 644, 460, 857, 28, 336, 286, 41, 695, 448, 431, 364, 149, 43, - 233, 63, 762, 902, 181, 240, 501, 584, 434, 275, 1008, 444, 443, 895, - 812, 612, 927, 383, 66, 961, 1006, 690, 346, 3, 881, 900, 747, 271, 672, - 162, 402, 456, 748, 971, 755, 490, 105, 808, 977, 72, 732, 182, 897, 625, - 163, 189, 947, 850, 46, 115, 403, 231, 151, 629, 278, 874, 16, 934, 110, - 492, 898, 256, 807, 598, 700, 498, 140, 481, 91, 523, 860, 134, 252, 771, - 824, 119, 38, 816, 820, 641, 342, 757, 513, 577, 990, 463, 40, 920, 955, - 17, 649, 533, 82, 103, 896, 862, 728, 259, 86, 466, 87, 253, 556, 323, - 457, 963, 432, 845, 527, 745, 849, 863, 1015, 888, 488, 567, 727, 132, - 674, 764, 109, 669, 6, 1003, 552, 246, 542, 96, 324, 781, 912, 248, 694, - 239, 980, 210, 880, 683, 144, 177, 325, 546, 491, 326, 339, 623, 941, 92, - 207, 783, 462, 263, 483, 517, 1012, 9, 620, 220, 984, 548, 512, 878, 421, - 113, 973, 280, 962, 159, 310, 945, 268, 465, 806, 889, 199, 76, 873, 865, - 34, 645, 227, 290, 418, 693, 926, 80, 569, 639, 11, 50, 291, 141, 206, - 544, 949, 185, 518, 133, 909, 135, 467, 376, 646, 914, 678, 841, 954, - 318, 242, 939, 951, 743, 1017, 976, 359, 167, 264, 100, 241, 218, 51, 12, - 758, 368, 453, 309, 192, 648, 826, 553, 473, 101, 478, 673, 397, 1001, - 118, 265, 331, 650, 356, 982, 652, 655, 510, 634, 145, 414, 830, 924, - 526, 966, 298, 737, 18, 504, 401, 697, 360, 288, 1020, 842, 203, 698, - 537, 676, 279, 581, 619, 536, 907, 876, 1019, 398, 152, 1010, 994, 68, - 42, 454, 580, 836, 99, 565, 137, 379, 503, 22, 77, 582, 282, 412, 352, - 611, 347, 300, 266, 570, 270, 911, 729, 44, 557, 108, 946, 637, 597, 461, - 630, 615, 238, 763, 681, 718, 334, 528, 200, 459, 413, 79, 24, 229, 713, - 906, 579, 384, 48, 893, 370, 923, 202, 917, 98, 794, 754, 197, 530, 662, - 52, 712, 677, 56, 62, 981, 509, 267, 789, 885, 561, 316, 684, 596, 226, - 13, 985, 779, 123, 720, 576, 753, 948, 406, 125, 315, 104, 519, 426, 502, - 313, 566, 1016, 767, 796, 281, 749, 740, 136, 84, 908, 424, 936, 198, - 355, 274, 735, 967, 5, 154, 428, 541, 785, 704, 486, 671, 600, 532, 381, - 540, 574, 187, 88, 378, 216, 621, 499, 419, 922, 485, 494, 476, 255, 114, - 188, 668, 297, 400, 918, 787, 158, 25, 458, 178, 564, 422, 768, 73, 1011, - 717, 575, 404, 547, 196, 829, 237, 394, 301, 37, 65, 176, 106, 89, 85, - 675, 979, 534, 803, 995, 363, 593, 120, 417, 452, 26, 699, 822, 223, 169, - 416, 235, 609, 773, 211, 607, 208, 302, 852, 965, 603, 357, 761, 247, - 817, 539, 250, 232, 272, 129, 568, 848, 624, 396, 710, 525, 183, 686, 10, - 285, 856, 307, 811, 160, 972, 55, 441, 289, 723, 305, 373, 351, 153, 733, - 409, 506, 975, 838, 573, 970, 988, 913, 471, 205, 337, 49, 594, 777, 549, - 815, 277, 27, 916, 333, 353, 844, 800, 146, 751, 186, 375, 769, 358, 392, - 883, 474, 788, 602, 74, 130, 329, 212, 155, 131, 102, 687, 293, 870, 742, - 726, 427, 217, 834, 904, 29, 127, 869, 407, 338, 832, 470, 482, 810, 399, - 439, 393, 604, 929, 682, 447, 714, 251, 455, 875, 319, 477, 464, 521, - 258, 377, 937, 489, 792, 172, 314, 327, 124, 20, 531, 953, 591, 886, 320, - 696, 71, 859, 578, 175, 587, 707, 663, 283, 179, 795, 989, 702, 940, 371, - 692, 689, 555, 903, 410, 651, 75, 429, 818, 362, 894, 515, 31, 545, 666, - 706, 952, 864, 269, 254, 349, 711, 802, 716, 784, 1007, 925, 801, 445, - 148, 260, 658, 385, 287, 262, 204, 126, 586, 1004, 236, 165, 854, 411, - 932, 560, 19, 215, 1002, 775, 653, 928, 901, 964, 884, 798, 839, 786, - 433, 610, 116, 855, 180, 479, 910, 1014, 599, 915, 905, 306, 516, 731, - 626, 978, 825, 344, 605, 654, 209 - }; - - readonly static int[] GF1024_LOG = new[] { - -1, 0, 99, 363, 198, 726, 462, 132, 297, 495, 825, 528, 561, 693, 231, - 66, 396, 429, 594, 990, 924, 264, 627, 33, 660, 759, 792, 858, 330, 891, - 165, 957, 104, 259, 518, 208, 280, 776, 416, 13, 426, 333, 618, 339, 641, - 52, 388, 140, 666, 852, 529, 560, 678, 213, 26, 832, 681, 309, 70, 194, - 97, 35, 682, 341, 203, 777, 358, 312, 617, 125, 307, 931, 379, 765, 875, - 951, 515, 628, 112, 659, 525, 196, 432, 134, 717, 781, 438, 440, 740, - 780, 151, 408, 487, 169, 239, 293, 467, 21, 672, 622, 557, 571, 881, 433, - 704, 376, 779, 22, 643, 460, 398, 116, 172, 503, 751, 389, 1004, 18, 576, - 415, 789, 6, 192, 696, 923, 702, 981, 892, 302, 816, 876, 880, 457, 537, - 411, 539, 716, 624, 224, 295, 406, 531, 7, 233, 478, 586, 864, 268, 974, - 338, 27, 392, 614, 839, 727, 879, 211, 250, 758, 507, 830, 129, 369, 384, - 36, 985, 12, 555, 232, 796, 221, 321, 920, 263, 42, 934, 778, 479, 761, - 939, 1006, 344, 381, 823, 44, 535, 866, 739, 752, 385, 119, 91, 566, 80, - 120, 117, 771, 675, 721, 514, 656, 271, 670, 602, 980, 850, 532, 488, - 803, 1022, 475, 801, 878, 57, 121, 991, 742, 888, 559, 105, 497, 291, - 215, 795, 236, 167, 692, 520, 272, 661, 229, 391, 814, 340, 184, 798, - 984, 773, 650, 473, 345, 558, 548, 326, 202, 145, 465, 810, 471, 158, - 813, 908, 412, 441, 964, 750, 401, 50, 915, 437, 975, 126, 979, 491, 556, - 577, 636, 685, 510, 963, 638, 367, 815, 310, 723, 349, 323, 857, 394, - 606, 505, 713, 630, 938, 106, 826, 332, 978, 599, 834, 521, 530, 248, - 883, 32, 153, 90, 754, 592, 304, 635, 775, 804, 1, 150, 836, 1013, 828, - 324, 565, 508, 113, 154, 708, 921, 703, 689, 138, 547, 911, 929, 82, 228, - 443, 468, 480, 483, 922, 135, 877, 61, 578, 111, 860, 654, 15, 331, 851, - 895, 484, 320, 218, 420, 190, 1019, 143, 362, 634, 141, 965, 10, 838, - 632, 861, 34, 722, 580, 808, 869, 554, 598, 65, 954, 787, 337, 187, 281, - 146, 563, 183, 668, 944, 171, 837, 23, 867, 541, 916, 741, 625, 123, 736, - 186, 357, 665, 977, 179, 156, 219, 220, 216, 67, 870, 902, 774, 98, 820, - 574, 613, 900, 755, 596, 370, 390, 769, 314, 701, 894, 56, 841, 949, 987, - 631, 658, 587, 204, 797, 790, 522, 745, 9, 502, 763, 86, 719, 288, 706, - 887, 728, 952, 311, 336, 446, 1002, 348, 96, 58, 199, 11, 901, 230, 833, - 188, 352, 351, 973, 3, 906, 335, 301, 266, 244, 791, 564, 619, 909, 371, - 444, 760, 657, 328, 647, 490, 425, 913, 511, 439, 540, 283, 40, 897, 849, - 60, 570, 872, 257, 749, 912, 572, 1007, 170, 407, 898, 492, 79, 747, 732, - 206, 454, 918, 375, 482, 399, 92, 748, 325, 163, 274, 405, 744, 260, 346, - 707, 626, 595, 118, 842, 136, 279, 684, 584, 101, 500, 422, 149, 956, - 1014, 493, 536, 705, 51, 914, 225, 409, 55, 822, 590, 448, 655, 205, 676, - 925, 735, 431, 784, 54, 609, 604, 39, 812, 737, 729, 466, 14, 533, 958, - 481, 770, 499, 855, 238, 182, 464, 569, 72, 947, 442, 642, 24, 87, 989, - 688, 88, 47, 762, 623, 709, 455, 817, 526, 637, 258, 84, 845, 738, 768, - 698, 423, 933, 664, 620, 607, 629, 212, 347, 249, 982, 935, 131, 89, 252, - 927, 189, 788, 853, 237, 691, 646, 403, 1010, 734, 253, 874, 807, 903, - 1020, 100, 802, 71, 799, 1003, 633, 355, 276, 300, 649, 64, 306, 161, - 608, 496, 743, 180, 485, 819, 383, 1016, 226, 308, 393, 648, 107, 19, 37, - 585, 2, 175, 645, 247, 527, 5, 419, 181, 317, 327, 519, 542, 289, 567, - 430, 579, 950, 582, 994, 1021, 583, 234, 240, 976, 41, 160, 109, 677, - 937, 210, 95, 959, 242, 753, 461, 114, 733, 368, 573, 458, 782, 605, 680, - 544, 299, 73, 652, 905, 477, 690, 93, 824, 882, 277, 946, 361, 17, 945, - 523, 472, 334, 930, 597, 603, 793, 404, 290, 942, 316, 731, 270, 960, - 936, 133, 122, 821, 966, 679, 662, 907, 282, 968, 767, 653, 20, 697, 222, - 164, 835, 30, 285, 886, 456, 436, 640, 286, 1015, 380, 840, 245, 724, - 137, 593, 173, 130, 715, 85, 885, 551, 246, 449, 103, 366, 372, 714, 313, - 865, 241, 699, 674, 374, 68, 421, 562, 292, 59, 809, 342, 651, 459, 227, - 46, 711, 764, 868, 53, 413, 278, 800, 255, 993, 318, 854, 319, 695, 315, - 469, 166, 489, 969, 730, 1001, 757, 873, 686, 197, 303, 919, 155, 673, - 940, 712, 25, 999, 63, 863, 972, 967, 785, 152, 296, 512, 402, 377, 45, - 899, 829, 354, 77, 69, 856, 417, 811, 953, 124, 418, 75, 794, 162, 414, - 1018, 568, 254, 265, 772, 588, 16, 896, 157, 889, 298, 621, 110, 844, - 1000, 108, 545, 601, 78, 862, 447, 185, 195, 818, 450, 387, 49, 805, 102, - 986, 1005, 827, 329, 28, 932, 410, 287, 435, 451, 962, 517, 48, 174, 43, - 893, 884, 261, 251, 516, 395, 910, 611, 29, 501, 223, 476, 364, 144, 871, - 998, 687, 928, 115, 453, 513, 176, 94, 168, 667, 955, 353, 434, 382, 400, - 139, 365, 996, 343, 948, 890, 1012, 663, 610, 718, 538, 1008, 639, 470, - 848, 543, 1011, 859, 671, 756, 83, 427, 159, 746, 669, 589, 971, 524, - 356, 995, 904, 256, 201, 988, 62, 397, 81, 720, 917, 209, 549, 943, 486, - 76, 148, 207, 509, 644, 386, 700, 534, 177, 550, 961, 926, 546, 428, 284, - 127, 294, 8, 269, 359, 506, 445, 997, 806, 591, 725, 178, 262, 846, 373, - 831, 504, 305, 843, 553, 378, 1017, 783, 474, 683, 581, 200, 498, 694, - 191, 217, 847, 941, 424, 235, 38, 74, 616, 786, 147, 4, 273, 214, 142, - 575, 992, 463, 983, 243, 360, 970, 350, 267, 615, 766, 494, 31, 1009, - 452, 710, 552, 128, 612, 600, 275, 322, 193 - }; - - protected static readonly byte[] Byteset; - static Bech32Encoder() - { - Byteset = Encoders.ASCII.DecodeData("qpzry9x8gf2tvdw0s3jn54khce6mua7l"); - } - private static readonly uint[] Generator = { 0x3b6a57b2U, 0x26508e6dU, 0x1ea119faU, 0x3d4233ddU, 0x2a1462b3U }; - - - uint syndrome(uint residue) - { - var low = residue & 0x1f; - return (uint)(low ^ (low << 10) ^ (low << 20) ^ - (((residue >> 5) & 1) != 0 ? 0x31edd3c4 : 0) ^ - (((residue >> 6) & 1) != 0 ? 0x335f86a8 : 0) ^ - (((residue >> 7) & 1) != 0 ? 0x363b8870 : 0) ^ - (((residue >> 8) & 1) != 0 ? 0x3e6390c9 : 0) ^ - (((residue >> 9) & 1) != 0 ? 0x2ec72192 : 0) ^ - (((residue >> 10) & 1) != 0 ? 0x1046f79d : 0) ^ - (((residue >> 11) & 1) != 0 ? 0x208d4e33 : 0) ^ - (((residue >> 12) & 1) != 0 ? 0x130ebd6f : 0) ^ - (((residue >> 13) & 1) != 0 ? 0x2499fade : 0) ^ - (((residue >> 14) & 1) != 0 ? 0x1b27d4b5 : 0) ^ - (((residue >> 15) & 1) != 0 ? 0x04be1eb4 : 0) ^ - (((residue >> 16) & 1) != 0 ? 0x0968b861 : 0) ^ - (((residue >> 17) & 1) != 0 ? 0x1055f0c2 : 0) ^ - (((residue >> 18) & 1) != 0 ? 0x20ab4584 : 0) ^ - (((residue >> 19) & 1) != 0 ? 0x1342af08 : 0) ^ - (((residue >> 20) & 1) != 0 ? 0x24f1f318 : 0) ^ - (((residue >> 21) & 1) != 0 ? 0x1be34739 : 0) ^ - (((residue >> 22) & 1) != 0 ? 0x35562f7b : 0) ^ - (((residue >> 23) & 1) != 0 ? 0x3a3c5bff : 0) ^ - (((residue >> 24) & 1) != 0 ? 0x266c96f7 : 0) ^ - (((residue >> 25) & 1) != 0 ? 0x25c78b65 : 0) ^ - (((residue >> 26) & 1) != 0 ? 0x1b1f13ea : 0) ^ - (((residue >> 27) & 1) != 0 ? 0x34baa2f4 : 0) ^ - (((residue >> 28) & 1) != 0 ? 0x3b61c0e1 : 0) ^ - (((residue >> 29) & 1) != 0 ? 0x265325c2 : 0)); - } - - - int[] locate_errors(uint residue, int length) - { - if (residue == 0) - { - return new int[0]; - } - var syn = syndrome(residue); - var s0 = syn & 0x3FF; - var s1 = (syn >> 10) & 0x3FF; - var s2 = syn >> 20; - var l_s0 = GF1024_LOG[s0]; - var l_s1 = GF1024_LOG[s1]; - var l_s2 = GF1024_LOG[s2]; - if (l_s0 != -1 && l_s1 != -1 && l_s2 != -1 && (2 * l_s1 - l_s2 - l_s0 + 2046) % 1023 == 0) - { - var p1 = (l_s1 - l_s0 + 1023) % 1023; - if (p1 >= length) - return new int[0]; - var l_e1 = l_s0 + (1023 - 997) * p1; - if ((l_e1 % 33) != 0) - return new int[0]; - return new[] { p1 }; - } - for (var p1 = 0; p1 < length; p1++) - { - var s2_s1p1 = s2 ^ (s1 == 0 ? 0 : GF1024_EXP[(l_s1 + p1) % 1023]); - if (s2_s1p1 == 0) - continue; - var s1_s0p1 = s1 ^ (s0 == 0 ? 0 : GF1024_EXP[(l_s0 + p1) % 1023]); - if (s1_s0p1 == 0) - continue; - var l_s1_s0p1 = GF1024_LOG[s1_s0p1]; - var p2 = (GF1024_LOG[s2_s1p1] - l_s1_s0p1 + 1023) % 1023; - if (p2 >= length || p1 == p2) - continue; - var s1_s0p2 = s1 ^ (s0 == 0 ? 0 : GF1024_EXP[(l_s0 + p2) % 1023]); - if (s1_s0p2 == 0) - continue; - var inv_p1_p2 = 1023 - GF1024_LOG[GF1024_EXP[p1] ^ GF1024_EXP[p2]]; - var l_e2 = l_s1_s0p1 + inv_p1_p2 + (1023 - 997) * p2; - if ((l_e2 % 33) != 0) - continue; - var l_e1 = GF1024_LOG[s1_s0p2] + inv_p1_p2 + (1023 - 997) * p1; - if ((l_e1 % 33) != 0) - continue; - if (p1 < p2) - { - return new int[] { p1, p2 }; - } - else - { - return new int[] { p2, p1 }; - } - } - return new int[0]; - } - - internal Bech32Encoder(string hrp) : this(hrp == null ? null : Encoders.ASCII.DecodeData(hrp.ToLowerInvariant())) - { - } - public Bech32Encoder(byte[] hrp) - { - if (hrp == null) - throw new ArgumentNullException(nameof(hrp)); - - _Hrp = hrp; - var len = hrp.Length; - _HrpExpand = new byte[(2 * len) + 1]; - for (int i = 0; i < len; i++) - { - _HrpExpand[i] = (byte)(hrp[i] >> 5); - _HrpExpand[i + len + 1] = (byte)(hrp[i] & 31); - } - } - - protected readonly byte[] _HrpExpand; - protected readonly byte[] _Hrp; - public byte[] HumanReadablePart - { - get - { - return _Hrp; - } - } - - private static uint Polymod(ReadOnlySpan values) - { - uint chk = 1; - for (int i = 0; i < values.Length; i++) - { - var top = chk >> 25; - chk = values[i] ^ ((chk & 0x1ffffff) << 5); - chk ^= ((top >> 0) & 1) == 1 ? Generator[0] : 0; - chk ^= ((top >> 1) & 1) == 1 ? Generator[1] : 0; - chk ^= ((top >> 2) & 1) == 1 ? Generator[2] : 0; - chk ^= ((top >> 3) & 1) == 1 ? Generator[3] : 0; - chk ^= ((top >> 4) & 1) == 1 ? Generator[4] : 0; - } - return chk; - } - - protected virtual bool VerifyChecksum(byte[] data, int bechStringLen, out Bech32EncodingType encodingType, out int[] errorPosition) - { - return VerifyChecksum(data.AsSpan(), bechStringLen, out encodingType, out errorPosition); - } - - protected virtual bool VerifyChecksum(ReadOnlySpan data, int bechStringLen, out Bech32EncodingType encodingType, out int[] errorPosition) - { - errorPosition = null; - Span values = _HrpExpand.Length + data.Length is int v && v > 256 ? new byte[v] : stackalloc byte[v]; - _HrpExpand.CopyTo(values); - data.CopyTo(values.Slice(_HrpExpand.Length)); - var polymod = Polymod(values); - if (polymod == Bech32EncodingType.BECH32.EncodingConstant) - encodingType = Bech32EncodingType.BECH32; - else if (polymod == Bech32EncodingType.BECH32M.EncodingConstant) - encodingType = Bech32EncodingType.BECH32M; - else - { - encodingType = null; - - var epos = Bech32EncodingType.All - .Select(e => locate_errors(polymod ^ (uint)e.EncodingConstant, bechStringLen - 1)) - .Where(e => e.Length != 0) - .OrderByDescending(e => e.Length) - .FirstOrDefault(); - errorPosition = epos; - if (epos is null || epos.Length == 0) - return false; - for (var ep = 0; ep < epos.Length; ++ep) - { - epos[ep] = bechStringLen - epos[ep] - (epos[ep] >= data.Length ? 2 : 1); - } - return false; - } - return true; - } - - private void CreateChecksum(ReadOnlySpan data, Bech32EncodingType encodingType, Span ret) - { - Span values = _HrpExpand.Length + data.Length + 6 is int v && v > 256 ? new byte[v] : - stackalloc byte[v]; - var valuesOffset = 0; - _HrpExpand.AsSpan().CopyTo(values.Slice(valuesOffset)); - valuesOffset += _HrpExpand.Length; - data.CopyTo(values.Slice(valuesOffset)); - valuesOffset += data.Length; - var polymod = Polymod(values) ^ encodingType.EncodingConstant; - foreach (var i in Enumerable.Range(0, 6)) - { - ret[i] = (byte)((polymod >> 5 * (5 - i)) & 31); - } - } - - public virtual string EncodeData(ReadOnlySpan data, Bech32EncodingType encodingType) - { - if (encodingType == null) - throw new ArgumentNullException(nameof(encodingType)); - if (SquashBytes) - data = ByteSquasher(data, 8, 5).AsSpan(); - - Span combined = _Hrp.Length + 1 + data.Length + 6 is int v && v > 256 ? new byte[v] : - stackalloc byte[v]; - int combinedOffset = 0; - _Hrp.CopyTo(combined); - combinedOffset += _Hrp.Length; - combined[combinedOffset] = 49; - combinedOffset++; - - data.CopyTo(combined.Slice(combinedOffset)); - combinedOffset += data.Length; - Span checkSum = stackalloc byte[6]; - CreateChecksum(data, encodingType, checkSum); - - checkSum.CopyTo(combined.Slice(combinedOffset, 6)); - combinedOffset += 6; - - for (int i = 0; i < data.Length + 6; i++) - { - combined[_Hrp.Length + 1 + i] = Byteset[combined[_Hrp.Length + 1 + i]]; - } - return Encoders.ASCII.EncodeData(combined.ToArray()); - } - - public static Bech32Encoder ExtractEncoderFromString(string test) - { - var i = test.LastIndexOf('1'); - if (i == -1) - throw new FormatException("Invalid Bech32 string"); - - return Encoders.Bech32(test.Substring(0, i)); - } - - protected virtual void CheckCase(string hrp) - { - if (hrp.Length is 0) - return; - bool isLowercase = char.IsUpper(hrp[0]); - for (int i = 1; i < hrp.Length; i++) - { - if (isLowercase != char.IsUpper(hrp[i]) && !char.IsDigit(hrp[i])) - throw new FormatException("Invalid bech32 string, mixed case detected"); - } - } - public byte[] DecodeDataRaw(string encoded, out Bech32EncodingType encodingType) - { - return DecodeDataCore(encoded, out encodingType); - } - public bool StrictLength { get; set; } = true; - public bool SquashBytes { get; set; } = false; - - protected virtual byte[] DecodeDataCore(string encoded, out Bech32EncodingType encodingType) - { - if (encoded == null) - throw new ArgumentNullException(nameof(encoded)); - CheckCase(encoded); - encoded = encoded.ToLowerInvariant(); - Span buffer = encoded.Length is int v && v > 256 ? new byte[v] : stackalloc byte[v]; - ((ASCIIEncoder)Encoders.ASCII).DecodeData(encoded, buffer); - var pos = encoded.LastIndexOf("1", StringComparison.OrdinalIgnoreCase); - if (pos < 1) - { - throw new FormatException("The Bech32 string is missing separator '1'"); - } - else if (pos + 7 > encoded.Length) - { - throw new FormatException("The Bech32 string is too short"); - } - else if (StrictLength && encoded.Length > 90) - { - throw new FormatException("The Bech32 string is too long"); - } - if (pos != _Hrp.Length) - { - throw new FormatException("Mismatching human readable part"); - } - - for (int i = 0; i < _Hrp.Length; i++) - { - if (buffer[i] != _Hrp[i]) - throw new FormatException("Mismatching human readable part"); - } - Span data = encoded.Length - pos - 1 is int v2 && v2 > 256 ? new byte[v2] : stackalloc byte[v2]; - for (int j = 0, i = pos + 1; i < encoded.Length; i++, j++) - { - int index = Array.IndexOf(Byteset, buffer[i]); - if (index == -1) - throw new FormatException("bech chars are out of range"); - data[j] = (byte)index; - } - - int[] error; - if (!VerifyChecksum(data, encoded.Length, out encodingType, out error)) - { - if (error == null || error.Length == 0) - throw new FormatException("Error while verifying Bech32 checksum"); - else - throw new Bech32FormatException($"Error in Bech32 string at {String.Join(",", error)}", error); - } - var arr = data.Slice(0, data.Length - 6).ToArray(); - if (SquashBytes) - { - arr = ByteSquasher(arr, 5, 8); - if (arr is null) - throw new FormatException("Invalid squashed bech32"); - } - return arr; - } - - private static byte[] ByteSquasher(ReadOnlySpan input, int inputWidth, int outputWidth) - { - var bitstash = 0; - var accumulator = 0; - var output = new List(); - var maxOutputValue = (1 << outputWidth) - 1; - for (var i = 0; i < input.Length; i++) - { - var c = input[i]; - if (c >> inputWidth != 0) - { - return null; - } - - accumulator = (accumulator << inputWidth) | c; - bitstash += inputWidth; - while (bitstash >= outputWidth) - { - bitstash -= outputWidth; - output.Add((byte)((accumulator >> bitstash) & maxOutputValue)); - } - } - - // pad if going from 8 to 5 - if (inputWidth == 8 && outputWidth == 5) - { - if (bitstash != 0) output.Add((byte)((accumulator << (outputWidth - bitstash)) & maxOutputValue)); - } - else if (bitstash >= inputWidth || ((accumulator << (outputWidth - bitstash)) & maxOutputValue) != 0) - { - // no pad from 5 to 8 allowed - return null; - } - - return output.ToArray(); - } - - protected virtual byte[] ConvertBits(IEnumerable data, int fromBits, int toBits, bool pad = true) - { - return ConvertBits(data.ToArray().AsSpan(), fromBits, toBits, pad); - } - - protected virtual byte[] ConvertBits(ReadOnlySpan data, int fromBits, int toBits, bool pad = true) - { - var acc = 0; - var bits = 0; - var maxv = (1 << toBits) - 1; - var ret = new List(64); - foreach (var value in data) - { - if ((value >> fromBits) > 0) - throw new FormatException("Invalid Bech32 string"); - acc = (acc << fromBits) | value; - bits += fromBits; - while (bits >= toBits) - { - bits -= toBits; - ret.Add((byte)((acc >> bits) & maxv)); - } - } - if (pad) - { - if (bits > 0) - { - ret.Add((byte)((acc << (toBits - bits)) & maxv)); - } - } - else if (bits >= fromBits || (byte)(((acc << (toBits - bits)) & maxv)) != 0) - { - throw new FormatException("Invalid Bech32 string"); - } - return ret.ToArray(); - } - - public virtual byte[] Decode(string addr, out byte witnessVerion) - { - if (addr == null) - throw new ArgumentNullException(nameof(addr)); - CheckCase(addr); - var data = DecodeDataCore(addr, out var encodingType); - var decoded = ConvertBits(data.AsSpan().Slice(1), 5, 8, false); - if (decoded.Length < 2 || decoded.Length > 40) - throw new FormatException("Invalid decoded data length"); - witnessVerion = data[0]; - if (witnessVerion == 0 && encodingType != Bech32EncodingType.BECH32) - throw new FormatException("Decoded data should have used BECH32 encoding"); - if (witnessVerion != 0 && encodingType != Bech32EncodingType.BECH32M) - throw new FormatException("Decoded data should have used BECH32M encoding"); - if (witnessVerion > 16) - throw new FormatException("Invalid decoded witness version"); - - if (witnessVerion == 0 && decoded.Length != 20 && decoded.Length != 32) - throw new FormatException("Decoded witness program with unknown length"); - return decoded; - } - - public string EncodeRaw(byte[] data, Bech32EncodingType encodingType) - { - return EncodeData(data.AsSpan(), encodingType); - } - - public string EncodeRaw(ReadOnlySpan data, Bech32EncodingType encodingType) - { - return EncodeData(data, encodingType); - } - - public string Encode(byte witnessVerion, byte[] witnessProgramm) - { - if (witnessProgramm == null) - throw new ArgumentNullException(nameof(witnessProgramm)); - return Encode(witnessVerion, witnessProgramm.AsSpan()); - } - - public string Encode(byte witnessVerion, ReadOnlySpan witnessProgramm) - { - if (witnessVerion > 16) - throw new ArgumentOutOfRangeException(nameof(witnessVerion), "Invalid decoded witnessVerion, should <= 0 and > 16"); - var bits = ConvertBits(witnessProgramm, 8, 5); - Span data = 1 + bits.Length is int v && v > 256 ? new byte[v] : stackalloc byte[v]; - data[0] = witnessVerion; - bits.AsSpan().CopyTo(data.Slice(1)); - var ret = EncodeData(data, witnessVerion == 0 ? Bech32EncodingType.BECH32 : Bech32EncodingType.BECH32M); - return ret; - } - } + public abstract class DataEncoder + { + // char.IsWhiteSpace fits well but it match other whitespaces + // characters too and also works for unicode characters. + public static bool IsSpace(char c) + { + switch (c) + { + case ' ': + case '\t': + case '\n': + case '\v': + case '\f': + case '\r': + return true; + } + return false; + } + + internal DataEncoder() { } + + public string EncodeData(byte[] data) + { + return EncodeData(data, 0, data.Length); + } + + public abstract string EncodeData(byte[] data, int offset, int count); + + public virtual string EncodeData(ReadOnlySpan data) + { + return this.EncodeData(data.ToArray()); + } + + public abstract byte[] DecodeData(string encoded); + } + + public class ASCIIEncoder : DataEncoder + { + //Do not using Encoding.ASCII (not portable) + public override byte[] DecodeData(string encoded) + { + if (String.IsNullOrEmpty(encoded)) + return new byte[0]; + Span r = encoded.Length is int v && v > 256 ? new byte[v] : stackalloc byte[v]; + for (int i = 0; i < r.Length; i++) + { + r[i] = (byte)encoded[i]; + } + return r.ToArray(); + } + + public void DecodeData(string encoded, Span output) + { + var l = encoded.Length; + for (int i = 0; i < l; i++) + { + output[i] = (byte)encoded[i]; + } + } + + public override string EncodeData(byte[] data, int offset, int count) + { + return new String(data.Skip(offset).Take(count).Select(o => (char)o).ToArray()).Replace( + "\0", + "" + ); + } + } + + public static class Encoders + { + static readonly ASCIIEncoder _ASCII = new ASCIIEncoder(); + public static DataEncoder ASCII + { + get { return _ASCII; } + } + + public static Bech32Encoder Bech32(string hrp) + { + return new Bech32Encoder(hrp); + } + + public static Bech32Encoder Bech32(byte[] hrp) + { + return new Bech32Encoder(hrp); + } + } + + public class Bech32FormatException : FormatException + { + public Bech32FormatException(string message, int[] indexes) + : base(message) + { + if (indexes == null) + throw new ArgumentNullException(nameof(indexes)); + ErrorIndexes = indexes; + Array.Sort(ErrorIndexes); + } + + public int[] ErrorIndexes { get; internal set; } + } + + public class Bech32EncodingType + { + static Bech32EncodingType() + { + BECH32 = new Bech32EncodingType(1); + BECH32M = new Bech32EncodingType(0x2bc830a3); + All = new Bech32EncodingType[] { BECH32, BECH32M }; + } + + public static readonly Bech32EncodingType BECH32; + public static readonly Bech32EncodingType BECH32M; + public static readonly Bech32EncodingType[] All; + + public Bech32EncodingType(int encodingConstant) + { + EncodingConstant = encodingConstant; + } + + public int EncodingConstant { get; } + } + + public class Bech32Encoder + { + static readonly int[] GF1024_EXP = new int[] + { + 1, + 303, + 635, + 446, + 997, + 640, + 121, + 142, + 959, + 420, + 350, + 438, + 166, + 39, + 543, + 335, + 831, + 691, + 117, + 632, + 719, + 97, + 107, + 374, + 558, + 797, + 54, + 150, + 858, + 877, + 724, + 1013, + 294, + 23, + 354, + 61, + 164, + 633, + 992, + 538, + 469, + 659, + 174, + 868, + 184, + 809, + 766, + 563, + 866, + 851, + 257, + 520, + 45, + 770, + 535, + 524, + 408, + 213, + 436, + 760, + 472, + 330, + 933, + 799, + 616, + 361, + 15, + 391, + 756, + 814, + 58, + 608, + 554, + 680, + 993, + 821, + 942, + 813, + 843, + 484, + 193, + 935, + 321, + 919, + 572, + 741, + 423, + 559, + 562, + 589, + 296, + 191, + 493, + 685, + 891, + 665, + 435, + 60, + 395, + 2, + 606, + 511, + 853, + 746, + 32, + 219, + 284, + 631, + 840, + 661, + 837, + 332, + 78, + 311, + 670, + 887, + 111, + 195, + 505, + 190, + 194, + 214, + 709, + 380, + 819, + 69, + 261, + 957, + 1018, + 161, + 739, + 588, + 7, + 708, + 83, + 328, + 507, + 736, + 317, + 899, + 47, + 348, + 1000, + 345, + 882, + 245, + 367, + 996, + 943, + 514, + 304, + 90, + 804, + 295, + 312, + 793, + 387, + 833, + 249, + 921, + 660, + 618, + 823, + 496, + 722, + 30, + 782, + 225, + 892, + 93, + 480, + 372, + 112, + 738, + 867, + 636, + 890, + 950, + 968, + 386, + 622, + 642, + 551, + 369, + 234, + 846, + 382, + 365, + 442, + 592, + 343, + 986, + 122, + 1023, + 59, + 847, + 81, + 790, + 4, + 437, + 983, + 931, + 244, + 64, + 415, + 529, + 487, + 944, + 35, + 938, + 664, + 156, + 583, + 53, + 999, + 222, + 390, + 987, + 341, + 388, + 389, + 170, + 721, + 879, + 138, + 522, + 627, + 765, + 322, + 230, + 440, + 14, + 168, + 143, + 656, + 991, + 224, + 595, + 550, + 94, + 657, + 752, + 667, + 1005, + 451, + 734, + 744, + 638, + 292, + 585, + 157, + 872, + 590, + 601, + 827, + 774, + 930, + 475, + 571, + 33, + 500, + 871, + 969, + 173, + 21, + 828, + 450, + 1009, + 147, + 960, + 705, + 201, + 228, + 998, + 497, + 1021, + 613, + 688, + 772, + 508, + 36, + 366, + 715, + 468, + 956, + 725, + 730, + 861, + 425, + 647, + 701, + 221, + 759, + 95, + 958, + 139, + 805, + 8, + 835, + 679, + 614, + 449, + 128, + 791, + 299, + 974, + 617, + 70, + 628, + 57, + 273, + 430, + 67, + 750, + 405, + 780, + 703, + 643, + 776, + 778, + 340, + 171, + 1022, + 276, + 308, + 495, + 243, + 644, + 460, + 857, + 28, + 336, + 286, + 41, + 695, + 448, + 431, + 364, + 149, + 43, + 233, + 63, + 762, + 902, + 181, + 240, + 501, + 584, + 434, + 275, + 1008, + 444, + 443, + 895, + 812, + 612, + 927, + 383, + 66, + 961, + 1006, + 690, + 346, + 3, + 881, + 900, + 747, + 271, + 672, + 162, + 402, + 456, + 748, + 971, + 755, + 490, + 105, + 808, + 977, + 72, + 732, + 182, + 897, + 625, + 163, + 189, + 947, + 850, + 46, + 115, + 403, + 231, + 151, + 629, + 278, + 874, + 16, + 934, + 110, + 492, + 898, + 256, + 807, + 598, + 700, + 498, + 140, + 481, + 91, + 523, + 860, + 134, + 252, + 771, + 824, + 119, + 38, + 816, + 820, + 641, + 342, + 757, + 513, + 577, + 990, + 463, + 40, + 920, + 955, + 17, + 649, + 533, + 82, + 103, + 896, + 862, + 728, + 259, + 86, + 466, + 87, + 253, + 556, + 323, + 457, + 963, + 432, + 845, + 527, + 745, + 849, + 863, + 1015, + 888, + 488, + 567, + 727, + 132, + 674, + 764, + 109, + 669, + 6, + 1003, + 552, + 246, + 542, + 96, + 324, + 781, + 912, + 248, + 694, + 239, + 980, + 210, + 880, + 683, + 144, + 177, + 325, + 546, + 491, + 326, + 339, + 623, + 941, + 92, + 207, + 783, + 462, + 263, + 483, + 517, + 1012, + 9, + 620, + 220, + 984, + 548, + 512, + 878, + 421, + 113, + 973, + 280, + 962, + 159, + 310, + 945, + 268, + 465, + 806, + 889, + 199, + 76, + 873, + 865, + 34, + 645, + 227, + 290, + 418, + 693, + 926, + 80, + 569, + 639, + 11, + 50, + 291, + 141, + 206, + 544, + 949, + 185, + 518, + 133, + 909, + 135, + 467, + 376, + 646, + 914, + 678, + 841, + 954, + 318, + 242, + 939, + 951, + 743, + 1017, + 976, + 359, + 167, + 264, + 100, + 241, + 218, + 51, + 12, + 758, + 368, + 453, + 309, + 192, + 648, + 826, + 553, + 473, + 101, + 478, + 673, + 397, + 1001, + 118, + 265, + 331, + 650, + 356, + 982, + 652, + 655, + 510, + 634, + 145, + 414, + 830, + 924, + 526, + 966, + 298, + 737, + 18, + 504, + 401, + 697, + 360, + 288, + 1020, + 842, + 203, + 698, + 537, + 676, + 279, + 581, + 619, + 536, + 907, + 876, + 1019, + 398, + 152, + 1010, + 994, + 68, + 42, + 454, + 580, + 836, + 99, + 565, + 137, + 379, + 503, + 22, + 77, + 582, + 282, + 412, + 352, + 611, + 347, + 300, + 266, + 570, + 270, + 911, + 729, + 44, + 557, + 108, + 946, + 637, + 597, + 461, + 630, + 615, + 238, + 763, + 681, + 718, + 334, + 528, + 200, + 459, + 413, + 79, + 24, + 229, + 713, + 906, + 579, + 384, + 48, + 893, + 370, + 923, + 202, + 917, + 98, + 794, + 754, + 197, + 530, + 662, + 52, + 712, + 677, + 56, + 62, + 981, + 509, + 267, + 789, + 885, + 561, + 316, + 684, + 596, + 226, + 13, + 985, + 779, + 123, + 720, + 576, + 753, + 948, + 406, + 125, + 315, + 104, + 519, + 426, + 502, + 313, + 566, + 1016, + 767, + 796, + 281, + 749, + 740, + 136, + 84, + 908, + 424, + 936, + 198, + 355, + 274, + 735, + 967, + 5, + 154, + 428, + 541, + 785, + 704, + 486, + 671, + 600, + 532, + 381, + 540, + 574, + 187, + 88, + 378, + 216, + 621, + 499, + 419, + 922, + 485, + 494, + 476, + 255, + 114, + 188, + 668, + 297, + 400, + 918, + 787, + 158, + 25, + 458, + 178, + 564, + 422, + 768, + 73, + 1011, + 717, + 575, + 404, + 547, + 196, + 829, + 237, + 394, + 301, + 37, + 65, + 176, + 106, + 89, + 85, + 675, + 979, + 534, + 803, + 995, + 363, + 593, + 120, + 417, + 452, + 26, + 699, + 822, + 223, + 169, + 416, + 235, + 609, + 773, + 211, + 607, + 208, + 302, + 852, + 965, + 603, + 357, + 761, + 247, + 817, + 539, + 250, + 232, + 272, + 129, + 568, + 848, + 624, + 396, + 710, + 525, + 183, + 686, + 10, + 285, + 856, + 307, + 811, + 160, + 972, + 55, + 441, + 289, + 723, + 305, + 373, + 351, + 153, + 733, + 409, + 506, + 975, + 838, + 573, + 970, + 988, + 913, + 471, + 205, + 337, + 49, + 594, + 777, + 549, + 815, + 277, + 27, + 916, + 333, + 353, + 844, + 800, + 146, + 751, + 186, + 375, + 769, + 358, + 392, + 883, + 474, + 788, + 602, + 74, + 130, + 329, + 212, + 155, + 131, + 102, + 687, + 293, + 870, + 742, + 726, + 427, + 217, + 834, + 904, + 29, + 127, + 869, + 407, + 338, + 832, + 470, + 482, + 810, + 399, + 439, + 393, + 604, + 929, + 682, + 447, + 714, + 251, + 455, + 875, + 319, + 477, + 464, + 521, + 258, + 377, + 937, + 489, + 792, + 172, + 314, + 327, + 124, + 20, + 531, + 953, + 591, + 886, + 320, + 696, + 71, + 859, + 578, + 175, + 587, + 707, + 663, + 283, + 179, + 795, + 989, + 702, + 940, + 371, + 692, + 689, + 555, + 903, + 410, + 651, + 75, + 429, + 818, + 362, + 894, + 515, + 31, + 545, + 666, + 706, + 952, + 864, + 269, + 254, + 349, + 711, + 802, + 716, + 784, + 1007, + 925, + 801, + 445, + 148, + 260, + 658, + 385, + 287, + 262, + 204, + 126, + 586, + 1004, + 236, + 165, + 854, + 411, + 932, + 560, + 19, + 215, + 1002, + 775, + 653, + 928, + 901, + 964, + 884, + 798, + 839, + 786, + 433, + 610, + 116, + 855, + 180, + 479, + 910, + 1014, + 599, + 915, + 905, + 306, + 516, + 731, + 626, + 978, + 825, + 344, + 605, + 654, + 209, + }; + + static readonly int[] GF1024_LOG = new[] + { + -1, + 0, + 99, + 363, + 198, + 726, + 462, + 132, + 297, + 495, + 825, + 528, + 561, + 693, + 231, + 66, + 396, + 429, + 594, + 990, + 924, + 264, + 627, + 33, + 660, + 759, + 792, + 858, + 330, + 891, + 165, + 957, + 104, + 259, + 518, + 208, + 280, + 776, + 416, + 13, + 426, + 333, + 618, + 339, + 641, + 52, + 388, + 140, + 666, + 852, + 529, + 560, + 678, + 213, + 26, + 832, + 681, + 309, + 70, + 194, + 97, + 35, + 682, + 341, + 203, + 777, + 358, + 312, + 617, + 125, + 307, + 931, + 379, + 765, + 875, + 951, + 515, + 628, + 112, + 659, + 525, + 196, + 432, + 134, + 717, + 781, + 438, + 440, + 740, + 780, + 151, + 408, + 487, + 169, + 239, + 293, + 467, + 21, + 672, + 622, + 557, + 571, + 881, + 433, + 704, + 376, + 779, + 22, + 643, + 460, + 398, + 116, + 172, + 503, + 751, + 389, + 1004, + 18, + 576, + 415, + 789, + 6, + 192, + 696, + 923, + 702, + 981, + 892, + 302, + 816, + 876, + 880, + 457, + 537, + 411, + 539, + 716, + 624, + 224, + 295, + 406, + 531, + 7, + 233, + 478, + 586, + 864, + 268, + 974, + 338, + 27, + 392, + 614, + 839, + 727, + 879, + 211, + 250, + 758, + 507, + 830, + 129, + 369, + 384, + 36, + 985, + 12, + 555, + 232, + 796, + 221, + 321, + 920, + 263, + 42, + 934, + 778, + 479, + 761, + 939, + 1006, + 344, + 381, + 823, + 44, + 535, + 866, + 739, + 752, + 385, + 119, + 91, + 566, + 80, + 120, + 117, + 771, + 675, + 721, + 514, + 656, + 271, + 670, + 602, + 980, + 850, + 532, + 488, + 803, + 1022, + 475, + 801, + 878, + 57, + 121, + 991, + 742, + 888, + 559, + 105, + 497, + 291, + 215, + 795, + 236, + 167, + 692, + 520, + 272, + 661, + 229, + 391, + 814, + 340, + 184, + 798, + 984, + 773, + 650, + 473, + 345, + 558, + 548, + 326, + 202, + 145, + 465, + 810, + 471, + 158, + 813, + 908, + 412, + 441, + 964, + 750, + 401, + 50, + 915, + 437, + 975, + 126, + 979, + 491, + 556, + 577, + 636, + 685, + 510, + 963, + 638, + 367, + 815, + 310, + 723, + 349, + 323, + 857, + 394, + 606, + 505, + 713, + 630, + 938, + 106, + 826, + 332, + 978, + 599, + 834, + 521, + 530, + 248, + 883, + 32, + 153, + 90, + 754, + 592, + 304, + 635, + 775, + 804, + 1, + 150, + 836, + 1013, + 828, + 324, + 565, + 508, + 113, + 154, + 708, + 921, + 703, + 689, + 138, + 547, + 911, + 929, + 82, + 228, + 443, + 468, + 480, + 483, + 922, + 135, + 877, + 61, + 578, + 111, + 860, + 654, + 15, + 331, + 851, + 895, + 484, + 320, + 218, + 420, + 190, + 1019, + 143, + 362, + 634, + 141, + 965, + 10, + 838, + 632, + 861, + 34, + 722, + 580, + 808, + 869, + 554, + 598, + 65, + 954, + 787, + 337, + 187, + 281, + 146, + 563, + 183, + 668, + 944, + 171, + 837, + 23, + 867, + 541, + 916, + 741, + 625, + 123, + 736, + 186, + 357, + 665, + 977, + 179, + 156, + 219, + 220, + 216, + 67, + 870, + 902, + 774, + 98, + 820, + 574, + 613, + 900, + 755, + 596, + 370, + 390, + 769, + 314, + 701, + 894, + 56, + 841, + 949, + 987, + 631, + 658, + 587, + 204, + 797, + 790, + 522, + 745, + 9, + 502, + 763, + 86, + 719, + 288, + 706, + 887, + 728, + 952, + 311, + 336, + 446, + 1002, + 348, + 96, + 58, + 199, + 11, + 901, + 230, + 833, + 188, + 352, + 351, + 973, + 3, + 906, + 335, + 301, + 266, + 244, + 791, + 564, + 619, + 909, + 371, + 444, + 760, + 657, + 328, + 647, + 490, + 425, + 913, + 511, + 439, + 540, + 283, + 40, + 897, + 849, + 60, + 570, + 872, + 257, + 749, + 912, + 572, + 1007, + 170, + 407, + 898, + 492, + 79, + 747, + 732, + 206, + 454, + 918, + 375, + 482, + 399, + 92, + 748, + 325, + 163, + 274, + 405, + 744, + 260, + 346, + 707, + 626, + 595, + 118, + 842, + 136, + 279, + 684, + 584, + 101, + 500, + 422, + 149, + 956, + 1014, + 493, + 536, + 705, + 51, + 914, + 225, + 409, + 55, + 822, + 590, + 448, + 655, + 205, + 676, + 925, + 735, + 431, + 784, + 54, + 609, + 604, + 39, + 812, + 737, + 729, + 466, + 14, + 533, + 958, + 481, + 770, + 499, + 855, + 238, + 182, + 464, + 569, + 72, + 947, + 442, + 642, + 24, + 87, + 989, + 688, + 88, + 47, + 762, + 623, + 709, + 455, + 817, + 526, + 637, + 258, + 84, + 845, + 738, + 768, + 698, + 423, + 933, + 664, + 620, + 607, + 629, + 212, + 347, + 249, + 982, + 935, + 131, + 89, + 252, + 927, + 189, + 788, + 853, + 237, + 691, + 646, + 403, + 1010, + 734, + 253, + 874, + 807, + 903, + 1020, + 100, + 802, + 71, + 799, + 1003, + 633, + 355, + 276, + 300, + 649, + 64, + 306, + 161, + 608, + 496, + 743, + 180, + 485, + 819, + 383, + 1016, + 226, + 308, + 393, + 648, + 107, + 19, + 37, + 585, + 2, + 175, + 645, + 247, + 527, + 5, + 419, + 181, + 317, + 327, + 519, + 542, + 289, + 567, + 430, + 579, + 950, + 582, + 994, + 1021, + 583, + 234, + 240, + 976, + 41, + 160, + 109, + 677, + 937, + 210, + 95, + 959, + 242, + 753, + 461, + 114, + 733, + 368, + 573, + 458, + 782, + 605, + 680, + 544, + 299, + 73, + 652, + 905, + 477, + 690, + 93, + 824, + 882, + 277, + 946, + 361, + 17, + 945, + 523, + 472, + 334, + 930, + 597, + 603, + 793, + 404, + 290, + 942, + 316, + 731, + 270, + 960, + 936, + 133, + 122, + 821, + 966, + 679, + 662, + 907, + 282, + 968, + 767, + 653, + 20, + 697, + 222, + 164, + 835, + 30, + 285, + 886, + 456, + 436, + 640, + 286, + 1015, + 380, + 840, + 245, + 724, + 137, + 593, + 173, + 130, + 715, + 85, + 885, + 551, + 246, + 449, + 103, + 366, + 372, + 714, + 313, + 865, + 241, + 699, + 674, + 374, + 68, + 421, + 562, + 292, + 59, + 809, + 342, + 651, + 459, + 227, + 46, + 711, + 764, + 868, + 53, + 413, + 278, + 800, + 255, + 993, + 318, + 854, + 319, + 695, + 315, + 469, + 166, + 489, + 969, + 730, + 1001, + 757, + 873, + 686, + 197, + 303, + 919, + 155, + 673, + 940, + 712, + 25, + 999, + 63, + 863, + 972, + 967, + 785, + 152, + 296, + 512, + 402, + 377, + 45, + 899, + 829, + 354, + 77, + 69, + 856, + 417, + 811, + 953, + 124, + 418, + 75, + 794, + 162, + 414, + 1018, + 568, + 254, + 265, + 772, + 588, + 16, + 896, + 157, + 889, + 298, + 621, + 110, + 844, + 1000, + 108, + 545, + 601, + 78, + 862, + 447, + 185, + 195, + 818, + 450, + 387, + 49, + 805, + 102, + 986, + 1005, + 827, + 329, + 28, + 932, + 410, + 287, + 435, + 451, + 962, + 517, + 48, + 174, + 43, + 893, + 884, + 261, + 251, + 516, + 395, + 910, + 611, + 29, + 501, + 223, + 476, + 364, + 144, + 871, + 998, + 687, + 928, + 115, + 453, + 513, + 176, + 94, + 168, + 667, + 955, + 353, + 434, + 382, + 400, + 139, + 365, + 996, + 343, + 948, + 890, + 1012, + 663, + 610, + 718, + 538, + 1008, + 639, + 470, + 848, + 543, + 1011, + 859, + 671, + 756, + 83, + 427, + 159, + 746, + 669, + 589, + 971, + 524, + 356, + 995, + 904, + 256, + 201, + 988, + 62, + 397, + 81, + 720, + 917, + 209, + 549, + 943, + 486, + 76, + 148, + 207, + 509, + 644, + 386, + 700, + 534, + 177, + 550, + 961, + 926, + 546, + 428, + 284, + 127, + 294, + 8, + 269, + 359, + 506, + 445, + 997, + 806, + 591, + 725, + 178, + 262, + 846, + 373, + 831, + 504, + 305, + 843, + 553, + 378, + 1017, + 783, + 474, + 683, + 581, + 200, + 498, + 694, + 191, + 217, + 847, + 941, + 424, + 235, + 38, + 74, + 616, + 786, + 147, + 4, + 273, + 214, + 142, + 575, + 992, + 463, + 983, + 243, + 360, + 970, + 350, + 267, + 615, + 766, + 494, + 31, + 1009, + 452, + 710, + 552, + 128, + 612, + 600, + 275, + 322, + 193, + }; + + protected static readonly byte[] Byteset; + + static Bech32Encoder() + { + Byteset = Encoders.ASCII.DecodeData("qpzry9x8gf2tvdw0s3jn54khce6mua7l"); + } + + private static readonly uint[] Generator = + { + 0x3b6a57b2U, + 0x26508e6dU, + 0x1ea119faU, + 0x3d4233ddU, + 0x2a1462b3U, + }; + + uint syndrome(uint residue) + { + var low = residue & 0x1f; + return (uint)( + low + ^ (low << 10) + ^ (low << 20) + ^ (((residue >> 5) & 1) != 0 ? 0x31edd3c4 : 0) + ^ (((residue >> 6) & 1) != 0 ? 0x335f86a8 : 0) + ^ (((residue >> 7) & 1) != 0 ? 0x363b8870 : 0) + ^ (((residue >> 8) & 1) != 0 ? 0x3e6390c9 : 0) + ^ (((residue >> 9) & 1) != 0 ? 0x2ec72192 : 0) + ^ (((residue >> 10) & 1) != 0 ? 0x1046f79d : 0) + ^ (((residue >> 11) & 1) != 0 ? 0x208d4e33 : 0) + ^ (((residue >> 12) & 1) != 0 ? 0x130ebd6f : 0) + ^ (((residue >> 13) & 1) != 0 ? 0x2499fade : 0) + ^ (((residue >> 14) & 1) != 0 ? 0x1b27d4b5 : 0) + ^ (((residue >> 15) & 1) != 0 ? 0x04be1eb4 : 0) + ^ (((residue >> 16) & 1) != 0 ? 0x0968b861 : 0) + ^ (((residue >> 17) & 1) != 0 ? 0x1055f0c2 : 0) + ^ (((residue >> 18) & 1) != 0 ? 0x20ab4584 : 0) + ^ (((residue >> 19) & 1) != 0 ? 0x1342af08 : 0) + ^ (((residue >> 20) & 1) != 0 ? 0x24f1f318 : 0) + ^ (((residue >> 21) & 1) != 0 ? 0x1be34739 : 0) + ^ (((residue >> 22) & 1) != 0 ? 0x35562f7b : 0) + ^ (((residue >> 23) & 1) != 0 ? 0x3a3c5bff : 0) + ^ (((residue >> 24) & 1) != 0 ? 0x266c96f7 : 0) + ^ (((residue >> 25) & 1) != 0 ? 0x25c78b65 : 0) + ^ (((residue >> 26) & 1) != 0 ? 0x1b1f13ea : 0) + ^ (((residue >> 27) & 1) != 0 ? 0x34baa2f4 : 0) + ^ (((residue >> 28) & 1) != 0 ? 0x3b61c0e1 : 0) + ^ (((residue >> 29) & 1) != 0 ? 0x265325c2 : 0) + ); + } + + int[] locate_errors(uint residue, int length) + { + if (residue == 0) + { + return new int[0]; + } + var syn = syndrome(residue); + var s0 = syn & 0x3FF; + var s1 = (syn >> 10) & 0x3FF; + var s2 = syn >> 20; + var l_s0 = GF1024_LOG[s0]; + var l_s1 = GF1024_LOG[s1]; + var l_s2 = GF1024_LOG[s2]; + if ( + l_s0 != -1 + && l_s1 != -1 + && l_s2 != -1 + && (2 * l_s1 - l_s2 - l_s0 + 2046) % 1023 == 0 + ) + { + var p1 = (l_s1 - l_s0 + 1023) % 1023; + if (p1 >= length) + return new int[0]; + var l_e1 = l_s0 + (1023 - 997) * p1; + if ((l_e1 % 33) != 0) + return new int[0]; + return new[] { p1 }; + } + for (var p1 = 0; p1 < length; p1++) + { + var s2_s1p1 = s2 ^ (s1 == 0 ? 0 : GF1024_EXP[(l_s1 + p1) % 1023]); + if (s2_s1p1 == 0) + continue; + var s1_s0p1 = s1 ^ (s0 == 0 ? 0 : GF1024_EXP[(l_s0 + p1) % 1023]); + if (s1_s0p1 == 0) + continue; + var l_s1_s0p1 = GF1024_LOG[s1_s0p1]; + var p2 = (GF1024_LOG[s2_s1p1] - l_s1_s0p1 + 1023) % 1023; + if (p2 >= length || p1 == p2) + continue; + var s1_s0p2 = s1 ^ (s0 == 0 ? 0 : GF1024_EXP[(l_s0 + p2) % 1023]); + if (s1_s0p2 == 0) + continue; + var inv_p1_p2 = 1023 - GF1024_LOG[GF1024_EXP[p1] ^ GF1024_EXP[p2]]; + var l_e2 = l_s1_s0p1 + inv_p1_p2 + (1023 - 997) * p2; + if ((l_e2 % 33) != 0) + continue; + var l_e1 = GF1024_LOG[s1_s0p2] + inv_p1_p2 + (1023 - 997) * p1; + if ((l_e1 % 33) != 0) + continue; + if (p1 < p2) + { + return new int[] { p1, p2 }; + } + else + { + return new int[] { p2, p1 }; + } + } + return new int[0]; + } + + internal Bech32Encoder(string hrp) + : this(hrp == null ? null : Encoders.ASCII.DecodeData(hrp.ToLowerInvariant())) { } + + public Bech32Encoder(byte[] hrp) + { + if (hrp == null) + throw new ArgumentNullException(nameof(hrp)); + + _Hrp = hrp; + var len = hrp.Length; + _HrpExpand = new byte[(2 * len) + 1]; + for (int i = 0; i < len; i++) + { + _HrpExpand[i] = (byte)(hrp[i] >> 5); + _HrpExpand[i + len + 1] = (byte)(hrp[i] & 31); + } + } + + protected readonly byte[] _HrpExpand; + protected readonly byte[] _Hrp; + public byte[] HumanReadablePart + { + get { return _Hrp; } + } + + private static uint Polymod(ReadOnlySpan values) + { + uint chk = 1; + for (int i = 0; i < values.Length; i++) + { + var top = chk >> 25; + chk = values[i] ^ ((chk & 0x1ffffff) << 5); + chk ^= ((top >> 0) & 1) == 1 ? Generator[0] : 0; + chk ^= ((top >> 1) & 1) == 1 ? Generator[1] : 0; + chk ^= ((top >> 2) & 1) == 1 ? Generator[2] : 0; + chk ^= ((top >> 3) & 1) == 1 ? Generator[3] : 0; + chk ^= ((top >> 4) & 1) == 1 ? Generator[4] : 0; + } + return chk; + } + + protected virtual bool VerifyChecksum( + byte[] data, + int bechStringLen, + out Bech32EncodingType encodingType, + out int[] errorPosition + ) + { + return VerifyChecksum( + data.AsSpan(), + bechStringLen, + out encodingType, + out errorPosition + ); + } + + protected virtual bool VerifyChecksum( + ReadOnlySpan data, + int bechStringLen, + out Bech32EncodingType encodingType, + out int[] errorPosition + ) + { + errorPosition = null; + Span values = + _HrpExpand.Length + data.Length is int v && v > 256 + ? new byte[v] + : stackalloc byte[v]; + _HrpExpand.CopyTo(values); + data.CopyTo(values.Slice(_HrpExpand.Length)); + var polymod = Polymod(values); + if (polymod == Bech32EncodingType.BECH32.EncodingConstant) + encodingType = Bech32EncodingType.BECH32; + else if (polymod == Bech32EncodingType.BECH32M.EncodingConstant) + encodingType = Bech32EncodingType.BECH32M; + else + { + encodingType = null; + + var epos = Bech32EncodingType + .All.Select(e => + locate_errors(polymod ^ (uint)e.EncodingConstant, bechStringLen - 1) + ) + .Where(e => e.Length != 0) + .OrderByDescending(e => e.Length) + .FirstOrDefault(); + errorPosition = epos; + if (epos is null || epos.Length == 0) + return false; + for (var ep = 0; ep < epos.Length; ++ep) + { + epos[ep] = bechStringLen - epos[ep] - (epos[ep] >= data.Length ? 2 : 1); + } + return false; + } + return true; + } + + private void CreateChecksum( + ReadOnlySpan data, + Bech32EncodingType encodingType, + Span ret + ) + { + Span values = + _HrpExpand.Length + data.Length + 6 is int v && v > 256 + ? new byte[v] + : stackalloc byte[v]; + var valuesOffset = 0; + _HrpExpand.AsSpan().CopyTo(values.Slice(valuesOffset)); + valuesOffset += _HrpExpand.Length; + data.CopyTo(values.Slice(valuesOffset)); + valuesOffset += data.Length; + var polymod = Polymod(values) ^ encodingType.EncodingConstant; + foreach (var i in Enumerable.Range(0, 6)) + { + ret[i] = (byte)((polymod >> 5 * (5 - i)) & 31); + } + } + + public virtual string EncodeData(ReadOnlySpan data, Bech32EncodingType encodingType) + { + if (encodingType == null) + throw new ArgumentNullException(nameof(encodingType)); + if (SquashBytes) + data = ByteSquasher(data, 8, 5).AsSpan(); + + Span combined = + _Hrp.Length + 1 + data.Length + 6 is int v && v > 256 + ? new byte[v] + : stackalloc byte[v]; + int combinedOffset = 0; + _Hrp.CopyTo(combined); + combinedOffset += _Hrp.Length; + combined[combinedOffset] = 49; + combinedOffset++; + + data.CopyTo(combined.Slice(combinedOffset)); + combinedOffset += data.Length; + Span checkSum = stackalloc byte[6]; + CreateChecksum(data, encodingType, checkSum); + + checkSum.CopyTo(combined.Slice(combinedOffset, 6)); + combinedOffset += 6; + + for (int i = 0; i < data.Length + 6; i++) + { + combined[_Hrp.Length + 1 + i] = Byteset[combined[_Hrp.Length + 1 + i]]; + } + return Encoders.ASCII.EncodeData(combined.ToArray()); + } + + public static Bech32Encoder ExtractEncoderFromString(string test) + { + var i = test.LastIndexOf('1'); + if (i == -1) + throw new FormatException("Invalid Bech32 string"); + + return Encoders.Bech32(test.Substring(0, i)); + } + + protected virtual void CheckCase(string hrp) + { + if (hrp.Length is 0) + return; + bool isLowercase = char.IsUpper(hrp[0]); + for (int i = 1; i < hrp.Length; i++) + { + if (isLowercase != char.IsUpper(hrp[i]) && !char.IsDigit(hrp[i])) + throw new FormatException("Invalid bech32 string, mixed case detected"); + } + } + + public byte[] DecodeDataRaw(string encoded, out Bech32EncodingType encodingType) + { + return DecodeDataCore(encoded, out encodingType); + } + + public bool StrictLength { get; set; } = true; + public bool SquashBytes { get; set; } = false; + + protected virtual byte[] DecodeDataCore(string encoded, out Bech32EncodingType encodingType) + { + if (encoded == null) + throw new ArgumentNullException(nameof(encoded)); + CheckCase(encoded); + encoded = encoded.ToLowerInvariant(); + Span buffer = + encoded.Length is int v && v > 256 ? new byte[v] : stackalloc byte[v]; + ((ASCIIEncoder)Encoders.ASCII).DecodeData(encoded, buffer); + var pos = encoded.LastIndexOf("1", StringComparison.OrdinalIgnoreCase); + if (pos < 1) + { + throw new FormatException("The Bech32 string is missing separator '1'"); + } + else if (pos + 7 > encoded.Length) + { + throw new FormatException("The Bech32 string is too short"); + } + else if (StrictLength && encoded.Length > 90) + { + throw new FormatException("The Bech32 string is too long"); + } + if (pos != _Hrp.Length) + { + throw new FormatException("Mismatching human readable part"); + } + + for (int i = 0; i < _Hrp.Length; i++) + { + if (buffer[i] != _Hrp[i]) + throw new FormatException("Mismatching human readable part"); + } + Span data = + encoded.Length - pos - 1 is int v2 && v2 > 256 ? new byte[v2] : stackalloc byte[v2]; + for (int j = 0, i = pos + 1; i < encoded.Length; i++, j++) + { + int index = Array.IndexOf(Byteset, buffer[i]); + if (index == -1) + throw new FormatException("bech chars are out of range"); + data[j] = (byte)index; + } + + int[] error; + if (!VerifyChecksum(data, encoded.Length, out encodingType, out error)) + { + if (error == null || error.Length == 0) + throw new FormatException("Error while verifying Bech32 checksum"); + else + throw new Bech32FormatException( + $"Error in Bech32 string at {String.Join(",", error)}", + error + ); + } + var arr = data.Slice(0, data.Length - 6).ToArray(); + if (SquashBytes) + { + arr = ByteSquasher(arr, 5, 8); + if (arr is null) + throw new FormatException("Invalid squashed bech32"); + } + return arr; + } + + private static byte[] ByteSquasher( + ReadOnlySpan input, + int inputWidth, + int outputWidth + ) + { + var bitstash = 0; + var accumulator = 0; + var output = new List(); + var maxOutputValue = (1 << outputWidth) - 1; + for (var i = 0; i < input.Length; i++) + { + var c = input[i]; + if (c >> inputWidth != 0) + { + return null; + } + + accumulator = (accumulator << inputWidth) | c; + bitstash += inputWidth; + while (bitstash >= outputWidth) + { + bitstash -= outputWidth; + output.Add((byte)((accumulator >> bitstash) & maxOutputValue)); + } + } + + // pad if going from 8 to 5 + if (inputWidth == 8 && outputWidth == 5) + { + if (bitstash != 0) + output.Add((byte)((accumulator << (outputWidth - bitstash)) & maxOutputValue)); + } + else if ( + bitstash >= inputWidth + || ((accumulator << (outputWidth - bitstash)) & maxOutputValue) != 0 + ) + { + // no pad from 5 to 8 allowed + return null; + } + + return output.ToArray(); + } + + protected virtual byte[] ConvertBits( + IEnumerable data, + int fromBits, + int toBits, + bool pad = true + ) + { + return ConvertBits(data.ToArray().AsSpan(), fromBits, toBits, pad); + } + + protected virtual byte[] ConvertBits( + ReadOnlySpan data, + int fromBits, + int toBits, + bool pad = true + ) + { + var acc = 0; + var bits = 0; + var maxv = (1 << toBits) - 1; + var ret = new List(64); + foreach (var value in data) + { + if ((value >> fromBits) > 0) + throw new FormatException("Invalid Bech32 string"); + acc = (acc << fromBits) | value; + bits += fromBits; + while (bits >= toBits) + { + bits -= toBits; + ret.Add((byte)((acc >> bits) & maxv)); + } + } + if (pad) + { + if (bits > 0) + { + ret.Add((byte)((acc << (toBits - bits)) & maxv)); + } + } + else if (bits >= fromBits || (byte)(((acc << (toBits - bits)) & maxv)) != 0) + { + throw new FormatException("Invalid Bech32 string"); + } + return ret.ToArray(); + } + + public virtual byte[] Decode(string addr, out byte witnessVerion) + { + if (addr == null) + throw new ArgumentNullException(nameof(addr)); + CheckCase(addr); + var data = DecodeDataCore(addr, out var encodingType); + var decoded = ConvertBits(data.AsSpan().Slice(1), 5, 8, false); + if (decoded.Length < 2 || decoded.Length > 40) + throw new FormatException("Invalid decoded data length"); + witnessVerion = data[0]; + if (witnessVerion == 0 && encodingType != Bech32EncodingType.BECH32) + throw new FormatException("Decoded data should have used BECH32 encoding"); + if (witnessVerion != 0 && encodingType != Bech32EncodingType.BECH32M) + throw new FormatException("Decoded data should have used BECH32M encoding"); + if (witnessVerion > 16) + throw new FormatException("Invalid decoded witness version"); + + if (witnessVerion == 0 && decoded.Length != 20 && decoded.Length != 32) + throw new FormatException("Decoded witness program with unknown length"); + return decoded; + } + + public string EncodeRaw(byte[] data, Bech32EncodingType encodingType) + { + return EncodeData(data.AsSpan(), encodingType); + } + + public string EncodeRaw(ReadOnlySpan data, Bech32EncodingType encodingType) + { + return EncodeData(data, encodingType); + } + + public string Encode(byte witnessVerion, byte[] witnessProgramm) + { + if (witnessProgramm == null) + throw new ArgumentNullException(nameof(witnessProgramm)); + return Encode(witnessVerion, witnessProgramm.AsSpan()); + } + + public string Encode(byte witnessVerion, ReadOnlySpan witnessProgramm) + { + if (witnessVerion > 16) + throw new ArgumentOutOfRangeException( + nameof(witnessVerion), + "Invalid decoded witnessVerion, should <= 0 and > 16" + ); + var bits = ConvertBits(witnessProgramm, 8, 5); + Span data = + 1 + bits.Length is int v && v > 256 ? new byte[v] : stackalloc byte[v]; + data[0] = witnessVerion; + bits.AsSpan().CopyTo(data.Slice(1)); + var ret = EncodeData( + data, + witnessVerion == 0 ? Bech32EncodingType.BECH32 : Bech32EncodingType.BECH32M + ); + return ret; + } + } } diff --git a/DotNut/NBitcoin/BitWriter.cs b/DotNut/NBitcoin/BitWriter.cs index c37fefd..2f6196e 100644 --- a/DotNut/NBitcoin/BitWriter.cs +++ b/DotNut/NBitcoin/BitWriter.cs @@ -3,96 +3,93 @@ namespace DotNut.NBitcoin { + class BitWriter + { + List values = new List(); - class BitWriter - { - List values = new List(); - public void Write(bool value) - { - values.Insert(Position, value); - _Position++; - } + public void Write(bool value) + { + values.Insert(Position, value); + _Position++; + } - internal void Write(byte[] bytes) - { - Write(bytes, bytes.Length * 8); - } + internal void Write(byte[] bytes) + { + Write(bytes, bytes.Length * 8); + } - public void Write(byte[] bytes, int bitCount) - { - bytes = SwapEndianBytes(bytes); - BitArray array = new BitArray(bytes); - values.InsertRange(Position, array.OfType().Take(bitCount)); - _Position += bitCount; - } + public void Write(byte[] bytes, int bitCount) + { + bytes = SwapEndianBytes(bytes); + BitArray array = new BitArray(bytes); + values.InsertRange(Position, array.OfType().Take(bitCount)); + _Position += bitCount; + } - public byte[] ToBytes() - { - var array = ToBitArray(); - var bytes = ToByteArray(array); - bytes = SwapEndianBytes(bytes); - return bytes; - } + public byte[] ToBytes() + { + var array = ToBitArray(); + var bytes = ToByteArray(array); + bytes = SwapEndianBytes(bytes); + return bytes; + } - //BitArray.CopyTo do not exist in portable lib - static byte[] ToByteArray(BitArray bits) - { - int arrayLength = bits.Length / 8; - if (bits.Length % 8 != 0) - arrayLength++; - byte[] array = new byte[arrayLength]; + //BitArray.CopyTo do not exist in portable lib + static byte[] ToByteArray(BitArray bits) + { + int arrayLength = bits.Length / 8; + if (bits.Length % 8 != 0) + arrayLength++; + byte[] array = new byte[arrayLength]; - for (int i = 0; i < bits.Length; i++) - { - int b = i / 8; - int offset = i % 8; - array[b] |= bits.Get(i) ? (byte)(1 << offset) : (byte)0; - } - return array; - } + for (int i = 0; i < bits.Length; i++) + { + int b = i / 8; + int offset = i % 8; + array[b] |= bits.Get(i) ? (byte)(1 << offset) : (byte)0; + } + return array; + } + public BitArray ToBitArray() + { + return new BitArray(values.ToArray()); + } - public BitArray ToBitArray() - { - return new BitArray(values.ToArray()); - } + public int[] ToIntegers() + { + var array = new BitArray(values.ToArray()); + return Wordlist.ToIntegers(array); + } - public int[] ToIntegers() - { - var array = new BitArray(values.ToArray()); - return Wordlist.ToIntegers(array); - } + static byte[] SwapEndianBytes(byte[] bytes) + { + byte[] output = new byte[bytes.Length]; + for (int i = 0; i < output.Length; i++) + { + byte newByte = 0; + for (int ib = 0; ib < 8; ib++) + { + newByte += (byte)(((bytes[i] >> ib) & 1) << (7 - ib)); + } + output[i] = newByte; + } + return output; + } + int _Position; + public int Position + { + get => _Position; + set => _Position = value; + } - static byte[] SwapEndianBytes(byte[] bytes) - { - byte[] output = new byte[bytes.Length]; - for (int i = 0; i < output.Length; i++) - { - byte newByte = 0; - for (int ib = 0; ib < 8; ib++) - { - newByte += (byte)(((bytes[i] >> ib) & 1) << (7 - ib)); - } - output[i] = newByte; - } - return output; - } - - - int _Position; - public int Position - { - get => _Position; - set => _Position = value; - } - public void Write(BitArray bitArray, int bitCount) - { - for (int i = 0; i < bitCount; i++) - { - Write(bitArray.Get(i)); - } - } - } - + public void Write(BitArray bitArray, int bitCount) + { + for (int i = 0; i < bitCount; i++) + { + Write(bitArray.Get(i)); + } + } + } } diff --git a/DotNut/BlindSignature.cs b/DotNut/NUT00/BlindSignature.cs similarity index 73% rename from DotNut/BlindSignature.cs rename to DotNut/NUT00/BlindSignature.cs index 8f99401..d373b79 100644 --- a/DotNut/BlindSignature.cs +++ b/DotNut/NUT00/BlindSignature.cs @@ -5,16 +5,17 @@ namespace DotNut; public class BlindSignature { - [JsonPropertyName("amount")] public ulong Amount { get; set; } + [JsonPropertyName("amount")] + public ulong Amount { get; set; } [JsonConverter(typeof(KeysetIdJsonConverter))] [JsonPropertyName("id")] public KeysetId Id { get; set; } - [JsonPropertyName("C_")] public PubKey C_ { get; set; } - - + [JsonPropertyName("C_")] + public PubKey C_ { get; set; } + [JsonPropertyName("dleq")] [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public DLEQProof? DLEQ { get; set; } -} \ No newline at end of file +} diff --git a/DotNut/NUT00/BlindedMessage.cs b/DotNut/NUT00/BlindedMessage.cs new file mode 100644 index 0000000..8f066a7 --- /dev/null +++ b/DotNut/NUT00/BlindedMessage.cs @@ -0,0 +1,19 @@ +using System.Text.Json.Serialization; + +namespace DotNut; + +public class BlindedMessage +{ + [JsonPropertyName("amount")] + public ulong Amount { get; set; } + + [JsonPropertyName("id")] + public KeysetId Id { get; set; } + + [JsonPropertyName("B_")] + public PubKey B_ { get; set; } + + [JsonPropertyName("witness")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public string? Witness { get; set; } +} diff --git a/DotNut/Cashu.cs b/DotNut/NUT00/Cashu.cs similarity index 54% rename from DotNut/Cashu.cs rename to DotNut/NUT00/Cashu.cs index b6800ca..fc781a1 100644 --- a/DotNut/Cashu.cs +++ b/DotNut/NUT00/Cashu.cs @@ -10,24 +10,27 @@ public static class Cashu private static readonly byte[] DOMAIN_SEPARATOR = "Secp256k1_HashToCurve_Cashu_"u8.ToArray(); private static readonly byte[] P2BK_PREFIX = "Cashu_P2BK_v1"u8.ToArray(); - - internal static readonly BigInteger N = - BigInteger.Parse("115792089237316195423570985008687907852837564279074904382605163141518161494337"); + + internal static readonly BigInteger N = BigInteger.Parse( + "115792089237316195423570985008687907852837564279074904382605163141518161494337" + ); + public static ECPubKey MessageToCurve(string message) { var hash = Encoding.UTF8.GetBytes(message); return HashToCurve(hash); - } + } public static ECPubKey HexToCurve(string hex) { var bytes = Convert.FromHexString(hex); return HashToCurve(bytes); } + public static ECPubKey HashToCurve(byte[] x) { var msgHash = SHA256.HashData(Concat(DOMAIN_SEPARATOR, x)); - for (uint counter = 0;; counter++) + for (uint counter = 0; ; counter++) { var counterBytes = BitConverter.GetBytes(counter); var publicKeyBytes = Concat([0x02], SHA256.HashData(Concat(msgHash, counterBytes))); @@ -35,52 +38,57 @@ public static ECPubKey HashToCurve(byte[] x) { return ECPubKey.Create(publicKeyBytes); } - catch (FormatException) - { - } + catch (FormatException) { } } } - public static GE ToGE(this Scalar scalar) - { - // Multiply the scalar by the generator point to get the group element - GEJ gej = Context.Instance.EcMultGenContext.MultGen(scalar); - return gej.ToGroupElement(); - } - - public static ECPubKey ToPubkey(this Scalar scalar) - { - return new ECPubKey(scalar.ToGE(), Context.Instance); - } - - public static ECPrivKey ToPrivateKey(this Scalar scalar) - { - return ECPrivKey.TryCreate(scalar, out var key) ? key : throw new InvalidOperationException(); - } - - public static ECPubKey ToPubkey(this GEJ gej) - { - return new ECPubKey(gej.ToGroupElement(), Context.Instance); - } - - public static ECPubKey ToPubkey(this GE ge) - { - return new ECPubKey(ge, Context.Instance); - } - + /// + /// Blinding + /// + /// hash_to_curve of the secret + /// Blinding factor + /// Blinded Y (Blinded message) public static ECPubKey ComputeB_(ECPubKey Y, ECPrivKey r) { //B_ = Y + rG return Y.Q.ToGroupElementJacobian().Add(r.CreatePubKey().Q).ToPubkey(); } + /// + /// Signing blinded message + /// + /// B_ blinded message + /// private key of mint (one for each amount) + /// Blind signature (on B_) public static ECPubKey ComputeC_(ECPubKey B_, ECPrivKey k) { //C_ = kB_ return (B_.Q * k.sec).ToPubkey(); } - + /// + /// Unblinding + /// + /// Blind signature + /// Blinding factor + /// Amount Pubkey + /// Unblinded Signature + public static ECPubKey ComputeC(ECPubKey C_, ECPrivKey r, ECPubKey A) + { + //C_ - rA = C + return C_ + .Q.ToGroupElementJacobian() + .Add((A.Q * r.sec).ToGroupElement().Negate()) + .ToPubkey(); + } + + /// + /// Creates DLEQ Proof. + /// + /// Blinded message + /// Privkey for given amount + /// Blinding factor + /// Tuple (e, s) representing the DLEQ proof public static (ECPrivKey e, ECPrivKey s) ComputeProof(ECPubKey B_, ECPrivKey a, ECPrivKey p) { //C_ - rK = kY + krG - krG = kY = C @@ -93,44 +101,133 @@ public static (ECPrivKey e, ECPrivKey s) ComputeProof(ECPubKey B_, ECPrivKey a, var s = p.TweakAdd(a.TweakMul(e.ToBytes()).ToBytes()); return (e.ToPrivateKey(), s); } - + + /// + /// Computes the challenge scalar 'e' for the DLEQ proof. + /// + /// Commitment point r*G + /// + /// + /// + /// The challenge scalar e derived as a SHA256 hash over the concatenation of the uncompressed points. public static Scalar ComputeE(ECPubKey R1, ECPubKey R2, ECPubKey K, ECPubKey C_) { - byte[] eBytes = Encoding.UTF8.GetBytes(string.Concat(new[] {R1, R2, K, C_}.Select(pk => pk.ToHex(false)))); + byte[] eBytes = Encoding.UTF8.GetBytes( + string.Concat(new[] { R1, R2, K, C_ }.Select(pk => pk.ToHex(false))) + ); return new Scalar(SHA256.HashData(eBytes)); } + /// + /// Verify DLEQ proof of Cashu proof. + /// + /// Cashu Proof + /// Amount Pubkey + /// public static bool Verify(this Proof proof, ECPubKey A) { - return VerifyProof(proof.Secret.ToCurve(),proof.DLEQ.R, proof.C, proof.DLEQ.E, proof.DLEQ.S, A); + return VerifyProof( + proof.Secret.ToCurve(), + proof.DLEQ.R, + proof.C, + proof.DLEQ.E, + proof.DLEQ.S, + A + ); } + + /// + /// Verify DLEQ proof of Blinded signature + /// + /// Blind Signature + /// Amount Pubkey + /// Blinded Message + /// public static bool Verify(this BlindSignature blindSig, ECPubKey A, ECPubKey B_) { - return Cashu.VerifyProof(B_, blindSig.C_, blindSig.DLEQ.E, blindSig.DLEQ.S, A); + return Cashu.VerifyProof(B_, blindSig.C_, blindSig.DLEQ.E, blindSig.DLEQ.S, A); } - + + /// + /// Verify DLEQ proof + /// + /// Blinded Message + /// Blinded Signature + /// Dleq.E returned by mint + /// Dleq.S returned by mint + /// Amount pubkey + /// public static bool VerifyProof(ECPubKey B_, ECPubKey C_, ECPrivKey e, ECPrivKey s, ECPubKey A) { - - var r1 = s.CreatePubKey().Q.ToGroupElementJacobian().Add((A.Q * e.sec.Negate()).ToGroupElement()).ToPubkey(); + var r1 = s.CreatePubKey() + .Q.ToGroupElementJacobian() + .Add((A.Q * e.sec.Negate()).ToGroupElement()) + .ToPubkey(); var r2 = (B_.Q * s.sec).Add((C_.Q * e.sec.Negate()).ToGroupElement()).ToPubkey(); var e_ = ComputeE(r1, r2, A, C_); return e.sec.Equals(e_); } - public static bool VerifyProof(ECPubKey Y, ECPrivKey r, ECPubKey C, ECPrivKey e, ECPrivKey s, ECPubKey A) + /// + /// Verify DLEQ proof + /// + /// Hash to curve result + /// Blinidng Factor + /// Amount Pubkey + /// Dleq.E returned by mint + /// Dleq.S returned by mint + /// Amount pubkey + /// + public static bool VerifyProof( + ECPubKey Y, + ECPrivKey r, + ECPubKey C, + ECPrivKey e, + ECPrivKey s, + ECPubKey A + ) { var C_ = C.Q.ToGroupElementJacobian().Add((A.Q * r.sec).ToGroupElement()).ToPubkey(); var B_ = Y.Q.ToGroupElementJacobian().Add(r.CreatePubKey().Q).ToPubkey(); return VerifyProof(B_, C_, e, s, A); } - public static ECPubKey ComputeC(ECPubKey C_, ECPrivKey r, ECPubKey A) + public static GE ToGE(this Scalar scalar) { - //C_ - rA = C - return C_.Q.ToGroupElementJacobian().Add((A.Q * r.sec).ToGroupElement().Negate()).ToPubkey(); + // Multiply the scalar by the generator point to get the group element + GEJ gej = Context.Instance.EcMultGenContext.MultGen(scalar); + return gej.ToGroupElement(); } + public static ECPubKey ToPubkey(this Scalar scalar) + { + return new ECPubKey(scalar.ToGE(), Context.Instance); + } + + public static ECPrivKey ToPrivateKey(this Scalar scalar) + { + return ECPrivKey.TryCreate(scalar, out var key) + ? key + : throw new InvalidOperationException(); + } + + public static ECPubKey ToPubkey(this GEJ gej) + { + return new ECPubKey(gej.ToGroupElement(), Context.Instance); + } + + public static ECPubKey ToPubkey(this GE ge) + { + return new ECPubKey(ge, Context.Instance); + } + + /// + /// Compute shared secret for P2Bk (ECDH) + /// + /// Privkey of Alice + /// Pubkey of Bob + /// Zx = x(e·P) or x(p·E) + /// If can't create xOnly pubkey with derived shared secret public static byte[] ComputeZx(ECPrivKey e, ECPubKey P) { var x = (e.sec * P.Q).ToGroupElement().x; @@ -138,11 +235,11 @@ public static byte[] ComputeZx(ECPrivKey e, ECPubKey P) ? xOnly.ToBytes() : throw new InvalidOperationException("Could not create xOnly pubkey"); } - + public static ECPrivKey ComputeRi(byte[] Zx, int i) { byte[] hash; - + hash = SHA256.HashData(Concat(P2BK_PREFIX, Zx, [(byte)(i & 0xFF)])); var hashValue = new BigInteger(hash); if (hashValue == 0 || hashValue.CompareTo(N) != -1) @@ -151,8 +248,7 @@ public static ECPrivKey ComputeRi(byte[] Zx, int i) } return ECPrivKey.Create(hash); } - - + private static byte[] Concat(params byte[][] arrays) { int totalLength = arrays.Sum(a => a?.Length ?? 0); @@ -161,42 +257,48 @@ private static byte[] Concat(params byte[][] arrays) foreach (var arr in arrays) { - if (arr == null || arr.Length == 0) continue; + if (arr == null || arr.Length == 0) + continue; Buffer.BlockCopy(arr, 0, result, offset, arr.Length); offset += arr.Length; } return result; } - + public static string ToHex(this ECPrivKey key) { return Convert.ToHexString(key.ToBytes()).ToLower(); } - + public static byte[] ToBytes(this ECPrivKey key) { Span output = stackalloc byte[32]; key.WriteToSpan(output); return output.ToArray(); } - + public static byte[] ToUncompressedBytes(this ECPubKey key) { Span output = stackalloc byte[65]; - key.WriteToSpan(false, output, out _); + key.WriteToSpan(false, output, out _); return output.ToArray(); } + public static string ToHex(this ECPubKey key, bool compressed = true) { - return compressed ? Convert.ToHexString(key.ToBytes(true)).ToLower() : Convert.ToHexString(key.ToUncompressedBytes()).ToLower(); + return compressed + ? Convert.ToHexString(key.ToBytes(true)).ToLower() + : Convert.ToHexString(key.ToUncompressedBytes()).ToLower(); } + public static string ToHex(this Scalar scalar) { return Convert.ToHexString(scalar.ToBytes()).ToLower(); } + public static string ToHex(this SecpSchnorrSignature sig) { return Convert.ToHexString(sig.ToBytes()).ToLower(); } -} \ No newline at end of file +} diff --git a/DotNut/NUT00/CashuToken.cs b/DotNut/NUT00/CashuToken.cs new file mode 100644 index 0000000..0c962bd --- /dev/null +++ b/DotNut/NUT00/CashuToken.cs @@ -0,0 +1,34 @@ +using System.Text.Json.Serialization; + +namespace DotNut; + +public class CashuToken +{ + public class Token + { + public Token() { } + + public Token(string mint, List proofs) + { + Mint = mint; + Proofs = proofs; + } + + [JsonPropertyName("mint")] + public string Mint { get; set; } + + [JsonPropertyName("proofs")] + public List Proofs { get; set; } + } + + [JsonPropertyName("token")] + public List Tokens { get; set; } + + [JsonPropertyName("unit")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public string? Unit { get; set; } + + [JsonPropertyName("memo")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public string? Memo { get; set; } +} diff --git a/DotNut/ISecret.cs b/DotNut/NUT00/ISecret.cs similarity index 99% rename from DotNut/ISecret.cs rename to DotNut/NUT00/ISecret.cs index 5e860f8..8996bc3 100644 --- a/DotNut/ISecret.cs +++ b/DotNut/NUT00/ISecret.cs @@ -9,4 +9,4 @@ public interface ISecret { byte[] GetBytes(); ECPubKey ToCurve(); -} \ No newline at end of file +} diff --git a/DotNut/Proof.cs b/DotNut/NUT00/Proof.cs similarity index 67% rename from DotNut/Proof.cs rename to DotNut/NUT00/Proof.cs index 5c9dd78..b433c20 100644 --- a/DotNut/Proof.cs +++ b/DotNut/NUT00/Proof.cs @@ -1,31 +1,30 @@ using System.Text.Json.Serialization; -using DotNut.JsonConverters; - namespace DotNut; public class Proof { - [JsonPropertyName("amount")] public ulong Amount { get; set; } + [JsonPropertyName("amount")] + public ulong Amount { get; set; } - [JsonConverter(typeof(KeysetIdJsonConverter))] [JsonPropertyName("id")] public KeysetId Id { get; set; } - [JsonPropertyName("secret")] public ISecret Secret { get; set; } + [JsonPropertyName("secret")] + public ISecret Secret { get; set; } - [JsonPropertyName("C")] public PubKey C { get; set; } + [JsonPropertyName("C")] + public PubKey C { get; set; } [JsonPropertyName("witness")] [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public string? Witness { get; set; } - + [JsonPropertyName("dleq")] [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public DLEQProof? DLEQ { get; set; } - + [JsonPropertyName("p2pk_e")] [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public PubKey? P2PkE { get; set; } // must not be exposed to mint - -} \ No newline at end of file +} diff --git a/DotNut/StringSecret.cs b/DotNut/NUT00/StringSecret.cs similarity index 99% rename from DotNut/StringSecret.cs rename to DotNut/NUT00/StringSecret.cs index 5181db0..1e530d1 100644 --- a/DotNut/StringSecret.cs +++ b/DotNut/NUT00/StringSecret.cs @@ -10,6 +10,7 @@ public StringSecret(string secret) } public string Secret { get; init; } + public byte[] GetBytes() { return System.Text.Encoding.UTF8.GetBytes(Secret); @@ -19,4 +20,4 @@ public ECPubKey ToCurve() { return Cashu.HashToCurve(GetBytes()); } -} \ No newline at end of file +} diff --git a/DotNut/Keyset.cs b/DotNut/NUT01/Keyset.cs similarity index 62% rename from DotNut/Keyset.cs rename to DotNut/NUT01/Keyset.cs index b831233..e2c1ec2 100644 --- a/DotNut/Keyset.cs +++ b/DotNut/NUT01/Keyset.cs @@ -1,4 +1,6 @@ -using System.Text; +using System.Net.Mime; +using System.Text; +using System.Text.Encodings.Web; using System.Text.Json.Serialization; using DotNut.JsonConverters; using SHA256 = System.Security.Cryptography.SHA256; @@ -8,15 +10,20 @@ namespace DotNut; [JsonConverter(typeof(KeysetJsonConverter))] public class Keyset : Dictionary { - public KeysetId GetKeysetId(byte version = 0x00, string? unit = null, ulong? inputFeePpk = null, string? finalExpiration = null) + public KeysetId GetKeysetId( + byte version = 0x00, + string? unit = null, + ulong? inputFeePpk = null, + ulong? finalExpiration = null + ) { // 1 - sort public keys by their amount in ascending order - if (Count == 0) throw new InvalidOperationException("Keyset cannot be empty."); - var sortedKeys = this - .OrderBy(x => x.Key); - + if (Count == 0) + throw new InvalidOperationException("Keyset cannot be empty."); + var sortedKeys = this.OrderBy(x => x.Key); + using SHA256 sha256 = SHA256.Create(); - + switch (version) { case 0x00: @@ -30,29 +37,36 @@ public KeysetId GetKeysetId(byte version = 0x00, string? unit = null, ulong? inp var hash = sha256.ComputeHash(sortedBytes); // 4 - take the first 14 characters of the hex-encoded hash // 5 - prefix it with a keyset ID version byte - return new KeysetId(Convert.ToHexString(new []{version}) + Convert.ToHexString(hash).Substring(0, 14).ToLower()); + return new KeysetId( + Convert.ToHexString(new[] { version }) + + Convert.ToHexString(hash).Substring(0, 14).ToLower() + ); } - + case 0x01: { MemoryStream stream = new MemoryStream(); - + // 2 - concatenate each amount and its corresponding public key hex string (as "amount:publickey_hex") // to a single byte array, separating each pair with a comma (",") var sortedBytes = Encoding.UTF8.GetBytes( string.Join( ",", - sortedKeys - .Select(pair => $"{pair.Key}:{pair.Value.ToString().ToLowerInvariant()}") + sortedKeys.Select(pair => + $"{pair.Key}:{pair.Value.ToString().ToLowerInvariant()}" + ) ) ); - + stream.Write(sortedBytes, 0, sortedBytes.Length); - + // 3 - add the lowercase UTF8-encoded unit string prefixed with "|unit:" to the byte array (e.g. "|unit:sat") if (String.IsNullOrWhiteSpace(unit)) - { - throw new ArgumentNullException( nameof(unit), $"Unit parameter is required with version: {version}"); + { + throw new ArgumentNullException( + nameof(unit), + $"Unit parameter is required with version: {version}" + ); } var unitBytes = Encoding.UTF8.GetBytes($"|unit:{unit.Trim().ToLowerInvariant()}"); @@ -63,37 +77,45 @@ public KeysetId GetKeysetId(byte version = 0x00, string? unit = null, ulong? inp // If input_fee_ppk is omitted, null, or 0, it MUST be omitted from the preimage. if (inputFeePpk.HasValue && inputFeePpk.Value != 0) { - var feeBytes = Encoding.UTF8.GetBytes($"|input_fee_ppk:{inputFeePpk.Value}"); - stream.Write(feeBytes, 0, feeBytes.Length); + var feeBytes = Encoding.UTF8.GetBytes($"|input_fee_ppk:{inputFeePpk.Value}"); + stream.Write(feeBytes, 0, feeBytes.Length); } - + // 5 - If a final expiration is specified, add the UTF8-encoded string prefixed with "|final_expiry:" (e.g. "|final_expiry:1896187313") - if (!string.IsNullOrWhiteSpace(finalExpiration)) + if (finalExpiration is not null) { - var expiryBytes = Encoding.UTF8.GetBytes($"|final_expiry:{finalExpiration.Trim()}"); + var expiryBytes = Encoding.UTF8.GetBytes( + $"|final_expiry:{finalExpiration.ToString()}" + ); stream.Write(expiryBytes, 0, expiryBytes.Length); } - + // 6 - HASH_SHA256 the concatenated byte array var hash = sha256.ComputeHash(stream.ToArray()); - + // 7 - prefix it with a keyset ID version byte "01" - return new KeysetId(Convert.ToHexString(new[] { version }) + - Convert.ToHexString(hash).ToLower()); + return new KeysetId( + Convert.ToHexString(new[] { version }) + Convert.ToHexString(hash).ToLower() + ); } default: throw new ArgumentException($"Unsupported keyset version: {version}"); } - } - public bool VerifyKeysetId(KeysetId keysetId, string? unit = null, ulong? inputFeePpk = null, string? finalExpiration = null) + public bool VerifyKeysetId( + KeysetId keysetId, + string? unit = null, + ulong? inputFeePpk = null, + ulong? finalExpiration = null + ) { byte version = keysetId.GetVersion(); var derived = GetKeysetId(version, unit, inputFeePpk, finalExpiration).ToString(); var presented = keysetId.ToString(); - if (presented.Length > derived.Length) return false; - return string.Equals(derived, presented, StringComparison.Ordinal) || - derived.StartsWith(presented, StringComparison.Ordinal); + if (presented.Length > derived.Length) + return false; + return string.Equals(derived, presented, StringComparison.InvariantCultureIgnoreCase) + || derived.StartsWith(presented, StringComparison.InvariantCultureIgnoreCase); } -} \ No newline at end of file +} diff --git a/DotNut/NUT02/FeeHelper.cs b/DotNut/NUT02/FeeHelper.cs new file mode 100644 index 0000000..adea066 --- /dev/null +++ b/DotNut/NUT02/FeeHelper.cs @@ -0,0 +1,29 @@ +using DotNut.ApiModels; + +namespace DotNut; + +public static class FeeHelper +{ + public static ulong ComputeFee( + this IEnumerable proofsToSpend, + Dictionary keysetFees + ) + { + ulong sum = 0; + foreach (var proof in proofsToSpend) + { + if (keysetFees.TryGetValue(proof.Id, out var fee)) + { + sum += fee; + } + } + + return (sum + 999) / 1000; + } + + public static ulong Sum(this IEnumerable values) + { + ArgumentNullException.ThrowIfNull(values); + return values.Aggregate(0, (current, v) => current + v); + } +} diff --git a/DotNut/KeysetId.cs b/DotNut/NUT02/KeysetId.cs similarity index 76% rename from DotNut/KeysetId.cs rename to DotNut/NUT02/KeysetId.cs index 7249d84..1c3ba7a 100644 --- a/DotNut/KeysetId.cs +++ b/DotNut/NUT02/KeysetId.cs @@ -4,14 +4,18 @@ namespace DotNut; [JsonConverter(typeof(KeysetIdJsonConverter))] -public class KeysetId : IEquatable,IEqualityComparer +public class KeysetId : IEquatable, IEqualityComparer { public bool Equals(KeysetId? x, KeysetId? y) { - if (ReferenceEquals(x, y)) return true; - if (ReferenceEquals(x, null)) return false; - if (ReferenceEquals(y, null)) return false; - if (x.GetType() != y.GetType()) return false; + if (ReferenceEquals(x, y)) + return true; + if (ReferenceEquals(x, null)) + return false; + if (ReferenceEquals(y, null)) + return false; + if (x.GetType() != y.GetType()) + return false; return string.Equals(x._id, y._id, StringComparison.InvariantCultureIgnoreCase); } @@ -27,7 +31,6 @@ public bool Equals(KeysetId? other) public override bool Equals(object? obj) { - return Equals(this, obj as KeysetId); } @@ -38,14 +41,15 @@ public override int GetHashCode() public static bool operator ==(KeysetId? left, KeysetId? right) { - return (left is null && right is null) || left?.Equals(right) is true || right?.Equals(left) is true; + return (left is null && right is null) + || left?.Equals(right) is true + || right?.Equals(left) is true; } public static bool operator !=(KeysetId? left, KeysetId? right) { return !(left == right); } - private readonly string _id; @@ -54,7 +58,8 @@ public KeysetId(string Id) if ( Id.Length != 66 // full length keysetId v2 && Id.Length != 16 // keysetId v1 or keysetId v2 short - && Id.Length != 12) // old pre-v1 base64 keysetId + && Id.Length != 12 + ) // old pre-v1 base64 keysetId { throw new ArgumentException("KeysetId must be 66, 16 or 12 (legacy) characters long"); } @@ -82,4 +87,4 @@ public byte[] GetBytes() { return Convert.FromHexString(_id); } -} \ No newline at end of file +} diff --git a/DotNut/NUT04/MintMethodSetting.cs b/DotNut/NUT04/MintMethodSetting.cs new file mode 100644 index 0000000..a2786ca --- /dev/null +++ b/DotNut/NUT04/MintMethodSetting.cs @@ -0,0 +1,22 @@ +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace DotNut; + +public class MintMethodSetting +{ + [JsonPropertyName("method")] + public string Method { get; set; } + + [JsonPropertyName("unit")] + public string Unit { get; set; } + + [JsonPropertyName("min_amount")] + public ulong? Min { get; set; } + + [JsonPropertyName("max_amount")] + public ulong? Max { get; set; } + + [JsonPropertyName("options")] + public JsonDocument? Options { get; set; } +} diff --git a/DotNut/NUT05/MeltMethodSetting.cs b/DotNut/NUT05/MeltMethodSetting.cs new file mode 100644 index 0000000..5a17597 --- /dev/null +++ b/DotNut/NUT05/MeltMethodSetting.cs @@ -0,0 +1,18 @@ +using System.Text.Json.Serialization; + +namespace DotNut; + +public class MeltMethodSetting +{ + [JsonPropertyName("method")] + public string Method { get; set; } + + [JsonPropertyName("unit")] + public string Unit { get; set; } + + [JsonPropertyName("min_amount")] + public ulong? Min { get; set; } + + [JsonPropertyName("max_amount")] + public ulong? Max { get; set; } +} diff --git a/DotNut/Nut10ProofSecret.cs b/DotNut/NUT10/Nut10ProofSecret.cs similarity index 58% rename from DotNut/Nut10ProofSecret.cs rename to DotNut/NUT10/Nut10ProofSecret.cs index 3389ba1..f30ce66 100644 --- a/DotNut/Nut10ProofSecret.cs +++ b/DotNut/NUT10/Nut10ProofSecret.cs @@ -6,16 +6,17 @@ public class Nut10ProofSecret { [JsonPropertyName("nonce")] public string Nonce { get; set; } - + [JsonPropertyName("data")] [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public string Data { get; set; } - + [JsonPropertyName("tags")] [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public string[][]? Tags { get; set; } public override bool Equals(object obj) => this.Equals(obj as Nut10ProofSecret); + public bool Equals(Nut10ProofSecret s) { if (s is null) @@ -27,21 +28,25 @@ public bool Equals(Nut10ProofSecret s) { return true; } - + if (this.GetType() != s.GetType()) { return false; } - - - return - this.Nonce == s.Nonce && - this.Data == s.Data && - ((this.Tags == null && s.Tags == null) || - (this.Tags != null && s.Tags != null && this.Tags.Length == s.Tags.Length && - this.Tags.Zip(s.Tags).All(pair => pair.First.SequenceEqual(pair.Second)))); + + return this.Nonce == s.Nonce + && this.Data == s.Data + && ( + (this.Tags == null && s.Tags == null) + || ( + this.Tags != null + && s.Tags != null + && this.Tags.Length == s.Tags.Length + && this.Tags.Zip(s.Tags).All(pair => pair.First.SequenceEqual(pair.Second)) + ) + ); } - + public override int GetHashCode() { var hash = new HashCode(); @@ -71,6 +76,24 @@ public override int GetHashCode() return first.Equals(second); } - public static bool operator !=(Nut10ProofSecret first, Nut10ProofSecret second) => !(first == second); + public static bool operator !=(Nut10ProofSecret first, Nut10ProofSecret second) => + !(first == second); -} \ No newline at end of file + /// + /// Helper for SIG_ALL equality check. Every proof has to have identical data and tags fields + /// + public bool SigAllEquals(Nut10ProofSecret other) + { + return other is { } o + && this.Data == o.Data + && ( + (this.Tags == null && o.Tags == null) + || ( + this.Tags != null + && o.Tags != null + && this.Tags.Length == o.Tags.Length + && this.Tags.Zip(o.Tags).All(pair => pair.First.SequenceEqual(pair.Second)) + ) + ); + } +} diff --git a/DotNut/Nut10Secret.cs b/DotNut/NUT10/Nut10Secret.cs similarity index 99% rename from DotNut/Nut10Secret.cs rename to DotNut/NUT10/Nut10Secret.cs index 05538b6..0799e43 100644 --- a/DotNut/Nut10Secret.cs +++ b/DotNut/NUT10/Nut10Secret.cs @@ -24,7 +24,6 @@ public Nut10Secret(string originalString) public string Key { get; set; } public Nut10ProofSecret ProofSecret { get; set; } - public byte[] GetBytes() { return _originalString != null @@ -36,4 +35,4 @@ public ECPubKey ToCurve() { return Cashu.HashToCurve(GetBytes()); } -} \ No newline at end of file +} diff --git a/DotNut/P2PKBuilder.cs b/DotNut/NUT11/P2PKBuilder.cs similarity index 65% rename from DotNut/P2PKBuilder.cs rename to DotNut/NUT11/P2PKBuilder.cs index eeebbbb..5198018 100644 --- a/DotNut/P2PKBuilder.cs +++ b/DotNut/NUT11/P2PKBuilder.cs @@ -3,7 +3,7 @@ namespace DotNut; -public class P2PKBuilder +public class P2PkBuilder { public DateTimeOffset? Lock { get; set; } public ECPubKey[]? RefundPubkeys { get; set; } @@ -11,11 +11,11 @@ public class P2PKBuilder public ECPubKey[] Pubkeys { get; set; } - //SIG_INPUTS, SIG_ALL + //SIG_INPUTS, SIG_ALL public string? SigFlag { get; set; } public string? Nonce { get; set; } public int? RefundSignatureThreshold { get; set; } - + public P2PKProofSecret Build() { Validate(); @@ -35,14 +35,12 @@ public P2PKProofSecret Build() tags.Add(new[] { "locktime", Lock.Value.ToUnixTimeSeconds().ToString() }); if (RefundPubkeys?.Any() is true) { - tags.Add(new[] { "refund" }.Concat(RefundPubkeys.Select(p => p.ToHex())) - .ToArray()); + tags.Add(new[] { "refund" }.Concat(RefundPubkeys.Select(p => p.ToHex())).ToArray()); RefundSignatureThreshold ??= 1; - } if (RefundSignatureThreshold is { } refundSignatureThreshold and > 1) { - tags.Add(new[] {"n_sigs_refund", refundSignatureThreshold.ToString() }); + tags.Add(new[] { "n_sigs_refund", refundSignatureThreshold.ToString() }); } } @@ -50,56 +48,76 @@ public P2PKProofSecret Build() { tags.Add(new[] { "n_sigs", SignatureThreshold.ToString() }); } - + return new P2PKProofSecret() { Data = Pubkeys.First().ToHex(), Nonce = Nonce ?? RandomNumberGenerator.GetHexString(32, true), - Tags = tags.ToArray() + Tags = tags.ToArray(), }; } - public static P2PKBuilder Load(P2PKProofSecret proofSecret) + public static P2PkBuilder Load(P2PKProofSecret proofSecret) { - var builder = new P2PKBuilder(); + var builder = new P2PkBuilder(); var primaryPubkey = proofSecret.Data.ToPubKey(); - var pubkeys = proofSecret.Tags?.FirstOrDefault(strings => strings.FirstOrDefault() == "pubkeys"); + var pubkeys = proofSecret.Tags?.FirstOrDefault(strings => + strings.FirstOrDefault() == "pubkeys" + ); if (pubkeys is not null && pubkeys.Length > 1) { - builder.Pubkeys = pubkeys.Skip(1).Select(s => s.ToPubKey()).Prepend(primaryPubkey).ToArray(); + builder.Pubkeys = pubkeys + .Skip(1) + .Select(s => s.ToPubKey()) + .Prepend(primaryPubkey) + .ToArray(); } else { builder.Pubkeys = [primaryPubkey]; } - var rawUnixTs = proofSecret.Tags?.FirstOrDefault(strings => strings.FirstOrDefault() == "locktime")?.Skip(1) + var rawUnixTs = proofSecret + .Tags?.FirstOrDefault(strings => strings.FirstOrDefault() == "locktime") + ?.Skip(1) ?.FirstOrDefault(); - builder.Lock = rawUnixTs is not null && long.TryParse(rawUnixTs, out var unixTs) - ? DateTimeOffset.FromUnixTimeSeconds(unixTs) - : null; - - var refund = proofSecret.Tags?.FirstOrDefault(strings => strings.FirstOrDefault() == "refund"); + builder.Lock = + rawUnixTs is not null && long.TryParse(rawUnixTs, out var unixTs) + ? DateTimeOffset.FromUnixTimeSeconds(unixTs) + : null; + + var refund = proofSecret.Tags?.FirstOrDefault(strings => + strings.FirstOrDefault() == "refund" + ); if (refund is not null && refund.Length > 1) { builder.RefundPubkeys = refund.Skip(1).Select(s => s.ToPubKey()).ToArray(); } - - var nSigsRefund = proofSecret.Tags?.FirstOrDefault(strings => strings.FirstOrDefault() == "n_sigs_refund")? - .Skip(1)?.FirstOrDefault(); - if (!string.IsNullOrEmpty(nSigsRefund) && int.TryParse(nSigsRefund, out var nSigsRefundValue)) + + var nSigsRefund = proofSecret + .Tags?.FirstOrDefault(strings => strings.FirstOrDefault() == "n_sigs_refund") + ?.Skip(1) + ?.FirstOrDefault(); + if ( + !string.IsNullOrEmpty(nSigsRefund) + && int.TryParse(nSigsRefund, out var nSigsRefundValue) + ) { builder.RefundSignatureThreshold = nSigsRefundValue; } - var sigFlag = proofSecret.Tags?.FirstOrDefault(strings => strings.FirstOrDefault() == "sigflag")?.Skip(1) + var sigFlag = proofSecret + .Tags?.FirstOrDefault(strings => strings.FirstOrDefault() == "sigflag") + ?.Skip(1) ?.FirstOrDefault(); if (!string.IsNullOrEmpty(sigFlag)) { builder.SigFlag = sigFlag; } - var nSigs = proofSecret.Tags?.FirstOrDefault(strings => strings.FirstOrDefault() == "n_sigs")?.Skip(1) + var nSigs = proofSecret + .Tags?.FirstOrDefault(strings => strings.FirstOrDefault() == "n_sigs") + ?.Skip(1) ?.FirstOrDefault(); if (!string.IsNullOrEmpty(nSigs) && int.TryParse(nSigs, out var nSigsValue)) { @@ -110,27 +128,28 @@ public static P2PKBuilder Load(P2PKProofSecret proofSecret) return builder; } - + private void Validate() { if (this.Pubkeys.Count() < SignatureThreshold) { throw new ArgumentException("Signature threshold bigger than provided pubkeys count!"); } - if(this.RefundSignatureThreshold is not null - && (RefundPubkeys is null || RefundPubkeys.Length < RefundSignatureThreshold)) + if ( + this.RefundSignatureThreshold is not null + && (RefundPubkeys is null || RefundPubkeys.Length < RefundSignatureThreshold) + ) { throw new ArgumentException("Signature threshold bigger than provided pubkeys count!"); } } - - + /* * ========================= * NUT-XX Pay to blinded key * ========================= */ - + //For sig_inputs, generates random p2pk_e for each input public P2PKProofSecret BuildBlinded(out ECPubKey p2pkE) { @@ -144,18 +163,17 @@ public P2PKProofSecret BuildBlinded(ECPrivKey p2pke) { var pubkeys = RefundPubkeys != null ? Pubkeys.Concat(RefundPubkeys).ToArray() : Pubkeys; var rs = new List(); - for (int i = 0; i < pubkeys.Length; i++) { var Zx = Cashu.ComputeZx(p2pke, pubkeys[i]); var Ri = Cashu.ComputeRi(Zx, i); rs.Add(Ri); } - _blindPubkeys(rs.ToArray()); + BlindPubkeys(rs.ToArray()); return Build(); } - - private void _blindPubkeys(ECPrivKey[] rs) + + protected void BlindPubkeys(ECPrivKey[] rs) { var expectedLength = Pubkeys.Length + (RefundPubkeys?.Length ?? 0); if (expectedLength != rs.Length) @@ -173,8 +191,25 @@ private void _blindPubkeys(ECPrivKey[] rs) if (RefundPubkeys != null) { - RefundPubkeys[i - Pubkeys.Length] = Cashu.ComputeB_(RefundPubkeys[i - Pubkeys.Length], rs[i]); + RefundPubkeys[i - Pubkeys.Length] = Cashu.ComputeB_( + RefundPubkeys[i - Pubkeys.Length], + rs[i] + ); } } } -} \ No newline at end of file + + public virtual P2PkBuilder Clone() + { + return new P2PkBuilder() + { + Lock = Lock, + RefundPubkeys = RefundPubkeys?.ToArray(), + SignatureThreshold = SignatureThreshold, + RefundSignatureThreshold = RefundSignatureThreshold, + Pubkeys = Pubkeys.ToArray(), + SigFlag = SigFlag, + Nonce = Nonce, + }; + } +} diff --git a/DotNut/P2PKProofSecret.cs b/DotNut/NUT11/P2PKProofSecret.cs similarity index 71% rename from DotNut/P2PKProofSecret.cs rename to DotNut/NUT11/P2PKProofSecret.cs index 294c472..466dc61 100644 --- a/DotNut/P2PKProofSecret.cs +++ b/DotNut/NUT11/P2PKProofSecret.cs @@ -1,4 +1,5 @@ using System.Text; +using System.Text.Json; using System.Text.Json.Serialization; using NBitcoin.Secp256k1; using SHA256 = System.Security.Cryptography.SHA256; @@ -9,7 +10,8 @@ public class P2PKProofSecret : Nut10ProofSecret { public const string Key = "P2PK"; - [JsonIgnore] P2PKBuilder Builder => P2PKBuilder.Load(this); + [JsonIgnore] + public virtual P2PkBuilder Builder => P2PkBuilder.Load(this); public virtual ECPubKey[] GetAllowedPubkeys(out int requiredSignatures) { @@ -17,16 +19,26 @@ public virtual ECPubKey[] GetAllowedPubkeys(out int requiredSignatures) requiredSignatures = builder.SignatureThreshold; return builder.Pubkeys; } - + + /// + /// Can have 3 types of values + /// null - no refund condition / timelock still on + /// 0 - proof is spendable without any signature + /// 1,2 ... int.MaxValue - amount of required signatures + /// + /// public virtual ECPubKey[] GetAllowedRefundPubkeys(out int? requiredSignatures) { var builder = Builder; - if (!builder.Lock.HasValue || builder.Lock.Value.ToUnixTimeSeconds() >= DateTimeOffset.Now.ToUnixTimeSeconds()) + if ( + !builder.Lock.HasValue + || builder.Lock.Value.ToUnixTimeSeconds() >= DateTimeOffset.Now.ToUnixTimeSeconds() + ) { requiredSignatures = null; // there's no refund condition, or timelock didn't expire yet :/ return []; } - + if (builder.RefundPubkeys == null) { requiredSignatures = 0; // proof is spendable without any signature @@ -35,63 +47,72 @@ public virtual ECPubKey[] GetAllowedRefundPubkeys(out int? requiredSignatures) requiredSignatures = builder.RefundSignatureThreshold ?? 1; return builder.RefundPubkeys ?? []; } - - /* + + /* * ====================================================================== * * If any of these returns null witness - well, witness is not necessary * * ====================================================================== * */ - + public virtual P2PKWitness? GenerateWitness(Proof proof, ECPrivKey[] keys) { return GenerateWitness(proof.Secret.GetBytes(), keys); - } - + } + public virtual P2PKWitness? GenerateWitness(BlindedMessage message, ECPrivKey[] keys) { return GenerateWitness(message.B_.Key.ToBytes(), keys); } - + public virtual P2PKWitness? GenerateWitness(byte[] msg, ECPrivKey[] keys) { var hash = SHA256.HashData(msg); return GenerateWitness(ECPrivKey.Create(hash), keys); } - + public virtual P2PKWitness? GenerateWitness(ECPrivKey hash, ECPrivKey[] keys) { var msg = hash.ToBytes(); - + var allowedKeys = GetAllowedPubkeys(out var requiredSignatures); var allowedRefundKeys = GetAllowedRefundPubkeys(out var requiredRefundSignatures); - + if (requiredRefundSignatures == 0) { return null; } - + // try normal path var (isValid, result) = TrySignPath(allowedKeys.ToArray(), requiredSignatures, keys, msg); if (isValid) - { + { return result; } - + // if it's after locktime - try refund path if (requiredRefundSignatures.HasValue && allowedRefundKeys.Any()) { - (isValid, result) = TrySignPath(allowedRefundKeys.ToArray(), requiredRefundSignatures.Value, keys, msg); + (isValid, result) = TrySignPath( + allowedRefundKeys.ToArray(), + requiredRefundSignatures.Value, + keys, + msg + ); if (isValid) { return result; } } - + throw new InvalidOperationException("Not enough valid keys to sign!"); } - - private (bool IsValid, P2PKWitness Witness) TrySignPath(ECPubKey[] allowedKeys, int requiredSignatures, - ECPrivKey[] availableKeys, byte[] msg) + + private (bool IsValid, P2PKWitness Witness) TrySignPath( + ECPubKey[] allowedKeys, + int requiredSignatures, + ECPrivKey[] availableKeys, + byte[] msg + ) { var allowedKeysSet = new HashSet(allowedKeys); var result = new P2PKWitness(); @@ -111,50 +132,79 @@ public virtual ECPubKey[] GetAllowedRefundPubkeys(out int? requiredSignatures) return (result.Signatures.Length >= requiredSignatures, result); } - - + public virtual bool VerifyWitness(Proof proof) + { + if (proof.Witness is null) + { + return false; + } + try + { + var witness = + JsonSerializer.Deserialize(proof.Witness) ?? new P2PKWitness(); + return VerifyWitness(proof.Secret, witness); + } + catch + { + return false; + } + } /* * ========================= * NUT-XX Pay to blinded key * ========================= */ - - public virtual P2PKWitness? GenerateBlindWitness(Proof proof, ECPrivKey[] keys, KeysetId keysetId) + + public virtual P2PKWitness? GenerateBlindWitness(Proof proof, ECPrivKey[] keys) { ArgumentNullException.ThrowIfNull(proof.P2PkE); - return GenerateBlindWitness(proof.Secret.GetBytes(), keys, keysetId, proof.P2PkE); + return GenerateBlindWitness(proof.Secret.GetBytes(), keys, proof.P2PkE); } - public virtual P2PKWitness? GenerateBlindWitness(Proof proof, ECPrivKey[] keys, KeysetId keysetId, ECPubKey P2PkE) + public virtual P2PKWitness? GenerateBlindWitness(Proof proof, ECPrivKey[] keys, ECPubKey P2PkE) { - return GenerateBlindWitness(proof.Secret.GetBytes(), keys, keysetId, P2PkE); + return GenerateBlindWitness(proof.Secret.GetBytes(), keys, P2PkE); } - - public virtual P2PKWitness? GenerateBlindWitness(BlindedMessage message, ECPrivKey[] keys, KeysetId keysetId, ECPubKey P2PkE) + + public virtual P2PKWitness? GenerateBlindWitness( + BlindedMessage message, + ECPrivKey[] keys, + ECPubKey P2PkE + ) { - return GenerateBlindWitness(message.B_.Key.ToBytes(), keys, keysetId, P2PkE); + return GenerateBlindWitness(message.B_.Key.ToBytes(), keys, P2PkE); } - - public virtual P2PKWitness? GenerateBlindWitness(byte[] msg, ECPrivKey[] keys, KeysetId keysetId, ECPubKey P2PkE) + + public virtual P2PKWitness? GenerateBlindWitness(byte[] msg, ECPrivKey[] keys, ECPubKey P2PkE) { var hash = SHA256.HashData(msg); - return GenerateBlindWitness(ECPrivKey.Create(hash), keys, keysetId, P2PkE); + return GenerateBlindWitness(ECPrivKey.Create(hash), keys, P2PkE); } - - public virtual P2PKWitness? GenerateBlindWitness(ECPrivKey hash, ECPrivKey[] keys, KeysetId keysetId, ECPubKey P2PkE) + + public virtual P2PKWitness? GenerateBlindWitness( + ECPrivKey hash, + ECPrivKey[] keys, + ECPubKey P2PkE + ) { var msg = hash.ToBytes(); - + var allowedKeys = GetAllowedPubkeys(out var requiredSignatures); var allowedRefundKeys = GetAllowedRefundPubkeys(out var requiredRefundSignatures); if (requiredRefundSignatures == 0) return null; - var (isValid, result) = TrySignBlindPath(allowedKeys.ToArray(), requiredSignatures, keys, P2PkE, msg); + var (isValid, result) = TrySignBlindPath( + allowedKeys.ToArray(), + requiredSignatures, + keys, + P2PkE, + msg + ); if (isValid) { return result; @@ -162,7 +212,13 @@ public virtual ECPubKey[] GetAllowedRefundPubkeys(out int? requiredSignatures) if (requiredRefundSignatures.HasValue && allowedRefundKeys.Any()) { - (isValid, result) = TrySignBlindPath(allowedRefundKeys.ToArray(), requiredRefundSignatures.Value, keys, P2PkE, msg); + (isValid, result) = TrySignBlindPath( + allowedRefundKeys.ToArray(), + requiredRefundSignatures.Value, + keys, + P2PkE, + msg + ); if (isValid) { return result; @@ -172,9 +228,13 @@ public virtual ECPubKey[] GetAllowedRefundPubkeys(out int? requiredSignatures) throw new InvalidOperationException("Not enough valid keys to sign any blind path"); } - - private (bool IsValid, P2PKWitness Witness) TrySignBlindPath(ECPubKey[] allowedKeys, int requiredSignatures, - ECPrivKey[] availableKeys, ECPubKey P2PkE, byte[] msg) + private (bool IsValid, P2PKWitness Witness) TrySignBlindPath( + ECPubKey[] allowedKeys, + int requiredSignatures, + ECPrivKey[] availableKeys, + ECPubKey P2PkE, + byte[] msg + ) { var allowedKeysSet = new HashSet(allowedKeys); var result = new P2PKWitness(); @@ -185,9 +245,10 @@ public virtual ECPubKey[] GetAllowedRefundPubkeys(out int? requiredSignatures) if (result.Signatures.Length >= requiredSignatures) break; - for (int i = 0; i < allowedKeys.Length; i++) + for (int i = 0; i < allowedKeys.Length; i++) { - if (usedSlots.Contains(i)) continue; + if (usedSlots.Contains(i)) + continue; var Zx = Cashu.ComputeZx(key, P2PkE); var ri = Cashu.ComputeRi(Zx, i); @@ -218,7 +279,6 @@ public virtual ECPubKey[] GetAllowedRefundPubkeys(out int? requiredSignatures) return (result.Signatures.Length >= requiredSignatures, result); } - public virtual bool VerifyWitness(string message, P2PKWitness witness) { var hash = SHA256.HashData(Encoding.UTF8.GetBytes(message)); @@ -240,8 +300,12 @@ public virtual bool VerifyWitnessHash(byte[] hash, P2PKWitness witness) { try { - var sigs = witness.Signatures - .Select(s => SecpSchnorrSignature.TryCreate(Convert.FromHexString(s), out var sig) ? sig : null) + var sigs = witness + .Signatures.Select(s => + SecpSchnorrSignature.TryCreate(Convert.FromHexString(s), out var sig) + ? sig + : null + ) .Where(signature => signature is not null) .ToArray(); @@ -251,17 +315,23 @@ public virtual bool VerifyWitnessHash(byte[] hash, P2PKWitness witness) { return true; } - + if (VerifyPath(allowedKeys.ToArray(), requiredSignatures, sigs, hash)) return true; - if (requiredRefundSignatures.HasValue && allowedRefundKeys.Any()) { - if (VerifyPath(allowedRefundKeys.ToArray(), requiredRefundSignatures.Value, sigs, hash)) + if ( + VerifyPath( + allowedRefundKeys.ToArray(), + requiredRefundSignatures.Value, + sigs, + hash + ) + ) return true; } - + return false; } catch (Exception e) @@ -269,9 +339,13 @@ public virtual bool VerifyWitnessHash(byte[] hash, P2PKWitness witness) return false; } } - - private bool VerifyPath(ECPubKey[] allowedKeys, int requiredSignatures, - SecpSchnorrSignature[] sigs, byte[] hash) + + private bool VerifyPath( + ECPubKey[] allowedKeys, + int requiredSignatures, + SecpSchnorrSignature[] sigs, + byte[] hash + ) { if (sigs.Length < requiredSignatures) { @@ -279,7 +353,7 @@ private bool VerifyPath(ECPubKey[] allowedKeys, int requiredSignatures, } var xonlyKeys = allowedKeys.Select(k => k.ToXOnlyPubKey()).ToArray(); var usedKeyIndices = new HashSet(); - + foreach (var sig in sigs) { for (int i = 0; i < xonlyKeys.Length; i++) @@ -291,8 +365,7 @@ private bool VerifyPath(ECPubKey[] allowedKeys, int requiredSignatures, } } } - + return usedKeyIndices.Count >= requiredSignatures; } - -} \ No newline at end of file +} diff --git a/DotNut/NUT11/P2PKWitness.cs b/DotNut/NUT11/P2PKWitness.cs new file mode 100644 index 0000000..e2325ad --- /dev/null +++ b/DotNut/NUT11/P2PKWitness.cs @@ -0,0 +1,9 @@ +using System.Text.Json.Serialization; + +namespace DotNut; + +public class P2PKWitness +{ + [JsonPropertyName("signatures")] + public string[] Signatures { get; set; } = Array.Empty(); +} diff --git a/DotNut/SigAllHandler.cs b/DotNut/NUT11/SigAllHandler.cs similarity index 60% rename from DotNut/SigAllHandler.cs rename to DotNut/NUT11/SigAllHandler.cs index 641383c..6d5b106 100644 --- a/DotNut/SigAllHandler.cs +++ b/DotNut/NUT11/SigAllHandler.cs @@ -12,16 +12,21 @@ public class SigAllHandler public List BlindedMessages { get; set; } public string? HTLCPreimage { get; set; } public string? MeltQuoteId { get; set; } - - private Nut10ProofSecret? _firstProofSecret; - - + + private Nut10ProofSecret? _firstProofSecret; + public bool TrySign(out string? witness) { witness = null; - - if ( BlindedMessages is null || Proofs is null || PrivKeys is null || - BlindedMessages.Count == 0 || Proofs.Count == 0 || PrivKeys.Count == 0) + + if ( + BlindedMessages is null + || Proofs is null + || PrivKeys is null + || BlindedMessages.Count == 0 + || Proofs.Count == 0 + || PrivKeys.Count == 0 + ) { return false; } @@ -29,86 +34,109 @@ public bool TrySign(out string? witness) byte[] msg; try { - var msgStr = GetMessageToSign(Proofs.ToArray(), BlindedMessages.ToArray(), MeltQuoteId); if (!ValidateFirstProof(Proofs[0], out var sec) || sec is null) { return false; } _firstProofSecret = sec; + + var msgStr = GetMessageToSign(Proofs.ToArray(), BlindedMessages.ToArray(), MeltQuoteId); msg = Encoding.UTF8.GetBytes(msgStr); } catch (ArgumentException) { return false; } - + if (_firstProofSecret is not P2PKProofSecret fps) { return false; } - - P2PKWitness witnessObj; - if (fps is HTLCProofSecret s && HTLCPreimage is {} preimage) + + P2PKWitness? witnessObj; + if (fps is HTLCProofSecret s && HTLCPreimage is { } preimage) { if (Proofs.First().P2PkE is { } E) { - witnessObj = s.GenerateBlindWitness(msg, + witnessObj = s.GenerateBlindWitness( + msg, PrivKeys.Select(pk => (ECPrivKey)pk).ToArray(), Convert.FromHexString(preimage), - Proofs[0].Id, E ); - witness = JsonSerializer.Serialize((HTLCWitness)witnessObj); - return true; } - witnessObj = - s.GenerateWitness(msg, PrivKeys.Select(pk => (ECPrivKey)pk).ToArray(), + else + { + witnessObj = s.GenerateWitness( + msg, + PrivKeys.Select(pk => (ECPrivKey)pk).ToArray(), Convert.FromHexString(preimage) - ); - witness = JsonSerializer.Serialize((HTLCWitness)witnessObj); + ); + } + if (witnessObj is not null) + witness = JsonSerializer.Serialize((HTLCWitness)witnessObj); return true; } - if (Proofs.First().P2PkE is { } e2) { - witnessObj = fps.GenerateBlindWitness(msg, PrivKeys.Select(pk => (ECPrivKey)pk).ToArray(), Proofs[0].Id, e2); - witness = JsonSerializer.Serialize(witnessObj); - return true; + witnessObj = fps.GenerateBlindWitness( + msg, + PrivKeys.Select(pk => (ECPrivKey)pk).ToArray(), + e2 + ); } - witnessObj = fps.GenerateWitness(msg, PrivKeys.Select(pk => (ECPrivKey)pk).ToArray()); - witness = JsonSerializer.Serialize(witnessObj); + else + { + witnessObj = fps.GenerateWitness(msg, PrivKeys.Select(pk => (ECPrivKey)pk).ToArray()); + } + if (witnessObj is not null) + witness = JsonSerializer.Serialize(witnessObj); return true; } - public static string GetMessageToSign(Proof[] inputs, BlindedMessage[] outputs, string? meltQuoteId = null) + public static string GetMessageToSign( + Proof[] inputs, + BlindedMessage[] outputs, + string? meltQuoteId = null + ) { if (inputs is null || inputs.Length == 0) { - throw new ArgumentException("At least one proof is required for SIG_ALL.", nameof(inputs)); + throw new ArgumentException( + "At least one proof is required for SIG_ALL.", + nameof(inputs) + ); } if (outputs is null || outputs.Length == 0) { - throw new ArgumentException("At least one blinded output is required for SIG_ALL.", nameof(outputs)); + throw new ArgumentException( + "At least one blinded output is required for SIG_ALL.", + nameof(outputs) + ); } - if (!ValidateFirstProof(inputs[0], out var firstSecret)) + if (!ValidateFirstProof(inputs[0], out var firstSecret) || firstSecret is null) { - throw new ArgumentException("Provided first proof is invalid"); + throw new ArgumentException("Provided first proof is invalid or null"); } var msg = new StringBuilder(); - + for (var i = 0; i < inputs.Length; i++) { var p = inputs[i]; if (p.Secret is not Nut10Secret nut10) { - throw new ArgumentException("When signing sig_all, every proof must be a nut 10 secret."); + throw new ArgumentException( + "When signing sig_all, every proof must be a nut 10 secret." + ); } - - if (firstSecret != nut10.ProofSecret) + + if (!firstSecret.SigAllEquals(nut10.ProofSecret)) { - throw new ArgumentException("When signing sig_all, every proof must have identical tags and data."); + throw new ArgumentException( + "When signing sig_all, every proof must have identical tags and data." + ); } // serialize as raw object var secret = JsonSerializer.Serialize((object)p.Secret); @@ -133,7 +161,8 @@ public static bool VerifySigAllWitness( Proof[] proofs, BlindedMessage[] blindedMessages, P2PKWitness witness, - string? meltQuoteId = null) + string? meltQuoteId = null + ) { if (proofs is null || proofs.Length == 0) { @@ -148,30 +177,37 @@ public static bool VerifySigAllWitness( msg = Encoding.UTF8.GetBytes(msgStr); } - catch(Exception ex) + catch (Exception ex) { return false; } - + if (proofs[0].Secret is not Nut10Secret nut10) return false; - + return nut10.ProofSecret switch { HTLCProofSecret htlcs => htlcs.VerifyWitness(msg, witness), P2PKProofSecret p2pks => p2pks.VerifyWitness(msg, witness), - _ => false + _ => false, }; } - public static bool VerifySigAllWitness(Proof[] proofs, BlindedMessage[] blindedMessages, string? meltQuoteId = null) + public static bool VerifySigAllWitness( + Proof[] proofs, + BlindedMessage[] blindedMessages, + string? meltQuoteId = null + ) { if (proofs is null || proofs.Length == 0) { return false; } var firstProof = proofs.FirstOrDefault(); - if (firstProof?.Secret is not Nut10Secret { ProofSecret: var proofSecret } || firstProof.Witness is null) + if ( + firstProof?.Secret is not Nut10Secret { ProofSecret: var proofSecret } + || firstProof.Witness is null + ) return false; P2PKWitness? witness; @@ -191,13 +227,14 @@ public static bool VerifySigAllWitness(Proof[] proofs, BlindedMessage[] blindedM { return false; } - return witness is not null && VerifySigAllWitness(proofs, blindedMessages, witness, meltQuoteId); + return witness is not null + && VerifySigAllWitness(proofs, blindedMessages, witness, meltQuoteId); } - + private static bool ValidateFirstProof(Proof firstProof, out Nut10ProofSecret? secret) { secret = null; - + if (firstProof.Secret is not Nut10Secret nut10) { return false; @@ -206,11 +243,11 @@ private static bool ValidateFirstProof(Proof firstProof, out Nut10ProofSecret? s var builder = nut10.ProofSecret switch { HTLCProofSecret htlcs => HTLCBuilder.Load(htlcs), - P2PKProofSecret p2pks => P2PKBuilder.Load(p2pks), + P2PKProofSecret p2pks => P2PkBuilder.Load(p2pks), // won't throw exception if there will be a new type of nut10 secret, but will return false - _ => new P2PKBuilder(){SigFlag = null} + _ => new P2PkBuilder() { SigFlag = null }, }; - + if (builder.SigFlag != "SIG_ALL") { return false; @@ -219,4 +256,18 @@ private static bool ValidateFirstProof(Proof firstProof, out Nut10ProofSecret? s secret = nut10.ProofSecret; return true; } -} \ No newline at end of file + + private static bool CheckIfEqualToFirst(Nut10ProofSecret first, Nut10ProofSecret other) => + first is { } a + && other is { } b + && a.Data == b.Data + && ( + (a.Tags == null && b.Tags == null) + || ( + a.Tags != null + && b.Tags != null + && a.Tags.Length == b.Tags.Length + && a.Tags.Zip(b.Tags).All(pair => pair.First.SequenceEqual(pair.Second)) + ) + ); +} diff --git a/DotNut/NUT12/DLEQ.cs b/DotNut/NUT12/DLEQ.cs new file mode 100644 index 0000000..0039a92 --- /dev/null +++ b/DotNut/NUT12/DLEQ.cs @@ -0,0 +1,12 @@ +using System.Text.Json.Serialization; + +namespace DotNut; + +public class DLEQ +{ + [JsonPropertyName("e")] + public PrivKey E { get; set; } + + [JsonPropertyName("s")] + public PrivKey S { get; set; } +} diff --git a/DotNut/NUT12/DLEQProof.cs b/DotNut/NUT12/DLEQProof.cs new file mode 100644 index 0000000..5936d5b --- /dev/null +++ b/DotNut/NUT12/DLEQProof.cs @@ -0,0 +1,9 @@ +using System.Text.Json.Serialization; + +namespace DotNut; + +public class DLEQProof : DLEQ +{ + [JsonPropertyName("r")] + public PrivKey R { get; set; } +} diff --git a/DotNut/NUT13/BIP32.cs b/DotNut/NUT13/BIP32.cs index dea15f5..1d9ae64 100644 --- a/DotNut/NUT13/BIP32.cs +++ b/DotNut/NUT13/BIP32.cs @@ -8,15 +8,12 @@ namespace DotNut.NUT13; public class BIP32 : IHdKeyAlgo { - public static readonly IHdKeyAlgo Instance = new BIP32(); private static readonly byte[] CurveBytes = "Bitcoin seed"u8.ToArray(); private static readonly BigInteger N = Cashu.N; - private BIP32() - { - } + private BIP32() { } public HdKey GetMasterKeyFromSeed(ReadOnlySpan seed) { @@ -26,7 +23,8 @@ public HdKey GetMasterKeyFromSeed(ReadOnlySpan seed) HMACSHA512.HashData(CurveBytes, seedCopy, seedCopy); var key = seedCopy[..32]; var keyInt = new BigInteger(key, true, true); - if (keyInt > N || keyInt.IsZero) continue; + if (keyInt > N || keyInt.IsZero) + continue; return new HdKey(key, seedCopy[32..]); } } @@ -36,15 +34,15 @@ public HdKey Derive(HdKey parent, KeyPathElement index) Span hash = index.Hardened ? IHdKeyAlgo.Bip32Hash(parent.ChainCode, index, 0x00, parent.PrivateKey) : IHdKeyAlgo.Bip32Hash(parent.ChainCode, index, GetPublic(parent.PrivateKey)); - - var parentKey = new BigInteger (parent.PrivateKey, true, true); + + var parentKey = new BigInteger(parent.PrivateKey, true, true); while (true) { var key = hash[..32]; var cc = hash[32..]; key.Reverse(); - var keyInt = new BigInteger (key, true); + var keyInt = new BigInteger(key, true); var res = BigInteger.Add(keyInt, parentKey) % N; if (keyInt > N || res.IsZero) @@ -66,6 +64,6 @@ public HdKey Derive(HdKey parent, KeyPathElement index) public byte[] GetPublic(ReadOnlySpan privateKey) { - return ECPrivKey.Create(privateKey).CreatePubKey().ToBytes(); + return ECPrivKey.Create(privateKey).CreatePubKey().ToBytes(); } -} \ No newline at end of file +} diff --git a/DotNut/NUT13/Nut13.cs b/DotNut/NUT13/Nut13.cs index c9f3a49..4dd7b6f 100644 --- a/DotNut/NUT13/Nut13.cs +++ b/DotNut/NUT13/Nut13.cs @@ -1,4 +1,5 @@ -using System.Security.Cryptography; +using System.Security.Cryptography; +using DotNut.Abstractions; using DotNut.NBitcoin.BIP39; using NBip32Fast; @@ -6,21 +7,63 @@ namespace DotNut.NUT13; public static class Nut13 { - public static byte[] DeriveBlindingFactor(this Mnemonic mnemonic, KeysetId keysetId, uint counter) => - DeriveBlindingFactor(mnemonic.DeriveSeed(), keysetId, counter); - - - public static StringSecret DeriveSecret(this Mnemonic mnemonic, KeysetId keysetId, uint counter) => - DeriveSecret(mnemonic.DeriveSeed(), keysetId, counter); - - + public static byte[] DeriveBlindingFactor( + this Mnemonic mnemonic, + KeysetId keysetId, + uint counter + ) => DeriveBlindingFactor(mnemonic.DeriveSeed(), keysetId, counter); + + public static StringSecret DeriveSecret( + this Mnemonic mnemonic, + KeysetId keysetId, + uint counter + ) => DeriveSecret(mnemonic.DeriveSeed(), keysetId, counter); + + public static List DeriveOutputs( + this Mnemonic mnemonic, + IEnumerable amounts, + KeysetId keysetId, + uint counter + ) + { + var outputs = new List(); + + var amountList = amounts.ToList(); + + for (uint i = 0; i < amountList.Count; i++) + { + var secret = DeriveSecret(mnemonic, keysetId, counter + i); + var r = new PrivKey(DeriveBlindingFactor(mnemonic, keysetId, counter + i)); + + var Y = secret.ToCurve(); + var B_ = Cashu.ComputeB_(Y, r); + + outputs.Add( + new OutputData() + { + BlindedMessage = new BlindedMessage() + { + Amount = amountList[(int)i], + Id = keysetId, + B_ = B_, + }, + Secret = secret, + BlindingFactor = r, + } + ); + } + + return outputs; + } + public static byte[] DeriveBlindingFactor(this byte[] seed, KeysetId keysetId, uint counter) { switch (keysetId.GetVersion()) { case 0x00: - return BIP32.Instance.DerivePath(GetNut13DerivationPath(keysetId, counter, false), seed).PrivateKey - .ToArray(); + return BIP32 + .Instance.DerivePath(GetNut13DerivationPath(keysetId, counter, false), seed) + .PrivateKey.ToArray(); case 0x01: { return DeriveHmac(seed, keysetId, counter, false); @@ -29,12 +72,15 @@ public static byte[] DeriveBlindingFactor(this byte[] seed, KeysetId keysetId, u throw new ArgumentException("Invalid keyset id prefix"); } } + public static StringSecret DeriveSecret(this byte[] seed, KeysetId keysetId, uint counter) { switch (keysetId.GetVersion()) { case 0x00: - var key = BIP32.Instance.DerivePath(GetNut13DerivationPath(keysetId, counter, true), seed).PrivateKey; + var key = BIP32 + .Instance.DerivePath(GetNut13DerivationPath(keysetId, counter, true), seed) + .PrivateKey; return new StringSecret(Convert.ToHexString(key).ToLower()); case 0x01: { @@ -44,9 +90,8 @@ public static StringSecret DeriveSecret(this byte[] seed, KeysetId keysetId, uin default: throw new ArgumentException("Invalid keyset id prefix"); } - } - + public static byte[] DeriveHmac(byte[] seed, KeysetId keysetId, uint counter, bool secretOrr) { byte[] counterBuffer = BitConverter.GetBytes((ulong)counter); @@ -54,26 +99,31 @@ public static byte[] DeriveHmac(byte[] seed, KeysetId keysetId, uint counter, bo { Array.Reverse(counterBuffer); } - - var message = "Cashu_KDF_HMAC_SHA256"u8.ToArray() + + var message = "Cashu_KDF_HMAC_SHA256"u8 + .ToArray() .Concat(Convert.FromHexString(keysetId.ToString())) .Concat(counterBuffer) .Append(secretOrr ? (byte)0x00 : (byte)0x01); - + using var hmac = new HMACSHA256(seed); return hmac.ComputeHash(message.ToArray()); } - + public const string Purpose = "129372'"; + public static KeyPath GetNut13DerivationPath(KeysetId keysetId, uint counter, bool secretOrr) - { - return (KeyPath) KeyPath.Parse($"m/{Purpose}/0'/{GetKeysetIdInt(keysetId)}'/{counter}'/{(secretOrr?0:1)}")!; + { + return (KeyPath) + KeyPath.Parse( + $"m/{Purpose}/0'/{GetKeysetIdInt(keysetId)}'/{counter}'/{(secretOrr ? 0 : 1)}" + )!; } - + public static long GetKeysetIdInt(KeysetId keysetId) { var keysetIdInt = long.Parse("0" + keysetId, System.Globalization.NumberStyles.HexNumber); var mod = (long)Math.Pow(2, 31) - 1; return keysetIdInt % mod; } -} \ No newline at end of file +} diff --git a/DotNut/NUT14/HTLCBuilder.cs b/DotNut/NUT14/HTLCBuilder.cs new file mode 100644 index 0000000..b976331 --- /dev/null +++ b/DotNut/NUT14/HTLCBuilder.cs @@ -0,0 +1,115 @@ +using System.Security.Cryptography; +using NBitcoin.Secp256k1; + +namespace DotNut; + +public class HTLCBuilder : P2PkBuilder +{ + public string HashLock { get; set; } + + /* + * ugly hack to reuse P2PkBuilder for HTLCs. + * P2PkBuilder expects a pubkey in `data` field, but we need to store a hashlock instead + * + * we inject a dummy pubkey so the loader doesn’t break, then remove it after load/build. + */ + private static readonly PubKey _dummy = + "020000000000000000000000000000000000000000000000000000000000000001".ToPubKey(); + + public static HTLCBuilder Load(HTLCProofSecret proofSecret) + { + var hashLock = proofSecret.Data; + if (hashLock.Length != 64) // hex string + { + throw new ArgumentException( + "HashLock must be 32 bytes (64 chars hex)", + nameof(HashLock) + ); + } + var tempProof = new P2PKProofSecret + { + Data = _dummy.ToString(), + Nonce = proofSecret.Nonce, + Tags = proofSecret.Tags, + }; + + var innerBuilder = P2PkBuilder.Load(tempProof); + innerBuilder.Pubkeys = innerBuilder.Pubkeys.Except([_dummy.Key]).ToArray(); + return new HTLCBuilder() + { + HashLock = hashLock, + Lock = innerBuilder.Lock, + Pubkeys = innerBuilder.Pubkeys, + RefundPubkeys = innerBuilder.RefundPubkeys, + SignatureThreshold = innerBuilder.SignatureThreshold, + SigFlag = innerBuilder.SigFlag, + Nonce = innerBuilder.Nonce, + }; + } + + public new HTLCProofSecret Build() + { + if (HashLock.Length != 64) + { + throw new ArgumentException( + "HashLock must be 32 bytes (64 chars hex)", + nameof(HashLock) + ); + } + var innerBuilder = new P2PkBuilder() + { + Lock = Lock, + Pubkeys = Pubkeys.ToArray(), + RefundPubkeys = RefundPubkeys, + SignatureThreshold = SignatureThreshold, + SigFlag = SigFlag, + Nonce = Nonce, + }; + innerBuilder.Pubkeys = innerBuilder.Pubkeys.Prepend(_dummy.Key).ToArray(); + + var p2pkProof = innerBuilder.Build(); + return new HTLCProofSecret() + { + Data = HashLock, + Nonce = p2pkProof.Nonce, + Tags = p2pkProof.Tags, + }; + } + + public new HTLCProofSecret BuildBlinded(out ECPubKey p2pkE) + { + var e = new PrivKey(RandomNumberGenerator.GetHexString(64)); + p2pkE = e.Key.CreatePubKey(); + return BuildBlinded(e); + } + + public new HTLCProofSecret BuildBlinded(ECPrivKey p2pke) + { + var pubkeys = RefundPubkeys != null ? Pubkeys.Concat(RefundPubkeys).ToArray() : Pubkeys; + var rs = new List(); + + for (int i = 0; i < pubkeys.Length; i++) + { + var Zx = Cashu.ComputeZx(p2pke, pubkeys[i]); + var Ri = Cashu.ComputeRi(Zx, i); + rs.Add(Ri); + } + BlindPubkeys(rs.ToArray()); + return Build(); + } + + public override HTLCBuilder Clone() + { + return new HTLCBuilder + { + HashLock = HashLock, + Lock = Lock, + RefundPubkeys = RefundPubkeys?.ToArray(), + SignatureThreshold = SignatureThreshold, + RefundSignatureThreshold = RefundSignatureThreshold, + Pubkeys = Pubkeys.ToArray(), + SigFlag = SigFlag, + Nonce = Nonce, + }; + } +} diff --git a/DotNut/NUT14/HTLCProofSecret.cs b/DotNut/NUT14/HTLCProofSecret.cs new file mode 100644 index 0000000..8bcdade --- /dev/null +++ b/DotNut/NUT14/HTLCProofSecret.cs @@ -0,0 +1,282 @@ +using System.Text; +using System.Text.Json.Serialization; +using NBitcoin.Secp256k1; +using SHA256 = System.Security.Cryptography.SHA256; + +namespace DotNut; + +public class HTLCProofSecret : P2PKProofSecret +{ + public const string Key = "HTLC"; + + [JsonIgnore] + public override HTLCBuilder Builder => HTLCBuilder.Load(this); + + public override ECPubKey[] GetAllowedPubkeys(out int requiredSignatures) + { + var builder = Builder; + // Pure hashlock (no pubkeys) - signatures are not required + requiredSignatures = builder.Pubkeys.Length == 0 ? 0 : builder.SignatureThreshold; + return builder.Pubkeys; + } + + public HTLCWitness? GenerateWitness(Proof proof, ECPrivKey[] keys, string preimage) + { + return GenerateWitness(proof.Secret.GetBytes(), keys, Convert.FromHexString(preimage)); + } + + public HTLCWitness? GenerateWitness( + BlindedMessage blindedMessage, + ECPrivKey[] keys, + string preimage + ) + { + return GenerateWitness( + blindedMessage.B_.Key.ToBytes(), + keys, + Convert.FromHexString(preimage) + ); + } + + public HTLCWitness? GenerateWitness(byte[] msg, ECPrivKey[] keys, byte[] preimage) + { + var hash = SHA256.HashData(msg); + return GenerateWitness(ECPrivKey.Create(hash), keys, preimage); + } + + public HTLCWitness? GenerateWitness(ECPrivKey hash, ECPrivKey[] keys, byte[] preimage) + { + var builder = Builder; + // validate preimage only if timelock hasn't expired + if ( + !builder.Lock.HasValue + || builder.Lock.Value.ToUnixTimeSeconds() > DateTimeOffset.Now.ToUnixTimeSeconds() + ) + { + if (!VerifyPreimage(preimage)) + throw new InvalidOperationException("Invalid preimage"); + } + var witness = base.GenerateWitness(hash, keys); + if (witness is null) + return null; // freely spendable (timelock expired, no refund keys) + return new HTLCWitness() + { + Signatures = witness.Signatures, + Preimage = Convert.ToHexString(preimage), + }; + } + + public HTLCWitness? GenerateBlindWitness(Proof proof, ECPrivKey[] keys, string preimage) + { + ArgumentNullException.ThrowIfNull(proof.P2PkE); + return GenerateBlindWitness(proof, keys, preimage, proof.P2PkE); + } + + public HTLCWitness? GenerateBlindWitness( + Proof proof, + ECPrivKey[] keys, + string preimage, + ECPubKey P2PkE + ) + { + return GenerateBlindWitness( + proof.Secret.GetBytes(), + keys, + Convert.FromHexString(preimage), + P2PkE + ); + } + + public HTLCWitness? GenerateBlindWitness( + BlindedMessage message, + ECPrivKey[] keys, + string preimage, + ECPubKey P2PkE + ) + { + return GenerateBlindWitness( + message.B_.Key.ToBytes(), + keys, + Convert.FromHexString(preimage), + P2PkE + ); + } + + public HTLCWitness? GenerateBlindWitness( + byte[] msg, + ECPrivKey[] keys, + byte[] preimage, + ECPubKey P2PkE + ) + { + var hash = SHA256.HashData(msg); + return GenerateBlindWitness(ECPrivKey.Create(hash), keys, preimage, P2PkE); + } + + public HTLCWitness? GenerateBlindWitness( + ECPrivKey hash, + ECPrivKey[] keys, + byte[] preimage, + ECPubKey P2PkE + ) + { + var builder = Builder; + // validate preimage only if timelock hasn't expired + if ( + !builder.Lock.HasValue + || builder.Lock.Value.ToUnixTimeSeconds() > DateTimeOffset.Now.ToUnixTimeSeconds() + ) + { + if (!VerifyPreimage(preimage)) + throw new InvalidOperationException("Invalid preimage"); + } + var witness = base.GenerateBlindWitness(hash, keys, P2PkE); + if (witness is null) + return null; // freely spendable (timelock expired, no refund keys) + return new HTLCWitness() + { + Signatures = witness.Signatures, + Preimage = Convert.ToHexString(preimage), + }; + } + + public bool VerifyPreimage(string preimage) + { + return Convert + .FromHexString(Builder.HashLock) + .SequenceEqual(SHA256.HashData(Convert.FromHexString(preimage))); + } + + public bool VerifyPreimage(byte[] preimage) + { + return Convert.FromHexString(Builder.HashLock).SequenceEqual(SHA256.HashData(preimage)); + } + + public bool VerifyWitness(string message, HTLCWitness witness) + { + var hash = SHA256.HashData(Encoding.UTF8.GetBytes(message)); + return VerifyWitnessHash(hash, witness); + } + + public bool VerifyWitness(ISecret secret, HTLCWitness witness) + { + if (secret is not Nut10Secret { ProofSecret: HTLCProofSecret }) + { + return false; + } + + return VerifyWitness(secret.GetBytes(), witness); + } + + [Obsolete("Use GenerateWitness(Proof proof, ECPrivKey[] keys, string preimage)")] + public override P2PKWitness GenerateWitness(Proof proof, ECPrivKey[] keys) + { + throw new InvalidOperationException( + "Use GenerateWitness(Proof proof, ECPrivKey[] keys, string preimage)" + ); + } + + [Obsolete("Use GenerateWitness(byte[] msg, ECPrivKey[] keys, byte[] preimage)")] + public override P2PKWitness GenerateWitness(BlindedMessage message, ECPrivKey[] keys) + { + throw new InvalidOperationException( + "Use GenerateWitness(BlindedMessage message, ECPrivKey[] keys, string preimage)" + ); + } + + [Obsolete("Use GenerateWitness(byte[] msg, ECPrivKey[] keys, byte[] preimage)")] + public override P2PKWitness GenerateWitness(byte[] msg, ECPrivKey[] keys) + { + throw new InvalidOperationException( + "Use GenerateWitness(byte[] msg, ECPrivKey[] keys, byte[] preimage)" + ); + } + + [Obsolete("Use GenerateBlindWitness(Proof proof, ECPrivKey[] keys, string preimage)")] + public override P2PKWitness GenerateBlindWitness(Proof proof, ECPrivKey[] keys) + { + throw new InvalidOperationException( + "Use GenerateBlindWitness(Proof proof, ECPrivKey[] keys, string preimage)" + ); + } + + [Obsolete("Use GenerateBlindWitness(Proof proof, ECPrivKey[] keys, string preimage, ECPubKey P2PkE)")] + public override P2PKWitness GenerateBlindWitness(Proof proof, ECPrivKey[] keys, ECPubKey P2PkE) + { + throw new InvalidOperationException( + "Use GenerateBlindWitness(Proof proof, ECPrivKey[] keys, string preimage, ECPubKey P2PkE)" + ); + } + + [Obsolete("Use GenerateBlindWitness(BlindedMessage message, ECPrivKey[] keys, string preimage, ECPubKey P2PkE)")] + public override P2PKWitness GenerateBlindWitness( + BlindedMessage message, + ECPrivKey[] keys, + ECPubKey P2PkE + ) + { + throw new InvalidOperationException( + "Use GenerateBlindWitness(BlindedMessage message, ECPrivKey[] keys, string preimage, ECPubKey P2PkE)" + ); + } + + [Obsolete("Use GenerateBlindWitness(byte[] msg, ECPrivKey[] keys, byte[] preimage, ECPubKey P2PkE)")] + public override P2PKWitness GenerateBlindWitness(byte[] msg, ECPrivKey[] keys, ECPubKey P2PkE) + { + throw new InvalidOperationException( + "Use GenerateBlindWitness(byte[] msg, ECPrivKey[] keys, byte[] preimage, ECPubKey P2PkE)" + ); + } + + [Obsolete("Use GenerateBlindWitness(ECPrivKey hash, ECPrivKey[] keys, byte[] preimage, ECPubKey P2PkE)")] + public override P2PKWitness GenerateBlindWitness( + ECPrivKey hash, + ECPrivKey[] keys, + ECPubKey P2PkE + ) + { + throw new InvalidOperationException( + "Use GenerateBlindWitness(ECPrivKey hash, ECPrivKey[] keys, byte[] preimage, ECPubKey P2PkE)" + ); + } + + [Obsolete("Use GenerateWitness(ECPrivKey hash, ECPrivKey[] keys, byte[] preimage)")] + public override P2PKWitness GenerateWitness(ECPrivKey hash, ECPrivKey[] keys) + { + throw new InvalidOperationException( + "Use GenerateWitness(ECPrivKey hash, ECPrivKey[] keys, byte[] preimage)" + ); + } + + public override bool VerifyWitness(string message, P2PKWitness witness) + { + return base.VerifyWitness(message, witness); + } + + public override bool VerifyWitness(ISecret secret, P2PKWitness witness) + { + return base.VerifyWitness(secret, witness); + } + + public override bool VerifyWitness(byte[] message, P2PKWitness witness) + { + return base.VerifyWitness(message, witness); + } + + public override bool VerifyWitnessHash(byte[] hash, P2PKWitness witness) + { + if (witness is not HTLCWitness htlcWitness) + { + return false; + } + var builder = Builder; + if ( + builder.Lock.HasValue + && builder.Lock.Value.ToUnixTimeSeconds() <= DateTimeOffset.Now.ToUnixTimeSeconds() + ) + { + return base.VerifyWitnessHash(hash, witness); + } + return VerifyPreimage(htlcWitness.Preimage) && base.VerifyWitnessHash(hash, witness); + } +} diff --git a/DotNut/HTLCWitness.cs b/DotNut/NUT14/HTLCWitness.cs similarity index 65% rename from DotNut/HTLCWitness.cs rename to DotNut/NUT14/HTLCWitness.cs index 46fb6cd..63e6ae5 100644 --- a/DotNut/HTLCWitness.cs +++ b/DotNut/NUT14/HTLCWitness.cs @@ -2,10 +2,10 @@ namespace DotNut; -public class HTLCWitness: P2PKWitness +public class HTLCWitness : P2PKWitness { // this field is nullable now, because after locktime expiry only signatures are needed. [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - [JsonPropertyName("preimage")] - public string? Preimage { get; set; } -} \ No newline at end of file + [JsonPropertyName("preimage")] + public string? Preimage { get; set; } +} diff --git a/DotNut/NUT15/MultipathPaymentSetting.cs b/DotNut/NUT15/MultipathPaymentSetting.cs new file mode 100644 index 0000000..7530421 --- /dev/null +++ b/DotNut/NUT15/MultipathPaymentSetting.cs @@ -0,0 +1,15 @@ +using System.Text.Json.Serialization; + +namespace DotNut; + +public class MultipathPaymentSetting +{ + [JsonPropertyName("method")] + public string Method { get; set; } + + [JsonPropertyName("unit")] + public List Unit { get; set; } + + [JsonPropertyName("mpp")] + public bool MultiPathPayments { get; set; } +} diff --git a/DotNut/HttpPaymentRequestInterfaceHandler.cs b/DotNut/NUT18/HttpPaymentRequestInterfaceHandler.cs similarity index 75% rename from DotNut/HttpPaymentRequestInterfaceHandler.cs rename to DotNut/NUT18/HttpPaymentRequestInterfaceHandler.cs index 2dafc54..435f27d 100644 --- a/DotNut/HttpPaymentRequestInterfaceHandler.cs +++ b/DotNut/NUT18/HttpPaymentRequestInterfaceHandler.cs @@ -10,16 +10,20 @@ public HttpPaymentRequestInterfaceHandler(HttpClient? httpClient) { _httpClient = httpClient ?? new HttpClient(); } + public bool CanHandle(PaymentRequest request) { return request.Transports.Any(t => t.Type == "post"); } - public async Task SendPayment(PaymentRequest request, PaymentRequestPayload payload, - CancellationToken cancellationToken = default) - { + public async Task SendPayment( + PaymentRequest request, + PaymentRequestPayload payload, + CancellationToken cancellationToken = default + ) + { var endpoint = new Uri(request.Transports.First(t => t.Type == "post").Target); - var response = await _httpClient.PostAsJsonAsync(endpoint, payload,cancellationToken); + var response = await _httpClient.PostAsJsonAsync(endpoint, payload, cancellationToken); response.EnsureSuccessStatusCode(); } -} \ No newline at end of file +} diff --git a/DotNut/Nut10LockingCondition.cs b/DotNut/NUT18/Nut10LockingCondition.cs similarity index 98% rename from DotNut/Nut10LockingCondition.cs rename to DotNut/NUT18/Nut10LockingCondition.cs index ba89386..70d52d4 100644 --- a/DotNut/Nut10LockingCondition.cs +++ b/DotNut/NUT18/Nut10LockingCondition.cs @@ -5,4 +5,4 @@ public class Nut10LockingCondition public string Kind { get; set; } public string Data { get; set; } public Tag[]? Tags { get; set; } -} \ No newline at end of file +} diff --git a/DotNut/PaymentRequest.cs b/DotNut/NUT18/PaymentRequest.cs similarity index 99% rename from DotNut/PaymentRequest.cs rename to DotNut/NUT18/PaymentRequest.cs index 2f9af49..1df8094 100644 --- a/DotNut/PaymentRequest.cs +++ b/DotNut/NUT18/PaymentRequest.cs @@ -39,4 +39,4 @@ public static PaymentRequest Parse(string creq) throw new FormatException("Invalid payment request"); } -} \ No newline at end of file +} diff --git a/DotNut/NUT18/PaymentRequestInterfaceHandler.cs b/DotNut/NUT18/PaymentRequestInterfaceHandler.cs new file mode 100644 index 0000000..4a44e9a --- /dev/null +++ b/DotNut/NUT18/PaymentRequestInterfaceHandler.cs @@ -0,0 +1,11 @@ +namespace DotNut; + +public interface PaymentRequestInterfaceHandler +{ + bool CanHandle(PaymentRequest request); + Task SendPayment( + PaymentRequest request, + PaymentRequestPayload payload, + CancellationToken cancellationToken = default + ); +} diff --git a/DotNut/NUT18/PaymentRequestPayload.cs b/DotNut/NUT18/PaymentRequestPayload.cs new file mode 100644 index 0000000..b8747d4 --- /dev/null +++ b/DotNut/NUT18/PaymentRequestPayload.cs @@ -0,0 +1,22 @@ +using System.Text.Json.Serialization; + +namespace DotNut; + +public class PaymentRequestPayload +{ + [JsonPropertyName("id")] + public string PaymentId { get; set; } + + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("memo")] + public string? Memo { get; set; } + + [JsonPropertyName("mint")] + public string Mint { get; set; } + + [JsonPropertyName("unit")] + public string Unit { get; set; } + + [JsonPropertyName("proofs")] + public Proof[] Proofs { get; set; } +} diff --git a/DotNut/PaymentRequestTransport.cs b/DotNut/NUT18/PaymentRequestTransport.cs similarity index 98% rename from DotNut/PaymentRequestTransport.cs rename to DotNut/NUT18/PaymentRequestTransport.cs index d1c3475..822ba35 100644 --- a/DotNut/PaymentRequestTransport.cs +++ b/DotNut/NUT18/PaymentRequestTransport.cs @@ -5,4 +5,4 @@ public class PaymentRequestTransport public string Type { get; set; } public string Target { get; set; } public Tag[]? Tags { get; set; } -} \ No newline at end of file +} diff --git a/DotNut/PaymentRequestTransportInitiator.cs b/DotNut/NUT18/PaymentRequestTransportInitiator.cs similarity index 86% rename from DotNut/PaymentRequestTransportInitiator.cs rename to DotNut/NUT18/PaymentRequestTransportInitiator.cs index a4ab7ad..259bd5a 100644 --- a/DotNut/PaymentRequestTransportInitiator.cs +++ b/DotNut/NUT18/PaymentRequestTransportInitiator.cs @@ -5,7 +5,9 @@ namespace DotNut; public class PaymentRequestTransportInitiator { private readonly IEnumerable _handlers; - public static ConcurrentBag Handlers { get; } = [ new HttpPaymentRequestInterfaceHandler(null) ]; + public static ConcurrentBag Handlers { get; } = + [new HttpPaymentRequestInterfaceHandler(null)]; + public PaymentRequestTransportInitiator(IEnumerable handlers) { _handlers = handlers; @@ -15,4 +17,4 @@ public PaymentRequestTransportInitiator() { _handlers = Handlers.ToArray(); } -} \ No newline at end of file +} diff --git a/DotNut/Tag.cs b/DotNut/NUT18/Tag.cs similarity index 91% rename from DotNut/Tag.cs rename to DotNut/NUT18/Tag.cs index 3a86dbd..c182fb7 100644 --- a/DotNut/Tag.cs +++ b/DotNut/NUT18/Tag.cs @@ -4,7 +4,7 @@ public class Tag { public string Key { get; set; } public List Value { get; set; } - + public Tag(string[] tag) { if (tag == null || tag.Length == 0) @@ -17,6 +17,6 @@ public Tag(string[] tag) public string[] ToArray() { - return [Key, ..Value]; + return [Key, .. Value]; } -} \ No newline at end of file +} diff --git a/DotNut/NUT20/MintQuoteSigner.cs b/DotNut/NUT20/MintQuoteSigner.cs new file mode 100644 index 0000000..693a717 --- /dev/null +++ b/DotNut/NUT20/MintQuoteSigner.cs @@ -0,0 +1,24 @@ +using System.Text; +using SHA256 = System.Security.Cryptography.SHA256; + +namespace DotNut; + +public static class MintQuoteSigner +{ + public static string SignMintQuote( + this PrivKey pk, + string quote, + List blindedMessages + ) + { + var sb = new StringBuilder(); + sb.Append(quote); + foreach (var blindedMessage in blindedMessages) + { + sb.Append(blindedMessage.B_); + } + var bytes = Encoding.UTF8.GetBytes(sb.ToString()); + var hash = SHA256.HashData(bytes); + return pk.Key.SignBIP340(hash).ToHex(); + } +} diff --git a/DotNut/P2PKWitness.cs b/DotNut/P2PKWitness.cs deleted file mode 100644 index 49185a2..0000000 --- a/DotNut/P2PKWitness.cs +++ /dev/null @@ -1,8 +0,0 @@ -using System.Text.Json.Serialization; - -namespace DotNut; - -public class P2PKWitness -{ - [JsonPropertyName("signatures")] public string[] Signatures { get; set; } = Array.Empty(); -} \ No newline at end of file diff --git a/DotNut/PaymentRequestInterfaceHandler.cs b/DotNut/PaymentRequestInterfaceHandler.cs deleted file mode 100644 index 27ed535..0000000 --- a/DotNut/PaymentRequestInterfaceHandler.cs +++ /dev/null @@ -1,7 +0,0 @@ -namespace DotNut; - -public interface PaymentRequestInterfaceHandler -{ - bool CanHandle(PaymentRequest request); - Task SendPayment(PaymentRequest request, PaymentRequestPayload payload, CancellationToken cancellationToken = default); -} \ No newline at end of file diff --git a/DotNut/PaymentRequestPayload.cs b/DotNut/PaymentRequestPayload.cs deleted file mode 100644 index 4fe46fc..0000000 --- a/DotNut/PaymentRequestPayload.cs +++ /dev/null @@ -1,13 +0,0 @@ -using System.Text.Json.Serialization; - -namespace DotNut; - -public class PaymentRequestPayload -{ - [JsonPropertyName("id")] public string PaymentId { get; set; } - [JsonPropertyName("memo")] public string? Memo { get; set; } - [JsonPropertyName("mint")] public string Mint { get; set; } - [JsonPropertyName("unit")] public string Unit { get; set; } - - [JsonPropertyName("proofs")] public Proof[] Proofs { get; set; } -} \ No newline at end of file diff --git a/DotNut/PrivKey.cs b/DotNut/PrivKey.cs index 0b6861e..adcc981 100644 --- a/DotNut/PrivKey.cs +++ b/DotNut/PrivKey.cs @@ -7,13 +7,19 @@ namespace DotNut; [JsonConverter(typeof(PrivKeyJsonConverter))] public class PrivKey { - [JsonIgnore(Condition = JsonIgnoreCondition.Always)] public readonly ECPrivKey Key; + [JsonIgnore(Condition = JsonIgnoreCondition.Always)] + public readonly ECPrivKey Key; public PrivKey(string hex) { Key = hex.ToPrivKey(); } + public PrivKey(byte[] bytes) + { + Key = Convert.ToHexString(bytes).ToPrivKey(); + } + private PrivKey(ECPrivKey ecPrivKey) { Key = ecPrivKey; @@ -23,7 +29,7 @@ public override string ToString() { return Convert.ToHexString(Key.ToBytes()).ToLower(); } - + public static implicit operator PrivKey(ECPrivKey ecPubKey) { return new PrivKey(ecPubKey); @@ -33,4 +39,4 @@ public static implicit operator ECPrivKey(PrivKey privKey) { return privKey.Key; } -} \ No newline at end of file +} diff --git a/DotNut/PubKey.cs b/DotNut/PubKey.cs index 0868b9d..4c1ca6c 100644 --- a/DotNut/PubKey.cs +++ b/DotNut/PubKey.cs @@ -7,7 +7,8 @@ namespace DotNut; [JsonConverter(typeof(PubKeyJsonConverter))] public class PubKey { - [JsonIgnore(Condition = JsonIgnoreCondition.Always)] public readonly ECPubKey Key; + [JsonIgnore(Condition = JsonIgnoreCondition.Always)] + public readonly ECPubKey Key; public PubKey(string hex, bool onlyAllowCompressed = false) { @@ -27,7 +28,7 @@ public override string ToString() { return Convert.ToHexString(Key.ToBytes()).ToLower(); } - + public static implicit operator PubKey(ECPubKey ecPubKey) { return new PubKey(ecPubKey); @@ -37,17 +38,18 @@ public static implicit operator ECPubKey(PubKey pubKey) { return pubKey.Key; } - + public override bool Equals(object? obj) { - if (ReferenceEquals(this, obj)) return true; - if (obj is not PubKey other) return false; + if (ReferenceEquals(this, obj)) + return true; + if (obj is not PubKey other) + return false; return this.Key == other.Key; } - + public override int GetHashCode() { return Key.GetHashCode(); } - -} \ No newline at end of file +} diff --git a/README.md b/README.md index a5ebc14..3b99e29 100644 --- a/README.md +++ b/README.md @@ -1,231 +1,187 @@ -# DotNut 🥜 +# DotNut -A complete C# implementation of the [Cashu protocol](https://cashu.space) - privacy-preserving electronic cash built on Bitcoin. +C# library for the [Cashu protocol](https://cashu.space) - a Chaumian e-cash system built on Bitcoin/Lightning. [![NuGet](https://img.shields.io/nuget/v/DotNut.svg)](https://www.nuget.org/packages/DotNut/) [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) -## What is Cashu? - -Cashu is a free and open-source Chaumian e-cash system built for Bitcoin. It offers near-perfect privacy for users and can serve as an excellent custodial scaling solution. DotNut provides a full-featured C# client library for interacting with Cashu mints. - ## Installation ```bash dotnet add package DotNut ``` -## Quick Start +## Usage -### 1. Connect to a Mint +The main entry point is the `Wallet` class, which exposes a fluent builder for connecting to a mint and performing operations. -```csharp -using DotNut; -using DotNut.Api; +### Setup -// Connect to a Cashu mint -var httpClient = new HttpClient(); -httpClient.BaseAddress = new Uri("https://testnut.cashu.space/"); -var client = new CashuHttpClient(httpClient); - -// Get mint information -var info = await client.GetInfo(); -Console.WriteLine($"Connected to: {info.Name}"); +```csharp +var wallet = Wallet.Create() + .WithMint("https://testnut.cashu.space") + .WithMnemonic("your twelve word mnemonic phrase here...") + .WithCounter(new InMemoryCounter()); ``` -### 2. Create and Send Tokens +### Mint (Lightning → tokens) ```csharp -using DotNut.Encoding; - -// Create a token from proofs (obtained from minting) -var token = new CashuToken -{ - Unit = "sat", - Memo = "Payment for coffee ☕", - Tokens = new List - { - new CashuToken.Token - { - Mint = "https://testnut.cashu.space", - Proofs = myProofs // Your token proofs - } - } -}; +var mintHandler = await wallet + .CreateMintQuote() + .WithAmount(1000) + .WithUnit("sat") + .ProcessAsyncBolt11(); -// Encode for sharing (creates a cashu token string) -string encodedToken = token.Encode("B"); // V4 format (compact) -Console.WriteLine($"Token to share: {encodedToken}"); +// Pay the Lightning invoice +Console.WriteLine(mintHandler.GetQuote().Request); -// Receive and decode a token -var receivedToken = CashuTokenHelper.Decode(encodedToken, out string version); -Console.WriteLine($"Received {receivedToken.TotalAmount()} sats"); +// After payment, mint the tokens +List proofs = await mintHandler.Mint(); ``` -### 3. Basic Mint Operations +### Swap (rebalance / receive token) ```csharp -using DotNut.ApiModels.Mint; - -// Create a mint quote for 1000 sats via Lightning -var mintQuote = await client.CreateMintQuote( - "bolt11", - new PostMintQuoteBolt11Request { Amount = 1000, Unit = "sat" } -); - -Console.WriteLine($"Pay this invoice: {mintQuote.Request}"); -// After paying the Lightning invoice, mint your tokens... - -// Create a melt quote to convert tokens back to Lightning -var meltQuote = await client.CreateMeltQuote( - "bolt11", - new PostMeltQuoteBolt11Request - { - Request = "lnbc1000n1...", // Lightning invoice to pay - Unit = "sat" - } -); +// From a cashuB... token string +List proofs = await wallet + .Swap() + .WithDLEQVerification() + .ProcessAsync(); // pass token string to SwapBuilder or use FromInputs() + +// From raw proofs +List rebalanced = await wallet + .Swap() + .FromInputs(existingProofs) + .ProcessAsync(); ``` -## Core Concepts - -### Tokens and Proofs -- **CashuToken**: Container for one or more tokens from different mints -- **Proof**: Cryptographic proof representing a specific amount -- **Secret**: The secret behind each proof (can be simple strings or complex conditions) - -### Privacy Features -- **Blind Signatures**: Mint doesn't know which tokens belong to whom -- **DLEQ Proofs**: Verify mint behavior without compromising privacy -- **Token Swapping**: Change denominations while maintaining privacy - -### Advanced Features -- **P2PK (Pay-to-Public-Key)**: Multi-signature spending conditions -- **HTLCs**: Hash Time-Locked Contracts for atomic swaps -- **Deterministic Secrets**: Generate secrets from mnemonic phrases - -## Working with Secrets +### Melt (tokens → Lightning) ```csharp -using DotNut; +var meltHandler = await wallet + .CreateMeltQuote() + .WithInvoice("lnbc...") + .WithUnit("sat") + .ProcessAsyncBolt11(); -// Simple string secret -var secret = new StringSecret("my-random-secret"); +List changeProofs = await meltHandler.Melt(inputProofs); +``` -// Deterministic secret from mnemonic (NUT-13) -var mnemonic = new Mnemonic("abandon abandon abandon..."); -var deterministicSecret = mnemonic.DeriveSecret(keysetId, counter: 0); +### Restore (from mnemonic) -// Pay-to-Public-Key secret (NUT-11) -var p2pkBuilder = new P2PkBuilder -{ - Pubkeys = new[] { pubkey1, pubkey2 }, - SignatureThreshold = 1, // 1-of-2 multisig - SigFlag = "SIG_INPUTS" -}; -var p2pkSecret = new Nut10Secret(P2PKProofSecret.Key, p2pkBuilder.Build()); +```csharp +IEnumerable recovered = await wallet + .Restore() + .ProcessAsync(); ``` -## Token Operations +### Token encoding ```csharp -// Check if proofs are still valid -var stateRequest = new PostCheckStateRequest { Ys = proofs.Select(p => p.Y).ToArray() }; -var stateResponse = await client.CheckState(stateRequest); - -// Swap tokens to different denominations -var swapRequest = new PostSwapRequest +var token = new CashuToken { - Inputs = inputProofs, - Outputs = newBlindedMessages + Unit = "sat", + Tokens = [new CashuToken.Token { Mint = "https://testnut.cashu.space", Proofs = proofs }] }; -var swapResponse = await client.Swap(swapRequest); -// Restore tokens from secrets (if you've lost proofs) -var restoreRequest = new PostRestoreRequest { Outputs = blindedMessages }; -var restoreResponse = await client.Restore(restoreRequest); -``` +string v4 = token.Encode("B"); // cashuB... (CBOR, compact) +string v3 = token.Encode("A"); // cashuA... (JSON) +string uri = token.Encode("B", makeUri: true); // cashu:cashuB... -## Token Encoding Formats +var decoded = CashuTokenHelper.Decode(v4, out string version); +``` -DotNut supports multiple token encoding formats: +### P2PK / HTLC spending conditions ```csharp -// V3 format (JSON-based) -string v3Token = token.Encode("A"); - -// V4 format (CBOR-based, more compact) -string v4Token = token.Encode("B"); - -// As URI for easy sharing -string tokenUri = token.Encode("B", makeUri: true); -// Result: "cashu:cashuB..." +// Mint tokens locked to a pubkey +var mintHandler = await wallet + .CreateMintQuote() + .WithAmount(500) + .WithP2PkLock(new P2PKBuilder { Pubkeys = [pubkey], SignatureThreshold = 1 }) + .ProcessAsyncBolt11(); + +// Spend P2PK-locked tokens by signing during swap +List proofs = await wallet + .Swap() + .FromInputs(lockedProofs) + .WithPrivkeys([privKey]) + .ProcessAsync(); ``` -## Error Handling +### Direct API access + +If you need raw protocol access without the wallet abstraction: ```csharp -try -{ - var response = await client.Swap(swapRequest); -} -catch (CashuProtocolException ex) -{ - Console.WriteLine($"Mint error: {ex.Error.Detail}"); - Console.WriteLine($"Error code: {ex.Error.Code}"); -} +var httpClient = new HttpClient { BaseAddress = new Uri("https://testnut.cashu.space/") }; +var api = new CashuHttpClient(httpClient); + +var info = await api.GetInfo(); +var keysets = await api.GetKeysets(); +var mintQuote = await api.CreateMintQuote( + "bolt11", new PostMintQuoteBolt11Request { Amount = 1000, Unit = "sat" } +); ``` -## Nostr Integration +### WebSockets (NUT-17) -DotNut includes a separate package for Nostr integration: +```csharp +var wallet = Wallet.Create() + .WithMint("https://testnut.cashu.space") + .WithWebsocketService(new WebsocketService()); -```bash -dotnet add package DotNut.Nostr +var ws = await wallet.GetWebsocketService(); +// subscribe to quote state changes, proof state updates, etc. ``` -This enables payment requests over Nostr (NUT-18) and other Nostr-based features. - -## Implemented Specifications - -Complete implementation of the [Cashu protocol specifications](https://github.com/cashubtc/nuts/): - -| NUT | Description | Status | -|-----|-------------|--------| -| [00](https://github.com/cashubtc/nuts/blob/main/00.md) | Cryptographic primitives | ✅ | -| [01](https://github.com/cashubtc/nuts/blob/main/01.md) | Mint public key distribution | ✅ | -| [02](https://github.com/cashubtc/nuts/blob/main/02.md) | Keysets and keyset IDs | ✅ | -| [03](https://github.com/cashubtc/nuts/blob/main/03.md) | Swapping tokens | ✅ | -| [04](https://github.com/cashubtc/nuts/blob/main/04.md) | Minting tokens | ✅ | -| [05](https://github.com/cashubtc/nuts/blob/main/05.md) | Melting tokens | ✅ | -| [06](https://github.com/cashubtc/nuts/blob/main/06.md) | Mint info | ✅ | -| [07](https://github.com/cashubtc/nuts/blob/main/07.md) | Token state check | ✅ | -| [08](https://github.com/cashubtc/nuts/blob/main/08.md) | Lightning fee return | ✅ | -| [09](https://github.com/cashubtc/nuts/blob/main/09.md) | Token restoration | ✅ | -| [10](https://github.com/cashubtc/nuts/blob/main/10.md) | Spending conditions | ✅ | -| [11](https://github.com/cashubtc/nuts/blob/main/11.md) | Pay-to-Public-Key (P2PK) | ✅ | -| [12](https://github.com/cashubtc/nuts/blob/main/12.md) | DLEQ proofs | ✅ | -| [13](https://github.com/cashubtc/nuts/blob/main/13.md) | Deterministic secrets | ✅ | -| [14](https://github.com/cashubtc/nuts/blob/main/14.md) | Hash Time-Locked Contracts | ✅ | -| [15](https://github.com/cashubtc/nuts/blob/main/15.md) | Multipath payments | ✅ | -| [18](https://github.com/cashubtc/nuts/blob/main/18.md) | Payment requests | ✅ | - +## Implemented NUTs + +| NUT | Description | +|----------------------------------------------------------|-----------------------------------------| +| [00](https://github.com/cashubtc/nuts/blob/main/00.md) | Cryptographic primitives & token format | +| [01](https://github.com/cashubtc/nuts/blob/main/01.md) | Mint public key distribution | +| [02](https://github.com/cashubtc/nuts/blob/main/02.md) | Keysets and keyset IDs | +| [03](https://github.com/cashubtc/nuts/blob/main/03.md) | Swapping tokens | +| [04](https://github.com/cashubtc/nuts/blob/main/04.md) | Minting tokens | +| [05](https://github.com/cashubtc/nuts/blob/main/05.md) | Melting tokens | +| [06](https://github.com/cashubtc/nuts/blob/main/06.md) | Mint info | +| [07](https://github.com/cashubtc/nuts/blob/main/07.md) | Token state check | +| [08](https://github.com/cashubtc/nuts/blob/main/08.md) | Lightning fee return | +| [09](https://github.com/cashubtc/nuts/blob/main/09.md) | Token restoration | +| [10](https://github.com/cashubtc/nuts/blob/main/10.md) | Spending conditions | +| [11](https://github.com/cashubtc/nuts/blob/main/11.md) | Pay-to-Public-Key (P2PK) | +| [12](https://github.com/cashubtc/nuts/blob/main/12.md) | DLEQ proofs | +| [13](https://github.com/cashubtc/nuts/blob/main/13.md) | Deterministic secrets (BIP39) | +| [14](https://github.com/cashubtc/nuts/blob/main/14.md) | Hash Time-Locked Contracts (HTLC) | +| [17](https://github.com/cashubtc/nuts/blob/main/17.md) | WebSocket subscriptions | +| [18](https://github.com/cashubtc/nuts/blob/main/18.md) | Payment requests | +| [20](https://github.com/cashubtc/nuts/blob/main/20.md) | Signature on Mint Quote | +| [23](https://github.com/cashubtc/nuts/blob/main/23.md) | BOLT11 | +| [25](https://github.com/cashubtc/nuts/blob/main/25.md) | BOLT12 | +| [26](https://github.com/cashubtc/nuts/blob/main/26.md) | Payment Request Bech32m Encoding | +| [27](https://github.com/cashubtc/nuts/blob/main/27.md) | Nostr Mint Backup | +| [28](https://github.com/cashubtc/nuts/blob/main/28.md) | Pay-to-Blinded-Key (P2BK) | + +TODO: + +| NUT | Description | +|--------------------------------------------------------|-----------------------------------------| +| [15](https://github.com/cashubtc/nuts/blob/main/15.md) | Multipath payments | +| [21](https://github.com/cashubtc/nuts/blob/main/21.md) | Clear Authentication | +| [22](https://github.com/cashubtc/nuts/blob/main/22.md) | Blind Authentication | ## Requirements -- .NET 8.0 or later -- HTTP client for mint communication - -## Contributing - -Contributions are welcome! Please feel free to submit issues, feature requests, or pull requests. +- .NET 8.0+ ## License -This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details. +MIT - see [LICENSE](LICENSE). ## Resources -- [Cashu Protocol](https://cashu.space) -- [Cashu Specifications (NUTs)](https://github.com/cashubtc/nuts/) -- [NuGet Package](https://www.nuget.org/packages/DotNut/) -- [GitHub Repository](https://github.com/Kukks/DotNut) +- [Cashu protocol](https://cashu.space) +- [NUT specifications](https://github.com/cashubtc/nuts/) +- [NuGet package](https://www.nuget.org/packages/DotNut/) +- [GitHub](https://github.com/Kukks/DotNut)