@arkade-os/sdk 0.3.8 → 0.3.9

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 (41) hide show
  1. package/README.md +78 -1
  2. package/dist/cjs/identity/singleKey.js +33 -1
  3. package/dist/cjs/index.js +17 -2
  4. package/dist/cjs/intent/index.js +31 -2
  5. package/dist/cjs/providers/ark.js +9 -3
  6. package/dist/cjs/providers/indexer.js +2 -2
  7. package/dist/cjs/wallet/batch.js +183 -0
  8. package/dist/cjs/wallet/index.js +15 -0
  9. package/dist/cjs/wallet/serviceWorker/request.js +0 -2
  10. package/dist/cjs/wallet/serviceWorker/wallet.js +98 -34
  11. package/dist/cjs/wallet/serviceWorker/worker.js +163 -72
  12. package/dist/cjs/wallet/utils.js +2 -2
  13. package/dist/cjs/wallet/vtxo-manager.js +5 -0
  14. package/dist/cjs/wallet/wallet.js +341 -359
  15. package/dist/esm/identity/singleKey.js +31 -0
  16. package/dist/esm/index.js +12 -7
  17. package/dist/esm/intent/index.js +31 -2
  18. package/dist/esm/providers/ark.js +9 -3
  19. package/dist/esm/providers/indexer.js +2 -2
  20. package/dist/esm/wallet/batch.js +180 -0
  21. package/dist/esm/wallet/index.js +14 -0
  22. package/dist/esm/wallet/serviceWorker/request.js +0 -2
  23. package/dist/esm/wallet/serviceWorker/wallet.js +96 -33
  24. package/dist/esm/wallet/serviceWorker/worker.js +165 -74
  25. package/dist/esm/wallet/utils.js +2 -2
  26. package/dist/esm/wallet/vtxo-manager.js +6 -1
  27. package/dist/esm/wallet/wallet.js +342 -362
  28. package/dist/types/identity/index.d.ts +5 -3
  29. package/dist/types/identity/singleKey.d.ts +20 -1
  30. package/dist/types/index.d.ts +11 -8
  31. package/dist/types/intent/index.d.ts +19 -2
  32. package/dist/types/providers/ark.d.ts +9 -8
  33. package/dist/types/providers/indexer.d.ts +2 -2
  34. package/dist/types/wallet/batch.d.ts +87 -0
  35. package/dist/types/wallet/index.d.ts +75 -16
  36. package/dist/types/wallet/serviceWorker/request.d.ts +5 -1
  37. package/dist/types/wallet/serviceWorker/wallet.d.ts +46 -15
  38. package/dist/types/wallet/serviceWorker/worker.d.ts +6 -3
  39. package/dist/types/wallet/utils.d.ts +8 -3
  40. package/dist/types/wallet/wallet.d.ts +87 -36
  41. package/package.json +123 -113
@@ -33,7 +33,8 @@ var __importStar = (this && this.__importStar) || (function () {
33
33
  };
34
34
  })();
35
35
  Object.defineProperty(exports, "__esModule", { value: true });
36
- exports.Wallet = void 0;
36
+ exports.Wallet = exports.ReadonlyWallet = void 0;
37
+ exports.getSequence = getSequence;
37
38
  exports.waitForIncomingFunds = waitForIncomingFunds;
38
39
  const base_1 = require("@scure/base");
39
40
  const bip68 = __importStar(require("bip68"));
@@ -56,74 +57,40 @@ const vtxo_manager_1 = require("./vtxo-manager");
56
57
  const arknote_1 = require("../arknote");
57
58
  const intent_1 = require("../intent");
58
59
  const indexer_1 = require("../providers/indexer");
59
- const txTree_1 = require("../tree/txTree");
60
60
  const unknownFields_1 = require("../utils/unknownFields");
61
61
  const inMemory_1 = require("../storage/inMemory");
62
62
  const walletRepository_1 = require("../repositories/walletRepository");
63
63
  const contractRepository_1 = require("../repositories/contractRepository");
64
64
  const utils_1 = require("./utils");
65
65
  const errors_1 = require("../providers/errors");
66
+ const batch_1 = require("./batch");
66
67
  /**
67
- * Main wallet implementation for Bitcoin transactions with Ark protocol support.
68
- * The wallet does not store any data locally and relies on Ark and onchain
69
- * providers to fetch UTXOs and VTXOs.
70
- *
71
- * @example
72
- * ```typescript
73
- * // Create a wallet with URL configuration
74
- * const wallet = await Wallet.create({
75
- * identity: SingleKey.fromHex('your_private_key'),
76
- * arkServerUrl: 'https://ark.example.com',
77
- * esploraUrl: 'https://mempool.space/api'
78
- * });
79
- *
80
- * // Or with custom provider instances (e.g., for Expo/React Native)
81
- * const wallet = await Wallet.create({
82
- * identity: SingleKey.fromHex('your_private_key'),
83
- * arkProvider: new ExpoArkProvider('https://ark.example.com'),
84
- * indexerProvider: new ExpoIndexerProvider('https://ark.example.com'),
85
- * esploraUrl: 'https://mempool.space/api'
86
- * });
87
- *
88
- * // Get addresses
89
- * const arkAddress = await wallet.getAddress();
90
- * const boardingAddress = await wallet.getBoardingAddress();
91
- *
92
- * // Send bitcoin
93
- * const txid = await wallet.sendBitcoin({
94
- * address: 'tb1...',
95
- * amount: 50000
96
- * });
97
- * ```
68
+ * Type guard function to check if an identity has a toReadonly method.
98
69
  */
99
- class Wallet {
100
- constructor(identity, network, networkName, onchainProvider, arkProvider, indexerProvider, arkServerPublicKey, offchainTapscript, boardingTapscript, serverUnrollScript, forfeitOutputScript, forfeitPubkey, dustAmount, walletRepository, contractRepository, renewalConfig) {
70
+ function hasToReadonly(identity) {
71
+ return (typeof identity === "object" &&
72
+ identity !== null &&
73
+ "toReadonly" in identity &&
74
+ typeof identity.toReadonly === "function");
75
+ }
76
+ class ReadonlyWallet {
77
+ constructor(identity, network, onchainProvider, indexerProvider, arkServerPublicKey, offchainTapscript, boardingTapscript, dustAmount, walletRepository, contractRepository) {
101
78
  this.identity = identity;
102
79
  this.network = network;
103
- this.networkName = networkName;
104
80
  this.onchainProvider = onchainProvider;
105
- this.arkProvider = arkProvider;
106
81
  this.indexerProvider = indexerProvider;
107
82
  this.arkServerPublicKey = arkServerPublicKey;
108
83
  this.offchainTapscript = offchainTapscript;
109
84
  this.boardingTapscript = boardingTapscript;
110
- this.serverUnrollScript = serverUnrollScript;
111
- this.forfeitOutputScript = forfeitOutputScript;
112
- this.forfeitPubkey = forfeitPubkey;
113
85
  this.dustAmount = dustAmount;
114
86
  this.walletRepository = walletRepository;
115
87
  this.contractRepository = contractRepository;
116
- this.renewalConfig = {
117
- enabled: renewalConfig?.enabled ?? false,
118
- ...vtxo_manager_1.DEFAULT_RENEWAL_CONFIG,
119
- ...renewalConfig,
120
- };
121
88
  }
122
- static async create(config) {
123
- const pubkey = await config.identity.xOnlyPublicKey();
124
- if (!pubkey) {
125
- throw new Error("Invalid configured public key");
126
- }
89
+ /**
90
+ * Protected helper to set up shared wallet configuration.
91
+ * Extracts common logic used by both ReadonlyWallet.create() and Wallet.create().
92
+ */
93
+ static async setupWalletConfig(config, pubkey) {
127
94
  // Use provided arkProvider instance or create a new one from arkServerUrl
128
95
  const arkProvider = config.arkProvider ||
129
96
  (() => {
@@ -187,25 +154,32 @@ class Wallet {
187
154
  });
188
155
  // Save tapscripts
189
156
  const offchainTapscript = bareVtxoTapscript;
190
- // the serverUnrollScript is the one used to create output scripts of the checkpoint transactions
191
- let serverUnrollScript;
192
- try {
193
- const raw = base_1.hex.decode(info.checkpointTapscript);
194
- serverUnrollScript = tapscript_1.CSVMultisigTapscript.decode(raw);
195
- }
196
- catch (e) {
197
- throw new Error("Invalid checkpointTapscript from server");
198
- }
199
- // parse the server forfeit address
200
- // server is expecting funds to be sent to this address
201
- const forfeitPubkey = base_1.hex.decode(info.forfeitPubkey).slice(1);
202
- const forfeitAddress = (0, btc_signer_1.Address)(network).decode(info.forfeitAddress);
203
- const forfeitOutputScript = btc_signer_1.OutScript.encode(forfeitAddress);
204
157
  // Set up storage and repositories
205
158
  const storage = config.storage || new inMemory_1.InMemoryStorageAdapter();
206
159
  const walletRepository = new walletRepository_1.WalletRepositoryImpl(storage);
207
160
  const contractRepository = new contractRepository_1.ContractRepositoryImpl(storage);
208
- return new Wallet(config.identity, network, info.network, onchainProvider, arkProvider, indexerProvider, serverPubKey, offchainTapscript, boardingTapscript, serverUnrollScript, forfeitOutputScript, forfeitPubkey, info.dust, walletRepository, contractRepository, config.renewalConfig);
161
+ return {
162
+ arkProvider,
163
+ indexerProvider,
164
+ onchainProvider,
165
+ network,
166
+ networkName: info.network,
167
+ serverPubKey,
168
+ offchainTapscript,
169
+ boardingTapscript,
170
+ dustAmount: info.dust,
171
+ walletRepository,
172
+ contractRepository,
173
+ info,
174
+ };
175
+ }
176
+ static async create(config) {
177
+ const pubkey = await config.identity.xOnlyPublicKey();
178
+ if (!pubkey) {
179
+ throw new Error("Invalid configured public key");
180
+ }
181
+ const setup = await ReadonlyWallet.setupWalletConfig(config, pubkey);
182
+ return new ReadonlyWallet(config.identity, setup.network, setup.onchainProvider, setup.indexerProvider, setup.serverPubKey, setup.offchainTapscript, setup.boardingTapscript, setup.dustAmount, setup.walletRepository, setup.contractRepository);
209
183
  }
210
184
  get arkAddress() {
211
185
  return this.offchainTapscript.address(this.network.hrp, this.arkServerPublicKey);
@@ -280,7 +254,7 @@ class Wallet {
280
254
  let vtxos = allVtxos.filter(_1.isSpendable);
281
255
  // all recoverable vtxos are spendable by definition
282
256
  if (!filter.withRecoverable) {
283
- vtxos = vtxos.filter((vtxo) => !(0, _1.isRecoverable)(vtxo));
257
+ vtxos = vtxos.filter((vtxo) => !(0, _1.isRecoverable)(vtxo) && !(0, _1.isExpired)(vtxo));
284
258
  }
285
259
  if (filter.withUnrolled) {
286
260
  const spentVtxos = allVtxos.filter((vtxo) => !(0, _1.isSpendable)(vtxo));
@@ -289,9 +263,6 @@ class Wallet {
289
263
  return vtxos;
290
264
  }
291
265
  async getTransactionHistory() {
292
- if (!this.indexerProvider) {
293
- return [];
294
- }
295
266
  const response = await this.indexerProvider.getVtxos({
296
267
  scripts: [base_1.hex.encode(this.offchainTapscript.pkScript)],
297
268
  });
@@ -395,6 +366,179 @@ class Wallet {
395
366
  await this.walletRepository.saveUtxos(boardingAddress, utxos);
396
367
  return utxos;
397
368
  }
369
+ async notifyIncomingFunds(eventCallback) {
370
+ const arkAddress = await this.getAddress();
371
+ const boardingAddress = await this.getBoardingAddress();
372
+ let onchainStopFunc;
373
+ let indexerStopFunc;
374
+ if (this.onchainProvider && boardingAddress) {
375
+ const findVoutOnTx = (tx) => {
376
+ return tx.vout.findIndex((v) => v.scriptpubkey_address === boardingAddress);
377
+ };
378
+ onchainStopFunc = await this.onchainProvider.watchAddresses([boardingAddress], (txs) => {
379
+ // find all utxos belonging to our boarding address
380
+ const coins = txs
381
+ // filter txs where address is in output
382
+ .filter((tx) => findVoutOnTx(tx) !== -1)
383
+ // return utxo as Coin
384
+ .map((tx) => {
385
+ const { txid, status } = tx;
386
+ const vout = findVoutOnTx(tx);
387
+ const value = Number(tx.vout[vout].value);
388
+ return { txid, vout, value, status };
389
+ });
390
+ // and notify via callback
391
+ eventCallback({
392
+ type: "utxo",
393
+ coins,
394
+ });
395
+ });
396
+ }
397
+ if (this.indexerProvider && arkAddress) {
398
+ const offchainScript = this.offchainTapscript;
399
+ const subscriptionId = await this.indexerProvider.subscribeForScripts([
400
+ base_1.hex.encode(offchainScript.pkScript),
401
+ ]);
402
+ const abortController = new AbortController();
403
+ const subscription = this.indexerProvider.getSubscription(subscriptionId, abortController.signal);
404
+ indexerStopFunc = async () => {
405
+ abortController.abort();
406
+ await this.indexerProvider?.unsubscribeForScripts(subscriptionId);
407
+ };
408
+ // Handle subscription updates asynchronously without blocking
409
+ (async () => {
410
+ try {
411
+ for await (const update of subscription) {
412
+ if (update.newVtxos?.length > 0 ||
413
+ update.spentVtxos?.length > 0) {
414
+ eventCallback({
415
+ type: "vtxo",
416
+ newVtxos: update.newVtxos.map((vtxo) => (0, utils_1.extendVirtualCoin)(this, vtxo)),
417
+ spentVtxos: update.spentVtxos.map((vtxo) => (0, utils_1.extendVirtualCoin)(this, vtxo)),
418
+ });
419
+ }
420
+ }
421
+ }
422
+ catch (error) {
423
+ console.error("Subscription error:", error);
424
+ }
425
+ })();
426
+ }
427
+ const stopFunc = () => {
428
+ onchainStopFunc?.();
429
+ indexerStopFunc?.();
430
+ };
431
+ return stopFunc;
432
+ }
433
+ async fetchPendingTxs() {
434
+ // get non-swept VTXOs, rely on the indexer only in case DB doesn't have the right state
435
+ const scripts = [base_1.hex.encode(this.offchainTapscript.pkScript)];
436
+ let { vtxos } = await this.indexerProvider.getVtxos({
437
+ scripts,
438
+ });
439
+ return vtxos
440
+ .filter((vtxo) => vtxo.virtualStatus.state !== "swept" &&
441
+ vtxo.virtualStatus.state !== "settled" &&
442
+ vtxo.arkTxId !== undefined)
443
+ .map((_) => _.arkTxId);
444
+ }
445
+ }
446
+ exports.ReadonlyWallet = ReadonlyWallet;
447
+ /**
448
+ * Main wallet implementation for Bitcoin transactions with Ark protocol support.
449
+ * The wallet does not store any data locally and relies on Ark and onchain
450
+ * providers to fetch UTXOs and VTXOs.
451
+ *
452
+ * @example
453
+ * ```typescript
454
+ * // Create a wallet with URL configuration
455
+ * const wallet = await Wallet.create({
456
+ * identity: SingleKey.fromHex('your_private_key'),
457
+ * arkServerUrl: 'https://ark.example.com',
458
+ * esploraUrl: 'https://mempool.space/api'
459
+ * });
460
+ *
461
+ * // Or with custom provider instances (e.g., for Expo/React Native)
462
+ * const wallet = await Wallet.create({
463
+ * identity: SingleKey.fromHex('your_private_key'),
464
+ * arkProvider: new ExpoArkProvider('https://ark.example.com'),
465
+ * indexerProvider: new ExpoIndexerProvider('https://ark.example.com'),
466
+ * esploraUrl: 'https://mempool.space/api'
467
+ * });
468
+ *
469
+ * // Get addresses
470
+ * const arkAddress = await wallet.getAddress();
471
+ * const boardingAddress = await wallet.getBoardingAddress();
472
+ *
473
+ * // Send bitcoin
474
+ * const txid = await wallet.sendBitcoin({
475
+ * address: 'tb1...',
476
+ * amount: 50000
477
+ * });
478
+ * ```
479
+ */
480
+ class Wallet extends ReadonlyWallet {
481
+ constructor(identity, network, networkName, onchainProvider, arkProvider, indexerProvider, arkServerPublicKey, offchainTapscript, boardingTapscript, serverUnrollScript, forfeitOutputScript, forfeitPubkey, dustAmount, walletRepository, contractRepository, renewalConfig) {
482
+ super(identity, network, onchainProvider, indexerProvider, arkServerPublicKey, offchainTapscript, boardingTapscript, dustAmount, walletRepository, contractRepository);
483
+ this.networkName = networkName;
484
+ this.arkProvider = arkProvider;
485
+ this.serverUnrollScript = serverUnrollScript;
486
+ this.forfeitOutputScript = forfeitOutputScript;
487
+ this.forfeitPubkey = forfeitPubkey;
488
+ this.identity = identity;
489
+ this.renewalConfig = {
490
+ enabled: renewalConfig?.enabled ?? false,
491
+ ...vtxo_manager_1.DEFAULT_RENEWAL_CONFIG,
492
+ ...renewalConfig,
493
+ };
494
+ }
495
+ static async create(config) {
496
+ const pubkey = await config.identity.xOnlyPublicKey();
497
+ if (!pubkey) {
498
+ throw new Error("Invalid configured public key");
499
+ }
500
+ const setup = await ReadonlyWallet.setupWalletConfig(config, pubkey);
501
+ // Compute Wallet-specific forfeit and unroll scripts
502
+ // the serverUnrollScript is the one used to create output scripts of the checkpoint transactions
503
+ let serverUnrollScript;
504
+ try {
505
+ const raw = base_1.hex.decode(setup.info.checkpointTapscript);
506
+ serverUnrollScript = tapscript_1.CSVMultisigTapscript.decode(raw);
507
+ }
508
+ catch (e) {
509
+ throw new Error("Invalid checkpointTapscript from server");
510
+ }
511
+ // parse the server forfeit address
512
+ // server is expecting funds to be sent to this address
513
+ const forfeitPubkey = base_1.hex.decode(setup.info.forfeitPubkey).slice(1);
514
+ const forfeitAddress = (0, btc_signer_1.Address)(setup.network).decode(setup.info.forfeitAddress);
515
+ const forfeitOutputScript = btc_signer_1.OutScript.encode(forfeitAddress);
516
+ return new Wallet(config.identity, setup.network, setup.networkName, setup.onchainProvider, setup.arkProvider, setup.indexerProvider, setup.serverPubKey, setup.offchainTapscript, setup.boardingTapscript, serverUnrollScript, forfeitOutputScript, forfeitPubkey, setup.dustAmount, setup.walletRepository, setup.contractRepository, config.renewalConfig);
517
+ }
518
+ /**
519
+ * Convert this wallet to a readonly wallet.
520
+ *
521
+ * @returns A readonly wallet with the same configuration but readonly identity
522
+ * @example
523
+ * ```typescript
524
+ * const wallet = await Wallet.create({ identity: SingleKey.fromHex('...'), ... });
525
+ * const readonlyWallet = await wallet.toReadonly();
526
+ *
527
+ * // Can query balance and addresses
528
+ * const balance = await readonlyWallet.getBalance();
529
+ * const address = await readonlyWallet.getAddress();
530
+ *
531
+ * // But cannot send transactions (type error)
532
+ * // readonlyWallet.sendBitcoin(...); // TypeScript error
533
+ * ```
534
+ */
535
+ async toReadonly() {
536
+ // Check if the identity has a toReadonly method using type guard
537
+ const readonlyIdentity = hasToReadonly(this.identity)
538
+ ? await this.identity.toReadonly()
539
+ : this.identity; // Identity extends ReadonlyIdentity, so this is safe
540
+ return new ReadonlyWallet(readonlyIdentity, this.network, this.onchainProvider, this.indexerProvider, this.arkServerPublicKey, this.offchainTapscript, this.boardingTapscript, this.dustAmount, this.walletRepository, this.contractRepository);
541
+ }
398
542
  async sendBitcoin(params) {
399
543
  if (params.amount <= 0) {
400
544
  throw new Error("Amount must be positive");
@@ -481,7 +625,7 @@ class Wallet {
481
625
  vout: outputs.length - 1,
482
626
  createdAt: new Date(createdAt),
483
627
  forfeitTapLeafScript: this.offchainTapscript.forfeit(),
484
- intentTapLeafScript: this.offchainTapscript.exit(),
628
+ intentTapLeafScript: this.offchainTapscript.forfeit(),
485
629
  isUnrolled: false,
486
630
  isSpent: false,
487
631
  tapTree: this.offchainTapscript.encode(),
@@ -591,285 +735,31 @@ class Wallet {
591
735
  this.makeDeleteIntentSignature(params.inputs),
592
736
  ]);
593
737
  const intentId = await this.safeRegisterIntent(intent);
738
+ const topics = [
739
+ ...signingPublicKeys,
740
+ ...params.inputs.map((input) => `${input.txid}:${input.vout}`),
741
+ ];
742
+ const handler = this.createBatchHandler(intentId, params.inputs, session);
594
743
  const abortController = new AbortController();
595
- // listen to settlement events
596
744
  try {
597
- let step;
598
- const topics = [
599
- ...signingPublicKeys,
600
- ...params.inputs.map((input) => `${input.txid}:${input.vout}`),
601
- ];
602
- const settlementStream = this.arkProvider.getEventStream(abortController.signal, topics);
603
- // batchId, sweepTapTreeRoot and forfeitOutputScript are set once the BatchStarted event is received
604
- let batchId;
605
- let sweepTapTreeRoot;
606
- const vtxoChunks = [];
607
- const connectorsChunks = [];
608
- let vtxoGraph;
609
- let connectorsGraph;
610
- for await (const event of settlementStream) {
611
- if (eventCallback) {
612
- eventCallback(event);
613
- }
614
- switch (event.type) {
615
- // the settlement failed
616
- case ark_1.SettlementEventType.BatchFailed:
617
- throw new Error(event.reason);
618
- case ark_1.SettlementEventType.BatchStarted:
619
- if (step !== undefined) {
620
- continue;
621
- }
622
- const res = await this.handleBatchStartedEvent(event, intentId, this.forfeitPubkey, this.forfeitOutputScript);
623
- if (!res.skip) {
624
- step = event.type;
625
- sweepTapTreeRoot = res.sweepTapTreeRoot;
626
- batchId = res.roundId;
627
- if (!hasOffchainOutputs) {
628
- // if there are no offchain outputs, we don't have to handle musig2 tree signatures
629
- // we can directly advance to the finalization step
630
- step = ark_1.SettlementEventType.TreeNonces;
631
- }
632
- }
633
- break;
634
- case ark_1.SettlementEventType.TreeTx:
635
- if (step !== ark_1.SettlementEventType.BatchStarted &&
636
- step !== ark_1.SettlementEventType.TreeNonces) {
637
- continue;
638
- }
639
- // index 0 = vtxo tree
640
- if (event.batchIndex === 0) {
641
- vtxoChunks.push(event.chunk);
642
- // index 1 = connectors tree
643
- }
644
- else if (event.batchIndex === 1) {
645
- connectorsChunks.push(event.chunk);
646
- }
647
- else {
648
- throw new Error(`Invalid batch index: ${event.batchIndex}`);
649
- }
650
- break;
651
- case ark_1.SettlementEventType.TreeSignature:
652
- if (step !== ark_1.SettlementEventType.TreeNonces) {
653
- continue;
654
- }
655
- if (!hasOffchainOutputs) {
656
- continue;
657
- }
658
- if (!vtxoGraph) {
659
- throw new Error("Vtxo graph not set, something went wrong");
660
- }
661
- // index 0 = vtxo graph
662
- if (event.batchIndex === 0) {
663
- const tapKeySig = base_1.hex.decode(event.signature);
664
- vtxoGraph.update(event.txid, (tx) => {
665
- tx.updateInput(0, {
666
- tapKeySig,
667
- });
668
- });
669
- }
670
- break;
671
- // the server has started the signing process of the vtxo tree transactions
672
- // the server expects the partial musig2 nonces for each tx
673
- case ark_1.SettlementEventType.TreeSigningStarted:
674
- if (step !== ark_1.SettlementEventType.BatchStarted) {
675
- continue;
676
- }
677
- if (hasOffchainOutputs) {
678
- if (!session) {
679
- throw new Error("Signing session not set");
680
- }
681
- if (!sweepTapTreeRoot) {
682
- throw new Error("Sweep tap tree root not set");
683
- }
684
- if (vtxoChunks.length === 0) {
685
- throw new Error("unsigned vtxo graph not received");
686
- }
687
- vtxoGraph = txTree_1.TxTree.create(vtxoChunks);
688
- await this.handleSettlementSigningEvent(event, sweepTapTreeRoot, session, vtxoGraph);
689
- }
690
- step = event.type;
691
- break;
692
- // the musig2 nonces of the vtxo tree transactions are generated
693
- // the server expects now the partial musig2 signatures
694
- case ark_1.SettlementEventType.TreeNonces:
695
- if (step !== ark_1.SettlementEventType.TreeSigningStarted) {
696
- continue;
697
- }
698
- if (hasOffchainOutputs) {
699
- if (!session) {
700
- throw new Error("Signing session not set");
701
- }
702
- const signed = await this.handleSettlementTreeNoncesEvent(event, session);
703
- if (signed) {
704
- step = event.type;
705
- }
706
- break;
707
- }
708
- step = event.type;
709
- break;
710
- // the vtxo tree is signed, craft, sign and submit forfeit transactions
711
- // if any boarding utxos are involved, the settlement tx is also signed
712
- case ark_1.SettlementEventType.BatchFinalization:
713
- if (step !== ark_1.SettlementEventType.TreeNonces) {
714
- continue;
715
- }
716
- if (!this.forfeitOutputScript) {
717
- throw new Error("Forfeit output script not set");
718
- }
719
- if (connectorsChunks.length > 0) {
720
- connectorsGraph = txTree_1.TxTree.create(connectorsChunks);
721
- (0, validation_1.validateConnectorsTxGraph)(event.commitmentTx, connectorsGraph);
722
- }
723
- await this.handleSettlementFinalizationEvent(event, params.inputs, this.forfeitOutputScript, connectorsGraph);
724
- step = event.type;
725
- break;
726
- // the settlement is done, last event to be received
727
- case ark_1.SettlementEventType.BatchFinalized:
728
- if (step !== ark_1.SettlementEventType.BatchFinalization) {
729
- continue;
730
- }
731
- if (event.id === batchId) {
732
- abortController.abort();
733
- return event.commitmentTxid;
734
- }
735
- }
736
- }
745
+ const stream = this.arkProvider.getEventStream(abortController.signal, topics);
746
+ return await batch_1.Batch.join(stream, handler, {
747
+ abortController,
748
+ skipVtxoTreeSigning: !hasOffchainOutputs,
749
+ eventCallback: eventCallback
750
+ ? (event) => Promise.resolve(eventCallback(event))
751
+ : undefined,
752
+ });
737
753
  }
738
754
  catch (error) {
739
- // close the stream
740
- abortController.abort();
741
- try {
742
- // delete the intent to not be stuck in the queue
743
- await this.arkProvider.deleteIntent(deleteIntent);
744
- }
745
- catch (error) {
746
- console.error("failed to delete intent: ", error);
747
- }
755
+ // delete the intent to not be stuck in the queue
756
+ await this.arkProvider.deleteIntent(deleteIntent).catch(() => { });
748
757
  throw error;
749
758
  }
750
- throw new Error("Settlement failed");
751
- }
752
- async notifyIncomingFunds(eventCallback) {
753
- const arkAddress = await this.getAddress();
754
- const boardingAddress = await this.getBoardingAddress();
755
- let onchainStopFunc;
756
- let indexerStopFunc;
757
- if (this.onchainProvider && boardingAddress) {
758
- const findVoutOnTx = (tx) => {
759
- return tx.vout.findIndex((v) => v.scriptpubkey_address === boardingAddress);
760
- };
761
- onchainStopFunc = await this.onchainProvider.watchAddresses([boardingAddress], (txs) => {
762
- // find all utxos belonging to our boarding address
763
- const coins = txs
764
- // filter txs where address is in output
765
- .filter((tx) => findVoutOnTx(tx) !== -1)
766
- // return utxo as Coin
767
- .map((tx) => {
768
- const { txid, status } = tx;
769
- const vout = findVoutOnTx(tx);
770
- const value = Number(tx.vout[vout].value);
771
- return { txid, vout, value, status };
772
- });
773
- // and notify via callback
774
- eventCallback({
775
- type: "utxo",
776
- coins,
777
- });
778
- });
779
- }
780
- if (this.indexerProvider && arkAddress) {
781
- const offchainScript = this.offchainTapscript;
782
- const subscriptionId = await this.indexerProvider.subscribeForScripts([
783
- base_1.hex.encode(offchainScript.pkScript),
784
- ]);
785
- const abortController = new AbortController();
786
- const subscription = this.indexerProvider.getSubscription(subscriptionId, abortController.signal);
787
- indexerStopFunc = async () => {
788
- abortController.abort();
789
- await this.indexerProvider?.unsubscribeForScripts(subscriptionId);
790
- };
791
- // Handle subscription updates asynchronously without blocking
792
- (async () => {
793
- try {
794
- for await (const update of subscription) {
795
- if (update.newVtxos?.length > 0 ||
796
- update.spentVtxos?.length > 0) {
797
- eventCallback({
798
- type: "vtxo",
799
- newVtxos: update.newVtxos.map((vtxo) => (0, utils_1.extendVirtualCoin)(this, vtxo)),
800
- spentVtxos: update.spentVtxos.map((vtxo) => (0, utils_1.extendVirtualCoin)(this, vtxo)),
801
- });
802
- }
803
- }
804
- }
805
- catch (error) {
806
- console.error("Subscription error:", error);
807
- }
808
- })();
809
- }
810
- const stopFunc = () => {
811
- onchainStopFunc?.();
812
- indexerStopFunc?.();
813
- };
814
- return stopFunc;
815
- }
816
- async handleBatchStartedEvent(event, intentId, forfeitPubKey, forfeitOutputScript) {
817
- const utf8IntentId = new TextEncoder().encode(intentId);
818
- const intentIdHash = (0, utils_js_1.sha256)(utf8IntentId);
819
- const intentIdHashStr = base_1.hex.encode(intentIdHash);
820
- let skip = true;
821
- // check if our intent ID hash matches any in the event
822
- for (const idHash of event.intentIdHashes) {
823
- if (idHash === intentIdHashStr) {
824
- if (!this.arkProvider) {
825
- throw new Error("Ark provider not configured");
826
- }
827
- await this.arkProvider.confirmRegistration(intentId);
828
- skip = false;
829
- }
830
- }
831
- if (skip) {
832
- return { skip };
833
- }
834
- const sweepTapscript = tapscript_1.CSVMultisigTapscript.encode({
835
- timelock: {
836
- value: event.batchExpiry,
837
- type: event.batchExpiry >= 512n ? "seconds" : "blocks",
838
- },
839
- pubkeys: [forfeitPubKey],
840
- }).script;
841
- const sweepTapTreeRoot = (0, payment_js_1.tapLeafHash)(sweepTapscript);
842
- return {
843
- roundId: event.id,
844
- sweepTapTreeRoot,
845
- forfeitOutputScript,
846
- skip: false,
847
- };
848
- }
849
- // validates the vtxo tree, creates a signing session and generates the musig2 nonces
850
- async handleSettlementSigningEvent(event, sweepTapTreeRoot, session, vtxoGraph) {
851
- // validate the unsigned vtxo tree
852
- const commitmentTx = btc_signer_1.Transaction.fromPSBT(base_1.base64.decode(event.unsignedCommitmentTx));
853
- (0, validation_1.validateVtxoTxGraph)(vtxoGraph, commitmentTx, sweepTapTreeRoot);
854
- // TODO check if our registered outputs are in the vtxo tree
855
- const sharedOutput = commitmentTx.getOutput(0);
856
- if (!sharedOutput?.amount) {
857
- throw new Error("Shared output not found");
759
+ finally {
760
+ // close the stream
761
+ abortController.abort();
858
762
  }
859
- session.init(vtxoGraph, sweepTapTreeRoot, sharedOutput.amount);
860
- const pubkey = base_1.hex.encode(await session.getPublicKey());
861
- const nonces = await session.getNonces();
862
- await this.arkProvider.submitTreeNonces(event.id, pubkey, nonces);
863
- }
864
- async handleSettlementTreeNoncesEvent(event, session) {
865
- const { hasAllNonces } = await session.aggregatedNonces(event.txid, event.nonces);
866
- // wait to receive and aggregate all nonces before sending signatures
867
- if (!hasAllNonces)
868
- return false;
869
- const signatures = await session.sign();
870
- const pubkey = base_1.hex.encode(await session.getPublicKey());
871
- await this.arkProvider.submitTreeSignatures(event.id, pubkey, signatures);
872
- return true;
873
763
  }
874
764
  async handleSettlementFinalizationEvent(event, inputs, forfeitOutputScript, connectorsGraph) {
875
765
  // the signed forfeits transactions to submit
@@ -959,9 +849,98 @@ class Wallet {
959
849
  : undefined);
960
850
  }
961
851
  }
852
+ /**
853
+ * @implements Batch.Handler interface.
854
+ * @param intentId - The intent ID.
855
+ * @param inputs - The inputs of the intent.
856
+ * @param session - The musig2 signing session, if not provided, the signing will be skipped.
857
+ */
858
+ createBatchHandler(intentId, inputs, session) {
859
+ let sweepTapTreeRoot;
860
+ return {
861
+ onBatchStarted: async (event) => {
862
+ const utf8IntentId = new TextEncoder().encode(intentId);
863
+ const intentIdHash = (0, utils_js_1.sha256)(utf8IntentId);
864
+ const intentIdHashStr = base_1.hex.encode(intentIdHash);
865
+ let skip = true;
866
+ // check if our intent ID hash matches any in the event
867
+ for (const idHash of event.intentIdHashes) {
868
+ if (idHash === intentIdHashStr) {
869
+ if (!this.arkProvider) {
870
+ throw new Error("Ark provider not configured");
871
+ }
872
+ await this.arkProvider.confirmRegistration(intentId);
873
+ skip = false;
874
+ }
875
+ }
876
+ if (skip) {
877
+ return { skip };
878
+ }
879
+ const sweepTapscript = tapscript_1.CSVMultisigTapscript.encode({
880
+ timelock: {
881
+ value: event.batchExpiry,
882
+ type: event.batchExpiry >= 512n ? "seconds" : "blocks",
883
+ },
884
+ pubkeys: [this.forfeitPubkey],
885
+ }).script;
886
+ sweepTapTreeRoot = (0, payment_js_1.tapLeafHash)(sweepTapscript);
887
+ return { skip: false };
888
+ },
889
+ onTreeSigningStarted: async (event, vtxoTree) => {
890
+ if (!session) {
891
+ return { skip: true };
892
+ }
893
+ if (!sweepTapTreeRoot) {
894
+ throw new Error("Sweep tap tree root not set");
895
+ }
896
+ const xOnlyPublicKeys = event.cosignersPublicKeys.map((k) => k.slice(2));
897
+ const signerPublicKey = await session.getPublicKey();
898
+ const xonlySignerPublicKey = signerPublicKey.subarray(1);
899
+ if (!xOnlyPublicKeys.includes(base_1.hex.encode(xonlySignerPublicKey))) {
900
+ // not a cosigner, skip the signing
901
+ return { skip: true };
902
+ }
903
+ // validate the unsigned vtxo tree
904
+ const commitmentTx = btc_signer_1.Transaction.fromPSBT(base_1.base64.decode(event.unsignedCommitmentTx));
905
+ (0, validation_1.validateVtxoTxGraph)(vtxoTree, commitmentTx, sweepTapTreeRoot);
906
+ // TODO check if our registered outputs are in the vtxo tree
907
+ const sharedOutput = commitmentTx.getOutput(0);
908
+ if (!sharedOutput?.amount) {
909
+ throw new Error("Shared output not found");
910
+ }
911
+ await session.init(vtxoTree, sweepTapTreeRoot, sharedOutput.amount);
912
+ const pubkey = base_1.hex.encode(await session.getPublicKey());
913
+ const nonces = await session.getNonces();
914
+ await this.arkProvider.submitTreeNonces(event.id, pubkey, nonces);
915
+ return { skip: false };
916
+ },
917
+ onTreeNonces: async (event) => {
918
+ if (!session) {
919
+ return { fullySigned: true }; // Signing complete (no signing needed)
920
+ }
921
+ const { hasAllNonces } = await session.aggregatedNonces(event.txid, event.nonces);
922
+ // wait to receive and aggregate all nonces before sending signatures
923
+ if (!hasAllNonces)
924
+ return { fullySigned: false };
925
+ const signatures = await session.sign();
926
+ const pubkey = base_1.hex.encode(await session.getPublicKey());
927
+ await this.arkProvider.submitTreeSignatures(event.id, pubkey, signatures);
928
+ return { fullySigned: true };
929
+ },
930
+ onBatchFinalization: async (event, _, connectorTree) => {
931
+ if (!this.forfeitOutputScript) {
932
+ throw new Error("Forfeit output script not set");
933
+ }
934
+ if (connectorTree) {
935
+ (0, validation_1.validateConnectorsTxGraph)(event.commitmentTx, connectorTree);
936
+ }
937
+ await this.handleSettlementFinalizationEvent(event, inputs, this.forfeitOutputScript, connectorTree);
938
+ },
939
+ };
940
+ }
962
941
  async safeRegisterIntent(intent) {
963
942
  try {
964
- return this.arkProvider.registerIntent(intent);
943
+ return await this.arkProvider.registerIntent(intent);
965
944
  }
966
945
  catch (error) {
967
946
  // catch the "already registered by another intent" error
@@ -989,12 +968,11 @@ class Wallet {
989
968
  expire_at: 0,
990
969
  cosigners_public_keys: cosignerPubKeys,
991
970
  };
992
- const encodedMessage = JSON.stringify(message, null, 0);
993
- const proof = intent_1.Intent.create(encodedMessage, inputs, outputs);
971
+ const proof = intent_1.Intent.create(message, inputs, outputs);
994
972
  const signedProof = await this.identity.sign(proof);
995
973
  return {
996
974
  proof: base_1.base64.encode(signedProof.toPSBT()),
997
- message: encodedMessage,
975
+ message,
998
976
  };
999
977
  }
1000
978
  async makeDeleteIntentSignature(coins) {
@@ -1003,12 +981,11 @@ class Wallet {
1003
981
  type: "delete",
1004
982
  expire_at: 0,
1005
983
  };
1006
- const encodedMessage = JSON.stringify(message, null, 0);
1007
- const proof = intent_1.Intent.create(encodedMessage, inputs, []);
984
+ const proof = intent_1.Intent.create(message, inputs, []);
1008
985
  const signedProof = await this.identity.sign(proof);
1009
986
  return {
1010
987
  proof: base_1.base64.encode(signedProof.toPSBT()),
1011
- message: encodedMessage,
988
+ message,
1012
989
  };
1013
990
  }
1014
991
  async makeGetPendingTxIntentSignature(vtxos) {
@@ -1017,12 +994,11 @@ class Wallet {
1017
994
  type: "get-pending-tx",
1018
995
  expire_at: 0,
1019
996
  };
1020
- const encodedMessage = JSON.stringify(message, null, 0);
1021
- const proof = intent_1.Intent.create(encodedMessage, inputs, []);
997
+ const proof = intent_1.Intent.create(message, inputs, []);
1022
998
  const signedProof = await this.identity.sign(proof);
1023
999
  return {
1024
1000
  proof: base_1.base64.encode(signedProof.toPSBT()),
1025
- message: encodedMessage,
1001
+ message,
1026
1002
  };
1027
1003
  }
1028
1004
  /**
@@ -1076,7 +1052,7 @@ class Wallet {
1076
1052
  const inputs = [];
1077
1053
  for (const input of coins) {
1078
1054
  const vtxoScript = base_2.VtxoScript.decode(input.tapTree);
1079
- const sequence = getSequence(input);
1055
+ const sequence = getSequence(input.intentTapLeafScript);
1080
1056
  const unknown = [unknownFields_1.VtxoTaprootTree.encode(input.tapTree)];
1081
1057
  if (input.extraWitness) {
1082
1058
  unknown.push(unknownFields_1.ConditionWitness.encode(input.extraWitness));
@@ -1098,15 +1074,21 @@ class Wallet {
1098
1074
  }
1099
1075
  exports.Wallet = Wallet;
1100
1076
  Wallet.MIN_FEE_RATE = 1; // sats/vbyte
1101
- function getSequence(coin) {
1077
+ function getSequence(tapLeafScript) {
1102
1078
  let sequence = undefined;
1103
1079
  try {
1104
- const scriptWithLeafVersion = coin.intentTapLeafScript[1];
1080
+ const scriptWithLeafVersion = tapLeafScript[1];
1105
1081
  const script = scriptWithLeafVersion.subarray(0, scriptWithLeafVersion.length - 1);
1106
- const params = tapscript_1.CSVMultisigTapscript.decode(script).params;
1107
- sequence = bip68.encode(params.timelock.type === "blocks"
1108
- ? { blocks: Number(params.timelock.value) }
1109
- : { seconds: Number(params.timelock.value) });
1082
+ try {
1083
+ const params = tapscript_1.CSVMultisigTapscript.decode(script).params;
1084
+ sequence = bip68.encode(params.timelock.type === "blocks"
1085
+ ? { blocks: Number(params.timelock.value) }
1086
+ : { seconds: Number(params.timelock.value) });
1087
+ }
1088
+ catch {
1089
+ const params = tapscript_1.CLTVMultisigTapscript.decode(script).params;
1090
+ sequence = Number(params.absoluteTimelock);
1091
+ }
1110
1092
  }
1111
1093
  catch { }
1112
1094
  return sequence;