@buildonspark/spark-sdk 0.0.15 → 0.0.17

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 (136) hide show
  1. package/dist/services/config.d.ts +1 -0
  2. package/dist/services/config.js +4 -1
  3. package/dist/services/config.js.map +1 -1
  4. package/dist/services/wallet-config.d.ts +2 -0
  5. package/dist/services/wallet-config.js +2 -0
  6. package/dist/services/wallet-config.js.map +1 -1
  7. package/dist/signer/signer.js +4 -3
  8. package/dist/signer/signer.js.map +1 -1
  9. package/dist/spark-sdk.d.ts +4 -4
  10. package/dist/spark-sdk.js +26 -10
  11. package/dist/spark-sdk.js.map +1 -1
  12. package/package.json +4 -3
  13. package/src/examples/example.js +247 -0
  14. package/src/examples/example.ts +207 -0
  15. package/src/graphql/client.ts +282 -0
  16. package/src/graphql/mutations/CompleteCoopExit.ts +19 -0
  17. package/src/graphql/mutations/CompleteLeavesSwap.ts +17 -0
  18. package/src/graphql/mutations/RequestCoopExit.ts +20 -0
  19. package/src/graphql/mutations/RequestLightningReceive.ts +26 -0
  20. package/src/graphql/mutations/RequestLightningSend.ts +17 -0
  21. package/src/graphql/mutations/RequestSwapLeaves.ts +24 -0
  22. package/src/graphql/objects/BitcoinNetwork.ts +22 -0
  23. package/src/graphql/objects/CompleteCoopExitInput.ts +41 -0
  24. package/src/graphql/objects/CompleteCoopExitOutput.ts +45 -0
  25. package/src/graphql/objects/CompleteLeavesSwapInput.ts +45 -0
  26. package/src/graphql/objects/CompleteLeavesSwapOutput.ts +45 -0
  27. package/src/graphql/objects/CompleteSeedReleaseInput.ts +41 -0
  28. package/src/graphql/objects/CompleteSeedReleaseOutput.ts +43 -0
  29. package/src/graphql/objects/Connection.ts +90 -0
  30. package/src/graphql/objects/CoopExitFeeEstimateInput.ts +41 -0
  31. package/src/graphql/objects/CoopExitFeeEstimateOutput.ts +52 -0
  32. package/src/graphql/objects/CoopExitRequest.ts +118 -0
  33. package/src/graphql/objects/CurrencyAmount.ts +74 -0
  34. package/src/graphql/objects/CurrencyUnit.ts +32 -0
  35. package/src/graphql/objects/Entity.ts +202 -0
  36. package/src/graphql/objects/GetChallengeInput.ts +37 -0
  37. package/src/graphql/objects/GetChallengeOutput.ts +43 -0
  38. package/src/graphql/objects/Invoice.ts +83 -0
  39. package/src/graphql/objects/Leaf.ts +59 -0
  40. package/src/graphql/objects/LeavesSwapFeeEstimateInput.ts +37 -0
  41. package/src/graphql/objects/LeavesSwapFeeEstimateOutput.ts +52 -0
  42. package/src/graphql/objects/LeavesSwapRequest.ts +192 -0
  43. package/src/graphql/objects/LightningReceiveFeeEstimateInput.ts +41 -0
  44. package/src/graphql/objects/LightningReceiveFeeEstimateOutput.ts +52 -0
  45. package/src/graphql/objects/LightningReceiveRequest.ts +147 -0
  46. package/src/graphql/objects/LightningReceiveRequestStatus.ts +34 -0
  47. package/src/graphql/objects/LightningSendFeeEstimateInput.ts +37 -0
  48. package/src/graphql/objects/LightningSendFeeEstimateOutput.ts +52 -0
  49. package/src/graphql/objects/LightningSendRequest.ts +134 -0
  50. package/src/graphql/objects/LightningSendRequestStatus.ts +28 -0
  51. package/src/graphql/objects/NotifyReceiverTransferInput.ts +41 -0
  52. package/src/graphql/objects/PageInfo.ts +58 -0
  53. package/src/graphql/objects/Provider.ts +41 -0
  54. package/src/graphql/objects/RequestCoopExitInput.ts +41 -0
  55. package/src/graphql/objects/RequestCoopExitOutput.ts +45 -0
  56. package/src/graphql/objects/RequestLeavesSwapInput.ts +55 -0
  57. package/src/graphql/objects/RequestLeavesSwapOutput.ts +45 -0
  58. package/src/graphql/objects/RequestLightningReceiveInput.ts +58 -0
  59. package/src/graphql/objects/RequestLightningReceiveOutput.ts +45 -0
  60. package/src/graphql/objects/RequestLightningSendInput.ts +41 -0
  61. package/src/graphql/objects/RequestLightningSendOutput.ts +45 -0
  62. package/src/graphql/objects/SparkCoopExitRequestStatus.ts +20 -0
  63. package/src/graphql/objects/SparkLeavesSwapRequestStatus.ts +20 -0
  64. package/src/graphql/objects/SparkTransferToLeavesConnection.ts +79 -0
  65. package/src/graphql/objects/SparkWalletUser.ts +86 -0
  66. package/src/graphql/objects/StartSeedReleaseInput.ts +37 -0
  67. package/src/graphql/objects/SwapLeaf.ts +53 -0
  68. package/src/graphql/objects/Transfer.ts +98 -0
  69. package/src/graphql/objects/UserLeafInput.ts +28 -0
  70. package/src/graphql/objects/VerifyChallengeInput.ts +51 -0
  71. package/src/graphql/objects/VerifyChallengeOutput.ts +43 -0
  72. package/src/graphql/objects/WalletUserIdentityPublicKeyInput.ts +37 -0
  73. package/src/graphql/objects/WalletUserIdentityPublicKeyOutput.ts +43 -0
  74. package/src/graphql/objects/index.ts +67 -0
  75. package/src/graphql/queries/CoopExitFeeEstimate.ts +18 -0
  76. package/src/graphql/queries/CurrentUser.ts +10 -0
  77. package/src/graphql/queries/LightningReceiveFeeEstimate.ts +18 -0
  78. package/src/graphql/queries/LightningSendFeeEstimate.ts +16 -0
  79. package/src/proto/common.ts +431 -0
  80. package/src/proto/google/protobuf/descriptor.ts +6625 -0
  81. package/src/proto/google/protobuf/duration.ts +197 -0
  82. package/src/proto/google/protobuf/empty.ts +83 -0
  83. package/src/proto/google/protobuf/timestamp.ts +226 -0
  84. package/src/proto/mock.ts +151 -0
  85. package/src/proto/spark.ts +12727 -0
  86. package/src/proto/spark_authn.ts +673 -0
  87. package/src/proto/validate/validate.ts +6047 -0
  88. package/src/services/config.ts +75 -0
  89. package/src/services/connection.ts +264 -0
  90. package/src/services/coop-exit.ts +190 -0
  91. package/src/services/deposit.ts +327 -0
  92. package/src/services/lightning.ts +341 -0
  93. package/src/services/lrc20.ts +42 -0
  94. package/src/services/token-transactions.ts +499 -0
  95. package/src/services/transfer.ts +1188 -0
  96. package/src/services/tree-creation.ts +618 -0
  97. package/src/services/wallet-config.ts +143 -0
  98. package/src/signer/signer.ts +532 -0
  99. package/src/spark-sdk.ts +1665 -0
  100. package/src/tests/adaptor-signature.test.ts +64 -0
  101. package/src/tests/bitcoin.test.ts +122 -0
  102. package/src/tests/coop-exit.test.ts +233 -0
  103. package/src/tests/deposit.test.ts +98 -0
  104. package/src/tests/keys.test.ts +82 -0
  105. package/src/tests/lightning.test.ts +307 -0
  106. package/src/tests/secret-sharing.test.ts +63 -0
  107. package/src/tests/swap.test.ts +252 -0
  108. package/src/tests/test-util.ts +92 -0
  109. package/src/tests/tokens.test.ts +47 -0
  110. package/src/tests/transfer.test.ts +371 -0
  111. package/src/tests/tree-creation.test.ts +56 -0
  112. package/src/tests/utils/spark-testing-wallet.ts +37 -0
  113. package/src/tests/utils/test-faucet.ts +257 -0
  114. package/src/types/grpc.ts +8 -0
  115. package/src/types/index.ts +3 -0
  116. package/src/utils/adaptor-signature.ts +189 -0
  117. package/src/utils/bitcoin.ts +138 -0
  118. package/src/utils/crypto.ts +14 -0
  119. package/src/utils/index.ts +12 -0
  120. package/src/utils/keys.ts +92 -0
  121. package/src/utils/mempool.ts +42 -0
  122. package/src/utils/network.ts +70 -0
  123. package/src/utils/proof.ts +17 -0
  124. package/src/utils/response-validation.ts +26 -0
  125. package/src/utils/secret-sharing.ts +263 -0
  126. package/src/utils/signing.ts +96 -0
  127. package/src/utils/token-hashing.ts +163 -0
  128. package/src/utils/token-keyshares.ts +31 -0
  129. package/src/utils/token-transactions.ts +71 -0
  130. package/src/utils/transaction.ts +45 -0
  131. package/src/utils/wasm-wrapper.ts +57 -0
  132. package/src/utils/wasm.ts +154 -0
  133. package/src/wasm/spark_bindings.d.ts +208 -0
  134. package/src/wasm/spark_bindings.js +1161 -0
  135. package/src/wasm/spark_bindings_bg.wasm +0 -0
  136. package/src/wasm/spark_bindings_bg.wasm.d.ts +136 -0
@@ -0,0 +1,1188 @@
1
+ import {
2
+ bytesToHex,
3
+ equalBytes,
4
+ hexToBytes,
5
+ numberToBytesBE,
6
+ } from "@noble/curves/abstract/utils";
7
+ import { secp256k1 } from "@noble/curves/secp256k1";
8
+ import { Transaction } from "@scure/btc-signer";
9
+ import { TransactionInput } from "@scure/btc-signer/psbt";
10
+ import { sha256 } from "@scure/btc-signer/utils";
11
+ import * as ecies from "eciesjs";
12
+ import { SignatureIntent } from "../proto/common.js";
13
+ import {
14
+ ClaimLeafKeyTweak,
15
+ ClaimTransferSignRefundsResponse,
16
+ CompleteSendTransferResponse,
17
+ LeafRefundTxSigningJob,
18
+ LeafRefundTxSigningResult,
19
+ LeafSwapResponse,
20
+ NodeSignatures,
21
+ QueryAllTransfersResponse,
22
+ QueryPendingTransfersResponse,
23
+ SendLeafKeyTweak,
24
+ SigningJob,
25
+ StartSendTransferResponse,
26
+ Transfer,
27
+ TransferStatus,
28
+ TreeNode,
29
+ } from "../proto/spark.js";
30
+ import { SigningCommitment } from "../signer/signer.js";
31
+ import {
32
+ getSigHashFromTx,
33
+ getTxFromRawTxBytes,
34
+ getTxId,
35
+ } from "../utils/bitcoin.js";
36
+ import { getCrypto } from "../utils/crypto.js";
37
+ import { VerifiableSecretShare } from "../utils/secret-sharing.js";
38
+ import {
39
+ createRefundTx,
40
+ getEphemeralAnchorOutput,
41
+ getNextTransactionSequence,
42
+ } from "../utils/transaction.js";
43
+ import { WalletConfigService } from "./config.js";
44
+ import { ConnectionManager } from "./connection.js";
45
+ const INITIAL_TIME_LOCK = 2000;
46
+
47
+ function initialSequence() {
48
+ return (1 << 30) | INITIAL_TIME_LOCK;
49
+ }
50
+
51
+ const crypto = getCrypto();
52
+
53
+ export type LeafKeyTweak = {
54
+ leaf: TreeNode;
55
+ signingPubKey: Uint8Array;
56
+ newSigningPubKey: Uint8Array;
57
+ };
58
+
59
+ export type ClaimLeafData = {
60
+ signingPubKey: Uint8Array;
61
+ tx?: Transaction;
62
+ refundTx?: Transaction;
63
+ signingNonceCommitment: SigningCommitment;
64
+ vout?: number;
65
+ };
66
+
67
+ export type LeafRefundSigningData = {
68
+ signingPubKey: Uint8Array;
69
+ receivingPubkey: Uint8Array;
70
+ tx: Transaction;
71
+ refundTx?: Transaction;
72
+ signingNonceCommitment: SigningCommitment;
73
+ vout: number;
74
+ };
75
+
76
+ export class BaseTransferService {
77
+ protected readonly config: WalletConfigService;
78
+ protected readonly connectionManager: ConnectionManager;
79
+
80
+ constructor(
81
+ config: WalletConfigService,
82
+ connectionManager: ConnectionManager,
83
+ ) {
84
+ this.config = config;
85
+ this.connectionManager = connectionManager;
86
+ }
87
+
88
+ async sendTransferTweakKey(
89
+ transfer: Transfer,
90
+ leaves: LeafKeyTweak[],
91
+ refundSignatureMap: Map<string, Uint8Array>,
92
+ ): Promise<Transfer> {
93
+ const keyTweakInputMap = await this.prepareSendTransferKeyTweaks(
94
+ transfer,
95
+ leaves,
96
+ refundSignatureMap,
97
+ );
98
+
99
+ let updatedTransfer: Transfer | undefined;
100
+ const errors: Error[] = [];
101
+ const promises = Object.entries(this.config.getSigningOperators()).map(
102
+ async ([identifier, operator]) => {
103
+ const sparkClient = await this.connectionManager.createSparkClient(
104
+ operator.address,
105
+ );
106
+
107
+ const leavesToSend = keyTweakInputMap.get(identifier);
108
+ if (!leavesToSend) {
109
+ errors.push(
110
+ new Error(`No leaves to send for operator ${identifier}`),
111
+ );
112
+ return;
113
+ }
114
+ let transferResp: CompleteSendTransferResponse;
115
+ try {
116
+ transferResp = await sparkClient.complete_send_transfer({
117
+ transferId: transfer.id,
118
+ ownerIdentityPublicKey:
119
+ await this.config.signer.getIdentityPublicKey(),
120
+ leavesToSend,
121
+ });
122
+ } catch (error) {
123
+ errors.push(new Error(`Error completing send transfer: ${error}`));
124
+ return;
125
+ }
126
+
127
+ if (!updatedTransfer) {
128
+ updatedTransfer = transferResp.transfer;
129
+ } else {
130
+ if (!transferResp.transfer) {
131
+ errors.push(
132
+ new Error(`No transfer response from operator ${identifier}`),
133
+ );
134
+ return;
135
+ }
136
+
137
+ if (!this.compareTransfers(updatedTransfer, transferResp.transfer)) {
138
+ errors.push(
139
+ new Error(`Inconsistent transfer response from operators`),
140
+ );
141
+ }
142
+ }
143
+ },
144
+ );
145
+
146
+ await Promise.all(promises);
147
+
148
+ if (errors.length > 0) {
149
+ throw new Error(`Error completing send transfer: ${errors[0]}`);
150
+ }
151
+
152
+ if (!updatedTransfer) {
153
+ throw new Error("No updated transfer found");
154
+ }
155
+
156
+ return updatedTransfer;
157
+ }
158
+
159
+ async signRefunds(
160
+ leafDataMap: Map<string, ClaimLeafData>,
161
+ operatorSigningResults: LeafRefundTxSigningResult[],
162
+ adaptorPubKey?: Uint8Array,
163
+ ): Promise<NodeSignatures[]> {
164
+ const nodeSignatures: NodeSignatures[] = [];
165
+ for (const operatorSigningResult of operatorSigningResults) {
166
+ const leafData = leafDataMap.get(operatorSigningResult.leafId);
167
+ if (
168
+ !leafData ||
169
+ !leafData.tx ||
170
+ leafData.vout === undefined ||
171
+ !leafData.refundTx
172
+ ) {
173
+ throw new Error(
174
+ `Leaf data not found for leaf ${operatorSigningResult.leafId}`,
175
+ );
176
+ }
177
+
178
+ const txOutput = leafData.tx?.getOutput(0);
179
+ if (!txOutput) {
180
+ throw new Error(
181
+ `Output not found for leaf ${operatorSigningResult.leafId}`,
182
+ );
183
+ }
184
+
185
+ const refundTxSighash = getSigHashFromTx(leafData.refundTx, 0, txOutput);
186
+
187
+ const userSignature = await this.config.signer.signFrost({
188
+ message: refundTxSighash,
189
+ publicKey: leafData.signingPubKey,
190
+ privateAsPubKey: leafData.signingPubKey,
191
+ selfCommitment: leafData.signingNonceCommitment,
192
+ statechainCommitments:
193
+ operatorSigningResult.refundTxSigningResult?.signingNonceCommitments,
194
+ adaptorPubKey: adaptorPubKey,
195
+ verifyingKey: operatorSigningResult.verifyingKey,
196
+ });
197
+
198
+ const refundAggregate = await this.config.signer.aggregateFrost({
199
+ message: refundTxSighash,
200
+ statechainSignatures:
201
+ operatorSigningResult.refundTxSigningResult?.signatureShares,
202
+ statechainPublicKeys:
203
+ operatorSigningResult.refundTxSigningResult?.publicKeys,
204
+ verifyingKey: operatorSigningResult.verifyingKey,
205
+ statechainCommitments:
206
+ operatorSigningResult.refundTxSigningResult?.signingNonceCommitments,
207
+ selfCommitment: leafData.signingNonceCommitment,
208
+ publicKey: leafData.signingPubKey,
209
+ selfSignature: userSignature,
210
+ adaptorPubKey: adaptorPubKey,
211
+ });
212
+
213
+ nodeSignatures.push({
214
+ nodeId: operatorSigningResult.leafId,
215
+ refundTxSignature: refundAggregate,
216
+ nodeTxSignature: new Uint8Array(),
217
+ });
218
+ }
219
+
220
+ return nodeSignatures;
221
+ }
222
+
223
+ private async prepareSendTransferKeyTweaks(
224
+ transfer: Transfer,
225
+ leaves: LeafKeyTweak[],
226
+ refundSignatureMap: Map<string, Uint8Array>,
227
+ ): Promise<Map<string, SendLeafKeyTweak[]>> {
228
+ const receiverEciesPubKey = ecies.PublicKey.fromHex(
229
+ bytesToHex(transfer.receiverIdentityPublicKey),
230
+ );
231
+
232
+ const leavesTweaksMap = new Map<string, SendLeafKeyTweak[]>();
233
+
234
+ for (const leaf of leaves) {
235
+ const refundSignature = refundSignatureMap.get(leaf.leaf.id);
236
+ const leafTweaksMap = await this.prepareSingleSendTransferKeyTweak(
237
+ transfer.id,
238
+ leaf,
239
+ receiverEciesPubKey,
240
+ refundSignature,
241
+ );
242
+ for (const [identifier, leafTweak] of leafTweaksMap) {
243
+ leavesTweaksMap.set(identifier, [
244
+ ...(leavesTweaksMap.get(identifier) || []),
245
+ leafTweak,
246
+ ]);
247
+ }
248
+ }
249
+
250
+ return leavesTweaksMap;
251
+ }
252
+
253
+ private async prepareSingleSendTransferKeyTweak(
254
+ transferID: string,
255
+ leaf: LeafKeyTweak,
256
+ receiverEciesPubKey: ecies.PublicKey,
257
+ refundSignature?: Uint8Array,
258
+ ): Promise<Map<string, SendLeafKeyTweak>> {
259
+ const signingOperators = this.config.getSigningOperators();
260
+ const pubKeyTweak =
261
+ await this.config.signer.subtractPrivateKeysGivenPublicKeys(
262
+ leaf.signingPubKey,
263
+ leaf.newSigningPubKey,
264
+ );
265
+
266
+ const shares = await this.config.signer.splitSecretWithProofs({
267
+ secret: pubKeyTweak,
268
+ curveOrder: secp256k1.CURVE.n,
269
+ threshold: this.config.getThreshold(),
270
+ numShares: Object.keys(signingOperators).length,
271
+ isSecretPubkey: true,
272
+ });
273
+
274
+ const pubkeySharesTweak = new Map<string, Uint8Array>();
275
+ for (const [identifier, operator] of Object.entries(signingOperators)) {
276
+ const share = this.findShare(shares, operator.id);
277
+ if (!share) {
278
+ throw new Error(`Share not found for operator ${operator.id}`);
279
+ }
280
+
281
+ const pubkeyTweak = secp256k1.getPublicKey(
282
+ numberToBytesBE(share.share, 32),
283
+ true,
284
+ );
285
+ pubkeySharesTweak.set(identifier, pubkeyTweak);
286
+ }
287
+
288
+ const secretCipher = await this.config.signer.encryptLeafPrivateKeyEcies(
289
+ receiverEciesPubKey.toBytes(),
290
+ leaf.newSigningPubKey,
291
+ );
292
+
293
+ const encoder = new TextEncoder();
294
+ const payload = new Uint8Array([
295
+ ...encoder.encode(leaf.leaf.id),
296
+ ...encoder.encode(transferID),
297
+ ...secretCipher,
298
+ ]);
299
+
300
+ const payloadHash = sha256(payload);
301
+ const signature = await this.config.signer.signMessageWithIdentityKey(
302
+ payloadHash,
303
+ true,
304
+ );
305
+
306
+ const leafTweaksMap = new Map<string, SendLeafKeyTweak>();
307
+ for (const [identifier, operator] of Object.entries(signingOperators)) {
308
+ const share = this.findShare(shares, operator.id);
309
+ if (!share) {
310
+ throw new Error(`Share not found for operator ${operator.id}`);
311
+ }
312
+
313
+ leafTweaksMap.set(identifier, {
314
+ leafId: leaf.leaf.id,
315
+ secretShareTweak: {
316
+ secretShare: numberToBytesBE(share.share, 32),
317
+ proofs: share.proofs,
318
+ },
319
+ pubkeySharesTweak: Object.fromEntries(pubkeySharesTweak),
320
+ secretCipher,
321
+ signature,
322
+ refundSignature: refundSignature ?? new Uint8Array(),
323
+ });
324
+ }
325
+
326
+ return leafTweaksMap;
327
+ }
328
+
329
+ protected findShare(shares: VerifiableSecretShare[], operatorID: number) {
330
+ const targetShareIndex = BigInt(operatorID + 1);
331
+ for (const s of shares) {
332
+ if (s.index === targetShareIndex) {
333
+ return s;
334
+ }
335
+ }
336
+ return undefined;
337
+ }
338
+
339
+ private compareTransfers(transfer1: Transfer, transfer2: Transfer) {
340
+ return (
341
+ transfer1.id === transfer2.id &&
342
+ equalBytes(
343
+ transfer1.senderIdentityPublicKey,
344
+ transfer2.senderIdentityPublicKey,
345
+ ) &&
346
+ transfer1.status === transfer2.status &&
347
+ transfer1.totalValue === transfer2.totalValue &&
348
+ transfer1.expiryTime?.getTime() === transfer2.expiryTime?.getTime() &&
349
+ transfer1.leaves.length === transfer2.leaves.length
350
+ );
351
+ }
352
+ }
353
+
354
+ export class TransferService extends BaseTransferService {
355
+ constructor(
356
+ config: WalletConfigService,
357
+ connectionManager: ConnectionManager,
358
+ ) {
359
+ super(config, connectionManager);
360
+ }
361
+
362
+ async sendTransfer(
363
+ leaves: LeafKeyTweak[],
364
+ receiverIdentityPubkey: Uint8Array,
365
+ ): Promise<Transfer> {
366
+ const { transfer, signatureMap } = await this.sendTransferSignRefund(
367
+ leaves,
368
+ receiverIdentityPubkey,
369
+ );
370
+
371
+ const transferWithTweakedKeys = await this.sendTransferTweakKey(
372
+ transfer,
373
+ leaves,
374
+ signatureMap,
375
+ );
376
+
377
+ return transferWithTweakedKeys;
378
+ }
379
+
380
+ async claimTransfer(transfer: Transfer, leaves: LeafKeyTweak[]) {
381
+ if (transfer.status === TransferStatus.TRANSFER_STATUS_SENDER_KEY_TWEAKED) {
382
+ await this.claimTransferTweakKeys(transfer, leaves);
383
+ }
384
+ const signatures = await this.claimTransferSignRefunds(transfer, leaves);
385
+
386
+ return await this.finalizeTransfer(signatures);
387
+ }
388
+
389
+ async queryPendingTransfers(): Promise<QueryPendingTransfersResponse> {
390
+ const sparkClient = await this.connectionManager.createSparkClient(
391
+ this.config.getCoordinatorAddress(),
392
+ );
393
+ let pendingTransfersResp: QueryPendingTransfersResponse;
394
+ try {
395
+ pendingTransfersResp = await sparkClient.query_pending_transfers({
396
+ participant: {
397
+ $case: "receiverIdentityPublicKey",
398
+ receiverIdentityPublicKey:
399
+ await this.config.signer.getIdentityPublicKey(),
400
+ },
401
+ network: this.config.getNetworkProto(),
402
+ });
403
+ } catch (error) {
404
+ throw new Error(`Error querying pending transfers: ${error}`);
405
+ }
406
+ return pendingTransfersResp;
407
+ }
408
+
409
+ async queryAllTransfers(
410
+ limit: number,
411
+ offset: number,
412
+ ): Promise<QueryAllTransfersResponse> {
413
+ const sparkClient = await this.connectionManager.createSparkClient(
414
+ this.config.getCoordinatorAddress(),
415
+ );
416
+
417
+ let allTransfersResp: QueryAllTransfersResponse;
418
+ try {
419
+ allTransfersResp = await sparkClient.query_all_transfers({
420
+ identityPublicKey: await this.config.signer.getIdentityPublicKey(),
421
+ limit,
422
+ offset,
423
+ });
424
+ } catch (error) {
425
+ throw new Error(`Error querying all transfers: ${error}`);
426
+ }
427
+ return allTransfersResp;
428
+ }
429
+
430
+ async verifyPendingTransfer(
431
+ transfer: Transfer,
432
+ ): Promise<Map<string, Uint8Array>> {
433
+ const leafPubKeyMap = new Map<string, Uint8Array>();
434
+ for (const leaf of transfer.leaves) {
435
+ if (!leaf.leaf) {
436
+ throw new Error("Leaf is undefined");
437
+ }
438
+ const encoder = new TextEncoder();
439
+ const leafIdBytes = encoder.encode(leaf.leaf.id);
440
+ const transferIdBytes = encoder.encode(transfer.id);
441
+ const payload = new Uint8Array([
442
+ ...leafIdBytes,
443
+ ...transferIdBytes,
444
+ ...leaf.secretCipher,
445
+ ]);
446
+ const payloadHash = sha256(payload);
447
+ if (
448
+ !secp256k1.verify(
449
+ leaf.signature,
450
+ payloadHash,
451
+ transfer.senderIdentityPublicKey,
452
+ )
453
+ ) {
454
+ throw new Error("Signature verification failed");
455
+ }
456
+
457
+ const leafSecret = await this.config.signer.decryptEcies(
458
+ leaf.secretCipher,
459
+ );
460
+
461
+ leafPubKeyMap.set(leaf.leaf.id, leafSecret);
462
+ }
463
+ return leafPubKeyMap;
464
+ }
465
+
466
+ async sendSwapSignRefund(
467
+ leaves: LeafKeyTweak[],
468
+ receiverIdentityPubkey: Uint8Array,
469
+ expiryTime: Date,
470
+ adaptorPubKey?: Uint8Array,
471
+ ): Promise<{
472
+ transfer: Transfer;
473
+ signatureMap: Map<string, Uint8Array>;
474
+ leafDataMap: Map<string, LeafRefundSigningData>;
475
+ signingResults: LeafRefundTxSigningResult[];
476
+ }> {
477
+ const transferId = crypto.randomUUID();
478
+
479
+ const leafDataMap = new Map<string, LeafRefundSigningData>();
480
+ for (const leaf of leaves) {
481
+ const signingNonceCommitment =
482
+ await this.config.signer.getRandomSigningCommitment();
483
+
484
+ const tx = getTxFromRawTxBytes(leaf.leaf.nodeTx);
485
+ const refundTx = getTxFromRawTxBytes(leaf.leaf.refundTx);
486
+ leafDataMap.set(leaf.leaf.id, {
487
+ signingPubKey: leaf.signingPubKey,
488
+ receivingPubkey: receiverIdentityPubkey,
489
+ signingNonceCommitment,
490
+ tx,
491
+ refundTx,
492
+ vout: leaf.leaf.vout,
493
+ });
494
+ }
495
+
496
+ const signingJobs = this.prepareRefundSoSigningJobs(leaves, leafDataMap);
497
+
498
+ const sparkClient = await this.connectionManager.createSparkClient(
499
+ this.config.getCoordinatorAddress(),
500
+ );
501
+
502
+ let response: LeafSwapResponse;
503
+ try {
504
+ response = await sparkClient.leaf_swap({
505
+ transfer: {
506
+ transferId,
507
+ leavesToSend: signingJobs,
508
+ ownerIdentityPublicKey:
509
+ await this.config.signer.getIdentityPublicKey(),
510
+ receiverIdentityPublicKey: receiverIdentityPubkey,
511
+ expiryTime: expiryTime,
512
+ },
513
+ swapId: crypto.randomUUID(),
514
+ adaptorPublicKey: adaptorPubKey || new Uint8Array(),
515
+ });
516
+ } catch (error) {
517
+ throw new Error(`Error initiating leaf swap: ${error}`);
518
+ }
519
+
520
+ if (!response.transfer) {
521
+ throw new Error("No transfer response from coordinator");
522
+ }
523
+
524
+ const signatures = await this.signRefunds(
525
+ leafDataMap,
526
+ response.signingResults,
527
+ adaptorPubKey,
528
+ );
529
+
530
+ const signatureMap = new Map<string, Uint8Array>();
531
+ for (const signature of signatures) {
532
+ signatureMap.set(signature.nodeId, signature.refundTxSignature);
533
+ }
534
+
535
+ return {
536
+ transfer: response.transfer,
537
+ signatureMap,
538
+ leafDataMap,
539
+ signingResults: response.signingResults,
540
+ };
541
+ }
542
+
543
+ async sendTransferSignRefund(
544
+ leaves: LeafKeyTweak[],
545
+ receiverIdentityPubkey: Uint8Array,
546
+ expiryTime?: Date,
547
+ ): Promise<{
548
+ transfer: Transfer;
549
+ signatureMap: Map<string, Uint8Array>;
550
+ leafDataMap: Map<string, LeafRefundSigningData>;
551
+ }> {
552
+ const transferID = crypto.randomUUID();
553
+
554
+ const leafDataMap = new Map<string, LeafRefundSigningData>();
555
+ for (const leaf of leaves) {
556
+ const signingNonceCommitment =
557
+ await this.config.signer.getRandomSigningCommitment();
558
+
559
+ const tx = getTxFromRawTxBytes(leaf.leaf.nodeTx);
560
+ const refundTx = getTxFromRawTxBytes(leaf.leaf.refundTx);
561
+ leafDataMap.set(leaf.leaf.id, {
562
+ signingPubKey: leaf.signingPubKey,
563
+ receivingPubkey: receiverIdentityPubkey,
564
+ signingNonceCommitment,
565
+ tx,
566
+ refundTx,
567
+ vout: leaf.leaf.vout,
568
+ });
569
+ }
570
+
571
+ const signingJobs = this.prepareRefundSoSigningJobs(leaves, leafDataMap);
572
+
573
+ const sparkClient = await this.connectionManager.createSparkClient(
574
+ this.config.getCoordinatorAddress(),
575
+ );
576
+
577
+ let response: StartSendTransferResponse;
578
+ try {
579
+ response = await sparkClient.start_send_transfer({
580
+ transferId: transferID,
581
+ leavesToSend: signingJobs,
582
+ ownerIdentityPublicKey: await this.config.signer.getIdentityPublicKey(),
583
+ receiverIdentityPublicKey: receiverIdentityPubkey,
584
+ expiryTime: expiryTime,
585
+ });
586
+ } catch (error) {
587
+ throw new Error(`Error starting send transfer: ${error}`);
588
+ }
589
+
590
+ const signatures = await this.signRefunds(
591
+ leafDataMap,
592
+ response.signingResults,
593
+ );
594
+
595
+ const signatureMap = new Map<string, Uint8Array>();
596
+ for (const signature of signatures) {
597
+ signatureMap.set(signature.nodeId, signature.refundTxSignature);
598
+ }
599
+
600
+ if (!response.transfer) {
601
+ throw new Error("No transfer response from coordinator");
602
+ }
603
+
604
+ return {
605
+ transfer: response.transfer,
606
+ signatureMap,
607
+ leafDataMap,
608
+ };
609
+ }
610
+
611
+ private prepareRefundSoSigningJobs(
612
+ leaves: LeafKeyTweak[],
613
+ leafDataMap: Map<string, LeafRefundSigningData>,
614
+ ): LeafRefundTxSigningJob[] {
615
+ const signingJobs: LeafRefundTxSigningJob[] = [];
616
+ for (const leaf of leaves) {
617
+ const refundSigningData = leafDataMap.get(leaf.leaf.id);
618
+ if (!refundSigningData) {
619
+ throw new Error(`Leaf data not found for leaf ${leaf.leaf.id}`);
620
+ }
621
+
622
+ const nodeTx = getTxFromRawTxBytes(leaf.leaf.nodeTx);
623
+ const nodeOutPoint: TransactionInput = {
624
+ txid: hexToBytes(getTxId(nodeTx)),
625
+ index: 0,
626
+ };
627
+
628
+ const currRefundTx = getTxFromRawTxBytes(leaf.leaf.refundTx);
629
+ const nextSequence = getNextTransactionSequence(
630
+ currRefundTx.getInput(0).sequence,
631
+ );
632
+ const amountSats = currRefundTx.getOutput(0).amount;
633
+ if (amountSats === undefined) {
634
+ throw new Error("Amount not found in signRefunds");
635
+ }
636
+
637
+ const refundTx = createRefundTx(
638
+ nextSequence,
639
+ nodeOutPoint,
640
+ amountSats,
641
+ refundSigningData.receivingPubkey,
642
+ this.config.getNetwork(),
643
+ );
644
+
645
+ refundSigningData.refundTx = refundTx;
646
+
647
+ const refundNonceCommitmentProto =
648
+ refundSigningData.signingNonceCommitment;
649
+
650
+ signingJobs.push({
651
+ leafId: leaf.leaf.id,
652
+ refundTxSigningJob: {
653
+ signingPublicKey: refundSigningData.signingPubKey,
654
+ rawTx: refundTx.toBytes(),
655
+ signingNonceCommitment: refundNonceCommitmentProto,
656
+ },
657
+ });
658
+ }
659
+
660
+ return signingJobs;
661
+ }
662
+
663
+ async claimTransferTweakKeys(transfer: Transfer, leaves: LeafKeyTweak[]) {
664
+ const leavesTweaksMap = await this.prepareClaimLeavesKeyTweaks(leaves);
665
+
666
+ const errors: Error[] = [];
667
+
668
+ const promises = Object.entries(this.config.getSigningOperators()).map(
669
+ async ([identifier, operator]) => {
670
+ const sparkClient = await this.connectionManager.createSparkClient(
671
+ operator.address,
672
+ );
673
+
674
+ const leavesToReceive = leavesTweaksMap.get(identifier);
675
+ if (!leavesToReceive) {
676
+ errors.push(
677
+ new Error(`No leaves to receive for operator ${identifier}`),
678
+ );
679
+ return;
680
+ }
681
+
682
+ try {
683
+ await sparkClient.claim_transfer_tweak_keys({
684
+ transferId: transfer.id,
685
+ ownerIdentityPublicKey:
686
+ await this.config.signer.getIdentityPublicKey(),
687
+ leavesToReceive,
688
+ });
689
+ } catch (error) {
690
+ errors.push(
691
+ new Error(`Error claiming transfer tweak keys: ${error}`),
692
+ );
693
+ return;
694
+ }
695
+ },
696
+ );
697
+
698
+ await Promise.all(promises);
699
+
700
+ if (errors.length > 0) {
701
+ throw new Error(`Error claiming transfer tweak keys: ${errors[0]}`);
702
+ }
703
+ }
704
+
705
+ private async prepareClaimLeavesKeyTweaks(
706
+ leaves: LeafKeyTweak[],
707
+ ): Promise<Map<string, ClaimLeafKeyTweak[]>> {
708
+ const leafDataMap = new Map<string, ClaimLeafKeyTweak[]>();
709
+ for (const leaf of leaves) {
710
+ const leafData = await this.prepareClaimLeafKeyTweaks(leaf);
711
+ for (const [identifier, leafTweak] of leafData) {
712
+ leafDataMap.set(identifier, [
713
+ ...(leafDataMap.get(identifier) || []),
714
+ leafTweak,
715
+ ]);
716
+ }
717
+ }
718
+ return leafDataMap;
719
+ }
720
+
721
+ private async prepareClaimLeafKeyTweaks(
722
+ leaf: LeafKeyTweak,
723
+ ): Promise<Map<string, ClaimLeafKeyTweak>> {
724
+ const signingOperators = this.config.getSigningOperators();
725
+
726
+ const pubKeyTweak =
727
+ await this.config.signer.subtractPrivateKeysGivenPublicKeys(
728
+ leaf.signingPubKey,
729
+ leaf.newSigningPubKey,
730
+ );
731
+
732
+ const shares = await this.config.signer.splitSecretWithProofs({
733
+ secret: pubKeyTweak,
734
+ curveOrder: secp256k1.CURVE.n,
735
+ threshold: this.config.getThreshold(),
736
+ numShares: Object.keys(signingOperators).length,
737
+ isSecretPubkey: true,
738
+ });
739
+
740
+ const pubkeySharesTweak = new Map<string, Uint8Array>();
741
+
742
+ for (const [identifier, operator] of Object.entries(signingOperators)) {
743
+ const share = this.findShare(shares, operator.id);
744
+ if (!share) {
745
+ throw new Error(`Share not found for operator ${operator.id}`);
746
+ }
747
+ const pubkeyTweak = secp256k1.getPublicKey(
748
+ numberToBytesBE(share.share, 32),
749
+ );
750
+ pubkeySharesTweak.set(identifier, pubkeyTweak);
751
+ }
752
+
753
+ const leafTweaksMap = new Map<string, ClaimLeafKeyTweak>();
754
+ for (const [identifier, operator] of Object.entries(signingOperators)) {
755
+ const share = this.findShare(shares, operator.id);
756
+ if (!share) {
757
+ throw new Error(`Share not found for operator ${operator.id}`);
758
+ }
759
+
760
+ leafTweaksMap.set(identifier, {
761
+ leafId: leaf.leaf.id,
762
+ secretShareTweak: {
763
+ secretShare: numberToBytesBE(share.share, 32),
764
+ proofs: share.proofs,
765
+ },
766
+ pubkeySharesTweak: Object.fromEntries(pubkeySharesTweak),
767
+ });
768
+ }
769
+
770
+ return leafTweaksMap;
771
+ }
772
+
773
+ async claimTransferSignRefunds(
774
+ transfer: Transfer,
775
+ leafKeys: LeafKeyTweak[],
776
+ ): Promise<NodeSignatures[]> {
777
+ const leafDataMap: Map<string, LeafRefundSigningData> = new Map();
778
+ for (const leafKey of leafKeys) {
779
+ const tx = getTxFromRawTxBytes(leafKey.leaf.nodeTx);
780
+ leafDataMap.set(leafKey.leaf.id, {
781
+ signingPubKey: leafKey.newSigningPubKey,
782
+ receivingPubkey: leafKey.newSigningPubKey,
783
+ signingNonceCommitment:
784
+ await this.config.signer.getRandomSigningCommitment(),
785
+ tx,
786
+ vout: leafKey.leaf.vout,
787
+ });
788
+ }
789
+
790
+ const signingJobs = this.prepareRefundSoSigningJobs(leafKeys, leafDataMap);
791
+
792
+ const sparkClient = await this.connectionManager.createSparkClient(
793
+ this.config.getCoordinatorAddress(),
794
+ );
795
+ let resp: ClaimTransferSignRefundsResponse;
796
+ try {
797
+ resp = await sparkClient.claim_transfer_sign_refunds({
798
+ transferId: transfer.id,
799
+ ownerIdentityPublicKey: await this.config.signer.getIdentityPublicKey(),
800
+ signingJobs,
801
+ });
802
+ } catch (error) {
803
+ throw new Error(`Error claiming transfer sign refunds: ${error}`);
804
+ }
805
+ return this.signRefunds(leafDataMap, resp.signingResults);
806
+ }
807
+
808
+ private async finalizeTransfer(nodeSignatures: NodeSignatures[]) {
809
+ const sparkClient = await this.connectionManager.createSparkClient(
810
+ this.config.getCoordinatorAddress(),
811
+ );
812
+ try {
813
+ return await sparkClient.finalize_node_signatures({
814
+ intent: SignatureIntent.TRANSFER,
815
+ nodeSignatures,
816
+ });
817
+ } catch (error) {
818
+ throw new Error(`Error finalizing node signatures in transfer: ${error}`);
819
+ }
820
+ }
821
+
822
+ async cancelSendTransfer(
823
+ transfer: Transfer,
824
+ operatorAddress: string,
825
+ ): Promise<Transfer | undefined> {
826
+ const sparkClient =
827
+ await this.connectionManager.createSparkClient(operatorAddress);
828
+
829
+ try {
830
+ const response = await sparkClient.cancel_send_transfer({
831
+ transferId: transfer.id,
832
+ senderIdentityPublicKey:
833
+ await this.config.signer.getIdentityPublicKey(),
834
+ });
835
+
836
+ return response.transfer;
837
+ } catch (error) {
838
+ throw new Error(`Error canceling send transfer: ${error}`);
839
+ }
840
+ }
841
+
842
+ async queryPendingTransfersBySender(
843
+ operatorAddress: string,
844
+ ): Promise<QueryPendingTransfersResponse> {
845
+ const sparkClient =
846
+ await this.connectionManager.createSparkClient(operatorAddress);
847
+ try {
848
+ return await sparkClient.query_pending_transfers({
849
+ participant: {
850
+ $case: "senderIdentityPublicKey",
851
+ senderIdentityPublicKey:
852
+ await this.config.signer.getIdentityPublicKey(),
853
+ },
854
+ network: this.config.getNetworkProto(),
855
+ });
856
+ } catch (error) {
857
+ throw new Error(`Error querying pending transfers by sender: ${error}`);
858
+ }
859
+ }
860
+
861
+ async refreshTimelockNodes(
862
+ nodes: TreeNode[],
863
+ parentNode: TreeNode,
864
+ signingPubKey: Uint8Array,
865
+ ) {
866
+ if (nodes.length === 0) {
867
+ throw Error("no nodes to refresh");
868
+ }
869
+
870
+ const signingJobs: SigningJob[] = [];
871
+ const newNodeTxs: Transaction[] = [];
872
+
873
+ for (let i = 0; i < nodes.length; i++) {
874
+ const node = nodes[i];
875
+ if (!node) {
876
+ throw Error("could not get node");
877
+ }
878
+ const nodeTx = getTxFromRawTxBytes(node?.nodeTx);
879
+ const input = nodeTx.getInput(0);
880
+
881
+ if (!input) {
882
+ throw Error("Could not fetch tx input");
883
+ }
884
+
885
+ const newTx = new Transaction({ allowUnknownOutputs: true });
886
+ for (let j = 0; j < nodeTx.outputsLength; j++) {
887
+ newTx.addOutput(nodeTx.getOutput(j));
888
+ }
889
+ if (i === 0) {
890
+ const currSequence = input.sequence;
891
+
892
+ newTx.addInput({
893
+ ...input,
894
+ sequence: getNextTransactionSequence(currSequence),
895
+ });
896
+ } else {
897
+ newTx.addInput({
898
+ ...input,
899
+ sequence: initialSequence(),
900
+ txid: newNodeTxs[i - 1]?.id,
901
+ });
902
+ }
903
+
904
+ signingJobs.push({
905
+ signingPublicKey: signingPubKey,
906
+ rawTx: newTx.toBytes(),
907
+ signingNonceCommitment:
908
+ await this.config.signer.getRandomSigningCommitment(),
909
+ });
910
+ newNodeTxs[i] = newTx;
911
+ }
912
+
913
+ const leaf = nodes[nodes.length - 1];
914
+ if (!leaf?.refundTx) {
915
+ throw Error("leaf does not have refund tx");
916
+ }
917
+ const refundTx = getTxFromRawTxBytes(leaf?.refundTx);
918
+ const newRefundTx = new Transaction({ allowUnknownOutputs: true });
919
+
920
+ for (let j = 0; j < refundTx.outputsLength; j++) {
921
+ newRefundTx.addOutput(refundTx.getOutput(j));
922
+ }
923
+
924
+ const refundTxInput = refundTx.getInput(0);
925
+ if (!refundTxInput) {
926
+ throw Error("refund tx doesn't have input");
927
+ }
928
+
929
+ if (!newNodeTxs[newNodeTxs.length - 1]) {
930
+ throw Error("Could not get last node tx");
931
+ }
932
+ newRefundTx.addInput({
933
+ ...refundTxInput,
934
+ sequence: initialSequence(),
935
+ txid: getTxId(newNodeTxs[newNodeTxs.length - 1]!),
936
+ });
937
+
938
+ const refundSigningJob = {
939
+ signingPublicKey: signingPubKey,
940
+ rawTx: newRefundTx.toBytes(),
941
+ signingNonceCommitment:
942
+ await this.config.signer.getRandomSigningCommitment(),
943
+ };
944
+
945
+ signingJobs.push(refundSigningJob);
946
+
947
+ const sparkClient = await this.connectionManager.createSparkClient(
948
+ this.config.getCoordinatorAddress(),
949
+ );
950
+
951
+ const response = await sparkClient.refresh_timelock({
952
+ leafId: leaf.id,
953
+ ownerIdentityPublicKey: await this.config.signer.getIdentityPublicKey(),
954
+ signingJobs,
955
+ });
956
+
957
+ if (signingJobs.length !== response.signingResults.length) {
958
+ throw Error(
959
+ `number of signing jobs and signing results do not match: ${signingJobs.length} !== ${response.signingResults.length}`,
960
+ );
961
+ }
962
+
963
+ let nodeSignatures: NodeSignatures[] = [];
964
+ let leafSignature: Uint8Array | undefined;
965
+ let refundSignature: Uint8Array | undefined;
966
+ let leafNodeId: string | undefined;
967
+ for (let i = 0; i < response.signingResults.length; i++) {
968
+ const signingResult = response.signingResults[i];
969
+ const signingJob = signingJobs[i];
970
+ if (!signingJob || !signingResult) {
971
+ throw Error("Signing job does not exist");
972
+ }
973
+
974
+ if (!signingJob.signingNonceCommitment) {
975
+ throw Error("nonce commitment does not exist");
976
+ }
977
+ const rawTx = getTxFromRawTxBytes(signingJob.rawTx);
978
+
979
+ let parentTx: Transaction | undefined;
980
+ let nodeId: string | undefined;
981
+ let vout: number | undefined;
982
+
983
+ if (i === nodes.length) {
984
+ nodeId = nodes[i - 1]?.id;
985
+ parentTx = newNodeTxs[i - 1];
986
+ vout = 0;
987
+ } else if (i === 0) {
988
+ nodeId = nodes[i]?.id;
989
+ parentTx = getTxFromRawTxBytes(parentNode.nodeTx);
990
+ vout = nodes[i]?.vout;
991
+ } else {
992
+ nodeId = nodes[i]?.id;
993
+ parentTx = newNodeTxs[i - 1];
994
+ vout = nodes[i]?.vout;
995
+ }
996
+
997
+ if (!parentTx || !nodeId || vout === undefined) {
998
+ throw Error("Could not parse signing results");
999
+ }
1000
+
1001
+ const txOut = parentTx.getOutput(vout);
1002
+
1003
+ const rawTxSighash = getSigHashFromTx(rawTx, 0, txOut);
1004
+
1005
+ const userSignature = await this.config.signer.signFrost({
1006
+ message: rawTxSighash,
1007
+ privateAsPubKey: signingPubKey,
1008
+ publicKey: signingPubKey,
1009
+ verifyingKey: signingResult.verifyingKey,
1010
+ selfCommitment: signingJob.signingNonceCommitment,
1011
+ statechainCommitments:
1012
+ signingResult.signingResult?.signingNonceCommitments,
1013
+ adaptorPubKey: new Uint8Array(),
1014
+ });
1015
+
1016
+ const signature = await this.config.signer.aggregateFrost({
1017
+ message: rawTxSighash,
1018
+ statechainSignatures: signingResult.signingResult?.signatureShares,
1019
+ statechainPublicKeys: signingResult.signingResult?.publicKeys,
1020
+ verifyingKey: signingResult.verifyingKey,
1021
+ statechainCommitments:
1022
+ signingResult.signingResult?.signingNonceCommitments,
1023
+ selfCommitment: signingJob.signingNonceCommitment,
1024
+ publicKey: signingPubKey,
1025
+ selfSignature: userSignature,
1026
+ adaptorPubKey: new Uint8Array(),
1027
+ });
1028
+
1029
+ if (i !== nodes.length && i !== nodes.length - 1) {
1030
+ nodeSignatures.push({
1031
+ nodeId: nodeId,
1032
+ nodeTxSignature: signature,
1033
+ refundTxSignature: new Uint8Array(),
1034
+ });
1035
+ } else if (i === nodes.length) {
1036
+ refundSignature = signature;
1037
+ } else if (i === nodes.length - 1) {
1038
+ leafNodeId = nodeId;
1039
+ leafSignature = signature;
1040
+ }
1041
+ }
1042
+
1043
+ if (!leafSignature || !refundSignature || !leafNodeId) {
1044
+ throw Error("leaf or refund signature does not exist");
1045
+ }
1046
+
1047
+ nodeSignatures.push({
1048
+ nodeId: leafNodeId,
1049
+ nodeTxSignature: leafSignature,
1050
+ refundTxSignature: refundSignature,
1051
+ });
1052
+
1053
+ return await sparkClient.finalize_node_signatures({
1054
+ intent: SignatureIntent.REFRESH,
1055
+ nodeSignatures,
1056
+ });
1057
+ }
1058
+
1059
+ private async extendTimelock(node: TreeNode, signingPubKey: Uint8Array) {
1060
+ const nodeTx = getTxFromRawTxBytes(node.nodeTx);
1061
+ const refundTx = getTxFromRawTxBytes(node.refundTx);
1062
+
1063
+ const refundSequence = refundTx.getInput(0).sequence || 0;
1064
+ const newNodeOutPoint: TransactionInput = {
1065
+ txid: hexToBytes(nodeTx.id),
1066
+ index: 0,
1067
+ };
1068
+
1069
+ const newNodeSequence = getNextTransactionSequence(refundSequence);
1070
+ const newNodeTx = new Transaction({ allowUnknownOutputs: true });
1071
+ newNodeTx.addInput({ ...newNodeOutPoint, sequence: newNodeSequence });
1072
+ newNodeTx.addOutput(nodeTx.getOutput(0));
1073
+ newNodeTx.addOutput(getEphemeralAnchorOutput());
1074
+
1075
+ const newRefundOutPoint: TransactionInput = {
1076
+ txid: hexToBytes(getTxId(newNodeTx)!),
1077
+ index: 0,
1078
+ };
1079
+
1080
+ const amountSats = refundTx.getOutput(0).amount;
1081
+ if (amountSats === undefined) {
1082
+ throw new Error("Amount not found in extendTimelock");
1083
+ }
1084
+
1085
+ const newRefundTx = createRefundTx(
1086
+ initialSequence(),
1087
+ newRefundOutPoint,
1088
+ amountSats,
1089
+ signingPubKey,
1090
+ this.config.getNetwork(),
1091
+ );
1092
+
1093
+ const nodeSighash = getSigHashFromTx(newNodeTx, 0, nodeTx.getOutput(0));
1094
+ const refundSighash = getSigHashFromTx(newRefundTx, 0, nodeTx.getOutput(0));
1095
+
1096
+ const newNodeSigningJob = {
1097
+ signingPublicKey: signingPubKey,
1098
+ rawTx: newNodeTx.toBytes(),
1099
+ signingNonceCommitment:
1100
+ await this.config.signer.getRandomSigningCommitment(),
1101
+ };
1102
+
1103
+ const newRefundSigningJob = {
1104
+ signingPublicKey: signingPubKey,
1105
+ rawTx: newRefundTx.toBytes(),
1106
+ signingNonceCommitment:
1107
+ await this.config.signer.getRandomSigningCommitment(),
1108
+ };
1109
+
1110
+ const sparkClient = await this.connectionManager.createSparkClient(
1111
+ this.config.getCoordinatorAddress(),
1112
+ );
1113
+
1114
+ const response = await sparkClient.extend_leaf({
1115
+ leafId: node.id,
1116
+ ownerIdentityPublicKey: await this.config.signer.getIdentityPublicKey(),
1117
+ nodeTxSigningJob: newNodeSigningJob,
1118
+ refundTxSigningJob: newRefundSigningJob,
1119
+ });
1120
+
1121
+ if (!response.nodeTxSigningResult || !response.refundTxSigningResult) {
1122
+ throw new Error("Signing result does not exist");
1123
+ }
1124
+
1125
+ const nodeUserSig = await this.config.signer.signFrost({
1126
+ message: nodeSighash,
1127
+ privateAsPubKey: signingPubKey,
1128
+ publicKey: signingPubKey,
1129
+ verifyingKey: response.nodeTxSigningResult.verifyingKey,
1130
+ selfCommitment: newNodeSigningJob.signingNonceCommitment,
1131
+ statechainCommitments:
1132
+ response.nodeTxSigningResult.signingResult?.signingNonceCommitments,
1133
+ adaptorPubKey: new Uint8Array(),
1134
+ });
1135
+
1136
+ const refundUserSig = await this.config.signer.signFrost({
1137
+ message: refundSighash,
1138
+ privateAsPubKey: signingPubKey,
1139
+ publicKey: signingPubKey,
1140
+ verifyingKey: response.refundTxSigningResult.verifyingKey,
1141
+ selfCommitment: newRefundSigningJob.signingNonceCommitment,
1142
+ statechainCommitments:
1143
+ response.refundTxSigningResult.signingResult?.signingNonceCommitments,
1144
+ adaptorPubKey: new Uint8Array(),
1145
+ });
1146
+
1147
+ const nodeSig = await this.config.signer.aggregateFrost({
1148
+ message: nodeSighash,
1149
+ statechainSignatures:
1150
+ response.nodeTxSigningResult.signingResult?.signatureShares,
1151
+ statechainPublicKeys:
1152
+ response.nodeTxSigningResult.signingResult?.publicKeys,
1153
+ verifyingKey: response.nodeTxSigningResult.verifyingKey,
1154
+ statechainCommitments:
1155
+ response.nodeTxSigningResult.signingResult?.signingNonceCommitments,
1156
+ selfCommitment: newNodeSigningJob.signingNonceCommitment,
1157
+ publicKey: signingPubKey,
1158
+ selfSignature: nodeUserSig,
1159
+ adaptorPubKey: new Uint8Array(),
1160
+ });
1161
+
1162
+ const refundSig = await this.config.signer.aggregateFrost({
1163
+ message: refundSighash,
1164
+ statechainSignatures:
1165
+ response.refundTxSigningResult.signingResult?.signatureShares,
1166
+ statechainPublicKeys:
1167
+ response.refundTxSigningResult.signingResult?.publicKeys,
1168
+ verifyingKey: response.refundTxSigningResult.verifyingKey,
1169
+ statechainCommitments:
1170
+ response.refundTxSigningResult.signingResult?.signingNonceCommitments,
1171
+ selfCommitment: newRefundSigningJob.signingNonceCommitment,
1172
+ publicKey: signingPubKey,
1173
+ selfSignature: refundUserSig,
1174
+ adaptorPubKey: new Uint8Array(),
1175
+ });
1176
+
1177
+ return await sparkClient.finalize_node_signatures({
1178
+ intent: SignatureIntent.EXTEND,
1179
+ nodeSignatures: [
1180
+ {
1181
+ nodeId: response.leafId,
1182
+ nodeTxSignature: nodeSig,
1183
+ refundTxSignature: refundSig,
1184
+ },
1185
+ ],
1186
+ });
1187
+ }
1188
+ }