@buildonspark/spark-sdk 0.2.2 → 0.2.4

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 +15 -0
  2. package/dist/{chunk-TM6CHQXC.js → chunk-3SEOTO43.js} +1 -1
  3. package/dist/{chunk-2ENZX6LT.js → chunk-AAZWSPUK.js} +84 -8
  4. package/dist/{chunk-4JD4HIAN.js → chunk-G4MSZ6DE.js} +299 -1
  5. package/dist/{chunk-S2AL73MZ.js → chunk-TVUMSHWA.js} +1 -1
  6. package/dist/{chunk-2TUM3R6C.js → chunk-W4ZRBSWM.js} +2351 -797
  7. package/dist/{chunk-CDLETEDT.js → chunk-WAQKYSDI.js} +13 -1
  8. package/dist/{client-CGTRS23n.d.ts → client-BF4cn8F4.d.ts} +15 -3
  9. package/dist/{client-CcYzmpmj.d.cts → client-KhNkrXz4.d.cts} +15 -3
  10. package/dist/debug.cjs +2948 -1023
  11. package/dist/debug.d.cts +19 -6
  12. package/dist/debug.d.ts +19 -6
  13. package/dist/debug.js +5 -5
  14. package/dist/graphql/objects/index.cjs +13 -1
  15. package/dist/graphql/objects/index.d.cts +2 -2
  16. package/dist/graphql/objects/index.d.ts +2 -2
  17. package/dist/graphql/objects/index.js +1 -1
  18. package/dist/index.cjs +2794 -858
  19. package/dist/index.d.cts +190 -9
  20. package/dist/index.d.ts +190 -9
  21. package/dist/index.js +32 -6
  22. package/dist/index.node.cjs +2931 -892
  23. package/dist/index.node.d.cts +10 -188
  24. package/dist/index.node.d.ts +10 -188
  25. package/dist/index.node.js +134 -6
  26. package/dist/native/index.cjs +2794 -858
  27. package/dist/native/index.d.cts +148 -40
  28. package/dist/native/index.d.ts +148 -40
  29. package/dist/native/index.js +2799 -877
  30. package/dist/proto/lrc20.d.cts +1 -1
  31. package/dist/proto/lrc20.d.ts +1 -1
  32. package/dist/proto/lrc20.js +1 -1
  33. package/dist/proto/spark.cjs +84 -8
  34. package/dist/proto/spark.d.cts +1 -1
  35. package/dist/proto/spark.d.ts +1 -1
  36. package/dist/proto/spark.js +1 -1
  37. package/dist/proto/spark_token.cjs +301 -0
  38. package/dist/proto/spark_token.d.cts +35 -2
  39. package/dist/proto/spark_token.d.ts +35 -2
  40. package/dist/proto/spark_token.js +8 -2
  41. package/dist/{sdk-types-DJ2ve9YY.d.cts → sdk-types-CB9HrW5O.d.cts} +1 -1
  42. package/dist/{sdk-types-DCIVdKUT.d.ts → sdk-types-CkRNraXT.d.ts} +1 -1
  43. package/dist/{spark-BUOx3U7Q.d.cts → spark-B_7nZx6T.d.cts} +112 -10
  44. package/dist/{spark-BUOx3U7Q.d.ts → spark-B_7nZx6T.d.ts} +112 -10
  45. package/dist/{spark-wallet-B_96y9BS.d.ts → spark-wallet-C1Tr_VKI.d.ts} +38 -28
  46. package/dist/{spark-wallet-CHwKQYJu.d.cts → spark-wallet-DG3x2obf.d.cts} +38 -28
  47. package/dist/spark-wallet.node-CGxoeCpH.d.ts +13 -0
  48. package/dist/spark-wallet.node-CN9LoB_O.d.cts +13 -0
  49. package/dist/tests/test-utils.cjs +1086 -218
  50. package/dist/tests/test-utils.d.cts +13 -13
  51. package/dist/tests/test-utils.d.ts +13 -13
  52. package/dist/tests/test-utils.js +56 -19
  53. package/dist/types/index.cjs +97 -9
  54. package/dist/types/index.d.cts +3 -3
  55. package/dist/types/index.d.ts +3 -3
  56. package/dist/types/index.js +3 -3
  57. package/dist/{xchain-address-D5MIHCDL.d.cts → xchain-address-BHu6CpZC.d.ts} +55 -8
  58. package/dist/{xchain-address-DLbW1iDh.d.ts → xchain-address-HBr6isnc.d.cts} +55 -8
  59. package/package.json +1 -1
  60. package/src/graphql/client.ts +8 -0
  61. package/src/graphql/mutations/CompleteLeavesSwap.ts +9 -1
  62. package/src/graphql/mutations/RequestSwapLeaves.ts +4 -0
  63. package/src/graphql/objects/CompleteLeavesSwapInput.ts +34 -34
  64. package/src/graphql/objects/LeavesSwapRequest.ts +4 -0
  65. package/src/graphql/objects/RequestLeavesSwapInput.ts +48 -47
  66. package/src/graphql/objects/SwapLeaf.ts +40 -32
  67. package/src/graphql/objects/UserLeafInput.ts +24 -0
  68. package/src/graphql/objects/UserRequest.ts +4 -0
  69. package/src/index.node.ts +1 -1
  70. package/src/native/index.ts +4 -5
  71. package/src/proto/spark.ts +172 -16
  72. package/src/proto/spark_token.ts +369 -0
  73. package/src/services/coop-exit.ts +171 -36
  74. package/src/services/deposit.ts +471 -74
  75. package/src/services/lightning.ts +18 -5
  76. package/src/services/signing.ts +162 -50
  77. package/src/services/token-transactions.ts +6 -2
  78. package/src/services/transfer.ts +950 -384
  79. package/src/services/tree-creation.ts +342 -121
  80. package/src/spark-wallet/spark-wallet.node.ts +71 -66
  81. package/src/spark-wallet/spark-wallet.ts +459 -166
  82. package/src/tests/integration/coop-exit.test.ts +3 -8
  83. package/src/tests/integration/deposit.test.ts +3 -3
  84. package/src/tests/integration/lightning.test.ts +521 -466
  85. package/src/tests/integration/swap.test.ts +559 -307
  86. package/src/tests/integration/transfer.test.ts +625 -623
  87. package/src/tests/integration/wallet.test.ts +2 -2
  88. package/src/tests/integration/watchtower.test.ts +211 -0
  89. package/src/tests/test-utils.ts +63 -14
  90. package/src/tests/utils/test-faucet.ts +4 -2
  91. package/src/utils/adaptor-signature.ts +15 -5
  92. package/src/utils/fetch.ts +75 -0
  93. package/src/utils/mempool.ts +9 -4
  94. package/src/utils/transaction.ts +388 -26
@@ -88,14 +88,16 @@ import {
88
88
  NetworkType,
89
89
  } from "../utils/network.js";
90
90
  import { sumAvailableTokens } from "../utils/token-transactions.js";
91
- import { getNextTransactionSequence } from "../utils/transaction.js";
91
+ import {
92
+ doesLeafNeedRefresh,
93
+ getCurrentTimelock,
94
+ } from "../utils/transaction.js";
92
95
 
93
96
  import { LRCWallet } from "@buildonspark/lrc20-sdk";
94
97
  import { sha256 } from "@noble/hashes/sha2";
95
98
  import { EventEmitter } from "eventemitter3";
96
99
  import { isReactNative } from "../constants.js";
97
100
  import { Network as NetworkProto, networkToJSON } from "../proto/spark.js";
98
- import { TokenTransactionWithStatus } from "../proto/spark_token.js";
99
101
  import {
100
102
  decodeInvoice,
101
103
  getNetworkFromInvoice,
@@ -136,6 +138,11 @@ import type {
136
138
  TransferParams,
137
139
  UserTokenMetadata,
138
140
  } from "./types.js";
141
+ import {
142
+ TokenTransactionWithStatus,
143
+ TokenMetadata,
144
+ } from "../proto/spark_token.js";
145
+ import { getFetch } from "../utils/fetch.js";
139
146
 
140
147
  /**
141
148
  * The SparkWallet class is the primary interface for interacting with the Spark network.
@@ -439,37 +446,43 @@ export class SparkWallet extends EventEmitter {
439
446
  }
440
447
 
441
448
  public async getLeaves(isBalanceCheck: boolean = false): Promise<TreeNode[]> {
442
- const leaves = await this.queryNodes({
443
- source: {
444
- $case: "ownerIdentityPubkey",
445
- ownerIdentityPubkey: await this.config.signer.getIdentityPublicKey(),
446
- },
447
- includeParents: false,
448
- network: NetworkToProto[this.config.getNetwork()],
449
- });
449
+ const operatorToLeaves = new Map<string, QueryNodesResponse>();
450
+ const ownerIdentityPubkey = await this.config.signer.getIdentityPublicKey();
451
+
452
+ let signingOperators = Object.entries(this.config.getSigningOperators());
453
+ if (isBalanceCheck) {
454
+ // If we're just checking the balance, we can just query the coordinator.
455
+ signingOperators = signingOperators.filter(
456
+ ([id, _]) => id === this.config.getCoordinatorIdentifier(),
457
+ );
458
+ }
459
+ await Promise.all(
460
+ signingOperators.map(async ([id, operator]) => {
461
+ const leaves = await this.queryNodes(
462
+ {
463
+ source: {
464
+ $case: "ownerIdentityPubkey",
465
+ ownerIdentityPubkey,
466
+ },
467
+ includeParents: false,
468
+ network: NetworkToProto[this.config.getNetwork()],
469
+ },
470
+ operator.address,
471
+ );
472
+ operatorToLeaves.set(id, leaves);
473
+ }),
474
+ );
450
475
 
476
+ const leaves = operatorToLeaves.get(
477
+ this.config.getCoordinatorIdentifier(),
478
+ )!;
451
479
  const leavesToIgnore: Set<string> = new Set();
452
- // Query the leaf states from other operators.
453
- // We'll ignore the leaves that are out of sync for now.
454
- // Still include the leaves that are out of sync for balance check.
455
480
  if (!isBalanceCheck) {
456
- for (const [id, operator] of Object.entries(
457
- this.config.getSigningOperators(),
458
- )) {
481
+ // Query the leaf states from other operators.
482
+ // We'll ignore the leaves that are out of sync for now.
483
+ // Still include the leaves that are out of sync for balance check.
484
+ for (const [id, operatorLeaves] of operatorToLeaves) {
459
485
  if (id !== this.config.getCoordinatorIdentifier()) {
460
- const operatorLeaves = await this.queryNodes(
461
- {
462
- source: {
463
- $case: "ownerIdentityPubkey",
464
- ownerIdentityPubkey:
465
- await this.config.signer.getIdentityPublicKey(),
466
- },
467
- includeParents: false,
468
- network: NetworkToProto[this.config.getNetwork()],
469
- },
470
- operator.address,
471
- );
472
-
473
486
  // Loop over leaves returned by coordinator.
474
487
  // If the leaf is not present in the operator's leaves, we'll ignore it.
475
488
  // If the leaf is present, we'll check if the leaf is in sync with the operator's leaf.
@@ -499,17 +512,30 @@ export class SparkWallet extends EventEmitter {
499
512
  }
500
513
  }
501
514
 
502
- const verifyKey = (
503
- pubkey1: Uint8Array,
504
- pubkey2: Uint8Array,
505
- verifyingKey: Uint8Array,
506
- ) => {
507
- return equalBytes(addPublicKeys(pubkey1, pubkey2), verifyingKey);
508
- };
515
+ const availableLeaves = Object.entries(leaves.nodes).filter(
516
+ ([_, node]) => node.status === "AVAILABLE",
517
+ );
509
518
 
510
- for (const [id, leaf] of Object.entries(leaves.nodes)) {
519
+ for (const [id, leaf] of availableLeaves) {
511
520
  if (
512
- !verifyKey(
521
+ leaf.parentNodeId &&
522
+ leaf.status === "AVAILABLE" &&
523
+ this.verifyKey(
524
+ await this.config.signer.getPublicKeyFromDerivation({
525
+ type: KeyDerivationType.LEAF,
526
+ path: leaf.parentNodeId,
527
+ }),
528
+ leaf.signingKeyshare?.publicKey ?? new Uint8Array(),
529
+ leaf.verifyingPublicKey,
530
+ )
531
+ ) {
532
+ this.transferLeavesToSelf([leaf], {
533
+ type: KeyDerivationType.LEAF,
534
+ path: leaf.parentNodeId,
535
+ });
536
+ leavesToIgnore.add(id);
537
+ } else if (
538
+ !this.verifyKey(
513
539
  await this.config.signer.getPublicKeyFromDerivation({
514
540
  type: KeyDerivationType.LEAF,
515
541
  path: leaf.id,
@@ -522,14 +548,33 @@ export class SparkWallet extends EventEmitter {
522
548
  }
523
549
  }
524
550
 
525
- return Object.entries(leaves.nodes)
526
- .filter(
527
- ([_, node]) =>
528
- node.status === "AVAILABLE" && !leavesToIgnore.has(node.id),
529
- )
551
+ return availableLeaves
552
+ .filter(([_, node]) => !leavesToIgnore.has(node.id))
530
553
  .map(([_, node]) => node);
531
554
  }
532
555
 
556
+ private async checkExtendLeaves(leaves: TreeNode[]): Promise<void> {
557
+ await this.withLeaves(async () => {
558
+ for (const leaf of leaves) {
559
+ if (!leaf.parentNodeId && leaf.status === "AVAILABLE") {
560
+ const res = await this.transferService.extendTimelock(leaf);
561
+ await this.transferLeavesToSelf(res.nodes, {
562
+ type: KeyDerivationType.LEAF,
563
+ path: leaf.id,
564
+ });
565
+ }
566
+ }
567
+ });
568
+ }
569
+
570
+ private verifyKey(
571
+ pubkey1: Uint8Array,
572
+ pubkey2: Uint8Array,
573
+ verifyingKey: Uint8Array,
574
+ ): boolean {
575
+ return equalBytes(addPublicKeys(pubkey1, pubkey2), verifyingKey);
576
+ }
577
+
533
578
  private async selectLeaves(
534
579
  targetAmounts: number[],
535
580
  ): Promise<Map<number, TreeNode[]>> {
@@ -705,6 +750,8 @@ export class SparkWallet extends EventEmitter {
705
750
  leaves = await this.checkExtendTimeLockNodes(leaves);
706
751
 
707
752
  this.leaves = leaves;
753
+ this.checkExtendLeaves(leaves);
754
+
708
755
  this.optimizeLeaves().catch((e) => {
709
756
  console.error("Failed to optimize leaves", e);
710
757
  });
@@ -1000,26 +1047,80 @@ export class SparkWallet extends EventEmitter {
1000
1047
  })),
1001
1048
  );
1002
1049
 
1003
- const { transfer, signatureMap } =
1004
- await this.transferService.startSwapSignRefund(
1005
- leafKeyTweaks,
1006
- hexToBytes(this.config.getSspIdentityPublicKey()),
1007
- new Date(Date.now() + 2 * 60 * 1000),
1008
- );
1050
+ const {
1051
+ transfer,
1052
+ signatureMap,
1053
+ directSignatureMap,
1054
+ directFromCpfpSignatureMap,
1055
+ } = await this.transferService.startSwapSignRefund(
1056
+ leafKeyTweaks,
1057
+ hexToBytes(this.config.getSspIdentityPublicKey()),
1058
+ new Date(Date.now() + 2 * 60 * 1000),
1059
+ );
1060
+
1009
1061
  try {
1010
1062
  if (!transfer.leaves[0]?.leaf) {
1063
+ console.error("[processSwapBatch] First leaf is missing");
1011
1064
  throw new Error("Failed to get leaf");
1012
1065
  }
1013
1066
 
1014
- const refundSignature = signatureMap.get(transfer.leaves[0].leaf.id);
1015
- if (!refundSignature) {
1016
- throw new Error("Failed to get refund signature");
1067
+ const cpfpRefundSignature = signatureMap.get(transfer.leaves[0].leaf.id);
1068
+ if (!cpfpRefundSignature) {
1069
+ console.error(
1070
+ "[processSwapBatch] Missing CPFP refund signature for first leaf",
1071
+ );
1072
+ throw new Error("Failed to get CPFP refund signature");
1073
+ }
1074
+
1075
+ const directRefundSignature = directSignatureMap.get(
1076
+ transfer.leaves[0].leaf.id,
1077
+ );
1078
+ if (!directRefundSignature) {
1079
+ console.error(
1080
+ "[processSwapBatch] Missing direct refund signature for first leaf",
1081
+ );
1082
+ throw new Error("Failed to get direct refund signature");
1083
+ }
1084
+
1085
+ const directFromCpfpRefundSignature = directFromCpfpSignatureMap.get(
1086
+ transfer.leaves[0].leaf.id,
1087
+ );
1088
+ if (!directFromCpfpRefundSignature) {
1089
+ console.error(
1090
+ "[processSwapBatch] Missing direct from CPFP refund signature for first leaf",
1091
+ );
1092
+ throw new Error("Failed to get direct from CPFP refund signature");
1093
+ }
1094
+
1095
+ const {
1096
+ adaptorPrivateKey: cpfpAdaptorPrivateKey,
1097
+ adaptorSignature: cpfpAdaptorSignature,
1098
+ } = generateAdaptorFromSignature(cpfpRefundSignature);
1099
+
1100
+ let directAdaptorPrivateKey: Uint8Array = new Uint8Array();
1101
+ let directAdaptorSignature: Uint8Array = new Uint8Array();
1102
+ let directFromCpfpAdaptorPrivateKey: Uint8Array = new Uint8Array();
1103
+ let directFromCpfpAdaptorSignature: Uint8Array = new Uint8Array();
1104
+
1105
+ if (directRefundSignature.length > 0) {
1106
+ const { adaptorPrivateKey, adaptorSignature } =
1107
+ generateAdaptorFromSignature(directRefundSignature);
1108
+
1109
+ directAdaptorPrivateKey = adaptorPrivateKey;
1110
+ directAdaptorSignature = adaptorSignature;
1017
1111
  }
1018
1112
 
1019
- const { adaptorPrivateKey, adaptorSignature } =
1020
- generateAdaptorFromSignature(refundSignature);
1113
+ if (directFromCpfpRefundSignature.length > 0) {
1114
+ const { adaptorPrivateKey, adaptorSignature } =
1115
+ generateAdaptorFromSignature(directFromCpfpRefundSignature);
1116
+ directFromCpfpAdaptorPrivateKey = adaptorPrivateKey;
1117
+ directFromCpfpAdaptorSignature = adaptorSignature;
1118
+ }
1021
1119
 
1022
1120
  if (!transfer.leaves[0].leaf) {
1121
+ console.error(
1122
+ "[processSwapBatch] First leaf missing when preparing user leaves",
1123
+ );
1023
1124
  throw new Error("Failed to get leaf");
1024
1125
  }
1025
1126
 
@@ -1029,42 +1130,128 @@ export class SparkWallet extends EventEmitter {
1029
1130
  raw_unsigned_refund_transaction: bytesToHex(
1030
1131
  transfer.leaves[0].intermediateRefundTx,
1031
1132
  ),
1032
- adaptor_added_signature: bytesToHex(adaptorSignature),
1133
+ direct_raw_unsigned_refund_transaction: bytesToHex(
1134
+ transfer.leaves[0].intermediateDirectRefundTx,
1135
+ ),
1136
+ direct_from_cpfp_raw_unsigned_refund_transaction: bytesToHex(
1137
+ transfer.leaves[0].intermediateDirectFromCpfpRefundTx,
1138
+ ),
1139
+ adaptor_added_signature: bytesToHex(cpfpAdaptorSignature),
1140
+ direct_adaptor_added_signature: bytesToHex(directAdaptorSignature),
1141
+ direct_from_cpfp_adaptor_added_signature: bytesToHex(
1142
+ directFromCpfpAdaptorSignature,
1143
+ ),
1033
1144
  });
1034
1145
 
1035
1146
  for (let i = 1; i < transfer.leaves.length; i++) {
1036
1147
  const leaf = transfer.leaves[i];
1037
1148
  if (!leaf?.leaf) {
1149
+ console.error(`[processSwapBatch] Leaf ${i + 1} is missing`);
1038
1150
  throw new Error("Failed to get leaf");
1039
1151
  }
1040
1152
 
1041
- const refundSignature = signatureMap.get(leaf.leaf.id);
1042
- if (!refundSignature) {
1043
- throw new Error("Failed to get refund signature");
1153
+ const cpfpRefundSignature = signatureMap.get(leaf.leaf.id);
1154
+ if (!cpfpRefundSignature) {
1155
+ console.error(
1156
+ `[processSwapBatch] Missing CPFP refund signature for leaf ${i + 1}`,
1157
+ );
1158
+ throw new Error("Failed to get CPFP refund signature");
1159
+ }
1160
+
1161
+ const directRefundSignature = directSignatureMap.get(leaf.leaf.id);
1162
+ if (!directRefundSignature) {
1163
+ console.error(
1164
+ `[processSwapBatch] Missing direct refund signature for leaf ${i + 1}`,
1165
+ );
1166
+ throw new Error("Failed to get direct refund signature");
1167
+ }
1168
+
1169
+ const directFromCpfpRefundSignature = directFromCpfpSignatureMap.get(
1170
+ leaf.leaf.id,
1171
+ );
1172
+ if (!directFromCpfpRefundSignature) {
1173
+ console.error(
1174
+ `[processSwapBatch] Missing direct from CPFP refund signature for leaf ${i + 1}`,
1175
+ );
1176
+ throw new Error("Failed to get direct from CPFP refund signature");
1044
1177
  }
1045
1178
 
1046
- const signature = generateSignatureFromExistingAdaptor(
1047
- refundSignature,
1048
- adaptorPrivateKey,
1179
+ const cpfpSignature = generateSignatureFromExistingAdaptor(
1180
+ cpfpRefundSignature,
1181
+ cpfpAdaptorPrivateKey,
1049
1182
  );
1050
1183
 
1184
+ let directSignature: Uint8Array = new Uint8Array();
1185
+ if (directRefundSignature.length > 0) {
1186
+ directSignature = generateSignatureFromExistingAdaptor(
1187
+ directRefundSignature,
1188
+ directAdaptorPrivateKey,
1189
+ );
1190
+ }
1191
+
1192
+ let directFromCpfpSignature: Uint8Array = new Uint8Array();
1193
+ if (directFromCpfpRefundSignature.length > 0) {
1194
+ directFromCpfpSignature = generateSignatureFromExistingAdaptor(
1195
+ directFromCpfpRefundSignature,
1196
+ directFromCpfpAdaptorPrivateKey,
1197
+ );
1198
+ }
1199
+
1051
1200
  userLeaves.push({
1052
1201
  leaf_id: leaf.leaf.id,
1053
1202
  raw_unsigned_refund_transaction: bytesToHex(
1054
1203
  leaf.intermediateRefundTx,
1055
1204
  ),
1056
- adaptor_added_signature: bytesToHex(signature),
1205
+ direct_raw_unsigned_refund_transaction: bytesToHex(
1206
+ leaf.intermediateDirectRefundTx,
1207
+ ),
1208
+ direct_from_cpfp_raw_unsigned_refund_transaction: bytesToHex(
1209
+ leaf.intermediateDirectFromCpfpRefundTx,
1210
+ ),
1211
+ adaptor_added_signature: bytesToHex(cpfpSignature),
1212
+ direct_adaptor_added_signature: bytesToHex(directSignature),
1213
+ direct_from_cpfp_adaptor_added_signature: bytesToHex(
1214
+ directFromCpfpSignature,
1215
+ ),
1057
1216
  });
1058
1217
  }
1059
1218
 
1060
1219
  const sspClient = this.getSspClient();
1061
- const adaptorPubkey = bytesToHex(
1062
- secp256k1.getPublicKey(adaptorPrivateKey),
1220
+ const cpfpAdaptorPubkey = bytesToHex(
1221
+ secp256k1.getPublicKey(cpfpAdaptorPrivateKey),
1063
1222
  );
1223
+ if (!cpfpAdaptorPubkey) {
1224
+ throw new Error("Failed to generate CPFP adaptor pubkey");
1225
+ }
1226
+
1227
+ let directAdaptorPubkey: string | undefined;
1228
+ if (directAdaptorPrivateKey.length > 0) {
1229
+ directAdaptorPubkey = bytesToHex(
1230
+ secp256k1.getPublicKey(directAdaptorPrivateKey),
1231
+ );
1232
+ }
1233
+
1234
+ let directFromCpfpAdaptorPubkey: string | undefined;
1235
+ if (directFromCpfpAdaptorPrivateKey.length > 0) {
1236
+ directFromCpfpAdaptorPubkey = bytesToHex(
1237
+ secp256k1.getPublicKey(directFromCpfpAdaptorPrivateKey),
1238
+ );
1239
+ }
1240
+
1064
1241
  let request: LeavesSwapRequest | null | undefined = null;
1242
+ const targetAmountSats =
1243
+ targetAmounts?.reduce((acc, amount) => acc + amount, 0) ||
1244
+ leavesBatch.reduce((acc, leaf) => acc + leaf.value, 0);
1245
+ const totalAmountSats = leavesBatch.reduce(
1246
+ (acc, leaf) => acc + leaf.value,
1247
+ 0,
1248
+ );
1249
+
1065
1250
  request = await sspClient.requestLeaveSwap({
1066
1251
  userLeaves,
1067
- adaptorPubkey,
1252
+ adaptorPubkey: cpfpAdaptorPubkey,
1253
+ directAdaptorPubkey: directAdaptorPubkey,
1254
+ directFromCpfpAdaptorPubkey: directFromCpfpAdaptorPubkey,
1068
1255
  targetAmountSats:
1069
1256
  targetAmounts?.reduce((acc, amount) => acc + amount, 0) ||
1070
1257
  leavesBatch.reduce((acc, leaf) => acc + leaf.value, 0),
@@ -1076,6 +1263,7 @@ export class SparkWallet extends EventEmitter {
1076
1263
  });
1077
1264
 
1078
1265
  if (!request) {
1266
+ console.error("[processSwapBatch] Leave swap request returned null");
1079
1267
  throw new Error("Failed to request leaves swap. No response returned.");
1080
1268
  }
1081
1269
 
@@ -1091,37 +1279,127 @@ export class SparkWallet extends EventEmitter {
1091
1279
  });
1092
1280
 
1093
1281
  if (Object.values(nodes.nodes).length !== request.swapLeaves.length) {
1282
+ console.error("[processSwapBatch] Node count mismatch:", {
1283
+ actual: Object.values(nodes.nodes).length,
1284
+ expected: request.swapLeaves.length,
1285
+ });
1094
1286
  throw new Error("Expected same number of nodes as swapLeaves");
1095
1287
  }
1096
1288
 
1097
1289
  for (const [nodeId, node] of Object.entries(nodes.nodes)) {
1098
1290
  if (!node.nodeTx) {
1291
+ console.error(`[processSwapBatch] Node tx missing for ${nodeId}`);
1099
1292
  throw new Error(`Node tx not found for leaf ${nodeId}`);
1100
1293
  }
1101
1294
 
1102
1295
  if (!node.verifyingPublicKey) {
1296
+ console.error(
1297
+ `[processSwapBatch] Verifying public key missing for ${nodeId}`,
1298
+ );
1103
1299
  throw new Error(`Node public key not found for leaf ${nodeId}`);
1104
1300
  }
1105
1301
 
1106
1302
  const leaf = request.swapLeaves.find((leaf) => leaf.leafId === nodeId);
1107
1303
  if (!leaf) {
1304
+ console.error(`[processSwapBatch] Leaf not found for node ${nodeId}`);
1108
1305
  throw new Error(`Leaf not found for node ${nodeId}`);
1109
1306
  }
1110
-
1111
- const nodeTx = getTxFromRawTxBytes(node.nodeTx);
1112
- const refundTxBytes = hexToBytes(leaf.rawUnsignedRefundTransaction);
1113
- const refundTx = getTxFromRawTxBytes(refundTxBytes);
1114
- const sighash = getSigHashFromTx(refundTx, 0, nodeTx.getOutput(0));
1307
+ // Apply CPFP adaptor signature
1308
+ const cpfpNodeTx = getTxFromRawTxBytes(node.nodeTx);
1309
+ const cpfpRefundTxBytes = hexToBytes(leaf.rawUnsignedRefundTransaction);
1310
+ const cpfpRefundTx = getTxFromRawTxBytes(cpfpRefundTxBytes);
1311
+ const cpfpSighash = getSigHashFromTx(
1312
+ cpfpRefundTx,
1313
+ 0,
1314
+ cpfpNodeTx.getOutput(0),
1315
+ );
1115
1316
 
1116
1317
  const nodePublicKey = node.verifyingPublicKey;
1117
-
1118
1318
  const taprootKey = computeTaprootKeyNoScript(nodePublicKey.slice(1));
1119
- const adaptorSignatureBytes = hexToBytes(leaf.adaptorSignedSignature);
1319
+ const cpfpAdaptorSignatureBytes = hexToBytes(
1320
+ leaf.adaptorSignedSignature,
1321
+ );
1322
+ applyAdaptorToSignature(
1323
+ taprootKey.slice(1),
1324
+ cpfpSighash,
1325
+ cpfpAdaptorSignatureBytes,
1326
+ cpfpAdaptorPrivateKey,
1327
+ );
1328
+
1329
+ // Apply direct adaptor signature
1330
+
1331
+ if (!leaf.directRawUnsignedRefundTransaction) {
1332
+ throw new Error(
1333
+ `Direct raw unsigned refund transaction missing for node ${nodeId}`,
1334
+ );
1335
+ }
1336
+ if (!leaf.directAdaptorSignedSignature) {
1337
+ throw new Error(
1338
+ `Direct adaptor signed signature missing for node ${nodeId}`,
1339
+ );
1340
+ }
1341
+
1342
+ const directNodeTx = getTxFromRawTxBytes(node.directTx);
1343
+
1344
+ const directRefundTxBytes = hexToBytes(
1345
+ leaf.directRawUnsignedRefundTransaction,
1346
+ );
1347
+
1348
+ const directRefundTx = getTxFromRawTxBytes(directRefundTxBytes);
1349
+
1350
+ const directSighash = getSigHashFromTx(
1351
+ directRefundTx,
1352
+ 0,
1353
+ directNodeTx.getOutput(0),
1354
+ );
1355
+
1356
+ if (!leaf.directFromCpfpAdaptorSignedSignature) {
1357
+ throw new Error(
1358
+ `Direct adaptor signed signature missing for node ${nodeId}`,
1359
+ );
1360
+ }
1361
+
1362
+ const directAdaptorSignatureBytes = hexToBytes(
1363
+ leaf.directAdaptorSignedSignature,
1364
+ );
1365
+
1366
+ applyAdaptorToSignature(
1367
+ taprootKey.slice(1),
1368
+ directSighash,
1369
+ directAdaptorSignatureBytes,
1370
+ directAdaptorPrivateKey,
1371
+ );
1372
+
1373
+ if (!leaf.directRawUnsignedRefundTransaction) {
1374
+ throw new Error(
1375
+ `Direct raw unsigned refund transaction missing for node ${nodeId}`,
1376
+ );
1377
+ }
1378
+ if (!leaf.directFromCpfpRawUnsignedRefundTransaction) {
1379
+ throw new Error(
1380
+ `Direct raw unsigned refund transaction missing for node ${nodeId}`,
1381
+ );
1382
+ }
1383
+
1384
+ const directFromCpfpRefundTxBytes = hexToBytes(
1385
+ leaf.directFromCpfpRawUnsignedRefundTransaction,
1386
+ );
1387
+ const directFromCpfpRefundTx = getTxFromRawTxBytes(
1388
+ directFromCpfpRefundTxBytes,
1389
+ );
1390
+ const directFromCpfpSighash = getSigHashFromTx(
1391
+ directFromCpfpRefundTx,
1392
+ 0,
1393
+ cpfpNodeTx.getOutput(0),
1394
+ );
1395
+ const directFromCpfpAdaptorSignatureBytes = hexToBytes(
1396
+ leaf.directFromCpfpAdaptorSignedSignature,
1397
+ );
1120
1398
  applyAdaptorToSignature(
1121
1399
  taprootKey.slice(1),
1122
- sighash,
1123
- adaptorSignatureBytes,
1124
- adaptorPrivateKey,
1400
+ directFromCpfpSighash,
1401
+ directFromCpfpAdaptorSignatureBytes,
1402
+ directFromCpfpAdaptorPrivateKey,
1125
1403
  );
1126
1404
  }
1127
1405
 
@@ -1129,15 +1407,24 @@ export class SparkWallet extends EventEmitter {
1129
1407
  transfer,
1130
1408
  leafKeyTweaks,
1131
1409
  signatureMap,
1410
+ directSignatureMap,
1411
+ directFromCpfpSignatureMap,
1132
1412
  );
1133
-
1134
1413
  const completeResponse = await sspClient.completeLeaveSwap({
1135
- adaptorSecretKey: bytesToHex(adaptorPrivateKey),
1414
+ adaptorSecretKey: bytesToHex(cpfpAdaptorPrivateKey),
1415
+ directAdaptorSecretKey: bytesToHex(directAdaptorPrivateKey),
1416
+ directFromCpfpAdaptorSecretKey: bytesToHex(
1417
+ directFromCpfpAdaptorPrivateKey,
1418
+ ),
1136
1419
  userOutboundTransferExternalId: transfer.id,
1137
1420
  leavesSwapRequestId: request.id,
1138
1421
  });
1139
1422
 
1140
1423
  if (!completeResponse || !completeResponse.inboundTransfer?.sparkId) {
1424
+ console.error(
1425
+ "[processSwapBatch] Invalid complete response:",
1426
+ completeResponse,
1427
+ );
1141
1428
  throw new Error("Failed to complete leaves swap");
1142
1429
  }
1143
1430
 
@@ -1146,6 +1433,7 @@ export class SparkWallet extends EventEmitter {
1146
1433
  );
1147
1434
 
1148
1435
  if (!incomingTransfer) {
1436
+ console.error("[processSwapBatch] No incoming transfer found");
1149
1437
  throw new Error("Failed to get incoming transfer");
1150
1438
  }
1151
1439
 
@@ -1156,6 +1444,11 @@ export class SparkWallet extends EventEmitter {
1156
1444
  optimize: false,
1157
1445
  });
1158
1446
  } catch (e) {
1447
+ console.error("[processSwapBatch] Error details:", {
1448
+ error: e,
1449
+ message: (e as Error).message,
1450
+ stack: (e as Error).stack,
1451
+ });
1159
1452
  await this.cancelAllSenderInitiatedTransfers();
1160
1453
  throw new Error(`Failed to request leaves swap: ${e}`);
1161
1454
  }
@@ -1769,8 +2062,9 @@ export class SparkWallet extends EventEmitter {
1769
2062
  });
1770
2063
  }
1771
2064
 
2065
+ const { fetch, Headers } = getFetch();
1772
2066
  const baseUrl = this.config.getElectrsUrl();
1773
- const headers: Record<string, string> = {};
2067
+ const headers = new Headers();
1774
2068
 
1775
2069
  let txHex: string | undefined;
1776
2070
 
@@ -1783,7 +2077,7 @@ export class SparkWallet extends EventEmitter {
1783
2077
  const auth = btoa(
1784
2078
  `${ELECTRS_CREDENTIALS.username}:${ELECTRS_CREDENTIALS.password}`,
1785
2079
  );
1786
- headers["Authorization"] = `Basic ${auth}`;
2080
+ headers.set("Authorization", `Basic ${auth}`);
1787
2081
  }
1788
2082
 
1789
2083
  const response = await fetch(`${baseUrl}/tx/${txid}/hex`, {
@@ -1940,8 +2234,9 @@ export class SparkWallet extends EventEmitter {
1940
2234
  }
1941
2235
 
1942
2236
  const nodes = await mutex.runExclusive(async () => {
2237
+ const { fetch, Headers } = getFetch();
1943
2238
  const baseUrl = this.config.getElectrsUrl();
1944
- const headers: Record<string, string> = {};
2239
+ const headers = new Headers();
1945
2240
 
1946
2241
  let txHex: string | undefined;
1947
2242
 
@@ -1954,7 +2249,7 @@ export class SparkWallet extends EventEmitter {
1954
2249
  const auth = btoa(
1955
2250
  `${ELECTRS_CREDENTIALS.username}:${ELECTRS_CREDENTIALS.password}`,
1956
2251
  );
1957
- headers["Authorization"] = `Basic ${auth}`;
2252
+ headers.set("Authorization", `Basic ${auth}`);
1958
2253
  }
1959
2254
 
1960
2255
  const response = await fetch(`${baseUrl}/tx/${txid}/hex`, {
@@ -2251,10 +2546,16 @@ export class SparkWallet extends EventEmitter {
2251
2546
 
2252
2547
  for (const node of nodes) {
2253
2548
  const nodeTx = getTxFromRawTxBytes(node.nodeTx);
2254
- const { needRefresh } = getNextTransactionSequence(
2255
- nodeTx.getInput(0).sequence,
2256
- );
2257
- if (needRefresh) {
2549
+ const sequence = nodeTx.getInput(0).sequence;
2550
+ if (!sequence) {
2551
+ throw new ValidationError("Invalid node transaction", {
2552
+ field: "sequence",
2553
+ value: nodeTx.getInput(0),
2554
+ expected: "Non-null sequence",
2555
+ });
2556
+ }
2557
+ const needsRefresh = doesLeafNeedRefresh(sequence, true);
2558
+ if (needsRefresh) {
2258
2559
  nodesToExtend.push(node);
2259
2560
  nodeIds.push(node.id);
2260
2561
  } else {
@@ -2299,11 +2600,16 @@ export class SparkWallet extends EventEmitter {
2299
2600
 
2300
2601
  for (const node of nodes) {
2301
2602
  const refundTx = getTxFromRawTxBytes(node.refundTx);
2302
- const { needRefresh } = getNextTransactionSequence(
2303
- refundTx.getInput(0).sequence,
2304
- true,
2305
- );
2306
- if (needRefresh) {
2603
+ const sequence = refundTx.getInput(0).sequence;
2604
+ if (!sequence) {
2605
+ throw new ValidationError("Invalid refund transaction", {
2606
+ field: "sequence",
2607
+ value: refundTx.getInput(0),
2608
+ expected: "Non-null sequence",
2609
+ });
2610
+ }
2611
+ const needsRefresh = doesLeafNeedRefresh(sequence);
2612
+ if (needsRefresh) {
2307
2613
  nodesToRefresh.push(node);
2308
2614
  nodeIds.push(node.id);
2309
2615
  } else {
@@ -2343,7 +2649,7 @@ export class SparkWallet extends EventEmitter {
2343
2649
  }
2344
2650
 
2345
2651
  const { nodes } = await this.transferService.refreshTimelockNodes(
2346
- [node],
2652
+ node,
2347
2653
  parentNode,
2348
2654
  );
2349
2655
 
@@ -2408,6 +2714,9 @@ export class SparkWallet extends EventEmitter {
2408
2714
  leaf: {
2409
2715
  ...leaf.leaf,
2410
2716
  refundTx: leaf.intermediateRefundTx,
2717
+ directRefundTx: leaf.intermediateDirectRefundTx,
2718
+ directFromCpfpRefundTx:
2719
+ leaf.intermediateDirectFromCpfpRefundTx,
2411
2720
  },
2412
2721
  keyDerivation: {
2413
2722
  type: KeyDerivationType.ECIES,
@@ -2503,7 +2812,9 @@ export class SparkWallet extends EventEmitter {
2503
2812
  transfer.status !==
2504
2813
  TransferStatus.TRANSFER_STATUS_RECEIVER_REFUND_SIGNED &&
2505
2814
  transfer.status !==
2506
- TransferStatus.TRANSFER_STATUS_RECEIVER_KEY_TWEAK_APPLIED
2815
+ TransferStatus.TRANSFER_STATUS_RECEIVER_KEY_TWEAK_APPLIED &&
2816
+ transfer.status !==
2817
+ TransferStatus.TRANSFER_STATUS_RECEIVER_KEY_TWEAK_LOCKED
2507
2818
  ) {
2508
2819
  continue;
2509
2820
  }
@@ -2857,6 +3168,8 @@ export class SparkWallet extends EventEmitter {
2857
3168
  swapResponse.transfer,
2858
3169
  leavesToSend,
2859
3170
  new Map(),
3171
+ new Map(),
3172
+ new Map(),
2860
3173
  );
2861
3174
 
2862
3175
  const sspResponse = await sspClient.requestLightningSend({
@@ -3405,12 +3718,16 @@ export class SparkWallet extends EventEmitter {
3405
3718
  tokenTransactionHashes,
3406
3719
  tokenIdentifiers,
3407
3720
  outputIds,
3721
+ pageSize = 100,
3722
+ offset = 0,
3408
3723
  }: {
3409
3724
  ownerPublicKeys?: string[];
3410
3725
  issuerPublicKeys?: string[];
3411
3726
  tokenTransactionHashes?: string[];
3412
3727
  tokenIdentifiers?: string[];
3413
3728
  outputIds?: string[];
3729
+ pageSize?: number;
3730
+ offset?: number;
3414
3731
  }): Promise<TokenTransactionWithStatus[]> {
3415
3732
  return this.tokenTransactionService.queryTokenTransactions({
3416
3733
  ownerPublicKeys,
@@ -3418,6 +3735,8 @@ export class SparkWallet extends EventEmitter {
3418
3735
  tokenTransactionHashes,
3419
3736
  tokenIdentifiers,
3420
3737
  outputIds,
3738
+ pageSize,
3739
+ offset,
3421
3740
  }) as Promise<TokenTransactionWithStatus[]>;
3422
3741
  }
3423
3742
 
@@ -3814,101 +4133,75 @@ export class SparkWallet extends EventEmitter {
3814
4133
  includeParents: true,
3815
4134
  });
3816
4135
 
3817
- const node = response.nodes[nodeId];
3818
- if (!node) {
4136
+ let leaf = response.nodes[nodeId];
4137
+ if (!leaf) {
3819
4138
  throw new ValidationError("Node not found", {
3820
4139
  field: "nodeId",
3821
4140
  value: nodeId,
3822
4141
  });
3823
4142
  }
3824
4143
 
3825
- if (!node.parentNodeId) {
3826
- throw new ValidationError("Node has no parent", {
3827
- field: "parentNodeId",
3828
- value: node.parentNodeId,
3829
- });
4144
+ let parentNode;
4145
+ let hasParentNode = false;
4146
+ if (!leaf.parentNodeId) {
4147
+ // skip node timelock check
4148
+ } else {
4149
+ hasParentNode = true;
4150
+ parentNode = response.nodes[leaf.parentNodeId];
4151
+ if (!parentNode) {
4152
+ throw new ValidationError("Parent node not found", {
4153
+ field: "parentNodeId",
4154
+ value: leaf.parentNodeId,
4155
+ });
4156
+ }
3830
4157
  }
3831
4158
 
3832
- const parentNode = response.nodes[node.parentNodeId];
3833
- if (!parentNode) {
3834
- throw new ValidationError("Parent node not found", {
3835
- field: "parentNodeId",
3836
- value: node.parentNodeId,
3837
- });
3838
- }
4159
+ const nodeTx = getTxFromRawTxBytes(leaf.nodeTx);
4160
+ const refundTx = getTxFromRawTxBytes(leaf.refundTx);
3839
4161
 
3840
- // Call the transfer service to refresh the timelock
3841
- const result = await this.transferService.refreshTimelockNodes(
3842
- [node],
3843
- parentNode,
3844
- );
4162
+ if (hasParentNode) {
4163
+ const nodeTimelock = getCurrentTimelock(nodeTx.getInput(0).sequence);
4164
+ if (nodeTimelock > 100) {
4165
+ const expiredNodeTxLeaf =
4166
+ await this.transferService.testonly_expireTimeLockNodeTx(
4167
+ leaf,
4168
+ parentNode,
4169
+ );
3845
4170
 
3846
- // Update the local leaves if this node is in our wallet
3847
- const leafIndex = this.leaves.findIndex((leaf) => leaf.id === node.id);
3848
- if (leafIndex !== -1 && result.nodes.length > 0) {
3849
- const newNode = result.nodes[0];
3850
- if (newNode) {
3851
- this.leaves[leafIndex] = newNode;
4171
+ if (!expiredNodeTxLeaf.nodes[0]) {
4172
+ throw new ValidationError("No expired node tx leaf", {
4173
+ field: "expiredNodeTxLeaf",
4174
+ value: expiredNodeTxLeaf,
4175
+ });
4176
+ }
4177
+ leaf = expiredNodeTxLeaf.nodes[0];
3852
4178
  }
3853
4179
  }
3854
- } catch (error) {
3855
- throw new NetworkError(
3856
- "Failed to refresh timelock",
3857
- {
3858
- method: "refresh_timelock",
3859
- },
3860
- error as Error,
3861
- );
3862
- }
3863
- }
3864
-
3865
- /**
3866
- * Refresh the timelock of a specific node's refund transaction only.
3867
- *
3868
- * @param {string} nodeId - The ID of the node whose refund transaction to refresh
3869
- * @returns {Promise<void>} Promise that resolves when the refund timelock is refreshed
3870
- */
3871
- public async testOnly_expireTimelockRefundTx(nodeId: string): Promise<void> {
3872
- const sparkClient = await this.connectionManager.createSparkClient(
3873
- this.config.getCoordinatorAddress(),
3874
- );
4180
+ const refundTimelock = getCurrentTimelock(refundTx.getInput(0).sequence);
3875
4181
 
3876
- try {
3877
- // Get the node
3878
- const response = await sparkClient.query_nodes({
3879
- source: {
3880
- $case: "nodeIds",
3881
- nodeIds: {
3882
- nodeIds: [nodeId],
3883
- },
3884
- },
3885
- includeParents: false,
3886
- });
4182
+ if (refundTimelock > 100) {
4183
+ const expiredTxLeaf =
4184
+ await this.transferService.testonly_expireTimeLockRefundtx(leaf);
3887
4185
 
3888
- const node = response.nodes[nodeId];
3889
- if (!node) {
3890
- throw new ValidationError("Node not found", {
3891
- field: "nodeId",
3892
- value: nodeId,
3893
- });
4186
+ if (!expiredTxLeaf.nodes[0]) {
4187
+ throw new ValidationError("No expired tx leaf", {
4188
+ field: "expiredTxLeaf",
4189
+ value: expiredTxLeaf,
4190
+ });
4191
+ }
4192
+ leaf = expiredTxLeaf.nodes[0];
3894
4193
  }
3895
4194
 
3896
- // Call the transfer service to refresh the refund timelock
3897
- const result = await this.transferService.refreshTimelockRefundTx(node);
3898
-
3899
4195
  // Update the local leaves if this node is in our wallet
3900
- const leafIndex = this.leaves.findIndex((leaf) => leaf.id === node.id);
3901
- if (leafIndex !== -1 && result.nodes.length > 0) {
3902
- const newNode = result.nodes[0];
3903
- if (newNode) {
3904
- this.leaves[leafIndex] = newNode;
3905
- }
4196
+ const leafIndex = this.leaves.findIndex((leaf) => leaf.id === leaf.id);
4197
+ if (leafIndex !== -1) {
4198
+ this.leaves[leafIndex] = leaf;
3906
4199
  }
3907
4200
  } catch (error) {
3908
4201
  throw new NetworkError(
3909
- "Failed to refresh refund timelock",
4202
+ "Failed to refresh timelock",
3910
4203
  {
3911
- method: "refresh_timelock_refund_tx",
4204
+ method: "refresh_timelock",
3912
4205
  },
3913
4206
  error as Error,
3914
4207
  );