@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.
- package/dist/cjs/contracts/contractWatcher.js +42 -20
- package/dist/cjs/providers/ark.js +65 -48
- package/dist/cjs/providers/indexer.js +60 -47
- package/dist/cjs/providers/utils.js +58 -12
- package/dist/cjs/wallet/delegator.js +27 -18
- package/dist/cjs/wallet/serviceWorker/wallet-message-handler.js +3 -1
- package/dist/cjs/wallet/vtxo-manager.js +7 -5
- package/dist/cjs/wallet/wallet.js +127 -153
- package/dist/esm/contracts/contractWatcher.js +40 -18
- package/dist/esm/providers/ark.js +65 -48
- package/dist/esm/providers/indexer.js +60 -47
- package/dist/esm/providers/utils.js +58 -12
- package/dist/esm/wallet/delegator.js +27 -18
- package/dist/esm/wallet/serviceWorker/wallet-message-handler.js +3 -1
- package/dist/esm/wallet/vtxo-manager.js +7 -5
- package/dist/esm/wallet/wallet.js +127 -153
- package/dist/types/contracts/contractWatcher.d.ts +3 -0
- package/dist/types/contracts/types.d.ts +5 -5
- package/dist/types/providers/utils.d.ts +9 -5
- package/dist/types/wallet/delegator.d.ts +8 -3
- package/dist/types/wallet/wallet.d.ts +7 -6
- package/package.json +4 -4
|
@@ -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
|
-
//
|
|
43
|
-
//
|
|
44
|
-
//
|
|
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
|
-
|
|
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
|
-
|
|
156
|
-
|
|
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
|
|
228
|
-
*
|
|
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
|
-
|
|
473
|
-
|
|
474
|
-
const
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
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
|
-
|
|
519
|
-
|
|
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
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
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
|
-
|
|
577
|
-
const
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
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
|
-
|
|
664
|
-
|
|
665
|
-
default_1.DefaultVtxo.Script
|
|
666
|
-
|
|
667
|
-
|
|
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(
|
|
694
|
-
serverPubKey: base_1.hex.encode(
|
|
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(
|
|
698
|
-
address:
|
|
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
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
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
|
-
|
|
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:
|
|
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:
|
|
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
|
|
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 =
|
|
240
|
+
const abortHandler = closeIterator;
|
|
240
241
|
signal?.addEventListener("abort", abortHandler);
|
|
241
242
|
try {
|
|
242
243
|
while (!signal?.aborted) {
|
|
243
|
-
|
|
244
|
-
|
|
244
|
+
const currentIterator = eventSourceIterator(new EventSource(url + queryParams));
|
|
245
|
+
iterator = currentIterator;
|
|
245
246
|
try {
|
|
246
|
-
for await (const event of
|
|
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
|
-
|
|
281
|
+
currentIterator.close();
|
|
282
|
+
iterator = null;
|
|
281
283
|
}
|
|
282
284
|
}
|
|
283
285
|
}
|
|
284
286
|
finally {
|
|
285
287
|
signal?.removeEventListener("abort", abortHandler);
|
|
286
|
-
|
|
288
|
+
closeIterator();
|
|
287
289
|
}
|
|
288
290
|
})();
|
|
289
291
|
const origReturn = gen.return.bind(gen);
|
|
290
292
|
gen.return = (value) => {
|
|
291
|
-
|
|
293
|
+
closeIterator();
|
|
292
294
|
return origReturn(value);
|
|
293
295
|
};
|
|
294
296
|
return gen;
|
|
295
297
|
}
|
|
296
|
-
|
|
298
|
+
getTransactionsStream(signal) {
|
|
297
299
|
const url = `${this.serverUrl}/v1/txs`;
|
|
298
|
-
|
|
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
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
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
|
-
|
|
318
|
-
|
|
319
|
-
|
|
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
|
-
|
|
329
|
-
|
|
330
|
-
|
|
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`;
|