@arkade-os/sdk 0.4.9 → 0.4.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cjs/contracts/contractManager.js +173 -25
- package/dist/cjs/index.js +4 -3
- package/dist/cjs/providers/indexer.js +27 -4
- package/dist/cjs/utils/arkTransaction.js +17 -6
- package/dist/cjs/utils/syncCursors.js +136 -0
- package/dist/cjs/wallet/delegator.js +9 -6
- package/dist/cjs/wallet/serviceWorker/wallet-message-handler.js +63 -33
- package/dist/cjs/wallet/serviceWorker/wallet.js +5 -3
- package/dist/cjs/wallet/vtxo-manager.js +30 -9
- package/dist/cjs/wallet/wallet.js +180 -38
- package/dist/cjs/worker/errors.js +3 -4
- package/dist/esm/contracts/contractManager.js +173 -25
- package/dist/esm/index.js +2 -2
- package/dist/esm/providers/indexer.js +27 -4
- package/dist/esm/utils/arkTransaction.js +17 -6
- package/dist/esm/utils/syncCursors.js +125 -0
- package/dist/esm/wallet/delegator.js +9 -6
- package/dist/esm/wallet/serviceWorker/wallet-message-handler.js +63 -33
- package/dist/esm/wallet/serviceWorker/wallet.js +6 -4
- package/dist/esm/wallet/vtxo-manager.js +30 -9
- package/dist/esm/wallet/wallet.js +180 -38
- package/dist/esm/worker/errors.js +2 -3
- package/dist/types/contracts/contractManager.d.ts +28 -7
- package/dist/types/contracts/index.d.ts +1 -1
- package/dist/types/index.d.ts +2 -2
- package/dist/types/providers/indexer.d.ts +16 -14
- package/dist/types/utils/syncCursors.d.ts +58 -0
- package/dist/types/wallet/serviceWorker/wallet-message-handler.d.ts +14 -2
- package/dist/types/wallet/vtxo-manager.d.ts +3 -2
- package/dist/types/wallet/wallet.d.ts +14 -0
- package/dist/types/worker/errors.d.ts +1 -0
- package/package.json +2 -2
|
@@ -5,6 +5,8 @@ const base_1 = require("@scure/base");
|
|
|
5
5
|
const contractWatcher_1 = require("./contractWatcher");
|
|
6
6
|
const handlers_1 = require("./handlers");
|
|
7
7
|
const utils_1 = require("../wallet/utils");
|
|
8
|
+
const syncCursors_1 = require("../utils/syncCursors");
|
|
9
|
+
const DEFAULT_PAGE_SIZE = 500;
|
|
8
10
|
/**
|
|
9
11
|
* Central manager for contract lifecycle and operations.
|
|
10
12
|
*
|
|
@@ -74,10 +76,14 @@ class ContractManager {
|
|
|
74
76
|
}
|
|
75
77
|
// Load persisted contracts
|
|
76
78
|
const contracts = await this.config.contractRepository.getContracts();
|
|
77
|
-
// fetch
|
|
78
|
-
//
|
|
79
|
-
|
|
80
|
-
|
|
79
|
+
// Delta-sync: fetch only VTXOs that changed since the last cursor,
|
|
80
|
+
// falling back to a full bootstrap for scripts seen for the first time.
|
|
81
|
+
await this.deltaSyncContracts(contracts);
|
|
82
|
+
// Reconcile the pending frontier: fetch all not-yet-finalized VTXOs
|
|
83
|
+
// to catch any that the delta window may have missed.
|
|
84
|
+
if (contracts.length > 0) {
|
|
85
|
+
await this.reconcilePendingFrontier(contracts);
|
|
86
|
+
}
|
|
81
87
|
// add all contracts to the watcher
|
|
82
88
|
const now = Date.now();
|
|
83
89
|
for (const contract of contracts) {
|
|
@@ -94,7 +100,9 @@ class ContractManager {
|
|
|
94
100
|
this.initialized = true;
|
|
95
101
|
// Start watching automatically
|
|
96
102
|
this.stopWatcherFn = await this.watcher.startWatching((event) => {
|
|
97
|
-
this.handleContractEvent(event)
|
|
103
|
+
this.handleContractEvent(event).catch((error) => {
|
|
104
|
+
console.error("Error handling contract event:", error);
|
|
105
|
+
});
|
|
98
106
|
});
|
|
99
107
|
}
|
|
100
108
|
/**
|
|
@@ -165,9 +173,9 @@ class ContractManager {
|
|
|
165
173
|
const dbFilter = this.buildContractsDbFilter(filter ?? {});
|
|
166
174
|
return await this.config.contractRepository.getContracts(dbFilter);
|
|
167
175
|
}
|
|
168
|
-
async getContractsWithVtxos(filter) {
|
|
176
|
+
async getContractsWithVtxos(filter, pageSize) {
|
|
169
177
|
const contracts = await this.getContracts(filter);
|
|
170
|
-
const vtxos = await this.getVtxosForContracts(contracts);
|
|
178
|
+
const vtxos = await this.getVtxosForContracts(contracts, pageSize);
|
|
171
179
|
return contracts.map((contract) => ({
|
|
172
180
|
contract,
|
|
173
181
|
vtxos: vtxos.get(contract.script) ?? [],
|
|
@@ -307,12 +315,43 @@ class ContractManager {
|
|
|
307
315
|
};
|
|
308
316
|
}
|
|
309
317
|
/**
|
|
310
|
-
* Force a
|
|
311
|
-
*
|
|
318
|
+
* Force a VTXO refresh from the indexer.
|
|
319
|
+
*
|
|
320
|
+
* Without options, clears all sync cursors and re-fetches every contract.
|
|
321
|
+
* With options, narrows the refresh to specific scripts and/or a time window.
|
|
312
322
|
*/
|
|
313
|
-
async refreshVtxos() {
|
|
314
|
-
|
|
315
|
-
|
|
323
|
+
async refreshVtxos(opts) {
|
|
324
|
+
let contracts = await this.config.contractRepository.getContracts();
|
|
325
|
+
if (opts?.scripts && opts.scripts.length > 0) {
|
|
326
|
+
const scriptSet = new Set(opts.scripts);
|
|
327
|
+
contracts = contracts.filter((c) => scriptSet.has(c.script));
|
|
328
|
+
}
|
|
329
|
+
const syncWindow = opts?.after !== undefined || opts?.before !== undefined
|
|
330
|
+
? {
|
|
331
|
+
after: opts.after ?? 0,
|
|
332
|
+
before: opts.before ?? Date.now(),
|
|
333
|
+
}
|
|
334
|
+
: undefined;
|
|
335
|
+
if (!syncWindow) {
|
|
336
|
+
// Full refresh — clear cursors so the next delta sync re-bootstraps.
|
|
337
|
+
if (opts?.scripts && opts.scripts.length > 0) {
|
|
338
|
+
await (0, syncCursors_1.clearSyncCursors)(this.config.walletRepository, opts.scripts);
|
|
339
|
+
}
|
|
340
|
+
else {
|
|
341
|
+
await (0, syncCursors_1.clearSyncCursors)(this.config.walletRepository);
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
const requestStartedAt = Date.now();
|
|
345
|
+
const fetched = await this.fetchContractVxosFromIndexer(contracts, true, undefined, syncWindow);
|
|
346
|
+
// Persist cursors so subsequent incremental syncs don't re-bootstrap.
|
|
347
|
+
const cutoff = (0, syncCursors_1.cursorCutoff)(requestStartedAt);
|
|
348
|
+
const cursorUpdates = {};
|
|
349
|
+
for (const script of fetched.keys()) {
|
|
350
|
+
cursorUpdates[script] = cutoff;
|
|
351
|
+
}
|
|
352
|
+
if (Object.keys(cursorUpdates).length > 0) {
|
|
353
|
+
await (0, syncCursors_1.advanceSyncCursors)(this.config.walletRepository, cursorUpdates);
|
|
354
|
+
}
|
|
316
355
|
}
|
|
317
356
|
/**
|
|
318
357
|
* Check if currently watching.
|
|
@@ -338,14 +377,13 @@ class ContractManager {
|
|
|
338
377
|
*/
|
|
339
378
|
async handleContractEvent(event) {
|
|
340
379
|
switch (event.type) {
|
|
341
|
-
//
|
|
380
|
+
// Delta-sync only the changed VTXOs for this contract.
|
|
342
381
|
case "vtxo_received":
|
|
343
382
|
case "vtxo_spent":
|
|
344
|
-
await this.
|
|
383
|
+
await this.deltaSyncContracts([event.contract]);
|
|
345
384
|
break;
|
|
346
385
|
case "connection_reset": {
|
|
347
|
-
//
|
|
348
|
-
// contracts so the repo stays consistent with bootstrap state
|
|
386
|
+
// After a reconnect we don't know what we missed — full refetch.
|
|
349
387
|
const activeWatchedContracts = this.watcher.getActiveContracts();
|
|
350
388
|
await this.fetchContractVxosFromIndexer(activeWatchedContracts, true);
|
|
351
389
|
break;
|
|
@@ -357,14 +395,100 @@ class ContractManager {
|
|
|
357
395
|
// Forward to all callbacks
|
|
358
396
|
this.emitEvent(event);
|
|
359
397
|
}
|
|
360
|
-
async getVtxosForContracts(contracts) {
|
|
398
|
+
async getVtxosForContracts(contracts, pageSize) {
|
|
361
399
|
if (contracts.length === 0) {
|
|
362
400
|
return new Map();
|
|
363
401
|
}
|
|
364
|
-
return await this.fetchContractVxosFromIndexer(contracts, false);
|
|
402
|
+
return await this.fetchContractVxosFromIndexer(contracts, false, pageSize);
|
|
365
403
|
}
|
|
366
|
-
|
|
367
|
-
|
|
404
|
+
/**
|
|
405
|
+
* Incrementally sync VTXOs for the given contracts.
|
|
406
|
+
* Uses per-script cursors to fetch only what changed since the last sync.
|
|
407
|
+
* Scripts without a cursor are bootstrapped with a full fetch.
|
|
408
|
+
*/
|
|
409
|
+
async deltaSyncContracts(contracts, pageSize) {
|
|
410
|
+
if (contracts.length === 0)
|
|
411
|
+
return new Map();
|
|
412
|
+
const cursors = await (0, syncCursors_1.getAllSyncCursors)(this.config.walletRepository);
|
|
413
|
+
// Partition into bootstrap (no cursor) and delta (has cursor) groups.
|
|
414
|
+
const bootstrap = [];
|
|
415
|
+
const delta = [];
|
|
416
|
+
for (const c of contracts) {
|
|
417
|
+
if (cursors[c.script] !== undefined) {
|
|
418
|
+
delta.push(c);
|
|
419
|
+
}
|
|
420
|
+
else {
|
|
421
|
+
bootstrap.push(c);
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
const result = new Map();
|
|
425
|
+
const cursorUpdates = {};
|
|
426
|
+
// Full bootstrap for new scripts.
|
|
427
|
+
if (bootstrap.length > 0) {
|
|
428
|
+
const requestStartedAt = Date.now();
|
|
429
|
+
const fetched = await this.fetchContractVxosFromIndexer(bootstrap, true);
|
|
430
|
+
const cutoff = (0, syncCursors_1.cursorCutoff)(requestStartedAt);
|
|
431
|
+
for (const [script, vtxos] of fetched) {
|
|
432
|
+
result.set(script, vtxos);
|
|
433
|
+
cursorUpdates[script] = cutoff;
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
// Delta sync for scripts with an existing cursor.
|
|
437
|
+
if (delta.length > 0) {
|
|
438
|
+
// Use the oldest cursor so the shared window covers every script.
|
|
439
|
+
const minCursor = Math.min(...delta.map((c) => cursors[c.script]));
|
|
440
|
+
const window = (0, syncCursors_1.computeSyncWindow)(minCursor);
|
|
441
|
+
if (window) {
|
|
442
|
+
const requestStartedAt = Date.now();
|
|
443
|
+
const fetched = await this.fetchContractVxosFromIndexer(delta, true, pageSize, window);
|
|
444
|
+
const cutoff = (0, syncCursors_1.cursorCutoff)(requestStartedAt);
|
|
445
|
+
for (const [script, vtxos] of fetched) {
|
|
446
|
+
result.set(script, vtxos);
|
|
447
|
+
cursorUpdates[script] = cutoff;
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
if (Object.keys(cursorUpdates).length > 0) {
|
|
452
|
+
await (0, syncCursors_1.advanceSyncCursors)(this.config.walletRepository, cursorUpdates);
|
|
453
|
+
}
|
|
454
|
+
return result;
|
|
455
|
+
}
|
|
456
|
+
/**
|
|
457
|
+
* Fetch all pending (not-yet-finalized) VTXOs and upsert them into the
|
|
458
|
+
* repository. This catches VTXOs whose state changed outside the delta
|
|
459
|
+
* window (e.g. a spend that hasn't settled yet).
|
|
460
|
+
*/
|
|
461
|
+
async reconcilePendingFrontier(contracts) {
|
|
462
|
+
const scripts = contracts.map((c) => c.script);
|
|
463
|
+
const scriptToContract = new Map(contracts.map((c) => [c.script, c]));
|
|
464
|
+
const { vtxos } = await this.config.indexerProvider.getVtxos({
|
|
465
|
+
scripts,
|
|
466
|
+
pendingOnly: true,
|
|
467
|
+
});
|
|
468
|
+
// Group by contract and upsert.
|
|
469
|
+
const byContract = new Map();
|
|
470
|
+
for (const vtxo of vtxos) {
|
|
471
|
+
if (!vtxo.script)
|
|
472
|
+
continue;
|
|
473
|
+
const contract = scriptToContract.get(vtxo.script);
|
|
474
|
+
if (!contract)
|
|
475
|
+
continue;
|
|
476
|
+
let arr = byContract.get(contract.address);
|
|
477
|
+
if (!arr) {
|
|
478
|
+
arr = [];
|
|
479
|
+
byContract.set(contract.address, arr);
|
|
480
|
+
}
|
|
481
|
+
arr.push({
|
|
482
|
+
...(0, utils_1.extendVtxoFromContract)(vtxo, contract),
|
|
483
|
+
contractScript: contract.script,
|
|
484
|
+
});
|
|
485
|
+
}
|
|
486
|
+
for (const [addr, contractVtxos] of byContract) {
|
|
487
|
+
await this.config.walletRepository.saveVtxos(addr, contractVtxos);
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
async fetchContractVxosFromIndexer(contracts, includeSpent, pageSize, syncWindow) {
|
|
491
|
+
const fetched = await this.fetchContractVtxosBulk(contracts, includeSpent, pageSize, syncWindow);
|
|
368
492
|
const result = new Map();
|
|
369
493
|
for (const [contractScript, vtxos] of fetched) {
|
|
370
494
|
result.set(contractScript, vtxos);
|
|
@@ -375,14 +499,14 @@ class ContractManager {
|
|
|
375
499
|
}
|
|
376
500
|
return result;
|
|
377
501
|
}
|
|
378
|
-
async fetchContractVtxosBulk(contracts, includeSpent) {
|
|
502
|
+
async fetchContractVtxosBulk(contracts, includeSpent, pageSize = DEFAULT_PAGE_SIZE, syncWindow) {
|
|
379
503
|
if (contracts.length === 0) {
|
|
380
504
|
return new Map();
|
|
381
505
|
}
|
|
382
506
|
// For a single contract, use the paginated path directly.
|
|
383
507
|
if (contracts.length === 1) {
|
|
384
508
|
const contract = contracts[0];
|
|
385
|
-
const vtxos = await this.fetchContractVtxosPaginated(contract, includeSpent);
|
|
509
|
+
const vtxos = await this.fetchContractVtxosPaginated(contract, includeSpent, pageSize, syncWindow);
|
|
386
510
|
return new Map([[contract.script, vtxos]]);
|
|
387
511
|
}
|
|
388
512
|
// For multiple contracts, batch all scripts into a single indexer call
|
|
@@ -391,14 +515,24 @@ class ContractManager {
|
|
|
391
515
|
const scriptToContract = new Map(contracts.map((c) => [c.script, c]));
|
|
392
516
|
const result = new Map(contracts.map((c) => [c.script, []]));
|
|
393
517
|
const scripts = contracts.map((c) => c.script);
|
|
394
|
-
const pageSize = 100;
|
|
395
518
|
const opts = includeSpent ? {} : { spendableOnly: true };
|
|
519
|
+
const windowOpts = syncWindow
|
|
520
|
+
? {
|
|
521
|
+
...(syncWindow.after !== undefined && {
|
|
522
|
+
after: syncWindow.after,
|
|
523
|
+
}),
|
|
524
|
+
...(syncWindow.before !== undefined && {
|
|
525
|
+
before: syncWindow.before,
|
|
526
|
+
}),
|
|
527
|
+
}
|
|
528
|
+
: {};
|
|
396
529
|
let pageIndex = 0;
|
|
397
530
|
let hasMore = true;
|
|
398
531
|
while (hasMore) {
|
|
399
532
|
const { vtxos, page } = await this.config.indexerProvider.getVtxos({
|
|
400
533
|
scripts,
|
|
401
534
|
...opts,
|
|
535
|
+
...windowOpts,
|
|
402
536
|
pageIndex,
|
|
403
537
|
pageSize,
|
|
404
538
|
});
|
|
@@ -417,19 +551,31 @@ class ContractManager {
|
|
|
417
551
|
}
|
|
418
552
|
hasMore = page ? vtxos.length === pageSize : false;
|
|
419
553
|
pageIndex++;
|
|
554
|
+
if (hasMore)
|
|
555
|
+
await new Promise((r) => setTimeout(r, 500));
|
|
420
556
|
}
|
|
421
557
|
return result;
|
|
422
558
|
}
|
|
423
|
-
async fetchContractVtxosPaginated(contract, includeSpent) {
|
|
424
|
-
const pageSize = 100;
|
|
559
|
+
async fetchContractVtxosPaginated(contract, includeSpent, pageSize = DEFAULT_PAGE_SIZE, syncWindow) {
|
|
425
560
|
const allVtxos = [];
|
|
426
561
|
let pageIndex = 0;
|
|
427
562
|
let hasMore = true;
|
|
428
563
|
const opts = includeSpent ? {} : { spendableOnly: true };
|
|
564
|
+
const windowOpts = syncWindow
|
|
565
|
+
? {
|
|
566
|
+
...(syncWindow.after !== undefined && {
|
|
567
|
+
after: syncWindow.after,
|
|
568
|
+
}),
|
|
569
|
+
...(syncWindow.before !== undefined && {
|
|
570
|
+
before: syncWindow.before,
|
|
571
|
+
}),
|
|
572
|
+
}
|
|
573
|
+
: {};
|
|
429
574
|
while (hasMore) {
|
|
430
575
|
const { vtxos, page } = await this.config.indexerProvider.getVtxos({
|
|
431
576
|
scripts: [contract.script],
|
|
432
577
|
...opts,
|
|
578
|
+
...windowOpts,
|
|
433
579
|
pageIndex,
|
|
434
580
|
pageSize,
|
|
435
581
|
});
|
|
@@ -441,6 +587,8 @@ class ContractManager {
|
|
|
441
587
|
}
|
|
442
588
|
hasMore = page ? vtxos.length === pageSize : false;
|
|
443
589
|
pageIndex++;
|
|
590
|
+
if (hasMore)
|
|
591
|
+
await new Promise((r) => setTimeout(r, 500));
|
|
444
592
|
}
|
|
445
593
|
return allVtxos;
|
|
446
594
|
}
|
package/dist/cjs/index.js
CHANGED
|
@@ -36,9 +36,9 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
36
36
|
};
|
|
37
37
|
})();
|
|
38
38
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
39
|
-
exports.
|
|
40
|
-
exports.
|
|
41
|
-
exports.isArkContract = void 0;
|
|
39
|
+
exports.VtxoTreeExpiry = exports.CosignerPublicKey = exports.getArkPsbtFields = exports.setArkPsbtField = exports.ArkPsbtFieldKeyType = exports.ArkPsbtFieldKey = exports.TapTreeCoder = exports.CLTVMultisigTapscript = exports.ConditionMultisigTapscript = exports.ConditionCSVMultisigTapscript = exports.CSVMultisigTapscript = exports.MultisigTapscript = exports.decodeTapscript = exports.ServiceWorkerReadonlyWallet = exports.ServiceWorkerWallet = exports.ServiceWorkerTimeoutError = exports.MessageBusNotInitializedError = exports.MESSAGE_BUS_NOT_INITIALIZED = exports.DelegatorNotConfiguredError = exports.ReadonlyWalletError = exports.WalletNotInitializedError = exports.WalletMessageHandler = exports.MessageBus = exports.setupServiceWorker = exports.SettlementEventType = exports.ChainTxType = exports.IndexerTxType = exports.TxType = exports.VHTLC = exports.VtxoScript = exports.DelegateVtxo = exports.DefaultVtxo = exports.ArkAddress = exports.RestIndexerProvider = exports.RestArkProvider = exports.EsploraProvider = exports.ESPLORA_URL = exports.RestDelegatorProvider = exports.DelegatorManagerImpl = exports.VtxoManager = exports.Ramps = exports.OnchainWallet = exports.ReadonlyDescriptorIdentity = exports.MnemonicIdentity = exports.SeedIdentity = exports.ReadonlySingleKey = exports.SingleKey = exports.ReadonlyWallet = exports.Wallet = exports.asset = void 0;
|
|
40
|
+
exports.contractFromArkContract = exports.decodeArkContract = exports.encodeArkContract = exports.VHTLCContractHandler = exports.DelegateContractHandler = exports.DefaultContractHandler = exports.contractHandlers = exports.ContractWatcher = exports.ContractManager = exports.getSequence = exports.isExpired = exports.isSubdust = exports.isSpendable = exports.isRecoverable = exports.buildForfeitTx = exports.validateConnectorsTxGraph = exports.validateVtxoTxGraph = exports.Batch = exports.maybeArkError = exports.ArkError = exports.Transaction = exports.Unroll = exports.P2A = exports.TxTree = exports.BIP322 = exports.Intent = exports.ContractRepositoryImpl = exports.WalletRepositoryImpl = exports.rollbackMigration = exports.getMigrationStatus = exports.requiresMigration = exports.migrateWalletRepository = exports.MIGRATION_KEY = exports.InMemoryContractRepository = exports.InMemoryWalletRepository = exports.IndexedDBContractRepository = exports.IndexedDBWalletRepository = exports.openDatabase = exports.closeDatabase = exports.networks = exports.ArkNote = exports.isValidArkAddress = exports.isVtxoExpiringSoon = exports.combineTapscriptSigs = exports.hasBoardingTxExpired = exports.waitForIncomingFunds = exports.verifyTapscriptSignatures = exports.buildOffchainTx = exports.ConditionWitness = exports.VtxoTaprootTree = void 0;
|
|
41
|
+
exports.isArkContract = exports.contractFromArkContractWithAddress = void 0;
|
|
42
42
|
const transaction_1 = require("./utils/transaction");
|
|
43
43
|
Object.defineProperty(exports, "Transaction", { enumerable: true, get: function () { return transaction_1.Transaction; } });
|
|
44
44
|
const singleKey_1 = require("./identity/singleKey");
|
|
@@ -180,5 +180,6 @@ Object.defineProperty(exports, "WalletNotInitializedError", { enumerable: true,
|
|
|
180
180
|
Object.defineProperty(exports, "ReadonlyWalletError", { enumerable: true, get: function () { return wallet_message_handler_1.ReadonlyWalletError; } });
|
|
181
181
|
Object.defineProperty(exports, "DelegatorNotConfiguredError", { enumerable: true, get: function () { return wallet_message_handler_1.DelegatorNotConfiguredError; } });
|
|
182
182
|
const errors_2 = require("./worker/errors");
|
|
183
|
+
Object.defineProperty(exports, "MESSAGE_BUS_NOT_INITIALIZED", { enumerable: true, get: function () { return errors_2.MESSAGE_BUS_NOT_INITIALIZED; } });
|
|
183
184
|
Object.defineProperty(exports, "MessageBusNotInitializedError", { enumerable: true, get: function () { return errors_2.MessageBusNotInitializedError; } });
|
|
184
185
|
Object.defineProperty(exports, "ServiceWorkerTimeoutError", { enumerable: true, get: function () { return errors_2.ServiceWorkerTimeoutError; } });
|
|
@@ -254,23 +254,40 @@ class RestIndexerProvider {
|
|
|
254
254
|
return data;
|
|
255
255
|
}
|
|
256
256
|
async getVtxos(opts) {
|
|
257
|
+
const hasScripts = (opts?.scripts?.length ?? 0) > 0;
|
|
258
|
+
const hasOutpoints = (opts?.outpoints?.length ?? 0) > 0;
|
|
257
259
|
// scripts and outpoints are mutually exclusive
|
|
258
|
-
if (
|
|
260
|
+
if (hasScripts && hasOutpoints) {
|
|
259
261
|
throw new Error("scripts and outpoints are mutually exclusive options");
|
|
260
262
|
}
|
|
261
|
-
if (!
|
|
263
|
+
if (!hasScripts && !hasOutpoints) {
|
|
262
264
|
throw new Error("Either scripts or outpoints must be provided");
|
|
263
265
|
}
|
|
266
|
+
const filterCount = [
|
|
267
|
+
opts?.spendableOnly,
|
|
268
|
+
opts?.spentOnly,
|
|
269
|
+
opts?.recoverableOnly,
|
|
270
|
+
].filter(Boolean).length;
|
|
271
|
+
if (filterCount > 1) {
|
|
272
|
+
throw new Error("spendableOnly, spentOnly, and recoverableOnly are mutually exclusive options");
|
|
273
|
+
}
|
|
274
|
+
if (opts?.after !== undefined &&
|
|
275
|
+
opts?.before !== undefined &&
|
|
276
|
+
opts.after !== 0 &&
|
|
277
|
+
opts.before !== 0 &&
|
|
278
|
+
opts.before <= opts.after) {
|
|
279
|
+
throw new Error("before must be greater than after");
|
|
280
|
+
}
|
|
264
281
|
let url = `${this.serverUrl}/v1/indexer/vtxos`;
|
|
265
282
|
const params = new URLSearchParams();
|
|
266
283
|
// Handle scripts with multi collection format
|
|
267
|
-
if (
|
|
284
|
+
if (hasScripts) {
|
|
268
285
|
opts.scripts.forEach((script) => {
|
|
269
286
|
params.append("scripts", script);
|
|
270
287
|
});
|
|
271
288
|
}
|
|
272
289
|
// Handle outpoints with multi collection format
|
|
273
|
-
if (
|
|
290
|
+
if (hasOutpoints) {
|
|
274
291
|
opts.outpoints.forEach((outpoint) => {
|
|
275
292
|
params.append("outpoints", `${outpoint.txid}:${outpoint.vout}`);
|
|
276
293
|
});
|
|
@@ -282,6 +299,12 @@ class RestIndexerProvider {
|
|
|
282
299
|
params.append("spentOnly", opts.spentOnly.toString());
|
|
283
300
|
if (opts.recoverableOnly !== undefined)
|
|
284
301
|
params.append("recoverableOnly", opts.recoverableOnly.toString());
|
|
302
|
+
if (opts.pendingOnly !== undefined)
|
|
303
|
+
params.append("pendingOnly", opts.pendingOnly.toString());
|
|
304
|
+
if (opts.after !== undefined)
|
|
305
|
+
params.append("after", opts.after.toString());
|
|
306
|
+
if (opts.before !== undefined)
|
|
307
|
+
params.append("before", opts.before.toString());
|
|
285
308
|
if (opts.pageIndex !== undefined)
|
|
286
309
|
params.append("page.index", opts.pageIndex.toString());
|
|
287
310
|
if (opts.pageSize !== undefined)
|
|
@@ -15,6 +15,7 @@ const anchor_1 = require("./anchor");
|
|
|
15
15
|
const unknownFields_1 = require("./unknownFields");
|
|
16
16
|
const transaction_1 = require("./transaction");
|
|
17
17
|
const address_1 = require("../script/address");
|
|
18
|
+
const extension_1 = require("../extension");
|
|
18
19
|
/**
|
|
19
20
|
* Builds an offchain transaction with checkpoint transactions.
|
|
20
21
|
*
|
|
@@ -28,16 +29,26 @@ const address_1 = require("../script/address");
|
|
|
28
29
|
* @returns Object containing the virtual transaction and checkpoint transactions
|
|
29
30
|
*/
|
|
30
31
|
function buildOffchainTx(inputs, outputs, serverUnrollScript) {
|
|
31
|
-
|
|
32
|
+
// TODO: use arkd /info
|
|
33
|
+
const MAX_OP_RETURN = 2;
|
|
34
|
+
let countOpReturn = 0;
|
|
35
|
+
let hasExtensionOutput = false;
|
|
32
36
|
for (const [index, output] of outputs.entries()) {
|
|
33
37
|
if (!output.script)
|
|
34
38
|
throw new Error(`missing output script ${index}`);
|
|
35
|
-
const
|
|
36
|
-
|
|
39
|
+
const isExtension = extension_1.Extension.isExtension(output.script);
|
|
40
|
+
const isOpReturn = isExtension || btc_signer_1.Script.decode(output.script)[0] === "RETURN";
|
|
41
|
+
if (isOpReturn) {
|
|
42
|
+
countOpReturn++;
|
|
43
|
+
}
|
|
44
|
+
if (!isExtension)
|
|
37
45
|
continue;
|
|
38
|
-
if (
|
|
39
|
-
throw new Error("multiple
|
|
40
|
-
|
|
46
|
+
if (hasExtensionOutput)
|
|
47
|
+
throw new Error("multiple extension outputs");
|
|
48
|
+
hasExtensionOutput = true;
|
|
49
|
+
}
|
|
50
|
+
if (countOpReturn > MAX_OP_RETURN) {
|
|
51
|
+
throw new Error(`too many OP_RETURN outputs: ${countOpReturn} > ${MAX_OP_RETURN}`);
|
|
41
52
|
}
|
|
42
53
|
const checkpoints = inputs.map((input) => buildCheckpointTx(input, serverUnrollScript));
|
|
43
54
|
const arkTx = buildVirtualTx(checkpoints.map((c) => c.input), outputs);
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.OVERLAP_MS = exports.SAFETY_LAG_MS = void 0;
|
|
4
|
+
exports.updateWalletState = updateWalletState;
|
|
5
|
+
exports.getSyncCursor = getSyncCursor;
|
|
6
|
+
exports.getAllSyncCursors = getAllSyncCursors;
|
|
7
|
+
exports.advanceSyncCursor = advanceSyncCursor;
|
|
8
|
+
exports.advanceSyncCursors = advanceSyncCursors;
|
|
9
|
+
exports.clearSyncCursors = clearSyncCursors;
|
|
10
|
+
exports.computeSyncWindow = computeSyncWindow;
|
|
11
|
+
exports.cursorCutoff = cursorCutoff;
|
|
12
|
+
/** Lag behind real-time to avoid racing with indexer writes. */
|
|
13
|
+
exports.SAFETY_LAG_MS = 30000;
|
|
14
|
+
/** Overlap window so boundary VTXOs are never missed. */
|
|
15
|
+
exports.OVERLAP_MS = 60000;
|
|
16
|
+
/**
|
|
17
|
+
* Per-repository mutex that serializes wallet-state mutations so that
|
|
18
|
+
* concurrent read-modify-write cycles (e.g. advanceSyncCursors racing
|
|
19
|
+
* with clearSyncCursors or setPendingTxFlag) never silently overwrite
|
|
20
|
+
* each other's changes.
|
|
21
|
+
*/
|
|
22
|
+
const walletStateLocks = new WeakMap();
|
|
23
|
+
/**
|
|
24
|
+
* Atomically read, mutate, and persist wallet state.
|
|
25
|
+
* All callers that modify wallet state should go through this helper
|
|
26
|
+
* to avoid lost-update races between interleaved async operations.
|
|
27
|
+
*/
|
|
28
|
+
async function updateWalletState(repo, updater) {
|
|
29
|
+
const prev = walletStateLocks.get(repo) ?? Promise.resolve();
|
|
30
|
+
const op = prev.then(async () => {
|
|
31
|
+
const state = (await repo.getWalletState()) ?? {};
|
|
32
|
+
await repo.saveWalletState(updater(state));
|
|
33
|
+
});
|
|
34
|
+
// Store a version that never rejects so the chain doesn't break.
|
|
35
|
+
walletStateLocks.set(repo, op.catch(() => { }));
|
|
36
|
+
return op;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Read the high-water mark for a single script.
|
|
40
|
+
* Returns `undefined` when the script has never been synced (bootstrap case).
|
|
41
|
+
*/
|
|
42
|
+
async function getSyncCursor(repo, script) {
|
|
43
|
+
const state = await repo.getWalletState();
|
|
44
|
+
return state?.settings?.vtxoSyncCursors?.[script];
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Read cursors for every previously-synced script.
|
|
48
|
+
*/
|
|
49
|
+
async function getAllSyncCursors(repo) {
|
|
50
|
+
const state = await repo.getWalletState();
|
|
51
|
+
return state?.settings?.vtxoSyncCursors ?? {};
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Advance the cursor for one script after a successful delta sync.
|
|
55
|
+
* `cursor` should be the `before` cutoff used in the request.
|
|
56
|
+
*/
|
|
57
|
+
async function advanceSyncCursor(repo, script, cursor) {
|
|
58
|
+
await updateWalletState(repo, (state) => {
|
|
59
|
+
const existing = state.settings?.vtxoSyncCursors ?? {};
|
|
60
|
+
return {
|
|
61
|
+
...state,
|
|
62
|
+
settings: {
|
|
63
|
+
...state.settings,
|
|
64
|
+
vtxoSyncCursors: { ...existing, [script]: cursor },
|
|
65
|
+
},
|
|
66
|
+
};
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Advance cursors for multiple scripts in a single write.
|
|
71
|
+
*/
|
|
72
|
+
async function advanceSyncCursors(repo, updates) {
|
|
73
|
+
await updateWalletState(repo, (state) => {
|
|
74
|
+
const existing = state.settings?.vtxoSyncCursors ?? {};
|
|
75
|
+
return {
|
|
76
|
+
...state,
|
|
77
|
+
settings: {
|
|
78
|
+
...state.settings,
|
|
79
|
+
vtxoSyncCursors: { ...existing, ...updates },
|
|
80
|
+
},
|
|
81
|
+
};
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Remove sync cursors, forcing a full re-bootstrap on next sync.
|
|
86
|
+
* When `scripts` is provided, only those cursors are cleared.
|
|
87
|
+
*/
|
|
88
|
+
async function clearSyncCursors(repo, scripts) {
|
|
89
|
+
await updateWalletState(repo, (state) => {
|
|
90
|
+
if (!scripts) {
|
|
91
|
+
const { vtxoSyncCursors: _, ...restSettings } = state.settings ?? {};
|
|
92
|
+
return {
|
|
93
|
+
...state,
|
|
94
|
+
settings: restSettings,
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
const existing = state.settings?.vtxoSyncCursors ?? {};
|
|
98
|
+
const filtered = { ...existing };
|
|
99
|
+
for (const s of scripts)
|
|
100
|
+
delete filtered[s];
|
|
101
|
+
return {
|
|
102
|
+
...state,
|
|
103
|
+
settings: {
|
|
104
|
+
...state.settings,
|
|
105
|
+
vtxoSyncCursors: filtered,
|
|
106
|
+
},
|
|
107
|
+
};
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* Compute the `after` lower-bound for a delta sync query.
|
|
112
|
+
* Returns `undefined` when the script has no cursor (bootstrap needed).
|
|
113
|
+
*
|
|
114
|
+
* No upper bound (`before`) is applied to the query so that freshly
|
|
115
|
+
* created VTXOs are never excluded. The safety lag is applied only
|
|
116
|
+
* when advancing the cursor (see {@link cursorCutoff}).
|
|
117
|
+
*/
|
|
118
|
+
function computeSyncWindow(cursor) {
|
|
119
|
+
if (cursor === undefined)
|
|
120
|
+
return undefined;
|
|
121
|
+
const after = Math.max(0, cursor - exports.OVERLAP_MS);
|
|
122
|
+
return { after };
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* The safe high-water mark for cursor advancement.
|
|
126
|
+
* Lags behind real-time by {@link SAFETY_LAG_MS} so that VTXOs still
|
|
127
|
+
* being indexed are re-queried on the next sync.
|
|
128
|
+
*
|
|
129
|
+
* When `requestStartedAt` is provided the cutoff is frozen to the
|
|
130
|
+
* request start rather than wall-clock at commit time, preventing
|
|
131
|
+
* long-running paginated fetches from advancing the cursor past the
|
|
132
|
+
* data they actually observed.
|
|
133
|
+
*/
|
|
134
|
+
function cursorCutoff(requestStartedAt) {
|
|
135
|
+
return (requestStartedAt ?? Date.now()) - exports.SAFETY_LAG_MS;
|
|
136
|
+
}
|
|
@@ -25,10 +25,13 @@ class DelegatorManagerImpl {
|
|
|
25
25
|
return { delegated: [], failed: [] };
|
|
26
26
|
}
|
|
27
27
|
const destinationScript = __1.ArkAddress.decode(destination).pkScript;
|
|
28
|
+
// fetch server and delegator info once, shared across all groups
|
|
29
|
+
const arkInfo = await this.arkInfoProvider.getInfo();
|
|
30
|
+
const delegateInfo = await this.delegatorProvider.getDelegateInfo();
|
|
28
31
|
// if explicit delegateAt is provided, delegate all vtxos at once without sorting
|
|
29
32
|
if (delegateAt) {
|
|
30
33
|
try {
|
|
31
|
-
await delegate(this.identity, this.delegatorProvider,
|
|
34
|
+
await delegate(this.identity, this.delegatorProvider, arkInfo, delegateInfo, vtxos, destinationScript, delegateAt);
|
|
32
35
|
}
|
|
33
36
|
catch (error) {
|
|
34
37
|
return { delegated: [], failed: [{ outpoints: vtxos, error }] };
|
|
@@ -55,7 +58,7 @@ class DelegatorManagerImpl {
|
|
|
55
58
|
// if no groups, it means we only need to delegate the recoverable vtxos
|
|
56
59
|
if (groupByExpiry.size === 0) {
|
|
57
60
|
try {
|
|
58
|
-
await delegate(this.identity, this.delegatorProvider,
|
|
61
|
+
await delegate(this.identity, this.delegatorProvider, arkInfo, delegateInfo, recoverableVtxos, destinationScript, delegateAt);
|
|
59
62
|
}
|
|
60
63
|
catch (error) {
|
|
61
64
|
return {
|
|
@@ -72,7 +75,7 @@ class DelegatorManagerImpl {
|
|
|
72
75
|
...recoverableVtxos,
|
|
73
76
|
]);
|
|
74
77
|
const groupsList = Array.from(groupByExpiry.entries());
|
|
75
|
-
const result = await Promise.allSettled(groupsList.map(async ([, vtxosGroup]) => delegate(this.identity, this.delegatorProvider,
|
|
78
|
+
const result = await Promise.allSettled(groupsList.map(async ([, vtxosGroup]) => delegate(this.identity, this.delegatorProvider, arkInfo, delegateInfo, vtxosGroup, destinationScript)));
|
|
76
79
|
const delegated = [];
|
|
77
80
|
const failed = [];
|
|
78
81
|
for (const [index, resultItem] of result.entries()) {
|
|
@@ -95,7 +98,7 @@ exports.DelegatorManagerImpl = DelegatorManagerImpl;
|
|
|
95
98
|
* should occur. If not provided, defaults to 12 hours before the earliest
|
|
96
99
|
* expiry time of the provided vtxos.
|
|
97
100
|
*/
|
|
98
|
-
async function delegate(identity, delegatorProvider,
|
|
101
|
+
async function delegate(identity, delegatorProvider, arkInfo, delegateInfo, vtxos, destinationScript, delegateAt) {
|
|
99
102
|
if (vtxos.length === 0) {
|
|
100
103
|
throw new Error("unable to delegate: no vtxos provided");
|
|
101
104
|
}
|
|
@@ -121,7 +124,7 @@ async function delegate(identity, delegatorProvider, arkInfoProvider, vtxos, des
|
|
|
121
124
|
}
|
|
122
125
|
}
|
|
123
126
|
}
|
|
124
|
-
const { fees, dust, forfeitAddress, network } =
|
|
127
|
+
const { fees, dust, forfeitAddress, network } = arkInfo;
|
|
125
128
|
const delegateAtSeconds = delegateAt.getTime() / 1000;
|
|
126
129
|
const estimator = new __1.Estimator({
|
|
127
130
|
...fees.intentFee,
|
|
@@ -145,7 +148,7 @@ async function delegate(identity, delegatorProvider, arkInfoProvider, vtxos, des
|
|
|
145
148
|
}
|
|
146
149
|
amount += BigInt(coin.value) - BigInt(inputFee.value);
|
|
147
150
|
}
|
|
148
|
-
const { delegatorAddress, pubkey, fee } =
|
|
151
|
+
const { delegatorAddress, pubkey, fee } = delegateInfo;
|
|
149
152
|
const outputs = [];
|
|
150
153
|
const delegatorFee = BigInt(Number(fee));
|
|
151
154
|
if (delegatorFee > 0n) {
|