@buildonspark/spark-sdk 0.3.8 → 0.4.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 (94) hide show
  1. package/CHANGELOG.md +18 -0
  2. package/android/build.gradle +1 -1
  3. package/android/src/main/java/uniffi/uniffi/spark_frost/spark_frost.kt +1361 -1367
  4. package/android/src/main/jniLibs/arm64-v8a/libuniffi_spark_frost.so +0 -0
  5. package/android/src/main/jniLibs/armeabi-v7a/libuniffi_spark_frost.so +0 -0
  6. package/android/src/main/jniLibs/x86/libuniffi_spark_frost.so +0 -0
  7. package/android/src/main/jniLibs/x86_64/libuniffi_spark_frost.so +0 -0
  8. package/dist/bare/index.cjs +1342 -1346
  9. package/dist/bare/index.d.cts +114 -68
  10. package/dist/bare/index.d.ts +114 -68
  11. package/dist/bare/index.js +1266 -1279
  12. package/dist/{chunk-FXIESWE6.js → chunk-27ILUWDJ.js} +1 -1
  13. package/dist/{chunk-5ASXVNTM.js → chunk-4YFT7DAE.js} +1 -1
  14. package/dist/{chunk-FP2CRVQH.js → chunk-G3LHXHF3.js} +1045 -1308
  15. package/dist/{chunk-XI6FCNYG.js → chunk-JLF6WJ7K.js} +1 -1
  16. package/dist/{chunk-3LPEQGVJ.js → chunk-LOXWCMZL.js} +1 -1
  17. package/dist/{chunk-5HU5W56H.js → chunk-WICAF6BS.js} +2 -2
  18. package/dist/{chunk-VFN34EOX.js → chunk-YEBEN7XD.js} +258 -0
  19. package/dist/{client-pNpGP15j.d.cts → client-BIqiUNy4.d.cts} +1 -1
  20. package/dist/{client-By-N7oJS.d.ts → client-BaQf-5gD.d.ts} +1 -1
  21. package/dist/debug.cjs +1330 -1336
  22. package/dist/debug.d.cts +20 -16
  23. package/dist/debug.d.ts +20 -16
  24. package/dist/debug.js +5 -5
  25. package/dist/graphql/objects/index.d.cts +3 -3
  26. package/dist/graphql/objects/index.d.ts +3 -3
  27. package/dist/index.cjs +1363 -1365
  28. package/dist/index.d.cts +7 -7
  29. package/dist/index.d.ts +7 -7
  30. package/dist/index.js +32 -24
  31. package/dist/index.node.cjs +1363 -1365
  32. package/dist/index.node.d.cts +7 -7
  33. package/dist/index.node.d.ts +7 -7
  34. package/dist/index.node.js +31 -23
  35. package/dist/{logging-DMFVY384.d.ts → logging-BNGm6dBp.d.ts} +42 -37
  36. package/dist/{logging-DxLp34Xm.d.cts → logging-D3IfXfHG.d.cts} +42 -37
  37. package/dist/native/index.react-native.cjs +1505 -1366
  38. package/dist/native/index.react-native.d.cts +112 -58
  39. package/dist/native/index.react-native.d.ts +112 -58
  40. package/dist/native/index.react-native.js +1415 -1289
  41. package/dist/proto/spark.cjs +258 -0
  42. package/dist/proto/spark.d.cts +1 -1
  43. package/dist/proto/spark.d.ts +1 -1
  44. package/dist/proto/spark.js +5 -1
  45. package/dist/proto/spark_token.d.cts +1 -1
  46. package/dist/proto/spark_token.d.ts +1 -1
  47. package/dist/proto/spark_token.js +2 -2
  48. package/dist/{spark-By6yHsrk.d.cts → spark-DOpheE8_.d.cts} +39 -2
  49. package/dist/{spark-By6yHsrk.d.ts → spark-DOpheE8_.d.ts} +39 -2
  50. package/dist/{spark-wallet.browser-C1dQknVj.d.ts → spark-wallet.browser-B2rGwjuM.d.ts} +2 -2
  51. package/dist/{spark-wallet.browser-CNMo3IvO.d.cts → spark-wallet.browser-Ck9No4Ks.d.cts} +2 -2
  52. package/dist/{spark-wallet.node-Og6__NMh.d.ts → spark-wallet.node-BqmKsGPs.d.ts} +2 -2
  53. package/dist/{spark-wallet.node-BZJhJZKq.d.cts → spark-wallet.node-C2TIkyt4.d.cts} +2 -2
  54. package/dist/tests/test-utils.cjs +1304 -1236
  55. package/dist/tests/test-utils.d.cts +18 -4
  56. package/dist/tests/test-utils.d.ts +18 -4
  57. package/dist/tests/test-utils.js +7 -7
  58. package/dist/{token-transactions-CLR3rnYi.d.cts → token-transactions-Db8mkjnU.d.cts} +2 -2
  59. package/dist/{token-transactions-C7yefB2S.d.ts → token-transactions-DoMcrxXQ.d.ts} +2 -2
  60. package/dist/types/index.cjs +256 -0
  61. package/dist/types/index.d.cts +2 -2
  62. package/dist/types/index.d.ts +2 -2
  63. package/dist/types/index.js +2 -2
  64. package/dist/{wallet-config-BoyMVa6n.d.ts → wallet-config-Bg3kWltL.d.ts} +42 -35
  65. package/dist/{wallet-config-xom-9UFF.d.cts → wallet-config-CuZKNo9S.d.cts} +42 -35
  66. package/ios/spark_frostFFI.xcframework/ios-arm64/SparkFrost +0 -0
  67. package/ios/spark_frostFFI.xcframework/ios-arm64/spark_frostFFI.framework/spark_frostFFI +0 -0
  68. package/ios/spark_frostFFI.xcframework/ios-arm64_x86_64-simulator/SparkFrost +0 -0
  69. package/ios/spark_frostFFI.xcframework/ios-arm64_x86_64-simulator/spark_frostFFI.framework/spark_frostFFI +0 -0
  70. package/ios/spark_frostFFI.xcframework/macos-arm64_x86_64/spark_frostFFI.framework/spark_frostFFI +0 -0
  71. package/package.json +1 -1
  72. package/src/index.react-native.ts +8 -2
  73. package/src/proto/spark.ts +348 -4
  74. package/src/services/config.ts +5 -0
  75. package/src/services/coop-exit.ts +26 -107
  76. package/src/services/deposit.ts +12 -48
  77. package/src/services/signing.ts +62 -49
  78. package/src/services/transfer.ts +437 -722
  79. package/src/services/wallet-config.ts +10 -0
  80. package/src/services/xhr-transport.ts +13 -3
  81. package/src/signer/signer.react-native.ts +73 -1
  82. package/src/spark-wallet/proto-descriptors.ts +1 -1
  83. package/src/spark-wallet/spark-wallet.ts +162 -315
  84. package/src/spark-wallet/types.ts +2 -2
  85. package/src/spark_descriptors.pb +0 -0
  86. package/src/tests/integration/lightning.test.ts +1 -27
  87. package/src/tests/integration/static_deposit.test.ts +6 -9
  88. package/src/tests/integration/unilateral-exit.test.ts +117 -0
  89. package/src/tests/optimize.test.ts +31 -1
  90. package/src/tests/utils/signing.ts +33 -0
  91. package/src/tests/utils/test-faucet.ts +61 -0
  92. package/src/utils/optimize.ts +42 -0
  93. package/src/utils/transaction.ts +250 -249
  94. package/src/utils/unilateral-exit.ts +1 -40
@@ -1,4 +1,4 @@
1
- import { isNode, isObject, mapCurrencyAmount } from "@lightsparkdev/core";
1
+ import { isObject, mapCurrencyAmount } from "@lightsparkdev/core";
2
2
  import { secp256k1 } from "@noble/curves/secp256k1";
3
3
  import {
4
4
  bytesToHex,
@@ -46,7 +46,6 @@ import {
46
46
  SigningJob,
47
47
  SubscribeToEventsResponse,
48
48
  Transfer,
49
- TransferEvent,
50
49
  TransferStatus,
51
50
  TransferType,
52
51
  TreeNode,
@@ -87,10 +86,7 @@ import {
87
86
  NetworkType,
88
87
  } from "../utils/network.js";
89
88
  import { sumAvailableTokens } from "../utils/token-transactions.js";
90
- import {
91
- doesLeafNeedRefresh,
92
- getCurrentTimelock,
93
- } from "../utils/transaction.js";
89
+ import { doesTxnNeedRenewed, isZeroTimelock } from "../utils/transaction.js";
94
90
 
95
91
  import { sha256 } from "@noble/hashes/sha2";
96
92
  import { trace, Tracer } from "@opentelemetry/api";
@@ -128,7 +124,6 @@ import {
128
124
  import { chunkArray } from "../utils/chunkArray.js";
129
125
  import { getFetch } from "../utils/fetch.js";
130
126
  import { addPublicKeys } from "../utils/keys.js";
131
- import { maximizeUnilateralExit } from "../utils/optimize.js";
132
127
  import { RetryContext, withRetry } from "../utils/retry.js";
133
128
  import {
134
129
  Bech32mTokenIdentifier,
@@ -156,6 +151,7 @@ import type {
156
151
  UserTokenMetadata,
157
152
  } from "./types.js";
158
153
  import { SparkWalletEvent } from "./types.js";
154
+ import { optimize, shouldOptimize } from "../utils/optimize.js";
159
155
 
160
156
  /**
161
157
  * The SparkWallet class is the primary interface for interacting with the Spark network.
@@ -303,16 +299,15 @@ export abstract class SparkWallet extends EventEmitter<SparkWalletEvents> {
303
299
  await this.claimTransfer({
304
300
  transfer: event.transfer.transfer,
305
301
  emit: true,
306
- optimize: true,
307
302
  });
308
303
  }
309
304
  } else if (isDepositStreamEvent(event)) {
310
305
  const deposit = event.deposit.deposit;
311
- const newLeaf = await this.transferService.extendTimelock(deposit);
312
- await this.transferLeavesToSelf(newLeaf.nodes, {
313
- type: KeyDerivationType.LEAF,
314
- path: deposit.id,
306
+
307
+ await this.withLeaves(async () => {
308
+ this.leaves.push(deposit);
315
309
  });
310
+
316
311
  this.emit(
317
312
  SparkWalletEvent.DepositConfirmed,
318
313
  deposit.id,
@@ -531,20 +526,6 @@ export abstract class SparkWallet extends EventEmitter<SparkWalletEvents> {
531
526
  .map(([_, node]) => node);
532
527
  }
533
528
 
534
- private async checkExtendLeaves(leaves: TreeNode[]): Promise<void> {
535
- await this.withLeaves(async () => {
536
- for (const leaf of leaves) {
537
- if (!leaf.parentNodeId && leaf.status === "AVAILABLE") {
538
- const res = await this.transferService.extendTimelock(leaf);
539
- await this.transferLeavesToSelf(res.nodes, {
540
- type: KeyDerivationType.LEAF,
541
- path: leaf.id,
542
- });
543
- }
544
- }
545
- });
546
- }
547
-
548
529
  private verifyKey(
549
530
  pubkey1: Uint8Array,
550
531
  pubkey2: Uint8Array,
@@ -687,76 +668,101 @@ export abstract class SparkWallet extends EventEmitter<SparkWalletEvents> {
687
668
  return nodes;
688
669
  }
689
670
 
690
- private areLeavesInefficient() {
691
- const totalAmount = this.getInternalBalance();
692
-
693
- if (this.leaves.length <= 1) {
694
- return false;
671
+ public async *optimizeLeaves(
672
+ multiplicity: number | undefined = undefined,
673
+ ): AsyncGenerator<
674
+ {
675
+ step: number;
676
+ total: number;
677
+ controller: AbortController;
678
+ },
679
+ void,
680
+ void
681
+ > {
682
+ const multiplicityValue =
683
+ multiplicity ?? this.config.getOptimizationOptions().multiplicity ?? 0;
684
+ if (multiplicityValue < 0) {
685
+ throw new ValidationError("Multiplicity cannot be negative");
686
+ } else if (multiplicityValue > 5) {
687
+ throw new ValidationError("Multiplicity cannot be greater than 5");
695
688
  }
696
689
 
697
- const nextLowerPowerOfTwo = 31 - Math.clz32(totalAmount);
690
+ if (
691
+ this.optimizationInProgress ||
692
+ !shouldOptimize(
693
+ this.leaves.map((leaf) => leaf.value),
694
+ multiplicityValue,
695
+ )
696
+ ) {
697
+ return;
698
+ }
698
699
 
699
- let remainingAmount = totalAmount;
700
- let optimalLeavesLength = 0;
700
+ const controller = new AbortController();
701
+ const release = await this.leavesMutex.acquire();
702
+ try {
703
+ this.optimizationInProgress = true;
701
704
 
702
- for (let i = nextLowerPowerOfTwo; i >= 0; i--) {
703
- const denomination = 2 ** i;
704
- while (remainingAmount >= denomination) {
705
- remainingAmount -= denomination;
706
- optimalLeavesLength++;
705
+ this.leaves = await this.getLeaves();
706
+ const swaps = optimize(
707
+ this.leaves.map((leaf) => leaf.value),
708
+ multiplicityValue,
709
+ );
710
+ if (swaps.length === 0) {
711
+ return;
707
712
  }
708
- }
709
-
710
- return this.leaves.length > optimalLeavesLength * 5;
711
- }
712
713
 
713
- private async optimizeLeaves() {
714
- if (this.optimizationInProgress || !this.areLeavesInefficient()) {
715
- return;
716
- }
714
+ yield {
715
+ step: 0,
716
+ total: swaps.length,
717
+ controller,
718
+ };
717
719
 
718
- await this.withLeaves(async () => {
719
- this.optimizationInProgress = true;
720
- try {
721
- this.leaves = await this.getLeaves();
722
- const swaps = maximizeUnilateralExit(
723
- this.leaves.map((leaf) => leaf.value),
724
- );
720
+ // Build a map from the denomination to the indices
721
+ const valueToNodes = new Map<number, TreeNode[]>();
722
+ this.leaves.forEach((leaf) => {
723
+ if (!valueToNodes.has(leaf.value)) {
724
+ valueToNodes.set(leaf.value, []);
725
+ }
726
+ valueToNodes.get(leaf.value)!.push(leaf);
727
+ });
725
728
 
726
- // Build a map from the denomination to the nodes
727
- const valueToNodes = new Map<number, TreeNode[]>();
728
- this.leaves.forEach((leaf) => {
729
- if (!valueToNodes.has(leaf.value)) {
730
- valueToNodes.set(leaf.value, []);
731
- }
732
- valueToNodes.get(leaf.value)!.push(leaf);
733
- });
729
+ // Select the leaves to send for each swap.
730
+ for (const swap of swaps) {
731
+ if (controller.signal.aborted) {
732
+ break;
733
+ }
734
734
 
735
- // Select the leaves to send for each swap.
736
- for (const swap of swaps) {
737
- const leavesToSend: TreeNode[] = [];
738
- for (const leafValue of swap.inLeaves) {
739
- const nodes = valueToNodes.get(leafValue);
740
- if (nodes && nodes.length > 0) {
741
- const node = nodes.shift()!;
742
- leavesToSend.push(node);
743
- } else {
744
- throw new InternalValidationError(
745
- `No unused leaf with value ${leafValue} found in leaves`,
746
- );
747
- }
735
+ const leavesToSend: TreeNode[] = [];
736
+ for (const leafValue of swap.inLeaves) {
737
+ const nodes = valueToNodes.get(leafValue);
738
+ if (nodes && nodes.length > 0) {
739
+ const node = nodes.shift()!;
740
+ leavesToSend.push(node);
741
+ } else {
742
+ throw new InternalValidationError(
743
+ `No unused leaf with value ${leafValue} found in leaves`,
744
+ );
748
745
  }
749
- await this.requestLeavesSwap({
750
- leaves: leavesToSend,
751
- targetAmounts: swap.outLeaves,
752
- });
753
746
  }
754
747
 
755
- this.leaves = await this.getLeaves();
756
- } finally {
757
- this.optimizationInProgress = false;
748
+ // TODO: Parallelize this.
749
+ await this.requestLeavesSwap({
750
+ leaves: leavesToSend,
751
+ targetAmounts: swap.outLeaves,
752
+ });
753
+
754
+ yield {
755
+ step: swaps.indexOf(swap) + 1,
756
+ total: swaps.length,
757
+ controller,
758
+ };
758
759
  }
759
- });
760
+
761
+ this.leaves = await this.getLeaves();
762
+ } finally {
763
+ this.optimizationInProgress = false;
764
+ release();
765
+ }
760
766
  }
761
767
 
762
768
  private async syncWallet() {
@@ -764,15 +770,15 @@ export abstract class SparkWallet extends EventEmitter<SparkWalletEvents> {
764
770
 
765
771
  let leaves = await this.getLeaves();
766
772
 
767
- leaves = await this.checkRefreshTimelockNodes(leaves);
768
- leaves = await this.checkExtendTimeLockNodes(leaves);
773
+ leaves = await this.checkRenewLeaves(leaves);
769
774
 
770
775
  this.leaves = leaves;
771
- this.checkExtendLeaves(leaves);
772
776
 
773
- this.optimizeLeaves().catch((e) => {
774
- console.error("Failed to optimize leaves", e);
775
- });
777
+ if (this.config.getOptimizationOptions().auto) {
778
+ for await (const _ of this.optimizeLeaves()) {
779
+ // run all optimizer steps, do nothing with them
780
+ }
781
+ }
776
782
  }
777
783
 
778
784
  private async withLeaves<T>(operation: () => Promise<T>): Promise<T> {
@@ -1473,7 +1479,6 @@ export abstract class SparkWallet extends EventEmitter<SparkWalletEvents> {
1473
1479
  return await this.claimTransfer({
1474
1480
  transfer: incomingTransfer,
1475
1481
  emit: false,
1476
- optimize: false,
1477
1482
  });
1478
1483
  } catch (e) {
1479
1484
  console.error("[processSwapBatch] Error details:", {
@@ -2333,29 +2338,7 @@ export abstract class SparkWallet extends EventEmitter<SparkWalletEvents> {
2333
2338
  depositTx,
2334
2339
  vout,
2335
2340
  });
2336
-
2337
- const resultingNodes: TreeNode[] = [];
2338
- for (const node of res.nodes) {
2339
- if (node.status === "AVAILABLE") {
2340
- const { nodes } = await this.transferService.extendTimelock(node);
2341
-
2342
- for (const n of nodes) {
2343
- if (n.status === "AVAILABLE") {
2344
- const transfer = await this.transferLeavesToSelf([n], {
2345
- type: KeyDerivationType.LEAF,
2346
- path: node.id,
2347
- });
2348
- resultingNodes.push(...transfer);
2349
- } else {
2350
- resultingNodes.push(n);
2351
- }
2352
- }
2353
- } else {
2354
- resultingNodes.push(node);
2355
- }
2356
- }
2357
-
2358
- return resultingNodes;
2341
+ return res.nodes;
2359
2342
  }
2360
2343
 
2361
2344
  /**
@@ -2528,6 +2511,10 @@ export abstract class SparkWallet extends EventEmitter<SparkWalletEvents> {
2528
2511
  vout,
2529
2512
  });
2530
2513
 
2514
+ await this.withLeaves(async () => {
2515
+ this.leaves.push(...nodes);
2516
+ });
2517
+
2531
2518
  return nodes;
2532
2519
  });
2533
2520
 
@@ -2637,6 +2624,7 @@ export abstract class SparkWallet extends EventEmitter<SparkWalletEvents> {
2637
2624
  : await this.claimTransfer({ transfer: pendingTransfer });
2638
2625
 
2639
2626
  const leavesToRemove = new Set(leaves.map((leaf) => leaf.id));
2627
+
2640
2628
  this.leaves = [
2641
2629
  ...this.leaves.filter((leaf) => !leavesToRemove.has(leaf.id)),
2642
2630
  ...resultNodes,
@@ -2735,8 +2723,7 @@ export abstract class SparkWallet extends EventEmitter<SparkWalletEvents> {
2735
2723
  `TreeNode group at index ${groupIndex} not found for amount ${amount} after selection`,
2736
2724
  );
2737
2725
  }
2738
- let available = await this.checkRefreshTimelockNodes(group);
2739
- available = await this.checkExtendTimeLockNodes(available);
2726
+ const available = await this.checkRenewLeaves(group);
2740
2727
 
2741
2728
  if (available.length < group.length) {
2742
2729
  throw new Error(
@@ -2787,7 +2774,7 @@ export abstract class SparkWallet extends EventEmitter<SparkWalletEvents> {
2787
2774
  transfer.id,
2788
2775
  );
2789
2776
  if (pending) {
2790
- await this.claimTransfer({ transfer: pending, optimize: true });
2777
+ await this.claimTransfer({ transfer: pending });
2791
2778
  }
2792
2779
  }
2793
2780
  return {
@@ -2842,87 +2829,55 @@ export abstract class SparkWallet extends EventEmitter<SparkWalletEvents> {
2842
2829
  };
2843
2830
  }
2844
2831
 
2845
- private async checkExtendTimeLockNodes(
2846
- nodes: TreeNode[],
2847
- ): Promise<TreeNode[]> {
2848
- const nodesToExtend: TreeNode[] = [];
2832
+ private async checkRenewLeaves(nodes: TreeNode[]): Promise<TreeNode[]> {
2833
+ const nodesToRenewNode: TreeNode[] = [];
2834
+ const nodesToRenewRefund: TreeNode[] = [];
2835
+ const nodesToRenewZeroTimelock: TreeNode[] = [];
2836
+
2849
2837
  const nodeIds: string[] = [];
2850
2838
  const validNodes: TreeNode[] = [];
2851
2839
 
2852
2840
  for (const node of nodes) {
2853
2841
  const nodeTx = getTxFromRawTxBytes(node.nodeTx);
2854
- const sequence = nodeTx.getInput(0).sequence;
2855
- if (!sequence) {
2842
+ const refundTx = getTxFromRawTxBytes(node.refundTx);
2843
+
2844
+ const nodeSequence = nodeTx.getInput(0).sequence;
2845
+ const refundSequence = refundTx.getInput(0).sequence;
2846
+
2847
+ if (nodeSequence === undefined) {
2856
2848
  throw new ValidationError("Invalid node transaction", {
2857
2849
  field: "sequence",
2858
2850
  value: nodeTx.getInput(0),
2859
2851
  expected: "Non-null sequence",
2860
2852
  });
2861
2853
  }
2862
- const needsRefresh = doesLeafNeedRefresh(sequence, true);
2863
- if (needsRefresh) {
2864
- nodesToExtend.push(node);
2865
- nodeIds.push(node.id);
2866
- } else {
2867
- validNodes.push(node);
2868
- }
2869
- }
2870
-
2871
- if (nodesToExtend.length === 0) {
2872
- return validNodes;
2873
- }
2874
-
2875
- const nodesToAdd: TreeNode[] = [];
2876
- for (const node of nodesToExtend) {
2877
- const { nodes } = await this.transferService.extendTimelock(node);
2878
- this.leaves = this.leaves.filter((leaf) => leaf.id !== node.id);
2879
- const newNodes = await this.transferLeavesToSelf(nodes, {
2880
- type: KeyDerivationType.LEAF,
2881
- path: node.id,
2882
- });
2883
- nodesToAdd.push(...newNodes);
2884
- }
2885
-
2886
- this.updateLeaves(nodeIds, nodesToAdd);
2887
- validNodes.push(...nodesToAdd);
2888
-
2889
- return validNodes;
2890
- }
2891
-
2892
- /**
2893
- * Internal method to refresh timelock nodes.
2894
- *
2895
- * @param {string} nodeId - The optional ID of the node to refresh. If not provided, all nodes will be checked.
2896
- * @returns {Promise<void>}
2897
- * @private
2898
- */
2899
- private async checkRefreshTimelockNodes(
2900
- nodes: TreeNode[],
2901
- ): Promise<TreeNode[]> {
2902
- const nodesToRefresh: TreeNode[] = [];
2903
- const nodeIds: string[] = [];
2904
- const validNodes: TreeNode[] = [];
2905
-
2906
- for (const node of nodes) {
2907
- const refundTx = getTxFromRawTxBytes(node.refundTx);
2908
- const sequence = refundTx.getInput(0).sequence;
2909
- if (!sequence) {
2854
+ if (!refundSequence) {
2910
2855
  throw new ValidationError("Invalid refund transaction", {
2911
2856
  field: "sequence",
2912
2857
  value: refundTx.getInput(0),
2913
2858
  expected: "Non-null sequence",
2914
2859
  });
2915
2860
  }
2916
- const needsRefresh = doesLeafNeedRefresh(sequence);
2917
- if (needsRefresh) {
2918
- nodesToRefresh.push(node);
2861
+
2862
+ if (doesTxnNeedRenewed(refundSequence)) {
2863
+ if (isZeroTimelock(nodeSequence)) {
2864
+ nodesToRenewZeroTimelock.push(node);
2865
+ } else if (doesTxnNeedRenewed(nodeSequence)) {
2866
+ nodesToRenewNode.push(node);
2867
+ } else {
2868
+ nodesToRenewRefund.push(node);
2869
+ }
2919
2870
  nodeIds.push(node.id);
2920
2871
  } else {
2921
2872
  validNodes.push(node);
2922
2873
  }
2923
2874
  }
2924
2875
 
2925
- if (nodesToRefresh.length === 0) {
2876
+ if (
2877
+ nodesToRenewNode.length === 0 &&
2878
+ nodesToRenewRefund.length === 0 &&
2879
+ nodesToRenewZeroTimelock.length === 0
2880
+ ) {
2926
2881
  return validNodes;
2927
2882
  }
2928
2883
 
@@ -2943,7 +2898,7 @@ export abstract class SparkWallet extends EventEmitter<SparkWalletEvents> {
2943
2898
  }
2944
2899
 
2945
2900
  const nodesToAdd: TreeNode[] = [];
2946
- for (const node of nodesToRefresh) {
2901
+ for (const node of nodesToRenewNode) {
2947
2902
  if (!node.parentNodeId) {
2948
2903
  throw new Error(`node ${node.id} has no parent`);
2949
2904
  }
@@ -2953,20 +2908,29 @@ export abstract class SparkWallet extends EventEmitter<SparkWalletEvents> {
2953
2908
  throw new Error(`parent node ${node.parentNodeId} not found`);
2954
2909
  }
2955
2910
 
2956
- const { nodes } = await this.transferService.refreshTimelockNodes(
2957
- node,
2958
- parentNode,
2959
- );
2911
+ const newNode = await this.transferService.renewNodeTxn(node, parentNode);
2912
+ nodesToAdd.push(newNode);
2913
+ }
2960
2914
 
2961
- if (nodes.length !== 1) {
2962
- throw new Error(`expected 1 node, got ${nodes.length}`);
2915
+ for (const node of nodesToRenewRefund) {
2916
+ if (!node.parentNodeId) {
2917
+ throw new Error(`node ${node.id} has no parent`);
2963
2918
  }
2964
2919
 
2965
- const newNode = nodes[0];
2966
- if (!newNode) {
2967
- throw new Error("Failed to refresh timelock node");
2920
+ const parentNode = nodesMap.get(node.parentNodeId);
2921
+ if (!parentNode) {
2922
+ throw new Error(`parent node ${node.parentNodeId} not found`);
2968
2923
  }
2969
2924
 
2925
+ const newNode = await this.transferService.renewRefundTxn(
2926
+ node,
2927
+ parentNode,
2928
+ );
2929
+ nodesToAdd.push(newNode);
2930
+ }
2931
+
2932
+ for (const node of nodesToRenewZeroTimelock) {
2933
+ const newNode = await this.transferService.renewZeroTimelockNodeTxn(node);
2970
2934
  nodesToAdd.push(newNode);
2971
2935
  }
2972
2936
 
@@ -3020,17 +2984,20 @@ export abstract class SparkWallet extends EventEmitter<SparkWalletEvents> {
3020
2984
  result: TreeNode[],
3021
2985
  transfer: Transfer,
3022
2986
  emit?: boolean,
3023
- optimize?: boolean,
3024
2987
  ): Promise<TreeNode[]> {
3025
- result = await this.checkRefreshTimelockNodes(result);
3026
- result = await this.checkExtendTimeLockNodes(result);
2988
+ result = await this.checkRenewLeaves(result);
3027
2989
 
3028
2990
  const existingIds = new Set(this.leaves.map((leaf) => leaf.id));
3029
2991
  const uniqueResults = result.filter((node) => !existingIds.has(node.id));
3030
2992
  this.leaves.push(...uniqueResults);
3031
2993
 
3032
- if (optimize && transfer.type !== TransferType.COUNTER_SWAP) {
3033
- await this.optimizeLeaves();
2994
+ if (
2995
+ this.config.getOptimizationOptions().auto &&
2996
+ transfer.type !== TransferType.COUNTER_SWAP
2997
+ ) {
2998
+ for await (const _ of this.optimizeLeaves()) {
2999
+ // run all optimizer steps, do nothing with them
3000
+ }
3034
3001
  }
3035
3002
 
3036
3003
  if (emit) {
@@ -3053,11 +3020,9 @@ export abstract class SparkWallet extends EventEmitter<SparkWalletEvents> {
3053
3020
  private async claimTransfer({
3054
3021
  transfer,
3055
3022
  emit,
3056
- optimize,
3057
3023
  }: {
3058
3024
  transfer: Transfer;
3059
3025
  emit?: boolean;
3060
- optimize?: boolean;
3061
3026
  }) {
3062
3027
  const onError = async (
3063
3028
  context: RetryContext<TreeNode[], Transfer>,
@@ -3115,12 +3080,7 @@ export abstract class SparkWallet extends EventEmitter<SparkWalletEvents> {
3115
3080
  return [];
3116
3081
  }
3117
3082
 
3118
- return await this.processClaimedTransferResults(
3119
- result,
3120
- transfer,
3121
- emit,
3122
- optimize,
3123
- );
3083
+ return await this.processClaimedTransferResults(result, transfer, emit);
3124
3084
  } catch (error) {
3125
3085
  console.warn(
3126
3086
  `Failed to claim transfer after all retries. Please try reinitializing your wallet in a few minutes. Transfer ID: ${transfer.id}`,
@@ -3169,7 +3129,7 @@ export abstract class SparkWallet extends EventEmitter<SparkWalletEvents> {
3169
3129
  continue;
3170
3130
  }
3171
3131
  promises.push(
3172
- this.claimTransfer({ transfer, emit, optimize: true })
3132
+ this.claimTransfer({ transfer, emit })
3173
3133
  .then(() => transfer.id)
3174
3134
  .catch((error) => {
3175
3135
  console.warn(`Failed to claim transfer ${transfer.id}:`, error);
@@ -3509,8 +3469,7 @@ export abstract class SparkWallet extends EventEmitter<SparkWalletEvents> {
3509
3469
  selectedLeaves,
3510
3470
  `no leaves for ${totalAmount}`,
3511
3471
  );
3512
- leaves = await this.checkRefreshTimelockNodes(leaves);
3513
- leaves = await this.checkExtendTimeLockNodes(leaves);
3472
+ leaves = await this.checkRenewLeaves(leaves);
3514
3473
 
3515
3474
  const leavesToSend: LeafKeyTweak[] = await Promise.all(
3516
3475
  leaves.map(async (leaf) => ({
@@ -3914,10 +3873,8 @@ export abstract class SparkWallet extends EventEmitter<SparkWalletEvents> {
3914
3873
  leavesToSendToSE = leavesForFee;
3915
3874
  }
3916
3875
 
3917
- leavesToSendToSsp = await this.checkRefreshTimelockNodes(leavesToSendToSsp);
3918
- leavesToSendToSsp = await this.checkExtendTimeLockNodes(leavesToSendToSsp);
3919
- leavesToSendToSE = await this.checkRefreshTimelockNodes(leavesToSendToSE);
3920
- leavesToSendToSE = await this.checkExtendTimeLockNodes(leavesToSendToSE);
3876
+ leavesToSendToSsp = await this.checkRenewLeaves(leavesToSendToSsp);
3877
+ leavesToSendToSE = await this.checkRenewLeaves(leavesToSendToSE);
3921
3878
 
3922
3879
  const leafKeyTweaks: LeafKeyTweak[] = await Promise.all(
3923
3880
  [...leavesToSendToSE, ...leavesToSendToSsp].map(async (leaf) => ({
@@ -4024,8 +3981,7 @@ export abstract class SparkWallet extends EventEmitter<SparkWalletEvents> {
4024
3981
  `no leaves for ${amountSats}`,
4025
3982
  );
4026
3983
 
4027
- leaves = await this.checkRefreshTimelockNodes(leaves);
4028
- leaves = await this.checkExtendTimeLockNodes(leaves);
3984
+ leaves = await this.checkRenewLeaves(leaves);
4029
3985
 
4030
3986
  const feeEstimate = await sspClient.getCoopExitFeeQuote({
4031
3987
  leafExternalIds: leaves.map((leaf) => leaf.id),
@@ -4661,16 +4617,6 @@ export abstract class SparkWallet extends EventEmitter<SparkWalletEvents> {
4661
4617
  );
4662
4618
  }
4663
4619
 
4664
- if (!nodeInput.sequence) {
4665
- throw new ValidationError(
4666
- `Node transaction has no sequence for ${isRootNode ? "root" : "non-root"} node`,
4667
- {
4668
- field: "sequence",
4669
- value: nodeInput.sequence,
4670
- },
4671
- );
4672
- }
4673
-
4674
4620
  const refundInput = refundTx.getInput(0);
4675
4621
  if (!refundInput) {
4676
4622
  throw new ValidationError(
@@ -4711,104 +4657,6 @@ export abstract class SparkWallet extends EventEmitter<SparkWalletEvents> {
4711
4657
  }
4712
4658
  }
4713
4659
 
4714
- /**
4715
- * Refresh the timelock of a specific node.
4716
- *
4717
- * @param {string} nodeId - The ID of the node to refresh
4718
- * @returns {Promise<void>} Promise that resolves when the timelock is refreshed
4719
- */
4720
- public async testOnly_expireTimelock(nodeId: string): Promise<void> {
4721
- const sparkClient = await this.connectionManager.createSparkClient(
4722
- this.config.getCoordinatorAddress(),
4723
- );
4724
-
4725
- try {
4726
- // First, get the node and its parent
4727
- const response = await sparkClient.query_nodes({
4728
- source: {
4729
- $case: "nodeIds",
4730
- nodeIds: {
4731
- nodeIds: [nodeId],
4732
- },
4733
- },
4734
- includeParents: true,
4735
- });
4736
-
4737
- let leaf = response.nodes[nodeId];
4738
- if (!leaf) {
4739
- throw new ValidationError("Node not found", {
4740
- field: "nodeId",
4741
- value: nodeId,
4742
- });
4743
- }
4744
-
4745
- let parentNode;
4746
- let hasParentNode = false;
4747
- if (!leaf.parentNodeId) {
4748
- // skip node timelock check
4749
- } else {
4750
- hasParentNode = true;
4751
- parentNode = response.nodes[leaf.parentNodeId];
4752
- if (!parentNode) {
4753
- throw new ValidationError("Parent node not found", {
4754
- field: "parentNodeId",
4755
- value: leaf.parentNodeId,
4756
- });
4757
- }
4758
- }
4759
-
4760
- const nodeTx = getTxFromRawTxBytes(leaf.nodeTx);
4761
- const refundTx = getTxFromRawTxBytes(leaf.refundTx);
4762
-
4763
- if (hasParentNode) {
4764
- const nodeTimelock = getCurrentTimelock(nodeTx.getInput(0).sequence);
4765
- if (nodeTimelock > 100) {
4766
- const expiredNodeTxLeaf =
4767
- await this.transferService.testonly_expireTimeLockNodeTx(
4768
- leaf,
4769
- parentNode,
4770
- );
4771
-
4772
- if (!expiredNodeTxLeaf.nodes[0]) {
4773
- throw new ValidationError("No expired node tx leaf", {
4774
- field: "expiredNodeTxLeaf",
4775
- value: expiredNodeTxLeaf,
4776
- });
4777
- }
4778
- leaf = expiredNodeTxLeaf.nodes[0];
4779
- }
4780
- }
4781
- const refundTimelock = getCurrentTimelock(refundTx.getInput(0).sequence);
4782
-
4783
- if (refundTimelock > 100) {
4784
- const expiredTxLeaf =
4785
- await this.transferService.testonly_expireTimeLockRefundtx(leaf);
4786
-
4787
- if (!expiredTxLeaf.nodes[0]) {
4788
- throw new ValidationError("No expired tx leaf", {
4789
- field: "expiredTxLeaf",
4790
- value: expiredTxLeaf,
4791
- });
4792
- }
4793
- leaf = expiredTxLeaf.nodes[0];
4794
- }
4795
-
4796
- // Update the local leaves if this node is in our wallet
4797
- const leafIndex = this.leaves.findIndex((leaf) => leaf.id === leaf.id);
4798
- if (leafIndex !== -1) {
4799
- this.leaves[leafIndex] = leaf;
4800
- }
4801
- } catch (error) {
4802
- throw new NetworkError(
4803
- "Failed to refresh timelock",
4804
- {
4805
- method: "refresh_timelock",
4806
- },
4807
- error as Error,
4808
- );
4809
- }
4810
- }
4811
-
4812
4660
  private cleanup() {
4813
4661
  if (this.claimTransfersInterval) {
4814
4662
  clearInterval(this.claimTransfersInterval);
@@ -5012,7 +4860,6 @@ export abstract class SparkWallet extends EventEmitter<SparkWalletEvents> {
5012
4860
  "getLightningSendRequest",
5013
4861
  "getCoopExitRequest",
5014
4862
  "checkTimelock",
5015
- "testOnly_expireTimelock",
5016
4863
  ] as const;
5017
4864
 
5018
4865
  methods.forEach((m) => this.wrapPublicSparkWalletMethodWithOtelSpan(m));