@arkade-os/sdk 0.4.13 → 0.4.15

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/esm/index.js CHANGED
@@ -1,6 +1,7 @@
1
1
  import { Transaction } from './utils/transaction.js';
2
2
  import { SingleKey, ReadonlySingleKey } from './identity/singleKey.js';
3
3
  import { SeedIdentity, MnemonicIdentity, ReadonlyDescriptorIdentity, } from './identity/seedIdentity.js';
4
+ import { isBatchSignable, } from './identity/index.js';
4
5
  import { ArkAddress } from './script/address.js';
5
6
  import { VHTLC } from './script/vhtlc.js';
6
7
  import { DefaultVtxo } from './script/default.js';
@@ -13,7 +14,7 @@ import { Wallet, ReadonlyWallet, waitForIncomingFunds, } from './wallet/wallet.j
13
14
  import { TxTree } from './tree/txTree.js';
14
15
  import { Ramps } from './wallet/ramps.js';
15
16
  import { isVtxoExpiringSoon, VtxoManager } from './wallet/vtxo-manager.js';
16
- import { ServiceWorkerWallet, ServiceWorkerReadonlyWallet, } from './wallet/serviceWorker/wallet.js';
17
+ import { ServiceWorkerWallet, ServiceWorkerReadonlyWallet, DEFAULT_MESSAGE_TIMEOUTS, } from './wallet/serviceWorker/wallet.js';
17
18
  import { OnchainWallet } from './wallet/onchain.js';
18
19
  import { setupServiceWorker } from './worker/browser/utils.js';
19
20
  import { ESPLORA_URL, EsploraProvider, } from './providers/onchain.js';
@@ -43,7 +44,7 @@ import { WalletMessageHandler, WalletNotInitializedError, ReadonlyWalletError, D
43
44
  import { MESSAGE_BUS_NOT_INITIALIZED, MessageBusNotInitializedError, ServiceWorkerTimeoutError, } from './worker/errors.js';
44
45
  export {
45
46
  // Wallets
46
- Wallet, ReadonlyWallet, SingleKey, ReadonlySingleKey, SeedIdentity, MnemonicIdentity, ReadonlyDescriptorIdentity, OnchainWallet, Ramps, VtxoManager, DelegatorManagerImpl, RestDelegatorProvider,
47
+ Wallet, ReadonlyWallet, SingleKey, ReadonlySingleKey, SeedIdentity, MnemonicIdentity, ReadonlyDescriptorIdentity, isBatchSignable, OnchainWallet, Ramps, VtxoManager, DelegatorManagerImpl, RestDelegatorProvider,
47
48
  // Providers
48
49
  ESPLORA_URL, EsploraProvider, RestArkProvider, RestIndexerProvider,
49
50
  // Script-related
@@ -51,7 +52,7 @@ ArkAddress, DefaultVtxo, DelegateVtxo, VtxoScript, VHTLC,
51
52
  // Enums
52
53
  TxType, IndexerTxType, ChainTxType, SettlementEventType,
53
54
  // Service Worker
54
- setupServiceWorker, MessageBus, WalletMessageHandler, WalletNotInitializedError, ReadonlyWalletError, DelegatorNotConfiguredError, MESSAGE_BUS_NOT_INITIALIZED, MessageBusNotInitializedError, ServiceWorkerTimeoutError, ServiceWorkerWallet, ServiceWorkerReadonlyWallet,
55
+ setupServiceWorker, MessageBus, WalletMessageHandler, WalletNotInitializedError, ReadonlyWalletError, DelegatorNotConfiguredError, MESSAGE_BUS_NOT_INITIALIZED, MessageBusNotInitializedError, ServiceWorkerTimeoutError, ServiceWorkerWallet, ServiceWorkerReadonlyWallet, DEFAULT_MESSAGE_TIMEOUTS,
55
56
  // Tapscript
56
57
  decodeTapscript, MultisigTapscript, CSVMultisigTapscript, ConditionCSVMultisigTapscript, ConditionMultisigTapscript, CLTVMultisigTapscript, TapTreeCoder,
57
58
  // Ark PSBT fields
@@ -498,7 +498,7 @@ export var CLTVMultisigTapscript;
498
498
  throw new Error(`Invalid script: too short (expected at least 3)`);
499
499
  }
500
500
  const locktime = asm[0];
501
- if (typeof locktime === "string" || typeof locktime === "number") {
501
+ if (typeof locktime === "string") {
502
502
  throw new Error("Invalid script: expected locktime number");
503
503
  }
504
504
  if (asm[1] !== "CHECKLOCKTIMEVERIFY" || asm[2] !== "DROP") {
@@ -512,7 +512,13 @@ export var CLTVMultisigTapscript;
512
512
  catch (error) {
513
513
  throw new Error(`Invalid multisig script: ${error instanceof Error ? error.message : String(error)}`);
514
514
  }
515
- const absoluteTimelock = MinimalScriptNum.decode(locktime);
515
+ let absoluteTimelock;
516
+ if (typeof locktime === "number") {
517
+ absoluteTimelock = BigInt(locktime);
518
+ }
519
+ else {
520
+ absoluteTimelock = MinimalScriptNum.decode(locktime);
521
+ }
516
522
  const reconstructed = encode({
517
523
  absoluteTimelock,
518
524
  ...multisig.params,
@@ -11,6 +11,47 @@ function isMessageBusNotInitializedError(error) {
11
11
  return (error instanceof Error &&
12
12
  error.message.includes(MESSAGE_BUS_NOT_INITIALIZED));
13
13
  }
14
+ export const DEFAULT_MESSAGE_TIMEOUTS = {
15
+ // Fast reads — fail quickly
16
+ GET_ADDRESS: 10000,
17
+ GET_BALANCE: 10000,
18
+ GET_BOARDING_ADDRESS: 10000,
19
+ GET_STATUS: 10000,
20
+ GET_DELEGATE_INFO: 10000,
21
+ IS_CONTRACT_MANAGER_WATCHING: 10000,
22
+ // Medium reads — may involve indexer queries
23
+ GET_VTXOS: 20000,
24
+ GET_BOARDING_UTXOS: 20000,
25
+ GET_TRANSACTION_HISTORY: 20000,
26
+ GET_CONTRACTS: 20000,
27
+ GET_CONTRACTS_WITH_VTXOS: 20000,
28
+ GET_SPENDABLE_PATHS: 20000,
29
+ GET_ALL_SPENDING_PATHS: 20000,
30
+ GET_ASSET_DETAILS: 20000,
31
+ GET_EXPIRING_VTXOS: 20000,
32
+ GET_EXPIRED_BOARDING_UTXOS: 20000,
33
+ GET_RECOVERABLE_BALANCE: 20000,
34
+ RELOAD_WALLET: 20000,
35
+ // Transactions — need more headroom
36
+ SEND_BITCOIN: 50000,
37
+ SEND: 50000,
38
+ SETTLE: 50000,
39
+ ISSUE: 50000,
40
+ REISSUE: 50000,
41
+ BURN: 50000,
42
+ DELEGATE: 50000,
43
+ RECOVER_VTXOS: 50000,
44
+ RENEW_VTXOS: 50000,
45
+ SWEEP_EXPIRED_BOARDING_UTXOS: 50000,
46
+ // Misc writes
47
+ INIT_WALLET: 30000,
48
+ CLEAR: 10000,
49
+ SIGN_TRANSACTION: 30000,
50
+ CREATE_CONTRACT: 30000,
51
+ UPDATE_CONTRACT: 30000,
52
+ DELETE_CONTRACT: 10000,
53
+ REFRESH_VTXOS: 30000,
54
+ };
14
55
  const DEDUPABLE_REQUEST_TYPES = new Set([
15
56
  "GET_ADDRESS",
16
57
  "GET_BALANCE",
@@ -126,6 +167,7 @@ export class ServiceWorkerReadonlyWallet {
126
167
  this.messageTag = messageTag;
127
168
  this.initConfig = null;
128
169
  this.initWalletPayload = null;
170
+ this.messageTimeouts = DEFAULT_MESSAGE_TIMEOUTS;
129
171
  this.reinitPromise = null;
130
172
  this.pingPromise = null;
131
173
  this.inflightRequests = new Map();
@@ -134,6 +176,9 @@ export class ServiceWorkerReadonlyWallet {
134
176
  this.contractRepository = contractRepository;
135
177
  this._readonlyAssetManager = new ServiceWorkerReadonlyAssetManager((msg) => this.sendMessage(msg), messageTag);
136
178
  }
179
+ getTimeoutForRequest(request) {
180
+ return this.messageTimeouts[request.type] ?? 30000;
181
+ }
137
182
  static async create(options) {
138
183
  const walletRepository = options.storage?.walletRepository ??
139
184
  new IndexedDBWalletRepository();
@@ -185,6 +230,12 @@ export class ServiceWorkerReadonlyWallet {
185
230
  };
186
231
  wallet.initWalletPayload = initConfig;
187
232
  wallet.messageBusTimeoutMs = options.messageBusTimeoutMs;
233
+ if (options.messageTimeouts) {
234
+ wallet.messageTimeouts = {
235
+ ...DEFAULT_MESSAGE_TIMEOUTS,
236
+ ...options.messageTimeouts,
237
+ };
238
+ }
188
239
  return wallet;
189
240
  }
190
241
  /**
@@ -220,7 +271,7 @@ export class ServiceWorkerReadonlyWallet {
220
271
  serviceWorker,
221
272
  });
222
273
  }
223
- sendMessageDirect(request) {
274
+ sendMessageDirect(request, timeoutMs) {
224
275
  return new Promise((resolve, reject) => {
225
276
  const cleanup = () => {
226
277
  clearTimeout(timeoutId);
@@ -229,7 +280,7 @@ export class ServiceWorkerReadonlyWallet {
229
280
  const timeoutId = setTimeout(() => {
230
281
  cleanup();
231
282
  reject(new ServiceWorkerTimeoutError(`Service worker message timed out (${request.type})`));
232
- }, 30000);
283
+ }, timeoutMs);
233
284
  const messageHandler = (event) => {
234
285
  const response = event.data;
235
286
  if (request.id !== response.id) {
@@ -252,14 +303,14 @@ export class ServiceWorkerReadonlyWallet {
252
303
  // first response for which isComplete returns true. The timeout resets
253
304
  // on every intermediate event so long-running but progressing operations
254
305
  // don't time out prematurely.
255
- sendMessageStreaming(request, onEvent, isComplete) {
306
+ sendMessageStreaming(request, onEvent, isComplete, timeoutMs) {
256
307
  return new Promise((resolve, reject) => {
257
308
  const resetTimeout = () => {
258
309
  clearTimeout(timeoutId);
259
310
  timeoutId = setTimeout(() => {
260
311
  cleanup();
261
312
  reject(new ServiceWorkerTimeoutError(`Service worker message timed out (${request.type})`));
262
- }, 30000);
313
+ }, timeoutMs);
263
314
  };
264
315
  const cleanup = () => {
265
316
  clearTimeout(timeoutId);
@@ -345,10 +396,11 @@ export class ServiceWorkerReadonlyWallet {
345
396
  await this.reinitialize();
346
397
  }
347
398
  }
399
+ const timeoutMs = this.getTimeoutForRequest(request);
348
400
  const maxRetries = 2;
349
401
  for (let attempt = 0;; attempt++) {
350
402
  try {
351
- return await this.sendMessageDirect(request);
403
+ return await this.sendMessageDirect(request, timeoutMs);
352
404
  }
353
405
  catch (error) {
354
406
  if (!isMessageBusNotInitializedError(error) ||
@@ -370,10 +422,11 @@ export class ServiceWorkerReadonlyWallet {
370
422
  await this.reinitialize();
371
423
  }
372
424
  }
425
+ const timeoutMs = this.getTimeoutForRequest(request);
373
426
  const maxRetries = 2;
374
427
  for (let attempt = 0;; attempt++) {
375
428
  try {
376
- return await this.sendMessageStreaming(request, onEvent, isComplete);
429
+ return await this.sendMessageStreaming(request, onEvent, isComplete, timeoutMs);
377
430
  }
378
431
  catch (error) {
379
432
  if (!isMessageBusNotInitializedError(error) ||
@@ -398,7 +451,7 @@ export class ServiceWorkerReadonlyWallet {
398
451
  id: getRandomId(),
399
452
  payload: this.initWalletPayload,
400
453
  };
401
- await this.sendMessageDirect(initMessage);
454
+ await this.sendMessageDirect(initMessage, this.getTimeoutForRequest(initMessage));
402
455
  })().finally(() => {
403
456
  this.reinitPromise = null;
404
457
  });
@@ -789,6 +842,12 @@ export class ServiceWorkerWallet extends ServiceWorkerReadonlyWallet {
789
842
  };
790
843
  wallet.initWalletPayload = initConfig;
791
844
  wallet.messageBusTimeoutMs = options.messageBusTimeoutMs;
845
+ if (options.messageTimeouts) {
846
+ wallet.messageTimeouts = {
847
+ ...DEFAULT_MESSAGE_TIMEOUTS,
848
+ ...options.messageTimeouts,
849
+ };
850
+ }
792
851
  return wallet;
793
852
  }
794
853
  /**
@@ -576,6 +576,7 @@ export class VtxoManager {
576
576
  return;
577
577
  }
578
578
  if (e.message.includes("VTXO_ALREADY_REGISTERED") ||
579
+ e.message.includes("VTXO_ALREADY_SPENT") ||
579
580
  e.message.includes("duplicated input")) {
580
581
  // VTXO is already being used in a concurrent
581
582
  // user-initiated operation. Skip silently — the
@@ -10,11 +10,12 @@ import { RestArkProvider, } from '../providers/ark.js';
10
10
  import { buildForfeitTx } from '../forfeit.js';
11
11
  import { validateConnectorsTxGraph, validateVtxoTxGraph, } from '../tree/validation.js';
12
12
  import { validateBatchRecipients } from './validation.js';
13
+ import { isBatchSignable } from '../identity/index.js';
13
14
  import { isExpired, isRecoverable, isSpendable, isSubdust, TxType, } from './index.js';
14
15
  import { createAssetPacket, selectedCoinsToAssetInputs, selectCoinsWithAsset, } from './asset.js';
15
16
  import { VtxoScript } from '../script/base.js';
16
17
  import { CSVMultisigTapscript } from '../script/tapscript.js';
17
- import { buildOffchainTx, hasBoardingTxExpired, isValidArkAddress, } from '../utils/arkTransaction.js';
18
+ import { buildOffchainTx, combineTapscriptSigs, hasBoardingTxExpired, isValidArkAddress, } from '../utils/arkTransaction.js';
18
19
  import { DEFAULT_RENEWAL_CONFIG, DEFAULT_SETTLEMENT_CONFIG, VtxoManager, } from './vtxo-manager.js';
19
20
  import { ArkNote } from '../arknote/index.js';
20
21
  import { Intent } from '../intent/index.js';
@@ -340,6 +341,19 @@ export class ReadonlyWallet {
340
341
  }
341
342
  const requestStartedAt = Date.now();
342
343
  const allVtxos = [];
344
+ const extendWithScript = (vtxo) => {
345
+ const vtxoScript = vtxo.script
346
+ ? scriptMap.get(vtxo.script)
347
+ : undefined;
348
+ if (!vtxoScript)
349
+ return undefined;
350
+ return {
351
+ ...vtxo,
352
+ forfeitTapLeafScript: vtxoScript.forfeit(),
353
+ intentTapLeafScript: vtxoScript.forfeit(),
354
+ tapTree: vtxoScript.encode(),
355
+ };
356
+ };
343
357
  // Full fetch for scripts with no cursor.
344
358
  if (bootstrapScripts.length > 0) {
345
359
  const response = await this.indexerProvider.getVtxos({
@@ -364,46 +378,76 @@ export class ReadonlyWallet {
364
378
  // Extend every fetched VTXO and upsert into the cache.
365
379
  const fetchedExtended = [];
366
380
  for (const vtxo of allVtxos) {
367
- const vtxoScript = vtxo.script
368
- ? scriptMap.get(vtxo.script)
369
- : undefined;
370
- if (!vtxoScript)
371
- continue;
372
- fetchedExtended.push({
373
- ...vtxo,
374
- forfeitTapLeafScript: vtxoScript.forfeit(),
375
- intentTapLeafScript: vtxoScript.forfeit(),
376
- tapTree: vtxoScript.encode(),
377
- });
381
+ const extended = extendWithScript(vtxo);
382
+ if (extended)
383
+ fetchedExtended.push(extended);
378
384
  }
379
385
  // Save VTXOs first, then advance cursors only on success.
380
386
  const cutoff = cursorCutoff(requestStartedAt);
381
387
  await this.walletRepository.saveVtxos(address, fetchedExtended);
382
388
  await advanceSyncCursors(this.walletRepository, Object.fromEntries(allScripts.map((s) => [s, cutoff])));
383
- // For delta syncs, reconcile pending (preconfirmed/spent) VTXOs
384
- // whose state may have changed since the cursor so that
385
- // getVtxos()/getTransactionHistory() don't serve stale state.
389
+ // Delta-sync reconciliation: full re-fetch for delta scripts.
390
+ //
391
+ // The delta fetch (above) only returns VTXOs changed after the
392
+ // cursor, so it can miss preconfirmed VTXOs that were consumed
393
+ // by a round between syncs. Rather than layering targeted
394
+ // queries (pendingOnly, spendableOnly) with pagination guards
395
+ // and set algebra, we perform a single unfiltered re-fetch for
396
+ // delta scripts. This is slightly more data over the wire but
397
+ // gives us complete, authoritative state in one call and keeps
398
+ // the reconciliation logic simple.
399
+ //
400
+ // Any cached non-spent VTXO that is absent from the full
401
+ // result set is marked spent; any VTXO whose state changed
402
+ // (e.g. preconfirmed → settled) is updated in place.
386
403
  if (hasDelta) {
387
- const { vtxos: pendingVtxos } = await this.indexerProvider.getVtxos({
404
+ const { vtxos: fullVtxos, page: fullPage } = await this.indexerProvider.getVtxos({
388
405
  scripts: deltaScripts,
389
- pendingOnly: true,
390
406
  });
391
- const pendingExtended = [];
392
- for (const vtxo of pendingVtxos) {
393
- const vtxoScript = vtxo.script
394
- ? scriptMap.get(vtxo.script)
395
- : undefined;
396
- if (!vtxoScript)
397
- continue;
398
- pendingExtended.push({
399
- ...vtxo,
400
- forfeitTapLeafScript: vtxoScript.forfeit(),
401
- intentTapLeafScript: vtxoScript.forfeit(),
402
- tapTree: vtxoScript.encode(),
403
- });
407
+ // Reconciliation is best-effort: if the response is
408
+ // paginated we don't have a complete picture, so we skip
409
+ // rather than act on partial data. Wallets with enough
410
+ // VTXOs to exceed a single page rely solely on the
411
+ // cursor-based delta mechanism for state updates.
412
+ const fullSetComplete = !fullPage || fullPage.total <= 1;
413
+ if (fullSetComplete) {
414
+ const fullOutpoints = new Map(fullVtxos.map((v) => [`${v.txid}:${v.vout}`, v]));
415
+ const deltaScriptSet = new Set(deltaScripts);
416
+ const cachedVtxos = await this.walletRepository.getVtxos(address);
417
+ const reconciledExtended = [];
418
+ for (const cached of cachedVtxos) {
419
+ if (!cached.script ||
420
+ !deltaScriptSet.has(cached.script) ||
421
+ cached.isSpent) {
422
+ continue;
423
+ }
424
+ const outpoint = `${cached.txid}:${cached.vout}`;
425
+ const fresh = fullOutpoints.get(outpoint);
426
+ if (!fresh) {
427
+ // Server no longer knows about this VTXO —
428
+ // it was spent between syncs.
429
+ reconciledExtended.push({
430
+ ...cached,
431
+ isSpent: true,
432
+ });
433
+ continue;
434
+ }
435
+ const extended = extendWithScript(fresh);
436
+ if (extended &&
437
+ extended.virtualStatus.state !==
438
+ cached.virtualStatus.state) {
439
+ // State transitioned (e.g. preconfirmed →
440
+ // settled) — update the cached entry.
441
+ reconciledExtended.push(extended);
442
+ }
443
+ }
444
+ if (reconciledExtended.length > 0) {
445
+ console.warn(`[ark-sdk] delta sync: reconciled ${reconciledExtended.length} stale VTXO(s) via full re-fetch`);
446
+ await this.walletRepository.saveVtxos(address, reconciledExtended);
447
+ }
404
448
  }
405
- if (pendingExtended.length > 0) {
406
- await this.walletRepository.saveVtxos(address, pendingExtended);
449
+ else {
450
+ console.warn("[ark-sdk] delta sync: skipping reconciliation — full re-fetch was paginated");
407
451
  }
408
452
  }
409
453
  return {
@@ -1713,16 +1757,47 @@ export class Wallet extends ReadonlyWallet {
1713
1757
  tapLeafScript: input.forfeitTapLeafScript,
1714
1758
  };
1715
1759
  }), outputs, this.serverUnrollScript);
1716
- const signedVirtualTx = await this.identity.sign(offchainTx.arkTx);
1760
+ let signedVirtualTx;
1761
+ let userSignedCheckpoints;
1762
+ if (isBatchSignable(this.identity)) {
1763
+ // Batch-sign arkTx + all checkpoints in one wallet popup.
1764
+ // Clone so the provider can't mutate originals before submitTx.
1765
+ const requests = [
1766
+ { tx: offchainTx.arkTx.clone() },
1767
+ ...offchainTx.checkpoints.map((c) => ({ tx: c.clone() })),
1768
+ ];
1769
+ const signed = await this.identity.signMultiple(requests);
1770
+ if (signed.length !== requests.length) {
1771
+ throw new Error(`signMultiple returned ${signed.length} transactions, expected ${requests.length}`);
1772
+ }
1773
+ const [firstSignedTx, ...signedCheckpoints] = signed;
1774
+ signedVirtualTx = firstSignedTx;
1775
+ userSignedCheckpoints = signedCheckpoints;
1776
+ }
1777
+ else {
1778
+ signedVirtualTx = await this.identity.sign(offchainTx.arkTx);
1779
+ }
1717
1780
  // Mark pending before submitting — if we crash between submit and
1718
1781
  // finalize, the next init will recover via finalizePendingTxs.
1719
1782
  await this.setPendingTxFlag(true);
1720
1783
  const { arkTxid, signedCheckpointTxs } = await this.arkProvider.submitTx(base64.encode(signedVirtualTx.toPSBT()), offchainTx.checkpoints.map((c) => base64.encode(c.toPSBT())));
1721
- const finalCheckpoints = await Promise.all(signedCheckpointTxs.map(async (c) => {
1722
- const tx = Transaction.fromPSBT(base64.decode(c));
1723
- const signedCheckpoint = await this.identity.sign(tx);
1724
- return base64.encode(signedCheckpoint.toPSBT());
1725
- }));
1784
+ let finalCheckpoints;
1785
+ if (userSignedCheckpoints) {
1786
+ // Merge pre-signed user signatures onto server-signed checkpoints
1787
+ finalCheckpoints = signedCheckpointTxs.map((c, i) => {
1788
+ const serverSigned = Transaction.fromPSBT(base64.decode(c));
1789
+ combineTapscriptSigs(userSignedCheckpoints[i], serverSigned);
1790
+ return base64.encode(serverSigned.toPSBT());
1791
+ });
1792
+ }
1793
+ else {
1794
+ // Legacy: sign each checkpoint individually (N popups)
1795
+ finalCheckpoints = await Promise.all(signedCheckpointTxs.map(async (c) => {
1796
+ const tx = Transaction.fromPSBT(base64.decode(c));
1797
+ const signedCheckpoint = await this.identity.sign(tx);
1798
+ return base64.encode(signedCheckpoint.toPSBT());
1799
+ }));
1800
+ }
1726
1801
  await this.arkProvider.finalizeTx(arkTxid, finalCheckpoints);
1727
1802
  try {
1728
1803
  await this.setPendingTxFlag(false);
@@ -12,6 +12,16 @@ export declare function sequenceToTimelock(sequence: number): RelativeTimelock;
12
12
  * Resolve wallet's role from explicit role or by matching pubkey.
13
13
  */
14
14
  export declare function resolveRole(contract: Contract, context: PathContext): "sender" | "receiver" | undefined;
15
+ /**
16
+ * Check if an absolute (CLTV) locktime is currently satisfied.
17
+ *
18
+ * Following the BIP65 convention:
19
+ * - locktime < 500_000_000 → interpreted as a block height; compared against `context.blockHeight`
20
+ * - locktime >= 500_000_000 → interpreted as a Unix timestamp (seconds); compared against `context.currentTime`
21
+ *
22
+ * Returns false if the relevant context field is missing.
23
+ */
24
+ export declare function isCltvSatisfied(context: PathContext, locktime: bigint): boolean;
15
25
  /**
16
26
  * Check if a CSV timelock is currently satisfied for the given context/VTXO.
17
27
  */
@@ -9,5 +9,25 @@ export interface ReadonlyIdentity {
9
9
  xOnlyPublicKey(): Promise<Uint8Array>;
10
10
  compressedPublicKey(): Promise<Uint8Array>;
11
11
  }
12
+ /** A single PSBT signing request within a batch. */
13
+ export interface SignRequest {
14
+ tx: Transaction;
15
+ inputIndexes?: number[];
16
+ }
17
+ /**
18
+ * Identity that supports signing multiple PSBTs in a single wallet interaction.
19
+ * Browser wallet providers that support batch signing (e.g. Xverse, UniSat, OKX)
20
+ * should implement this interface to reduce the number of confirmation popups
21
+ * from N+1 to 1 during Arkade send transactions.
22
+ *
23
+ * Contract: implementations MUST return exactly one `Transaction` per request,
24
+ * in the same order as the input array. The SDK validates this at runtime and
25
+ * will throw if the lengths do not match.
26
+ */
27
+ export interface BatchSignableIdentity extends Identity {
28
+ signMultiple(requests: SignRequest[]): Promise<Transaction[]>;
29
+ }
30
+ /** Type guard for identities that support batch signing. */
31
+ export declare function isBatchSignable(identity: Identity): identity is BatchSignableIdentity;
12
32
  export * from "./singleKey";
13
33
  export * from "./seedIdentity";
@@ -61,7 +61,7 @@ export declare class SeedIdentity implements Identity {
61
61
  * @param seed - 64-byte seed (typically from mnemonicToSeedSync)
62
62
  * @param opts - Network selection or custom descriptor.
63
63
  */
64
- static fromSeed(seed: Uint8Array, opts: SeedIdentityOptions): SeedIdentity;
64
+ static fromSeed(seed: Uint8Array, opts?: SeedIdentityOptions): SeedIdentity;
65
65
  xOnlyPublicKey(): Promise<Uint8Array>;
66
66
  compressedPublicKey(): Promise<Uint8Array>;
67
67
  sign(tx: Transaction, inputIndexes?: number[]): Promise<Transaction>;
@@ -99,7 +99,7 @@ export declare class MnemonicIdentity extends SeedIdentity {
99
99
  * @param phrase - BIP39 mnemonic phrase (12 or 24 words)
100
100
  * @param opts - Network selection or custom descriptor, plus optional passphrase
101
101
  */
102
- static fromMnemonic(phrase: string, opts: MnemonicOptions): MnemonicIdentity;
102
+ static fromMnemonic(phrase: string, opts?: MnemonicOptions): MnemonicIdentity;
103
103
  }
104
104
  /**
105
105
  * Watch-only identity from an output descriptor.
@@ -2,7 +2,7 @@ import { Transaction } from "./utils/transaction";
2
2
  import { SingleKey, ReadonlySingleKey } from "./identity/singleKey";
3
3
  import { SeedIdentity, MnemonicIdentity, ReadonlyDescriptorIdentity } from "./identity/seedIdentity";
4
4
  import type { SeedIdentityOptions, MnemonicOptions, NetworkOptions, DescriptorOptions } from "./identity/seedIdentity";
5
- import { Identity, ReadonlyIdentity } from "./identity";
5
+ import { Identity, ReadonlyIdentity, BatchSignableIdentity, SignRequest, isBatchSignable } from "./identity";
6
6
  import { ArkAddress } from "./script/address";
7
7
  import { VHTLC } from "./script/vhtlc";
8
8
  import { DefaultVtxo } from "./script/default";
@@ -17,7 +17,8 @@ import { SignerSession, TreeNonces, TreePartialSigs } from "./tree/signingSessio
17
17
  import { Ramps } from "./wallet/ramps";
18
18
  import { isVtxoExpiringSoon, VtxoManager } from "./wallet/vtxo-manager";
19
19
  import type { IVtxoManager, SettlementConfig } from "./wallet/vtxo-manager";
20
- import { ServiceWorkerWallet, ServiceWorkerReadonlyWallet } from "./wallet/serviceWorker/wallet";
20
+ import { ServiceWorkerWallet, ServiceWorkerReadonlyWallet, DEFAULT_MESSAGE_TIMEOUTS } from "./wallet/serviceWorker/wallet";
21
+ import type { MessageTimeouts } from "./wallet/serviceWorker/wallet";
21
22
  import { OnchainWallet } from "./wallet/onchain";
22
23
  import { setupServiceWorker } from "./worker/browser/utils";
23
24
  import { ESPLORA_URL, EsploraProvider, OnchainProvider, ExplorerTransaction } from "./providers/onchain";
@@ -49,5 +50,5 @@ import { IContractManager } from "./contracts/contractManager";
49
50
  import { closeDatabase, openDatabase } from "./repositories/indexedDB/manager";
50
51
  import { WalletMessageHandler, WalletNotInitializedError, ReadonlyWalletError, DelegatorNotConfiguredError } from "./wallet/serviceWorker/wallet-message-handler";
51
52
  import { MESSAGE_BUS_NOT_INITIALIZED, 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, MESSAGE_BUS_NOT_INITIALIZED, 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, };
53
+ export { Wallet, ReadonlyWallet, SingleKey, ReadonlySingleKey, SeedIdentity, MnemonicIdentity, ReadonlyDescriptorIdentity, isBatchSignable, 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, MESSAGE_BUS_NOT_INITIALIZED, MessageBusNotInitializedError, ServiceWorkerTimeoutError, ServiceWorkerWallet, ServiceWorkerReadonlyWallet, DEFAULT_MESSAGE_TIMEOUTS, 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, };
54
+ export type { Identity, ReadonlyIdentity, BatchSignableIdentity, SignRequest, 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, MessageTimeouts, IDelegatorManager, DelegatorProvider, DelegateInfo, DelegateOptions, WalletRepository, ContractRepository, MigrationStatus, };
@@ -8,6 +8,9 @@ import type { IContractManager } from "../../contracts/contractManager";
8
8
  import type { IDelegatorManager } from "../delegator";
9
9
  import type { IVtxoManager, SettlementConfig } from "../vtxo-manager";
10
10
  import type { ContractWatcherConfig } from "../../contracts/contractWatcher";
11
+ type RequestType = WalletUpdaterRequest["type"];
12
+ export type MessageTimeouts = Partial<Record<RequestType, number>>;
13
+ export declare const DEFAULT_MESSAGE_TIMEOUTS: Readonly<Record<RequestType, number>>;
11
14
  type PrivateKeyIdentity = Identity & {
12
15
  toHex(): string;
13
16
  };
@@ -55,6 +58,7 @@ interface ServiceWorkerWalletOptions {
55
58
  messageBusTimeoutMs?: number;
56
59
  settlementConfig?: SettlementConfig | false;
57
60
  watcherConfig?: Partial<Omit<ContractWatcherConfig, "indexerProvider">>;
61
+ messageTimeouts?: MessageTimeouts;
58
62
  }
59
63
  export type ServiceWorkerWalletCreateOptions = ServiceWorkerWalletOptions & {
60
64
  serviceWorker: ServiceWorker;
@@ -90,11 +94,13 @@ export declare class ServiceWorkerReadonlyWallet implements IReadonlyWallet {
90
94
  protected initConfig: MessageBusInitConfig | null;
91
95
  protected initWalletPayload: RequestInitWallet["payload"] | null;
92
96
  protected messageBusTimeoutMs?: number;
97
+ protected messageTimeouts: Record<RequestType, number>;
93
98
  private reinitPromise;
94
99
  private pingPromise;
95
100
  private inflightRequests;
96
101
  get assetManager(): IReadonlyAssetManager;
97
102
  protected constructor(serviceWorker: ServiceWorker, identity: ReadonlyIdentity, walletRepository: WalletRepository, contractRepository: ContractRepository, messageTag: string);
103
+ private getTimeoutForRequest;
98
104
  static create(options: ServiceWorkerWalletCreateOptions): Promise<ServiceWorkerReadonlyWallet>;
99
105
  /**
100
106
  * Simplified setup method that handles service worker registration,
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@arkade-os/sdk",
3
- "version": "0.4.13",
4
- "description": "Bitcoin wallet SDK with Taproot and Ark integration",
3
+ "version": "0.4.15",
4
+ "description": "TypeScript SDK for building Bitcoin wallets using the Arkade protocol",
5
5
  "type": "module",
6
6
  "main": "./dist/cjs/index.js",
7
7
  "module": "./dist/esm/index.js",
@@ -80,7 +80,7 @@
80
80
  "registry": "https://registry.npmjs.org/"
81
81
  },
82
82
  "dependencies": {
83
- "@bitcoinerlab/descriptors-scure": "3.1.4",
83
+ "@bitcoinerlab/descriptors-scure": "3.1.7",
84
84
  "@marcbachmann/cel-js": "7.3.1",
85
85
  "@noble/curves": "2.0.1",
86
86
  "@noble/secp256k1": "3.0.0",
@@ -130,12 +130,20 @@
130
130
  }
131
131
  },
132
132
  "keywords": [
133
- "bitcoin",
134
- "wallet",
135
- "taproot",
136
133
  "ark",
134
+ "arkade",
135
+ "bip39",
136
+ "bitcoin",
137
+ "btc",
138
+ "crypto",
139
+ "lightning",
137
140
  "sdk",
138
- "arkade"
141
+ "stablecoin",
142
+ "taproot",
143
+ "typescript",
144
+ "utxo",
145
+ "vtxo",
146
+ "wallet"
139
147
  ],
140
148
  "author": "Ark Labs",
141
149
  "license": "MIT",
@@ -159,11 +167,11 @@
159
167
  "test:master": "ARK_ENV=master vitest run",
160
168
  "test:unit": "vitest run --exclude test/e2e",
161
169
  "test:setup": "node test/setup.mjs",
162
- "regtest": "pnpm test:down-docker && pnpm test:build-docker && pnpm test:up-docker && pnpm test:setup-docker",
170
+ "regtest:start": "./regtest/start-env.sh",
171
+ "regtest:stop": "./regtest/stop-env.sh",
172
+ "regtest:clean": "./regtest/clean-env.sh",
173
+ "regtest": "pnpm regtest:clean && pnpm regtest:start && pnpm test:setup-docker",
163
174
  "test:setup-docker": "node test/setup.mjs docker",
164
- "test:build-docker": "docker compose -f docker-compose.yml build --no-cache",
165
- "test:up-docker": "docker compose -f docker-compose.yml up -d",
166
- "test:down-docker": "docker compose -f docker-compose.yml down",
167
175
  "test:integration": "vitest run test/e2e/**",
168
176
  "test:integration-docker": "ARK_ENV=docker vitest run test/e2e/**",
169
177
  "test:watch": "vitest",