@arkade-os/sdk 0.4.20 → 0.4.22

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.
@@ -39,10 +39,28 @@ const contractManager_1 = require("../contracts/contractManager");
39
39
  const handlers_1 = require("../contracts/handlers");
40
40
  const helpers_1 = require("../contracts/handlers/helpers");
41
41
  const syncCursors_1 = require("../utils/syncCursors");
42
- // Hardcoded unilateral exit delay for mainnet (~7 days in seconds).
43
- // Pinned here so that address derivation stays stable for existing mainnet
44
- // wallets even after the server lowers the delay it advertises.
42
+ // Historical unilateral exit delay for mainnet (~7 days in seconds).
43
+ // Kept so existing wallets can still discover and spend VTXOs sent to the
44
+ // legacy address after arkd starts advertising a different delay.
45
45
  const MAINNET_UNILATERAL_EXIT_DELAY = 605184n;
46
+ function delayToTimelock(delay) {
47
+ return {
48
+ value: delay,
49
+ type: delay < 512n ? "blocks" : "seconds",
50
+ };
51
+ }
52
+ function dedupeTimelocks(timelocks) {
53
+ const seen = new Set();
54
+ const deduped = [];
55
+ for (const timelock of timelocks) {
56
+ const sequence = (0, helpers_1.timelockToSequence)(timelock).toString();
57
+ if (seen.has(sequence))
58
+ continue;
59
+ seen.add(sequence);
60
+ deduped.push(timelock);
61
+ }
62
+ return deduped;
63
+ }
46
64
  /**
47
65
  * Type guard function to check if an identity has a toReadonly method.
48
66
  */
@@ -56,7 +74,7 @@ class ReadonlyWallet {
56
74
  get assetManager() {
57
75
  return this._assetManager;
58
76
  }
59
- constructor(identity, network, onchainProvider, indexerProvider, arkServerPublicKey, offchainTapscript, boardingTapscript, dustAmount, walletRepository, contractRepository, delegatorProvider, watcherConfig) {
77
+ constructor(identity, network, onchainProvider, indexerProvider, arkServerPublicKey, offchainTapscript, boardingTapscript, dustAmount, walletRepository, contractRepository, delegatorProvider, watcherConfig, walletContractTimelocks) {
60
78
  this.identity = identity;
61
79
  this.network = network;
62
80
  this.onchainProvider = onchainProvider;
@@ -89,6 +107,15 @@ class ReadonlyWallet {
89
107
  }
90
108
  this.watcherConfig = watcherConfig;
91
109
  this._assetManager = new asset_manager_1.ReadonlyAssetManager(this.indexerProvider);
110
+ // Defensive for direct-construction callers; setupWalletConfig already
111
+ // passes a deduped list through the public create() factories.
112
+ this.walletContractTimelocks =
113
+ walletContractTimelocks && walletContractTimelocks.length > 0
114
+ ? dedupeTimelocks(walletContractTimelocks)
115
+ : [
116
+ this.offchainTapscript.options.csvTimelock ??
117
+ default_1.DefaultVtxo.Script.DEFAULT_TIMELOCK,
118
+ ];
92
119
  }
93
120
  /**
94
121
  * Protected helper to set up shared wallet configuration.
@@ -144,17 +171,17 @@ class ReadonlyWallet {
144
171
  throw new Error("invalid exitTimelock");
145
172
  }
146
173
  }
147
- // On mainnet, pin the unilateral exit delay to the historical value so
148
- // that addresses derived by existing wallets remain stable even if the
149
- // server starts advertising a shorter delay.
150
- const unilateralExitDelay = info.network === "bitcoin"
151
- ? MAINNET_UNILATERAL_EXIT_DELAY
152
- : info.unilateralExitDelay;
174
+ const arkdExitTimelock = delayToTimelock(info.unilateralExitDelay);
153
175
  // create unilateral exit timelock
154
- const exitTimelock = config.exitTimelock ?? {
155
- value: unilateralExitDelay,
156
- type: unilateralExitDelay < 512n ? "blocks" : "seconds",
157
- };
176
+ const exitTimelock = config.exitTimelock ?? arkdExitTimelock;
177
+ const walletContractTimelocks = config.exitTimelock
178
+ ? [exitTimelock]
179
+ : dedupeTimelocks([
180
+ arkdExitTimelock,
181
+ ...(info.network === "bitcoin"
182
+ ? [delayToTimelock(MAINNET_UNILATERAL_EXIT_DELAY)]
183
+ : []),
184
+ ]);
158
185
  // validate boarding timelock passed in config if any
159
186
  if (config.boardingTimelock) {
160
187
  const { value, type } = config.boardingTimelock;
@@ -204,6 +231,7 @@ class ReadonlyWallet {
204
231
  contractRepository,
205
232
  info,
206
233
  delegatorProvider: config.delegatorProvider,
234
+ walletContractTimelocks,
207
235
  };
208
236
  }
209
237
  /**
@@ -218,14 +246,14 @@ class ReadonlyWallet {
218
246
  throw new Error("Invalid configured public key");
219
247
  }
220
248
  const setup = await ReadonlyWallet.setupWalletConfig(config, pubkey);
221
- return new ReadonlyWallet(config.identity, setup.network, setup.onchainProvider, setup.indexerProvider, setup.serverPubKey, setup.offchainTapscript, setup.boardingTapscript, setup.dustAmount, setup.walletRepository, setup.contractRepository, setup.delegatorProvider, config.watcherConfig);
249
+ return new ReadonlyWallet(config.identity, setup.network, setup.onchainProvider, setup.indexerProvider, setup.serverPubKey, setup.offchainTapscript, setup.boardingTapscript, setup.dustAmount, setup.walletRepository, setup.contractRepository, setup.delegatorProvider, config.watcherConfig, setup.walletContractTimelocks);
222
250
  }
223
251
  get arkAddress() {
224
252
  return this.offchainTapscript.address(this.network.hrp, this.arkServerPublicKey);
225
253
  }
226
254
  /**
227
- * Get the contract script for the wallet's default address.
228
- * This is the pkScript hex, used to identify the wallet in ContractManager.
255
+ * Get the pkScript hex for the wallet's primary offchain address.
256
+ * For the full wallet-owned script set registered in ContractManager, use getWalletScripts().
229
257
  */
230
258
  get defaultContractScript() {
231
259
  return base_1.hex.encode(this.offchainTapscript.pkScript);
@@ -469,56 +497,39 @@ class ReadonlyWallet {
469
497
  });
470
498
  }
471
499
  if (this.indexerProvider && arkAddress) {
472
- const walletScripts = await this.getWalletScripts();
473
- const subscriptionId = await this.indexerProvider.subscribeForScripts(walletScripts);
474
- const abortController = new AbortController();
475
- const subscription = this.indexerProvider.getSubscription(subscriptionId, abortController.signal);
476
- indexerStopFunc = async () => {
477
- abortController.abort();
478
- await this.indexerProvider?.unsubscribeForScripts(subscriptionId);
479
- };
480
- // Handle subscription updates asynchronously without blocking.
481
- // Subscription covers all wallet scripts (default + delegate) plus
482
- // any additional registered contracts. Virtual outputs carry a
483
- // `script` field from the indexer which the contract manager
484
- // resolves to the owning contract so the extension uses the
485
- // correct forfeit/intent tapscripts.
486
- (async () => {
487
- try {
488
- const cm = await this.getContractManager();
489
- for await (const update of subscription) {
490
- if (update.newVtxos?.length === 0 &&
491
- update.spentVtxos?.length === 0) {
492
- continue;
493
- }
494
- // Isolate per-update annotation failures (e.g. a VTXO
495
- // arriving for a contract we haven't registered yet).
496
- // Without this a single bad update would kill the
497
- // for-await loop and silently drop every subsequent
498
- // subscription event for the session.
499
- try {
500
- // Default to `[]` so a one-sided update (e.g.
501
- // only `newVtxos`) doesn't pass `undefined` into
502
- // annotateVtxos and throw on `.length`.
503
- const [newVtxos, spentVtxos] = await Promise.all([
504
- cm.annotateVtxos(update.newVtxos ?? []),
505
- cm.annotateVtxos(update.spentVtxos ?? []),
506
- ]);
507
- eventCallback({
508
- type: "vtxo",
509
- newVtxos,
510
- spentVtxos,
511
- });
512
- }
513
- catch (error) {
514
- console.warn("Dropping subscription update after annotation failed; next sync will reconcile:", error);
515
- }
516
- }
500
+ // Share the ContractWatcher's single subscription instead of
501
+ // opening a second SSE stream.
502
+ const cm = await this.getContractManager();
503
+ // Serialize annotation+notification: parallel `annotateVtxos`
504
+ // awaits could resolve out of order and deliver eventCallback
505
+ // calls in the wrong sequence (e.g. `vtxo_spent` before its
506
+ // matching `vtxo_received`).
507
+ let annotationQueue = Promise.resolve();
508
+ indexerStopFunc = cm.onContractEvent((event) => {
509
+ if (event.type !== "vtxo_received" &&
510
+ event.type !== "vtxo_spent") {
511
+ return;
517
512
  }
518
- catch (error) {
519
- console.error("Subscription error:", error);
513
+ if (event.contract.type !== "default" &&
514
+ event.contract.type !== "delegate") {
515
+ return;
520
516
  }
521
- })();
517
+ // `event.vtxos` carries placeholder tapscript fields from
518
+ // the watcher; `annotateVtxos` fills them in.
519
+ annotationQueue = annotationQueue.then(async () => {
520
+ try {
521
+ const annotated = await cm.annotateVtxos(event.vtxos);
522
+ eventCallback({
523
+ type: "vtxo",
524
+ newVtxos: event.type === "vtxo_received" ? annotated : [],
525
+ spentVtxos: event.type === "vtxo_spent" ? annotated : [],
526
+ });
527
+ }
528
+ catch (error) {
529
+ console.warn("Dropping subscription update after annotation failed; next sync will reconcile:", error);
530
+ }
531
+ });
532
+ });
522
533
  }
523
534
  const stopFunc = () => {
524
535
  onchainStopFunc?.();
@@ -545,27 +556,13 @@ class ReadonlyWallet {
545
556
  /**
546
557
  * Get all pkScript hex strings for the wallet's own addresses
547
558
  * (both delegate and non-delegate, current and historical).
548
- * Falls back to only the current script if ContractManager is not yet initialized.
549
559
  */
550
560
  async getWalletScripts() {
551
- // Only use the contract manager if it's already initialized or
552
- // currently initializing never trigger initialization here to
553
- // avoid blocking callers that don't need it.
554
- if (this._contractManager || this._contractManagerInitializing) {
555
- try {
556
- const manager = await this.getContractManager();
557
- const contracts = await manager.getContracts({
558
- type: ["default", "delegate"],
559
- });
560
- if (contracts.length > 0) {
561
- return contracts.map((c) => c.script);
562
- }
563
- }
564
- catch {
565
- // fall through to current script only
566
- }
567
- }
568
- return [base_1.hex.encode(this.offchainTapscript.pkScript)];
561
+ const manager = await this.getContractManager();
562
+ const contracts = await manager.getContracts({
563
+ type: ["default", "delegate"],
564
+ });
565
+ return contracts.map((c) => c.script);
569
566
  }
570
567
  /**
571
568
  * Build a map of scriptHex → VtxoScript for all wallet contracts,
@@ -573,26 +570,17 @@ class ReadonlyWallet {
573
570
  */
574
571
  async getScriptMap() {
575
572
  const map = new Map();
576
- // Always include the current script
577
- const currentScriptHex = base_1.hex.encode(this.offchainTapscript.pkScript);
578
- map.set(currentScriptHex, this.offchainTapscript);
579
- if (this._contractManager) {
580
- try {
581
- const contracts = await this._contractManager.getContracts({
582
- type: ["default", "delegate"],
583
- });
584
- for (const contract of contracts) {
585
- if (map.has(contract.script))
586
- continue;
587
- const handler = handlers_1.contractHandlers.get(contract.type);
588
- if (handler) {
589
- const script = handler.createScript(contract.params);
590
- map.set(contract.script, script);
591
- }
592
- }
593
- }
594
- catch {
595
- // ContractManager error — only current script in map
573
+ const manager = await this.getContractManager();
574
+ const contracts = await manager.getContracts({
575
+ type: ["default", "delegate"],
576
+ });
577
+ for (const contract of contracts) {
578
+ if (map.has(contract.script))
579
+ continue;
580
+ const handler = handlers_1.contractHandlers.get(contract.type);
581
+ if (handler) {
582
+ const script = handler.createScript(contract.params);
583
+ map.set(contract.script, script);
596
584
  }
597
585
  }
598
586
  return map;
@@ -660,62 +648,48 @@ class ReadonlyWallet {
660
648
  walletRepository: this.walletRepository,
661
649
  watcherConfig: this.watcherConfig,
662
650
  });
663
- // Register the wallet's current address as a contract
664
- const csvTimelock = this.offchainTapscript.options.csvTimelock ??
665
- default_1.DefaultVtxo.Script.DEFAULT_TIMELOCK;
666
- const csvTimelockStr = (0, helpers_1.timelockToSequence)(csvTimelock).toString();
667
- const isDelegateScript = this.offchainTapscript instanceof delegate_1.DelegateVtxo.Script;
668
- if (isDelegateScript) {
669
- const delegateScript = this
670
- .offchainTapscript;
671
- // Register the delegate contract (current address)
672
- await manager.createContract({
673
- type: "delegate",
674
- params: {
675
- pubKey: base_1.hex.encode(delegateScript.options.pubKey),
676
- serverPubKey: base_1.hex.encode(delegateScript.options.serverPubKey),
677
- delegatePubKey: base_1.hex.encode(delegateScript.options.delegatePubKey),
678
- csvTimelock: csvTimelockStr,
679
- },
680
- script: this.defaultContractScript,
681
- address: await this.getAddress(),
682
- state: "active",
683
- });
684
- // Also register the non-delegate version so old virtual outputs remain visible
685
- const nonDelegateScript = new default_1.DefaultVtxo.Script({
686
- pubKey: delegateScript.options.pubKey,
687
- serverPubKey: delegateScript.options.serverPubKey,
651
+ for (const csvTimelock of this.walletContractTimelocks) {
652
+ const csvTimelockStr = (0, helpers_1.timelockToSequence)(csvTimelock).toString();
653
+ const defaultScript = new default_1.DefaultVtxo.Script({
654
+ pubKey: this.offchainTapscript.options.pubKey,
655
+ serverPubKey: this.offchainTapscript.options.serverPubKey,
688
656
  csvTimelock,
689
657
  });
690
658
  await manager.createContract({
691
659
  type: "default",
692
660
  params: {
693
- pubKey: base_1.hex.encode(delegateScript.options.pubKey),
694
- serverPubKey: base_1.hex.encode(delegateScript.options.serverPubKey),
661
+ pubKey: base_1.hex.encode(defaultScript.options.pubKey),
662
+ serverPubKey: base_1.hex.encode(defaultScript.options.serverPubKey),
695
663
  csvTimelock: csvTimelockStr,
696
664
  },
697
- script: base_1.hex.encode(nonDelegateScript.pkScript),
698
- address: nonDelegateScript
665
+ script: base_1.hex.encode(defaultScript.pkScript),
666
+ address: defaultScript
699
667
  .address(this.network.hrp, this.arkServerPublicKey)
700
668
  .encode(),
701
669
  state: "active",
702
670
  });
703
- }
704
- else {
705
- // Register the default contract (current address)
706
- await manager.createContract({
707
- type: "default",
708
- params: {
709
- pubKey: base_1.hex.encode(this.offchainTapscript.options.pubKey),
710
- serverPubKey: base_1.hex.encode(this.offchainTapscript.options.serverPubKey),
711
- csvTimelock: csvTimelockStr,
712
- },
713
- script: this.defaultContractScript,
714
- address: await this.getAddress(),
715
- state: "active",
716
- });
717
- // Any old "delegate" contract from a prior wallet incarnation
718
- // is already loaded by ContractManager.initialize() from ContractRepository
671
+ if (this.offchainTapscript instanceof delegate_1.DelegateVtxo.Script) {
672
+ const delegateScript = new delegate_1.DelegateVtxo.Script({
673
+ pubKey: this.offchainTapscript.options.pubKey,
674
+ serverPubKey: this.offchainTapscript.options.serverPubKey,
675
+ delegatePubKey: this.offchainTapscript.options.delegatePubKey,
676
+ csvTimelock,
677
+ });
678
+ await manager.createContract({
679
+ type: "delegate",
680
+ params: {
681
+ pubKey: base_1.hex.encode(delegateScript.options.pubKey),
682
+ serverPubKey: base_1.hex.encode(delegateScript.options.serverPubKey),
683
+ delegatePubKey: base_1.hex.encode(delegateScript.options.delegatePubKey),
684
+ csvTimelock: csvTimelockStr,
685
+ },
686
+ script: base_1.hex.encode(delegateScript.pkScript),
687
+ address: delegateScript
688
+ .address(this.network.hrp, this.arkServerPublicKey)
689
+ .encode(),
690
+ state: "active",
691
+ });
692
+ }
719
693
  }
720
694
  return manager;
721
695
  }
@@ -799,8 +773,8 @@ class Wallet extends ReadonlyWallet {
799
773
  }
800
774
  constructor(identity, network, onchainProvider, arkProvider, indexerProvider, arkServerPublicKey, offchainTapscript, boardingTapscript, serverUnrollScript, forfeitOutputScript, forfeitPubkey, dustAmount, walletRepository, contractRepository,
801
775
  /** @deprecated Use settlementConfig */
802
- renewalConfig, delegatorProvider, watcherConfig, settlementConfig) {
803
- super(identity, network, onchainProvider, indexerProvider, arkServerPublicKey, offchainTapscript, boardingTapscript, dustAmount, walletRepository, contractRepository, delegatorProvider, watcherConfig);
776
+ renewalConfig, delegatorProvider, watcherConfig, settlementConfig, walletContractTimelocks) {
777
+ super(identity, network, onchainProvider, indexerProvider, arkServerPublicKey, offchainTapscript, boardingTapscript, dustAmount, walletRepository, contractRepository, delegatorProvider, watcherConfig, walletContractTimelocks);
804
778
  this.arkProvider = arkProvider;
805
779
  this.serverUnrollScript = serverUnrollScript;
806
780
  this.forfeitOutputScript = forfeitOutputScript;
@@ -920,7 +894,7 @@ class Wallet extends ReadonlyWallet {
920
894
  const forfeitPubkey = base_1.hex.decode(setup.info.forfeitPubkey).slice(1);
921
895
  const forfeitAddress = (0, btc_signer_1.Address)(setup.network).decode(setup.info.forfeitAddress);
922
896
  const forfeitOutputScript = btc_signer_1.OutScript.encode(forfeitAddress);
923
- const wallet = new Wallet(config.identity, setup.network, setup.onchainProvider, setup.arkProvider, setup.indexerProvider, setup.serverPubKey, setup.offchainTapscript, setup.boardingTapscript, serverUnrollScript, forfeitOutputScript, forfeitPubkey, setup.dustAmount, setup.walletRepository, setup.contractRepository, config.renewalConfig, config.delegatorProvider, config.watcherConfig, config.settlementConfig);
897
+ const wallet = new Wallet(config.identity, setup.network, setup.onchainProvider, setup.arkProvider, setup.indexerProvider, setup.serverPubKey, setup.offchainTapscript, setup.boardingTapscript, serverUnrollScript, forfeitOutputScript, forfeitPubkey, setup.dustAmount, setup.walletRepository, setup.contractRepository, config.renewalConfig, config.delegatorProvider, config.watcherConfig, config.settlementConfig, setup.walletContractTimelocks);
924
898
  await wallet.getVtxoManager();
925
899
  return wallet;
926
900
  }
@@ -946,7 +920,7 @@ class Wallet extends ReadonlyWallet {
946
920
  const readonlyIdentity = hasToReadonly(this.identity)
947
921
  ? await this.identity.toReadonly()
948
922
  : this.identity; // Identity extends ReadonlyIdentity, so this is safe
949
- return new ReadonlyWallet(readonlyIdentity, this.network, this.onchainProvider, this.indexerProvider, this.arkServerPublicKey, this.offchainTapscript, this.boardingTapscript, this.dustAmount, this.walletRepository, this.contractRepository, this.delegatorProvider, this.watcherConfig);
923
+ return new ReadonlyWallet(readonlyIdentity, this.network, this.onchainProvider, this.indexerProvider, this.arkServerPublicKey, this.offchainTapscript, this.boardingTapscript, this.dustAmount, this.walletRepository, this.contractRepository, this.delegatorProvider, this.watcherConfig, this.walletContractTimelocks);
950
924
  }
951
925
  /** Returns the delegator manager when delegation support is configured. */
952
926
  async getDelegatorManager() {
@@ -1,3 +1,4 @@
1
+ import { extendVirtualCoinForContract } from '../wallet/utils.js';
1
2
  import { isEventSourceError } from '../providers/utils.js';
2
3
  /**
3
4
  * Watches multiple contracts for virtual output state changes with resilient connection handling.
@@ -251,13 +252,18 @@ export class ContractWatcher {
251
252
  }
252
253
  /**
253
254
  * Connect to the subscription.
255
+ *
256
+ * @param skipUpdate - Skip the leading `updateSubscription` call when
257
+ * the caller has already established `subscriptionId`.
254
258
  */
255
- async connect() {
259
+ async connect(skipUpdate = false) {
256
260
  if (!this.isWatching)
257
261
  return;
258
262
  this.connectionState = "connecting";
259
263
  try {
260
- await this.updateSubscription();
264
+ if (!skipUpdate) {
265
+ await this.updateSubscription();
266
+ }
261
267
  // Poll immediately after connection to sync state
262
268
  await this.pollAllContracts();
263
269
  this.connectionState = "connected";
@@ -388,11 +394,30 @@ export class ContractWatcher {
388
394
  }
389
395
  }
390
396
  async tryUpdateSubscription() {
397
+ const hadSubscription = this.subscriptionId !== undefined;
391
398
  try {
392
399
  await this.updateSubscription();
393
400
  }
394
401
  catch (error) {
395
402
  // nothing, the connection will be retried later
403
+ return;
404
+ }
405
+ // Cold start: `startWatching` may have run with zero scripts,
406
+ // leaving `listenLoop` parked behind the reconnect timer. Kick
407
+ // `connect` now so streaming resumes without waiting on the
408
+ // backoff. `skipUpdate` avoids re-issuing `subscribeForScripts`.
409
+ const justGotSubscription = !hadSubscription && this.subscriptionId !== undefined;
410
+ const listenerParked = this.connectionState === "disconnected" ||
411
+ this.connectionState === "reconnecting";
412
+ if (this.isWatching && justGotSubscription && listenerParked) {
413
+ if (this.reconnectTimeoutId) {
414
+ clearTimeout(this.reconnectTimeoutId);
415
+ this.reconnectTimeoutId = undefined;
416
+ }
417
+ this.reconnectAttempts = 0;
418
+ this.connect(true).catch((error) => {
419
+ console.warn("ContractWatcher cold-start connect failed:", error);
420
+ });
396
421
  }
397
422
  }
398
423
  /**
@@ -526,18 +551,22 @@ export class ContractWatcher {
526
551
  const state = this.contracts.get(contractScript);
527
552
  if (!state)
528
553
  return;
554
+ const extended = [];
555
+ for (const v of vtxos) {
556
+ try {
557
+ const extendedVtxo = extendVirtualCoinForContract(v, state.contract);
558
+ extended.push({ ...extendedVtxo, contractScript });
559
+ }
560
+ catch {
561
+ console.warn("failed to extend vtxo: ", v);
562
+ extended.push({ ...v, contractScript });
563
+ }
564
+ }
529
565
  switch (eventType) {
530
566
  case "vtxo_received":
531
567
  this.eventCallback({
532
568
  type: "vtxo_received",
533
- vtxos: vtxos.map((v) => ({
534
- ...v,
535
- contractScript,
536
- // These fields may not be available from basic VirtualCoin
537
- forfeitTapLeafScript: undefined,
538
- intentTapLeafScript: undefined,
539
- tapTree: undefined,
540
- })),
569
+ vtxos: extended,
541
570
  contractScript,
542
571
  contract: state.contract,
543
572
  timestamp,
@@ -546,14 +575,7 @@ export class ContractWatcher {
546
575
  case "vtxo_spent":
547
576
  this.eventCallback({
548
577
  type: "vtxo_spent",
549
- vtxos: vtxos.map((v) => ({
550
- ...v,
551
- contractScript,
552
- // These fields may not be available from basic VirtualCoin
553
- forfeitTapLeafScript: undefined,
554
- intentTapLeafScript: undefined,
555
- tapTree: undefined,
556
- })),
578
+ vtxos: extended,
557
579
  contractScript,
558
580
  contract: state.contract,
559
581
  timestamp,
@@ -232,18 +232,19 @@ export class RestArkProvider {
232
232
  // leak the underlying SSE connection. `return()` is overridden below
233
233
  // so that closing the generator also closes the connection even when
234
234
  // the body is currently suspended at an await point.
235
- let eventSource = null;
235
+ let iterator = null;
236
+ const closeIterator = () => iterator?.close();
236
237
  // eslint-disable-next-line @typescript-eslint/no-this-alias
237
238
  const self = this;
238
239
  const gen = (async function* () {
239
- const abortHandler = () => eventSource?.close();
240
+ const abortHandler = closeIterator;
240
241
  signal?.addEventListener("abort", abortHandler);
241
242
  try {
242
243
  while (!signal?.aborted) {
243
- eventSource = new EventSource(url + queryParams);
244
- const iterator = eventSourceIterator(eventSource);
244
+ const currentIterator = eventSourceIterator(new EventSource(url + queryParams));
245
+ iterator = currentIterator;
245
246
  try {
246
- for await (const event of iterator) {
247
+ for await (const event of currentIterator) {
247
248
  if (signal?.aborted)
248
249
  break;
249
250
  try {
@@ -277,71 +278,87 @@ export class RestArkProvider {
277
278
  throw error;
278
279
  }
279
280
  finally {
280
- eventSource.close();
281
+ currentIterator.close();
282
+ iterator = null;
281
283
  }
282
284
  }
283
285
  }
284
286
  finally {
285
287
  signal?.removeEventListener("abort", abortHandler);
286
- eventSource?.close();
288
+ closeIterator();
287
289
  }
288
290
  })();
289
291
  const origReturn = gen.return.bind(gen);
290
292
  gen.return = (value) => {
291
- eventSource?.close();
293
+ closeIterator();
292
294
  return origReturn(value);
293
295
  };
294
296
  return gen;
295
297
  }
296
- async *getTransactionsStream(signal) {
298
+ getTransactionsStream(signal) {
297
299
  const url = `${this.serverUrl}/v1/txs`;
298
- while (!signal?.aborted) {
300
+ let iterator = null;
301
+ const closeIterator = () => iterator?.close();
302
+ // eslint-disable-next-line @typescript-eslint/no-this-alias
303
+ const self = this;
304
+ const gen = (async function* () {
305
+ const abortHandler = closeIterator;
306
+ signal?.addEventListener("abort", abortHandler);
299
307
  try {
300
- const eventSource = new EventSource(url);
301
- // Set up abort handling
302
- const abortHandler = () => {
303
- eventSource.close();
304
- };
305
- signal?.addEventListener("abort", abortHandler);
306
- try {
307
- for await (const event of eventSourceIterator(eventSource)) {
308
- if (signal?.aborted)
309
- break;
310
- try {
311
- const data = JSON.parse(event.data);
312
- const txNotification = this.parseTransactionNotification(data);
313
- if (txNotification) {
314
- yield txNotification;
308
+ while (!signal?.aborted) {
309
+ try {
310
+ const currentIterator = eventSourceIterator(new EventSource(url));
311
+ iterator = currentIterator;
312
+ for await (const event of currentIterator) {
313
+ if (signal?.aborted)
314
+ break;
315
+ try {
316
+ const data = JSON.parse(event.data);
317
+ const txNotification = self.parseTransactionNotification(data);
318
+ if (txNotification) {
319
+ yield txNotification;
320
+ }
321
+ }
322
+ catch (err) {
323
+ console.error("Failed to parse transaction notification:", err);
324
+ throw err;
315
325
  }
316
326
  }
317
- catch (err) {
318
- console.error("Failed to parse transaction notification:", err);
319
- throw err;
327
+ }
328
+ catch (error) {
329
+ if (signal?.aborted ||
330
+ (error instanceof Error &&
331
+ error.name === "AbortError")) {
332
+ break;
320
333
  }
334
+ // ignore timeout errors, they're expected when the server is not sending anything for 5 min
335
+ if (isFetchTimeoutError(error)) {
336
+ console.debug("Timeout error ignored");
337
+ continue;
338
+ }
339
+ if (isEventSourceError(error)) {
340
+ throw error;
341
+ }
342
+ console.error("Transaction stream error:", error);
343
+ throw error;
344
+ }
345
+ finally {
346
+ closeIterator();
347
+ iterator = null;
321
348
  }
322
- }
323
- finally {
324
- signal?.removeEventListener("abort", abortHandler);
325
- eventSource.close();
326
349
  }
327
350
  }
328
- catch (error) {
329
- if (signal?.aborted ||
330
- (error instanceof Error && error.name === "AbortError")) {
331
- break;
332
- }
333
- // ignore timeout errors, they're expected when the server is not sending anything for 5 min
334
- if (isFetchTimeoutError(error)) {
335
- console.debug("Timeout error ignored");
336
- continue;
337
- }
338
- if (isEventSourceError(error)) {
339
- throw error;
340
- }
341
- console.error("Transaction stream error:", error);
342
- throw error;
351
+ finally {
352
+ signal?.removeEventListener("abort", abortHandler);
353
+ closeIterator();
343
354
  }
344
- }
355
+ })();
356
+ const origReturn = gen.return.bind(gen);
357
+ gen.return = (value) => {
358
+ closeIterator();
359
+ return origReturn(value);
360
+ };
361
+ return gen;
345
362
  }
346
363
  async getPendingTxs(intent) {
347
364
  const url = `${this.serverUrl}/v1/tx/pending`;