@arkade-os/sdk 0.4.7 → 0.4.8
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/identity/seedIdentity.js +2 -2
- package/dist/cjs/index.js +9 -2
- package/dist/cjs/wallet/serviceWorker/wallet-message-handler.js +140 -7
- package/dist/cjs/wallet/serviceWorker/wallet.js +264 -34
- package/dist/cjs/wallet/vtxo-manager.js +51 -45
- package/dist/cjs/wallet/wallet.js +94 -34
- package/dist/cjs/worker/errors.js +17 -0
- package/dist/cjs/worker/messageBus.js +7 -2
- package/dist/esm/identity/seedIdentity.js +2 -2
- package/dist/esm/index.js +3 -2
- package/dist/esm/wallet/serviceWorker/wallet-message-handler.js +136 -6
- package/dist/esm/wallet/serviceWorker/wallet.js +264 -34
- package/dist/esm/wallet/vtxo-manager.js +51 -45
- package/dist/esm/wallet/wallet.js +94 -34
- package/dist/esm/worker/errors.js +12 -0
- package/dist/esm/worker/messageBus.js +7 -2
- package/dist/types/identity/seedIdentity.d.ts +5 -2
- package/dist/types/index.d.ts +5 -4
- package/dist/types/wallet/serviceWorker/wallet-message-handler.d.ts +80 -3
- package/dist/types/wallet/serviceWorker/wallet.d.ts +8 -0
- package/dist/types/wallet/vtxo-manager.d.ts +24 -2
- package/dist/types/wallet/wallet.d.ts +10 -0
- package/dist/types/worker/errors.d.ts +6 -0
- package/package.json +1 -1
|
@@ -148,43 +148,6 @@ function getExpiringAndRecoverableVtxos(vtxos, thresholdMs, dustAmount) {
|
|
|
148
148
|
((0, _1.isSpendable)(vtxo) && (0, _1.isExpired)(vtxo)) ||
|
|
149
149
|
(0, _1.isSubdust)(vtxo, dustAmount));
|
|
150
150
|
}
|
|
151
|
-
/**
|
|
152
|
-
* VtxoManager is a unified class for managing VTXO lifecycle operations including
|
|
153
|
-
* recovery of swept/expired VTXOs and renewal to prevent expiration.
|
|
154
|
-
*
|
|
155
|
-
* Key Features:
|
|
156
|
-
* - **Recovery**: Reclaim swept or expired VTXOs back to the wallet
|
|
157
|
-
* - **Renewal**: Refresh VTXO expiration time before they expire
|
|
158
|
-
* - **Smart subdust handling**: Automatically includes subdust VTXOs when economically viable
|
|
159
|
-
* - **Expiry monitoring**: Check for VTXOs that are expiring soon
|
|
160
|
-
*
|
|
161
|
-
* VTXOs become recoverable when:
|
|
162
|
-
* - The Ark server sweeps them (virtualStatus.state === "swept") and they remain spendable
|
|
163
|
-
* - They are preconfirmed subdust (to consolidate small amounts without locking liquidity on settled VTXOs)
|
|
164
|
-
*
|
|
165
|
-
* @example
|
|
166
|
-
* ```typescript
|
|
167
|
-
* // Initialize with renewal config
|
|
168
|
-
* const manager = new VtxoManager(wallet, {
|
|
169
|
-
* enabled: true,
|
|
170
|
-
* thresholdMs: 86400000
|
|
171
|
-
* });
|
|
172
|
-
*
|
|
173
|
-
* // Check recoverable balance
|
|
174
|
-
* const balance = await manager.getRecoverableBalance();
|
|
175
|
-
* if (balance.recoverable > 0n) {
|
|
176
|
-
* console.log(`Can recover ${balance.recoverable} sats`);
|
|
177
|
-
* const txid = await manager.recoverVtxos();
|
|
178
|
-
* }
|
|
179
|
-
*
|
|
180
|
-
* // Check for expiring VTXOs
|
|
181
|
-
* const expiring = await manager.getExpiringVtxos();
|
|
182
|
-
* if (expiring.length > 0) {
|
|
183
|
-
* console.log(`${expiring.length} VTXOs expiring soon`);
|
|
184
|
-
* const txid = await manager.renewVtxos();
|
|
185
|
-
* }
|
|
186
|
-
* ```
|
|
187
|
-
*/
|
|
188
151
|
class VtxoManager {
|
|
189
152
|
constructor(wallet,
|
|
190
153
|
/** @deprecated Use settlementConfig instead */
|
|
@@ -194,6 +157,7 @@ class VtxoManager {
|
|
|
194
157
|
this.knownBoardingUtxos = new Set();
|
|
195
158
|
this.sweptBoardingUtxos = new Set();
|
|
196
159
|
this.pollInProgress = false;
|
|
160
|
+
this.consecutivePollFailures = 0;
|
|
197
161
|
// Normalize: prefer settlementConfig, fall back to renewalConfig, default to enabled
|
|
198
162
|
if (settlementConfig !== undefined) {
|
|
199
163
|
this.settlementConfig = settlementConfig;
|
|
@@ -592,6 +556,14 @@ class VtxoManager {
|
|
|
592
556
|
// As more VTXOs are received, the threshold will be raised.
|
|
593
557
|
return;
|
|
594
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
|
+
}
|
|
595
567
|
}
|
|
596
568
|
console.error("Error renewing VTXOs:", e);
|
|
597
569
|
});
|
|
@@ -608,19 +580,36 @@ class VtxoManager {
|
|
|
608
580
|
return undefined;
|
|
609
581
|
}
|
|
610
582
|
}
|
|
583
|
+
/** Computes the next poll delay, applying exponential backoff on failures. */
|
|
584
|
+
getNextPollDelay() {
|
|
585
|
+
if (this.settlementConfig === false)
|
|
586
|
+
return 0;
|
|
587
|
+
const baseMs = this.settlementConfig.pollIntervalMs ??
|
|
588
|
+
exports.DEFAULT_SETTLEMENT_CONFIG.pollIntervalMs;
|
|
589
|
+
if (this.consecutivePollFailures === 0)
|
|
590
|
+
return baseMs;
|
|
591
|
+
const backoff = Math.min(baseMs * Math.pow(2, this.consecutivePollFailures), VtxoManager.MAX_BACKOFF_MS);
|
|
592
|
+
return backoff;
|
|
593
|
+
}
|
|
611
594
|
/**
|
|
612
595
|
* Starts a polling loop that:
|
|
613
596
|
* 1. Auto-settles new boarding UTXOs into Ark
|
|
614
597
|
* 2. Sweeps expired boarding UTXOs (when boardingUtxoSweep is enabled)
|
|
598
|
+
*
|
|
599
|
+
* Uses setTimeout chaining (not setInterval) so a slow/blocked poll
|
|
600
|
+
* cannot stack up and the next delay can incorporate backoff.
|
|
615
601
|
*/
|
|
616
602
|
startBoardingUtxoPoll() {
|
|
617
603
|
if (this.settlementConfig === false)
|
|
618
604
|
return;
|
|
619
|
-
|
|
620
|
-
exports.DEFAULT_SETTLEMENT_CONFIG.pollIntervalMs;
|
|
621
|
-
// Run once immediately, then on interval
|
|
605
|
+
// Run once immediately, then schedule next
|
|
622
606
|
this.pollBoardingUtxos();
|
|
623
|
-
|
|
607
|
+
}
|
|
608
|
+
schedulePoll() {
|
|
609
|
+
if (this.settlementConfig === false)
|
|
610
|
+
return;
|
|
611
|
+
const delay = this.getNextPollDelay();
|
|
612
|
+
this.pollTimeoutId = setTimeout(() => this.pollBoardingUtxos(), delay);
|
|
624
613
|
}
|
|
625
614
|
async pollBoardingUtxos() {
|
|
626
615
|
// Guard: wallet must support boarding UTXO + sweep operations
|
|
@@ -630,6 +619,7 @@ class VtxoManager {
|
|
|
630
619
|
if (this.pollInProgress)
|
|
631
620
|
return;
|
|
632
621
|
this.pollInProgress = true;
|
|
622
|
+
let hadError = false;
|
|
633
623
|
try {
|
|
634
624
|
// Settle new (unexpired) UTXOs first, then sweep expired ones.
|
|
635
625
|
// Sequential to avoid racing for the same UTXOs.
|
|
@@ -637,6 +627,7 @@ class VtxoManager {
|
|
|
637
627
|
await this.settleBoardingUtxos();
|
|
638
628
|
}
|
|
639
629
|
catch (e) {
|
|
630
|
+
hadError = true;
|
|
640
631
|
console.error("Error auto-settling boarding UTXOs:", e);
|
|
641
632
|
}
|
|
642
633
|
const sweepEnabled = this.settlementConfig !== false &&
|
|
@@ -649,13 +640,21 @@ class VtxoManager {
|
|
|
649
640
|
catch (e) {
|
|
650
641
|
if (!(e instanceof Error) ||
|
|
651
642
|
!e.message.includes("No expired boarding UTXOs")) {
|
|
643
|
+
hadError = true;
|
|
652
644
|
console.error("Error auto-sweeping boarding UTXOs:", e);
|
|
653
645
|
}
|
|
654
646
|
}
|
|
655
647
|
}
|
|
656
648
|
}
|
|
657
649
|
finally {
|
|
650
|
+
if (hadError) {
|
|
651
|
+
this.consecutivePollFailures++;
|
|
652
|
+
}
|
|
653
|
+
else {
|
|
654
|
+
this.consecutivePollFailures = 0;
|
|
655
|
+
}
|
|
658
656
|
this.pollInProgress = false;
|
|
657
|
+
this.schedulePoll();
|
|
659
658
|
}
|
|
660
659
|
}
|
|
661
660
|
/**
|
|
@@ -672,7 +671,13 @@ class VtxoManager {
|
|
|
672
671
|
// accidentally settling expired UTXOs (which would conflict with sweep).
|
|
673
672
|
let expiredSet;
|
|
674
673
|
try {
|
|
675
|
-
const
|
|
674
|
+
const boardingTimelock = this.getBoardingTimelock();
|
|
675
|
+
let chainTipHeight;
|
|
676
|
+
if (boardingTimelock.type === "blocks") {
|
|
677
|
+
const tip = await this.getOnchainProvider().getChainTip();
|
|
678
|
+
chainTipHeight = tip.height;
|
|
679
|
+
}
|
|
680
|
+
const expired = boardingUtxos.filter((utxo) => (0, arkTransaction_1.hasBoardingTxExpired)(utxo, boardingTimelock, chainTipHeight));
|
|
676
681
|
expiredSet = new Set(expired.map((u) => `${u.txid}:${u.vout}`));
|
|
677
682
|
}
|
|
678
683
|
catch {
|
|
@@ -698,9 +703,9 @@ class VtxoManager {
|
|
|
698
703
|
}
|
|
699
704
|
async dispose() {
|
|
700
705
|
this.disposePromise ?? (this.disposePromise = (async () => {
|
|
701
|
-
if (this.
|
|
702
|
-
|
|
703
|
-
this.
|
|
706
|
+
if (this.pollTimeoutId) {
|
|
707
|
+
clearTimeout(this.pollTimeoutId);
|
|
708
|
+
this.pollTimeoutId = undefined;
|
|
704
709
|
}
|
|
705
710
|
const subscription = await this.contractEventsSubscriptionReady;
|
|
706
711
|
this.contractEventsSubscription = undefined;
|
|
@@ -713,3 +718,4 @@ class VtxoManager {
|
|
|
713
718
|
}
|
|
714
719
|
}
|
|
715
720
|
exports.VtxoManager = VtxoManager;
|
|
721
|
+
VtxoManager.MAX_BACKOFF_MS = 5 * 60 * 1000; // 5 minutes
|
|
@@ -62,6 +62,19 @@ class ReadonlyWallet {
|
|
|
62
62
|
this.walletRepository = walletRepository;
|
|
63
63
|
this.contractRepository = contractRepository;
|
|
64
64
|
this.delegatorProvider = delegatorProvider;
|
|
65
|
+
// Guard: detect identity/server network mismatch for descriptor-based identities.
|
|
66
|
+
// This duplicates the check in setupWalletConfig() so that subclasses
|
|
67
|
+
// bypassing the factory still get the safety net.
|
|
68
|
+
if ("descriptor" in identity) {
|
|
69
|
+
const descriptor = identity.descriptor;
|
|
70
|
+
const identityIsMainnet = !descriptor.includes("tpub");
|
|
71
|
+
const serverIsMainnet = network.bech32 === "bc";
|
|
72
|
+
if (identityIsMainnet !== serverIsMainnet) {
|
|
73
|
+
throw new Error(`Network mismatch: identity uses ${identityIsMainnet ? "mainnet" : "testnet"} derivation ` +
|
|
74
|
+
`but wallet network is ${serverIsMainnet ? "mainnet" : "testnet"}. ` +
|
|
75
|
+
`Create identity with { isMainnet: ${serverIsMainnet} } to match.`);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
65
78
|
this.watcherConfig = watcherConfig;
|
|
66
79
|
this._assetManager = new asset_manager_1.ReadonlyAssetManager(this.indexerProvider);
|
|
67
80
|
}
|
|
@@ -89,6 +102,24 @@ class ReadonlyWallet {
|
|
|
89
102
|
const indexerProvider = config.indexerProvider || new indexer_1.RestIndexerProvider(indexerUrl);
|
|
90
103
|
const info = await arkProvider.getInfo();
|
|
91
104
|
const network = (0, networks_1.getNetwork)(info.network);
|
|
105
|
+
// Guard: detect identity/server network mismatch for seed-based identities.
|
|
106
|
+
// A mainnet descriptor (xpub, coin type 0) connected to a testnet server
|
|
107
|
+
// (or vice versa) means wrong derivation path → wrong keys → potential fund loss.
|
|
108
|
+
if ("descriptor" in config.identity) {
|
|
109
|
+
const descriptor = config.identity.descriptor;
|
|
110
|
+
const identityIsMainnet = !descriptor.includes("tpub");
|
|
111
|
+
const serverIsMainnet = info.network === "bitcoin";
|
|
112
|
+
if (identityIsMainnet && !serverIsMainnet) {
|
|
113
|
+
throw new Error(`Network mismatch: identity uses mainnet derivation (coin type 0) ` +
|
|
114
|
+
`but Ark server is on ${info.network}. ` +
|
|
115
|
+
`Create identity with { isMainnet: false } to use testnet derivation.`);
|
|
116
|
+
}
|
|
117
|
+
if (!identityIsMainnet && serverIsMainnet) {
|
|
118
|
+
throw new Error(`Network mismatch: identity uses testnet derivation (coin type 1) ` +
|
|
119
|
+
`but Ark server is on mainnet. ` +
|
|
120
|
+
`Create identity with { isMainnet: true } or omit isMainnet (defaults to mainnet).`);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
92
123
|
// Extract esploraUrl from provider if not explicitly provided
|
|
93
124
|
const esploraUrl = config.esploraUrl || onchain_1.ESPLORA_URL[info.network];
|
|
94
125
|
// Use provided onchainProvider instance or create a new one
|
|
@@ -664,6 +695,20 @@ exports.ReadonlyWallet = ReadonlyWallet;
|
|
|
664
695
|
* ```
|
|
665
696
|
*/
|
|
666
697
|
class Wallet extends ReadonlyWallet {
|
|
698
|
+
_withTxLock(fn) {
|
|
699
|
+
let release;
|
|
700
|
+
const lock = new Promise((r) => (release = r));
|
|
701
|
+
const prev = this._txLock;
|
|
702
|
+
this._txLock = lock;
|
|
703
|
+
return prev.then(async () => {
|
|
704
|
+
try {
|
|
705
|
+
return await fn();
|
|
706
|
+
}
|
|
707
|
+
finally {
|
|
708
|
+
release();
|
|
709
|
+
}
|
|
710
|
+
});
|
|
711
|
+
}
|
|
667
712
|
constructor(identity, network, networkName, onchainProvider, arkProvider, indexerProvider, arkServerPublicKey, offchainTapscript, boardingTapscript, serverUnrollScript, forfeitOutputScript, forfeitPubkey, dustAmount, walletRepository, contractRepository,
|
|
668
713
|
/** @deprecated Use settlementConfig */
|
|
669
714
|
renewalConfig, delegatorProvider, watcherConfig, settlementConfig) {
|
|
@@ -673,6 +718,13 @@ class Wallet extends ReadonlyWallet {
|
|
|
673
718
|
this.serverUnrollScript = serverUnrollScript;
|
|
674
719
|
this.forfeitOutputScript = forfeitOutputScript;
|
|
675
720
|
this.forfeitPubkey = forfeitPubkey;
|
|
721
|
+
/**
|
|
722
|
+
* Async mutex that serializes all operations submitting VTXOs to the Ark
|
|
723
|
+
* server (`settle`, `send`, `sendBitcoin`). This prevents VtxoManager's
|
|
724
|
+
* background renewal from racing with user-initiated transactions for the
|
|
725
|
+
* same VTXO inputs.
|
|
726
|
+
*/
|
|
727
|
+
this._txLock = Promise.resolve();
|
|
676
728
|
this.identity = identity;
|
|
677
729
|
// Backwards-compatible: keep renewalConfig populated for any code reading it
|
|
678
730
|
this.renewalConfig = {
|
|
@@ -811,40 +863,42 @@ class Wallet extends ReadonlyWallet {
|
|
|
811
863
|
throw new Error("Invalid Ark address " + params.address);
|
|
812
864
|
}
|
|
813
865
|
if (params.selectedVtxos && params.selectedVtxos.length > 0) {
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
866
|
+
return this._withTxLock(async () => {
|
|
867
|
+
const selectedVtxoSum = params
|
|
868
|
+
.selectedVtxos.map((v) => v.value)
|
|
869
|
+
.reduce((a, b) => a + b, 0);
|
|
870
|
+
if (selectedVtxoSum < params.amount) {
|
|
871
|
+
throw new Error("Selected VTXOs do not cover specified amount");
|
|
872
|
+
}
|
|
873
|
+
const changeAmount = selectedVtxoSum - params.amount;
|
|
874
|
+
const selected = {
|
|
875
|
+
inputs: params.selectedVtxos,
|
|
876
|
+
changeAmount: BigInt(changeAmount),
|
|
877
|
+
};
|
|
878
|
+
const outputAddress = address_1.ArkAddress.decode(params.address);
|
|
879
|
+
const outputScript = BigInt(params.amount) < this.dustAmount
|
|
880
|
+
? outputAddress.subdustPkScript
|
|
881
|
+
: outputAddress.pkScript;
|
|
882
|
+
const outputs = [
|
|
883
|
+
{
|
|
884
|
+
script: outputScript,
|
|
885
|
+
amount: BigInt(params.amount),
|
|
886
|
+
},
|
|
887
|
+
];
|
|
888
|
+
// add change output if needed
|
|
889
|
+
if (selected.changeAmount > 0n) {
|
|
890
|
+
const changeOutputScript = selected.changeAmount < this.dustAmount
|
|
891
|
+
? this.arkAddress.subdustPkScript
|
|
892
|
+
: this.arkAddress.pkScript;
|
|
893
|
+
outputs.push({
|
|
894
|
+
script: changeOutputScript,
|
|
895
|
+
amount: BigInt(selected.changeAmount),
|
|
896
|
+
});
|
|
897
|
+
}
|
|
898
|
+
const { arkTxid, signedCheckpointTxs } = await this.buildAndSubmitOffchainTx(selected.inputs, outputs);
|
|
899
|
+
await this.updateDbAfterOffchainTx(selected.inputs, arkTxid, signedCheckpointTxs, params.amount, selected.changeAmount, selected.changeAmount > 0n ? outputs.length - 1 : 0);
|
|
900
|
+
return arkTxid;
|
|
901
|
+
});
|
|
848
902
|
}
|
|
849
903
|
return this.send({
|
|
850
904
|
address: params.address,
|
|
@@ -852,6 +906,9 @@ class Wallet extends ReadonlyWallet {
|
|
|
852
906
|
});
|
|
853
907
|
}
|
|
854
908
|
async settle(params, eventCallback) {
|
|
909
|
+
return this._withTxLock(() => this._settleImpl(params, eventCallback));
|
|
910
|
+
}
|
|
911
|
+
async _settleImpl(params, eventCallback) {
|
|
855
912
|
if (params?.inputs) {
|
|
856
913
|
for (const input of params.inputs) {
|
|
857
914
|
// validate arknotes inputs
|
|
@@ -1353,6 +1410,9 @@ class Wallet extends ReadonlyWallet {
|
|
|
1353
1410
|
* ```
|
|
1354
1411
|
*/
|
|
1355
1412
|
async send(...args) {
|
|
1413
|
+
return this._withTxLock(() => this._sendImpl(...args));
|
|
1414
|
+
}
|
|
1415
|
+
async _sendImpl(...args) {
|
|
1356
1416
|
if (args.length === 0) {
|
|
1357
1417
|
throw new Error("At least one receiver is required");
|
|
1358
1418
|
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.ServiceWorkerTimeoutError = exports.MessageBusNotInitializedError = void 0;
|
|
4
|
+
class MessageBusNotInitializedError extends Error {
|
|
5
|
+
constructor() {
|
|
6
|
+
super("MessageBus not initialized");
|
|
7
|
+
this.name = "MessageBusNotInitializedError";
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
exports.MessageBusNotInitializedError = MessageBusNotInitializedError;
|
|
11
|
+
class ServiceWorkerTimeoutError extends Error {
|
|
12
|
+
constructor(detail) {
|
|
13
|
+
super(detail);
|
|
14
|
+
this.name = "ServiceWorkerTimeoutError";
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
exports.ServiceWorkerTimeoutError = ServiceWorkerTimeoutError;
|
|
@@ -8,6 +8,7 @@ const delegator_1 = require("../providers/delegator");
|
|
|
8
8
|
const identity_1 = require("../identity");
|
|
9
9
|
const wallet_1 = require("../wallet/wallet");
|
|
10
10
|
const base_1 = require("@scure/base");
|
|
11
|
+
const errors_1 = require("./errors");
|
|
11
12
|
class MessageBus {
|
|
12
13
|
constructor(walletRepository, contractRepository, { messageHandlers, tickIntervalMs = 10000, messageTimeoutMs = 30000, debug = false, buildServices, }) {
|
|
13
14
|
this.walletRepository = walletRepository;
|
|
@@ -183,6 +184,10 @@ class MessageBus {
|
|
|
183
184
|
}
|
|
184
185
|
async processMessage(event) {
|
|
185
186
|
const { id, tag, broadcast } = event.data;
|
|
187
|
+
if (tag === "PING") {
|
|
188
|
+
event.source?.postMessage({ id, tag: "PONG" });
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
186
191
|
if (tag === "INITIALIZE_MESSAGE_BUS") {
|
|
187
192
|
if (this.debug) {
|
|
188
193
|
console.log("Init Command received");
|
|
@@ -207,7 +212,7 @@ class MessageBus {
|
|
|
207
212
|
event.source?.postMessage({
|
|
208
213
|
id,
|
|
209
214
|
tag: tag ?? "unknown",
|
|
210
|
-
error: new
|
|
215
|
+
error: new errors_1.MessageBusNotInitializedError(),
|
|
211
216
|
});
|
|
212
217
|
return;
|
|
213
218
|
}
|
|
@@ -278,7 +283,7 @@ class MessageBus {
|
|
|
278
283
|
return promise;
|
|
279
284
|
return new Promise((resolve, reject) => {
|
|
280
285
|
const timer = self.setTimeout(() => {
|
|
281
|
-
reject(new
|
|
286
|
+
reject(new errors_1.ServiceWorkerTimeoutError(`Message handler timed out after ${this.messageTimeoutMs}ms (${label})`));
|
|
282
287
|
}, this.messageTimeoutMs);
|
|
283
288
|
promise.then((val) => {
|
|
284
289
|
self.clearTimeout(timer);
|
|
@@ -102,7 +102,7 @@ export class SeedIdentity {
|
|
|
102
102
|
static fromSeed(seed, opts) {
|
|
103
103
|
const descriptor = hasDescriptor(opts)
|
|
104
104
|
? opts.descriptor
|
|
105
|
-
: buildDescriptor(seed, opts.isMainnet);
|
|
105
|
+
: buildDescriptor(seed, opts.isMainnet ?? true);
|
|
106
106
|
return new SeedIdentity(seed, descriptor);
|
|
107
107
|
}
|
|
108
108
|
async xOnlyPublicKey() {
|
|
@@ -190,7 +190,7 @@ export class MnemonicIdentity extends SeedIdentity {
|
|
|
190
190
|
const seed = mnemonicToSeedSync(phrase, passphrase);
|
|
191
191
|
const descriptor = hasDescriptor(opts)
|
|
192
192
|
? opts.descriptor
|
|
193
|
-
: buildDescriptor(seed, opts.isMainnet);
|
|
193
|
+
: buildDescriptor(seed, opts.isMainnet ?? true);
|
|
194
194
|
return new MnemonicIdentity(seed, descriptor);
|
|
195
195
|
}
|
|
196
196
|
}
|
package/dist/esm/index.js
CHANGED
|
@@ -39,7 +39,8 @@ export * as asset from './extension/asset/index.js';
|
|
|
39
39
|
// Contracts
|
|
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
|
-
import { WalletMessageHandler } from './wallet/serviceWorker/wallet-message-handler.js';
|
|
42
|
+
import { WalletMessageHandler, WalletNotInitializedError, ReadonlyWalletError, DelegatorNotConfiguredError, } from './wallet/serviceWorker/wallet-message-handler.js';
|
|
43
|
+
import { MessageBusNotInitializedError, ServiceWorkerTimeoutError, } from './worker/errors.js';
|
|
43
44
|
export {
|
|
44
45
|
// Wallets
|
|
45
46
|
Wallet, ReadonlyWallet, SingleKey, ReadonlySingleKey, SeedIdentity, MnemonicIdentity, ReadonlyDescriptorIdentity, OnchainWallet, Ramps, VtxoManager, DelegatorManagerImpl, RestDelegatorProvider,
|
|
@@ -50,7 +51,7 @@ ArkAddress, DefaultVtxo, DelegateVtxo, VtxoScript, VHTLC,
|
|
|
50
51
|
// Enums
|
|
51
52
|
TxType, IndexerTxType, ChainTxType, SettlementEventType,
|
|
52
53
|
// Service Worker
|
|
53
|
-
setupServiceWorker, MessageBus, WalletMessageHandler, ServiceWorkerWallet, ServiceWorkerReadonlyWallet,
|
|
54
|
+
setupServiceWorker, MessageBus, WalletMessageHandler, WalletNotInitializedError, ReadonlyWalletError, DelegatorNotConfiguredError, MessageBusNotInitializedError, ServiceWorkerTimeoutError, ServiceWorkerWallet, ServiceWorkerReadonlyWallet,
|
|
54
55
|
// Tapscript
|
|
55
56
|
decodeTapscript, MultisigTapscript, CSVMultisigTapscript, ConditionCSVMultisigTapscript, ConditionMultisigTapscript, CLTVMultisigTapscript, TapTreeCoder,
|
|
56
57
|
// Ark PSBT fields
|
|
@@ -1,6 +1,24 @@
|
|
|
1
1
|
import { RestIndexerProvider } from '../../providers/indexer.js';
|
|
2
2
|
import { isExpired, isRecoverable, isSpendable, isSubdust, } from '../index.js';
|
|
3
3
|
import { extendCoin, extendVirtualCoin } from '../utils.js';
|
|
4
|
+
export class WalletNotInitializedError extends Error {
|
|
5
|
+
constructor() {
|
|
6
|
+
super("Wallet handler not initialized");
|
|
7
|
+
this.name = "WalletNotInitializedError";
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
export class ReadonlyWalletError extends Error {
|
|
11
|
+
constructor() {
|
|
12
|
+
super("Read-only wallet: operation requires signing");
|
|
13
|
+
this.name = "ReadonlyWalletError";
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
export class DelegatorNotConfiguredError extends Error {
|
|
17
|
+
constructor() {
|
|
18
|
+
super("Delegator not configured");
|
|
19
|
+
this.name = "DelegatorNotConfiguredError";
|
|
20
|
+
}
|
|
21
|
+
}
|
|
4
22
|
export const DEFAULT_MESSAGE_TAG = "WALLET_UPDATER";
|
|
5
23
|
export class WalletMessageHandler {
|
|
6
24
|
/**
|
|
@@ -21,7 +39,31 @@ export class WalletMessageHandler {
|
|
|
21
39
|
this.walletRepository = repositories.walletRepository;
|
|
22
40
|
}
|
|
23
41
|
async stop() {
|
|
24
|
-
|
|
42
|
+
if (this.incomingFundsSubscription) {
|
|
43
|
+
this.incomingFundsSubscription();
|
|
44
|
+
this.incomingFundsSubscription = undefined;
|
|
45
|
+
}
|
|
46
|
+
if (this.contractEventsSubscription) {
|
|
47
|
+
this.contractEventsSubscription();
|
|
48
|
+
this.contractEventsSubscription = undefined;
|
|
49
|
+
}
|
|
50
|
+
// Dispose the wallet to stop VtxoManager background tasks
|
|
51
|
+
// (auto-renewal, boarding UTXO polling) and ContractWatcher.
|
|
52
|
+
try {
|
|
53
|
+
if (this.wallet) {
|
|
54
|
+
await this.wallet.dispose();
|
|
55
|
+
}
|
|
56
|
+
else if (this.readonlyWallet) {
|
|
57
|
+
await this.readonlyWallet.dispose();
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
catch (_) {
|
|
61
|
+
// best-effort teardown
|
|
62
|
+
}
|
|
63
|
+
this.wallet = undefined;
|
|
64
|
+
this.readonlyWallet = undefined;
|
|
65
|
+
this.arkProvider = undefined;
|
|
66
|
+
this.indexerProvider = undefined;
|
|
25
67
|
}
|
|
26
68
|
async tick(_now) {
|
|
27
69
|
const results = await Promise.allSettled(this.onNextTick.map((fn) => fn()));
|
|
@@ -44,7 +86,7 @@ export class WalletMessageHandler {
|
|
|
44
86
|
}
|
|
45
87
|
requireWallet() {
|
|
46
88
|
if (!this.wallet) {
|
|
47
|
-
throw new
|
|
89
|
+
throw new ReadonlyWalletError();
|
|
48
90
|
}
|
|
49
91
|
return this.wallet;
|
|
50
92
|
}
|
|
@@ -66,7 +108,7 @@ export class WalletMessageHandler {
|
|
|
66
108
|
if (!this.readonlyWallet) {
|
|
67
109
|
return this.tagged({
|
|
68
110
|
id,
|
|
69
|
-
error: new
|
|
111
|
+
error: new WalletNotInitializedError(),
|
|
70
112
|
});
|
|
71
113
|
}
|
|
72
114
|
try {
|
|
@@ -294,7 +336,7 @@ export class WalletMessageHandler {
|
|
|
294
336
|
const wallet = this.requireWallet();
|
|
295
337
|
const delegatorManager = await wallet.getDelegatorManager();
|
|
296
338
|
if (!delegatorManager) {
|
|
297
|
-
throw new
|
|
339
|
+
throw new DelegatorNotConfiguredError();
|
|
298
340
|
}
|
|
299
341
|
const info = await delegatorManager.getDelegateInfo();
|
|
300
342
|
return this.tagged({
|
|
@@ -303,6 +345,83 @@ export class WalletMessageHandler {
|
|
|
303
345
|
payload: { info },
|
|
304
346
|
});
|
|
305
347
|
}
|
|
348
|
+
case "RECOVER_VTXOS": {
|
|
349
|
+
const wallet = this.requireWallet();
|
|
350
|
+
const vtxoManager = await wallet.getVtxoManager();
|
|
351
|
+
const txid = await vtxoManager.recoverVtxos((e) => {
|
|
352
|
+
this.scheduleForNextTick(() => this.tagged({
|
|
353
|
+
id,
|
|
354
|
+
type: "RECOVER_VTXOS_EVENT",
|
|
355
|
+
payload: e,
|
|
356
|
+
}));
|
|
357
|
+
});
|
|
358
|
+
return this.tagged({
|
|
359
|
+
id,
|
|
360
|
+
type: "RECOVER_VTXOS_SUCCESS",
|
|
361
|
+
payload: { txid },
|
|
362
|
+
});
|
|
363
|
+
}
|
|
364
|
+
case "GET_RECOVERABLE_BALANCE": {
|
|
365
|
+
const wallet = this.requireWallet();
|
|
366
|
+
const vtxoManager = await wallet.getVtxoManager();
|
|
367
|
+
const balance = await vtxoManager.getRecoverableBalance();
|
|
368
|
+
return this.tagged({
|
|
369
|
+
id,
|
|
370
|
+
type: "RECOVERABLE_BALANCE",
|
|
371
|
+
payload: {
|
|
372
|
+
recoverable: balance.recoverable.toString(),
|
|
373
|
+
subdust: balance.subdust.toString(),
|
|
374
|
+
includesSubdust: balance.includesSubdust,
|
|
375
|
+
vtxoCount: balance.vtxoCount,
|
|
376
|
+
},
|
|
377
|
+
});
|
|
378
|
+
}
|
|
379
|
+
case "GET_EXPIRING_VTXOS": {
|
|
380
|
+
const wallet = this.requireWallet();
|
|
381
|
+
const vtxoManager = await wallet.getVtxoManager();
|
|
382
|
+
const vtxos = await vtxoManager.getExpiringVtxos(message.payload.thresholdMs);
|
|
383
|
+
return this.tagged({
|
|
384
|
+
id,
|
|
385
|
+
type: "EXPIRING_VTXOS",
|
|
386
|
+
payload: { vtxos },
|
|
387
|
+
});
|
|
388
|
+
}
|
|
389
|
+
case "RENEW_VTXOS": {
|
|
390
|
+
const wallet = this.requireWallet();
|
|
391
|
+
const vtxoManager = await wallet.getVtxoManager();
|
|
392
|
+
const txid = await vtxoManager.renewVtxos((e) => {
|
|
393
|
+
this.scheduleForNextTick(() => this.tagged({
|
|
394
|
+
id,
|
|
395
|
+
type: "RENEW_VTXOS_EVENT",
|
|
396
|
+
payload: e,
|
|
397
|
+
}));
|
|
398
|
+
});
|
|
399
|
+
return this.tagged({
|
|
400
|
+
id,
|
|
401
|
+
type: "RENEW_VTXOS_SUCCESS",
|
|
402
|
+
payload: { txid },
|
|
403
|
+
});
|
|
404
|
+
}
|
|
405
|
+
case "GET_EXPIRED_BOARDING_UTXOS": {
|
|
406
|
+
const wallet = this.requireWallet();
|
|
407
|
+
const vtxoManager = await wallet.getVtxoManager();
|
|
408
|
+
const utxos = await vtxoManager.getExpiredBoardingUtxos();
|
|
409
|
+
return this.tagged({
|
|
410
|
+
id,
|
|
411
|
+
type: "EXPIRED_BOARDING_UTXOS",
|
|
412
|
+
payload: { utxos },
|
|
413
|
+
});
|
|
414
|
+
}
|
|
415
|
+
case "SWEEP_EXPIRED_BOARDING_UTXOS": {
|
|
416
|
+
const wallet = this.requireWallet();
|
|
417
|
+
const vtxoManager = await wallet.getVtxoManager();
|
|
418
|
+
const txid = await vtxoManager.sweepExpiredBoardingUtxos();
|
|
419
|
+
return this.tagged({
|
|
420
|
+
id,
|
|
421
|
+
type: "SWEEP_EXPIRED_BOARDING_UTXOS_SUCCESS",
|
|
422
|
+
payload: { txid },
|
|
423
|
+
});
|
|
424
|
+
}
|
|
306
425
|
default:
|
|
307
426
|
console.error("Unknown message type", message);
|
|
308
427
|
throw new Error("Unknown message");
|
|
@@ -478,6 +597,17 @@ export class WalletMessageHandler {
|
|
|
478
597
|
}
|
|
479
598
|
});
|
|
480
599
|
await this.ensureContractEventBroadcasting();
|
|
600
|
+
// Eagerly start the VtxoManager so its background tasks (auto-renewal,
|
|
601
|
+
// boarding UTXO polling/sweep) run inside the service worker without
|
|
602
|
+
// waiting for a client to send a vtxo-manager message first.
|
|
603
|
+
if (this.wallet) {
|
|
604
|
+
try {
|
|
605
|
+
await this.wallet.getVtxoManager();
|
|
606
|
+
}
|
|
607
|
+
catch (error) {
|
|
608
|
+
console.error("Error starting VtxoManager:", error);
|
|
609
|
+
}
|
|
610
|
+
}
|
|
481
611
|
}
|
|
482
612
|
async handleSettle(message) {
|
|
483
613
|
const wallet = this.requireWallet();
|
|
@@ -520,7 +650,7 @@ export class WalletMessageHandler {
|
|
|
520
650
|
const wallet = this.requireWallet();
|
|
521
651
|
const delegatorManager = await wallet.getDelegatorManager();
|
|
522
652
|
if (!delegatorManager) {
|
|
523
|
-
throw new
|
|
653
|
+
throw new DelegatorNotConfiguredError();
|
|
524
654
|
}
|
|
525
655
|
const { vtxoOutpoints, destination, delegateAt } = message.payload;
|
|
526
656
|
const allVtxos = await wallet.getVtxos();
|
|
@@ -547,7 +677,7 @@ export class WalletMessageHandler {
|
|
|
547
677
|
}
|
|
548
678
|
async handleGetVtxos(message) {
|
|
549
679
|
if (!this.readonlyWallet) {
|
|
550
|
-
throw new
|
|
680
|
+
throw new WalletNotInitializedError();
|
|
551
681
|
}
|
|
552
682
|
const vtxos = await this.getSpendableVtxos();
|
|
553
683
|
const dustAmount = this.readonlyWallet.dustAmount;
|