@buildonspark/spark-sdk 0.5.0 → 0.5.1

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 (81) hide show
  1. package/CHANGELOG.md +26 -0
  2. package/dist/bare/index.cjs +1608 -3635
  3. package/dist/bare/index.d.cts +27 -435
  4. package/dist/bare/index.d.ts +27 -435
  5. package/dist/bare/index.js +1608 -3634
  6. package/dist/{chunk-RU434ZAE.js → chunk-F3BFSHVR.js} +357 -391
  7. package/dist/{chunk-UYEB2VPG.js → chunk-IOIEBLMK.js} +7 -1
  8. package/dist/{chunk-EU3I7GFB.js → chunk-STB6WMU7.js} +1 -1
  9. package/dist/{chunk-JE3MXMPW.js → chunk-UTECVGQQ.js} +93 -202
  10. package/dist/{chunk-ZP6Z6DFX.js → chunk-YFVVYZCS.js} +37 -5
  11. package/dist/{client-D1dLzWu0.d.ts → client-C9kc4cog.d.cts} +9 -3
  12. package/dist/{client-CVn0R_eM.d.cts → client-eyjf4knu.d.ts} +9 -3
  13. package/dist/graphql/objects/index.cjs +7 -1
  14. package/dist/graphql/objects/index.d.cts +3 -3
  15. package/dist/graphql/objects/index.d.ts +3 -3
  16. package/dist/graphql/objects/index.js +1 -1
  17. package/dist/index.browser.d.ts +27 -435
  18. package/dist/index.browser.js +1613 -3639
  19. package/dist/index.node.cjs +1613 -3640
  20. package/dist/index.node.d.cts +7 -8
  21. package/dist/index.node.d.ts +7 -8
  22. package/dist/index.node.js +5 -7
  23. package/dist/native/index.react-native.cjs +1613 -3640
  24. package/dist/native/index.react-native.d.cts +27 -435
  25. package/dist/native/index.react-native.d.ts +27 -435
  26. package/dist/native/index.react-native.js +1613 -3639
  27. package/dist/proto/spark.cjs +93 -202
  28. package/dist/proto/spark.d.cts +1 -1
  29. package/dist/proto/spark.d.ts +1 -1
  30. package/dist/proto/spark.js +1 -1
  31. package/dist/proto/spark_token.cjs +36 -4
  32. package/dist/proto/spark_token.d.cts +4 -1
  33. package/dist/proto/spark_token.d.ts +4 -1
  34. package/dist/proto/spark_token.js +2 -2
  35. package/dist/{spark-2Fxnvl8K.d.cts → spark-d6w3BLGZ.d.cts} +10 -328
  36. package/dist/{spark-2Fxnvl8K.d.ts → spark-d6w3BLGZ.d.ts} +10 -328
  37. package/dist/{spark-wallet.node-DlhZiDgY.d.ts → spark-wallet.node-MReThHBY.d.ts} +6 -7
  38. package/dist/{spark-wallet.node-xKJXzAEd.d.cts → spark-wallet.node-eR0svGws.d.cts} +6 -7
  39. package/dist/tests/test-utils.cjs +409 -2429
  40. package/dist/tests/test-utils.d.cts +3 -3
  41. package/dist/tests/test-utils.d.ts +3 -3
  42. package/dist/tests/test-utils.js +5 -5
  43. package/dist/types/index.cjs +100 -203
  44. package/dist/types/index.d.cts +2 -2
  45. package/dist/types/index.d.ts +2 -2
  46. package/dist/types/index.js +3 -3
  47. package/package.json +3 -3
  48. package/src/graphql/client.ts +36 -1
  49. package/src/graphql/objects/LightningSendRequestStatus.ts +25 -13
  50. package/src/proto/common.ts +1 -1
  51. package/src/proto/google/protobuf/descriptor.ts +1 -1
  52. package/src/proto/google/protobuf/duration.ts +1 -1
  53. package/src/proto/google/protobuf/empty.ts +1 -1
  54. package/src/proto/google/protobuf/timestamp.ts +1 -1
  55. package/src/proto/mock.ts +1 -1
  56. package/src/proto/spark.ts +113 -446
  57. package/src/proto/spark_authn.ts +1 -1
  58. package/src/proto/spark_token.ts +41 -2
  59. package/src/proto/validate/validate.ts +1 -1
  60. package/src/services/connection/connection.ts +23 -60
  61. package/src/services/coop-exit.ts +3 -5
  62. package/src/services/deposit.ts +1 -1
  63. package/src/services/lightning.ts +1 -1
  64. package/src/services/signing.ts +5 -6
  65. package/src/services/transfer.ts +250 -240
  66. package/src/services/wallet-config.ts +22 -5
  67. package/src/spark-wallet/proto-descriptors.ts +1 -1
  68. package/src/spark-wallet/proto-reflection.ts +0 -2
  69. package/src/spark-wallet/spark-wallet.ts +2 -2
  70. package/src/spark_descriptors.pb +0 -0
  71. package/src/tests/bufbuild-reflection.test.ts +2 -3
  72. package/src/tests/integration/coop-exit.test.ts +6 -1
  73. package/src/tests/integration/htlc.test.ts +5 -0
  74. package/src/tests/integration/lightning.test.ts +24 -4
  75. package/src/tests/integration/time-sync.test.ts +18 -0
  76. package/src/tests/integration/transfer.test.ts +42 -7
  77. package/src/tests/ssp-client-retry.test.ts +161 -0
  78. package/src/tests/token-hashing.test.ts +92 -0
  79. package/src/utils/token-hashing.ts +4 -51
  80. package/src/utils/transaction.ts +1 -2
  81. package/src/utils/unilateral-exit.ts +139 -142
@@ -7,7 +7,11 @@ import * as btc from "@scure/btc-signer";
7
7
  import * as psbt from "@scure/btc-signer/psbt";
8
8
  import type { SparkServiceClient } from "../proto/spark.js";
9
9
  import { TreeNode } from "../proto/spark.js";
10
- import { getTxFromRawTxHex, getTxId } from "../utils/bitcoin.js";
10
+ import {
11
+ getTxFromRawTxHex,
12
+ getTxId,
13
+ getTxEstimatedVbytesSizeByNumberOfInputsOutputs,
14
+ } from "../utils/bitcoin.js";
11
15
  import { isTxBroadcast } from "../utils/mempool.js";
12
16
 
13
17
  // Types
@@ -66,132 +70,29 @@ export function isEphemeralAnchorOutput(
66
70
  ): boolean {
67
71
  return Boolean(
68
72
  amount === 0n &&
69
- script &&
70
- // Pattern 1: Bare OP_TRUE (single byte 0x51)
71
- ((script.length === 1 && script[0] === 0x51) ||
72
- // Pattern 2: Push OP_TRUE (two bytes 0x01 0x51) - MALFORMED but we detect it
73
- (script.length === 2 && script[0] === 0x01 && script[1] === 0x51) ||
74
- // Pattern 3: Bitcoin v29 ephemeral anchor script (7 bytes: 015152014e0173)
75
- (script.length === 7 &&
76
- script[0] === 0x01 &&
77
- script[1] === 0x51 &&
78
- script[2] === 0x52 &&
79
- script[3] === 0x01 &&
80
- script[4] === 0x4e &&
81
- script[5] === 0x01 &&
82
- script[6] === 0x73) ||
83
- // Pattern 4: Bitcoin ephemeral anchor OP_1 + push 2 bytes (4 bytes: 51024e73)
84
- (script.length === 4 &&
85
- script[0] === 0x51 &&
86
- script[1] === 0x02 &&
87
- script[2] === 0x4e &&
88
- script[3] === 0x73)),
73
+ script &&
74
+ // Pattern 1: Bare OP_TRUE (single byte 0x51)
75
+ ((script.length === 1 && script[0] === 0x51) ||
76
+ // Pattern 2: Push OP_TRUE (two bytes 0x01 0x51) - MALFORMED but we detect it
77
+ (script.length === 2 && script[0] === 0x01 && script[1] === 0x51) ||
78
+ // Pattern 3: Bitcoin v29 ephemeral anchor script (7 bytes: 015152014e0173)
79
+ (script.length === 7 &&
80
+ script[0] === 0x01 &&
81
+ script[1] === 0x51 &&
82
+ script[2] === 0x52 &&
83
+ script[3] === 0x01 &&
84
+ script[4] === 0x4e &&
85
+ script[5] === 0x01 &&
86
+ script[6] === 0x73) ||
87
+ // Pattern 4: Bitcoin ephemeral anchor OP_1 + push 2 bytes (4 bytes: 51024e73)
88
+ (script.length === 4 &&
89
+ script[0] === 0x51 &&
90
+ script[1] === 0x02 &&
91
+ script[2] === 0x4e &&
92
+ script[3] === 0x73)),
89
93
  );
90
94
  }
91
95
 
92
- // Main function to generate unilateral exit tx chains for broadcasting non-CPFP transactions
93
- export async function constructUnilateralExitTxs(
94
- nodeHexStrings: string[],
95
- sparkClient?: SparkServiceClient,
96
- network?: any, // Network enum from the proto
97
- ): Promise<TxChain[]> {
98
- const result: TxChain[] = [];
99
-
100
- // Convert hex strings to TreeNode objects
101
- const nodes = nodeHexStrings.map((hex) => TreeNode.decode(hexToBytes(hex)));
102
-
103
- // Create a map of nodes by ID for easy lookup
104
- const nodeMap = new Map<string, TreeNode>();
105
- for (const node of nodes) {
106
- nodeMap.set(node.id, node);
107
- }
108
-
109
- // For each provided node, build its complete chain to the root
110
- for (const node of nodes) {
111
- const transactions: string[] = [];
112
-
113
- // Build the chain from this node to the root
114
- const chain: TreeNode[] = [];
115
- let currentNode = node;
116
-
117
- // Walk up the chain to find the root, querying for each parent
118
- while (currentNode) {
119
- chain.unshift(currentNode);
120
-
121
- if (currentNode.parentNodeId) {
122
- // Check if we already have the parent in our map (from previous queries)
123
- let parentNode = nodeMap.get(currentNode.parentNodeId);
124
-
125
- if (!parentNode && sparkClient) {
126
- // Query for the parent node
127
- try {
128
- const response = await sparkClient.query_nodes({
129
- source: {
130
- $case: "nodeIds",
131
- nodeIds: {
132
- nodeIds: [currentNode.parentNodeId],
133
- },
134
- },
135
- includeParents: true,
136
- network: network || 0, // Default to mainnet if not provided
137
- });
138
-
139
- parentNode = response.nodes[currentNode.parentNodeId];
140
-
141
- if (parentNode) {
142
- // Add to our map for future use
143
- nodeMap.set(currentNode.parentNodeId, parentNode);
144
- }
145
- } catch (error) {
146
- console.warn(
147
- `Failed to query parent node ${currentNode.parentNodeId}: ${error}`,
148
- );
149
- break;
150
- }
151
- }
152
-
153
- if (parentNode) {
154
- currentNode = parentNode;
155
- } else {
156
- if (!sparkClient) {
157
- console.warn(
158
- `Parent node ${currentNode.parentNodeId} not found. Provide a sparkClient to fetch missing parents.`,
159
- );
160
- } else {
161
- console.warn(
162
- `Parent node ${currentNode.parentNodeId} not found in database. Chain may be incomplete.`,
163
- );
164
- }
165
- break;
166
- }
167
- } else {
168
- // We've reached the root
169
- break;
170
- }
171
- }
172
-
173
- // Now walk down the chain from root to leaf to build transactions
174
- for (const chainNode of chain) {
175
- // Add node tx
176
- const nodeTx = bytesToHex(chainNode.nodeTx);
177
- transactions.push(nodeTx);
178
-
179
- // If this is the original node we started with, also add its refund tx
180
- if (chainNode.id === node.id) {
181
- const refundTx = bytesToHex(chainNode.refundTx);
182
- transactions.push(refundTx);
183
- }
184
- }
185
-
186
- result.push({
187
- leafId: node.id,
188
- transactions,
189
- });
190
- }
191
-
192
- return result;
193
- }
194
-
195
96
  // Main function to generate unilateral exit tx chains for broadcasting CPFP transactions
196
97
  export async function constructUnilateralExitFeeBumpPackages(
197
98
  nodeHexStrings: string[],
@@ -279,6 +180,10 @@ export async function constructUnilateralExitFeeBumpPackages(
279
180
 
280
181
  // Walk up the chain to find the root, querying for each parent
281
182
  while (currentNode) {
183
+ // Only proceed with nodes that are available for exit
184
+ if (currentNode.status !== "AVAILABLE") {
185
+ break;
186
+ }
282
187
  chain.unshift(currentNode);
283
188
 
284
189
  if (currentNode.parentNodeId) {
@@ -379,7 +284,7 @@ export async function constructUnilateralExitFeeBumpPackages(
379
284
  const {
380
285
  feeBumpPsbt: nodeFeeBumpPsbt,
381
286
  usedUtxos,
382
- correctedParentTx,
287
+ parentTx,
383
288
  } = constructFeeBumpTx(nodeTxHex, availableUtxos, feeRate, undefined);
384
289
 
385
290
  const feeBumpTx = btc.Transaction.fromPSBT(hexToBytes(nodeFeeBumpPsbt));
@@ -414,7 +319,7 @@ export async function constructUnilateralExitFeeBumpPackages(
414
319
  });
415
320
 
416
321
  // Use the corrected parent transaction if it was fixed
417
- const finalNodeTx = correctedParentTx || nodeTxHex;
322
+ const finalNodeTx = parentTx || nodeTxHex;
418
323
  txPackages.push({ tx: finalNodeTx, feeBumpPsbt: nodeFeeBumpPsbt });
419
324
 
420
325
  // If this is the original node we started with, also add its refund tx
@@ -509,13 +414,89 @@ export function hash160(data: Uint8Array): Uint8Array {
509
414
  return ripemd160(sha256Hash);
510
415
  }
511
416
 
417
+ // Helper function to calculate transaction vSize from hex
418
+ function calculateTransactionVSize(txHex: string): number {
419
+ try {
420
+ const tx = getTxFromRawTxHex(txHex);
421
+ const numInputs = tx.inputsLength;
422
+ const numOutputs = tx.outputsLength;
423
+
424
+ return getTxEstimatedVbytesSizeByNumberOfInputsOutputs(
425
+ numInputs,
426
+ numOutputs,
427
+ );
428
+ } catch (error) {
429
+ console.warn(
430
+ `Failed to calculate transaction vSize: ${error}, falling back to default estimate`,
431
+ );
432
+ // Fall back to default for typical transactions
433
+ return 191;
434
+ }
435
+ }
436
+
437
+ // Helper function to select optimal UTXOs for fee payment
438
+ function selectUtxosForFee(
439
+ utxos: Utxo[],
440
+ parentTxSize: number,
441
+ feeRate?: FeeRate,
442
+ ): Utxo[] {
443
+ if (utxos.length === 0) {
444
+ throw new Error("No UTXOs available for selection");
445
+ }
446
+
447
+ // Sort UTXOs by value in descending order (largest first)
448
+ const sortedUtxos = [...utxos].sort((a, b) => {
449
+ if (a.value > b.value) return -1;
450
+ if (a.value < b.value) return 1;
451
+ return 0;
452
+ });
453
+
454
+ const selectedUtxos: Utxo[] = [];
455
+ let totalValue = 0n;
456
+
457
+ // If no fee rate provided, use all available UTXOs (fallback behavior)
458
+ if (!feeRate?.satPerVbyte) {
459
+ return sortedUtxos;
460
+ }
461
+
462
+ // Try to find the minimum number of UTXOs needed to cover the fee
463
+ for (let i = 0; i < sortedUtxos.length; i++) {
464
+ const utxo = sortedUtxos[i];
465
+ if (!utxo) {
466
+ continue;
467
+ }
468
+ selectedUtxos.push(utxo);
469
+ totalValue += utxo.value;
470
+
471
+ // Calculate child transaction size with current number of selected UTXOs
472
+ const childTxSize = getTxEstimatedVbytesSizeByNumberOfInputsOutputs(
473
+ selectedUtxos.length + 1, // selected UTXOs + ephemeral anchor
474
+ 1, // single change output
475
+ );
476
+
477
+ // Calculate total fee needed for CPFP package
478
+ const totalVbytes = parentTxSize + childTxSize;
479
+ const requiredFee = BigInt(Math.ceil(totalVbytes * feeRate.satPerVbyte));
480
+
481
+ // Minimum change amount (dust threshold)
482
+ const minChange = 546n;
483
+
484
+ // Check if we have enough to cover fee + minimum change
485
+ if (totalValue >= requiredFee + minChange) {
486
+ return selectedUtxos;
487
+ }
488
+ }
489
+
490
+ return sortedUtxos;
491
+ }
492
+
512
493
  // Helper function to construct a fee bump tx for a given tx using available UTXOs
513
494
  export function constructFeeBumpTx(
514
495
  txHex: string,
515
496
  utxos: Utxo[],
516
497
  feeRate: FeeRate,
517
498
  previousFeeBumpTx?: string, // Optional previous fee bump tx to chain from
518
- ): { feeBumpPsbt: string; usedUtxos: Utxo[]; correctedParentTx?: string } {
499
+ ): { feeBumpPsbt: string; usedUtxos: Utxo[]; parentTx?: string } {
519
500
  // Validate inputs first
520
501
  if (!txHex || txHex.length === 0) {
521
502
  throw new Error("Transaction hex string is empty or undefined");
@@ -526,18 +507,17 @@ export function constructFeeBumpTx(
526
507
  }
527
508
 
528
509
  // Check for and fix malformed ephemeral anchor BEFORE parsing
529
- let correctedTxHex = txHex;
530
510
 
531
511
  // Decode the parent tx using the utility function with error handling
532
512
  let parentTx: any;
533
513
  try {
534
- parentTx = getTxFromRawTxHex(correctedTxHex);
514
+ parentTx = getTxFromRawTxHex(txHex);
535
515
  if (!parentTx) {
536
516
  throw new Error("getTxFromRawTxHex returned null/undefined");
537
517
  }
538
518
  } catch (parseError) {
539
519
  throw new Error(
540
- `Failed to parse parent transaction hex: ${parseError}. Transaction hex: ${correctedTxHex}`,
520
+ `Failed to parse parent transaction hex: ${parseError}. Transaction hex: ${txHex}`,
541
521
  );
542
522
  }
543
523
 
@@ -591,12 +571,16 @@ export function constructFeeBumpTx(
591
571
  throw new Error("No ephemeral anchor output found");
592
572
  if (!ephemeralAnchorOutput.script)
593
573
  throw new Error("No script found in ephemeral anchor output");
594
-
595
- // Use all available UTXOs for funding
596
574
  if (utxos.length === 0) {
597
575
  throw new Error("No UTXOs available for fee bump");
598
576
  }
599
577
 
578
+ // Calculate parent transaction size for CPFP fee calculation
579
+ const parentTxSize = calculateTransactionVSize(txHex);
580
+
581
+ // Select optimal UTXOs based on fee requirements
582
+ const selectedUtxos = selectUtxosForFee(utxos, parentTxSize, feeRate);
583
+
600
584
  // Create a new transaction using the builder pattern
601
585
  const builder = new btc.Transaction({
602
586
  version: 3,
@@ -604,15 +588,15 @@ export function constructFeeBumpTx(
604
588
  allowLegacyWitnessUtxo: true,
605
589
  }); // ✅ set v3 in constructor
606
590
 
607
- // Track total value and process each funding UTXO
591
+ // Track total value and process each selected funding UTXO
608
592
  let totalValue = 0n;
609
593
  const processedUtxos: Array<{
610
594
  utxo: Utxo;
611
595
  p2wpkhScript: Uint8Array;
612
596
  }> = [];
613
597
 
614
- for (let i = 0; i < utxos.length; i++) {
615
- const fundingUtxo = utxos[i];
598
+ for (let i = 0; i < selectedUtxos.length; i++) {
599
+ const fundingUtxo = selectedUtxos[i];
616
600
  if (!fundingUtxo) {
617
601
  throw new Error(`UTXO at index ${i} is undefined`);
618
602
  }
@@ -648,20 +632,33 @@ export function constructFeeBumpTx(
648
632
  });
649
633
  }
650
634
 
651
- // Add ephemeral anchor output as the last input - use direct script spend (not P2WSH)
635
+ // Add ephemeral anchor output as the last input - use direct script spend
652
636
  builder.addInput({
653
637
  txid: parentTxIdFromLib,
654
638
  index: ephemeralAnchorIndex,
655
639
  sequence: 0xffffffff,
656
640
  witnessUtxo: {
657
- script: ephemeralAnchorOutput.script, // Use the original script directly (not P2WSH wrapped)
641
+ script: ephemeralAnchorOutput.script,
658
642
  amount: 0n,
659
643
  },
660
644
  });
661
645
 
662
- // Use fixed fee of 1500 satoshis
663
- // TODO(aakelrod): fix this
664
- const fee = 1500n;
646
+ // Calculate child transaction size based on number of inputs and outputs
647
+ const childTxSize = getTxEstimatedVbytesSizeByNumberOfInputsOutputs(
648
+ selectedUtxos.length + 1, // funding UTXOs + ephemeral anchor
649
+ 1, // single change output
650
+ );
651
+
652
+ const totalVbytes = parentTxSize + childTxSize;
653
+
654
+ // If no fee rate provided, fall back to fixed 1500 satoshis
655
+ let fee: bigint;
656
+ if (feeRate?.satPerVbyte) {
657
+ // Calculate total fee needed for the entire package at target rate
658
+ fee = BigInt(Math.ceil(totalVbytes * feeRate.satPerVbyte));
659
+ } else {
660
+ fee = 1500n;
661
+ }
665
662
 
666
663
  // Minimum change amount (546 satoshis for a standard output)
667
664
  const remainingValue = totalValue - fee;
@@ -716,7 +713,7 @@ export function constructFeeBumpTx(
716
713
  // Return both the fee bump transaction hex and the UTXOs used
717
714
  return {
718
715
  feeBumpPsbt: psbtHex,
719
- usedUtxos: utxos,
720
- correctedParentTx: correctedTxHex !== txHex ? correctedTxHex : undefined,
716
+ usedUtxos: selectedUtxos,
717
+ parentTx: txHex !== txHex ? txHex : undefined,
721
718
  };
722
719
  }