@buildonspark/spark-sdk 0.1.43 → 0.1.44

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 (112) hide show
  1. package/CHANGELOG.md +7 -0
  2. package/dist/{RequestLightningSendInput-Na1mHdWg.d.cts → RequestLightningSendInput-BxbCtwpV.d.cts} +43 -4
  3. package/dist/{RequestLightningSendInput-D7fZdT4A.d.ts → RequestLightningSendInput-RGel43ks.d.ts} +43 -4
  4. package/dist/address/index.d.cts +2 -2
  5. package/dist/address/index.d.ts +2 -2
  6. package/dist/address/index.js +2 -2
  7. package/dist/{chunk-M6A4KFIG.js → chunk-4Q2ZDYYU.js} +332 -221
  8. package/dist/{chunk-WWOTVNPP.js → chunk-A2ZLMH6I.js} +323 -142
  9. package/dist/{chunk-VA7MV4MZ.js → chunk-B3AMIGJG.js} +1 -1
  10. package/dist/{chunk-DQYKQJRZ.js → chunk-CIZNCBKE.js} +29 -9
  11. package/dist/{chunk-BUTZWYBW.js → chunk-DAXGVPVM.js} +2 -2
  12. package/dist/{chunk-DOA6QXYQ.js → chunk-EKFD62HN.js} +2 -1
  13. package/dist/{chunk-TIUBYNN5.js → chunk-HTMXTJRK.js} +1 -1
  14. package/dist/{chunk-TOSP3INR.js → chunk-I54FARY2.js} +4 -2
  15. package/dist/{chunk-MIVX3GHD.js → chunk-K4BJARWM.js} +1 -1
  16. package/dist/{chunk-O4RYNJNB.js → chunk-KEKGSH7B.js} +1 -1
  17. package/dist/{chunk-GYQR4B4P.js → chunk-NBCNYDWJ.js} +2 -2
  18. package/dist/{chunk-ABZA6R5S.js → chunk-SQKXGAIR.js} +1 -1
  19. package/dist/{chunk-VFJQNBFX.js → chunk-UBT6EDVJ.js} +5 -2
  20. package/dist/{chunk-HRQRRDSS.js → chunk-WPTRVD2V.js} +3 -3
  21. package/dist/{chunk-IRW5TWMH.js → chunk-XX4RRWOX.js} +5 -5
  22. package/dist/graphql/objects/index.d.cts +5 -43
  23. package/dist/graphql/objects/index.d.ts +5 -43
  24. package/dist/graphql/objects/index.js +1 -1
  25. package/dist/{index-BJOc8Ur-.d.cts → index-CZmDdSts.d.cts} +24 -10
  26. package/dist/{index-7RYRH5wc.d.ts → index-ClIRO_3y.d.ts} +24 -10
  27. package/dist/index.cjs +827 -414
  28. package/dist/index.d.cts +6 -6
  29. package/dist/index.d.ts +6 -6
  30. package/dist/index.js +15 -15
  31. package/dist/index.node.cjs +830 -417
  32. package/dist/index.node.d.cts +7 -7
  33. package/dist/index.node.d.ts +7 -7
  34. package/dist/index.node.js +18 -18
  35. package/dist/native/index.cjs +827 -414
  36. package/dist/native/index.d.cts +88 -23
  37. package/dist/native/index.d.ts +88 -23
  38. package/dist/native/index.js +819 -407
  39. package/dist/{network-D5lKssVl.d.cts → network-CfxLnaot.d.cts} +1 -1
  40. package/dist/{network-xkBSpaTn.d.ts → network-CroCOQ0B.d.ts} +1 -1
  41. package/dist/proto/lrc20.d.cts +1 -1
  42. package/dist/proto/lrc20.d.ts +1 -1
  43. package/dist/proto/lrc20.js +2 -2
  44. package/dist/proto/spark.cjs +332 -221
  45. package/dist/proto/spark.d.cts +1 -1
  46. package/dist/proto/spark.d.ts +1 -1
  47. package/dist/proto/spark.js +3 -5
  48. package/dist/proto/spark_token.cjs +149 -9
  49. package/dist/proto/spark_token.d.cts +1 -1
  50. package/dist/proto/spark_token.d.ts +1 -1
  51. package/dist/proto/spark_token.js +2 -2
  52. package/dist/{sdk-types-B-q9py_P.d.cts → sdk-types-BeCBoozO.d.cts} +1 -1
  53. package/dist/{sdk-types-BPoPgzda.d.ts → sdk-types-CTbTdDbE.d.ts} +1 -1
  54. package/dist/services/config.cjs +7 -3
  55. package/dist/services/config.d.cts +4 -4
  56. package/dist/services/config.d.ts +4 -4
  57. package/dist/services/config.js +5 -5
  58. package/dist/services/connection.cjs +334 -218
  59. package/dist/services/connection.d.cts +4 -4
  60. package/dist/services/connection.d.ts +4 -4
  61. package/dist/services/connection.js +4 -4
  62. package/dist/services/index.cjs +364 -227
  63. package/dist/services/index.d.cts +4 -4
  64. package/dist/services/index.d.ts +4 -4
  65. package/dist/services/index.js +9 -9
  66. package/dist/services/lrc-connection.cjs +5 -2
  67. package/dist/services/lrc-connection.d.cts +4 -4
  68. package/dist/services/lrc-connection.d.ts +4 -4
  69. package/dist/services/lrc-connection.js +4 -4
  70. package/dist/services/token-transactions.cjs +28 -8
  71. package/dist/services/token-transactions.d.cts +4 -4
  72. package/dist/services/token-transactions.d.ts +4 -4
  73. package/dist/services/token-transactions.js +3 -3
  74. package/dist/services/wallet-config.cjs +2 -1
  75. package/dist/services/wallet-config.d.cts +4 -4
  76. package/dist/services/wallet-config.d.ts +4 -4
  77. package/dist/services/wallet-config.js +1 -1
  78. package/dist/signer/signer.cjs +5 -2
  79. package/dist/signer/signer.d.cts +2 -2
  80. package/dist/signer/signer.d.ts +2 -2
  81. package/dist/signer/signer.js +2 -2
  82. package/dist/{signer-wqesWifN.d.ts → signer-D7vfYik9.d.ts} +1 -1
  83. package/dist/{signer-IO3oMRNj.d.cts → signer-DaY8c60s.d.cts} +1 -1
  84. package/dist/{spark-CDm4gqS6.d.ts → spark-C4ZrsgjC.d.cts} +47 -29
  85. package/dist/{spark-CDm4gqS6.d.cts → spark-C4ZrsgjC.d.ts} +47 -29
  86. package/dist/types/index.cjs +331 -219
  87. package/dist/types/index.d.cts +5 -5
  88. package/dist/types/index.d.ts +5 -5
  89. package/dist/types/index.js +3 -3
  90. package/dist/utils/index.cjs +298 -28
  91. package/dist/utils/index.d.cts +5 -5
  92. package/dist/utils/index.d.ts +5 -5
  93. package/dist/utils/index.js +6 -6
  94. package/package.json +1 -1
  95. package/src/constants.ts +5 -1
  96. package/src/graphql/client.ts +28 -0
  97. package/src/graphql/mutations/RequestCoopExit.ts +6 -0
  98. package/src/graphql/mutations/RequestSwapLeaves.ts +2 -0
  99. package/src/graphql/queries/GetCoopExitFeeQuote.ts +20 -0
  100. package/src/proto/spark.ts +454 -322
  101. package/src/services/token-transactions.ts +12 -0
  102. package/src/services/transfer.ts +0 -3
  103. package/src/services/wallet-config.ts +1 -0
  104. package/src/spark-wallet/spark-wallet.node.ts +3 -3
  105. package/src/spark-wallet/spark-wallet.ts +301 -125
  106. package/src/tests/integration/deposit.test.ts +16 -0
  107. package/src/tests/integration/ssp/coop-exit.test.ts +85 -21
  108. package/src/tests/integration/ssp/swap.test.ts +47 -0
  109. package/src/tests/tokens.test.ts +3 -2
  110. package/src/utils/token-hashing.ts +28 -10
  111. package/src/utils/transaction.ts +2 -2
  112. package/src/logger.ts +0 -3
@@ -23,7 +23,7 @@ import SspClient from "../graphql/client.js";
23
23
  import {
24
24
  BitcoinNetwork,
25
25
  ClaimStaticDepositOutput,
26
- CoopExitFeeEstimatesOutput,
26
+ CoopExitFeeQuote,
27
27
  CoopExitRequest,
28
28
  ExitSpeed,
29
29
  LeavesSwapFeeEstimateOutput,
@@ -31,8 +31,8 @@ import {
31
31
  LightningReceiveRequest,
32
32
  LightningSendFeeEstimateInput,
33
33
  LightningSendRequest,
34
+ RequestCoopExitInput,
34
35
  StaticDepositQuoteOutput,
35
- SwapLeaf,
36
36
  UserLeafInput,
37
37
  } from "../graphql/objects/index.js";
38
38
  import GraphQLTransferObj from "../graphql/objects/Transfer.js";
@@ -102,7 +102,7 @@ import {
102
102
  SparkAddressFormat,
103
103
  } from "../address/index.js";
104
104
  import { isReactNative } from "../constants.js";
105
- import { networkToJSON } from "../proto/spark.js";
105
+ import { networkToJSON, Network as NetworkProto } from "../proto/spark.js";
106
106
  import {
107
107
  decodeInvoice,
108
108
  getNetworkFromInvoice,
@@ -524,14 +524,40 @@ export class SparkWallet extends EventEmitter {
524
524
  .map(([_, node]) => node);
525
525
  }
526
526
 
527
- private async selectLeaves(targetAmount: number): Promise<TreeNode[]> {
528
- if (targetAmount <= 0) {
527
+ private async selectLeaves(
528
+ targetAmounts: number[],
529
+ ): Promise<Map<number, TreeNode[]>> {
530
+ if (targetAmounts.length === 0) {
531
+ throw new ValidationError("Target amounts must be non-empty", {
532
+ field: "targetAmounts",
533
+ value: targetAmounts,
534
+ });
535
+ }
536
+
537
+ if (targetAmounts.some((amount) => amount <= 0)) {
529
538
  throw new ValidationError("Target amount must be positive", {
530
- field: "targetAmount",
531
- value: targetAmount,
539
+ field: "targetAmounts",
540
+ value: targetAmounts,
532
541
  });
533
542
  }
534
543
 
544
+ const totalTargetAmount = targetAmounts.reduce(
545
+ (acc, amount) => acc + amount,
546
+ 0,
547
+ );
548
+ const totalBalance = this.getInternalBalance();
549
+
550
+ if (totalTargetAmount > totalBalance) {
551
+ throw new ValidationError(
552
+ "Total target amount exceeds available balance",
553
+ {
554
+ field: "targetAmounts",
555
+ value: totalTargetAmount,
556
+ expected: `less than or equal to ${totalBalance}`,
557
+ },
558
+ );
559
+ }
560
+
535
561
  const leaves = await this.getLeaves();
536
562
  if (leaves.length === 0) {
537
563
  throw new ValidationError("No owned leaves found", {
@@ -541,37 +567,63 @@ export class SparkWallet extends EventEmitter {
541
567
 
542
568
  leaves.sort((a, b) => b.value - a.value);
543
569
 
544
- let amount = 0;
545
- let nodes: TreeNode[] = [];
546
- for (const leaf of leaves) {
547
- if (targetAmount - amount >= leaf.value) {
548
- amount += leaf.value;
549
- nodes.push(leaf);
550
- }
551
- }
570
+ const selectLeavesForTargets = (
571
+ targetAmounts: number[],
572
+ leaves: TreeNode[],
573
+ ) => {
574
+ const usedLeaves = new Set<string>();
575
+ const results: Map<number, TreeNode[]> = new Map();
576
+ let totalAmount = 0;
552
577
 
553
- if (amount !== targetAmount) {
554
- await this.requestLeavesSwap({ targetAmount });
578
+ for (const targetAmount of targetAmounts) {
579
+ const nodes: TreeNode[] = [];
580
+ let amount = 0;
555
581
 
556
- amount = 0;
557
- nodes = [];
558
- const newLeaves = await this.getLeaves();
559
- newLeaves.sort((a, b) => b.value - a.value);
560
- for (const leaf of newLeaves) {
561
- if (targetAmount - amount >= leaf.value) {
562
- amount += leaf.value;
563
- nodes.push(leaf);
582
+ for (const leaf of leaves) {
583
+ if (usedLeaves.has(leaf.id)) {
584
+ continue;
585
+ }
586
+
587
+ if (targetAmount - amount >= leaf.value) {
588
+ amount += leaf.value;
589
+ nodes.push(leaf);
590
+ usedLeaves.add(leaf.id);
591
+ }
564
592
  }
593
+
594
+ totalAmount += amount;
595
+ results.set(targetAmount, nodes);
565
596
  }
597
+
598
+ return {
599
+ results,
600
+ foundSelections: totalAmount === totalTargetAmount,
601
+ };
602
+ };
603
+
604
+ let { results, foundSelections } = selectLeavesForTargets(
605
+ targetAmounts,
606
+ leaves,
607
+ );
608
+
609
+ if (!foundSelections) {
610
+ const newLeaves = await this.requestLeavesSwap({ targetAmounts });
611
+
612
+ newLeaves.sort((a, b) => b.value - a.value);
613
+
614
+ ({ results, foundSelections } = selectLeavesForTargets(
615
+ targetAmounts,
616
+ newLeaves,
617
+ ));
566
618
  }
567
619
 
568
- if (nodes.reduce((acc, leaf) => acc + leaf.value, 0) !== targetAmount) {
620
+ if (!foundSelections) {
569
621
  throw new Error(
570
- `Failed to select leaves for target amount ${targetAmount}`,
622
+ `Failed to select leaves for target amount ${totalTargetAmount}`,
571
623
  );
572
624
  }
573
625
 
574
- return nodes;
626
+ return results;
575
627
  }
576
628
 
577
629
  private async selectLeavesForSwap(targetAmount: number) {
@@ -853,32 +905,57 @@ export class SparkWallet extends EventEmitter {
853
905
  * @private
854
906
  */
855
907
  private async requestLeavesSwap({
856
- targetAmount,
908
+ targetAmounts,
857
909
  leaves,
858
910
  }: {
859
- targetAmount?: number;
911
+ targetAmounts?: number[];
860
912
  leaves?: TreeNode[];
861
- }) {
862
- if (targetAmount && targetAmount <= 0) {
863
- throw new Error("targetAmount must be positive");
913
+ }): Promise<TreeNode[]> {
914
+ if (targetAmounts && targetAmounts.some((amount) => amount <= 0)) {
915
+ throw new Error("specified targetAmount must be positive");
864
916
  }
865
917
 
866
- if (targetAmount && !Number.isSafeInteger(targetAmount)) {
918
+ if (
919
+ targetAmounts &&
920
+ targetAmounts.some((amount) => !Number.isSafeInteger(amount))
921
+ ) {
867
922
  throw new ValidationError("targetAmount must be less than 2^53", {
868
- field: "targetAmount",
869
- value: targetAmount,
923
+ field: "targetAmounts",
924
+ value: targetAmounts,
870
925
  expected: "smaller or equal to " + Number.MAX_SAFE_INTEGER,
871
926
  });
872
927
  }
873
928
 
874
929
  let leavesToSwap: TreeNode[];
875
- if (targetAmount && leaves && leaves.length > 0) {
876
- if (targetAmount < leaves.reduce((acc, leaf) => acc + leaf.value, 0)) {
930
+ const totalTargetAmount = targetAmounts?.reduce(
931
+ (acc, amount) => acc + amount,
932
+ 0,
933
+ );
934
+
935
+ if (totalTargetAmount) {
936
+ const totalBalance = this.getInternalBalance();
937
+
938
+ if (totalTargetAmount > totalBalance) {
939
+ throw new ValidationError(
940
+ "Total target amount exceeds available balance",
941
+ {
942
+ field: "targetAmounts",
943
+ value: totalTargetAmount,
944
+ expected: `less than or equal to ${totalBalance}`,
945
+ },
946
+ );
947
+ }
948
+ }
949
+
950
+ if (totalTargetAmount && leaves && leaves.length > 0) {
951
+ if (
952
+ totalTargetAmount < leaves.reduce((acc, leaf) => acc + leaf.value, 0)
953
+ ) {
877
954
  throw new Error("targetAmount is less than the sum of leaves");
878
955
  }
879
956
  leavesToSwap = leaves;
880
- } else if (targetAmount) {
881
- leavesToSwap = await this.selectLeavesForSwap(targetAmount);
957
+ } else if (totalTargetAmount) {
958
+ leavesToSwap = await this.selectLeavesForSwap(totalTargetAmount);
882
959
  } else if (leaves && leaves.length > 0) {
883
960
  leavesToSwap = leaves;
884
961
  } else {
@@ -889,10 +966,10 @@ export class SparkWallet extends EventEmitter {
889
966
 
890
967
  const batches = chunkArray(leavesToSwap, 100);
891
968
 
892
- const results: SwapLeaf[] = [];
969
+ const results: TreeNode[] = [];
893
970
  for (const batch of batches) {
894
- const result = await this.processSwapBatch(batch, targetAmount);
895
- results.push(...result.swapLeaves);
971
+ const result = await this.processSwapBatch(batch, targetAmounts);
972
+ results.push(...result);
896
973
  }
897
974
 
898
975
  return results;
@@ -903,8 +980,8 @@ export class SparkWallet extends EventEmitter {
903
980
  */
904
981
  private async processSwapBatch(
905
982
  leavesBatch: TreeNode[],
906
- targetAmount?: number,
907
- ): Promise<LeavesSwapRequest> {
983
+ targetAmounts?: number[],
984
+ ): Promise<TreeNode[]> {
908
985
  const leafKeyTweaks = await Promise.all(
909
986
  leavesBatch.map(async (leaf) => ({
910
987
  leaf,
@@ -981,9 +1058,10 @@ export class SparkWallet extends EventEmitter {
981
1058
  userLeaves,
982
1059
  adaptorPubkey,
983
1060
  targetAmountSats:
984
- targetAmount ||
1061
+ targetAmounts?.reduce((acc, amount) => acc + amount, 0) ||
985
1062
  leavesBatch.reduce((acc, leaf) => acc + leaf.value, 0),
986
1063
  totalAmountSats: leavesBatch.reduce((acc, leaf) => acc + leaf.value, 0),
1064
+ targetAmountSatsList: targetAmounts,
987
1065
  // TODO: Request fee from SSP
988
1066
  feeSats: 0,
989
1067
  idempotencyKey: uuidv7(),
@@ -1051,13 +1129,24 @@ export class SparkWallet extends EventEmitter {
1051
1129
  leavesSwapRequestId: request.id,
1052
1130
  });
1053
1131
 
1054
- if (!completeResponse) {
1132
+ if (!completeResponse || !completeResponse.inboundTransfer?.sparkId) {
1055
1133
  throw new Error("Failed to complete leaves swap");
1056
1134
  }
1057
1135
 
1058
- await this.claimTransfers(TransferType.COUNTER_SWAP);
1136
+ const incomingTransfer = await this.transferService.queryTransfer(
1137
+ completeResponse.inboundTransfer.sparkId,
1138
+ );
1139
+
1140
+ if (!incomingTransfer) {
1141
+ throw new Error("Failed to get incoming transfer");
1142
+ }
1059
1143
 
1060
- return completeResponse;
1144
+ return await this.claimTransfer({
1145
+ transfer: incomingTransfer,
1146
+ emit: false,
1147
+ retryCount: 0,
1148
+ optimize: false,
1149
+ });
1061
1150
  } catch (e) {
1062
1151
  await this.cancelAllSenderInitiatedTransfers();
1063
1152
  throw new Error(`Failed to request leaves swap: ${e}`);
@@ -1750,16 +1839,60 @@ export class SparkWallet extends EventEmitter {
1750
1839
  * @returns {Promise<string[]>} The unused deposit addresses
1751
1840
  */
1752
1841
  public async getUnusedDepositAddresses(): Promise<string[]> {
1842
+ return (await this.queryAllUnusedDepositAddresses({})).map(
1843
+ (addr) => addr.depositAddress,
1844
+ );
1845
+ }
1846
+
1847
+ /**
1848
+ * Gets all unused deposit addresses for the wallet.
1849
+ *
1850
+ * @param {Object} params - Parameters for querying unused deposit addresses
1851
+ * @param {Uint8Array<ArrayBufferLike>} [params.identityPublicKey] - The identity public key
1852
+ * @param {NetworkProto} [params.network] - The network
1853
+ * @returns {Promise<DepositAddressQueryResult[]>} The unused deposit addresses
1854
+ */
1855
+ private async queryAllUnusedDepositAddresses({
1856
+ identityPublicKey,
1857
+ network,
1858
+ }: {
1859
+ identityPublicKey?: Uint8Array<ArrayBufferLike>;
1860
+ network?: NetworkProto | undefined;
1861
+ }): Promise<DepositAddressQueryResult[]> {
1753
1862
  const sparkClient = await this.connectionManager.createSparkClient(
1754
1863
  this.config.getCoordinatorAddress(),
1755
1864
  );
1756
- return (
1757
- await sparkClient.query_unused_deposit_addresses({
1758
- identityPublicKey: await this.config.signer.getIdentityPublicKey(),
1759
- network: NetworkToProto[this.config.getNetwork()],
1760
- })
1761
- ).depositAddresses.map((addr) => addr.depositAddress);
1865
+
1866
+ let limit = 100;
1867
+ let offset = 0;
1868
+ const pastOffsets = new Set<number>();
1869
+ const depositAddresses: DepositAddressQueryResult[] = [];
1870
+
1871
+ while (offset >= 0) {
1872
+ // Prevent infinite loop in case error with coordinator
1873
+ if (pastOffsets.has(offset)) {
1874
+ console.warn("Offset has already been seen, stopping");
1875
+ break;
1876
+ }
1877
+
1878
+ const response = await sparkClient.query_unused_deposit_addresses({
1879
+ identityPublicKey:
1880
+ identityPublicKey ??
1881
+ (await this.config.signer.getIdentityPublicKey()),
1882
+ network: network ?? NetworkToProto[this.config.getNetwork()],
1883
+ limit,
1884
+ offset,
1885
+ });
1886
+
1887
+ depositAddresses.push(...response.depositAddresses);
1888
+
1889
+ pastOffsets.add(offset);
1890
+ offset = response.offset;
1891
+ }
1892
+
1893
+ return depositAddresses;
1762
1894
  }
1895
+
1763
1896
  /**
1764
1897
  * Claims a deposit to the wallet.
1765
1898
  * Note that if you used advancedDeposit, you don't need to call this function.
@@ -1816,19 +1949,15 @@ export class SparkWallet extends EventEmitter {
1816
1949
  }
1817
1950
  const depositTx = getTxFromRawTxHex(txHex);
1818
1951
 
1819
- const sparkClient = await this.connectionManager.createSparkClient(
1820
- this.config.getCoordinatorAddress(),
1821
- );
1822
-
1823
1952
  const unusedDepositAddresses: Map<string, DepositAddressQueryResult> =
1824
1953
  new Map(
1825
1954
  (
1826
- await sparkClient.query_unused_deposit_addresses({
1955
+ await this.queryAllUnusedDepositAddresses({
1827
1956
  identityPublicKey:
1828
1957
  await this.config.signer.getIdentityPublicKey(),
1829
1958
  network: NetworkToProto[this.config.getNetwork()],
1830
1959
  })
1831
- ).depositAddresses.map((addr) => [addr.depositAddress, addr]),
1960
+ ).map((addr) => [addr.depositAddress, addr]),
1832
1961
  );
1833
1962
  let depositAddress: DepositAddressQueryResult | undefined;
1834
1963
  let vout = 0;
@@ -1888,17 +2017,15 @@ export class SparkWallet extends EventEmitter {
1888
2017
  */
1889
2018
  public async advancedDeposit(txHex: string) {
1890
2019
  const depositTx = getTxFromRawTxHex(txHex);
1891
- const sparkClient = await this.connectionManager.createSparkClient(
1892
- this.config.getCoordinatorAddress(),
1893
- );
2020
+
1894
2021
  const unusedDepositAddresses: Map<string, DepositAddressQueryResult> =
1895
2022
  new Map(
1896
2023
  (
1897
- await sparkClient.query_unused_deposit_addresses({
2024
+ await this.queryAllUnusedDepositAddresses({
1898
2025
  identityPublicKey: await this.config.signer.getIdentityPublicKey(),
1899
2026
  network: NetworkToProto[this.config.getNetwork()],
1900
2027
  })
1901
- ).depositAddresses.map((addr) => [addr.depositAddress, addr]),
2028
+ ).map((addr) => [addr.depositAddress, addr]),
1902
2029
  );
1903
2030
 
1904
2031
  let vout = 0;
@@ -2023,7 +2150,9 @@ export class SparkWallet extends EventEmitter {
2023
2150
  );
2024
2151
 
2025
2152
  return await this.withLeaves(async () => {
2026
- let leavesToSend = await this.selectLeaves(amountSats);
2153
+ let leavesToSend = (await this.selectLeaves([amountSats])).get(
2154
+ amountSats,
2155
+ )!;
2027
2156
 
2028
2157
  leavesToSend = await this.checkRefreshTimelockNodes(leavesToSend);
2029
2158
  leavesToSend = await this.checkExtendTimeLockNodes(leavesToSend);
@@ -2325,7 +2454,7 @@ export class SparkWallet extends EventEmitter {
2325
2454
  transfer.status !==
2326
2455
  TransferStatus.TRANSFER_STATUS_RECEIVER_KEY_TWEAKED &&
2327
2456
  transfer.status !==
2328
- TransferStatus.TRANSFER_STATUSR_RECEIVER_REFUND_SIGNED &&
2457
+ TransferStatus.TRANSFER_STATUS_RECEIVER_REFUND_SIGNED &&
2329
2458
  transfer.status !==
2330
2459
  TransferStatus.TRANSFER_STATUS_RECEIVER_KEY_TWEAK_APPLIED
2331
2460
  ) {
@@ -2648,7 +2777,7 @@ export class SparkWallet extends EventEmitter {
2648
2777
  });
2649
2778
  }
2650
2779
 
2651
- let leaves = await this.selectLeaves(totalAmount);
2780
+ let leaves = (await this.selectLeaves([totalAmount])).get(totalAmount)!;
2652
2781
 
2653
2782
  leaves = await this.checkRefreshTimelockNodes(leaves);
2654
2783
  leaves = await this.checkExtendTimeLockNodes(leaves);
@@ -2786,17 +2915,24 @@ export class SparkWallet extends EventEmitter {
2786
2915
  *
2787
2916
  * @param {Object} params - Parameters for the withdrawal
2788
2917
  * @param {string} params.onchainAddress - The Bitcoin address where the funds should be sent
2789
- * @param {number} [params.amountSats] - The amount in satoshis to withdraw. If not specified, attempts to withdraw all available funds
2918
+ * @param {CoopExitFeeQuote} params.feeQuote - The fee quote for the withdrawal
2919
+ * @param {ExitSpeed} params.exitSpeed - The exit speed chosen for the withdrawal
2920
+ * @param {number} [params.amountSats] - The amount in satoshis to withdraw. If not specified, attempts to withdraw all available funds and deductFeeFromWithdrawalAmount is set to true.
2921
+ * @param {boolean} [params.deductFeeFromWithdrawalAmount] - Controls how the withdrawal fee is handled. If true, the fee is deducted from the withdrawal amount (amountSats), meaning the recipient will receive amountSats minus the fee. If false, the fee is paid separately from the wallet balance, meaning the recipient will receive the full amountSats.
2790
2922
  * @returns {Promise<CoopExitRequest | null | undefined>} The withdrawal request details, or null/undefined if the request cannot be completed
2791
2923
  */
2792
2924
  public async withdraw({
2793
2925
  onchainAddress,
2794
2926
  exitSpeed,
2927
+ feeQuote,
2795
2928
  amountSats,
2929
+ deductFeeFromWithdrawalAmount = true,
2796
2930
  }: {
2797
2931
  onchainAddress: string;
2798
2932
  exitSpeed: ExitSpeed;
2933
+ feeQuote: CoopExitFeeQuote;
2799
2934
  amountSats?: number;
2935
+ deductFeeFromWithdrawalAmount?: boolean;
2800
2936
  }) {
2801
2937
  if (!Number.isSafeInteger(amountSats)) {
2802
2938
  throw new ValidationError("Sats amount must be less than 2^53", {
@@ -2806,7 +2942,13 @@ export class SparkWallet extends EventEmitter {
2806
2942
  });
2807
2943
  }
2808
2944
  return await this.withLeaves(async () => {
2809
- return await this.coopExit(onchainAddress, exitSpeed, amountSats);
2945
+ return await this.coopExit(
2946
+ onchainAddress,
2947
+ feeQuote,
2948
+ exitSpeed,
2949
+ deductFeeFromWithdrawalAmount,
2950
+ amountSats,
2951
+ );
2810
2952
  });
2811
2953
  }
2812
2954
 
@@ -2820,7 +2962,9 @@ export class SparkWallet extends EventEmitter {
2820
2962
  */
2821
2963
  private async coopExit(
2822
2964
  onchainAddress: string,
2965
+ feeEstimate: CoopExitFeeQuote,
2823
2966
  exitSpeed: ExitSpeed,
2967
+ deductFeeFromWithdrawalAmount: boolean,
2824
2968
  targetAmountSats?: number,
2825
2969
  ) {
2826
2970
  if (!Number.isSafeInteger(targetAmountSats)) {
@@ -2831,53 +2975,46 @@ export class SparkWallet extends EventEmitter {
2831
2975
  });
2832
2976
  }
2833
2977
 
2834
- let leavesToSend: TreeNode[] = [];
2835
- if (targetAmountSats) {
2836
- leavesToSend = await this.selectLeaves(targetAmountSats);
2837
- } else {
2838
- leavesToSend = this.leaves.map((leaf) => ({
2839
- ...leaf,
2840
- }));
2978
+ if (!targetAmountSats) {
2979
+ deductFeeFromWithdrawalAmount = true;
2841
2980
  }
2842
2981
 
2843
- const sspClient = this.getSspClient();
2844
- const feeEstimate = await sspClient.getCoopExitFeeEstimate({
2845
- leafExternalIds: leavesToSend.map((leaf) => leaf.id),
2846
- withdrawalAddress: onchainAddress,
2847
- });
2982
+ let fee: number | undefined;
2983
+ switch (exitSpeed) {
2984
+ case ExitSpeed.FAST:
2985
+ fee =
2986
+ (feeEstimate.l1BroadcastFeeFast?.originalValue || 0) +
2987
+ (feeEstimate.userFeeFast?.originalValue || 0);
2988
+ break;
2989
+ case ExitSpeed.MEDIUM:
2990
+ fee =
2991
+ (feeEstimate.l1BroadcastFeeMedium?.originalValue || 0) +
2992
+ (feeEstimate.userFeeMedium?.originalValue || 0);
2993
+ break;
2994
+ case ExitSpeed.SLOW:
2995
+ fee =
2996
+ (feeEstimate.l1BroadcastFeeSlow?.originalValue || 0) +
2997
+ (feeEstimate.userFeeSlow?.originalValue || 0);
2998
+ break;
2999
+ default:
3000
+ throw new ValidationError("Invalid exit speed", {
3001
+ field: "exitSpeed",
3002
+ value: exitSpeed,
3003
+ expected: "FAST, MEDIUM, or SLOW",
3004
+ });
3005
+ }
2848
3006
 
2849
- if (feeEstimate) {
2850
- let fee: number | undefined;
2851
- switch (exitSpeed) {
2852
- case ExitSpeed.FAST:
2853
- fee =
2854
- (feeEstimate.speedFast?.l1BroadcastFee.originalValue || 0) +
2855
- (feeEstimate.speedFast?.userFee.originalValue || 0);
2856
- break;
2857
- case ExitSpeed.MEDIUM:
2858
- fee =
2859
- (feeEstimate.speedMedium?.l1BroadcastFee.originalValue || 0) +
2860
- (feeEstimate.speedMedium?.userFee.originalValue || 0);
2861
- break;
2862
- case ExitSpeed.SLOW:
2863
- fee =
2864
- (feeEstimate.speedSlow?.l1BroadcastFee.originalValue || 0) +
2865
- (feeEstimate.speedSlow?.userFee.originalValue || 0);
2866
- break;
2867
- default:
2868
- throw new ValidationError("Invalid exit speed", {
2869
- field: "exitSpeed",
2870
- value: exitSpeed,
2871
- expected: "FAST, MEDIUM, or SLOW",
2872
- });
2873
- }
3007
+ let leavesToSendToSsp: TreeNode[] = [];
3008
+ let leavesToSendToSE: TreeNode[] = [];
2874
3009
 
2875
- if (
2876
- fee !== undefined &&
2877
- fee > leavesToSend.reduce((acc, leaf) => acc + leaf.value, 0)
2878
- ) {
3010
+ if (deductFeeFromWithdrawalAmount) {
3011
+ leavesToSendToSsp = targetAmountSats
3012
+ ? (await this.selectLeaves([targetAmountSats])).get(targetAmountSats)!
3013
+ : this.leaves;
3014
+
3015
+ if (fee > leavesToSendToSsp.reduce((acc, leaf) => acc + leaf.value, 0)) {
2879
3016
  throw new ValidationError(
2880
- "The fee for the withdrawal is greater than the target amount",
3017
+ "The fee for the withdrawal is greater than the target withdrawal amount",
2881
3018
  {
2882
3019
  field: "fee",
2883
3020
  value: fee,
@@ -2885,12 +3022,38 @@ export class SparkWallet extends EventEmitter {
2885
3022
  },
2886
3023
  );
2887
3024
  }
3025
+ } else {
3026
+ if (!targetAmountSats) {
3027
+ throw new ValidationError(
3028
+ "targetAmountSats is required when deductFeeFromWithdrawalAmount is false",
3029
+ {
3030
+ field: "targetAmountSats",
3031
+ value: targetAmountSats,
3032
+ expected: "defined when deductFeeFromWithdrawalAmount is false",
3033
+ },
3034
+ );
3035
+ }
3036
+
3037
+ const leaves = await this.selectLeaves([targetAmountSats, fee]);
3038
+
3039
+ const leavesForTargetAmount = leaves.get(targetAmountSats);
3040
+ const leavesForFee = leaves.get(fee);
3041
+
3042
+ if (!leavesForTargetAmount || !leavesForFee) {
3043
+ throw new Error("Failed to select leaves for target amount and fee");
3044
+ }
3045
+
3046
+ leavesToSendToSsp = leavesForTargetAmount;
3047
+ leavesToSendToSE = leavesForFee;
2888
3048
  }
2889
- leavesToSend = await this.checkRefreshTimelockNodes(leavesToSend);
2890
- leavesToSend = await this.checkExtendTimeLockNodes(leavesToSend);
3049
+
3050
+ leavesToSendToSsp = await this.checkRefreshTimelockNodes(leavesToSendToSsp);
3051
+ leavesToSendToSsp = await this.checkExtendTimeLockNodes(leavesToSendToSsp);
3052
+ leavesToSendToSE = await this.checkRefreshTimelockNodes(leavesToSendToSE);
3053
+ leavesToSendToSE = await this.checkExtendTimeLockNodes(leavesToSendToSE);
2891
3054
 
2892
3055
  const leafKeyTweaks = await Promise.all(
2893
- leavesToSend.map(async (leaf) => ({
3056
+ [...leavesToSendToSE, ...leavesToSendToSsp].map(async (leaf) => ({
2894
3057
  leaf,
2895
3058
  signingPubKey: await this.config.signer.generatePublicKey(
2896
3059
  sha256(leaf.id),
@@ -2899,13 +3062,26 @@ export class SparkWallet extends EventEmitter {
2899
3062
  })),
2900
3063
  );
2901
3064
 
2902
- const coopExitRequest = await sspClient.requestCoopExit({
2903
- leafExternalIds: leavesToSend.map((leaf) => leaf.id),
3065
+ const requestCoopExitParams: RequestCoopExitInput = {
3066
+ leafExternalIds: leavesToSendToSsp.map((leaf) => leaf.id),
2904
3067
  withdrawalAddress: onchainAddress,
2905
3068
  idempotencyKey: uuidv7(),
2906
3069
  exitSpeed,
2907
- withdrawAll: true,
2908
- });
3070
+ withdrawAll: deductFeeFromWithdrawalAmount,
3071
+ };
3072
+
3073
+ if (!deductFeeFromWithdrawalAmount) {
3074
+ requestCoopExitParams.feeQuoteId = feeEstimate.id;
3075
+ requestCoopExitParams.feeLeafExternalIds = leavesToSendToSE.map(
3076
+ (leaf) => leaf.id,
3077
+ );
3078
+ }
3079
+
3080
+ const sspClient = this.getSspClient();
3081
+
3082
+ const coopExitRequest = await sspClient.requestCoopExit(
3083
+ requestCoopExitParams,
3084
+ );
2909
3085
 
2910
3086
  if (!coopExitRequest?.rawConnectorTransaction) {
2911
3087
  throw new Error("Failed to request coop exit");
@@ -2952,15 +3128,15 @@ export class SparkWallet extends EventEmitter {
2952
3128
  * @param {Object} params - Input parameters for fee estimation
2953
3129
  * @param {number} params.amountSats - The amount in satoshis to withdraw
2954
3130
  * @param {string} params.withdrawalAddress - The Bitcoin address where the funds should be sent
2955
- * @returns {Promise<CoopExitFeeEstimatesOutput | null>} Fee estimate for the withdrawal
3131
+ * @returns {Promise<CoopExitFeeQuote | null>} Fee estimate for the withdrawal
2956
3132
  */
2957
- public async getWithdrawalFeeEstimate({
3133
+ public async getWithdrawalFeeQuote({
2958
3134
  amountSats,
2959
3135
  withdrawalAddress,
2960
3136
  }: {
2961
3137
  amountSats: number;
2962
3138
  withdrawalAddress: string;
2963
- }): Promise<CoopExitFeeEstimatesOutput | null> {
3139
+ }): Promise<CoopExitFeeQuote | null> {
2964
3140
  const sspClient = this.getSspClient();
2965
3141
 
2966
3142
  if (!Number.isSafeInteger(amountSats)) {
@@ -2971,12 +3147,12 @@ export class SparkWallet extends EventEmitter {
2971
3147
  });
2972
3148
  }
2973
3149
 
2974
- let leaves = await this.selectLeaves(amountSats);
3150
+ let leaves = (await this.selectLeaves([amountSats])).get(amountSats)!;
2975
3151
 
2976
3152
  leaves = await this.checkRefreshTimelockNodes(leaves);
2977
3153
  leaves = await this.checkExtendTimeLockNodes(leaves);
2978
3154
 
2979
- const feeEstimate = await sspClient.getCoopExitFeeEstimate({
3155
+ const feeEstimate = await sspClient.getCoopExitFeeQuote({
2980
3156
  leafExternalIds: leaves.map((leaf) => leaf.id),
2981
3157
  withdrawalAddress,
2982
3158
  });