@buildonspark/spark-sdk 0.2.3 → 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 (73) hide show
  1. package/CHANGELOG.md +8 -0
  2. package/dist/{chunk-PTRXJS7Q.js → chunk-TVUMSHWA.js} +1 -1
  3. package/dist/{chunk-PLLJIZC3.js → chunk-W4ZRBSWM.js} +2298 -778
  4. package/dist/{chunk-CDLETEDT.js → chunk-WAQKYSDI.js} +13 -1
  5. package/dist/{client-CGTRS23n.d.ts → client-BF4cn8F4.d.ts} +15 -3
  6. package/dist/{client-CcYzmpmj.d.cts → client-KhNkrXz4.d.cts} +15 -3
  7. package/dist/debug.cjs +2282 -762
  8. package/dist/debug.d.cts +17 -4
  9. package/dist/debug.d.ts +17 -4
  10. package/dist/debug.js +2 -2
  11. package/dist/graphql/objects/index.cjs +13 -1
  12. package/dist/graphql/objects/index.d.cts +2 -2
  13. package/dist/graphql/objects/index.d.ts +2 -2
  14. package/dist/graphql/objects/index.js +1 -1
  15. package/dist/index.cjs +2283 -752
  16. package/dist/index.d.cts +189 -8
  17. package/dist/index.d.ts +189 -8
  18. package/dist/index.js +29 -3
  19. package/dist/index.node.cjs +2387 -753
  20. package/dist/index.node.d.cts +9 -189
  21. package/dist/index.node.d.ts +9 -189
  22. package/dist/index.node.js +131 -3
  23. package/dist/native/index.cjs +2283 -752
  24. package/dist/native/index.d.cts +95 -30
  25. package/dist/native/index.d.ts +95 -30
  26. package/dist/native/index.js +2284 -767
  27. package/dist/{spark-wallet-CxcGPXRB.d.ts → spark-wallet-C1Tr_VKI.d.ts} +31 -25
  28. package/dist/{spark-wallet-DJJm19BP.d.cts → spark-wallet-DG3x2obf.d.cts} +31 -25
  29. package/dist/spark-wallet.node-CGxoeCpH.d.ts +13 -0
  30. package/dist/spark-wallet.node-CN9LoB_O.d.cts +13 -0
  31. package/dist/tests/test-utils.cjs +570 -73
  32. package/dist/tests/test-utils.d.cts +11 -11
  33. package/dist/tests/test-utils.d.ts +11 -11
  34. package/dist/tests/test-utils.js +53 -16
  35. package/dist/types/index.cjs +13 -1
  36. package/dist/types/index.d.cts +1 -1
  37. package/dist/types/index.d.ts +1 -1
  38. package/dist/types/index.js +1 -1
  39. package/dist/{xchain-address-Bh9w1SeC.d.ts → xchain-address-BHu6CpZC.d.ts} +54 -7
  40. package/dist/{xchain-address-SZ7dkVUE.d.cts → xchain-address-HBr6isnc.d.cts} +54 -7
  41. package/package.json +1 -1
  42. package/src/graphql/client.ts +8 -0
  43. package/src/graphql/mutations/CompleteLeavesSwap.ts +9 -1
  44. package/src/graphql/mutations/RequestSwapLeaves.ts +4 -0
  45. package/src/graphql/objects/CompleteLeavesSwapInput.ts +34 -34
  46. package/src/graphql/objects/LeavesSwapRequest.ts +4 -0
  47. package/src/graphql/objects/RequestLeavesSwapInput.ts +48 -47
  48. package/src/graphql/objects/SwapLeaf.ts +40 -32
  49. package/src/graphql/objects/UserLeafInput.ts +24 -0
  50. package/src/graphql/objects/UserRequest.ts +4 -0
  51. package/src/index.node.ts +1 -1
  52. package/src/native/index.ts +4 -5
  53. package/src/services/coop-exit.ts +171 -36
  54. package/src/services/deposit.ts +471 -74
  55. package/src/services/lightning.ts +18 -5
  56. package/src/services/signing.ts +162 -50
  57. package/src/services/transfer.ts +950 -384
  58. package/src/services/tree-creation.ts +342 -121
  59. package/src/spark-wallet/spark-wallet.node.ts +71 -66
  60. package/src/spark-wallet/spark-wallet.ts +405 -153
  61. package/src/tests/integration/coop-exit.test.ts +3 -8
  62. package/src/tests/integration/deposit.test.ts +3 -3
  63. package/src/tests/integration/lightning.test.ts +521 -466
  64. package/src/tests/integration/swap.test.ts +559 -307
  65. package/src/tests/integration/transfer.test.ts +625 -623
  66. package/src/tests/integration/wallet.test.ts +2 -2
  67. package/src/tests/integration/watchtower.test.ts +211 -0
  68. package/src/tests/test-utils.ts +63 -14
  69. package/src/tests/utils/test-faucet.ts +4 -2
  70. package/src/utils/adaptor-signature.ts +15 -5
  71. package/src/utils/fetch.ts +75 -0
  72. package/src/utils/mempool.ts +9 -4
  73. package/src/utils/transaction.ts +388 -26
@@ -7,7 +7,7 @@ import {
7
7
  import { secp256k1 } from "@noble/curves/secp256k1";
8
8
  import { sha256 } from "@noble/hashes/sha2";
9
9
  import { Transaction } from "@scure/btc-signer";
10
- import { TransactionInput } from "@scure/btc-signer/psbt";
10
+ import { TransactionInput, TransactionOutput } from "@scure/btc-signer/psbt";
11
11
  import * as ecies from "eciesjs";
12
12
  import { uuidv7 } from "uuidv7";
13
13
  import {
@@ -49,24 +49,26 @@ import {
49
49
  import { NetworkToProto } from "../utils/network.js";
50
50
  import { VerifiableSecretShare } from "../utils/secret-sharing.js";
51
51
  import {
52
- createRefundTx,
52
+ createNodeTxs,
53
+ createRefundTxs,
54
+ DIRECT_TIMELOCK_OFFSET,
55
+ getCurrentTimelock,
53
56
  getEphemeralAnchorOutput,
54
57
  getNextTransactionSequence,
55
58
  getTransactionSequence,
59
+ INITIAL_DIRECT_SEQUENCE,
60
+ INITIAL_SEQUENCE,
61
+ maybeApplyFee,
62
+ TEST_UNILATERAL_DIRECT_SEQUENCE,
63
+ TEST_UNILATERAL_SEQUENCE,
56
64
  } from "../utils/transaction.js";
57
65
  import { getTransferPackageSigningPayload } from "../utils/transfer_package.js";
58
66
  import { WalletConfigService } from "./config.js";
59
67
  import { ConnectionManager } from "./connection.js";
60
68
  import { SigningService } from "./signing.js";
61
69
  import { SigningOperator } from "./wallet-config.js";
62
- const INITIAL_TIME_LOCK = 2000;
63
-
64
70
  const DEFAULT_EXPIRY_TIME = 10 * 60 * 1000;
65
71
 
66
- function initialSequence() {
67
- return (1 << 30) | INITIAL_TIME_LOCK;
68
- }
69
-
70
72
  export type LeafKeyTweak = {
71
73
  leaf: TreeNode;
72
74
  keyDerivation: KeyDerivation;
@@ -84,9 +86,14 @@ export type ClaimLeafData = {
84
86
  export type LeafRefundSigningData = {
85
87
  keyDerivation: KeyDerivation;
86
88
  receivingPubkey: Uint8Array;
89
+ signingNonceCommitment: SigningCommitmentWithOptionalNonce;
90
+ directSigningNonceCommitment: SigningCommitmentWithOptionalNonce;
87
91
  tx: Transaction;
92
+ directTx?: Transaction;
88
93
  refundTx?: Transaction;
89
- signingNonceCommitment: SigningCommitmentWithOptionalNonce;
94
+ directRefundTx?: Transaction;
95
+ directFromCpfpRefundTx?: Transaction;
96
+ directFromCpfpRefundSigningNonceCommitment: SigningCommitmentWithOptionalNonce;
90
97
  vout: number;
91
98
  };
92
99
 
@@ -124,13 +131,17 @@ export class BaseTransferService {
124
131
  async sendTransferTweakKey(
125
132
  transfer: Transfer,
126
133
  leaves: LeafKeyTweak[],
127
- refundSignatureMap: Map<string, Uint8Array>,
134
+ cpfpRefundSignatureMap: Map<string, Uint8Array>,
135
+ directRefundSignatureMap: Map<string, Uint8Array>,
136
+ directFromCpfpRefundSignatureMap: Map<string, Uint8Array>,
128
137
  ): Promise<Transfer> {
129
138
  const keyTweakInputMap = await this.prepareSendTransferKeyTweaks(
130
139
  transfer.id,
131
140
  transfer.receiverIdentityPublicKey,
132
141
  leaves,
133
- refundSignatureMap,
142
+ cpfpRefundSignatureMap,
143
+ directRefundSignatureMap,
144
+ directFromCpfpRefundSignatureMap,
134
145
  );
135
146
 
136
147
  let updatedTransfer: Transfer | undefined;
@@ -179,15 +190,31 @@ export class BaseTransferService {
179
190
  async deliverTransferPackage(
180
191
  transfer: Transfer,
181
192
  leaves: LeafKeyTweak[],
182
- refundSignatureMap: Map<string, Uint8Array>,
193
+ cpfpRefundSignatureMap: Map<string, Uint8Array>,
194
+ directRefundSignatureMap: Map<string, Uint8Array>,
195
+ directFromCpfpRefundSignatureMap: Map<string, Uint8Array>,
183
196
  ): Promise<Transfer> {
184
197
  const keyTweakInputMap = await this.prepareSendTransferKeyTweaks(
185
198
  transfer.id,
186
199
  transfer.receiverIdentityPublicKey,
187
200
  leaves,
188
- refundSignatureMap,
201
+ cpfpRefundSignatureMap,
202
+ directRefundSignatureMap,
203
+ directFromCpfpRefundSignatureMap,
189
204
  );
190
205
 
206
+ for (const [key, operator] of Object.entries(
207
+ this.config.getSigningOperators(),
208
+ )) {
209
+ const tweaks = keyTweakInputMap.get(key);
210
+ if (!tweaks) {
211
+ throw new ValidationError("No tweaks for operator", {
212
+ field: "operator",
213
+ value: key,
214
+ });
215
+ }
216
+ }
217
+
191
218
  const transferPackage = await this.prepareTransferPackage(
192
219
  transfer.id,
193
220
  keyTweakInputMap,
@@ -223,6 +250,8 @@ export class BaseTransferService {
223
250
  receiverIdentityPubkey,
224
251
  leaves,
225
252
  new Map<string, Uint8Array>(),
253
+ new Map<string, Uint8Array>(),
254
+ new Map<string, Uint8Array>(),
226
255
  );
227
256
 
228
257
  const transferPackage = await this.prepareTransferPackage(
@@ -239,7 +268,7 @@ export class BaseTransferService {
239
268
  let response: StartTransferResponse;
240
269
 
241
270
  try {
242
- response = await sparkClient.start_transfer({
271
+ response = await sparkClient.start_transfer_v2({
243
272
  transferId: transferID,
244
273
  ownerIdentityPublicKey: await this.config.signer.getIdentityPublicKey(),
245
274
  receiverIdentityPublicKey: receiverIdentityPubkey,
@@ -277,15 +306,24 @@ export class BaseTransferService {
277
306
  for (const leaf of leaves) {
278
307
  nodes.push(leaf.leaf.id);
279
308
  }
280
-
281
309
  const signingCommitments = await sparkClient.get_signing_commitments({
282
310
  nodeIds: nodes,
311
+ count: 3,
283
312
  });
284
313
 
285
- const leafSigningJobs = await this.signingService.signRefunds(
314
+ const {
315
+ cpfpLeafSigningJobs,
316
+ directLeafSigningJobs,
317
+ directFromCpfpLeafSigningJobs,
318
+ } = await this.signingService.signRefunds(
286
319
  leaves,
287
- signingCommitments.signingCommitments,
288
320
  receiverIdentityPubkey,
321
+ signingCommitments.signingCommitments.slice(0, leaves.length),
322
+ signingCommitments.signingCommitments.slice(
323
+ leaves.length,
324
+ 2 * leaves.length,
325
+ ),
326
+ signingCommitments.signingCommitments.slice(2 * leaves.length),
289
327
  );
290
328
 
291
329
  const encryptedKeyTweaks: { [key: string]: Uint8Array } = {};
@@ -312,12 +350,11 @@ export class BaseTransferService {
312
350
  }
313
351
 
314
352
  const transferPackage: TransferPackage = {
315
- leavesToSend: leafSigningJobs,
353
+ leavesToSend: cpfpLeafSigningJobs,
316
354
  keyTweakPackage: encryptedKeyTweaks,
317
355
  userSignature: new Uint8Array(),
318
- // TODO: Add direct refund signature
319
- directLeavesToSend: [],
320
- directFromCpfpLeavesToSend: [],
356
+ directLeavesToSend: directLeafSigningJobs,
357
+ directFromCpfpLeavesToSend: directFromCpfpLeafSigningJobs,
321
358
  };
322
359
 
323
360
  const transferPackageSigningPayload = getTransferPackageSigningPayload(
@@ -351,6 +388,7 @@ export class BaseTransferService {
351
388
  });
352
389
  }
353
390
  let transferResp: FinalizeTransferResponse;
391
+
354
392
  try {
355
393
  transferResp = await sparkClient.finalize_transfer({
356
394
  transferId: transfer.id,
@@ -392,9 +430,11 @@ export class BaseTransferService {
392
430
  }
393
431
 
394
432
  async signRefunds(
395
- leafDataMap: Map<string, ClaimLeafData>,
433
+ leafDataMap: Map<string, LeafRefundSigningData>,
396
434
  operatorSigningResults: LeafRefundTxSigningResult[],
397
- adaptorPubKey?: Uint8Array,
435
+ cpfpAdaptorPubKey?: Uint8Array,
436
+ directAdaptorPubKey?: Uint8Array,
437
+ directFromCpfpAdaptorPubKey?: Uint8Array,
398
438
  ): Promise<NodeSignatures[]> {
399
439
  const nodeSignatures: NodeSignatures[] = [];
400
440
  for (const operatorSigningResult of operatorSigningResults) {
@@ -417,24 +457,28 @@ export class BaseTransferService {
417
457
  );
418
458
  }
419
459
 
420
- const refundTxSighash = getSigHashFromTx(leafData.refundTx, 0, txOutput);
421
-
460
+ // Sign CPFP refund transaction
461
+ const cpfpRefundTxSighash = getSigHashFromTx(
462
+ leafData.refundTx,
463
+ 0,
464
+ txOutput,
465
+ );
422
466
  const publicKey = await this.config.signer.getPublicKeyFromDerivation(
423
467
  leafData.keyDerivation,
424
468
  );
425
- const userSignature = await this.config.signer.signFrost({
426
- message: refundTxSighash,
469
+ const cpfpUserSignature = await this.config.signer.signFrost({
470
+ message: cpfpRefundTxSighash,
427
471
  publicKey,
428
472
  keyDerivation: leafData.keyDerivation,
429
473
  selfCommitment: leafData.signingNonceCommitment,
430
474
  statechainCommitments:
431
475
  operatorSigningResult.refundTxSigningResult?.signingNonceCommitments,
432
- adaptorPubKey: adaptorPubKey,
476
+ adaptorPubKey: cpfpAdaptorPubKey,
433
477
  verifyingKey: operatorSigningResult.verifyingKey,
434
478
  });
435
479
 
436
- const refundAggregate = await this.config.signer.aggregateFrost({
437
- message: refundTxSighash,
480
+ const cpfpRefundAggregate = await this.config.signer.aggregateFrost({
481
+ message: cpfpRefundTxSighash,
438
482
  statechainSignatures:
439
483
  operatorSigningResult.refundTxSigningResult?.signatureShares,
440
484
  statechainPublicKeys:
@@ -444,18 +488,105 @@ export class BaseTransferService {
444
488
  operatorSigningResult.refundTxSigningResult?.signingNonceCommitments,
445
489
  selfCommitment: leafData.signingNonceCommitment,
446
490
  publicKey,
447
- selfSignature: userSignature,
448
- adaptorPubKey: adaptorPubKey,
491
+ selfSignature: cpfpUserSignature,
492
+ adaptorPubKey: cpfpAdaptorPubKey,
449
493
  });
450
494
 
495
+ // Sign direct refund transaction
496
+
497
+ let directRefundAggregate: Uint8Array | undefined;
498
+ let directFromCpfpRefundAggregate: Uint8Array | undefined;
499
+ if (leafData.directTx) {
500
+ const directTxOutput = leafData.directTx.getOutput(0);
501
+
502
+ if (leafData.directRefundTx) {
503
+ const directRefundTxSighash = getSigHashFromTx(
504
+ leafData.directRefundTx,
505
+ 0,
506
+ directTxOutput,
507
+ );
508
+
509
+ const directUserSignature = await this.config.signer.signFrost({
510
+ message: directRefundTxSighash,
511
+ publicKey,
512
+ keyDerivation: leafData.keyDerivation,
513
+ selfCommitment: leafData.directSigningNonceCommitment,
514
+ statechainCommitments:
515
+ operatorSigningResult.directRefundTxSigningResult
516
+ ?.signingNonceCommitments,
517
+ adaptorPubKey: directAdaptorPubKey,
518
+ verifyingKey: operatorSigningResult.verifyingKey,
519
+ });
520
+
521
+ directRefundAggregate = await this.config.signer.aggregateFrost({
522
+ message: directRefundTxSighash,
523
+ statechainSignatures:
524
+ operatorSigningResult.directRefundTxSigningResult
525
+ ?.signatureShares,
526
+ statechainPublicKeys:
527
+ operatorSigningResult.directRefundTxSigningResult?.publicKeys,
528
+ verifyingKey: operatorSigningResult.verifyingKey,
529
+ statechainCommitments:
530
+ operatorSigningResult.directRefundTxSigningResult
531
+ ?.signingNonceCommitments,
532
+ selfCommitment: leafData.directSigningNonceCommitment,
533
+ publicKey,
534
+ selfSignature: directUserSignature,
535
+ adaptorPubKey: directAdaptorPubKey,
536
+ });
537
+ }
538
+
539
+ if (leafData.directFromCpfpRefundTx) {
540
+ const directFromCpfpRefundTxSighash = getSigHashFromTx(
541
+ leafData.directFromCpfpRefundTx,
542
+ 0,
543
+ txOutput,
544
+ );
545
+
546
+ const directFromCpfpUserSignature =
547
+ await this.config.signer.signFrost({
548
+ message: directFromCpfpRefundTxSighash,
549
+ publicKey,
550
+ keyDerivation: leafData.keyDerivation,
551
+ selfCommitment:
552
+ leafData.directFromCpfpRefundSigningNonceCommitment,
553
+ statechainCommitments:
554
+ operatorSigningResult.directFromCpfpRefundTxSigningResult
555
+ ?.signingNonceCommitments,
556
+ adaptorPubKey: directFromCpfpAdaptorPubKey,
557
+ verifyingKey: operatorSigningResult.verifyingKey,
558
+ });
559
+
560
+ directFromCpfpRefundAggregate =
561
+ await this.config.signer.aggregateFrost({
562
+ message: directFromCpfpRefundTxSighash,
563
+ statechainSignatures:
564
+ operatorSigningResult.directFromCpfpRefundTxSigningResult
565
+ ?.signatureShares,
566
+ statechainPublicKeys:
567
+ operatorSigningResult.directFromCpfpRefundTxSigningResult
568
+ ?.publicKeys,
569
+ verifyingKey: operatorSigningResult.verifyingKey,
570
+ statechainCommitments:
571
+ operatorSigningResult.directFromCpfpRefundTxSigningResult
572
+ ?.signingNonceCommitments,
573
+ selfCommitment:
574
+ leafData.directFromCpfpRefundSigningNonceCommitment,
575
+ publicKey,
576
+ selfSignature: directFromCpfpUserSignature,
577
+ adaptorPubKey: directFromCpfpAdaptorPubKey,
578
+ });
579
+ }
580
+ }
581
+
451
582
  nodeSignatures.push({
452
583
  nodeId: operatorSigningResult.leafId,
453
- refundTxSignature: refundAggregate,
454
584
  nodeTxSignature: new Uint8Array(),
455
- // TODO: Add direct refund signature
456
585
  directNodeTxSignature: new Uint8Array(),
457
- directRefundTxSignature: new Uint8Array(),
458
- directFromCpfpRefundTxSignature: new Uint8Array(),
586
+ refundTxSignature: cpfpRefundAggregate,
587
+ directRefundTxSignature: directRefundAggregate ?? new Uint8Array(),
588
+ directFromCpfpRefundTxSignature:
589
+ directFromCpfpRefundAggregate ?? new Uint8Array(),
459
590
  });
460
591
  }
461
592
  return nodeSignatures;
@@ -465,7 +596,9 @@ export class BaseTransferService {
465
596
  transferID: string,
466
597
  receiverIdentityPubkey: Uint8Array,
467
598
  leaves: LeafKeyTweak[],
468
- refundSignatureMap: Map<string, Uint8Array>,
599
+ cpfpRefundSignatureMap: Map<string, Uint8Array>,
600
+ directRefundSignatureMap: Map<string, Uint8Array>,
601
+ directFromCpfpRefundSignatureMap: Map<string, Uint8Array>,
469
602
  ): Promise<Map<string, SendLeafKeyTweak[]>> {
470
603
  const receiverEciesPubKey = ecies.PublicKey.fromHex(
471
604
  bytesToHex(receiverIdentityPubkey),
@@ -474,12 +607,18 @@ export class BaseTransferService {
474
607
  const leavesTweaksMap = new Map<string, SendLeafKeyTweak[]>();
475
608
 
476
609
  for (const leaf of leaves) {
477
- const refundSignature = refundSignatureMap.get(leaf.leaf.id);
610
+ const cpfpRefundSignature = cpfpRefundSignatureMap.get(leaf.leaf.id);
611
+ const directRefundSignature = directRefundSignatureMap.get(leaf.leaf.id);
612
+ const directFromCpfpRefundSignature =
613
+ directFromCpfpRefundSignatureMap.get(leaf.leaf.id);
614
+
478
615
  const leafTweaksMap = await this.prepareSingleSendTransferKeyTweak(
479
616
  transferID,
480
617
  leaf,
481
618
  receiverEciesPubKey,
482
- refundSignature,
619
+ cpfpRefundSignature,
620
+ directRefundSignature,
621
+ directFromCpfpRefundSignature,
483
622
  );
484
623
  for (const [identifier, leafTweak] of leafTweaksMap) {
485
624
  leavesTweaksMap.set(identifier, [
@@ -496,7 +635,9 @@ export class BaseTransferService {
496
635
  transferID: string,
497
636
  leaf: LeafKeyTweak,
498
637
  receiverEciesPubKey: ecies.PublicKey,
499
- refundSignature?: Uint8Array,
638
+ cpfpRefundSignature?: Uint8Array,
639
+ directRefundSignature?: Uint8Array,
640
+ directFromCpfpRefundSignature?: Uint8Array,
500
641
  ): Promise<Map<string, SendLeafKeyTweak>> {
501
642
  const signingOperators = this.config.getSigningOperators();
502
643
 
@@ -553,10 +694,10 @@ export class BaseTransferService {
553
694
  pubkeySharesTweak: Object.fromEntries(pubkeySharesTweak),
554
695
  secretCipher,
555
696
  signature,
556
- refundSignature: refundSignature ?? new Uint8Array(),
557
- // TODO: Add direct refund signature
558
- directRefundSignature: new Uint8Array(),
559
- directFromCpfpRefundSignature: new Uint8Array(),
697
+ refundSignature: cpfpRefundSignature ?? new Uint8Array(),
698
+ directRefundSignature: directRefundSignature ?? new Uint8Array(),
699
+ directFromCpfpRefundSignature:
700
+ directFromCpfpRefundSignature ?? new Uint8Array(),
560
701
  });
561
702
  }
562
703
 
@@ -605,7 +746,12 @@ export class TransferService extends BaseTransferService {
605
746
  leaves: LeafKeyTweak[],
606
747
  receiverIdentityPubkey: Uint8Array,
607
748
  ): Promise<Transfer> {
608
- const { transfer, signatureMap } = await this.sendTransferSignRefund(
749
+ const {
750
+ transfer,
751
+ signatureMap,
752
+ directSignatureMap,
753
+ directFromCpfpSignatureMap,
754
+ } = await this.sendTransferSignRefund(
609
755
  leaves,
610
756
  receiverIdentityPubkey,
611
757
  new Date(Date.now() + DEFAULT_EXPIRY_TIME),
@@ -615,6 +761,8 @@ export class TransferService extends BaseTransferService {
615
761
  transfer,
616
762
  leaves,
617
763
  signatureMap,
764
+ directSignatureMap,
765
+ directFromCpfpSignatureMap,
618
766
  );
619
767
 
620
768
  return transferWithTweakedKeys;
@@ -748,19 +896,28 @@ export class TransferService extends BaseTransferService {
748
896
  ): Promise<{
749
897
  transfer: Transfer;
750
898
  signatureMap: Map<string, Uint8Array>;
899
+ directSignatureMap: Map<string, Uint8Array>;
900
+ directFromCpfpSignatureMap: Map<string, Uint8Array>;
751
901
  leafDataMap: Map<string, LeafRefundSigningData>;
752
902
  }> {
753
- const { transfer, signatureMap, leafDataMap } =
754
- await this.sendTransferSignRefundInternal(
755
- leaves,
756
- receiverIdentityPubkey,
757
- expiryTime,
758
- false,
759
- );
903
+ const {
904
+ transfer,
905
+ signatureMap,
906
+ directSignatureMap,
907
+ directFromCpfpSignatureMap,
908
+ leafDataMap,
909
+ } = await this.sendTransferSignRefundInternal(
910
+ leaves,
911
+ receiverIdentityPubkey,
912
+ expiryTime,
913
+ false,
914
+ );
760
915
 
761
916
  return {
762
917
  transfer,
763
918
  signatureMap,
919
+ directSignatureMap,
920
+ directFromCpfpSignatureMap,
764
921
  leafDataMap,
765
922
  };
766
923
  }
@@ -772,19 +929,28 @@ export class TransferService extends BaseTransferService {
772
929
  ): Promise<{
773
930
  transfer: Transfer;
774
931
  signatureMap: Map<string, Uint8Array>;
932
+ directSignatureMap: Map<string, Uint8Array>;
933
+ directFromCpfpSignatureMap: Map<string, Uint8Array>;
775
934
  leafDataMap: Map<string, LeafRefundSigningData>;
776
935
  }> {
777
- const { transfer, signatureMap, leafDataMap } =
778
- await this.sendTransferSignRefundInternal(
779
- leaves,
780
- receiverIdentityPubkey,
781
- expiryTime,
782
- true,
783
- );
936
+ const {
937
+ transfer,
938
+ signatureMap,
939
+ directSignatureMap,
940
+ directFromCpfpSignatureMap,
941
+ leafDataMap,
942
+ } = await this.sendTransferSignRefundInternal(
943
+ leaves,
944
+ receiverIdentityPubkey,
945
+ expiryTime,
946
+ true,
947
+ );
784
948
 
785
949
  return {
786
950
  transfer,
787
951
  signatureMap,
952
+ directSignatureMap,
953
+ directFromCpfpSignatureMap,
788
954
  leafDataMap,
789
955
  };
790
956
  }
@@ -793,10 +959,14 @@ export class TransferService extends BaseTransferService {
793
959
  leaves: LeafKeyTweak[],
794
960
  receiverIdentityPubkey: Uint8Array,
795
961
  expiryTime: Date,
796
- adaptorPubKey?: Uint8Array,
962
+ cpfpAdaptorPubKey?: Uint8Array,
963
+ directAdaptorPubKey?: Uint8Array,
964
+ directFromCpfpAdaptorPubKey?: Uint8Array,
797
965
  ): Promise<{
798
966
  transfer: Transfer;
799
967
  signatureMap: Map<string, Uint8Array>;
968
+ directSignatureMap: Map<string, Uint8Array>;
969
+ directFromCpfpSignatureMap: Map<string, Uint8Array>;
800
970
  leafDataMap: Map<string, LeafRefundSigningData>;
801
971
  signingResults: LeafRefundTxSigningResult[];
802
972
  }> {
@@ -805,7 +975,9 @@ export class TransferService extends BaseTransferService {
805
975
  receiverIdentityPubkey,
806
976
  expiryTime,
807
977
  true,
808
- adaptorPubKey,
978
+ cpfpAdaptorPubKey,
979
+ directAdaptorPubKey,
980
+ directFromCpfpAdaptorPubKey,
809
981
  );
810
982
  }
811
983
 
@@ -814,10 +986,14 @@ export class TransferService extends BaseTransferService {
814
986
  receiverIdentityPubkey: Uint8Array,
815
987
  expiryTime: Date,
816
988
  forSwap: boolean,
817
- adaptorPubKey?: Uint8Array,
989
+ cpfpAdaptorPubKey?: Uint8Array,
990
+ directAdaptorPubKey?: Uint8Array,
991
+ directFromCpfpAdaptorPubKey?: Uint8Array,
818
992
  ): Promise<{
819
993
  transfer: Transfer;
820
994
  signatureMap: Map<string, Uint8Array>;
995
+ directSignatureMap: Map<string, Uint8Array>;
996
+ directFromCpfpSignatureMap: Map<string, Uint8Array>;
821
997
  leafDataMap: Map<string, LeafRefundSigningData>;
822
998
  signingResults: LeafRefundTxSigningResult[];
823
999
  }> {
@@ -826,16 +1002,39 @@ export class TransferService extends BaseTransferService {
826
1002
  for (const leaf of leaves) {
827
1003
  const signingNonceCommitment =
828
1004
  await this.config.signer.getRandomSigningCommitment();
1005
+ const directSigningNonceCommitment =
1006
+ await this.config.signer.getRandomSigningCommitment();
1007
+ const directFromCpfpRefundSigningNonceCommitment =
1008
+ await this.config.signer.getRandomSigningCommitment();
829
1009
 
830
1010
  const tx = getTxFromRawTxBytes(leaf.leaf.nodeTx);
831
1011
  const refundTx = getTxFromRawTxBytes(leaf.leaf.refundTx);
832
1012
 
1013
+ const directTx =
1014
+ leaf.leaf.directTx.length > 0
1015
+ ? getTxFromRawTxBytes(leaf.leaf.directTx)
1016
+ : undefined;
1017
+
1018
+ const directRefundTx =
1019
+ leaf.leaf.directRefundTx.length > 0
1020
+ ? getTxFromRawTxBytes(leaf.leaf.directRefundTx)
1021
+ : undefined;
1022
+ const directFromCpfpRefundTx =
1023
+ leaf.leaf.directFromCpfpRefundTx.length > 0
1024
+ ? getTxFromRawTxBytes(leaf.leaf.directFromCpfpRefundTx)
1025
+ : undefined;
1026
+
833
1027
  leafDataMap.set(leaf.leaf.id, {
834
1028
  keyDerivation: leaf.keyDerivation,
835
1029
  receivingPubkey: receiverIdentityPubkey,
836
1030
  signingNonceCommitment,
1031
+ directSigningNonceCommitment,
837
1032
  tx,
1033
+ directTx,
838
1034
  refundTx,
1035
+ directRefundTx,
1036
+ directFromCpfpRefundTx,
1037
+ directFromCpfpRefundSigningNonceCommitment,
839
1038
  vout: leaf.leaf.vout,
840
1039
  });
841
1040
  }
@@ -844,15 +1043,19 @@ export class TransferService extends BaseTransferService {
844
1043
  leaves,
845
1044
  leafDataMap,
846
1045
  );
847
-
1046
+ // 0197d19f-7419-7d51-9d2b-247127b5ab0a
848
1047
  const sparkClient = await this.connectionManager.createSparkClient(
849
1048
  this.config.getCoordinatorAddress(),
850
1049
  );
851
1050
 
852
1051
  let response: CounterLeafSwapResponse;
853
1052
  try {
854
- if (adaptorPubKey !== undefined) {
855
- response = await sparkClient.counter_leaf_swap({
1053
+ if (
1054
+ cpfpAdaptorPubKey !== undefined ||
1055
+ directAdaptorPubKey !== undefined ||
1056
+ directFromCpfpAdaptorPubKey !== undefined
1057
+ ) {
1058
+ response = await sparkClient.counter_leaf_swap_v2({
856
1059
  transfer: {
857
1060
  transferId,
858
1061
  leavesToSend: signingJobs,
@@ -862,10 +1065,12 @@ export class TransferService extends BaseTransferService {
862
1065
  expiryTime: expiryTime,
863
1066
  },
864
1067
  swapId: uuidv7(),
865
- adaptorPublicKey: adaptorPubKey || new Uint8Array(),
1068
+ adaptorPublicKey: cpfpAdaptorPubKey,
1069
+ directAdaptorPublicKey: directAdaptorPubKey,
1070
+ directFromCpfpAdaptorPublicKey: directFromCpfpAdaptorPubKey,
866
1071
  });
867
1072
  } else if (forSwap) {
868
- response = await sparkClient.start_leaf_swap({
1073
+ response = await sparkClient.start_leaf_swap_v2({
869
1074
  transferId,
870
1075
  leavesToSend: signingJobs,
871
1076
  ownerIdentityPublicKey:
@@ -874,7 +1079,7 @@ export class TransferService extends BaseTransferService {
874
1079
  expiryTime: expiryTime,
875
1080
  });
876
1081
  } else {
877
- response = await sparkClient.start_transfer({
1082
+ response = await sparkClient.start_transfer_v2({
878
1083
  transferId,
879
1084
  leavesToSend: signingJobs,
880
1085
  ownerIdentityPublicKey:
@@ -894,17 +1099,31 @@ export class TransferService extends BaseTransferService {
894
1099
  const signatures = await this.signRefunds(
895
1100
  leafDataMap,
896
1101
  response.signingResults,
897
- adaptorPubKey,
1102
+ cpfpAdaptorPubKey,
1103
+ directAdaptorPubKey,
1104
+ directFromCpfpAdaptorPubKey,
898
1105
  );
899
1106
 
900
- const signatureMap = new Map<string, Uint8Array>();
1107
+ const cpfpSignatureMap = new Map<string, Uint8Array>();
1108
+ const directSignatureMap = new Map<string, Uint8Array>();
1109
+ const directFromCpfpSignatureMap = new Map<string, Uint8Array>();
901
1110
  for (const signature of signatures) {
902
- signatureMap.set(signature.nodeId, signature.refundTxSignature);
1111
+ cpfpSignatureMap.set(signature.nodeId, signature.refundTxSignature);
1112
+ directSignatureMap.set(
1113
+ signature.nodeId,
1114
+ signature.directRefundTxSignature,
1115
+ );
1116
+ directFromCpfpSignatureMap.set(
1117
+ signature.nodeId,
1118
+ signature.directFromCpfpRefundTxSignature,
1119
+ );
903
1120
  }
904
1121
 
905
1122
  return {
906
1123
  transfer: response.transfer,
907
- signatureMap,
1124
+ signatureMap: cpfpSignatureMap,
1125
+ directSignatureMap: directSignatureMap,
1126
+ directFromCpfpSignatureMap: directFromCpfpSignatureMap,
908
1127
  leafDataMap,
909
1128
  signingResults: response.signingResults,
910
1129
  };
@@ -923,48 +1142,89 @@ export class TransferService extends BaseTransferService {
923
1142
  }
924
1143
 
925
1144
  const nodeTx = getTxFromRawTxBytes(leaf.leaf.nodeTx);
926
- const nodeOutPoint: TransactionInput = {
1145
+ const cpfpNodeOutPoint: TransactionInput = {
927
1146
  txid: hexToBytes(getTxId(nodeTx)),
928
1147
  index: 0,
929
1148
  };
930
1149
 
1150
+ let directNodeTx: Transaction | undefined;
1151
+ let directNodeOutPoint: TransactionInput | undefined;
1152
+ if (leaf.leaf.directTx.length > 0) {
1153
+ directNodeTx = getTxFromRawTxBytes(leaf.leaf.directTx);
1154
+ directNodeOutPoint = {
1155
+ txid: hexToBytes(getTxId(directNodeTx)),
1156
+ index: 0,
1157
+ };
1158
+ }
1159
+
931
1160
  const currRefundTx = getTxFromRawTxBytes(leaf.leaf.refundTx);
932
- const nextSequence = isForClaim
933
- ? getTransactionSequence(currRefundTx.getInput(0).sequence)
934
- : getNextTransactionSequence(currRefundTx.getInput(0).sequence)
935
- .nextSequence;
1161
+
1162
+ const sequence = currRefundTx.getInput(0).sequence;
1163
+ if (!sequence) {
1164
+ throw new ValidationError("Invalid refund transaction", {
1165
+ field: "sequence",
1166
+ value: currRefundTx.getInput(0),
1167
+ expected: "Non-null sequence",
1168
+ });
1169
+ }
1170
+ const { nextSequence, nextDirectSequence } = isForClaim
1171
+ ? getTransactionSequence(sequence)
1172
+ : getNextTransactionSequence(sequence);
936
1173
 
937
1174
  const amountSats = currRefundTx.getOutput(0).amount;
938
1175
  if (amountSats === undefined) {
939
1176
  throw new Error("Amount not found in signRefunds");
940
1177
  }
941
1178
 
942
- const refundTx = createRefundTx(
943
- nextSequence,
944
- nodeOutPoint,
945
- amountSats,
946
- refundSigningData.receivingPubkey,
947
- this.config.getNetwork(),
948
- );
1179
+ const { cpfpRefundTx, directRefundTx, directFromCpfpRefundTx } =
1180
+ createRefundTxs({
1181
+ sequence: nextSequence,
1182
+ directSequence: nextDirectSequence,
1183
+ input: cpfpNodeOutPoint,
1184
+ directInput: directNodeOutPoint,
1185
+ amountSats,
1186
+ receivingPubkey: refundSigningData.receivingPubkey,
1187
+ network: this.config.getNetwork(),
1188
+ });
949
1189
 
950
- refundSigningData.refundTx = refundTx;
1190
+ refundSigningData.refundTx = cpfpRefundTx;
1191
+ refundSigningData.directRefundTx = directRefundTx;
1192
+ refundSigningData.directFromCpfpRefundTx = directFromCpfpRefundTx;
951
1193
 
952
- const refundNonceCommitmentProto =
1194
+ const cpfpRefundNonceCommitmentProto =
953
1195
  refundSigningData.signingNonceCommitment;
954
-
1196
+ const directRefundNonceCommitmentProto =
1197
+ refundSigningData.directSigningNonceCommitment;
1198
+ const directFromCpfpRefundNonceCommitmentProto =
1199
+ refundSigningData.directFromCpfpRefundSigningNonceCommitment;
1200
+
1201
+ const signingPublicKey =
1202
+ await this.config.signer.getPublicKeyFromDerivation(
1203
+ refundSigningData.keyDerivation,
1204
+ );
955
1205
  signingJobs.push({
956
1206
  leafId: leaf.leaf.id,
957
1207
  refundTxSigningJob: {
958
- signingPublicKey: await this.config.signer.getPublicKeyFromDerivation(
959
- refundSigningData.keyDerivation,
960
- ),
961
- rawTx: refundTx.toBytes(),
962
- signingNonceCommitment: refundNonceCommitmentProto.commitment,
1208
+ signingPublicKey,
1209
+ rawTx: cpfpRefundTx.toBytes(),
1210
+ signingNonceCommitment: cpfpRefundNonceCommitmentProto.commitment,
963
1211
  },
964
- // TODO: Add direct refund signature
965
- directRefundTxSigningJob: undefined,
966
- // TODO: Add direct refund signature
967
- directFromCpfpRefundTxSigningJob: undefined,
1212
+ directRefundTxSigningJob: directRefundTx
1213
+ ? {
1214
+ signingPublicKey,
1215
+ rawTx: directRefundTx.toBytes(),
1216
+ signingNonceCommitment:
1217
+ directRefundNonceCommitmentProto.commitment,
1218
+ }
1219
+ : undefined,
1220
+ directFromCpfpRefundTxSigningJob: directFromCpfpRefundTx
1221
+ ? {
1222
+ signingPublicKey,
1223
+ rawTx: directFromCpfpRefundTx.toBytes(),
1224
+ signingNonceCommitment:
1225
+ directFromCpfpRefundNonceCommitmentProto.commitment,
1226
+ }
1227
+ : undefined,
968
1228
  });
969
1229
  }
970
1230
 
@@ -1122,6 +1382,11 @@ export class TransferService extends BaseTransferService {
1122
1382
  const leafDataMap: Map<string, LeafRefundSigningData> = new Map();
1123
1383
  for (const leafKey of leafKeys) {
1124
1384
  const tx = getTxFromRawTxBytes(leafKey.leaf.nodeTx);
1385
+ const directTx =
1386
+ leafKey.leaf.directTx.length > 0
1387
+ ? getTxFromRawTxBytes(leafKey.leaf.directTx)
1388
+ : undefined;
1389
+
1125
1390
  leafDataMap.set(leafKey.leaf.id, {
1126
1391
  keyDerivation: leafKey.newKeyDerivation,
1127
1392
  receivingPubkey: await this.config.signer.getPublicKeyFromDerivation(
@@ -1129,7 +1394,12 @@ export class TransferService extends BaseTransferService {
1129
1394
  ),
1130
1395
  signingNonceCommitment:
1131
1396
  await this.config.signer.getRandomSigningCommitment(),
1397
+ directSigningNonceCommitment:
1398
+ await this.config.signer.getRandomSigningCommitment(),
1399
+ directFromCpfpRefundSigningNonceCommitment:
1400
+ await this.config.signer.getRandomSigningCommitment(),
1132
1401
  tx,
1402
+ directTx,
1133
1403
  vout: leafKey.leaf.vout,
1134
1404
  });
1135
1405
  }
@@ -1154,7 +1424,7 @@ export class TransferService extends BaseTransferService {
1154
1424
  }
1155
1425
  }
1156
1426
  try {
1157
- resp = await sparkClient.claim_transfer_sign_refunds({
1427
+ resp = await sparkClient.claim_transfer_sign_refunds_v2({
1158
1428
  transferId: transfer.id,
1159
1429
  ownerIdentityPublicKey: await this.config.signer.getIdentityPublicKey(),
1160
1430
  signingJobs,
@@ -1170,7 +1440,7 @@ export class TransferService extends BaseTransferService {
1170
1440
  this.config.getCoordinatorAddress(),
1171
1441
  );
1172
1442
  try {
1173
- return await sparkClient.finalize_node_signatures({
1443
+ return await sparkClient.finalize_node_signatures_v2({
1174
1444
  intent: SignatureIntent.TRANSFER,
1175
1445
  nodeSignatures,
1176
1446
  });
@@ -1223,135 +1493,180 @@ export class TransferService extends BaseTransferService {
1223
1493
  }
1224
1494
  }
1225
1495
 
1226
- async refreshTimelockNodes(nodes: TreeNode[], parentNode: TreeNode) {
1227
- if (nodes.length === 0) {
1228
- throw Error("no nodes to refresh");
1496
+ private async refreshTimelockNodesInternal(
1497
+ node: TreeNode,
1498
+ parentNode: TreeNode,
1499
+ useTestUnilateralSequence?: boolean,
1500
+ ) {
1501
+ const signingJobs: (SigningJobWithOptionalNonce & {
1502
+ type: "node" | "directNode" | "cpfp" | "direct" | "directFromCpfp";
1503
+ parentTxOut: TransactionOutput;
1504
+ })[] = [];
1505
+
1506
+ const parentNodeTx = getTxFromRawTxBytes(parentNode.nodeTx);
1507
+ const parentNodeOutput: TransactionOutput = parentNodeTx.getOutput(0);
1508
+ if (!parentNodeOutput) {
1509
+ throw Error("Could not get parent node output");
1229
1510
  }
1230
1511
 
1231
- const signingJobs: SigningJobWithOptionalNonce[] = [];
1232
- const newNodeTxs: Transaction[] = [];
1233
-
1234
- for (let i = 0; i < nodes.length; i++) {
1235
- const node = nodes[i];
1236
- if (!node) {
1237
- throw Error("could not get node");
1238
- }
1239
- const nodeTx = getTxFromRawTxBytes(node?.nodeTx);
1240
- const input = nodeTx.getInput(0);
1512
+ const nodeTx = getTxFromRawTxBytes(node.nodeTx);
1513
+ const nodeInput = nodeTx.getInput(0);
1241
1514
 
1242
- if (!input) {
1243
- throw Error("Could not fetch tx input");
1244
- }
1515
+ const nodeOutput = nodeTx.getOutput(0);
1516
+ if (!nodeOutput) {
1517
+ throw Error("Could not get node output");
1518
+ }
1245
1519
 
1246
- const newTx = new Transaction({ version: 3, allowUnknownOutputs: true });
1520
+ let directNodeTx: Transaction | undefined;
1521
+ let directNodeInput: TransactionInput | undefined;
1522
+ if (node.directTx.length > 0) {
1523
+ directNodeTx = getTxFromRawTxBytes(node.directTx);
1524
+ directNodeInput = directNodeTx.getInput(0);
1525
+ }
1247
1526
 
1248
- // Apply fee to the main output
1249
- const originalOutput = nodeTx.getOutput(0);
1250
- if (!originalOutput) {
1251
- throw Error("Could not get original output");
1252
- }
1527
+ const currSequence = nodeInput.sequence;
1253
1528
 
1254
- newTx.addOutput({
1255
- script: originalOutput.script!,
1256
- amount: originalOutput.amount!,
1529
+ if (!currSequence) {
1530
+ throw new ValidationError("Invalid node transaction", {
1531
+ field: "sequence",
1532
+ value: nodeInput,
1533
+ expected: "Non-null sequence",
1257
1534
  });
1535
+ }
1258
1536
 
1259
- // Copy any additional outputs (like ephemeral anchors) without fee reduction
1260
- for (let j = 1; j < nodeTx.outputsLength; j++) {
1261
- const additionalOutput = nodeTx.getOutput(j);
1262
- if (additionalOutput) {
1263
- newTx.addOutput(additionalOutput);
1264
- }
1265
- }
1537
+ let { nextSequence, nextDirectSequence } = getNextTransactionSequence(
1538
+ currSequence,
1539
+ true,
1540
+ );
1266
1541
 
1267
- if (i === 0) {
1268
- const currSequence = input.sequence;
1542
+ const output = {
1543
+ script: parentNodeOutput.script!,
1544
+ amount: parentNodeOutput.amount!,
1545
+ };
1269
1546
 
1270
- newTx.addInput({
1271
- ...input,
1272
- sequence: getNextTransactionSequence(currSequence).nextSequence,
1273
- });
1274
- } else {
1275
- newTx.addInput({
1276
- ...input,
1277
- sequence: initialSequence(),
1278
- txid: newNodeTxs[i - 1]?.id,
1279
- });
1280
- }
1547
+ const newNodeInput: TransactionInput = {
1548
+ txid: nodeInput.txid,
1549
+ index: nodeInput.index,
1550
+ sequence: useTestUnilateralSequence
1551
+ ? TEST_UNILATERAL_SEQUENCE
1552
+ : nextSequence,
1553
+ };
1281
1554
 
1282
- signingJobs.push({
1283
- signingPublicKey: await this.config.signer.getPublicKeyFromDerivation({
1284
- type: KeyDerivationType.LEAF,
1285
- path: node.id,
1286
- }),
1287
- rawTx: newTx.toBytes(),
1288
- signingNonceCommitment:
1289
- await this.config.signer.getRandomSigningCommitment(),
1290
- });
1291
- newNodeTxs[i] = newTx;
1292
- }
1555
+ const newDirectInput: TransactionInput | undefined =
1556
+ directNodeTx && directNodeInput
1557
+ ? {
1558
+ txid: directNodeInput.txid,
1559
+ index: directNodeInput.index,
1560
+ sequence: useTestUnilateralSequence
1561
+ ? TEST_UNILATERAL_DIRECT_SEQUENCE
1562
+ : nextDirectSequence,
1563
+ }
1564
+ : undefined;
1565
+
1566
+ const { cpfpNodeTx, directNodeTx: newDirectNodeTx } = createNodeTxs(
1567
+ output,
1568
+ newNodeInput,
1569
+ newDirectInput,
1570
+ );
1293
1571
 
1294
- const leaf = nodes[nodes.length - 1];
1295
- if (!leaf?.refundTx) {
1296
- throw Error("leaf does not have refund tx");
1572
+ const newCpfpNodeOutput: TransactionOutput = cpfpNodeTx.getOutput(0);
1573
+ if (!newCpfpNodeOutput) {
1574
+ throw Error("Could not get new cpfp node output");
1297
1575
  }
1298
- const refundTx = getTxFromRawTxBytes(leaf?.refundTx);
1299
- const newRefundTx = new Transaction({
1300
- version: 3,
1301
- allowUnknownOutputs: true,
1302
- });
1303
1576
 
1304
- // Apply fee to the refund output
1305
- const originalRefundOutput = refundTx.getOutput(0);
1306
- if (!originalRefundOutput) {
1307
- throw Error("Could not get original refund output");
1308
- }
1577
+ const newDirectNodeOutput: TransactionOutput | undefined =
1578
+ newDirectNodeTx?.getOutput(0);
1309
1579
 
1310
- newRefundTx.addOutput({
1311
- script: originalRefundOutput.script!,
1312
- amount: originalRefundOutput.amount!,
1580
+ const signingPublicKey =
1581
+ await this.config.signer.getPublicKeyFromDerivation({
1582
+ type: KeyDerivationType.LEAF,
1583
+ path: node.id,
1584
+ });
1585
+
1586
+ signingJobs.push({
1587
+ signingPublicKey,
1588
+ rawTx: cpfpNodeTx.toBytes(),
1589
+ signingNonceCommitment:
1590
+ await this.config.signer.getRandomSigningCommitment(),
1591
+ type: "node",
1592
+ parentTxOut: parentNodeOutput,
1313
1593
  });
1314
1594
 
1315
- // Copy any additional outputs (like ephemeral anchors) without fee reduction
1316
- for (let j = 1; j < refundTx.outputsLength; j++) {
1317
- const additionalOutput = refundTx.getOutput(j);
1318
- if (additionalOutput) {
1319
- newRefundTx.addOutput(additionalOutput);
1320
- }
1595
+ if (newDirectNodeTx) {
1596
+ signingJobs.push({
1597
+ signingPublicKey,
1598
+ rawTx: newDirectNodeTx.toBytes(),
1599
+ signingNonceCommitment:
1600
+ await this.config.signer.getRandomSigningCommitment(),
1601
+ type: "directNode",
1602
+ parentTxOut: parentNodeOutput,
1603
+ });
1321
1604
  }
1322
1605
 
1323
- const refundTxInput = refundTx.getInput(0);
1324
- if (!refundTxInput) {
1325
- throw Error("refund tx doesn't have input");
1326
- }
1606
+ const newCpfpRefundOutPoint: TransactionInput = {
1607
+ txid: hexToBytes(getTxId(cpfpNodeTx)!),
1608
+ index: 0,
1609
+ };
1327
1610
 
1328
- if (!newNodeTxs[newNodeTxs.length - 1]) {
1329
- throw Error("Could not get last node tx");
1611
+ let newDirectRefundOutPoint: TransactionInput | undefined;
1612
+ if (newDirectNodeTx) {
1613
+ newDirectRefundOutPoint = {
1614
+ txid: hexToBytes(getTxId(newDirectNodeTx)!),
1615
+ index: 0,
1616
+ };
1330
1617
  }
1331
- newRefundTx.addInput({
1332
- ...refundTxInput,
1333
- sequence: initialSequence(),
1334
- txid: getTxId(newNodeTxs[newNodeTxs.length - 1]!),
1335
- });
1336
1618
 
1337
- const refundSigningJob = {
1338
- signingPublicKey: await this.config.signer.getPublicKeyFromDerivation({
1339
- type: KeyDerivationType.LEAF,
1340
- path: leaf.id,
1341
- }),
1342
- rawTx: newRefundTx.toBytes(),
1619
+ const { cpfpRefundTx, directRefundTx, directFromCpfpRefundTx } =
1620
+ createRefundTxs({
1621
+ sequence: INITIAL_SEQUENCE,
1622
+ directSequence: INITIAL_DIRECT_SEQUENCE,
1623
+ input: newCpfpRefundOutPoint,
1624
+ directInput: newDirectRefundOutPoint,
1625
+ amountSats: nodeOutput.amount!,
1626
+ receivingPubkey: await this.config.signer.getPublicKeyFromDerivation({
1627
+ type: KeyDerivationType.LEAF,
1628
+ path: node.id,
1629
+ }),
1630
+ network: this.config.getNetwork(),
1631
+ });
1632
+
1633
+ signingJobs.push({
1634
+ signingPublicKey,
1635
+ rawTx: cpfpRefundTx.toBytes(),
1343
1636
  signingNonceCommitment:
1344
1637
  await this.config.signer.getRandomSigningCommitment(),
1345
- };
1638
+ type: "cpfp",
1639
+ parentTxOut: newCpfpNodeOutput,
1640
+ });
1641
+
1642
+ if (directRefundTx && newDirectNodeOutput) {
1643
+ signingJobs.push({
1644
+ signingPublicKey,
1645
+ rawTx: directRefundTx.toBytes(),
1646
+ signingNonceCommitment:
1647
+ await this.config.signer.getRandomSigningCommitment(),
1648
+ type: "direct",
1649
+ parentTxOut: newDirectNodeOutput,
1650
+ });
1651
+ }
1346
1652
 
1347
- signingJobs.push(refundSigningJob);
1653
+ if (directFromCpfpRefundTx && newCpfpNodeOutput) {
1654
+ signingJobs.push({
1655
+ signingPublicKey,
1656
+ rawTx: directFromCpfpRefundTx.toBytes(),
1657
+ signingNonceCommitment:
1658
+ await this.config.signer.getRandomSigningCommitment(),
1659
+ type: "directFromCpfp",
1660
+ parentTxOut: newCpfpNodeOutput,
1661
+ });
1662
+ }
1348
1663
 
1349
1664
  const sparkClient = await this.connectionManager.createSparkClient(
1350
1665
  this.config.getCoordinatorAddress(),
1351
1666
  );
1352
1667
 
1353
- const response = await sparkClient.refresh_timelock({
1354
- leafId: leaf.id,
1668
+ const response = await sparkClient.refresh_timelock_v2({
1669
+ leafId: node.id,
1355
1670
  ownerIdentityPublicKey: await this.config.signer.getIdentityPublicKey(),
1356
1671
  signingJobs: signingJobs.map(getSigningJobProto),
1357
1672
  });
@@ -1363,52 +1678,31 @@ export class TransferService extends BaseTransferService {
1363
1678
  }
1364
1679
 
1365
1680
  let nodeSignatures: NodeSignatures[] = [];
1366
- let leafSignature: Uint8Array | undefined;
1367
- let refundSignature: Uint8Array | undefined;
1368
- let leafNodeId: string | undefined;
1369
- for (let i = 0; i < response.signingResults.length; i++) {
1370
- const signingResult = response.signingResults[i];
1681
+ let leafCpfpSignature: Uint8Array | undefined;
1682
+ let leafDirectSignature: Uint8Array | undefined;
1683
+ let cpfpRefundSignature: Uint8Array | undefined;
1684
+ let directRefundSignature: Uint8Array | undefined;
1685
+ let directFromCpfpRefundSignature: Uint8Array | undefined;
1686
+
1687
+ for (const [i, signingResult] of response.signingResults.entries()) {
1371
1688
  const signingJob = signingJobs[i];
1372
1689
  if (!signingJob || !signingResult) {
1373
1690
  throw Error("Signing job does not exist");
1374
1691
  }
1375
1692
 
1376
- if (!signingJob.signingNonceCommitment) {
1377
- throw Error("nonce commitment does not exist");
1378
- }
1379
1693
  const rawTx = getTxFromRawTxBytes(signingJob.rawTx);
1380
-
1381
- let parentTx: Transaction | undefined;
1382
- let nodeId: string | undefined;
1383
- let vout: number | undefined;
1384
-
1385
- if (i === nodes.length) {
1386
- nodeId = nodes[i - 1]?.id;
1387
- parentTx = newNodeTxs[i - 1];
1388
- vout = 0;
1389
- } else if (i === 0) {
1390
- nodeId = nodes[i]?.id;
1391
- parentTx = getTxFromRawTxBytes(parentNode.nodeTx);
1392
- vout = nodes[i]?.vout;
1393
- } else {
1394
- nodeId = nodes[i]?.id;
1395
- parentTx = newNodeTxs[i - 1];
1396
- vout = nodes[i]?.vout;
1694
+ const txOut = signingJob.parentTxOut;
1695
+ if (!txOut) {
1696
+ throw Error("Could not get tx out");
1397
1697
  }
1398
1698
 
1399
- if (!parentTx || !nodeId || vout === undefined) {
1400
- throw Error("Could not parse signing results");
1401
- }
1402
-
1403
- const txOut = parentTx.getOutput(vout);
1404
-
1405
1699
  const rawTxSighash = getSigHashFromTx(rawTx, 0, txOut);
1406
1700
 
1407
1701
  const userSignature = await this.config.signer.signFrost({
1408
1702
  message: rawTxSighash,
1409
1703
  keyDerivation: {
1410
1704
  type: KeyDerivationType.LEAF,
1411
- path: nodeId,
1705
+ path: node.id,
1412
1706
  },
1413
1707
  publicKey: signingJob.signingPublicKey,
1414
1708
  verifyingKey: signingResult.verifyingKey,
@@ -1431,39 +1725,30 @@ export class TransferService extends BaseTransferService {
1431
1725
  adaptorPubKey: new Uint8Array(),
1432
1726
  });
1433
1727
 
1434
- if (i !== nodes.length && i !== nodes.length - 1) {
1435
- nodeSignatures.push({
1436
- nodeId: nodeId,
1437
- nodeTxSignature: signature,
1438
- refundTxSignature: new Uint8Array(),
1439
- // TODO: Add direct refund signature
1440
- directNodeTxSignature: new Uint8Array(),
1441
- directRefundTxSignature: new Uint8Array(),
1442
- directFromCpfpRefundTxSignature: new Uint8Array(),
1443
- });
1444
- } else if (i === nodes.length) {
1445
- refundSignature = signature;
1446
- } else if (i === nodes.length - 1) {
1447
- leafNodeId = nodeId;
1448
- leafSignature = signature;
1728
+ if (signingJob.type === "node") {
1729
+ leafCpfpSignature = signature;
1730
+ } else if (signingJob.type === "directNode") {
1731
+ leafDirectSignature = signature;
1732
+ } else if (signingJob.type === "cpfp") {
1733
+ cpfpRefundSignature = signature;
1734
+ } else if (signingJob.type === "direct") {
1735
+ directRefundSignature = signature;
1736
+ } else if (signingJob.type === "directFromCpfp") {
1737
+ directFromCpfpRefundSignature = signature;
1449
1738
  }
1450
1739
  }
1451
1740
 
1452
- if (!leafSignature || !refundSignature || !leafNodeId) {
1453
- throw Error("leaf or refund signature does not exist");
1454
- }
1455
-
1456
1741
  nodeSignatures.push({
1457
- nodeId: leafNodeId,
1458
- nodeTxSignature: leafSignature,
1459
- refundTxSignature: refundSignature,
1460
- // TODO: Add direct refund signature
1461
- directNodeTxSignature: new Uint8Array(),
1462
- directRefundTxSignature: new Uint8Array(),
1463
- directFromCpfpRefundTxSignature: new Uint8Array(),
1742
+ nodeId: node.id,
1743
+ nodeTxSignature: leafCpfpSignature || new Uint8Array(),
1744
+ directNodeTxSignature: leafDirectSignature || new Uint8Array(),
1745
+ refundTxSignature: cpfpRefundSignature || new Uint8Array(),
1746
+ directRefundTxSignature: directRefundSignature || new Uint8Array(),
1747
+ directFromCpfpRefundTxSignature:
1748
+ directFromCpfpRefundSignature || new Uint8Array(),
1464
1749
  });
1465
1750
 
1466
- const result = await sparkClient.finalize_node_signatures({
1751
+ const result = await sparkClient.finalize_node_signatures_v2({
1467
1752
  intent: SignatureIntent.REFRESH,
1468
1753
  nodeSignatures,
1469
1754
  });
@@ -1471,6 +1756,10 @@ export class TransferService extends BaseTransferService {
1471
1756
  return result;
1472
1757
  }
1473
1758
 
1759
+ async refreshTimelockNodes(node: TreeNode, parentNode: TreeNode) {
1760
+ return await this.refreshTimelockNodesInternal(node, parentNode);
1761
+ }
1762
+
1474
1763
  async extendTimelock(node: TreeNode) {
1475
1764
  const nodeTx = getTxFromRawTxBytes(node.nodeTx);
1476
1765
  const refundTx = getTxFromRawTxBytes(node.refundTx);
@@ -1481,8 +1770,12 @@ export class TransferService extends BaseTransferService {
1481
1770
  index: 0,
1482
1771
  };
1483
1772
 
1484
- const { nextSequence: newNodeSequence } =
1485
- getNextTransactionSequence(refundSequence);
1773
+ const {
1774
+ nextSequence: newNodeSequence,
1775
+ nextDirectSequence: newDirectNodeSequence,
1776
+ } = getNextTransactionSequence(refundSequence);
1777
+
1778
+ // Create new CPFP node tx
1486
1779
  const newNodeTx = new Transaction({
1487
1780
  version: 3,
1488
1781
  allowUnknownOutputs: true,
@@ -1495,82 +1788,177 @@ export class TransferService extends BaseTransferService {
1495
1788
  throw Error("Could not get original node output");
1496
1789
  }
1497
1790
 
1498
- // const feeReducedAmount = maybeApplyFee(originalOutput.amount!);
1499
1791
  newNodeTx.addOutput({
1500
1792
  script: originalOutput.script!,
1501
- amount: originalOutput.amount!, // feeReducedAmount,
1793
+ amount: originalOutput.amount!,
1502
1794
  });
1503
1795
 
1504
1796
  newNodeTx.addOutput(getEphemeralAnchorOutput());
1505
1797
 
1506
- const newRefundOutPoint: TransactionInput = {
1798
+ // Create new direct node tx
1799
+
1800
+ let newDirectNodeTx: Transaction | undefined;
1801
+ if (node.directTx.length > 0) {
1802
+ newDirectNodeTx = new Transaction({
1803
+ version: 3,
1804
+ allowUnknownOutputs: true,
1805
+ });
1806
+
1807
+ newDirectNodeTx.addInput({
1808
+ ...newNodeOutPoint,
1809
+ sequence: newDirectNodeSequence,
1810
+ });
1811
+
1812
+ newDirectNodeTx.addOutput({
1813
+ script: originalOutput.script!,
1814
+ amount: maybeApplyFee(originalOutput.amount!),
1815
+ });
1816
+ }
1817
+
1818
+ const newCpfpRefundOutPoint: TransactionInput = {
1507
1819
  txid: hexToBytes(getTxId(newNodeTx)!),
1508
1820
  index: 0,
1509
1821
  };
1510
1822
 
1823
+ let newDirectRefundOutPoint: TransactionInput | undefined;
1824
+ if (newDirectNodeTx) {
1825
+ newDirectRefundOutPoint = {
1826
+ txid: hexToBytes(getTxId(newDirectNodeTx)!),
1827
+ index: 0,
1828
+ };
1829
+ }
1830
+
1511
1831
  const amountSats = refundTx.getOutput(0).amount;
1512
1832
  if (amountSats === undefined) {
1513
1833
  throw new Error("Amount not found in extendTimelock");
1514
1834
  }
1515
1835
 
1516
- const signingPubKey = await this.config.signer.getPublicKeyFromDerivation({
1517
- type: KeyDerivationType.LEAF,
1518
- path: node.id,
1836
+ const signingPublicKey =
1837
+ await this.config.signer.getPublicKeyFromDerivation({
1838
+ type: KeyDerivationType.LEAF,
1839
+ path: node.id,
1840
+ });
1841
+ // Create both CPFP and direct refund transactions
1842
+
1843
+ const {
1844
+ cpfpRefundTx: newCpfpRefundTx,
1845
+ directRefundTx: newDirectRefundTx,
1846
+ directFromCpfpRefundTx: newDirectFromCpfpRefundTx,
1847
+ } = createRefundTxs({
1848
+ sequence: INITIAL_SEQUENCE,
1849
+ directSequence: INITIAL_DIRECT_SEQUENCE,
1850
+ input: newCpfpRefundOutPoint,
1851
+ directInput: newDirectRefundOutPoint,
1852
+ amountSats,
1853
+ receivingPubkey: signingPublicKey,
1854
+ network: this.config.getNetwork(),
1519
1855
  });
1520
1856
 
1521
- // Apply fee to the refund transaction as well
1522
- // const feeReducedRefundAmount = maybeApplyFee(amountSats);
1523
- const newRefundTx = createRefundTx(
1524
- initialSequence(),
1525
- newRefundOutPoint,
1526
- amountSats, // feeReducedRefundAmount,
1527
- signingPubKey,
1528
- this.config.getNetwork(),
1529
- );
1857
+ if (!newCpfpRefundTx) {
1858
+ throw new ValidationError(
1859
+ "Failed to create refund transactions in extendTimelock",
1860
+ );
1861
+ }
1530
1862
 
1531
1863
  const nodeSighash = getSigHashFromTx(newNodeTx, 0, nodeTx.getOutput(0));
1532
- const refundSighash = getSigHashFromTx(
1533
- newRefundTx,
1864
+
1865
+ const directNodeSighash = newDirectNodeTx
1866
+ ? getSigHashFromTx(newDirectNodeTx, 0, nodeTx.getOutput(0))
1867
+ : undefined;
1868
+
1869
+ const cpfpRefundSighash = getSigHashFromTx(
1870
+ newCpfpRefundTx,
1534
1871
  0,
1535
1872
  newNodeTx.getOutput(0),
1536
1873
  );
1874
+ const directRefundSighash =
1875
+ newDirectNodeTx && newDirectRefundTx
1876
+ ? getSigHashFromTx(newDirectRefundTx, 0, newDirectNodeTx.getOutput(0))
1877
+ : undefined;
1878
+
1879
+ const directFromCpfpRefundSighash = newDirectFromCpfpRefundTx
1880
+ ? getSigHashFromTx(newDirectFromCpfpRefundTx, 0, newNodeTx.getOutput(0))
1881
+ : undefined;
1537
1882
 
1538
1883
  const newNodeSigningJob = {
1539
- signingPublicKey: signingPubKey,
1884
+ signingPublicKey,
1540
1885
  rawTx: newNodeTx.toBytes(),
1541
1886
  signingNonceCommitment:
1542
1887
  await this.config.signer.getRandomSigningCommitment(),
1543
1888
  };
1544
1889
 
1545
- const newRefundSigningJob = {
1546
- signingPublicKey: signingPubKey,
1547
- rawTx: newRefundTx.toBytes(),
1890
+ const newDirectNodeSigningJob: SigningJobWithOptionalNonce | undefined =
1891
+ newDirectNodeTx
1892
+ ? {
1893
+ signingPublicKey,
1894
+ rawTx: newDirectNodeTx.toBytes(),
1895
+ signingNonceCommitment:
1896
+ await this.config.signer.getRandomSigningCommitment(),
1897
+ }
1898
+ : undefined;
1899
+
1900
+ const newCpfpRefundSigningJob = {
1901
+ signingPublicKey,
1902
+ rawTx: newCpfpRefundTx.toBytes(),
1548
1903
  signingNonceCommitment:
1549
1904
  await this.config.signer.getRandomSigningCommitment(),
1550
1905
  };
1551
1906
 
1907
+ const newDirectRefundSigningJob: SigningJobWithOptionalNonce | undefined =
1908
+ newDirectRefundTx
1909
+ ? {
1910
+ signingPublicKey,
1911
+ rawTx: newDirectRefundTx.toBytes(),
1912
+ signingNonceCommitment:
1913
+ await this.config.signer.getRandomSigningCommitment(),
1914
+ }
1915
+ : undefined;
1916
+
1917
+ const newDirectFromCpfpRefundSigningJob:
1918
+ | SigningJobWithOptionalNonce
1919
+ | undefined = newDirectFromCpfpRefundTx
1920
+ ? {
1921
+ signingPublicKey,
1922
+ rawTx: newDirectFromCpfpRefundTx.toBytes(),
1923
+ signingNonceCommitment:
1924
+ await this.config.signer.getRandomSigningCommitment(),
1925
+ }
1926
+ : undefined;
1927
+
1552
1928
  const sparkClient = await this.connectionManager.createSparkClient(
1553
1929
  this.config.getCoordinatorAddress(),
1554
1930
  );
1555
1931
 
1556
- const response = await sparkClient.extend_leaf({
1932
+ const response = await sparkClient.extend_leaf_v2({
1557
1933
  leafId: node.id,
1558
1934
  ownerIdentityPublicKey: await this.config.signer.getIdentityPublicKey(),
1559
1935
  nodeTxSigningJob: getSigningJobProto(newNodeSigningJob),
1560
- refundTxSigningJob: getSigningJobProto(newRefundSigningJob),
1936
+ directNodeTxSigningJob: newDirectNodeSigningJob
1937
+ ? getSigningJobProto(newDirectNodeSigningJob)
1938
+ : undefined,
1939
+ refundTxSigningJob: getSigningJobProto(newCpfpRefundSigningJob),
1940
+ directRefundTxSigningJob: newDirectRefundSigningJob
1941
+ ? getSigningJobProto(newDirectRefundSigningJob)
1942
+ : undefined,
1943
+ directFromCpfpRefundTxSigningJob: newDirectFromCpfpRefundSigningJob
1944
+ ? getSigningJobProto(newDirectFromCpfpRefundSigningJob)
1945
+ : undefined,
1561
1946
  });
1562
1947
 
1563
1948
  if (!response.nodeTxSigningResult || !response.refundTxSigningResult) {
1564
1949
  throw new Error("Signing result does not exist");
1565
1950
  }
1566
1951
 
1952
+ const keyDerivation: KeyDerivation = {
1953
+ type: KeyDerivationType.LEAF,
1954
+ path: node.id,
1955
+ };
1956
+
1957
+ // Sign CPFP node transaction
1567
1958
  const nodeUserSig = await this.config.signer.signFrost({
1568
1959
  message: nodeSighash,
1569
- keyDerivation: {
1570
- type: KeyDerivationType.LEAF,
1571
- path: node.id,
1572
- },
1573
- publicKey: signingPubKey,
1960
+ keyDerivation,
1961
+ publicKey: signingPublicKey,
1574
1962
  verifyingKey: response.nodeTxSigningResult.verifyingKey,
1575
1963
  selfCommitment: newNodeSigningJob.signingNonceCommitment,
1576
1964
  statechainCommitments:
@@ -1578,20 +1966,7 @@ export class TransferService extends BaseTransferService {
1578
1966
  adaptorPubKey: new Uint8Array(),
1579
1967
  });
1580
1968
 
1581
- const refundUserSig = await this.config.signer.signFrost({
1582
- message: refundSighash,
1583
- keyDerivation: {
1584
- type: KeyDerivationType.LEAF,
1585
- path: node.id,
1586
- },
1587
- publicKey: signingPubKey,
1588
- verifyingKey: response.refundTxSigningResult.verifyingKey,
1589
- selfCommitment: newRefundSigningJob.signingNonceCommitment,
1590
- statechainCommitments:
1591
- response.refundTxSigningResult.signingResult?.signingNonceCommitments,
1592
- adaptorPubKey: new Uint8Array(),
1593
- });
1594
-
1969
+ // Aggregate CPFP node signature
1595
1970
  const nodeSig = await this.config.signer.aggregateFrost({
1596
1971
  message: nodeSighash,
1597
1972
  statechainSignatures:
@@ -1602,13 +1977,63 @@ export class TransferService extends BaseTransferService {
1602
1977
  statechainCommitments:
1603
1978
  response.nodeTxSigningResult.signingResult?.signingNonceCommitments,
1604
1979
  selfCommitment: newNodeSigningJob.signingNonceCommitment,
1605
- publicKey: signingPubKey,
1980
+ publicKey: signingPublicKey,
1606
1981
  selfSignature: nodeUserSig,
1607
1982
  adaptorPubKey: new Uint8Array(),
1608
1983
  });
1609
1984
 
1610
- const refundSig = await this.config.signer.aggregateFrost({
1611
- message: refundSighash,
1985
+ let directNodeSig: Uint8Array | undefined;
1986
+ if (
1987
+ directNodeSighash &&
1988
+ newDirectNodeSigningJob &&
1989
+ response.directNodeTxSigningResult
1990
+ ) {
1991
+ // Sign direct node transaction
1992
+ const directNodeUserSig = await this.config.signer.signFrost({
1993
+ message: directNodeSighash,
1994
+ keyDerivation,
1995
+ publicKey: signingPublicKey,
1996
+ verifyingKey: response.directNodeTxSigningResult.verifyingKey,
1997
+ selfCommitment: newDirectNodeSigningJob.signingNonceCommitment,
1998
+ statechainCommitments:
1999
+ response.directNodeTxSigningResult.signingResult
2000
+ ?.signingNonceCommitments,
2001
+ adaptorPubKey: new Uint8Array(),
2002
+ });
2003
+
2004
+ // Aggregate direct node signature
2005
+ directNodeSig = await this.config.signer.aggregateFrost({
2006
+ message: directNodeSighash,
2007
+ statechainSignatures:
2008
+ response.directNodeTxSigningResult.signingResult?.signatureShares,
2009
+ statechainPublicKeys:
2010
+ response.directNodeTxSigningResult.signingResult?.publicKeys,
2011
+ verifyingKey: response.directNodeTxSigningResult.verifyingKey,
2012
+ statechainCommitments:
2013
+ response.directNodeTxSigningResult.signingResult
2014
+ ?.signingNonceCommitments,
2015
+ selfCommitment: newDirectNodeSigningJob.signingNonceCommitment,
2016
+ publicKey: signingPublicKey,
2017
+ selfSignature: directNodeUserSig,
2018
+ adaptorPubKey: new Uint8Array(),
2019
+ });
2020
+ }
2021
+
2022
+ // Sign CPFP refund transaction
2023
+ const cpfpRefundUserSig = await this.config.signer.signFrost({
2024
+ message: cpfpRefundSighash,
2025
+ keyDerivation,
2026
+ publicKey: signingPublicKey,
2027
+ verifyingKey: response.refundTxSigningResult.verifyingKey,
2028
+ selfCommitment: newCpfpRefundSigningJob.signingNonceCommitment,
2029
+ statechainCommitments:
2030
+ response.refundTxSigningResult.signingResult?.signingNonceCommitments,
2031
+ adaptorPubKey: new Uint8Array(),
2032
+ });
2033
+
2034
+ // Aggregate CPFP refund signature
2035
+ const cpfpRefundSig = await this.config.signer.aggregateFrost({
2036
+ message: cpfpRefundSighash,
1612
2037
  statechainSignatures:
1613
2038
  response.refundTxSigningResult.signingResult?.signatureShares,
1614
2039
  statechainPublicKeys:
@@ -1616,137 +2041,278 @@ export class TransferService extends BaseTransferService {
1616
2041
  verifyingKey: response.refundTxSigningResult.verifyingKey,
1617
2042
  statechainCommitments:
1618
2043
  response.refundTxSigningResult.signingResult?.signingNonceCommitments,
1619
- selfCommitment: newRefundSigningJob.signingNonceCommitment,
1620
- publicKey: signingPubKey,
1621
- selfSignature: refundUserSig,
2044
+ selfCommitment: newCpfpRefundSigningJob.signingNonceCommitment,
2045
+ publicKey: signingPublicKey,
2046
+ selfSignature: cpfpRefundUserSig,
1622
2047
  adaptorPubKey: new Uint8Array(),
1623
2048
  });
1624
2049
 
1625
- return await sparkClient.finalize_node_signatures({
2050
+ let directRefundSig: Uint8Array | undefined;
2051
+ if (
2052
+ directRefundSighash &&
2053
+ newDirectRefundSigningJob &&
2054
+ response.directRefundTxSigningResult
2055
+ ) {
2056
+ // Sign direct refund transaction
2057
+ const directRefundUserSig = await this.config.signer.signFrost({
2058
+ message: directRefundSighash,
2059
+ keyDerivation,
2060
+ publicKey: signingPublicKey,
2061
+ verifyingKey: response.directRefundTxSigningResult.verifyingKey,
2062
+ selfCommitment: newDirectRefundSigningJob.signingNonceCommitment,
2063
+ statechainCommitments:
2064
+ response.directRefundTxSigningResult.signingResult
2065
+ ?.signingNonceCommitments,
2066
+ adaptorPubKey: new Uint8Array(),
2067
+ });
2068
+
2069
+ // Aggregate direct refund signature
2070
+ directRefundSig = await this.config.signer.aggregateFrost({
2071
+ message: directRefundSighash,
2072
+ statechainSignatures:
2073
+ response.directRefundTxSigningResult.signingResult?.signatureShares,
2074
+ statechainPublicKeys:
2075
+ response.directRefundTxSigningResult.signingResult?.publicKeys,
2076
+ verifyingKey: response.directRefundTxSigningResult.verifyingKey,
2077
+ statechainCommitments:
2078
+ response.directRefundTxSigningResult.signingResult
2079
+ ?.signingNonceCommitments,
2080
+ selfCommitment: newDirectRefundSigningJob.signingNonceCommitment,
2081
+ publicKey: signingPublicKey,
2082
+ selfSignature: directRefundUserSig,
2083
+ adaptorPubKey: new Uint8Array(),
2084
+ });
2085
+ }
2086
+
2087
+ let directFromCpfpRefundSig: Uint8Array | undefined;
2088
+ if (
2089
+ directFromCpfpRefundSighash &&
2090
+ newDirectFromCpfpRefundSigningJob &&
2091
+ response.directFromCpfpRefundTxSigningResult
2092
+ ) {
2093
+ // Sign direct from CPFP refund transaction
2094
+ const directFromCpfpRefundUserSig = await this.config.signer.signFrost({
2095
+ message: directFromCpfpRefundSighash,
2096
+ keyDerivation,
2097
+ publicKey: signingPublicKey,
2098
+ verifyingKey: response.directFromCpfpRefundTxSigningResult.verifyingKey,
2099
+ selfCommitment:
2100
+ newDirectFromCpfpRefundSigningJob.signingNonceCommitment,
2101
+ statechainCommitments:
2102
+ response.directFromCpfpRefundTxSigningResult.signingResult
2103
+ ?.signingNonceCommitments,
2104
+ adaptorPubKey: new Uint8Array(),
2105
+ });
2106
+
2107
+ // Aggregate direct from CPFP refund signature
2108
+ directFromCpfpRefundSig = await this.config.signer.aggregateFrost({
2109
+ message: directFromCpfpRefundSighash,
2110
+ statechainSignatures:
2111
+ response.directFromCpfpRefundTxSigningResult.signingResult
2112
+ ?.signatureShares,
2113
+ statechainPublicKeys:
2114
+ response.directFromCpfpRefundTxSigningResult.signingResult
2115
+ ?.publicKeys,
2116
+ verifyingKey: response.directFromCpfpRefundTxSigningResult.verifyingKey,
2117
+ statechainCommitments:
2118
+ response.directFromCpfpRefundTxSigningResult.signingResult
2119
+ ?.signingNonceCommitments,
2120
+ selfCommitment:
2121
+ newDirectFromCpfpRefundSigningJob.signingNonceCommitment,
2122
+ publicKey: signingPublicKey,
2123
+ selfSignature: directFromCpfpRefundUserSig,
2124
+ adaptorPubKey: new Uint8Array(),
2125
+ });
2126
+ }
2127
+
2128
+ return await sparkClient.finalize_node_signatures_v2({
1626
2129
  intent: SignatureIntent.EXTEND,
1627
2130
  nodeSignatures: [
1628
2131
  {
1629
2132
  nodeId: response.leafId,
1630
2133
  nodeTxSignature: nodeSig,
1631
- refundTxSignature: refundSig,
2134
+ directNodeTxSignature: directNodeSig,
2135
+ refundTxSignature: cpfpRefundSig,
2136
+ directRefundTxSignature: directRefundSig,
2137
+ directFromCpfpRefundTxSignature: directFromCpfpRefundSig,
1632
2138
  },
1633
2139
  ],
1634
2140
  });
1635
2141
  }
1636
2142
 
1637
- async refreshTimelockRefundTx(node: TreeNode) {
2143
+ async testonly_expireTimeLockNodeTx(node: TreeNode, parentNode: TreeNode) {
2144
+ return await this.refreshTimelockNodesInternal(node, parentNode, true);
2145
+ }
2146
+
2147
+ async testonly_expireTimeLockRefundtx(node: TreeNode) {
1638
2148
  const nodeTx = getTxFromRawTxBytes(node.nodeTx);
1639
- const refundTx = getTxFromRawTxBytes(node.refundTx);
2149
+ const directNodeTx =
2150
+ node.directTx.length > 0 ? getTxFromRawTxBytes(node.directTx) : undefined;
2151
+
2152
+ const cpfpRefundTx = getTxFromRawTxBytes(node.refundTx);
2153
+
2154
+ const currSequence = cpfpRefundTx.getInput(0).sequence || 0;
2155
+ const currTimelock = getCurrentTimelock(currSequence);
2156
+ if (currTimelock <= 100) {
2157
+ throw new ValidationError("Cannot expire timelock below 100", {
2158
+ field: "currTimelock",
2159
+ value: currTimelock,
2160
+ expected: "Timelock greater than 100",
2161
+ });
2162
+ }
2163
+ const nextSequence = TEST_UNILATERAL_SEQUENCE;
2164
+ const nextDirectSequence =
2165
+ TEST_UNILATERAL_SEQUENCE + DIRECT_TIMELOCK_OFFSET;
1640
2166
 
1641
- const currSequence = refundTx.getInput(0).sequence || 0;
1642
- const { nextSequence } = getNextTransactionSequence(currSequence);
2167
+ const nodeOutput = nodeTx.getOutput(0);
2168
+ if (!nodeOutput) {
2169
+ throw Error("Could not get node output");
2170
+ }
1643
2171
 
1644
- const signingPubKey = await this.config.signer.getPublicKeyFromDerivation({
2172
+ const keyDerivation: KeyDerivation = {
1645
2173
  type: KeyDerivationType.LEAF,
1646
2174
  path: node.id,
1647
- });
1648
-
1649
- const newRefundTx = new Transaction({
1650
- version: 3,
1651
- allowUnknownOutputs: true,
1652
- });
1653
-
1654
- // Apply fee to the refund output
1655
- const originalRefundOutput = refundTx.getOutput(0);
1656
- if (!originalRefundOutput) {
1657
- throw Error("Could not get original refund output");
1658
- }
1659
-
1660
- newRefundTx.addOutput({
1661
- script: originalRefundOutput.script!,
1662
- amount: originalRefundOutput.amount!,
1663
- });
2175
+ };
2176
+ const signingPublicKey =
2177
+ await this.config.signer.getPublicKeyFromDerivation(keyDerivation);
1664
2178
 
1665
- // Copy any additional outputs (like ephemeral anchors) without fee reduction
1666
- for (let j = 1; j < refundTx.outputsLength; j++) {
1667
- const additionalOutput = refundTx.getOutput(j);
1668
- if (additionalOutput) {
1669
- newRefundTx.addOutput(additionalOutput);
1670
- }
1671
- }
2179
+ const cpfpRefundOutPoint: TransactionInput = {
2180
+ txid: hexToBytes(getTxId(nodeTx)!),
2181
+ index: 0,
2182
+ };
1672
2183
 
1673
- const refundTxInput = refundTx.getInput(0);
1674
- if (!refundTxInput) {
1675
- throw Error("refund tx doesn't have input");
2184
+ let directRefundOutPoint: TransactionInput | undefined;
2185
+ if (directNodeTx) {
2186
+ directRefundOutPoint = {
2187
+ txid: hexToBytes(getTxId(directNodeTx)!),
2188
+ index: 0,
2189
+ };
1676
2190
  }
1677
2191
 
1678
- newRefundTx.addInput({
1679
- ...refundTxInput,
2192
+ const {
2193
+ cpfpRefundTx: newCpfpRefundTx,
2194
+ directRefundTx: newDirectRefundTx,
2195
+ directFromCpfpRefundTx: newDirectFromCpfpRefundTx,
2196
+ } = createRefundTxs({
1680
2197
  sequence: nextSequence,
2198
+ directSequence: nextDirectSequence,
2199
+ input: cpfpRefundOutPoint,
2200
+ directInput: directRefundOutPoint,
2201
+ amountSats: nodeOutput.amount!,
2202
+ receivingPubkey: signingPublicKey,
2203
+ network: this.config.getNetwork(),
1681
2204
  });
1682
2205
 
1683
- const refundSigningJob = {
1684
- signingPublicKey: signingPubKey,
1685
- rawTx: newRefundTx.toBytes(),
2206
+ const signingJobs: (SigningJobWithOptionalNonce & {
2207
+ type: "cpfp" | "direct" | "directFromCpfp";
2208
+ parentTxOut: TransactionOutput;
2209
+ })[] = [];
2210
+
2211
+ signingJobs.push({
2212
+ signingPublicKey,
2213
+ rawTx: newCpfpRefundTx.toBytes(),
1686
2214
  signingNonceCommitment:
1687
2215
  await this.config.signer.getRandomSigningCommitment(),
1688
- };
2216
+ type: "cpfp",
2217
+ parentTxOut: nodeOutput,
2218
+ });
2219
+
2220
+ const directNodeTxOut = directNodeTx?.getOutput(0);
2221
+ if (newDirectRefundTx && directNodeTxOut) {
2222
+ signingJobs.push({
2223
+ signingPublicKey,
2224
+ rawTx: newDirectRefundTx.toBytes(),
2225
+ signingNonceCommitment:
2226
+ await this.config.signer.getRandomSigningCommitment(),
2227
+ type: "direct",
2228
+ parentTxOut: directNodeTxOut,
2229
+ });
2230
+ }
2231
+
2232
+ if (newDirectFromCpfpRefundTx) {
2233
+ signingJobs.push({
2234
+ signingPublicKey,
2235
+ rawTx: newDirectFromCpfpRefundTx.toBytes(),
2236
+ signingNonceCommitment:
2237
+ await this.config.signer.getRandomSigningCommitment(),
2238
+ type: "directFromCpfp",
2239
+ parentTxOut: nodeOutput,
2240
+ });
2241
+ }
1689
2242
 
1690
2243
  const sparkClient = await this.connectionManager.createSparkClient(
1691
2244
  this.config.getCoordinatorAddress(),
1692
2245
  );
1693
2246
 
1694
- const response = await sparkClient.refresh_timelock({
2247
+ const response = await sparkClient.refresh_timelock_v2({
1695
2248
  leafId: node.id,
1696
2249
  ownerIdentityPublicKey: await this.config.signer.getIdentityPublicKey(),
1697
- signingJobs: [getSigningJobProto(refundSigningJob)],
2250
+ signingJobs: signingJobs.map(getSigningJobProto),
1698
2251
  });
1699
2252
 
1700
- if (response.signingResults.length !== 1) {
2253
+ if (response.signingResults.length !== signingJobs.length) {
1701
2254
  throw Error(
1702
- `Expected 1 signing result, got ${response.signingResults.length}`,
2255
+ `Expected ${signingJobs.length} signing results, got ${response.signingResults.length}`,
1703
2256
  );
1704
2257
  }
1705
2258
 
1706
- const signingResult = response.signingResults[0];
1707
- if (!signingResult || !refundSigningJob.signingNonceCommitment) {
1708
- throw Error("Signing result or nonce commitment does not exist");
1709
- }
2259
+ let cpfpRefundSignature: Uint8Array | undefined;
2260
+ let directRefundSignature: Uint8Array | undefined;
2261
+ let directFromCpfpRefundSignature: Uint8Array | undefined;
1710
2262
 
1711
- const rawTx = getTxFromRawTxBytes(refundSigningJob.rawTx);
1712
- const txOut = nodeTx.getOutput(0);
2263
+ for (const [i, signingJob] of signingJobs.entries()) {
2264
+ const signingResult = response.signingResults[i];
2265
+ if (!signingResult) {
2266
+ throw Error("Signing result does not exist");
2267
+ }
1713
2268
 
1714
- const rawTxSighash = getSigHashFromTx(rawTx, 0, txOut);
2269
+ const rawTx = getTxFromRawTxBytes(signingJob.rawTx);
2270
+ const txOut = signingJob.parentTxOut;
2271
+ const rawTxSighash = getSigHashFromTx(rawTx, 0, txOut);
1715
2272
 
1716
- const userSignature = await this.config.signer.signFrost({
1717
- message: rawTxSighash,
1718
- keyDerivation: {
1719
- type: KeyDerivationType.LEAF,
1720
- path: node.id,
1721
- },
1722
- publicKey: signingPubKey,
1723
- verifyingKey: signingResult.verifyingKey,
1724
- selfCommitment: refundSigningJob.signingNonceCommitment,
1725
- statechainCommitments:
1726
- signingResult.signingResult?.signingNonceCommitments,
1727
- adaptorPubKey: new Uint8Array(),
1728
- });
2273
+ const userSignature = await this.config.signer.signFrost({
2274
+ message: rawTxSighash,
2275
+ keyDerivation,
2276
+ publicKey: signingPublicKey,
2277
+ verifyingKey: signingResult.verifyingKey,
2278
+ selfCommitment: signingJob.signingNonceCommitment,
2279
+ statechainCommitments:
2280
+ signingResult.signingResult?.signingNonceCommitments,
2281
+ adaptorPubKey: new Uint8Array(),
2282
+ });
1729
2283
 
1730
- const signature = await this.config.signer.aggregateFrost({
1731
- message: rawTxSighash,
1732
- statechainSignatures: signingResult.signingResult?.signatureShares,
1733
- statechainPublicKeys: signingResult.signingResult?.publicKeys,
1734
- verifyingKey: signingResult.verifyingKey,
1735
- statechainCommitments:
1736
- signingResult.signingResult?.signingNonceCommitments,
1737
- selfCommitment: refundSigningJob.signingNonceCommitment,
1738
- publicKey: signingPubKey,
1739
- selfSignature: userSignature,
1740
- adaptorPubKey: new Uint8Array(),
1741
- });
2284
+ const signature = await this.config.signer.aggregateFrost({
2285
+ message: rawTxSighash,
2286
+ statechainSignatures: signingResult.signingResult?.signatureShares,
2287
+ statechainPublicKeys: signingResult.signingResult?.publicKeys,
2288
+ verifyingKey: signingResult.verifyingKey,
2289
+ statechainCommitments:
2290
+ signingResult.signingResult?.signingNonceCommitments,
2291
+ selfCommitment: signingJob.signingNonceCommitment,
2292
+ publicKey: signingPublicKey,
2293
+ selfSignature: userSignature,
2294
+ adaptorPubKey: new Uint8Array(),
2295
+ });
1742
2296
 
1743
- const result = await sparkClient.finalize_node_signatures({
2297
+ if (signingJob.type === "cpfp") {
2298
+ cpfpRefundSignature = signature;
2299
+ } else if (signingJob.type === "direct") {
2300
+ directRefundSignature = signature;
2301
+ } else if (signingJob.type === "directFromCpfp") {
2302
+ directFromCpfpRefundSignature = signature;
2303
+ }
2304
+ }
2305
+
2306
+ const result = await sparkClient.finalize_node_signatures_v2({
1744
2307
  intent: SignatureIntent.REFRESH,
1745
2308
  nodeSignatures: [
1746
2309
  {
1747
2310
  nodeId: node.id,
1748
2311
  nodeTxSignature: new Uint8Array(),
1749
- refundTxSignature: signature,
2312
+ directNodeTxSignature: new Uint8Array(),
2313
+ refundTxSignature: cpfpRefundSignature,
2314
+ directRefundTxSignature: directRefundSignature,
2315
+ directFromCpfpRefundTxSignature: directFromCpfpRefundSignature,
1750
2316
  },
1751
2317
  ],
1752
2318
  });