@buildonspark/spark-sdk 0.1.38 → 0.1.40

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 (104) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/android/build/intermediates/incremental/mergeDebugJniLibFolders/merger.xml +1 -1
  3. package/android/build/intermediates/library_jni/debug/copyDebugJniLibsProjectOnly/jni/arm64-v8a/libspark_frost.so +0 -0
  4. package/android/build/intermediates/library_jni/debug/copyDebugJniLibsProjectOnly/jni/arm64-v8a/libuniffi_spark_frost.so +0 -0
  5. package/android/build/intermediates/library_jni/debug/copyDebugJniLibsProjectOnly/jni/armeabi-v7a/libspark_frost.so +0 -0
  6. package/android/build/intermediates/library_jni/debug/copyDebugJniLibsProjectOnly/jni/armeabi-v7a/libuniffi_spark_frost.so +0 -0
  7. package/android/build/intermediates/library_jni/debug/copyDebugJniLibsProjectOnly/jni/x86/libspark_frost.so +0 -0
  8. package/android/build/intermediates/library_jni/debug/copyDebugJniLibsProjectOnly/jni/x86/libuniffi_spark_frost.so +0 -0
  9. package/android/build/intermediates/library_jni/debug/copyDebugJniLibsProjectOnly/jni/x86_64/libspark_frost.so +0 -0
  10. package/android/build/intermediates/library_jni/debug/copyDebugJniLibsProjectOnly/jni/x86_64/libuniffi_spark_frost.so +0 -0
  11. package/android/build/intermediates/merged_jni_libs/debug/mergeDebugJniLibFolders/out/arm64-v8a/libspark_frost.so +0 -0
  12. package/android/build/intermediates/merged_jni_libs/debug/mergeDebugJniLibFolders/out/arm64-v8a/libuniffi_spark_frost.so +0 -0
  13. package/android/build/intermediates/merged_jni_libs/debug/mergeDebugJniLibFolders/out/armeabi-v7a/libspark_frost.so +0 -0
  14. package/android/build/intermediates/merged_jni_libs/debug/mergeDebugJniLibFolders/out/armeabi-v7a/libuniffi_spark_frost.so +0 -0
  15. package/android/build/intermediates/merged_jni_libs/debug/mergeDebugJniLibFolders/out/x86/libspark_frost.so +0 -0
  16. package/android/build/intermediates/merged_jni_libs/debug/mergeDebugJniLibFolders/out/x86/libuniffi_spark_frost.so +0 -0
  17. package/android/build/intermediates/merged_jni_libs/debug/mergeDebugJniLibFolders/out/x86_64/libspark_frost.so +0 -0
  18. package/android/build/intermediates/merged_jni_libs/debug/mergeDebugJniLibFolders/out/x86_64/libuniffi_spark_frost.so +0 -0
  19. package/android/build/intermediates/merged_native_libs/debug/mergeDebugNativeLibs/out/lib/arm64-v8a/libspark_frost.so +0 -0
  20. package/android/build/intermediates/merged_native_libs/debug/mergeDebugNativeLibs/out/lib/arm64-v8a/libuniffi_spark_frost.so +0 -0
  21. package/android/build/intermediates/merged_native_libs/debug/mergeDebugNativeLibs/out/lib/armeabi-v7a/libspark_frost.so +0 -0
  22. package/android/build/intermediates/merged_native_libs/debug/mergeDebugNativeLibs/out/lib/armeabi-v7a/libuniffi_spark_frost.so +0 -0
  23. package/android/build/intermediates/merged_native_libs/debug/mergeDebugNativeLibs/out/lib/x86/libspark_frost.so +0 -0
  24. package/android/build/intermediates/merged_native_libs/debug/mergeDebugNativeLibs/out/lib/x86/libuniffi_spark_frost.so +0 -0
  25. package/android/build/intermediates/merged_native_libs/debug/mergeDebugNativeLibs/out/lib/x86_64/libspark_frost.so +0 -0
  26. package/android/build/intermediates/merged_native_libs/debug/mergeDebugNativeLibs/out/lib/x86_64/libuniffi_spark_frost.so +0 -0
  27. package/dist/{RequestLightningSendInput-B4JdzclX.d.ts → RequestLightningSendInput-CJtcHOnu.d.ts} +1 -1
  28. package/dist/{RequestLightningSendInput-39_zGri6.d.cts → RequestLightningSendInput-DfmfqzZo.d.cts} +1 -1
  29. package/dist/address/index.d.cts +1 -1
  30. package/dist/address/index.d.ts +1 -1
  31. package/dist/address/index.js +2 -2
  32. package/dist/{chunk-W3EC5XSA.js → chunk-5MNQB2T4.js} +2 -2
  33. package/dist/chunk-ED3ZAFDI.js +784 -0
  34. package/dist/{chunk-VJTDG4BQ.js → chunk-HK6LPV6Z.js} +10 -1
  35. package/dist/{chunk-7WRK6WNJ.js → chunk-LHT4QTFK.js} +556 -41
  36. package/dist/{chunk-RAPBVYJY.js → chunk-RFCXPGDM.js} +26 -4
  37. package/dist/{chunk-DI7QXUQJ.js → chunk-W2VXS35Y.js} +4 -4
  38. package/dist/graphql/objects/index.d.cts +5 -4
  39. package/dist/graphql/objects/index.d.ts +5 -4
  40. package/dist/{index-CxAi2L8y.d.ts → index-BDEYgYxP.d.ts} +42 -4
  41. package/dist/{index-Dm17Ggfe.d.cts → index-CLdtdMU4.d.cts} +42 -4
  42. package/dist/index.cjs +1069 -40
  43. package/dist/index.d.cts +6 -6
  44. package/dist/index.d.ts +6 -6
  45. package/dist/index.js +33 -17
  46. package/dist/index.node.cjs +1069 -40
  47. package/dist/index.node.d.cts +6 -6
  48. package/dist/index.node.d.ts +6 -6
  49. package/dist/index.node.js +33 -17
  50. package/dist/native/index.cjs +1069 -40
  51. package/dist/native/index.d.cts +108 -5
  52. package/dist/native/index.d.ts +108 -5
  53. package/dist/native/index.js +1065 -40
  54. package/dist/{network-GFGEHkS4.d.cts → network-B10hBoHp.d.cts} +8 -1
  55. package/dist/{network-DobHpaV6.d.ts → network-CCgyIsGl.d.ts} +8 -1
  56. package/dist/services/config.cjs +29 -12
  57. package/dist/services/config.d.cts +4 -4
  58. package/dist/services/config.d.ts +4 -4
  59. package/dist/services/config.js +5 -5
  60. package/dist/services/connection.d.cts +4 -4
  61. package/dist/services/connection.d.ts +4 -4
  62. package/dist/services/connection.js +2 -2
  63. package/dist/services/index.cjs +30 -13
  64. package/dist/services/index.d.cts +4 -4
  65. package/dist/services/index.d.ts +4 -4
  66. package/dist/services/index.js +8 -8
  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 +1 -1
  70. package/dist/services/token-transactions.cjs +1 -1
  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.d.cts +4 -4
  75. package/dist/services/wallet-config.d.ts +4 -4
  76. package/dist/signer/signer.cjs +23 -6
  77. package/dist/signer/signer.d.cts +3 -2
  78. package/dist/signer/signer.d.ts +3 -2
  79. package/dist/signer/signer.js +1 -1
  80. package/dist/{signer-DFGw9RRp.d.ts → signer-C5h1DpjF.d.ts} +4 -1
  81. package/dist/{signer-C1t40Wus.d.cts → signer-CYwn7h9U.d.cts} +4 -1
  82. package/dist/types/index.d.cts +4 -3
  83. package/dist/types/index.d.ts +4 -3
  84. package/dist/utils/index.cjs +891 -2
  85. package/dist/utils/index.d.cts +62 -6
  86. package/dist/utils/index.d.ts +62 -6
  87. package/dist/utils/index.js +23 -7
  88. package/package.json +1 -1
  89. package/src/services/deposit.ts +23 -5
  90. package/src/services/token-transactions.ts +1 -1
  91. package/src/services/transfer.ts +218 -11
  92. package/src/services/tree-creation.ts +29 -14
  93. package/src/signer/signer.ts +47 -5
  94. package/src/spark-wallet/spark-wallet.ts +430 -4
  95. package/src/tests/integration/swap.test.ts +225 -0
  96. package/src/tests/integration/tree-creation.test.ts +5 -1
  97. package/src/utils/index.ts +1 -0
  98. package/src/utils/mempool.ts +26 -1
  99. package/src/utils/network.ts +15 -0
  100. package/src/utils/transaction.ts +22 -2
  101. package/src/utils/unilateral-exit.ts +729 -0
  102. package/dist/chunk-E5SL7XTO.js +0 -301
  103. package/dist/{chunk-LIP2K6KR.js → chunk-2CDJZQN4.js} +3 -3
  104. package/dist/{chunk-RGWBSZIO.js → chunk-I4JI6TYN.js} +4 -4
@@ -24,7 +24,10 @@ import {
24
24
  getTxId,
25
25
  } from "../utils/bitcoin.js";
26
26
  import { getNetwork, Network } from "../utils/network.js";
27
- import { getEphemeralAnchorOutput } from "../utils/transaction.js";
27
+ import {
28
+ DEFAULT_FEE_SATS,
29
+ getEphemeralAnchorOutput,
30
+ } from "../utils/transaction.js";
28
31
  import { WalletConfigService } from "./config.js";
29
32
  import { ConnectionManager } from "./connection.js";
30
33
 
@@ -42,6 +45,17 @@ export type CreationNodeWithNonces = CreationNode & {
42
45
 
43
46
  const INITIAL_TIME_LOCK = 2000;
44
47
 
48
+ /**
49
+ * Subtracts the default fee from the amount if it's greater than the fee.
50
+ * Returns the original amount if it's less than or equal to the fee.
51
+ */
52
+ function maybeApplyFee(amount: bigint): bigint {
53
+ if (amount > BigInt(DEFAULT_FEE_SATS)) {
54
+ return amount - BigInt(DEFAULT_FEE_SATS);
55
+ }
56
+ return amount;
57
+ }
58
+
45
59
  export class TreeCreationService {
46
60
  private readonly config: WalletConfigService;
47
61
  private readonly connectionManager: ConnectionManager;
@@ -303,7 +317,7 @@ export class TreeCreationService {
303
317
  children: [],
304
318
  };
305
319
 
306
- const tx = new Transaction();
320
+ const tx = new Transaction({ version: 3 });
307
321
  tx.addInput({
308
322
  txid: getTxId(parentTx),
309
323
  index: vout,
@@ -316,10 +330,9 @@ export class TreeCreationService {
316
330
 
317
331
  tx.addOutput({
318
332
  script: parentTxOut.script,
319
- amount: parentTxOut.amount,
333
+ amount: parentTxOut.amount, // maybeApplyFee(parentTxOut.amount),
320
334
  });
321
335
 
322
- // Add ephemeral anchor
323
336
  tx.addOutput(getEphemeralAnchorOutput());
324
337
 
325
338
  const signingNonceCommitment =
@@ -342,7 +355,7 @@ export class TreeCreationService {
342
355
  children: [],
343
356
  };
344
357
 
345
- const childTx = new Transaction();
358
+ const childTx = new Transaction({ version: 3 });
346
359
  childTx.addInput({
347
360
  txid: getTxId(tx),
348
361
  index: 0,
@@ -351,10 +364,9 @@ export class TreeCreationService {
351
364
 
352
365
  childTx.addOutput({
353
366
  script: parentTxOut.script,
354
- amount: parentTxOut.amount,
367
+ amount: parentTxOut.amount, // maybeApplyFee(parentTxOut.amount),
355
368
  });
356
369
 
357
- // Add ephemeral anchor
358
370
  childTx.addOutput(getEphemeralAnchorOutput());
359
371
 
360
372
  const childSigningNonceCommitment =
@@ -368,7 +380,7 @@ export class TreeCreationService {
368
380
  childCreationNode.nodeTxSigningCommitment = childSigningNonceCommitment;
369
381
  childCreationNode.nodeTxSigningJob = childSigningJob;
370
382
 
371
- const refundTx = new Transaction();
383
+ const refundTx = new Transaction({ version: 3 });
372
384
  refundTx.addInput({
373
385
  txid: getTxId(childTx),
374
386
  index: 0,
@@ -385,9 +397,11 @@ export class TreeCreationService {
385
397
  const refundPkScript = OutScript.encode(refundAddress);
386
398
  refundTx.addOutput({
387
399
  script: refundPkScript,
388
- amount: parentTxOut.amount,
400
+ amount: maybeApplyFee(parentTxOut.amount),
389
401
  });
390
402
 
403
+ refundTx.addOutput(getEphemeralAnchorOutput());
404
+
391
405
  const refundSigningNonceCommitment =
392
406
  await this.config.signer.getRandomSigningCommitment();
393
407
 
@@ -415,7 +429,7 @@ export class TreeCreationService {
415
429
  if (!parentTxOutput?.script || !parentTxOutput?.amount) {
416
430
  throw new Error("parentTxOutput is undefined");
417
431
  }
418
- const rootNodeTx = new Transaction();
432
+ const rootNodeTx = new Transaction({ version: 3 });
419
433
  rootNodeTx.addInput({
420
434
  txid: getTxId(parentTx),
421
435
  index: vout,
@@ -428,15 +442,16 @@ export class TreeCreationService {
428
442
  }
429
443
  const childAddress = Address(getNetwork(network)).decode(child.address);
430
444
  const childPkScript = OutScript.encode(childAddress);
445
+
446
+ // First subtract fee from total amount, then split between children
447
+ // const feeAdjustedAmount = maybeApplyFee(parentTxOutput.amount);
431
448
  rootNodeTx.addOutput({
432
449
  script: childPkScript,
433
- amount: parentTxOutput.amount / 2n,
450
+ amount: parentTxOutput.amount / 2n, // feeAdjustedAmount / 2n,
434
451
  });
435
452
  }
436
453
 
437
- // Add ephemeral anchor output
438
- const anchor = getEphemeralAnchorOutput();
439
- rootNodeTx.addOutput(anchor);
454
+ rootNodeTx.addOutput(getEphemeralAnchorOutput());
440
455
 
441
456
  const rootNodeSigningCommitment =
442
457
  await this.config.signer.getRandomSigningCommitment();
@@ -1,6 +1,7 @@
1
1
  import {
2
2
  bytesToHex,
3
3
  bytesToNumberBE,
4
+ equalBytes,
4
5
  hexToBytes,
5
6
  } from "@noble/curves/abstract/utils";
6
7
  import { schnorr, secp256k1 } from "@noble/curves/secp256k1";
@@ -35,12 +36,16 @@ const getSparkFrostModule = async () => {
35
36
  return sparkFrostModule;
36
37
  };
37
38
 
38
- import { PARITY } from "@buildonspark/lrc20-sdk";
39
- import type { Psbt } from "bitcoinjs-lib";
40
- import { TokenSigner, Receipt } from "@buildonspark/lrc20-sdk";
41
- import { privateNegate, privateAdd } from "@bitcoinerlab/secp256k1";
39
+ import { privateAdd, privateNegate } from "@bitcoinerlab/secp256k1";
40
+ import {
41
+ fromPrivateKey,
42
+ PARITY,
43
+ Receipt,
44
+ TokenSigner,
45
+ } from "@buildonspark/lrc20-sdk";
42
46
  import { sha256 } from "@noble/hashes/sha2";
43
- import { fromPrivateKey } from "@buildonspark/lrc20-sdk";
47
+ import { Transaction } from "@scure/btc-signer";
48
+ import type { Psbt } from "bitcoinjs-lib";
44
49
 
45
50
  export type SigningNonce = {
46
51
  binding: Uint8Array;
@@ -223,6 +228,12 @@ interface SparkSigner extends TokenSigner {
223
228
  signature: Uint8Array,
224
229
  ): Promise<boolean>;
225
230
 
231
+ signTransactionIndex(
232
+ tx: Transaction,
233
+ index: number,
234
+ publicKey: Uint8Array,
235
+ ): void;
236
+
226
237
  encryptLeafPrivateKeyEcies(
227
238
  receiverPublicKey: Uint8Array,
228
239
  publicKey: Uint8Array,
@@ -790,6 +801,37 @@ class DefaultSparkSigner implements SparkSigner {
790
801
  const receiptProof = privateAdd(privateKey, pxhPubkey)!;
791
802
  return Buffer.from(receiptProof);
792
803
  }
804
+
805
+ signTransactionIndex(
806
+ tx: Transaction,
807
+ index: number,
808
+ publicKey: Uint8Array,
809
+ ): void {
810
+ let privateKey: Uint8Array | undefined | null;
811
+
812
+ if (
813
+ equalBytes(publicKey, this.identityKey?.publicKey ?? new Uint8Array())
814
+ ) {
815
+ privateKey = this.identityKey?.privateKey;
816
+ } else if (
817
+ equalBytes(publicKey, this.depositKey?.publicKey ?? new Uint8Array())
818
+ ) {
819
+ privateKey = this.depositKey?.privateKey;
820
+ } else {
821
+ privateKey = hexToBytes(
822
+ this.publicKeyToPrivateKeyMap.get(bytesToHex(publicKey)) ?? "",
823
+ );
824
+ }
825
+
826
+ if (!privateKey) {
827
+ throw new ValidationError("Private key not found for public key", {
828
+ field: "privateKey",
829
+ value: bytesToHex(publicKey),
830
+ });
831
+ }
832
+
833
+ tx.signIdx(privateKey, index);
834
+ }
793
835
  }
794
836
  export { DefaultSparkSigner };
795
837
  export type { SparkSigner };
@@ -72,6 +72,7 @@ import {
72
72
  } from "../utils/adaptor-signature.js";
73
73
  import {
74
74
  computeTaprootKeyNoScript,
75
+ getP2TRScriptFromPublicKey,
75
76
  getP2WPKHAddressFromPublicKey,
76
77
  getSigHashFromTx,
77
78
  getTxFromRawTxBytes,
@@ -430,9 +431,7 @@ export class SparkWallet extends EventEmitter {
430
431
  }
431
432
  }
432
433
 
433
- private async getLeaves(
434
- isBalanceCheck: boolean = false,
435
- ): Promise<TreeNode[]> {
434
+ public async getLeaves(isBalanceCheck: boolean = false): Promise<TreeNode[]> {
436
435
  const leaves = await this.queryNodes({
437
436
  source: {
438
437
  $case: "ownerIdentityPubkey",
@@ -987,7 +986,7 @@ export class SparkWallet extends EventEmitter {
987
986
  );
988
987
  }
989
988
 
990
- await this.transferService.sendTransferTweakKey(
989
+ await this.transferService.deliverTransferPackage(
991
990
  transfer,
992
991
  leafKeyTweaks,
993
992
  signatureMap,
@@ -3116,6 +3115,148 @@ export class SparkWallet extends EventEmitter {
3116
3115
  return this.config.signer.validateMessageWithIdentityKey(hash, signature);
3117
3116
  }
3118
3117
 
3118
+ /**
3119
+ * Signs a transaction with wallet keys.
3120
+ *
3121
+ * @param {string} txHex - The transaction hex to sign
3122
+ * @param {string} keyType - The type of key to use for signing ("identity", "deposit", or "auto-detect")
3123
+ * @returns {Promise<string>} The signed transaction hex
3124
+ */
3125
+ public async signTransaction(
3126
+ txHex: string,
3127
+ keyType: string = "auto-detect",
3128
+ ): Promise<string> {
3129
+ try {
3130
+ // Parse the transaction
3131
+ const tx = Transaction.fromRaw(hexToBytes(txHex));
3132
+
3133
+ let publicKey: Uint8Array;
3134
+
3135
+ switch (keyType.toLowerCase()) {
3136
+ case "identity":
3137
+ publicKey = await this.config.signer.getIdentityPublicKey();
3138
+ break;
3139
+ case "deposit":
3140
+ publicKey = await this.config.signer.getDepositSigningKey();
3141
+ break;
3142
+ case "auto-detect":
3143
+ default:
3144
+ // Try to auto-detect which key to use by examining the transaction inputs
3145
+ const detectedKey = await this.detectKeyForTransaction(tx);
3146
+ if (detectedKey) {
3147
+ publicKey = detectedKey.publicKey;
3148
+ } else {
3149
+ // Fallback to identity key
3150
+ publicKey = await this.config.signer.getIdentityPublicKey();
3151
+ }
3152
+ break;
3153
+ }
3154
+
3155
+ // Check each input to determine which ones need signing
3156
+ let inputsSigned = 0;
3157
+ for (let i = 0; i < tx.inputsLength; i++) {
3158
+ const input = tx.getInput(i);
3159
+ if (!input?.witnessUtxo?.script) {
3160
+ continue;
3161
+ }
3162
+
3163
+ const script = input.witnessUtxo.script;
3164
+
3165
+ // Check if this is an ephemeral anchor (OP_TRUE script)
3166
+ // OP_TRUE is represented as a single byte: 0x51
3167
+ if (script.length === 1 && script[0] === 0x51) {
3168
+ continue;
3169
+ }
3170
+
3171
+ // Check if this script matches one of our keys
3172
+ const identityScript = getP2TRScriptFromPublicKey(
3173
+ publicKey,
3174
+ this.config.getNetwork(),
3175
+ );
3176
+
3177
+ if (bytesToHex(script) === bytesToHex(identityScript)) {
3178
+ // Sign this specific input
3179
+ try {
3180
+ this.config.signer.signTransactionIndex(tx, i, publicKey);
3181
+ inputsSigned++;
3182
+ } catch (error) {
3183
+ throw new ValidationError(`Failed to sign input ${i}: ${error}`, {
3184
+ field: "input",
3185
+ value: i,
3186
+ });
3187
+ }
3188
+ }
3189
+ }
3190
+
3191
+ if (inputsSigned === 0) {
3192
+ throw new Error(
3193
+ "No inputs were signed. Check that the transaction contains inputs controlled by this wallet.",
3194
+ );
3195
+ }
3196
+
3197
+ tx.finalize();
3198
+
3199
+ const signedTxHex = tx.hex;
3200
+
3201
+ return signedTxHex;
3202
+ } catch (error) {
3203
+ console.error("❌ Error signing transaction:", error);
3204
+ throw error;
3205
+ }
3206
+ }
3207
+
3208
+ /**
3209
+ * Helper method to auto-detect which key should be used for signing a transaction.
3210
+ */
3211
+ private async detectKeyForTransaction(tx: Transaction): Promise<{
3212
+ publicKey: Uint8Array;
3213
+ keyType: string;
3214
+ } | null> {
3215
+ try {
3216
+ // Get available keys
3217
+ const identityPubKey = await this.config.signer.getIdentityPublicKey();
3218
+ const depositPubKey = await this.config.signer.getDepositSigningKey();
3219
+
3220
+ // Check if any inputs reference outputs that would be controlled by our keys
3221
+ for (let i = 0; i < tx.inputsLength; i++) {
3222
+ const input = tx.getInput(i);
3223
+ if (input?.witnessUtxo?.script) {
3224
+ const script = input.witnessUtxo.script;
3225
+
3226
+ // Check if this script corresponds to one of our keys
3227
+ // This is a simplified check - in practice, you might need more sophisticated script analysis
3228
+ const identityScript = getP2TRScriptFromPublicKey(
3229
+ identityPubKey,
3230
+ this.config.getNetwork(),
3231
+ );
3232
+ const depositScript = getP2TRScriptFromPublicKey(
3233
+ depositPubKey,
3234
+ this.config.getNetwork(),
3235
+ );
3236
+
3237
+ if (bytesToHex(script) === bytesToHex(identityScript)) {
3238
+ return {
3239
+ publicKey: identityPubKey,
3240
+ keyType: "identity",
3241
+ };
3242
+ }
3243
+
3244
+ if (bytesToHex(script) === bytesToHex(depositScript)) {
3245
+ return {
3246
+ publicKey: depositPubKey,
3247
+ keyType: "deposit",
3248
+ };
3249
+ }
3250
+ }
3251
+ }
3252
+
3253
+ return null;
3254
+ } catch (error) {
3255
+ console.warn("Error during key auto-detection:", error);
3256
+ return null;
3257
+ }
3258
+ }
3259
+
3119
3260
  /**
3120
3261
  * Get a Lightning receive request by ID.
3121
3262
  *
@@ -3153,6 +3294,291 @@ export class SparkWallet extends EventEmitter {
3153
3294
  return await sspClient.getCoopExitRequest(id);
3154
3295
  }
3155
3296
 
3297
+ /**
3298
+ * Check the remaining timelock on a given node.
3299
+ *
3300
+ * @param {string} nodeId - The ID of the node to check
3301
+ * @returns {Promise<{nodeTimelock: number, refundTimelock: number}>} The remaining timelocks in blocks for both node and refund transactions
3302
+ */
3303
+ public async checkTimelock(nodeId: string): Promise<{
3304
+ nodeTimelock: number;
3305
+ refundTimelock: number;
3306
+ }> {
3307
+ const sparkClient = await this.connectionManager.createSparkClient(
3308
+ this.config.getCoordinatorAddress(),
3309
+ );
3310
+
3311
+ try {
3312
+ const response = await sparkClient.query_nodes({
3313
+ source: {
3314
+ $case: "nodeIds",
3315
+ nodeIds: {
3316
+ nodeIds: [nodeId],
3317
+ },
3318
+ },
3319
+ includeParents: false,
3320
+ network: NetworkToProto[this.config.getNetwork()],
3321
+ });
3322
+
3323
+ const node = response.nodes[nodeId];
3324
+ if (!node) {
3325
+ throw new ValidationError("Node not found", {
3326
+ field: "nodeId",
3327
+ value: nodeId,
3328
+ });
3329
+ }
3330
+
3331
+ // Check if this is a root node (no parent)
3332
+ const isRootNode = !node.parentNodeId;
3333
+
3334
+ // Validate transaction data exists
3335
+ if (!node.nodeTx || node.nodeTx.length === 0) {
3336
+ throw new ValidationError(
3337
+ `Node transaction data is missing or empty for ${isRootNode ? "root" : "non-root"} node`,
3338
+ {
3339
+ field: "nodeTx",
3340
+ value: node.nodeTx?.length || 0,
3341
+ },
3342
+ );
3343
+ }
3344
+
3345
+ if (!node.refundTx || node.refundTx.length === 0) {
3346
+ throw new ValidationError(
3347
+ `Refund transaction data is missing or empty for ${isRootNode ? "root" : "non-root"} node`,
3348
+ {
3349
+ field: "refundTx",
3350
+ value: node.refundTx?.length || 0,
3351
+ },
3352
+ );
3353
+ }
3354
+
3355
+ let nodeTx, refundTx;
3356
+
3357
+ try {
3358
+ // Get the node transaction to check its timelock
3359
+ nodeTx = getTxFromRawTxBytes(node.nodeTx);
3360
+ } catch (error) {
3361
+ throw new ValidationError(
3362
+ `Failed to parse node transaction for ${isRootNode ? "root" : "non-root"} node: ${error instanceof Error ? error.message : String(error)}`,
3363
+ {
3364
+ field: "nodeTx",
3365
+ value: node.nodeTx.length,
3366
+ },
3367
+ );
3368
+ }
3369
+
3370
+ try {
3371
+ // Get the refund transaction to check its timelock
3372
+ refundTx = getTxFromRawTxBytes(node.refundTx);
3373
+ } catch (error) {
3374
+ throw new ValidationError(
3375
+ `Failed to parse refund transaction for ${isRootNode ? "root" : "non-root"} node: ${error instanceof Error ? error.message : String(error)}`,
3376
+ {
3377
+ field: "refundTx",
3378
+ value: node.refundTx.length,
3379
+ },
3380
+ );
3381
+ }
3382
+
3383
+ const nodeInput = nodeTx.getInput(0);
3384
+ if (!nodeInput) {
3385
+ throw new ValidationError(
3386
+ `Node transaction has no inputs for ${isRootNode ? "root" : "non-root"} node`,
3387
+ {
3388
+ field: "nodeInput",
3389
+ value: nodeTx.inputsLength,
3390
+ },
3391
+ );
3392
+ }
3393
+
3394
+ if (!nodeInput.sequence) {
3395
+ throw new ValidationError(
3396
+ `Node transaction has no sequence for ${isRootNode ? "root" : "non-root"} node`,
3397
+ {
3398
+ field: "sequence",
3399
+ value: nodeInput.sequence,
3400
+ },
3401
+ );
3402
+ }
3403
+
3404
+ const refundInput = refundTx.getInput(0);
3405
+ if (!refundInput) {
3406
+ throw new ValidationError(
3407
+ `Refund transaction has no inputs for ${isRootNode ? "root" : "non-root"} node`,
3408
+ {
3409
+ field: "refundInput",
3410
+ value: refundTx.inputsLength,
3411
+ },
3412
+ );
3413
+ }
3414
+
3415
+ if (!refundInput.sequence) {
3416
+ throw new ValidationError(
3417
+ `Refund transaction has no sequence for ${isRootNode ? "root" : "non-root"} node`,
3418
+ {
3419
+ field: "sequence",
3420
+ value: refundInput.sequence,
3421
+ },
3422
+ );
3423
+ }
3424
+
3425
+ // Extract timelock from sequence (lower 16 bits)
3426
+ const nodeTimelock = nodeInput.sequence & 0xffff;
3427
+ const refundTimelock = refundInput.sequence & 0xffff;
3428
+
3429
+ return {
3430
+ nodeTimelock,
3431
+ refundTimelock,
3432
+ };
3433
+ } catch (error) {
3434
+ throw new NetworkError(
3435
+ `Failed to check timelock for node ${nodeId}`,
3436
+ {
3437
+ method: "query_nodes",
3438
+ },
3439
+ error as Error,
3440
+ );
3441
+ }
3442
+ }
3443
+
3444
+ /**
3445
+ * Refresh the timelock of a specific node.
3446
+ *
3447
+ * @param {string} nodeId - The ID of the node to refresh
3448
+ * @returns {Promise<void>} Promise that resolves when the timelock is refreshed
3449
+ */
3450
+ public async testOnly_expireTimelock(nodeId: string): Promise<void> {
3451
+ const sparkClient = await this.connectionManager.createSparkClient(
3452
+ this.config.getCoordinatorAddress(),
3453
+ );
3454
+
3455
+ try {
3456
+ // First, get the node and its parent
3457
+ const response = await sparkClient.query_nodes({
3458
+ source: {
3459
+ $case: "nodeIds",
3460
+ nodeIds: {
3461
+ nodeIds: [nodeId],
3462
+ },
3463
+ },
3464
+ includeParents: true,
3465
+ });
3466
+
3467
+ const node = response.nodes[nodeId];
3468
+ if (!node) {
3469
+ throw new ValidationError("Node not found", {
3470
+ field: "nodeId",
3471
+ value: nodeId,
3472
+ });
3473
+ }
3474
+
3475
+ if (!node.parentNodeId) {
3476
+ throw new ValidationError("Node has no parent", {
3477
+ field: "parentNodeId",
3478
+ value: node.parentNodeId,
3479
+ });
3480
+ }
3481
+
3482
+ const parentNode = response.nodes[node.parentNodeId];
3483
+ if (!parentNode) {
3484
+ throw new ValidationError("Parent node not found", {
3485
+ field: "parentNodeId",
3486
+ value: node.parentNodeId,
3487
+ });
3488
+ }
3489
+
3490
+ // Generate signing public key for this node
3491
+ const signingPubKey = await this.config.signer.generatePublicKey(
3492
+ sha256(node.id),
3493
+ );
3494
+
3495
+ // Call the transfer service to refresh the timelock
3496
+ const result = await this.transferService.refreshTimelockNodes(
3497
+ [node],
3498
+ parentNode,
3499
+ signingPubKey,
3500
+ );
3501
+
3502
+ // Update the local leaves if this node is in our wallet
3503
+ const leafIndex = this.leaves.findIndex((leaf) => leaf.id === node.id);
3504
+ if (leafIndex !== -1 && result.nodes.length > 0) {
3505
+ const newNode = result.nodes[0];
3506
+ if (newNode) {
3507
+ this.leaves[leafIndex] = newNode;
3508
+ }
3509
+ }
3510
+ } catch (error) {
3511
+ throw new NetworkError(
3512
+ "Failed to refresh timelock",
3513
+ {
3514
+ method: "refresh_timelock",
3515
+ },
3516
+ error as Error,
3517
+ );
3518
+ }
3519
+ }
3520
+
3521
+ /**
3522
+ * Refresh the timelock of a specific node's refund transaction only.
3523
+ *
3524
+ * @param {string} nodeId - The ID of the node whose refund transaction to refresh
3525
+ * @returns {Promise<void>} Promise that resolves when the refund timelock is refreshed
3526
+ */
3527
+ public async testOnly_expireTimelockRefundTx(nodeId: string): Promise<void> {
3528
+ const sparkClient = await this.connectionManager.createSparkClient(
3529
+ this.config.getCoordinatorAddress(),
3530
+ );
3531
+
3532
+ try {
3533
+ // Get the node
3534
+ const response = await sparkClient.query_nodes({
3535
+ source: {
3536
+ $case: "nodeIds",
3537
+ nodeIds: {
3538
+ nodeIds: [nodeId],
3539
+ },
3540
+ },
3541
+ includeParents: false,
3542
+ });
3543
+
3544
+ const node = response.nodes[nodeId];
3545
+ if (!node) {
3546
+ throw new ValidationError("Node not found", {
3547
+ field: "nodeId",
3548
+ value: nodeId,
3549
+ });
3550
+ }
3551
+
3552
+ // Generate signing public key for this node
3553
+ const signingPubKey = await this.config.signer.generatePublicKey(
3554
+ sha256(node.id),
3555
+ );
3556
+
3557
+ // Call the transfer service to refresh the refund timelock
3558
+ const result = await this.transferService.refreshTimelockRefundTx(
3559
+ node,
3560
+ signingPubKey,
3561
+ );
3562
+
3563
+ // Update the local leaves if this node is in our wallet
3564
+ const leafIndex = this.leaves.findIndex((leaf) => leaf.id === node.id);
3565
+ if (leafIndex !== -1 && result.nodes.length > 0) {
3566
+ const newNode = result.nodes[0];
3567
+ if (newNode) {
3568
+ this.leaves[leafIndex] = newNode;
3569
+ }
3570
+ }
3571
+ } catch (error) {
3572
+ throw new NetworkError(
3573
+ "Failed to refresh refund timelock",
3574
+ {
3575
+ method: "refresh_timelock_refund_tx",
3576
+ },
3577
+ error as Error,
3578
+ );
3579
+ }
3580
+ }
3581
+
3156
3582
  private cleanup() {
3157
3583
  if (this.claimTransfersInterval) {
3158
3584
  clearInterval(this.claimTransfersInterval);