@sequence0/sdk 1.1.0 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (115) hide show
  1. package/README.md +1 -1
  2. package/dist/chains/algorand.d.ts +44 -0
  3. package/dist/chains/algorand.d.ts.map +1 -0
  4. package/dist/chains/algorand.js +148 -0
  5. package/dist/chains/algorand.js.map +1 -0
  6. package/dist/chains/aptos.d.ts +39 -0
  7. package/dist/chains/aptos.d.ts.map +1 -0
  8. package/dist/chains/aptos.js +168 -0
  9. package/dist/chains/aptos.js.map +1 -0
  10. package/dist/chains/bitcoin-taproot.d.ts +77 -14
  11. package/dist/chains/bitcoin-taproot.d.ts.map +1 -1
  12. package/dist/chains/bitcoin-taproot.js +324 -65
  13. package/dist/chains/bitcoin-taproot.js.map +1 -1
  14. package/dist/chains/bitcoin.d.ts +12 -7
  15. package/dist/chains/bitcoin.d.ts.map +1 -1
  16. package/dist/chains/bitcoin.js +14 -9
  17. package/dist/chains/bitcoin.js.map +1 -1
  18. package/dist/chains/cardano.d.ts +42 -0
  19. package/dist/chains/cardano.d.ts.map +1 -0
  20. package/dist/chains/cardano.js +188 -0
  21. package/dist/chains/cardano.js.map +1 -0
  22. package/dist/chains/cosmos.d.ts +42 -0
  23. package/dist/chains/cosmos.d.ts.map +1 -0
  24. package/dist/chains/cosmos.js +216 -0
  25. package/dist/chains/cosmos.js.map +1 -0
  26. package/dist/chains/dogecoin-litecoin.d.ts +57 -0
  27. package/dist/chains/dogecoin-litecoin.d.ts.map +1 -0
  28. package/dist/chains/dogecoin-litecoin.js +521 -0
  29. package/dist/chains/dogecoin-litecoin.js.map +1 -0
  30. package/dist/chains/ethereum.d.ts.map +1 -1
  31. package/dist/chains/ethereum.js +16 -0
  32. package/dist/chains/ethereum.js.map +1 -1
  33. package/dist/chains/hedera.d.ts +113 -0
  34. package/dist/chains/hedera.d.ts.map +1 -0
  35. package/dist/chains/hedera.js +302 -0
  36. package/dist/chains/hedera.js.map +1 -0
  37. package/dist/chains/icp.d.ts +95 -0
  38. package/dist/chains/icp.d.ts.map +1 -0
  39. package/dist/chains/icp.js +520 -0
  40. package/dist/chains/icp.js.map +1 -0
  41. package/dist/chains/kaspa.d.ts +152 -0
  42. package/dist/chains/kaspa.d.ts.map +1 -0
  43. package/dist/chains/kaspa.js +790 -0
  44. package/dist/chains/kaspa.js.map +1 -0
  45. package/dist/chains/multiversx.d.ts +143 -0
  46. package/dist/chains/multiversx.d.ts.map +1 -0
  47. package/dist/chains/multiversx.js +524 -0
  48. package/dist/chains/multiversx.js.map +1 -0
  49. package/dist/chains/near.d.ts +40 -0
  50. package/dist/chains/near.d.ts.map +1 -0
  51. package/dist/chains/near.js +170 -0
  52. package/dist/chains/near.js.map +1 -0
  53. package/dist/chains/polkadot.d.ts +43 -0
  54. package/dist/chains/polkadot.d.ts.map +1 -0
  55. package/dist/chains/polkadot.js +179 -0
  56. package/dist/chains/polkadot.js.map +1 -0
  57. package/dist/chains/ripple.d.ts +41 -0
  58. package/dist/chains/ripple.d.ts.map +1 -0
  59. package/dist/chains/ripple.js +190 -0
  60. package/dist/chains/ripple.js.map +1 -0
  61. package/dist/chains/stellar.d.ts +40 -0
  62. package/dist/chains/stellar.d.ts.map +1 -0
  63. package/dist/chains/stellar.js +156 -0
  64. package/dist/chains/stellar.js.map +1 -0
  65. package/dist/chains/sui.d.ts +44 -0
  66. package/dist/chains/sui.d.ts.map +1 -0
  67. package/dist/chains/sui.js +157 -0
  68. package/dist/chains/sui.js.map +1 -0
  69. package/dist/chains/tezos.d.ts +43 -0
  70. package/dist/chains/tezos.d.ts.map +1 -0
  71. package/dist/chains/tezos.js +162 -0
  72. package/dist/chains/tezos.js.map +1 -0
  73. package/dist/chains/ton.d.ts +40 -0
  74. package/dist/chains/ton.d.ts.map +1 -0
  75. package/dist/chains/ton.js +168 -0
  76. package/dist/chains/ton.js.map +1 -0
  77. package/dist/chains/tron.d.ts +41 -0
  78. package/dist/chains/tron.d.ts.map +1 -0
  79. package/dist/chains/tron.js +124 -0
  80. package/dist/chains/tron.js.map +1 -0
  81. package/dist/core/client.d.ts +4 -5
  82. package/dist/core/client.d.ts.map +1 -1
  83. package/dist/core/client.js +54 -29
  84. package/dist/core/client.js.map +1 -1
  85. package/dist/core/types.d.ts +161 -1
  86. package/dist/core/types.d.ts.map +1 -1
  87. package/dist/erc4337/types.js +2 -2
  88. package/dist/index.d.ts +21 -3
  89. package/dist/index.d.ts.map +1 -1
  90. package/dist/index.js +85 -4
  91. package/dist/index.js.map +1 -1
  92. package/dist/utils/discovery.d.ts.map +1 -1
  93. package/dist/utils/discovery.js +56 -1
  94. package/dist/utils/discovery.js.map +1 -1
  95. package/dist/utils/eip712.d.ts +36 -0
  96. package/dist/utils/eip712.d.ts.map +1 -0
  97. package/dist/utils/eip712.js +80 -0
  98. package/dist/utils/eip712.js.map +1 -0
  99. package/dist/utils/fee.d.ts +2 -2
  100. package/dist/utils/fee.js +2 -2
  101. package/dist/utils/optional-deps.d.ts +9 -0
  102. package/dist/utils/optional-deps.d.ts.map +1 -0
  103. package/dist/utils/optional-deps.js +21 -0
  104. package/dist/utils/optional-deps.js.map +1 -0
  105. package/dist/utils/validation.d.ts +8 -0
  106. package/dist/utils/validation.d.ts.map +1 -1
  107. package/dist/utils/validation.js +24 -1
  108. package/dist/utils/validation.js.map +1 -1
  109. package/dist/utils/websocket.js +1 -1
  110. package/dist/utils/websocket.js.map +1 -1
  111. package/dist/wallet/wallet.d.ts +16 -2
  112. package/dist/wallet/wallet.d.ts.map +1 -1
  113. package/dist/wallet/wallet.js +131 -58
  114. package/dist/wallet/wallet.js.map +1 -1
  115. package/package.json +35 -1
@@ -8,10 +8,15 @@
8
8
  * FROST-secp256k1 produces BIP-340 compatible Schnorr signatures that are
9
9
  * NATIVE to Taproot key-path spends -- no signature format conversion needed.
10
10
  *
11
- * Depends on the WASM crate's bitcoin.rs for:
12
- * - Address derivation (group pubkey -> bc1p... P2TR address)
13
- * - Transaction construction (inputs/outputs -> unsigned TX + sighash)
14
- * - Signature attachment (FROST sig -> witness data)
11
+ * Key features:
12
+ * - **Real secp256k1 EC point arithmetic** for BIP-341 key tweaking
13
+ * (lift_x, point_add, scalar_mul -- no external crypto dependencies)
14
+ * - **Per-input sighash computation** for multi-input transactions
15
+ * (each input gets its own BIP-341 sighash for independent FROST signing)
16
+ * - **Full transaction serialization** with proper segwit witness structure
17
+ * - **Broadcast via Mempool.space API** (mainnet, testnet, signet, regtest)
18
+ *
19
+ * No external dependencies beyond Node.js crypto (SHA-256).
15
20
  *
16
21
  * @example
17
22
  * ```typescript
@@ -101,9 +106,33 @@ class BitcoinTaprootAdapter {
101
106
  xOnlyPubkey: bytesToHex(xOnly),
102
107
  outputKey: bytesToHex(outputKey),
103
108
  scriptPubkey,
109
+ tapTweak: bytesToHex(tweak),
104
110
  network: this.network,
105
111
  };
106
112
  }
113
+ /**
114
+ * Compute the Taproot tweak for a FROST group public key.
115
+ *
116
+ * The FROST signing protocol must apply this tweak to the group private key
117
+ * before signing. This ensures the Schnorr signature verifies against the
118
+ * tweaked output key (which is what the scriptPubKey commits to).
119
+ *
120
+ * The tweak scalar t = hash_TapTweak(internal_key) is returned as hex.
121
+ * During FROST signing, the group's secret share is tweaked:
122
+ * tweaked_share = share + t (mod n)
123
+ *
124
+ * @param groupPubkeyHex - Hex-encoded FROST group verifying key (33 or 32 bytes)
125
+ * @returns Hex-encoded 32-byte tweak scalar
126
+ */
127
+ getTapTweak(groupPubkeyHex) {
128
+ const pubkeyClean = groupPubkeyHex.startsWith('0x')
129
+ ? groupPubkeyHex.slice(2)
130
+ : groupPubkeyHex;
131
+ const pubkeyBytes = hexToBytes(pubkeyClean);
132
+ const xOnly = extractXOnlyPubkey(pubkeyBytes);
133
+ const tweak = computeTapTweak(xOnly);
134
+ return bytesToHex(tweak);
135
+ }
107
136
  // ────────────────────────────────────────────────
108
137
  // UTXO Management
109
138
  // ────────────────────────────────────────────────
@@ -283,9 +312,11 @@ class BitcoinTaprootAdapter {
283
312
  value: u.value,
284
313
  scriptPubkey: u.scriptPubkey,
285
314
  }));
286
- // Build the unsigned transaction
315
+ // Build the unsigned transaction with per-input sighashes
316
+ const sighashes = this.computeAllSighashes(inputs, outputs);
287
317
  const unsignedTx = {
288
- sighash: this.computeSighash(inputs, outputs),
318
+ sighash: sighashes[0],
319
+ sighashes,
289
320
  rawUnsigned: this.serializeUnsignedTx(inputs, outputs),
290
321
  inputs,
291
322
  outputs,
@@ -328,8 +359,10 @@ class BitcoinTaprootAdapter {
328
359
  throw new errors_1.ChainError(`Insufficient funds: ${totalInput} sat available, ` +
329
360
  `${totalOutput + fee} sat needed (${totalOutput} output + ${fee} fee)`, 'bitcoin');
330
361
  }
362
+ const sighashes = this.computeAllSighashes(inputs, outputs);
331
363
  return {
332
- sighash: this.computeSighash(inputs, outputs),
364
+ sighash: sighashes[0],
365
+ sighashes,
333
366
  rawUnsigned: this.serializeUnsignedTx(inputs, outputs),
334
367
  inputs,
335
368
  outputs,
@@ -338,25 +371,29 @@ class BitcoinTaprootAdapter {
338
371
  };
339
372
  }
340
373
  /**
341
- * Attach a FROST Schnorr signature to an unsigned Taproot transaction.
374
+ * Attach FROST Schnorr signature(s) to an unsigned Taproot transaction.
375
+ *
376
+ * The FROST signing protocol produces 64-byte BIP-340 Schnorr signatures
377
+ * (R_x || s) that are directly used as Taproot witness for key-path spends.
342
378
  *
343
- * The FROST signing protocol produces a 64-byte BIP-340 Schnorr signature
344
- * (R_x || s) that is directly used as the Taproot witness for key-path spends.
379
+ * For single-input transactions: pass a single 128-char hex signature.
380
+ * For multi-input transactions: pass signatures separated by commas, or
381
+ * a single signature that will be applied to all inputs (if all inputs
382
+ * share the same signing key and the caller signs each sighash separately).
345
383
  *
346
384
  * @param unsignedTxHex - Hex-encoded unsigned transaction (from buildTransaction)
347
- * @param signatureHex - 64-byte FROST Schnorr signature (hex-encoded, 128 chars)
385
+ * @param signatureHex - 64-byte FROST Schnorr signature(s). For multi-input
386
+ * transactions, separate per-input signatures with commas.
348
387
  * @returns Hex-encoded signed transaction ready for broadcast
349
388
  */
350
389
  async attachSignature(unsignedTxHex, signatureHex) {
351
390
  try {
352
- const sig = signatureHex.startsWith('0x') ? signatureHex.slice(2) : signatureHex;
353
- if (sig.length !== 128) {
354
- throw new Error(`Schnorr signature must be 64 bytes (128 hex chars), got ${sig.length} chars`);
355
- }
356
391
  // Parse the unsigned transaction
357
392
  const unsignedTx = JSON.parse(Buffer.from(unsignedTxHex, 'hex').toString());
393
+ // Parse signature(s)
394
+ const signatures = this.parseSignatures(signatureHex, unsignedTx.inputs.length);
358
395
  // Build the signed transaction with Taproot witness
359
- const signedTx = this.serializeSignedTx(unsignedTx, sig);
396
+ const signedTx = this.serializeSignedTx(unsignedTx, signatures);
360
397
  return Buffer.from(JSON.stringify(signedTx)).toString('hex');
361
398
  }
362
399
  catch (e) {
@@ -364,18 +401,58 @@ class BitcoinTaprootAdapter {
364
401
  }
365
402
  }
366
403
  /**
367
- * Attach a FROST Schnorr signature with full output (returns structured data).
404
+ * Attach FROST Schnorr signature(s) with full output (returns structured data).
368
405
  *
369
406
  * @param unsignedTx - The UnsignedTaprootTx from buildUnsignedTx
370
- * @param signatureHex - 64-byte FROST Schnorr signature (hex, 128 chars)
407
+ * @param signatureHex - 64-byte FROST Schnorr signature(s) (hex). For multi-input
408
+ * transactions, pass an array of per-input signatures or a comma-separated string.
371
409
  * @returns SignedTaprootTx with raw_signed, txid, and vsize
372
410
  */
373
411
  attachSignatureToTx(unsignedTx, signatureHex) {
374
- const sig = signatureHex.startsWith('0x') ? signatureHex.slice(2) : signatureHex;
375
- if (sig.length !== 128) {
376
- throw new errors_1.ChainError(`Schnorr signature must be 64 bytes (128 hex chars), got ${sig.length} chars`, 'bitcoin');
412
+ const sigs = Array.isArray(signatureHex)
413
+ ? signatureHex.map(s => {
414
+ const clean = s.startsWith('0x') ? s.slice(2) : s;
415
+ if (clean.length !== 128) {
416
+ throw new errors_1.ChainError(`Schnorr signature must be 64 bytes (128 hex chars), got ${clean.length}`, 'bitcoin');
417
+ }
418
+ return clean;
419
+ })
420
+ : this.parseSignatures(signatureHex, unsignedTx.inputs.length);
421
+ return this.serializeSignedTx(unsignedTx, sigs);
422
+ }
423
+ /**
424
+ * Parse signature hex into per-input signatures.
425
+ * Supports: single sig (applied to all inputs), comma-separated, or concatenated.
426
+ */
427
+ parseSignatures(signatureHex, inputCount) {
428
+ const raw = signatureHex.startsWith('0x') ? signatureHex.slice(2) : signatureHex;
429
+ // Check for comma-separated signatures
430
+ if (raw.includes(',')) {
431
+ const parts = raw.split(',').map(s => s.trim());
432
+ if (parts.length !== inputCount) {
433
+ throw new Error(`Expected ${inputCount} signatures for ${inputCount} inputs, got ${parts.length}`);
434
+ }
435
+ for (const s of parts) {
436
+ if (s.length !== 128) {
437
+ throw new Error(`Each Schnorr signature must be 128 hex chars, got ${s.length}`);
438
+ }
439
+ }
440
+ return parts;
377
441
  }
378
- return this.serializeSignedTx(unsignedTx, sig);
442
+ // Single signature (128 hex chars) — replicate for all inputs
443
+ if (raw.length === 128) {
444
+ return new Array(inputCount).fill(raw);
445
+ }
446
+ // Concatenated signatures (128 * inputCount hex chars)
447
+ if (raw.length === 128 * inputCount) {
448
+ const sigs = [];
449
+ for (let i = 0; i < inputCount; i++) {
450
+ sigs.push(raw.slice(i * 128, (i + 1) * 128));
451
+ }
452
+ return sigs;
453
+ }
454
+ throw new Error(`Invalid signature format: expected 128 hex chars (single), ` +
455
+ `${128 * inputCount} chars (concatenated), or comma-separated. Got ${raw.length} chars.`);
379
456
  }
380
457
  // ────────────────────────────────────────────────
381
458
  // Broadcast
@@ -562,10 +639,16 @@ class BitcoinTaprootAdapter {
562
639
  return bytesToHex(new Uint8Array(buf));
563
640
  }
564
641
  /**
565
- * Serialize a signed Taproot transaction.
642
+ * Serialize a signed Taproot transaction with per-input signatures.
643
+ *
644
+ * @param unsignedTx - The unsigned transaction data
645
+ * @param signatures - Array of per-input signature hex strings (128 chars each)
566
646
  */
567
- serializeSignedTx(unsignedTx, signatureHex) {
568
- const sigBytes = hexToBytes(signatureHex);
647
+ serializeSignedTx(unsignedTx, signatures) {
648
+ if (signatures.length !== unsignedTx.inputs.length) {
649
+ throw new Error(`Signature count (${signatures.length}) does not match ` +
650
+ `input count (${unsignedTx.inputs.length})`);
651
+ }
569
652
  const buf = [];
570
653
  // Version 2
571
654
  pushLE32(buf, 2);
@@ -591,8 +674,9 @@ class BitcoinTaprootAdapter {
591
674
  pushVarint(buf, script.length);
592
675
  buf.push(...script);
593
676
  }
594
- // Witness: the Schnorr signature for each input
677
+ // Witness: per-input Schnorr signatures for key-path spend
595
678
  for (let i = 0; i < unsignedTx.inputs.length; i++) {
679
+ const sigBytes = hexToBytes(signatures[i]);
596
680
  buf.push(0x01); // 1 witness item
597
681
  buf.push(0x40); // 64 bytes (Schnorr signature, no sighash type suffix)
598
682
  buf.push(...sigBytes);
@@ -630,20 +714,18 @@ class BitcoinTaprootAdapter {
630
714
  * - Spend type (0x00 for key-path, no annex)
631
715
  * - Input index (4 bytes LE)
632
716
  */
633
- computeSighash(inputs, outputs) {
634
- // The sighash is a tagged hash: hash_TapSighash(message)
717
+ /**
718
+ * Compute all per-input BIP-341 sighashes for the transaction.
719
+ *
720
+ * Each input has its own sighash because the input_index field differs.
721
+ * The common transaction-level hashes (prevouts, amounts, scripts, sequences,
722
+ * outputs) are precomputed once and reused across all inputs.
723
+ *
724
+ * @returns Array of hex-encoded sighashes, one per input
725
+ */
726
+ computeAllSighashes(inputs, outputs) {
727
+ // Precompute the transaction-level hashes (shared across all inputs)
635
728
  const tagHash = sha256(new TextEncoder().encode('TapSighash'));
636
- const parts = [];
637
- // Tag hash prefix (used twice per BIP-340 tagged hash convention)
638
- parts.push(...tagHash, ...tagHash);
639
- // Epoch (1 byte)
640
- parts.push(0x00);
641
- // Sighash type: SIGHASH_DEFAULT (1 byte)
642
- parts.push(0x00);
643
- // Transaction version (4 bytes LE)
644
- pushLE32(parts, 2);
645
- // Locktime (4 bytes LE)
646
- pushLE32(parts, 0);
647
729
  // SHA-256 of prevouts
648
730
  const prevoutsBuf = [];
649
731
  for (const input of inputs) {
@@ -652,13 +734,13 @@ class BitcoinTaprootAdapter {
652
734
  prevoutsBuf.push(...txidBytes);
653
735
  pushLE32(prevoutsBuf, input.vout);
654
736
  }
655
- parts.push(...sha256(new Uint8Array(prevoutsBuf)));
737
+ const hashPrevouts = sha256(new Uint8Array(prevoutsBuf));
656
738
  // SHA-256 of amounts
657
739
  const amountsBuf = [];
658
740
  for (const input of inputs) {
659
741
  pushLE64(amountsBuf, input.value);
660
742
  }
661
- parts.push(...sha256(new Uint8Array(amountsBuf)));
743
+ const hashAmounts = sha256(new Uint8Array(amountsBuf));
662
744
  // SHA-256 of scriptPubKeys (with compact size prefix)
663
745
  const scriptsBuf = [];
664
746
  for (const input of inputs) {
@@ -666,13 +748,13 @@ class BitcoinTaprootAdapter {
666
748
  pushVarint(scriptsBuf, script.length);
667
749
  scriptsBuf.push(...script);
668
750
  }
669
- parts.push(...sha256(new Uint8Array(scriptsBuf)));
751
+ const hashScripts = sha256(new Uint8Array(scriptsBuf));
670
752
  // SHA-256 of sequences
671
753
  const seqsBuf = [];
672
754
  for (let i = 0; i < inputs.length; i++) {
673
755
  pushLE32(seqsBuf, 0xfffffffd);
674
756
  }
675
- parts.push(...sha256(new Uint8Array(seqsBuf)));
757
+ const hashSequences = sha256(new Uint8Array(seqsBuf));
676
758
  // SHA-256 of outputs
677
759
  const outputsBuf = [];
678
760
  for (const output of outputs) {
@@ -681,18 +763,185 @@ class BitcoinTaprootAdapter {
681
763
  pushVarint(outputsBuf, script.length);
682
764
  outputsBuf.push(...script);
683
765
  }
684
- parts.push(...sha256(new Uint8Array(outputsBuf)));
685
- // Spend type: 0x00 (key-path, no annex)
686
- parts.push(0x00);
687
- // Input index: 0 (sign first input; for multi-input, each input would
688
- // need its own sighash, but FROST signs all inputs with the same key)
689
- pushLE32(parts, 0);
690
- // Final sighash = SHA-256 of the tagged hash message
691
- const sighash = sha256(new Uint8Array(parts));
692
- return bytesToHex(sighash);
766
+ const hashOutputs = sha256(new Uint8Array(outputsBuf));
767
+ // Compute per-input sighash
768
+ const sighashes = [];
769
+ for (let inputIdx = 0; inputIdx < inputs.length; inputIdx++) {
770
+ const parts = [];
771
+ // Tag hash prefix (used twice per BIP-340 tagged hash convention)
772
+ parts.push(...tagHash, ...tagHash);
773
+ // Epoch (1 byte)
774
+ parts.push(0x00);
775
+ // Sighash type: SIGHASH_DEFAULT (1 byte)
776
+ parts.push(0x00);
777
+ // Transaction version (4 bytes LE)
778
+ pushLE32(parts, 2);
779
+ // Locktime (4 bytes LE)
780
+ pushLE32(parts, 0);
781
+ // Precomputed hashes
782
+ parts.push(...hashPrevouts);
783
+ parts.push(...hashAmounts);
784
+ parts.push(...hashScripts);
785
+ parts.push(...hashSequences);
786
+ parts.push(...hashOutputs);
787
+ // Spend type: 0x00 (key-path, no annex)
788
+ parts.push(0x00);
789
+ // Input index (4 bytes LE) — THIS is what differs per input
790
+ pushLE32(parts, inputIdx);
791
+ // Final sighash = SHA-256 of the tagged hash message
792
+ const sighash = sha256(new Uint8Array(parts));
793
+ sighashes.push(bytesToHex(sighash));
794
+ }
795
+ return sighashes;
693
796
  }
694
797
  }
695
798
  exports.BitcoinTaprootAdapter = BitcoinTaprootAdapter;
799
+ const Secp256k1 = {
800
+ /** Field prime */
801
+ P: 0xfffffffffffffffffffffffffffffffffffffffffffffffffffffffefffffc2fn,
802
+ /** Curve order */
803
+ N: 0xfffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364141n,
804
+ /** Generator x */
805
+ Gx: 0x79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798n,
806
+ /** Generator y */
807
+ Gy: 0x483ada7726a3c4655da4fbfc0e1108a8fd17b448a68554199c47d08ffb10d4b8n,
808
+ /** Get generator point */
809
+ G() {
810
+ return { x: this.Gx, y: this.Gy };
811
+ },
812
+ /**
813
+ * Modular exponentiation: base^exp mod m
814
+ * Uses square-and-multiply for efficiency.
815
+ */
816
+ modPow(base, exp, m) {
817
+ let result = 1n;
818
+ base = ((base % m) + m) % m;
819
+ while (exp > 0n) {
820
+ if (exp & 1n) {
821
+ result = (result * base) % m;
822
+ }
823
+ exp >>= 1n;
824
+ base = (base * base) % m;
825
+ }
826
+ return result;
827
+ },
828
+ /** Modular inverse using Fermat's little theorem: a^(p-2) mod p */
829
+ modInverse(a, m) {
830
+ return this.modPow(((a % m) + m) % m, m - 2n, m);
831
+ },
832
+ /** Modular square root: returns r such that r^2 = a (mod p), or throws.
833
+ * Uses the Tonelli-Shanks shortcut for p = 3 mod 4: r = a^((p+1)/4) mod p */
834
+ modSqrt(a) {
835
+ const p = this.P;
836
+ // secp256k1 p % 4 == 3, so we can use the simple formula
837
+ const r = this.modPow(((a % p) + p) % p, (p + 1n) / 4n, p);
838
+ if ((r * r) % p !== ((a % p) + p) % p) {
839
+ throw new Error('No square root exists for this value');
840
+ }
841
+ return r;
842
+ },
843
+ /**
844
+ * Lift an x-only public key to a full curve point with even y-coordinate.
845
+ * Per BIP-340: given x, compute y = sqrt(x^3 + 7), pick the even root.
846
+ */
847
+ liftX(xBytes) {
848
+ const x = bytesToBigInt(xBytes);
849
+ const p = this.P;
850
+ if (x >= p) {
851
+ throw new Error('x-coordinate exceeds field prime');
852
+ }
853
+ // y^2 = x^3 + 7 mod p
854
+ const y2 = (this.modPow(x, 3n, p) + 7n) % p;
855
+ let y = this.modSqrt(y2);
856
+ // BIP-340: pick the even y (y % 2 == 0)
857
+ if (y & 1n) {
858
+ y = p - y;
859
+ }
860
+ return { x, y };
861
+ },
862
+ /**
863
+ * Point addition: P + Q on secp256k1.
864
+ * Handles identity, doubling, and general addition.
865
+ */
866
+ pointAdd(p1, p2) {
867
+ if (p1 === null)
868
+ return p2;
869
+ if (p2 === null)
870
+ return p1;
871
+ const p = this.P;
872
+ if (p1.x === p2.x) {
873
+ if (p1.y === p2.y) {
874
+ // Point doubling
875
+ return this.pointDouble(p1);
876
+ }
877
+ // P + (-P) = O (point at infinity)
878
+ return null;
879
+ }
880
+ // General addition: lambda = (y2 - y1) / (x2 - x1)
881
+ const dy = ((p2.y - p1.y) % p + p) % p;
882
+ const dx = ((p2.x - p1.x) % p + p) % p;
883
+ const lambda = (dy * this.modInverse(dx, p)) % p;
884
+ const x3 = ((lambda * lambda - p1.x - p2.x) % p + p) % p;
885
+ const y3 = ((lambda * (p1.x - x3) - p1.y) % p + p) % p;
886
+ return { x: x3, y: y3 };
887
+ },
888
+ /** Point doubling: 2P on secp256k1 */
889
+ pointDouble(pt) {
890
+ const p = this.P;
891
+ if (pt.y === 0n)
892
+ return null;
893
+ // lambda = (3 * x^2 + a) / (2 * y) where a = 0 for secp256k1
894
+ const num = (3n * pt.x * pt.x) % p;
895
+ const den = (2n * pt.y) % p;
896
+ const lambda = (num * this.modInverse(den, p)) % p;
897
+ const x3 = ((lambda * lambda - 2n * pt.x) % p + p) % p;
898
+ const y3 = ((lambda * (pt.x - x3) - pt.y) % p + p) % p;
899
+ return { x: x3, y: y3 };
900
+ },
901
+ /**
902
+ * Scalar multiplication: n * G using double-and-add.
903
+ * Only used for tweaking, so performance is not critical.
904
+ */
905
+ mulG(n) {
906
+ const order = this.N;
907
+ n = ((n % order) + order) % order;
908
+ if (n === 0n) {
909
+ throw new Error('Zero scalar in mulG');
910
+ }
911
+ let result = null;
912
+ let base = this.G();
913
+ let k = n;
914
+ while (k > 0n) {
915
+ if (k & 1n) {
916
+ result = this.pointAdd(result, base);
917
+ }
918
+ base = this.pointAdd(base, base);
919
+ k >>= 1n;
920
+ }
921
+ if (result === null) {
922
+ throw new Error('mulG produced point at infinity');
923
+ }
924
+ return result;
925
+ },
926
+ };
927
+ /** Convert a Uint8Array to a bigint (big-endian) */
928
+ function bytesToBigInt(bytes) {
929
+ let result = 0n;
930
+ for (const b of bytes) {
931
+ result = (result << 8n) | BigInt(b);
932
+ }
933
+ return result;
934
+ }
935
+ /** Convert a bigint to a 32-byte Uint8Array (big-endian, zero-padded) */
936
+ function bigIntToBytes32(n) {
937
+ const bytes = new Uint8Array(32);
938
+ let val = n;
939
+ for (let i = 31; i >= 0; i--) {
940
+ bytes[i] = Number(val & 0xffn);
941
+ val >>= 8n;
942
+ }
943
+ return bytes;
944
+ }
696
945
  // ────────────────────────────────────────────────
697
946
  // Pure Helper Functions
698
947
  // ────────────────────────────────────────────────
@@ -727,23 +976,33 @@ function computeTapTweak(internalKey) {
727
976
  return sha256(msg);
728
977
  }
729
978
  /**
730
- * Tweak a public key with the given tweak.
979
+ * Tweak a public key per BIP-341: Q = lift_x(P) + int(tweak) * G
731
980
  *
732
- * In a full implementation, this would use secp256k1 point addition:
733
- * Q = P + tweak * G
981
+ * Uses real secp256k1 EC point arithmetic. The output key Q is the
982
+ * x-only coordinate of the resulting point. If the resulting point
983
+ * has an odd y-coordinate, the tweak is negated (BIP-341 spec).
734
984
  *
735
- * This implementation uses a deterministic tagged-hash derivation that
736
- * produces consistent output keys for address derivation. The agent-node
737
- * uses proper EC point arithmetic via k256.
985
+ * @param internalKey - 32-byte x-only internal public key
986
+ * @param tweak - 32-byte tweak scalar (hash_TapTweak output)
987
+ * @returns 32-byte x-only output key
738
988
  */
739
989
  function tweakPublicKey(internalKey, tweak) {
740
- const tag = sha256(new TextEncoder().encode('TapTweak/OutputKey'));
741
- const msg = new Uint8Array(tag.length * 2 + internalKey.length + tweak.length);
742
- msg.set(tag, 0);
743
- msg.set(tag, tag.length);
744
- msg.set(internalKey, tag.length * 2);
745
- msg.set(tweak, tag.length * 2 + internalKey.length);
746
- return sha256(msg);
990
+ // Lift the x-only key to a full point with even y
991
+ const P = Secp256k1.liftX(internalKey);
992
+ // Convert tweak bytes to a bigint scalar
993
+ const t = bytesToBigInt(tweak);
994
+ if (t >= Secp256k1.N) {
995
+ throw new Error('Tweak scalar exceeds curve order');
996
+ }
997
+ // Compute t * G
998
+ const tG = Secp256k1.mulG(t);
999
+ // Compute Q = P + t * G
1000
+ const Q = Secp256k1.pointAdd(P, tG);
1001
+ if (Q === null) {
1002
+ throw new Error('Tweaked key is point at infinity');
1003
+ }
1004
+ // Return x-coordinate of Q as 32 bytes
1005
+ return bigIntToBytes32(Q.x);
747
1006
  }
748
1007
  /** Get the Bech32 HRP for a Bitcoin network */
749
1008
  function networkToHrp(network) {