@buildonspark/spark-sdk 0.1.39 → 0.1.41

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 (125) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/README.md +1 -1
  3. package/dist/{RequestLightningSendInput-39_zGri6.d.cts → RequestLightningSendInput-DXcLoiCe.d.cts} +10 -2
  4. package/dist/{RequestLightningSendInput-B4JdzclX.d.ts → RequestLightningSendInput-mXUWn_cp.d.ts} +10 -2
  5. package/dist/address/index.cjs +138 -6
  6. package/dist/address/index.d.cts +18 -6
  7. package/dist/address/index.d.ts +18 -6
  8. package/dist/address/index.js +5 -2
  9. package/dist/{chunk-FWQPAPXK.js → chunk-2ZXXLPG2.js} +1 -1
  10. package/dist/{chunk-S7KD6DDL.js → chunk-6YVPOQ2A.js} +41 -20
  11. package/dist/{chunk-ZUVYYR5T.js → chunk-7EFSUADA.js} +1 -0
  12. package/dist/{chunk-NS4UZRQ7.js → chunk-ABZA6R5S.js} +1 -1
  13. package/dist/{chunk-57XLH3ZR.js → chunk-ATEHMLKP.js} +23 -23
  14. package/dist/{chunk-VJTDG4BQ.js → chunk-HK6LPV6Z.js} +10 -1
  15. package/dist/{chunk-W3EC5XSA.js → chunk-J5W5Q2ZP.js} +337 -72
  16. package/dist/{chunk-TKYOYOYJ.js → chunk-KKSU7OZO.js} +653 -76
  17. package/dist/chunk-L3EHBOUX.js +0 -0
  18. package/dist/{chunk-C5LTJBI7.js → chunk-M6A4KFIG.js} +125 -226
  19. package/dist/{chunk-A74XSEW3.js → chunk-MIVX3GHD.js} +1 -1
  20. package/dist/{chunk-RGWBSZIO.js → chunk-ROKY5KS4.js} +23 -3
  21. package/dist/{chunk-LIP2K6KR.js → chunk-TM4TOEOX.js} +26 -8
  22. package/dist/{chunk-RAPBVYJY.js → chunk-UKT6OFLO.js} +125 -35
  23. package/dist/chunk-VA7MV4MZ.js +1073 -0
  24. package/dist/chunk-YEZDPUFY.js +840 -0
  25. package/dist/{chunk-DI7QXUQJ.js → chunk-ZXDE2XMU.js} +8 -5
  26. package/dist/graphql/objects/index.cjs +6 -3
  27. package/dist/graphql/objects/index.d.cts +6 -5
  28. package/dist/graphql/objects/index.d.ts +6 -5
  29. package/dist/graphql/objects/index.js +1 -1
  30. package/dist/{index-DEo_hdN3.d.cts → index-CFh4uWzi.d.cts} +60 -6
  31. package/dist/{index-BVY0yH_H.d.ts → index-OSDtPMmC.d.ts} +60 -6
  32. package/dist/index.cjs +3316 -954
  33. package/dist/index.d.cts +9 -8
  34. package/dist/index.d.ts +9 -8
  35. package/dist/index.js +48 -26
  36. package/dist/index.node.cjs +3316 -954
  37. package/dist/index.node.d.cts +9 -8
  38. package/dist/index.node.d.ts +9 -8
  39. package/dist/index.node.js +48 -26
  40. package/dist/native/index.cjs +3323 -961
  41. package/dist/native/index.d.cts +542 -260
  42. package/dist/native/index.d.ts +542 -260
  43. package/dist/native/index.js +3192 -838
  44. package/dist/{network-DobHpaV6.d.ts → network-BF2GYPye.d.ts} +9 -2
  45. package/dist/{network-GFGEHkS4.d.cts → network-BiwBmoOg.d.cts} +9 -2
  46. package/dist/proto/lrc20.d.cts +1 -1
  47. package/dist/proto/lrc20.d.ts +1 -1
  48. package/dist/proto/lrc20.js +2 -2
  49. package/dist/proto/spark.cjs +125 -226
  50. package/dist/proto/spark.d.cts +1 -1
  51. package/dist/proto/spark.d.ts +1 -1
  52. package/dist/proto/spark.js +3 -5
  53. package/dist/proto/spark_token.cjs +1364 -0
  54. package/dist/proto/spark_token.d.cts +209 -0
  55. package/dist/proto/spark_token.d.ts +209 -0
  56. package/dist/proto/spark_token.js +32 -0
  57. package/dist/{sdk-types-BuVMn2rX.d.cts → sdk-types-CfhdFnsA.d.cts} +1 -1
  58. package/dist/{sdk-types-BeI6DM_M.d.ts → sdk-types-MnQrHolg.d.ts} +1 -1
  59. package/dist/services/config.cjs +64 -40
  60. package/dist/services/config.d.cts +6 -5
  61. package/dist/services/config.d.ts +6 -5
  62. package/dist/services/config.js +7 -7
  63. package/dist/services/connection.cjs +1108 -306
  64. package/dist/services/connection.d.cts +10 -5
  65. package/dist/services/connection.d.ts +10 -5
  66. package/dist/services/connection.js +3 -2
  67. package/dist/services/index.cjs +1702 -488
  68. package/dist/services/index.d.cts +6 -5
  69. package/dist/services/index.d.ts +6 -5
  70. package/dist/services/index.js +16 -14
  71. package/dist/services/lrc-connection.d.cts +5 -5
  72. package/dist/services/lrc-connection.d.ts +5 -5
  73. package/dist/services/lrc-connection.js +3 -3
  74. package/dist/services/token-transactions.cjs +637 -247
  75. package/dist/services/token-transactions.d.cts +19 -8
  76. package/dist/services/token-transactions.d.ts +19 -8
  77. package/dist/services/token-transactions.js +5 -4
  78. package/dist/services/wallet-config.cjs +1 -0
  79. package/dist/services/wallet-config.d.cts +6 -5
  80. package/dist/services/wallet-config.d.ts +6 -5
  81. package/dist/services/wallet-config.js +1 -1
  82. package/dist/signer/signer.cjs +122 -35
  83. package/dist/signer/signer.d.cts +4 -3
  84. package/dist/signer/signer.d.ts +4 -3
  85. package/dist/signer/signer.js +8 -4
  86. package/dist/{signer-C1t40Wus.d.cts → signer-BhLS7SYR.d.cts} +35 -14
  87. package/dist/{signer-DFGw9RRp.d.ts → signer-CylxIujU.d.ts} +35 -14
  88. package/dist/{spark-DXYE9gMM.d.ts → spark-DjR1b3TC.d.cts} +13 -21
  89. package/dist/{spark-DXYE9gMM.d.cts → spark-DjR1b3TC.d.ts} +13 -21
  90. package/dist/types/index.cjs +130 -227
  91. package/dist/types/index.d.cts +6 -5
  92. package/dist/types/index.d.ts +6 -5
  93. package/dist/types/index.js +3 -3
  94. package/dist/utils/index.cjs +1169 -3
  95. package/dist/utils/index.d.cts +66 -6
  96. package/dist/utils/index.d.ts +66 -6
  97. package/dist/utils/index.js +35 -14
  98. package/package.json +6 -2
  99. package/src/address/address.ts +41 -6
  100. package/src/graphql/client.ts +15 -0
  101. package/src/graphql/objects/Transfer.ts +7 -0
  102. package/src/graphql/queries/Transfer.ts +10 -0
  103. package/src/proto/spark.ts +215 -337
  104. package/src/proto/spark_token.ts +1407 -0
  105. package/src/services/config.ts +4 -0
  106. package/src/services/connection.ts +37 -1
  107. package/src/services/deposit.ts +23 -5
  108. package/src/services/token-transactions.ts +426 -75
  109. package/src/services/transfer.ts +182 -11
  110. package/src/services/tree-creation.ts +29 -14
  111. package/src/services/wallet-config.ts +2 -0
  112. package/src/signer/signer.ts +190 -48
  113. package/src/spark-wallet/spark-wallet.ts +510 -6
  114. package/src/tests/integration/transfer.test.ts +186 -214
  115. package/src/tests/integration/tree-creation.test.ts +5 -1
  116. package/src/tests/signer.test.ts +34 -0
  117. package/src/tests/transaction.test.ts +12 -0
  118. package/src/tests/xchain-address.test.ts +28 -0
  119. package/src/utils/index.ts +2 -0
  120. package/src/utils/mempool.ts +26 -1
  121. package/src/utils/network.ts +15 -0
  122. package/src/utils/transaction.ts +51 -3
  123. package/src/utils/unilateral-exit.ts +729 -0
  124. package/src/utils/xchain-address.ts +36 -0
  125. package/dist/chunk-E5SL7XTO.js +0 -301
@@ -4,6 +4,7 @@ import {
4
4
  bytesToNumberBE,
5
5
  equalBytes,
6
6
  hexToBytes,
7
+ numberToVarBytesBE,
7
8
  } from "@noble/curves/abstract/utils";
8
9
  import { secp256k1 } from "@noble/curves/secp256k1";
9
10
  import { validateMnemonic } from "@scure/bip39";
@@ -11,7 +12,7 @@ import { wordlist } from "@scure/bip39/wordlists/english";
11
12
  import { Address, OutScript, Transaction } from "@scure/btc-signer";
12
13
  import { TransactionInput } from "@scure/btc-signer/psbt";
13
14
  import { Mutex } from "async-mutex";
14
- import { uuidv7 } from "uuidv7";
15
+ import { uuidv7, uuidv7obj } from "uuidv7";
15
16
  import {
16
17
  ConfigurationError,
17
18
  NetworkError,
@@ -34,6 +35,7 @@ import {
34
35
  SwapLeaf,
35
36
  UserLeafInput,
36
37
  } from "../graphql/objects/index.js";
38
+ import GraphQLTransferObj from "../graphql/objects/Transfer.js";
37
39
  import {
38
40
  DepositAddressQueryResult,
39
41
  OutputWithPreviousTransactionData,
@@ -72,6 +74,7 @@ import {
72
74
  } from "../utils/adaptor-signature.js";
73
75
  import {
74
76
  computeTaprootKeyNoScript,
77
+ getP2TRScriptFromPublicKey,
75
78
  getP2WPKHAddressFromPublicKey,
76
79
  getSigHashFromTx,
77
80
  getTxFromRawTxBytes,
@@ -95,6 +98,7 @@ import { EventEmitter } from "eventemitter3";
95
98
  import {
96
99
  decodeSparkAddress,
97
100
  encodeSparkAddress,
101
+ isValidPublicKey,
98
102
  SparkAddressFormat,
99
103
  } from "../address/index.js";
100
104
  import { isReactNative } from "../constants.js";
@@ -430,9 +434,7 @@ export class SparkWallet extends EventEmitter {
430
434
  }
431
435
  }
432
436
 
433
- private async getLeaves(
434
- isBalanceCheck: boolean = false,
435
- ): Promise<TreeNode[]> {
437
+ public async getLeaves(isBalanceCheck: boolean = false): Promise<TreeNode[]> {
436
438
  const leaves = await this.queryNodes({
437
439
  source: {
438
440
  $case: "ownerIdentityPubkey",
@@ -687,6 +689,56 @@ export class SparkWallet extends EventEmitter {
687
689
  return this.sparkAddress;
688
690
  }
689
691
 
692
+ public async createSparkPaymentIntent(
693
+ assetIdentifier?: string,
694
+ assetAmount: bigint = BigInt(0),
695
+ memo?: string,
696
+ ): Promise<SparkAddressFormat> {
697
+ const MAX_UINT128 = BigInt("340282366920938463463374607431768211455");
698
+ if (assetAmount < 0 || assetAmount > MAX_UINT128) {
699
+ throw new ValidationError(
700
+ "Asset amount must be between 0 and MAX_UINT128",
701
+ {
702
+ field: "assetAmount",
703
+ value: assetAmount,
704
+ },
705
+ );
706
+ }
707
+ if (memo) {
708
+ const encoder = new TextEncoder();
709
+ const memoBytes = encoder.encode(memo);
710
+ if (memoBytes.length > 120) {
711
+ throw new ValidationError(
712
+ "Memo exceeds the maximum allowed byte length of 120.",
713
+ {
714
+ field: "memo",
715
+ value: memo,
716
+ expected: "less than 120 bytes",
717
+ },
718
+ );
719
+ }
720
+ }
721
+ if (assetIdentifier) {
722
+ isValidPublicKey(assetIdentifier);
723
+ }
724
+ const paymentRequest = encodeSparkAddress({
725
+ identityPublicKey: bytesToHex(
726
+ await this.config.signer.getIdentityPublicKey(),
727
+ ),
728
+ network: this.config.getNetworkType(),
729
+ paymentIntentFields: {
730
+ id: uuidv7obj().bytes,
731
+ assetIdentifier: assetIdentifier
732
+ ? hexToBytes(assetIdentifier)
733
+ : undefined,
734
+ assetAmount: numberToVarBytesBE(assetAmount),
735
+ memo: memo,
736
+ },
737
+ });
738
+
739
+ return paymentRequest;
740
+ }
741
+
690
742
  /**
691
743
  * Initializes the wallet using either a mnemonic phrase or a raw seed.
692
744
  * initWallet will also claim any pending incoming lightning payment, spark transfer,
@@ -1967,7 +2019,7 @@ export class SparkWallet extends EventEmitter {
1967
2019
 
1968
2020
  const isSelfTransfer = equalBytes(
1969
2021
  signerIdentityPublicKey,
1970
- hexToBytes(receiverAddress),
2022
+ hexToBytes(receiverAddress.identityPublicKey),
1971
2023
  );
1972
2024
 
1973
2025
  return await this.withLeaves(async () => {
@@ -1988,7 +2040,7 @@ export class SparkWallet extends EventEmitter {
1988
2040
 
1989
2041
  const transfer = await this.transferService.sendTransferWithKeyTweaks(
1990
2042
  leafKeyTweaks,
1991
- hexToBytes(receiverAddress),
2043
+ hexToBytes(receiverAddress.identityPublicKey),
1992
2044
  );
1993
2045
 
1994
2046
  const leavesToRemove = new Set(leavesToSend.map((leaf) => leaf.id));
@@ -2896,6 +2948,31 @@ export class SparkWallet extends EventEmitter {
2896
2948
  return feeEstimate;
2897
2949
  }
2898
2950
 
2951
+ /**
2952
+ * Gets a transfer that has been sent by the SSP to the wallet.
2953
+ *
2954
+ * @param {string} id - The ID of the transfer
2955
+ * @returns {Promise<GraphQLTransferObj | null>} The transfer
2956
+ */
2957
+ public async getTransferFromSsp(
2958
+ id: string,
2959
+ ): Promise<GraphQLTransferObj | null> {
2960
+ const sspClient = this.getSspClient();
2961
+ return await sspClient.getTransfer(id);
2962
+ }
2963
+
2964
+ /**
2965
+ * Gets a transfer, that the wallet is a participant of, in the Spark network.
2966
+ * Only contains data about the spark->spark transfer, use getTransferFromSsp if you're
2967
+ * looking for information related to a lightning transfer.
2968
+ *
2969
+ * @param {string} id - The ID of the transfer
2970
+ * @returns {Promise<Transfer | undefined>} The transfer
2971
+ */
2972
+ public async getTransfer(id: string): Promise<Transfer | undefined> {
2973
+ return await this.transferService.queryTransfer(id);
2974
+ }
2975
+
2899
2976
  // ***** Token Flow *****
2900
2977
 
2901
2978
  /**
@@ -3116,6 +3193,148 @@ export class SparkWallet extends EventEmitter {
3116
3193
  return this.config.signer.validateMessageWithIdentityKey(hash, signature);
3117
3194
  }
3118
3195
 
3196
+ /**
3197
+ * Signs a transaction with wallet keys.
3198
+ *
3199
+ * @param {string} txHex - The transaction hex to sign
3200
+ * @param {string} keyType - The type of key to use for signing ("identity", "deposit", or "auto-detect")
3201
+ * @returns {Promise<string>} The signed transaction hex
3202
+ */
3203
+ public async signTransaction(
3204
+ txHex: string,
3205
+ keyType: string = "auto-detect",
3206
+ ): Promise<string> {
3207
+ try {
3208
+ // Parse the transaction
3209
+ const tx = Transaction.fromRaw(hexToBytes(txHex));
3210
+
3211
+ let publicKey: Uint8Array;
3212
+
3213
+ switch (keyType.toLowerCase()) {
3214
+ case "identity":
3215
+ publicKey = await this.config.signer.getIdentityPublicKey();
3216
+ break;
3217
+ case "deposit":
3218
+ publicKey = await this.config.signer.getDepositSigningKey();
3219
+ break;
3220
+ case "auto-detect":
3221
+ default:
3222
+ // Try to auto-detect which key to use by examining the transaction inputs
3223
+ const detectedKey = await this.detectKeyForTransaction(tx);
3224
+ if (detectedKey) {
3225
+ publicKey = detectedKey.publicKey;
3226
+ } else {
3227
+ // Fallback to identity key
3228
+ publicKey = await this.config.signer.getIdentityPublicKey();
3229
+ }
3230
+ break;
3231
+ }
3232
+
3233
+ // Check each input to determine which ones need signing
3234
+ let inputsSigned = 0;
3235
+ for (let i = 0; i < tx.inputsLength; i++) {
3236
+ const input = tx.getInput(i);
3237
+ if (!input?.witnessUtxo?.script) {
3238
+ continue;
3239
+ }
3240
+
3241
+ const script = input.witnessUtxo.script;
3242
+
3243
+ // Check if this is an ephemeral anchor (OP_TRUE script)
3244
+ // OP_TRUE is represented as a single byte: 0x51
3245
+ if (script.length === 1 && script[0] === 0x51) {
3246
+ continue;
3247
+ }
3248
+
3249
+ // Check if this script matches one of our keys
3250
+ const identityScript = getP2TRScriptFromPublicKey(
3251
+ publicKey,
3252
+ this.config.getNetwork(),
3253
+ );
3254
+
3255
+ if (bytesToHex(script) === bytesToHex(identityScript)) {
3256
+ // Sign this specific input
3257
+ try {
3258
+ this.config.signer.signTransactionIndex(tx, i, publicKey);
3259
+ inputsSigned++;
3260
+ } catch (error) {
3261
+ throw new ValidationError(`Failed to sign input ${i}: ${error}`, {
3262
+ field: "input",
3263
+ value: i,
3264
+ });
3265
+ }
3266
+ }
3267
+ }
3268
+
3269
+ if (inputsSigned === 0) {
3270
+ throw new Error(
3271
+ "No inputs were signed. Check that the transaction contains inputs controlled by this wallet.",
3272
+ );
3273
+ }
3274
+
3275
+ tx.finalize();
3276
+
3277
+ const signedTxHex = tx.hex;
3278
+
3279
+ return signedTxHex;
3280
+ } catch (error) {
3281
+ console.error("❌ Error signing transaction:", error);
3282
+ throw error;
3283
+ }
3284
+ }
3285
+
3286
+ /**
3287
+ * Helper method to auto-detect which key should be used for signing a transaction.
3288
+ */
3289
+ private async detectKeyForTransaction(tx: Transaction): Promise<{
3290
+ publicKey: Uint8Array;
3291
+ keyType: string;
3292
+ } | null> {
3293
+ try {
3294
+ // Get available keys
3295
+ const identityPubKey = await this.config.signer.getIdentityPublicKey();
3296
+ const depositPubKey = await this.config.signer.getDepositSigningKey();
3297
+
3298
+ // Check if any inputs reference outputs that would be controlled by our keys
3299
+ for (let i = 0; i < tx.inputsLength; i++) {
3300
+ const input = tx.getInput(i);
3301
+ if (input?.witnessUtxo?.script) {
3302
+ const script = input.witnessUtxo.script;
3303
+
3304
+ // Check if this script corresponds to one of our keys
3305
+ // This is a simplified check - in practice, you might need more sophisticated script analysis
3306
+ const identityScript = getP2TRScriptFromPublicKey(
3307
+ identityPubKey,
3308
+ this.config.getNetwork(),
3309
+ );
3310
+ const depositScript = getP2TRScriptFromPublicKey(
3311
+ depositPubKey,
3312
+ this.config.getNetwork(),
3313
+ );
3314
+
3315
+ if (bytesToHex(script) === bytesToHex(identityScript)) {
3316
+ return {
3317
+ publicKey: identityPubKey,
3318
+ keyType: "identity",
3319
+ };
3320
+ }
3321
+
3322
+ if (bytesToHex(script) === bytesToHex(depositScript)) {
3323
+ return {
3324
+ publicKey: depositPubKey,
3325
+ keyType: "deposit",
3326
+ };
3327
+ }
3328
+ }
3329
+ }
3330
+
3331
+ return null;
3332
+ } catch (error) {
3333
+ console.warn("Error during key auto-detection:", error);
3334
+ return null;
3335
+ }
3336
+ }
3337
+
3119
3338
  /**
3120
3339
  * Get a Lightning receive request by ID.
3121
3340
  *
@@ -3153,6 +3372,291 @@ export class SparkWallet extends EventEmitter {
3153
3372
  return await sspClient.getCoopExitRequest(id);
3154
3373
  }
3155
3374
 
3375
+ /**
3376
+ * Check the remaining timelock on a given node.
3377
+ *
3378
+ * @param {string} nodeId - The ID of the node to check
3379
+ * @returns {Promise<{nodeTimelock: number, refundTimelock: number}>} The remaining timelocks in blocks for both node and refund transactions
3380
+ */
3381
+ public async checkTimelock(nodeId: string): Promise<{
3382
+ nodeTimelock: number;
3383
+ refundTimelock: number;
3384
+ }> {
3385
+ const sparkClient = await this.connectionManager.createSparkClient(
3386
+ this.config.getCoordinatorAddress(),
3387
+ );
3388
+
3389
+ try {
3390
+ const response = await sparkClient.query_nodes({
3391
+ source: {
3392
+ $case: "nodeIds",
3393
+ nodeIds: {
3394
+ nodeIds: [nodeId],
3395
+ },
3396
+ },
3397
+ includeParents: false,
3398
+ network: NetworkToProto[this.config.getNetwork()],
3399
+ });
3400
+
3401
+ const node = response.nodes[nodeId];
3402
+ if (!node) {
3403
+ throw new ValidationError("Node not found", {
3404
+ field: "nodeId",
3405
+ value: nodeId,
3406
+ });
3407
+ }
3408
+
3409
+ // Check if this is a root node (no parent)
3410
+ const isRootNode = !node.parentNodeId;
3411
+
3412
+ // Validate transaction data exists
3413
+ if (!node.nodeTx || node.nodeTx.length === 0) {
3414
+ throw new ValidationError(
3415
+ `Node transaction data is missing or empty for ${isRootNode ? "root" : "non-root"} node`,
3416
+ {
3417
+ field: "nodeTx",
3418
+ value: node.nodeTx?.length || 0,
3419
+ },
3420
+ );
3421
+ }
3422
+
3423
+ if (!node.refundTx || node.refundTx.length === 0) {
3424
+ throw new ValidationError(
3425
+ `Refund transaction data is missing or empty for ${isRootNode ? "root" : "non-root"} node`,
3426
+ {
3427
+ field: "refundTx",
3428
+ value: node.refundTx?.length || 0,
3429
+ },
3430
+ );
3431
+ }
3432
+
3433
+ let nodeTx, refundTx;
3434
+
3435
+ try {
3436
+ // Get the node transaction to check its timelock
3437
+ nodeTx = getTxFromRawTxBytes(node.nodeTx);
3438
+ } catch (error) {
3439
+ throw new ValidationError(
3440
+ `Failed to parse node transaction for ${isRootNode ? "root" : "non-root"} node: ${error instanceof Error ? error.message : String(error)}`,
3441
+ {
3442
+ field: "nodeTx",
3443
+ value: node.nodeTx.length,
3444
+ },
3445
+ );
3446
+ }
3447
+
3448
+ try {
3449
+ // Get the refund transaction to check its timelock
3450
+ refundTx = getTxFromRawTxBytes(node.refundTx);
3451
+ } catch (error) {
3452
+ throw new ValidationError(
3453
+ `Failed to parse refund transaction for ${isRootNode ? "root" : "non-root"} node: ${error instanceof Error ? error.message : String(error)}`,
3454
+ {
3455
+ field: "refundTx",
3456
+ value: node.refundTx.length,
3457
+ },
3458
+ );
3459
+ }
3460
+
3461
+ const nodeInput = nodeTx.getInput(0);
3462
+ if (!nodeInput) {
3463
+ throw new ValidationError(
3464
+ `Node transaction has no inputs for ${isRootNode ? "root" : "non-root"} node`,
3465
+ {
3466
+ field: "nodeInput",
3467
+ value: nodeTx.inputsLength,
3468
+ },
3469
+ );
3470
+ }
3471
+
3472
+ if (!nodeInput.sequence) {
3473
+ throw new ValidationError(
3474
+ `Node transaction has no sequence for ${isRootNode ? "root" : "non-root"} node`,
3475
+ {
3476
+ field: "sequence",
3477
+ value: nodeInput.sequence,
3478
+ },
3479
+ );
3480
+ }
3481
+
3482
+ const refundInput = refundTx.getInput(0);
3483
+ if (!refundInput) {
3484
+ throw new ValidationError(
3485
+ `Refund transaction has no inputs for ${isRootNode ? "root" : "non-root"} node`,
3486
+ {
3487
+ field: "refundInput",
3488
+ value: refundTx.inputsLength,
3489
+ },
3490
+ );
3491
+ }
3492
+
3493
+ if (!refundInput.sequence) {
3494
+ throw new ValidationError(
3495
+ `Refund transaction has no sequence for ${isRootNode ? "root" : "non-root"} node`,
3496
+ {
3497
+ field: "sequence",
3498
+ value: refundInput.sequence,
3499
+ },
3500
+ );
3501
+ }
3502
+
3503
+ // Extract timelock from sequence (lower 16 bits)
3504
+ const nodeTimelock = nodeInput.sequence & 0xffff;
3505
+ const refundTimelock = refundInput.sequence & 0xffff;
3506
+
3507
+ return {
3508
+ nodeTimelock,
3509
+ refundTimelock,
3510
+ };
3511
+ } catch (error) {
3512
+ throw new NetworkError(
3513
+ `Failed to check timelock for node ${nodeId}`,
3514
+ {
3515
+ method: "query_nodes",
3516
+ },
3517
+ error as Error,
3518
+ );
3519
+ }
3520
+ }
3521
+
3522
+ /**
3523
+ * Refresh the timelock of a specific node.
3524
+ *
3525
+ * @param {string} nodeId - The ID of the node to refresh
3526
+ * @returns {Promise<void>} Promise that resolves when the timelock is refreshed
3527
+ */
3528
+ public async testOnly_expireTimelock(nodeId: string): Promise<void> {
3529
+ const sparkClient = await this.connectionManager.createSparkClient(
3530
+ this.config.getCoordinatorAddress(),
3531
+ );
3532
+
3533
+ try {
3534
+ // First, get the node and its parent
3535
+ const response = await sparkClient.query_nodes({
3536
+ source: {
3537
+ $case: "nodeIds",
3538
+ nodeIds: {
3539
+ nodeIds: [nodeId],
3540
+ },
3541
+ },
3542
+ includeParents: true,
3543
+ });
3544
+
3545
+ const node = response.nodes[nodeId];
3546
+ if (!node) {
3547
+ throw new ValidationError("Node not found", {
3548
+ field: "nodeId",
3549
+ value: nodeId,
3550
+ });
3551
+ }
3552
+
3553
+ if (!node.parentNodeId) {
3554
+ throw new ValidationError("Node has no parent", {
3555
+ field: "parentNodeId",
3556
+ value: node.parentNodeId,
3557
+ });
3558
+ }
3559
+
3560
+ const parentNode = response.nodes[node.parentNodeId];
3561
+ if (!parentNode) {
3562
+ throw new ValidationError("Parent node not found", {
3563
+ field: "parentNodeId",
3564
+ value: node.parentNodeId,
3565
+ });
3566
+ }
3567
+
3568
+ // Generate signing public key for this node
3569
+ const signingPubKey = await this.config.signer.generatePublicKey(
3570
+ sha256(node.id),
3571
+ );
3572
+
3573
+ // Call the transfer service to refresh the timelock
3574
+ const result = await this.transferService.refreshTimelockNodes(
3575
+ [node],
3576
+ parentNode,
3577
+ signingPubKey,
3578
+ );
3579
+
3580
+ // Update the local leaves if this node is in our wallet
3581
+ const leafIndex = this.leaves.findIndex((leaf) => leaf.id === node.id);
3582
+ if (leafIndex !== -1 && result.nodes.length > 0) {
3583
+ const newNode = result.nodes[0];
3584
+ if (newNode) {
3585
+ this.leaves[leafIndex] = newNode;
3586
+ }
3587
+ }
3588
+ } catch (error) {
3589
+ throw new NetworkError(
3590
+ "Failed to refresh timelock",
3591
+ {
3592
+ method: "refresh_timelock",
3593
+ },
3594
+ error as Error,
3595
+ );
3596
+ }
3597
+ }
3598
+
3599
+ /**
3600
+ * Refresh the timelock of a specific node's refund transaction only.
3601
+ *
3602
+ * @param {string} nodeId - The ID of the node whose refund transaction to refresh
3603
+ * @returns {Promise<void>} Promise that resolves when the refund timelock is refreshed
3604
+ */
3605
+ public async testOnly_expireTimelockRefundTx(nodeId: string): Promise<void> {
3606
+ const sparkClient = await this.connectionManager.createSparkClient(
3607
+ this.config.getCoordinatorAddress(),
3608
+ );
3609
+
3610
+ try {
3611
+ // Get the node
3612
+ const response = await sparkClient.query_nodes({
3613
+ source: {
3614
+ $case: "nodeIds",
3615
+ nodeIds: {
3616
+ nodeIds: [nodeId],
3617
+ },
3618
+ },
3619
+ includeParents: false,
3620
+ });
3621
+
3622
+ const node = response.nodes[nodeId];
3623
+ if (!node) {
3624
+ throw new ValidationError("Node not found", {
3625
+ field: "nodeId",
3626
+ value: nodeId,
3627
+ });
3628
+ }
3629
+
3630
+ // Generate signing public key for this node
3631
+ const signingPubKey = await this.config.signer.generatePublicKey(
3632
+ sha256(node.id),
3633
+ );
3634
+
3635
+ // Call the transfer service to refresh the refund timelock
3636
+ const result = await this.transferService.refreshTimelockRefundTx(
3637
+ node,
3638
+ signingPubKey,
3639
+ );
3640
+
3641
+ // Update the local leaves if this node is in our wallet
3642
+ const leafIndex = this.leaves.findIndex((leaf) => leaf.id === node.id);
3643
+ if (leafIndex !== -1 && result.nodes.length > 0) {
3644
+ const newNode = result.nodes[0];
3645
+ if (newNode) {
3646
+ this.leaves[leafIndex] = newNode;
3647
+ }
3648
+ }
3649
+ } catch (error) {
3650
+ throw new NetworkError(
3651
+ "Failed to refresh refund timelock",
3652
+ {
3653
+ method: "refresh_timelock_refund_tx",
3654
+ },
3655
+ error as Error,
3656
+ );
3657
+ }
3658
+ }
3659
+
3156
3660
  private cleanup() {
3157
3661
  if (this.claimTransfersInterval) {
3158
3662
  clearInterval(this.claimTransfersInterval);