@arkade-os/sdk 0.4.6 → 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.
@@ -57,6 +57,19 @@ export class ReadonlyWallet {
57
57
  this.walletRepository = walletRepository;
58
58
  this.contractRepository = contractRepository;
59
59
  this.delegatorProvider = delegatorProvider;
60
+ // Guard: detect identity/server network mismatch for descriptor-based identities.
61
+ // This duplicates the check in setupWalletConfig() so that subclasses
62
+ // bypassing the factory still get the safety net.
63
+ if ("descriptor" in identity) {
64
+ const descriptor = identity.descriptor;
65
+ const identityIsMainnet = !descriptor.includes("tpub");
66
+ const serverIsMainnet = network.bech32 === "bc";
67
+ if (identityIsMainnet !== serverIsMainnet) {
68
+ throw new Error(`Network mismatch: identity uses ${identityIsMainnet ? "mainnet" : "testnet"} derivation ` +
69
+ `but wallet network is ${serverIsMainnet ? "mainnet" : "testnet"}. ` +
70
+ `Create identity with { isMainnet: ${serverIsMainnet} } to match.`);
71
+ }
72
+ }
60
73
  this.watcherConfig = watcherConfig;
61
74
  this._assetManager = new ReadonlyAssetManager(this.indexerProvider);
62
75
  }
@@ -84,6 +97,24 @@ export class ReadonlyWallet {
84
97
  const indexerProvider = config.indexerProvider || new RestIndexerProvider(indexerUrl);
85
98
  const info = await arkProvider.getInfo();
86
99
  const network = getNetwork(info.network);
100
+ // Guard: detect identity/server network mismatch for seed-based identities.
101
+ // A mainnet descriptor (xpub, coin type 0) connected to a testnet server
102
+ // (or vice versa) means wrong derivation path → wrong keys → potential fund loss.
103
+ if ("descriptor" in config.identity) {
104
+ const descriptor = config.identity.descriptor;
105
+ const identityIsMainnet = !descriptor.includes("tpub");
106
+ const serverIsMainnet = info.network === "bitcoin";
107
+ if (identityIsMainnet && !serverIsMainnet) {
108
+ throw new Error(`Network mismatch: identity uses mainnet derivation (coin type 0) ` +
109
+ `but Ark server is on ${info.network}. ` +
110
+ `Create identity with { isMainnet: false } to use testnet derivation.`);
111
+ }
112
+ if (!identityIsMainnet && serverIsMainnet) {
113
+ throw new Error(`Network mismatch: identity uses testnet derivation (coin type 1) ` +
114
+ `but Ark server is on mainnet. ` +
115
+ `Create identity with { isMainnet: true } or omit isMainnet (defaults to mainnet).`);
116
+ }
117
+ }
87
118
  // Extract esploraUrl from provider if not explicitly provided
88
119
  const esploraUrl = config.esploraUrl || ESPLORA_URL[info.network];
89
120
  // Use provided onchainProvider instance or create a new one
@@ -658,6 +689,20 @@ export class ReadonlyWallet {
658
689
  * ```
659
690
  */
660
691
  export class Wallet extends ReadonlyWallet {
692
+ _withTxLock(fn) {
693
+ let release;
694
+ const lock = new Promise((r) => (release = r));
695
+ const prev = this._txLock;
696
+ this._txLock = lock;
697
+ return prev.then(async () => {
698
+ try {
699
+ return await fn();
700
+ }
701
+ finally {
702
+ release();
703
+ }
704
+ });
705
+ }
661
706
  constructor(identity, network, networkName, onchainProvider, arkProvider, indexerProvider, arkServerPublicKey, offchainTapscript, boardingTapscript, serverUnrollScript, forfeitOutputScript, forfeitPubkey, dustAmount, walletRepository, contractRepository,
662
707
  /** @deprecated Use settlementConfig */
663
708
  renewalConfig, delegatorProvider, watcherConfig, settlementConfig) {
@@ -667,6 +712,13 @@ export class Wallet extends ReadonlyWallet {
667
712
  this.serverUnrollScript = serverUnrollScript;
668
713
  this.forfeitOutputScript = forfeitOutputScript;
669
714
  this.forfeitPubkey = forfeitPubkey;
715
+ /**
716
+ * Async mutex that serializes all operations submitting VTXOs to the Ark
717
+ * server (`settle`, `send`, `sendBitcoin`). This prevents VtxoManager's
718
+ * background renewal from racing with user-initiated transactions for the
719
+ * same VTXO inputs.
720
+ */
721
+ this._txLock = Promise.resolve();
670
722
  this.identity = identity;
671
723
  // Backwards-compatible: keep renewalConfig populated for any code reading it
672
724
  this.renewalConfig = {
@@ -805,40 +857,42 @@ export class Wallet extends ReadonlyWallet {
805
857
  throw new Error("Invalid Ark address " + params.address);
806
858
  }
807
859
  if (params.selectedVtxos && params.selectedVtxos.length > 0) {
808
- const selectedVtxoSum = params.selectedVtxos
809
- .map((v) => v.value)
810
- .reduce((a, b) => a + b, 0);
811
- if (selectedVtxoSum < params.amount) {
812
- throw new Error("Selected VTXOs do not cover specified amount");
813
- }
814
- const changeAmount = selectedVtxoSum - params.amount;
815
- const selected = {
816
- inputs: params.selectedVtxos,
817
- changeAmount: BigInt(changeAmount),
818
- };
819
- const outputAddress = ArkAddress.decode(params.address);
820
- const outputScript = BigInt(params.amount) < this.dustAmount
821
- ? outputAddress.subdustPkScript
822
- : outputAddress.pkScript;
823
- const outputs = [
824
- {
825
- script: outputScript,
826
- amount: BigInt(params.amount),
827
- },
828
- ];
829
- // add change output if needed
830
- if (selected.changeAmount > 0n) {
831
- const changeOutputScript = selected.changeAmount < this.dustAmount
832
- ? this.arkAddress.subdustPkScript
833
- : this.arkAddress.pkScript;
834
- outputs.push({
835
- script: changeOutputScript,
836
- amount: BigInt(selected.changeAmount),
837
- });
838
- }
839
- const { arkTxid, signedCheckpointTxs } = await this.buildAndSubmitOffchainTx(selected.inputs, outputs);
840
- await this.updateDbAfterOffchainTx(selected.inputs, arkTxid, signedCheckpointTxs, params.amount, selected.changeAmount, selected.changeAmount > 0n ? outputs.length - 1 : 0);
841
- return arkTxid;
860
+ return this._withTxLock(async () => {
861
+ const selectedVtxoSum = params
862
+ .selectedVtxos.map((v) => v.value)
863
+ .reduce((a, b) => a + b, 0);
864
+ if (selectedVtxoSum < params.amount) {
865
+ throw new Error("Selected VTXOs do not cover specified amount");
866
+ }
867
+ const changeAmount = selectedVtxoSum - params.amount;
868
+ const selected = {
869
+ inputs: params.selectedVtxos,
870
+ changeAmount: BigInt(changeAmount),
871
+ };
872
+ const outputAddress = ArkAddress.decode(params.address);
873
+ const outputScript = BigInt(params.amount) < this.dustAmount
874
+ ? outputAddress.subdustPkScript
875
+ : outputAddress.pkScript;
876
+ const outputs = [
877
+ {
878
+ script: outputScript,
879
+ amount: BigInt(params.amount),
880
+ },
881
+ ];
882
+ // add change output if needed
883
+ if (selected.changeAmount > 0n) {
884
+ const changeOutputScript = selected.changeAmount < this.dustAmount
885
+ ? this.arkAddress.subdustPkScript
886
+ : this.arkAddress.pkScript;
887
+ outputs.push({
888
+ script: changeOutputScript,
889
+ amount: BigInt(selected.changeAmount),
890
+ });
891
+ }
892
+ const { arkTxid, signedCheckpointTxs } = await this.buildAndSubmitOffchainTx(selected.inputs, outputs);
893
+ await this.updateDbAfterOffchainTx(selected.inputs, arkTxid, signedCheckpointTxs, params.amount, selected.changeAmount, selected.changeAmount > 0n ? outputs.length - 1 : 0);
894
+ return arkTxid;
895
+ });
842
896
  }
843
897
  return this.send({
844
898
  address: params.address,
@@ -846,6 +900,9 @@ export class Wallet extends ReadonlyWallet {
846
900
  });
847
901
  }
848
902
  async settle(params, eventCallback) {
903
+ return this._withTxLock(() => this._settleImpl(params, eventCallback));
904
+ }
905
+ async _settleImpl(params, eventCallback) {
849
906
  if (params?.inputs) {
850
907
  for (const input of params.inputs) {
851
908
  // validate arknotes inputs
@@ -1347,6 +1404,9 @@ export class Wallet extends ReadonlyWallet {
1347
1404
  * ```
1348
1405
  */
1349
1406
  async send(...args) {
1407
+ return this._withTxLock(() => this._sendImpl(...args));
1408
+ }
1409
+ async _sendImpl(...args) {
1350
1410
  if (args.length === 0) {
1351
1411
  throw new Error("At least one receiver is required");
1352
1412
  }
@@ -0,0 +1,12 @@
1
+ export class MessageBusNotInitializedError extends Error {
2
+ constructor() {
3
+ super("MessageBus not initialized");
4
+ this.name = "MessageBusNotInitializedError";
5
+ }
6
+ }
7
+ export class ServiceWorkerTimeoutError extends Error {
8
+ constructor(detail) {
9
+ super(detail);
10
+ this.name = "ServiceWorkerTimeoutError";
11
+ }
12
+ }
@@ -5,6 +5,7 @@ import { RestDelegatorProvider } from '../providers/delegator.js';
5
5
  import { ReadonlySingleKey, SingleKey } from '../identity/index.js';
6
6
  import { ReadonlyWallet, Wallet } from '../wallet/wallet.js';
7
7
  import { hex } from "@scure/base";
8
+ import { MessageBusNotInitializedError, ServiceWorkerTimeoutError, } from './errors.js';
8
9
  export class MessageBus {
9
10
  constructor(walletRepository, contractRepository, { messageHandlers, tickIntervalMs = 10000, messageTimeoutMs = 30000, debug = false, buildServices, }) {
10
11
  this.walletRepository = walletRepository;
@@ -180,6 +181,10 @@ export class MessageBus {
180
181
  }
181
182
  async processMessage(event) {
182
183
  const { id, tag, broadcast } = event.data;
184
+ if (tag === "PING") {
185
+ event.source?.postMessage({ id, tag: "PONG" });
186
+ return;
187
+ }
183
188
  if (tag === "INITIALIZE_MESSAGE_BUS") {
184
189
  if (this.debug) {
185
190
  console.log("Init Command received");
@@ -204,7 +209,7 @@ export class MessageBus {
204
209
  event.source?.postMessage({
205
210
  id,
206
211
  tag: tag ?? "unknown",
207
- error: new Error("MessageBus not initialized"),
212
+ error: new MessageBusNotInitializedError(),
208
213
  });
209
214
  return;
210
215
  }
@@ -275,7 +280,7 @@ export class MessageBus {
275
280
  return promise;
276
281
  return new Promise((resolve, reject) => {
277
282
  const timer = self.setTimeout(() => {
278
- reject(new Error(`Message handler timed out after ${this.messageTimeoutMs}ms (${label})`));
283
+ reject(new ServiceWorkerTimeoutError(`Message handler timed out after ${this.messageTimeoutMs}ms (${label})`));
279
284
  }, this.messageTimeoutMs);
280
285
  promise.then((val) => {
281
286
  self.clearTimeout(timer);
@@ -3,8 +3,11 @@ import { Transaction } from "../utils/transaction";
3
3
  import { SignerSession } from "../tree/signingSession";
4
4
  /** Use default BIP86 derivation with network selection. */
5
5
  export interface NetworkOptions {
6
- /** Mainnet (coin type 0) or testnet (coin type 1). */
7
- isMainnet: boolean;
6
+ /**
7
+ * Mainnet (coin type 0) or testnet (coin type 1).
8
+ * @default true
9
+ */
10
+ isMainnet?: boolean;
8
11
  }
9
12
  /** Use a custom output descriptor for derivation. */
10
13
  export interface DescriptorOptions {
@@ -16,7 +16,7 @@ import { TxTree, TxTreeNode } from "./tree/txTree";
16
16
  import { SignerSession, TreeNonces, TreePartialSigs } from "./tree/signingSession";
17
17
  import { Ramps } from "./wallet/ramps";
18
18
  import { isVtxoExpiringSoon, VtxoManager } from "./wallet/vtxo-manager";
19
- import type { SettlementConfig } from "./wallet/vtxo-manager";
19
+ import type { IVtxoManager, SettlementConfig } from "./wallet/vtxo-manager";
20
20
  import { ServiceWorkerWallet, ServiceWorkerReadonlyWallet } from "./wallet/serviceWorker/wallet";
21
21
  import { OnchainWallet } from "./wallet/onchain";
22
22
  import { setupServiceWorker } from "./worker/browser/utils";
@@ -47,6 +47,7 @@ import { ContractManager, ContractWatcher, contractHandlers, DefaultContractHand
47
47
  import type { Contract, ContractVtxo, ContractState, ContractEvent, ContractEventCallback, ContractBalance, ContractWithVtxos, ContractHandler, PathSelection, PathContext, ContractManagerConfig, CreateContractParams, ContractWatcherConfig, ParsedArkContract, DefaultContractParams, DelegateContractParams, VHTLCContractParams } from "./contracts";
48
48
  import { IContractManager } from "./contracts/contractManager";
49
49
  import { closeDatabase, openDatabase } from "./repositories/indexedDB/manager";
50
- import { WalletMessageHandler } from "./wallet/serviceWorker/wallet-message-handler";
51
- export { Wallet, ReadonlyWallet, SingleKey, ReadonlySingleKey, SeedIdentity, MnemonicIdentity, ReadonlyDescriptorIdentity, OnchainWallet, Ramps, VtxoManager, DelegatorManagerImpl, RestDelegatorProvider, ESPLORA_URL, EsploraProvider, RestArkProvider, RestIndexerProvider, ArkAddress, DefaultVtxo, DelegateVtxo, VtxoScript, VHTLC, TxType, IndexerTxType, ChainTxType, SettlementEventType, setupServiceWorker, MessageBus, WalletMessageHandler, ServiceWorkerWallet, ServiceWorkerReadonlyWallet, decodeTapscript, MultisigTapscript, CSVMultisigTapscript, ConditionCSVMultisigTapscript, ConditionMultisigTapscript, CLTVMultisigTapscript, TapTreeCoder, ArkPsbtFieldKey, ArkPsbtFieldKeyType, setArkPsbtField, getArkPsbtFields, CosignerPublicKey, VtxoTreeExpiry, VtxoTaprootTree, ConditionWitness, buildOffchainTx, verifyTapscriptSignatures, waitForIncomingFunds, hasBoardingTxExpired, combineTapscriptSigs, isVtxoExpiringSoon, isValidArkAddress, ArkNote, networks, closeDatabase, openDatabase, IndexedDBWalletRepository, IndexedDBContractRepository, InMemoryWalletRepository, InMemoryContractRepository, MIGRATION_KEY, migrateWalletRepository, requiresMigration, getMigrationStatus, rollbackMigration, WalletRepositoryImpl, ContractRepositoryImpl, Intent, BIP322, TxTree, P2A, Unroll, Transaction, ArkError, maybeArkError, Batch, validateVtxoTxGraph, validateConnectorsTxGraph, buildForfeitTx, isRecoverable, isSpendable, isSubdust, isExpired, getSequence, ContractManager, ContractWatcher, contractHandlers, DefaultContractHandler, DelegateContractHandler, VHTLCContractHandler, encodeArkContract, decodeArkContract, contractFromArkContract, contractFromArkContractWithAddress, isArkContract, };
52
- export type { Identity, ReadonlyIdentity, IWallet, IReadonlyWallet, BaseWalletConfig, WalletConfig, ReadonlyWalletConfig, ProviderClass, ArkTransaction, Coin, ExtendedCoin, ExtendedVirtualCoin, WalletBalance, SendBitcoinParams, SettleParams, Status, VirtualStatus, Outpoint, VirtualCoin, TxKey, TapscriptType, ArkTxInput, OffchainTx, TapLeaves, IncomingFunds, SeedIdentityOptions, MnemonicOptions, NetworkOptions, DescriptorOptions, IndexerProvider, PageResponse, BatchInfo, ChainTx, CommitmentTx, TxHistoryRecord, Vtxo, VtxoChain, Tx, OnchainProvider, ArkProvider, SettlementEvent, FeeInfo, ArkInfo, SignedIntent, Output, TxNotification, ExplorerTransaction, BatchFinalizationEvent, BatchFinalizedEvent, BatchFailedEvent, TreeSigningStartedEvent, TreeNoncesEvent, BatchStartedEvent, TreeTxEvent, TreeSignatureEvent, ScheduledSession, PaginationOptions, SubscriptionResponse, SubscriptionHeartbeat, SubscriptionEvent, Network, NetworkName, ArkTapscript, RelativeTimelock, EncodedVtxoScript, TapLeafScript, SignerSession, TreeNonces, TreePartialSigs, GetVtxosFilter, SettlementConfig, Asset, Recipient, IssuanceParams, IssuanceResult, ReissuanceParams, BurnParams, AssetDetails, AssetMetadata, KnownMetadata, Nonces, PartialSig, ArkPsbtFieldCoder, TxTreeNode, AnchorBumper, StorageConfig, Contract, ContractVtxo, ContractState, ContractEvent, ContractEventCallback, ContractBalance, ContractWithVtxos, ContractHandler, IContractManager, PathSelection, PathContext, ContractManagerConfig, CreateContractParams, ContractWatcherConfig, ParsedArkContract, DefaultContractParams, DelegateContractParams, VHTLCContractParams, MessageHandler, RequestEnvelope, ResponseEnvelope, IDelegatorManager, DelegatorProvider, DelegateInfo, DelegateOptions, WalletRepository, ContractRepository, MigrationStatus, };
50
+ import { WalletMessageHandler, WalletNotInitializedError, ReadonlyWalletError, DelegatorNotConfiguredError } from "./wallet/serviceWorker/wallet-message-handler";
51
+ import { MessageBusNotInitializedError, ServiceWorkerTimeoutError } from "./worker/errors";
52
+ export { Wallet, ReadonlyWallet, SingleKey, ReadonlySingleKey, SeedIdentity, MnemonicIdentity, ReadonlyDescriptorIdentity, OnchainWallet, Ramps, VtxoManager, DelegatorManagerImpl, RestDelegatorProvider, ESPLORA_URL, EsploraProvider, RestArkProvider, RestIndexerProvider, ArkAddress, DefaultVtxo, DelegateVtxo, VtxoScript, VHTLC, TxType, IndexerTxType, ChainTxType, SettlementEventType, setupServiceWorker, MessageBus, WalletMessageHandler, WalletNotInitializedError, ReadonlyWalletError, DelegatorNotConfiguredError, MessageBusNotInitializedError, ServiceWorkerTimeoutError, ServiceWorkerWallet, ServiceWorkerReadonlyWallet, decodeTapscript, MultisigTapscript, CSVMultisigTapscript, ConditionCSVMultisigTapscript, ConditionMultisigTapscript, CLTVMultisigTapscript, TapTreeCoder, ArkPsbtFieldKey, ArkPsbtFieldKeyType, setArkPsbtField, getArkPsbtFields, CosignerPublicKey, VtxoTreeExpiry, VtxoTaprootTree, ConditionWitness, buildOffchainTx, verifyTapscriptSignatures, waitForIncomingFunds, hasBoardingTxExpired, combineTapscriptSigs, isVtxoExpiringSoon, isValidArkAddress, ArkNote, networks, closeDatabase, openDatabase, IndexedDBWalletRepository, IndexedDBContractRepository, InMemoryWalletRepository, InMemoryContractRepository, MIGRATION_KEY, migrateWalletRepository, requiresMigration, getMigrationStatus, rollbackMigration, WalletRepositoryImpl, ContractRepositoryImpl, Intent, BIP322, TxTree, P2A, Unroll, Transaction, ArkError, maybeArkError, Batch, validateVtxoTxGraph, validateConnectorsTxGraph, buildForfeitTx, isRecoverable, isSpendable, isSubdust, isExpired, getSequence, ContractManager, ContractWatcher, contractHandlers, DefaultContractHandler, DelegateContractHandler, VHTLCContractHandler, encodeArkContract, decodeArkContract, contractFromArkContract, contractFromArkContractWithAddress, isArkContract, };
53
+ export type { Identity, ReadonlyIdentity, IWallet, IReadonlyWallet, BaseWalletConfig, WalletConfig, ReadonlyWalletConfig, ProviderClass, ArkTransaction, Coin, ExtendedCoin, ExtendedVirtualCoin, WalletBalance, SendBitcoinParams, SettleParams, Status, VirtualStatus, Outpoint, VirtualCoin, TxKey, TapscriptType, ArkTxInput, OffchainTx, TapLeaves, IncomingFunds, SeedIdentityOptions, MnemonicOptions, NetworkOptions, DescriptorOptions, IndexerProvider, PageResponse, BatchInfo, ChainTx, CommitmentTx, TxHistoryRecord, Vtxo, VtxoChain, Tx, OnchainProvider, ArkProvider, SettlementEvent, FeeInfo, ArkInfo, SignedIntent, Output, TxNotification, ExplorerTransaction, BatchFinalizationEvent, BatchFinalizedEvent, BatchFailedEvent, TreeSigningStartedEvent, TreeNoncesEvent, BatchStartedEvent, TreeTxEvent, TreeSignatureEvent, ScheduledSession, PaginationOptions, SubscriptionResponse, SubscriptionHeartbeat, SubscriptionEvent, Network, NetworkName, ArkTapscript, RelativeTimelock, EncodedVtxoScript, TapLeafScript, SignerSession, TreeNonces, TreePartialSigs, GetVtxosFilter, SettlementConfig, IVtxoManager, Asset, Recipient, IssuanceParams, IssuanceResult, ReissuanceParams, BurnParams, AssetDetails, AssetMetadata, KnownMetadata, Nonces, PartialSig, ArkPsbtFieldCoder, TxTreeNode, AnchorBumper, StorageConfig, Contract, ContractVtxo, ContractState, ContractEvent, ContractEventCallback, ContractBalance, ContractWithVtxos, ContractHandler, IContractManager, PathSelection, PathContext, ContractManagerConfig, CreateContractParams, ContractWatcherConfig, ParsedArkContract, DefaultContractParams, DelegateContractParams, VHTLCContractParams, MessageHandler, RequestEnvelope, ResponseEnvelope, IDelegatorManager, DelegatorProvider, DelegateInfo, DelegateOptions, WalletRepository, ContractRepository, MigrationStatus, };
@@ -1,10 +1,19 @@
1
1
  import { SettlementEvent } from "../../providers/ark";
2
2
  import type { Contract, ContractEvent, ContractWithVtxos, GetContractsFilter, PathSelection } from "../../contracts";
3
3
  import type { CreateContractParams, GetAllSpendingPathsOptions, GetSpendablePathsOptions } from "../../contracts/contractManager";
4
- import { ArkTransaction, AssetDetails, BurnParams, ExtendedCoin, GetVtxosFilter, IssuanceParams, IssuanceResult, IWallet, Recipient, ReissuanceParams, SendBitcoinParams, SettleParams, WalletBalance } from "../index";
4
+ import { ArkTransaction, AssetDetails, BurnParams, ExtendedCoin, ExtendedVirtualCoin, GetVtxosFilter, IssuanceParams, IssuanceResult, IWallet, Recipient, ReissuanceParams, SendBitcoinParams, SettleParams, WalletBalance } from "../index";
5
5
  import { DelegateInfo } from "../../providers/delegator";
6
6
  import { MessageHandler, RequestEnvelope, ResponseEnvelope } from "../../worker/messageBus";
7
7
  import { Transaction } from "../../utils/transaction";
8
+ export declare class WalletNotInitializedError extends Error {
9
+ constructor();
10
+ }
11
+ export declare class ReadonlyWalletError extends Error {
12
+ constructor();
13
+ }
14
+ export declare class DelegatorNotConfiguredError extends Error {
15
+ constructor();
16
+ }
8
17
  export declare const DEFAULT_MESSAGE_TAG = "WALLET_UPDATER";
9
18
  export type RequestInitWallet = RequestEnvelope & {
10
19
  type: "INIT_WALLET";
@@ -236,6 +245,14 @@ export type ResponseSettleEvent = ResponseEnvelope & {
236
245
  type: "SETTLE_EVENT";
237
246
  payload: SettlementEvent;
238
247
  };
248
+ export type ResponseRecoverVtxosEvent = ResponseEnvelope & {
249
+ type: "RECOVER_VTXOS_EVENT";
250
+ payload: SettlementEvent;
251
+ };
252
+ export type ResponseRenewVtxosEvent = ResponseEnvelope & {
253
+ type: "RENEW_VTXOS_EVENT";
254
+ payload: SettlementEvent;
255
+ };
239
256
  export type ResponseUtxoUpdate = ResponseEnvelope & {
240
257
  broadcast: true;
241
258
  type: "UTXO_UPDATE";
@@ -355,8 +372,68 @@ export type ResponseGetDelegateInfo = ResponseEnvelope & {
355
372
  info: DelegateInfo;
356
373
  };
357
374
  };
358
- export type WalletUpdaterRequest = RequestInitWallet | RequestSettle | RequestSendBitcoin | RequestGetAddress | RequestGetBoardingAddress | RequestGetBalance | RequestGetVtxos | RequestGetBoardingUtxos | RequestGetTransactionHistory | RequestGetStatus | RequestClear | RequestReloadWallet | RequestSignTransaction | RequestCreateContract | RequestGetContracts | RequestGetContractsWithVtxos | RequestUpdateContract | RequestDeleteContract | RequestGetSpendablePaths | RequestGetAllSpendingPaths | RequestIsContractManagerWatching | RequestSend | RequestGetAssetDetails | RequestIssue | RequestReissue | RequestBurn | RequestDelegate | RequestGetDelegateInfo;
359
- export type WalletUpdaterResponse = ResponseEnvelope & (ResponseInitWallet | ResponseSettle | ResponseSettleEvent | ResponseSendBitcoin | ResponseGetAddress | ResponseGetBoardingAddress | ResponseGetBalance | ResponseGetVtxos | ResponseGetBoardingUtxos | ResponseGetTransactionHistory | ResponseGetStatus | ResponseClear | ResponseReloadWallet | ResponseUtxoUpdate | ResponseVtxoUpdate | ResponseSignTransaction | ResponseCreateContract | ResponseGetContracts | ResponseGetContractsWithVtxos | ResponseUpdateContract | ResponseDeleteContract | ResponseGetSpendablePaths | ResponseGetAllSpendingPaths | ResponseIsContractManagerWatching | ResponseContractEvent | ResponseSend | ResponseGetAssetDetails | ResponseIssue | ResponseReissue | ResponseBurn | ResponseDelegate | ResponseGetDelegateInfo);
375
+ export type RequestRecoverVtxos = RequestEnvelope & {
376
+ type: "RECOVER_VTXOS";
377
+ };
378
+ export type ResponseRecoverVtxos = ResponseEnvelope & {
379
+ type: "RECOVER_VTXOS_SUCCESS";
380
+ payload: {
381
+ txid: string;
382
+ };
383
+ };
384
+ export type RequestGetRecoverableBalance = RequestEnvelope & {
385
+ type: "GET_RECOVERABLE_BALANCE";
386
+ };
387
+ export type ResponseGetRecoverableBalance = ResponseEnvelope & {
388
+ type: "RECOVERABLE_BALANCE";
389
+ payload: {
390
+ recoverable: string;
391
+ subdust: string;
392
+ includesSubdust: boolean;
393
+ vtxoCount: number;
394
+ };
395
+ };
396
+ export type RequestGetExpiringVtxos = RequestEnvelope & {
397
+ type: "GET_EXPIRING_VTXOS";
398
+ payload: {
399
+ thresholdMs?: number;
400
+ };
401
+ };
402
+ export type ResponseGetExpiringVtxos = ResponseEnvelope & {
403
+ type: "EXPIRING_VTXOS";
404
+ payload: {
405
+ vtxos: ExtendedVirtualCoin[];
406
+ };
407
+ };
408
+ export type RequestRenewVtxos = RequestEnvelope & {
409
+ type: "RENEW_VTXOS";
410
+ };
411
+ export type ResponseRenewVtxos = ResponseEnvelope & {
412
+ type: "RENEW_VTXOS_SUCCESS";
413
+ payload: {
414
+ txid: string;
415
+ };
416
+ };
417
+ export type RequestGetExpiredBoardingUtxos = RequestEnvelope & {
418
+ type: "GET_EXPIRED_BOARDING_UTXOS";
419
+ };
420
+ export type ResponseGetExpiredBoardingUtxos = ResponseEnvelope & {
421
+ type: "EXPIRED_BOARDING_UTXOS";
422
+ payload: {
423
+ utxos: ExtendedCoin[];
424
+ };
425
+ };
426
+ export type RequestSweepExpiredBoardingUtxos = RequestEnvelope & {
427
+ type: "SWEEP_EXPIRED_BOARDING_UTXOS";
428
+ };
429
+ export type ResponseSweepExpiredBoardingUtxos = ResponseEnvelope & {
430
+ type: "SWEEP_EXPIRED_BOARDING_UTXOS_SUCCESS";
431
+ payload: {
432
+ txid: string;
433
+ };
434
+ };
435
+ export type WalletUpdaterRequest = RequestInitWallet | RequestSettle | RequestSendBitcoin | RequestGetAddress | RequestGetBoardingAddress | RequestGetBalance | RequestGetVtxos | RequestGetBoardingUtxos | RequestGetTransactionHistory | RequestGetStatus | RequestClear | RequestReloadWallet | RequestSignTransaction | RequestCreateContract | RequestGetContracts | RequestGetContractsWithVtxos | RequestUpdateContract | RequestDeleteContract | RequestGetSpendablePaths | RequestGetAllSpendingPaths | RequestIsContractManagerWatching | RequestSend | RequestGetAssetDetails | RequestIssue | RequestReissue | RequestBurn | RequestDelegate | RequestGetDelegateInfo | RequestRecoverVtxos | RequestGetRecoverableBalance | RequestGetExpiringVtxos | RequestRenewVtxos | RequestGetExpiredBoardingUtxos | RequestSweepExpiredBoardingUtxos;
436
+ export type WalletUpdaterResponse = ResponseEnvelope & (ResponseInitWallet | ResponseSettle | ResponseSettleEvent | ResponseSendBitcoin | ResponseGetAddress | ResponseGetBoardingAddress | ResponseGetBalance | ResponseGetVtxos | ResponseGetBoardingUtxos | ResponseGetTransactionHistory | ResponseGetStatus | ResponseClear | ResponseReloadWallet | ResponseUtxoUpdate | ResponseVtxoUpdate | ResponseSignTransaction | ResponseCreateContract | ResponseGetContracts | ResponseGetContractsWithVtxos | ResponseUpdateContract | ResponseDeleteContract | ResponseGetSpendablePaths | ResponseGetAllSpendingPaths | ResponseIsContractManagerWatching | ResponseContractEvent | ResponseSend | ResponseGetAssetDetails | ResponseIssue | ResponseReissue | ResponseBurn | ResponseDelegate | ResponseGetDelegateInfo | ResponseRecoverVtxos | ResponseRecoverVtxosEvent | ResponseGetRecoverableBalance | ResponseGetExpiringVtxos | ResponseRenewVtxos | ResponseRenewVtxosEvent | ResponseGetExpiredBoardingUtxos | ResponseSweepExpiredBoardingUtxos);
360
437
  export declare class WalletMessageHandler implements MessageHandler<WalletUpdaterRequest, WalletUpdaterResponse> {
361
438
  readonly messageTag: string;
362
439
  private wallet;
@@ -6,6 +6,7 @@ import { ContractRepository } from "../../repositories/contractRepository";
6
6
  import { RequestInitWallet, ResponseGetStatus, WalletUpdaterRequest, WalletUpdaterResponse } from "./wallet-message-handler";
7
7
  import type { IContractManager } from "../../contracts/contractManager";
8
8
  import type { IDelegatorManager } from "../delegator";
9
+ import type { IVtxoManager } from "../vtxo-manager";
9
10
  type PrivateKeyIdentity = Identity & {
10
11
  toHex(): string;
11
12
  };
@@ -82,6 +83,8 @@ export declare class ServiceWorkerReadonlyWallet implements IReadonlyWallet {
82
83
  protected initWalletPayload: RequestInitWallet["payload"] | null;
83
84
  protected messageBusTimeoutMs?: number;
84
85
  private reinitPromise;
86
+ private pingPromise;
87
+ private inflightRequests;
85
88
  get assetManager(): IReadonlyAssetManager;
86
89
  protected constructor(serviceWorker: ServiceWorker, identity: ReadonlyIdentity, walletRepository: WalletRepository, contractRepository: ContractRepository, messageTag: string);
87
90
  static create(options: ServiceWorkerWalletCreateOptions): Promise<ServiceWorkerReadonlyWallet>;
@@ -108,7 +111,11 @@ export declare class ServiceWorkerReadonlyWallet implements IReadonlyWallet {
108
111
  */
109
112
  static setup(options: ServiceWorkerWalletSetupOptions): Promise<ServiceWorkerReadonlyWallet>;
110
113
  private sendMessageDirect;
114
+ private sendMessageStreaming;
111
115
  protected sendMessage(request: WalletUpdaterRequest): Promise<WalletUpdaterResponse>;
116
+ private pingServiceWorker;
117
+ private sendMessageWithRetry;
118
+ protected sendMessageWithEvents(request: WalletUpdaterRequest, onEvent: (response: WalletUpdaterResponse) => void, isComplete: (response: WalletUpdaterResponse) => boolean): Promise<WalletUpdaterResponse>;
112
119
  private reinitialize;
113
120
  clear(): Promise<void>;
114
121
  getAddress(): Promise<string>;
@@ -157,5 +164,6 @@ export declare class ServiceWorkerWallet extends ServiceWorkerReadonlyWallet imp
157
164
  settle(params?: SettleParams, callback?: (event: SettlementEvent) => void): Promise<string>;
158
165
  send(...recipients: Recipient[]): Promise<string>;
159
166
  getDelegatorManager(): Promise<IDelegatorManager | undefined>;
167
+ getVtxoManager(): Promise<IVtxoManager>;
160
168
  }
161
169
  export {};
@@ -145,7 +145,21 @@ export declare function getExpiringAndRecoverableVtxos(vtxos: ExtendedVirtualCoi
145
145
  * }
146
146
  * ```
147
147
  */
148
- export declare class VtxoManager implements AsyncDisposable {
148
+ export interface IVtxoManager {
149
+ recoverVtxos(eventCallback?: (event: SettlementEvent) => void): Promise<string>;
150
+ getRecoverableBalance(): Promise<{
151
+ recoverable: bigint;
152
+ subdust: bigint;
153
+ includesSubdust: boolean;
154
+ vtxoCount: number;
155
+ }>;
156
+ getExpiringVtxos(thresholdMs?: number): Promise<ExtendedVirtualCoin[]>;
157
+ renewVtxos(eventCallback?: (event: SettlementEvent) => void): Promise<string>;
158
+ getExpiredBoardingUtxos(): Promise<ExtendedCoin[]>;
159
+ sweepExpiredBoardingUtxos(): Promise<string>;
160
+ dispose(): Promise<void>;
161
+ }
162
+ export declare class VtxoManager implements AsyncDisposable, IVtxoManager {
149
163
  readonly wallet: IWallet;
150
164
  /** @deprecated Use settlementConfig instead */
151
165
  readonly renewalConfig?: RenewalConfig | undefined;
@@ -153,10 +167,12 @@ export declare class VtxoManager implements AsyncDisposable {
153
167
  private contractEventsSubscription?;
154
168
  private readonly contractEventsSubscriptionReady;
155
169
  private disposePromise?;
156
- private pollIntervalId?;
170
+ private pollTimeoutId?;
157
171
  private knownBoardingUtxos;
158
172
  private sweptBoardingUtxos;
159
173
  private pollInProgress;
174
+ private consecutivePollFailures;
175
+ private static readonly MAX_BACKOFF_MS;
160
176
  constructor(wallet: IWallet,
161
177
  /** @deprecated Use settlementConfig instead */
162
178
  renewalConfig?: RenewalConfig | undefined, settlementConfig?: SettlementConfig | false);
@@ -323,12 +339,18 @@ export declare class VtxoManager implements AsyncDisposable {
323
339
  /** Returns the wallet's identity for transaction signing. */
324
340
  private getIdentity;
325
341
  private initializeSubscription;
342
+ /** Computes the next poll delay, applying exponential backoff on failures. */
343
+ private getNextPollDelay;
326
344
  /**
327
345
  * Starts a polling loop that:
328
346
  * 1. Auto-settles new boarding UTXOs into Ark
329
347
  * 2. Sweeps expired boarding UTXOs (when boardingUtxoSweep is enabled)
348
+ *
349
+ * Uses setTimeout chaining (not setInterval) so a slow/blocked poll
350
+ * cannot stack up and the next delay can incorporate backoff.
330
351
  */
331
352
  private startBoardingUtxoPoll;
353
+ private schedulePoll;
332
354
  private pollBoardingUtxos;
333
355
  /**
334
356
  * Auto-settle new (unexpired) boarding UTXOs into the Ark.
@@ -171,6 +171,14 @@ export declare class Wallet extends ReadonlyWallet implements IWallet {
171
171
  private _vtxoManager?;
172
172
  private _vtxoManagerInitializing?;
173
173
  private _walletAssetManager?;
174
+ /**
175
+ * Async mutex that serializes all operations submitting VTXOs to the Ark
176
+ * server (`settle`, `send`, `sendBitcoin`). This prevents VtxoManager's
177
+ * background renewal from racing with user-initiated transactions for the
178
+ * same VTXO inputs.
179
+ */
180
+ private _txLock;
181
+ private _withTxLock;
174
182
  /** @deprecated Use settlementConfig instead */
175
183
  readonly renewalConfig: Required<Omit<WalletConfig["renewalConfig"], "enabled">> & {
176
184
  enabled: boolean;
@@ -209,6 +217,7 @@ export declare class Wallet extends ReadonlyWallet implements IWallet {
209
217
  */
210
218
  sendBitcoin(params: SendBitcoinParams): Promise<string>;
211
219
  settle(params?: SettleParams, eventCallback?: (event: SettlementEvent) => void): Promise<string>;
220
+ private _settleImpl;
212
221
  private handleSettlementFinalizationEvent;
213
222
  /**
214
223
  * @implements Batch.Handler interface.
@@ -247,6 +256,7 @@ export declare class Wallet extends ReadonlyWallet implements IWallet {
247
256
  * ```
248
257
  */
249
258
  send(...args: Recipient[]): Promise<string>;
259
+ private _sendImpl;
250
260
  /**
251
261
  * Build an offchain transaction from the given inputs and outputs,
252
262
  * sign it, submit to the ark provider, and finalize.
@@ -0,0 +1,6 @@
1
+ export declare class MessageBusNotInitializedError extends Error {
2
+ constructor();
3
+ }
4
+ export declare class ServiceWorkerTimeoutError extends Error {
5
+ constructor(detail: string);
6
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@arkade-os/sdk",
3
- "version": "0.4.6",
3
+ "version": "0.4.8",
4
4
  "description": "Bitcoin wallet SDK with Taproot and Ark integration",
5
5
  "type": "module",
6
6
  "main": "./dist/cjs/index.js",