@arkade-os/sdk 0.4.16 → 0.4.17

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.
@@ -25,8 +25,6 @@ const DEFAULT_PAGE_SIZE = 500;
25
25
  * const manager = await ContractManager.create({
26
26
  * indexerProvider: wallet.indexerProvider,
27
27
  * contractRepository: wallet.contractRepository,
28
- * walletRepository: wallet.walletRepository,
29
- * getDefaultAddress: () => wallet.getAddress(),
30
28
  * });
31
29
  *
32
30
  * // Create a new VHTLC contract
@@ -90,7 +88,7 @@ class ContractManager {
90
88
  const contracts = await this.config.contractRepository.getContracts();
91
89
  // Delta-sync: fetch only virtual outputs that changed since the last cursor,
92
90
  // falling back to a full bootstrap for scripts seen for the first time.
93
- await this.deltaSyncContracts(contracts);
91
+ await this.deltaSyncContracts(contracts, undefined, true);
94
92
  // Reconcile the pending frontier: fetch all not-yet-finalized virtual outputs
95
93
  // to catch any that the delta window may have missed.
96
94
  if (contracts.length > 0) {
@@ -423,21 +421,39 @@ class ContractManager {
423
421
  if (contracts.length === 0) {
424
422
  return new Map();
425
423
  }
426
- return await this.fetchContractVxosFromIndexer(contracts, false, pageSize);
424
+ // Deduplicate concurrent callers against an in-flight fetch so we don't
425
+ // issue redundant round-trips. Once the fetch settles we clear the
426
+ // reference so the next call triggers a fresh fetch.
427
+ // TODO: can be removed once we fix the persistence layer (address vs scripts)
428
+ if (this.syncVtxosCallInflight) {
429
+ return this.syncVtxosCallInflight;
430
+ }
431
+ this.syncVtxosCallInflight = this.fetchContractVxosFromIndexer(contracts, true, pageSize).finally(() => {
432
+ this.syncVtxosCallInflight = undefined;
433
+ });
434
+ return this.syncVtxosCallInflight;
427
435
  }
428
436
  /**
429
437
  * Incrementally sync virtual outputs for the given contracts.
430
438
  * Uses per-script cursors to fetch only what changed since the last sync.
431
439
  * Scripts without a cursor are bootstrapped with a full fetch.
432
440
  */
433
- async deltaSyncContracts(contracts, pageSize) {
441
+ async deltaSyncContracts(contracts, pageSize, force) {
434
442
  if (contracts.length === 0)
435
443
  return new Map();
444
+ // If forced, we are treating all contracts as boostrapped and we clean the VTXO list
445
+ if (force === true) {
446
+ await Promise.all(contracts.map((contract) => this.config.walletRepository.deleteVtxos(contract.address)));
447
+ }
436
448
  const cursors = await (0, syncCursors_1.getAllSyncCursors)(this.config.walletRepository);
437
449
  // Partition into bootstrap (no cursor) and delta (has cursor) groups.
438
450
  const bootstrap = [];
439
451
  const delta = [];
440
452
  for (const c of contracts) {
453
+ if (force) {
454
+ bootstrap.push(c);
455
+ continue;
456
+ }
441
457
  if (cursors[c.script] !== undefined) {
442
458
  delta.push(c);
443
459
  }
@@ -39,6 +39,10 @@ const contractManager_1 = require("../contracts/contractManager");
39
39
  const handlers_1 = require("../contracts/handlers");
40
40
  const helpers_1 = require("../contracts/handlers/helpers");
41
41
  const syncCursors_1 = require("../utils/syncCursors");
42
+ // Hardcoded unilateral exit delay for mainnet (~7 days in seconds).
43
+ // Pinned here so that address derivation stays stable for existing mainnet
44
+ // wallets even after the server lowers the delay it advertises.
45
+ const MAINNET_UNILATERAL_EXIT_DELAY = 605184n;
42
46
  /**
43
47
  * Type guard function to check if an identity has a toReadonly method.
44
48
  */
@@ -134,10 +138,16 @@ class ReadonlyWallet {
134
138
  throw new Error("invalid exitTimelock");
135
139
  }
136
140
  }
141
+ // On mainnet, pin the unilateral exit delay to the historical value so
142
+ // that addresses derived by existing wallets remain stable even if the
143
+ // server starts advertising a shorter delay.
144
+ const unilateralExitDelay = info.network === "bitcoin"
145
+ ? MAINNET_UNILATERAL_EXIT_DELAY
146
+ : info.unilateralExitDelay;
137
147
  // create unilateral exit timelock
138
148
  const exitTimelock = config.exitTimelock ?? {
139
- value: info.unilateralExitDelay,
140
- type: info.unilateralExitDelay < 512n ? "blocks" : "seconds",
149
+ value: unilateralExitDelay,
150
+ type: unilateralExitDelay < 512n ? "blocks" : "seconds",
141
151
  };
142
152
  // validate boarding timelock passed in config if any
143
153
  if (config.boardingTimelock) {
@@ -292,14 +302,10 @@ class ReadonlyWallet {
292
302
  * @param filter - Optional flags controlling whether recoverable or unrolled VTXOs are included
293
303
  */
294
304
  async getVtxos(filter) {
295
- const { isDelta, fetchedExtended, address } = await this.syncVtxos();
296
305
  const f = filter ?? { withRecoverable: true, withUnrolled: false };
297
- // For delta syncs, read the full merged set from cache so old
298
- // Virtual outputs that weren't in the delta are still returned.
299
- const vtxos = isDelta
300
- ? await this.walletRepository.getVtxos(address)
301
- : fetchedExtended;
302
- return vtxos.filter((vtxo) => {
306
+ const contractManager = await this.getContractManager();
307
+ const contractsWithVtxos = await contractManager.getContractsWithVtxos();
308
+ return contractsWithVtxos.flatMap(({ vtxos }) => vtxos.filter((vtxo) => {
303
309
  if ((0, _1.isSpendable)(vtxo)) {
304
310
  if (!f.withRecoverable &&
305
311
  ((0, _1.isRecoverable)(vtxo) || (0, _1.isExpired)(vtxo))) {
@@ -308,7 +314,7 @@ class ReadonlyWallet {
308
314
  return true;
309
315
  }
310
316
  return !!(f.withUnrolled && vtxo.isUnrolled);
311
- });
317
+ }));
312
318
  }
313
319
  /**
314
320
  * Return wallet transaction history derived from Arkade state and boarding transactions.
@@ -778,7 +784,6 @@ class ReadonlyWallet {
778
784
  indexerProvider: this.indexerProvider,
779
785
  contractRepository: this.contractRepository,
780
786
  walletRepository: this.walletRepository,
781
- getDefaultAddress: () => this.getAddress(),
782
787
  watcherConfig: this.watcherConfig,
783
788
  });
784
789
  // Register the wallet's current address as a contract
@@ -22,8 +22,6 @@ const DEFAULT_PAGE_SIZE = 500;
22
22
  * const manager = await ContractManager.create({
23
23
  * indexerProvider: wallet.indexerProvider,
24
24
  * contractRepository: wallet.contractRepository,
25
- * walletRepository: wallet.walletRepository,
26
- * getDefaultAddress: () => wallet.getAddress(),
27
25
  * });
28
26
  *
29
27
  * // Create a new VHTLC contract
@@ -87,7 +85,7 @@ export class ContractManager {
87
85
  const contracts = await this.config.contractRepository.getContracts();
88
86
  // Delta-sync: fetch only virtual outputs that changed since the last cursor,
89
87
  // falling back to a full bootstrap for scripts seen for the first time.
90
- await this.deltaSyncContracts(contracts);
88
+ await this.deltaSyncContracts(contracts, undefined, true);
91
89
  // Reconcile the pending frontier: fetch all not-yet-finalized virtual outputs
92
90
  // to catch any that the delta window may have missed.
93
91
  if (contracts.length > 0) {
@@ -420,21 +418,39 @@ export class ContractManager {
420
418
  if (contracts.length === 0) {
421
419
  return new Map();
422
420
  }
423
- return await this.fetchContractVxosFromIndexer(contracts, false, pageSize);
421
+ // Deduplicate concurrent callers against an in-flight fetch so we don't
422
+ // issue redundant round-trips. Once the fetch settles we clear the
423
+ // reference so the next call triggers a fresh fetch.
424
+ // TODO: can be removed once we fix the persistence layer (address vs scripts)
425
+ if (this.syncVtxosCallInflight) {
426
+ return this.syncVtxosCallInflight;
427
+ }
428
+ this.syncVtxosCallInflight = this.fetchContractVxosFromIndexer(contracts, true, pageSize).finally(() => {
429
+ this.syncVtxosCallInflight = undefined;
430
+ });
431
+ return this.syncVtxosCallInflight;
424
432
  }
425
433
  /**
426
434
  * Incrementally sync virtual outputs for the given contracts.
427
435
  * Uses per-script cursors to fetch only what changed since the last sync.
428
436
  * Scripts without a cursor are bootstrapped with a full fetch.
429
437
  */
430
- async deltaSyncContracts(contracts, pageSize) {
438
+ async deltaSyncContracts(contracts, pageSize, force) {
431
439
  if (contracts.length === 0)
432
440
  return new Map();
441
+ // If forced, we are treating all contracts as boostrapped and we clean the VTXO list
442
+ if (force === true) {
443
+ await Promise.all(contracts.map((contract) => this.config.walletRepository.deleteVtxos(contract.address)));
444
+ }
433
445
  const cursors = await getAllSyncCursors(this.config.walletRepository);
434
446
  // Partition into bootstrap (no cursor) and delta (has cursor) groups.
435
447
  const bootstrap = [];
436
448
  const delta = [];
437
449
  for (const c of contracts) {
450
+ if (force) {
451
+ bootstrap.push(c);
452
+ continue;
453
+ }
438
454
  if (cursors[c.script] !== undefined) {
439
455
  delta.push(c);
440
456
  }
@@ -34,6 +34,10 @@ import { ContractManager } from '../contracts/contractManager.js';
34
34
  import { contractHandlers } from '../contracts/handlers/index.js';
35
35
  import { timelockToSequence } from '../contracts/handlers/helpers.js';
36
36
  import { advanceSyncCursors, clearSyncCursors, computeSyncWindow, cursorCutoff, getAllSyncCursors, updateWalletState, } from '../utils/syncCursors.js';
37
+ // Hardcoded unilateral exit delay for mainnet (~7 days in seconds).
38
+ // Pinned here so that address derivation stays stable for existing mainnet
39
+ // wallets even after the server lowers the delay it advertises.
40
+ const MAINNET_UNILATERAL_EXIT_DELAY = 605184n;
37
41
  /**
38
42
  * Type guard function to check if an identity has a toReadonly method.
39
43
  */
@@ -129,10 +133,16 @@ export class ReadonlyWallet {
129
133
  throw new Error("invalid exitTimelock");
130
134
  }
131
135
  }
136
+ // On mainnet, pin the unilateral exit delay to the historical value so
137
+ // that addresses derived by existing wallets remain stable even if the
138
+ // server starts advertising a shorter delay.
139
+ const unilateralExitDelay = info.network === "bitcoin"
140
+ ? MAINNET_UNILATERAL_EXIT_DELAY
141
+ : info.unilateralExitDelay;
132
142
  // create unilateral exit timelock
133
143
  const exitTimelock = config.exitTimelock ?? {
134
- value: info.unilateralExitDelay,
135
- type: info.unilateralExitDelay < 512n ? "blocks" : "seconds",
144
+ value: unilateralExitDelay,
145
+ type: unilateralExitDelay < 512n ? "blocks" : "seconds",
136
146
  };
137
147
  // validate boarding timelock passed in config if any
138
148
  if (config.boardingTimelock) {
@@ -287,14 +297,10 @@ export class ReadonlyWallet {
287
297
  * @param filter - Optional flags controlling whether recoverable or unrolled VTXOs are included
288
298
  */
289
299
  async getVtxos(filter) {
290
- const { isDelta, fetchedExtended, address } = await this.syncVtxos();
291
300
  const f = filter ?? { withRecoverable: true, withUnrolled: false };
292
- // For delta syncs, read the full merged set from cache so old
293
- // Virtual outputs that weren't in the delta are still returned.
294
- const vtxos = isDelta
295
- ? await this.walletRepository.getVtxos(address)
296
- : fetchedExtended;
297
- return vtxos.filter((vtxo) => {
301
+ const contractManager = await this.getContractManager();
302
+ const contractsWithVtxos = await contractManager.getContractsWithVtxos();
303
+ return contractsWithVtxos.flatMap(({ vtxos }) => vtxos.filter((vtxo) => {
298
304
  if (isSpendable(vtxo)) {
299
305
  if (!f.withRecoverable &&
300
306
  (isRecoverable(vtxo) || isExpired(vtxo))) {
@@ -303,7 +309,7 @@ export class ReadonlyWallet {
303
309
  return true;
304
310
  }
305
311
  return !!(f.withUnrolled && vtxo.isUnrolled);
306
- });
312
+ }));
307
313
  }
308
314
  /**
309
315
  * Return wallet transaction history derived from Arkade state and boarding transactions.
@@ -773,7 +779,6 @@ export class ReadonlyWallet {
773
779
  indexerProvider: this.indexerProvider,
774
780
  contractRepository: this.contractRepository,
775
781
  walletRepository: this.walletRepository,
776
- getDefaultAddress: () => this.getAddress(),
777
782
  watcherConfig: this.watcherConfig,
778
783
  });
779
784
  // Register the wallet's current address as a contract
@@ -118,8 +118,6 @@ export interface ContractManagerConfig {
118
118
  contractRepository: ContractRepository;
119
119
  /** The wallet repository for virtual output storage (single source of truth) */
120
120
  walletRepository: WalletRepository;
121
- /** Function to get the wallet's default Arkade address */
122
- getDefaultAddress: () => Promise<string>;
123
121
  /** Watcher configuration */
124
122
  watcherConfig?: Partial<ContractWatcherConfig>;
125
123
  }
@@ -148,8 +146,6 @@ export type CreateContractParams = Omit<Contract, "createdAt" | "state"> & {
148
146
  * const manager = await ContractManager.create({
149
147
  * indexerProvider: wallet.indexerProvider,
150
148
  * contractRepository: wallet.contractRepository,
151
- * walletRepository: wallet.walletRepository,
152
- * getDefaultAddress: () => wallet.getAddress(),
153
149
  * });
154
150
  *
155
151
  * // Create a new VHTLC contract
@@ -185,6 +181,7 @@ export declare class ContractManager implements IContractManager {
185
181
  private initialized;
186
182
  private eventCallbacks;
187
183
  private stopWatcherFn?;
184
+ private syncVtxosCallInflight?;
188
185
  private constructor();
189
186
  /**
190
187
  * Static factory method for creating a new ContractManager.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@arkade-os/sdk",
3
- "version": "0.4.16",
3
+ "version": "0.4.17",
4
4
  "description": "TypeScript SDK for building Bitcoin wallets using the Arkade protocol",
5
5
  "type": "module",
6
6
  "main": "./dist/cjs/index.js",