@arkade-os/sdk 0.4.8 → 0.4.10

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 (37) hide show
  1. package/dist/cjs/contracts/contractManager.js +59 -11
  2. package/dist/cjs/contracts/contractWatcher.js +21 -2
  3. package/dist/cjs/index.js +4 -3
  4. package/dist/cjs/providers/expoIndexer.js +1 -0
  5. package/dist/cjs/providers/indexer.js +1 -0
  6. package/dist/cjs/utils/arkTransaction.js +17 -6
  7. package/dist/cjs/utils/transactionHistory.js +2 -1
  8. package/dist/cjs/wallet/serviceWorker/wallet-message-handler.js +109 -29
  9. package/dist/cjs/wallet/serviceWorker/wallet.js +25 -2
  10. package/dist/cjs/wallet/vtxo-manager.js +81 -50
  11. package/dist/cjs/wallet/wallet.js +46 -34
  12. package/dist/cjs/worker/errors.js +3 -4
  13. package/dist/cjs/worker/messageBus.js +7 -0
  14. package/dist/esm/contracts/contractManager.js +59 -11
  15. package/dist/esm/contracts/contractWatcher.js +21 -2
  16. package/dist/esm/index.js +2 -2
  17. package/dist/esm/providers/expoIndexer.js +1 -0
  18. package/dist/esm/providers/indexer.js +1 -0
  19. package/dist/esm/utils/arkTransaction.js +17 -6
  20. package/dist/esm/utils/transactionHistory.js +2 -1
  21. package/dist/esm/wallet/serviceWorker/wallet-message-handler.js +109 -29
  22. package/dist/esm/wallet/serviceWorker/wallet.js +26 -3
  23. package/dist/esm/wallet/vtxo-manager.js +81 -50
  24. package/dist/esm/wallet/wallet.js +46 -34
  25. package/dist/esm/worker/errors.js +2 -3
  26. package/dist/esm/worker/messageBus.js +7 -0
  27. package/dist/types/contracts/contractManager.d.ts +10 -0
  28. package/dist/types/index.d.ts +2 -2
  29. package/dist/types/repositories/serialization.d.ts +1 -0
  30. package/dist/types/utils/transactionHistory.d.ts +1 -1
  31. package/dist/types/wallet/index.d.ts +2 -0
  32. package/dist/types/wallet/serviceWorker/wallet-message-handler.d.ts +23 -6
  33. package/dist/types/wallet/serviceWorker/wallet.d.ts +9 -1
  34. package/dist/types/wallet/vtxo-manager.d.ts +5 -0
  35. package/dist/types/worker/errors.d.ts +1 -0
  36. package/dist/types/worker/messageBus.d.ts +6 -0
  37. package/package.json +1 -1
@@ -157,7 +157,12 @@ class VtxoManager {
157
157
  this.knownBoardingUtxos = new Set();
158
158
  this.sweptBoardingUtxos = new Set();
159
159
  this.pollInProgress = false;
160
+ this.disposed = false;
160
161
  this.consecutivePollFailures = 0;
162
+ // Guards against renewal feedback loop: when renewVtxos() settles, the
163
+ // server emits new VTXOs → vtxo_received → renewVtxos() again → infinite loop.
164
+ this.renewalInProgress = false;
165
+ this.lastRenewalTimestamp = 0;
161
166
  // Normalize: prefer settlementConfig, fall back to renewalConfig, default to enabled
162
167
  if (settlementConfig !== undefined) {
163
168
  this.settlementConfig = settlementConfig;
@@ -341,32 +346,43 @@ class VtxoManager {
341
346
  * ```
342
347
  */
343
348
  async renewVtxos(eventCallback) {
344
- // Get all VTXOs (including recoverable ones)
345
- // Use default threshold to bypass settlementConfig gate (manual API should always work)
346
- const vtxos = await this.getExpiringVtxos(this.settlementConfig !== false &&
347
- this.settlementConfig?.vtxoThreshold !== undefined
348
- ? this.settlementConfig.vtxoThreshold * 1000
349
- : exports.DEFAULT_RENEWAL_CONFIG.thresholdMs);
350
- if (vtxos.length === 0) {
351
- throw new Error("No VTXOs available to renew");
352
- }
353
- const totalAmount = vtxos.reduce((sum, vtxo) => sum + vtxo.value, 0);
354
- // Get dust amount from wallet
355
- const dustAmount = getDustAmount(this.wallet);
356
- // Check if total amount is above dust threshold
357
- if (BigInt(totalAmount) < dustAmount) {
358
- throw new Error(`Total amount ${totalAmount} is below dust threshold ${dustAmount}`);
349
+ if (this.renewalInProgress) {
350
+ throw new Error("Renewal already in progress");
351
+ }
352
+ this.renewalInProgress = true;
353
+ try {
354
+ // Get all VTXOs (including recoverable ones)
355
+ // Use default threshold to bypass settlementConfig gate (manual API should always work)
356
+ const vtxos = await this.getExpiringVtxos(this.settlementConfig !== false &&
357
+ this.settlementConfig?.vtxoThreshold !== undefined
358
+ ? this.settlementConfig.vtxoThreshold * 1000
359
+ : exports.DEFAULT_RENEWAL_CONFIG.thresholdMs);
360
+ if (vtxos.length === 0) {
361
+ throw new Error("No VTXOs available to renew");
362
+ }
363
+ const totalAmount = vtxos.reduce((sum, vtxo) => sum + vtxo.value, 0);
364
+ // Get dust amount from wallet
365
+ const dustAmount = getDustAmount(this.wallet);
366
+ // Check if total amount is above dust threshold
367
+ if (BigInt(totalAmount) < dustAmount) {
368
+ throw new Error(`Total amount ${totalAmount} is below dust threshold ${dustAmount}`);
369
+ }
370
+ const arkAddress = await this.wallet.getAddress();
371
+ const txid = await this.wallet.settle({
372
+ inputs: vtxos,
373
+ outputs: [
374
+ {
375
+ address: arkAddress,
376
+ amount: BigInt(totalAmount),
377
+ },
378
+ ],
379
+ }, eventCallback);
380
+ this.lastRenewalTimestamp = Date.now();
381
+ return txid;
382
+ }
383
+ finally {
384
+ this.renewalInProgress = false;
359
385
  }
360
- const arkAddress = await this.wallet.getAddress();
361
- return this.wallet.settle({
362
- inputs: vtxos,
363
- outputs: [
364
- {
365
- address: arkAddress,
366
- amount: BigInt(totalAmount),
367
- },
368
- ],
369
- }, eventCallback);
370
386
  }
371
387
  // ========== Boarding UTXO Sweep Methods ==========
372
388
  /**
@@ -534,7 +550,11 @@ class VtxoManager {
534
550
  }
535
551
  // Start polling for boarding UTXOs independently of contract manager
536
552
  // SSE setup. Use a short delay to let the wallet finish construction.
537
- setTimeout(() => this.startBoardingUtxoPoll(), 1000);
553
+ this.startupPollTimeoutId = setTimeout(() => {
554
+ if (this.disposed)
555
+ return;
556
+ this.startBoardingUtxoPoll();
557
+ }, 1000);
538
558
  try {
539
559
  const [delegatorManager, contractManager, destination] = await Promise.all([
540
560
  this.wallet.getDelegatorManager(),
@@ -545,28 +565,33 @@ class VtxoManager {
545
565
  if (event.type !== "vtxo_received") {
546
566
  return;
547
567
  }
548
- this.renewVtxos().catch((e) => {
549
- if (e instanceof Error) {
550
- if (e.message.includes("No VTXOs available to renew")) {
551
- // Not an error, just no VTXO eligible for renewal.
552
- return;
568
+ const msSinceLastRenewal = Date.now() - this.lastRenewalTimestamp;
569
+ const shouldRenew = !this.renewalInProgress &&
570
+ msSinceLastRenewal >= VtxoManager.RENEWAL_COOLDOWN_MS;
571
+ if (shouldRenew) {
572
+ this.renewVtxos().catch((e) => {
573
+ if (e instanceof Error) {
574
+ if (e.message.includes("No VTXOs available to renew")) {
575
+ // Not an error, just no VTXO eligible for renewal.
576
+ return;
577
+ }
578
+ if (e.message.includes("is below dust threshold")) {
579
+ // Not an error, just below dust threshold.
580
+ // As more VTXOs are received, the threshold will be raised.
581
+ return;
582
+ }
583
+ if (e.message.includes("VTXO_ALREADY_REGISTERED") ||
584
+ e.message.includes("duplicated input")) {
585
+ // VTXO is already being used in a concurrent
586
+ // user-initiated operation. Skip silently — the
587
+ // wallet's tx lock serializes these, but the
588
+ // renewal will retry on the next cycle.
589
+ return;
590
+ }
553
591
  }
554
- if (e.message.includes("is below dust threshold")) {
555
- // Not an error, just below dust threshold.
556
- // As more VTXOs are received, the threshold will be raised.
557
- return;
558
- }
559
- if (e.message.includes("VTXO_ALREADY_REGISTERED") ||
560
- e.message.includes("duplicated input")) {
561
- // VTXO is already being used in a concurrent
562
- // user-initiated operation. Skip silently — the
563
- // wallet's tx lock serializes these, but the
564
- // renewal will retry on the next cycle.
565
- return;
566
- }
567
- }
568
- console.error("Error renewing VTXOs:", e);
569
- });
592
+ console.error("Error renewing VTXOs:", e);
593
+ });
594
+ }
570
595
  delegatorManager
571
596
  ?.delegate(event.vtxos, destination)
572
597
  .catch((e) => {
@@ -606,7 +631,7 @@ class VtxoManager {
606
631
  this.pollBoardingUtxos();
607
632
  }
608
633
  schedulePoll() {
609
- if (this.settlementConfig === false)
634
+ if (this.disposed || this.settlementConfig === false)
610
635
  return;
611
636
  const delay = this.getNextPollDelay();
612
637
  this.pollTimeoutId = setTimeout(() => this.pollBoardingUtxos(), delay);
@@ -680,8 +705,8 @@ class VtxoManager {
680
705
  const expired = boardingUtxos.filter((utxo) => (0, arkTransaction_1.hasBoardingTxExpired)(utxo, boardingTimelock, chainTipHeight));
681
706
  expiredSet = new Set(expired.map((u) => `${u.txid}:${u.vout}`));
682
707
  }
683
- catch {
684
- return;
708
+ catch (e) {
709
+ throw e instanceof Error ? e : new Error(String(e));
685
710
  }
686
711
  const unsettledUtxos = boardingUtxos.filter((u) => !this.knownBoardingUtxos.has(`${u.txid}:${u.vout}`) &&
687
712
  !expiredSet.has(`${u.txid}:${u.vout}`));
@@ -703,6 +728,11 @@ class VtxoManager {
703
728
  }
704
729
  async dispose() {
705
730
  this.disposePromise ?? (this.disposePromise = (async () => {
731
+ this.disposed = true;
732
+ if (this.startupPollTimeoutId) {
733
+ clearTimeout(this.startupPollTimeoutId);
734
+ this.startupPollTimeoutId = undefined;
735
+ }
706
736
  if (this.pollTimeoutId) {
707
737
  clearTimeout(this.pollTimeoutId);
708
738
  this.pollTimeoutId = undefined;
@@ -719,3 +749,4 @@ class VtxoManager {
719
749
  }
720
750
  exports.VtxoManager = VtxoManager;
721
751
  VtxoManager.MAX_BACKOFF_MS = 5 * 60 * 1000; // 5 minutes
752
+ VtxoManager.RENEWAL_COOLDOWN_MS = 30000; // 30 seconds
@@ -278,27 +278,33 @@ class ReadonlyWallet {
278
278
  const scriptMap = await this.getScriptMap();
279
279
  const f = filter ?? { withRecoverable: true, withUnrolled: false };
280
280
  const allExtended = [];
281
- // Query each script separately so we can extend VTXOs with the correct tapscript
282
- for (const [scriptHex, vtxoScript] of scriptMap) {
283
- const response = await this.indexerProvider.getVtxos({
284
- scripts: [scriptHex],
285
- });
286
- let vtxos = response.vtxos.filter(_1.isSpendable);
287
- if (!f.withRecoverable) {
288
- vtxos = vtxos.filter((vtxo) => !(0, _1.isRecoverable)(vtxo) && !(0, _1.isExpired)(vtxo));
289
- }
290
- if (f.withUnrolled) {
291
- const spentVtxos = response.vtxos.filter((vtxo) => !(0, _1.isSpendable)(vtxo));
292
- vtxos.push(...spentVtxos.filter((vtxo) => vtxo.isUnrolled));
281
+ // Batch all scripts into a single indexer call
282
+ const allScripts = [...scriptMap.keys()];
283
+ const response = await this.indexerProvider.getVtxos({
284
+ scripts: allScripts,
285
+ });
286
+ for (const vtxo of response.vtxos) {
287
+ const vtxoScript = vtxo.script
288
+ ? scriptMap.get(vtxo.script)
289
+ : undefined;
290
+ if (!vtxoScript)
291
+ continue;
292
+ if ((0, _1.isSpendable)(vtxo)) {
293
+ if (!f.withRecoverable &&
294
+ ((0, _1.isRecoverable)(vtxo) || (0, _1.isExpired)(vtxo))) {
295
+ continue;
296
+ }
293
297
  }
294
- for (const vtxo of vtxos) {
295
- allExtended.push({
296
- ...vtxo,
297
- forfeitTapLeafScript: vtxoScript.forfeit(),
298
- intentTapLeafScript: vtxoScript.forfeit(),
299
- tapTree: vtxoScript.encode(),
300
- });
298
+ else {
299
+ if (!f.withUnrolled || !vtxo.isUnrolled)
300
+ continue;
301
301
  }
302
+ allExtended.push({
303
+ ...vtxo,
304
+ forfeitTapLeafScript: vtxoScript.forfeit(),
305
+ intentTapLeafScript: vtxoScript.forfeit(),
306
+ tapTree: vtxoScript.encode(),
307
+ });
302
308
  }
303
309
  // Update cache with fresh data
304
310
  await this.walletRepository.saveVtxos(address, allExtended);
@@ -310,7 +316,7 @@ class ReadonlyWallet {
310
316
  const { boardingTxs, commitmentsToIgnore } = await this.getBoardingTxs();
311
317
  const getTxCreatedAt = (txid) => this.indexerProvider
312
318
  .getVtxos({ outpoints: [{ txid, vout: 0 }] })
313
- .then((res) => res.vtxos[0]?.createdAt.getTime() || 0);
319
+ .then((res) => res.vtxos[0]?.createdAt.getTime());
314
320
  return (0, transactionHistory_1.buildTransactionHistory)(response.vtxos, boardingTxs, commitmentsToIgnore, getTxCreatedAt);
315
321
  }
316
322
  async getBoardingTxs() {
@@ -1344,23 +1350,29 @@ class Wallet extends ReadonlyWallet {
1344
1350
  async finalizePendingTxs(vtxos) {
1345
1351
  const MAX_INPUTS_PER_INTENT = 20;
1346
1352
  if (!vtxos || vtxos.length === 0) {
1347
- // Query per-script so each VTXO is extended with the correct tapscript
1353
+ // Batch all scripts into a single indexer call
1348
1354
  const scriptMap = await this.getScriptMap();
1349
1355
  const allExtended = [];
1350
- for (const [scriptHex, vtxoScript] of scriptMap) {
1351
- const { vtxos: fetchedVtxos } = await this.indexerProvider.getVtxos({
1352
- scripts: [scriptHex],
1353
- });
1354
- const pending = fetchedVtxos.filter((vtxo) => vtxo.virtualStatus.state !== "swept" &&
1355
- vtxo.virtualStatus.state !== "settled");
1356
- for (const vtxo of pending) {
1357
- allExtended.push({
1358
- ...vtxo,
1359
- forfeitTapLeafScript: vtxoScript.forfeit(),
1360
- intentTapLeafScript: vtxoScript.forfeit(),
1361
- tapTree: vtxoScript.encode(),
1362
- });
1356
+ const allScripts = [...scriptMap.keys()];
1357
+ const { vtxos: fetchedVtxos } = await this.indexerProvider.getVtxos({
1358
+ scripts: allScripts,
1359
+ });
1360
+ for (const vtxo of fetchedVtxos) {
1361
+ const vtxoScript = vtxo.script
1362
+ ? scriptMap.get(vtxo.script)
1363
+ : undefined;
1364
+ if (!vtxoScript)
1365
+ continue;
1366
+ if (vtxo.virtualStatus.state === "swept" ||
1367
+ vtxo.virtualStatus.state === "settled") {
1368
+ continue;
1363
1369
  }
1370
+ allExtended.push({
1371
+ ...vtxo,
1372
+ forfeitTapLeafScript: vtxoScript.forfeit(),
1373
+ intentTapLeafScript: vtxoScript.forfeit(),
1374
+ tapTree: vtxoScript.encode(),
1375
+ });
1364
1376
  }
1365
1377
  if (allExtended.length === 0) {
1366
1378
  return { finalized: [], pending: [] };
@@ -1,17 +1,16 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.ServiceWorkerTimeoutError = exports.MessageBusNotInitializedError = void 0;
3
+ exports.ServiceWorkerTimeoutError = exports.MessageBusNotInitializedError = exports.MESSAGE_BUS_NOT_INITIALIZED = void 0;
4
+ exports.MESSAGE_BUS_NOT_INITIALIZED = "MessageBus not initialized";
4
5
  class MessageBusNotInitializedError extends Error {
5
6
  constructor() {
6
- super("MessageBus not initialized");
7
- this.name = "MessageBusNotInitializedError";
7
+ super(exports.MESSAGE_BUS_NOT_INITIALIZED);
8
8
  }
9
9
  }
10
10
  exports.MessageBusNotInitializedError = MessageBusNotInitializedError;
11
11
  class ServiceWorkerTimeoutError extends Error {
12
12
  constructor(detail) {
13
13
  super(detail);
14
- this.name = "ServiceWorkerTimeoutError";
15
14
  }
16
15
  }
17
16
  exports.ServiceWorkerTimeoutError = ServiceWorkerTimeoutError;
@@ -152,8 +152,12 @@ class MessageBus {
152
152
  identity,
153
153
  arkServerUrl: config.arkServer.url,
154
154
  arkServerPublicKey: config.arkServer.publicKey,
155
+ indexerUrl: config.indexerUrl,
156
+ esploraUrl: config.esploraUrl,
155
157
  storage,
156
158
  delegatorProvider,
159
+ settlementConfig: config.settlementConfig,
160
+ watcherConfig: config.watcherConfig,
157
161
  });
158
162
  return { wallet, arkProvider, readonlyWallet: wallet };
159
163
  }
@@ -163,8 +167,11 @@ class MessageBus {
163
167
  identity,
164
168
  arkServerUrl: config.arkServer.url,
165
169
  arkServerPublicKey: config.arkServer.publicKey,
170
+ indexerUrl: config.indexerUrl,
171
+ esploraUrl: config.esploraUrl,
166
172
  storage,
167
173
  delegatorProvider,
174
+ watcherConfig: config.watcherConfig,
168
175
  });
169
176
  return { readonlyWallet, arkProvider };
170
177
  }
@@ -71,9 +71,10 @@ export class ContractManager {
71
71
  }
72
72
  // Load persisted contracts
73
73
  const contracts = await this.config.contractRepository.getContracts();
74
- // fetch latest VTXOs for all contracts, ensure cache is up to date
74
+ // fetch all VTXOs (including spent/swept) for all contracts,
75
+ // so the repository has full history for transaction history and balance
75
76
  // TODO: what if the user has 1k contracts?
76
- await this.getVtxosForContracts(contracts);
77
+ await this.fetchContractVxosFromIndexer(contracts, true);
77
78
  // add all contracts to the watcher
78
79
  const now = Date.now();
79
80
  for (const contract of contracts) {
@@ -136,8 +137,8 @@ export class ContractManager {
136
137
  };
137
138
  // Persist
138
139
  await this.config.contractRepository.saveContract(contract);
139
- // ensure we have the latest VTXOs for this contract
140
- await this.getVtxosForContracts([contract]);
140
+ // fetch all VTXOs (including spent/swept) for this contract
141
+ await this.fetchContractVxosFromIndexer([contract], true);
141
142
  // Add to watcher
142
143
  await this.watcher.addContract(contract);
143
144
  return contract;
@@ -302,6 +303,14 @@ export class ContractManager {
302
303
  this.eventCallbacks.delete(callback);
303
304
  };
304
305
  }
306
+ /**
307
+ * Force a full VTXO refresh from the indexer for all contracts.
308
+ * Populates the wallet repository with complete VTXO history.
309
+ */
310
+ async refreshVtxos() {
311
+ const contracts = await this.config.contractRepository.getContracts();
312
+ await this.fetchContractVxosFromIndexer(contracts, true);
313
+ }
305
314
  /**
306
315
  * Check if currently watching.
307
316
  */
@@ -331,11 +340,13 @@ export class ContractManager {
331
340
  case "vtxo_spent":
332
341
  await this.fetchContractVxosFromIndexer([event.contract], true);
333
342
  break;
334
- case "connection_reset":
335
- // Refetch all VTXOs for all active contracts
343
+ case "connection_reset": {
344
+ // Refetch all VTXOs (including spent/swept) for all active
345
+ // contracts so the repo stays consistent with bootstrap state
336
346
  const activeWatchedContracts = this.watcher.getActiveContracts();
337
- await this.fetchContractVxosFromIndexer(activeWatchedContracts, false);
347
+ await this.fetchContractVxosFromIndexer(activeWatchedContracts, true);
338
348
  break;
349
+ }
339
350
  case "contract_expired":
340
351
  // just update DB
341
352
  await this.config.contractRepository.saveContract(event.contract);
@@ -362,11 +373,48 @@ export class ContractManager {
362
373
  return result;
363
374
  }
364
375
  async fetchContractVtxosBulk(contracts, includeSpent) {
365
- const result = new Map();
366
- await Promise.all(contracts.map(async (contract) => {
376
+ if (contracts.length === 0) {
377
+ return new Map();
378
+ }
379
+ // For a single contract, use the paginated path directly.
380
+ if (contracts.length === 1) {
381
+ const contract = contracts[0];
367
382
  const vtxos = await this.fetchContractVtxosPaginated(contract, includeSpent);
368
- result.set(contract.script, vtxos);
369
- }));
383
+ return new Map([[contract.script, vtxos]]);
384
+ }
385
+ // For multiple contracts, batch all scripts into a single indexer call
386
+ // per page to minimise round-trips. Results are keyed by script so we
387
+ // can distribute them back to the correct contract afterwards.
388
+ const scriptToContract = new Map(contracts.map((c) => [c.script, c]));
389
+ const result = new Map(contracts.map((c) => [c.script, []]));
390
+ const scripts = contracts.map((c) => c.script);
391
+ const pageSize = 100;
392
+ const opts = includeSpent ? {} : { spendableOnly: true };
393
+ let pageIndex = 0;
394
+ let hasMore = true;
395
+ while (hasMore) {
396
+ const { vtxos, page } = await this.config.indexerProvider.getVtxos({
397
+ scripts,
398
+ ...opts,
399
+ pageIndex,
400
+ pageSize,
401
+ });
402
+ for (const vtxo of vtxos) {
403
+ // Match the VTXO back to its contract via the script field
404
+ // populated by the indexer.
405
+ if (!vtxo.script)
406
+ continue;
407
+ const contract = scriptToContract.get(vtxo.script);
408
+ if (!contract)
409
+ continue;
410
+ result.get(contract.script).push({
411
+ ...extendVtxoFromContract(vtxo, contract),
412
+ contractScript: contract.script,
413
+ });
414
+ }
415
+ hasMore = page ? vtxos.length === pageSize : false;
416
+ pageIndex++;
417
+ }
370
418
  return result;
371
419
  }
372
420
  async fetchContractVtxosPaginated(contract, includeSpent) {
@@ -410,8 +410,27 @@ export class ContractWatcher {
410
410
  }
411
411
  return;
412
412
  }
413
- this.subscriptionId =
414
- await this.config.indexerProvider.subscribeForScripts(scriptsToWatch, this.subscriptionId);
413
+ try {
414
+ this.subscriptionId =
415
+ await this.config.indexerProvider.subscribeForScripts(scriptsToWatch, this.subscriptionId);
416
+ }
417
+ catch (error) {
418
+ // If we sent a stale subscription ID that the server no longer
419
+ // recognises, clear it and retry to create a fresh subscription.
420
+ // The server currently returns HTTP 500 with a JSON body whose
421
+ // message field looks like "subscription <uuid> not found".
422
+ // All other errors (network failures, parse errors, etc.) are rethrown.
423
+ const isStale = error instanceof Error &&
424
+ /subscription\s+\S+\s+not\s+found/i.test(error.message);
425
+ if (this.subscriptionId && isStale) {
426
+ this.subscriptionId = undefined;
427
+ this.subscriptionId =
428
+ await this.config.indexerProvider.subscribeForScripts(scriptsToWatch);
429
+ }
430
+ else {
431
+ throw error;
432
+ }
433
+ }
415
434
  }
416
435
  /**
417
436
  * Main listening loop for subscription events.
package/dist/esm/index.js CHANGED
@@ -40,7 +40,7 @@ export * as asset from './extension/asset/index.js';
40
40
  import { ContractManager, ContractWatcher, contractHandlers, DefaultContractHandler, DelegateContractHandler, VHTLCContractHandler, encodeArkContract, decodeArkContract, contractFromArkContract, contractFromArkContractWithAddress, isArkContract, } from './contracts/index.js';
41
41
  import { closeDatabase, openDatabase } from './repositories/indexedDB/manager.js';
42
42
  import { WalletMessageHandler, WalletNotInitializedError, ReadonlyWalletError, DelegatorNotConfiguredError, } from './wallet/serviceWorker/wallet-message-handler.js';
43
- import { MessageBusNotInitializedError, ServiceWorkerTimeoutError, } from './worker/errors.js';
43
+ import { MESSAGE_BUS_NOT_INITIALIZED, MessageBusNotInitializedError, ServiceWorkerTimeoutError, } from './worker/errors.js';
44
44
  export {
45
45
  // Wallets
46
46
  Wallet, ReadonlyWallet, SingleKey, ReadonlySingleKey, SeedIdentity, MnemonicIdentity, ReadonlyDescriptorIdentity, OnchainWallet, Ramps, VtxoManager, DelegatorManagerImpl, RestDelegatorProvider,
@@ -51,7 +51,7 @@ ArkAddress, DefaultVtxo, DelegateVtxo, VtxoScript, VHTLC,
51
51
  // Enums
52
52
  TxType, IndexerTxType, ChainTxType, SettlementEventType,
53
53
  // Service Worker
54
- setupServiceWorker, MessageBus, WalletMessageHandler, WalletNotInitializedError, ReadonlyWalletError, DelegatorNotConfiguredError, MessageBusNotInitializedError, ServiceWorkerTimeoutError, ServiceWorkerWallet, ServiceWorkerReadonlyWallet,
54
+ setupServiceWorker, MessageBus, WalletMessageHandler, WalletNotInitializedError, ReadonlyWalletError, DelegatorNotConfiguredError, MESSAGE_BUS_NOT_INITIALIZED, MessageBusNotInitializedError, ServiceWorkerTimeoutError, ServiceWorkerWallet, ServiceWorkerReadonlyWallet,
55
55
  // Tapscript
56
56
  decodeTapscript, MultisigTapscript, CSVMultisigTapscript, ConditionCSVMultisigTapscript, ConditionMultisigTapscript, CLTVMultisigTapscript, TapTreeCoder,
57
57
  // Ark PSBT fields
@@ -28,6 +28,7 @@ function convertVtxo(vtxo) {
28
28
  createdAt: new Date(Number(vtxo.createdAt) * 1000),
29
29
  isUnrolled: vtxo.isUnrolled,
30
30
  isSpent: vtxo.isSpent,
31
+ script: vtxo.script,
31
32
  assets: vtxo.assets?.map((a) => ({
32
33
  assetId: a.assetId,
33
34
  amount: Number(a.amount),
@@ -402,6 +402,7 @@ function convertVtxo(vtxo) {
402
402
  createdAt: new Date(Number(vtxo.createdAt) * 1000),
403
403
  isUnrolled: vtxo.isUnrolled,
404
404
  isSpent: vtxo.isSpent,
405
+ script: vtxo.script,
405
406
  assets: vtxo.assets?.map((a) => ({
406
407
  assetId: a.assetId,
407
408
  amount: Number(a.amount),
@@ -8,6 +8,7 @@ import { P2A } from './anchor.js';
8
8
  import { setArkPsbtField, VtxoTaprootTree } from './unknownFields.js';
9
9
  import { Transaction } from './transaction.js';
10
10
  import { ArkAddress } from '../script/address.js';
11
+ import { Extension } from '../extension/index.js';
11
12
  /**
12
13
  * Builds an offchain transaction with checkpoint transactions.
13
14
  *
@@ -21,16 +22,26 @@ import { ArkAddress } from '../script/address.js';
21
22
  * @returns Object containing the virtual transaction and checkpoint transactions
22
23
  */
23
24
  export function buildOffchainTx(inputs, outputs, serverUnrollScript) {
24
- let hasOpReturn = false;
25
+ // TODO: use arkd /info
26
+ const MAX_OP_RETURN = 2;
27
+ let countOpReturn = 0;
28
+ let hasExtensionOutput = false;
25
29
  for (const [index, output] of outputs.entries()) {
26
30
  if (!output.script)
27
31
  throw new Error(`missing output script ${index}`);
28
- const isOpReturn = Script.decode(output.script)[0] === "RETURN";
29
- if (!isOpReturn)
32
+ const isExtension = Extension.isExtension(output.script);
33
+ const isOpReturn = isExtension || Script.decode(output.script)[0] === "RETURN";
34
+ if (isOpReturn) {
35
+ countOpReturn++;
36
+ }
37
+ if (!isExtension)
30
38
  continue;
31
- if (hasOpReturn)
32
- throw new Error("multiple OP_RETURN outputs");
33
- hasOpReturn = true;
39
+ if (hasExtensionOutput)
40
+ throw new Error("multiple extension outputs");
41
+ hasExtensionOutput = true;
42
+ }
43
+ if (countOpReturn > MAX_OP_RETURN) {
44
+ throw new Error(`too many OP_RETURN outputs: ${countOpReturn} > ${MAX_OP_RETURN}`);
34
45
  }
35
46
  const checkpoints = inputs.map((input) => buildCheckpointTx(input, serverUnrollScript));
36
47
  const arkTx = buildVirtualTx(checkpoints.map((c) => c.input), outputs);
@@ -115,7 +115,8 @@ export async function buildTransactionHistory(vtxos, allBoardingTxs, commitments
115
115
  txAmount = spentAmount;
116
116
  // TODO: fetch the vtxo with /v1/indexer/vtxos?outpoints=<vtxo.arkTxid:0> to know when the tx was made
117
117
  txTime = getTxCreatedAt
118
- ? await getTxCreatedAt(vtxo.arkTxId)
118
+ ? ((await getTxCreatedAt(vtxo.arkTxId)) ??
119
+ vtxo.createdAt.getTime() + 1)
119
120
  : vtxo.createdAt.getTime() + 1;
120
121
  }
121
122
  const assets = subtractAssets(allSpent, changes);