@arkade-os/sdk 0.4.16 → 0.4.18
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/README.md +16 -6
- package/dist/cjs/contracts/arkcontract.js +0 -2
- package/dist/cjs/contracts/contractManager.js +111 -199
- package/dist/cjs/contracts/contractWatcher.js +86 -115
- package/dist/cjs/repositories/indexedDB/manager.js +6 -3
- package/dist/cjs/repositories/indexedDB/schema.js +47 -2
- package/dist/cjs/repositories/indexedDB/walletRepository.js +21 -2
- package/dist/cjs/repositories/realm/contractRepository.js +0 -4
- package/dist/cjs/repositories/realm/index.js +3 -1
- package/dist/cjs/repositories/realm/schemas.js +50 -1
- package/dist/cjs/repositories/realm/walletRepository.js +8 -4
- package/dist/cjs/repositories/scriptFromAddress.js +16 -0
- package/dist/cjs/repositories/sqlite/contractRepository.js +2 -6
- package/dist/cjs/repositories/sqlite/walletRepository.js +121 -33
- package/dist/cjs/utils/syncCursors.js +48 -56
- package/dist/cjs/wallet/expo/background.js +0 -13
- package/dist/cjs/wallet/expo/wallet.js +1 -6
- package/dist/cjs/wallet/serviceWorker/wallet-message-handler.js +16 -7
- package/dist/cjs/wallet/serviceWorker/wallet.js +19 -0
- package/dist/cjs/wallet/utils.js +41 -10
- package/dist/cjs/wallet/vtxo-manager.js +153 -39
- package/dist/cjs/wallet/wallet.js +84 -202
- package/dist/cjs/worker/expo/processors/contractPollProcessor.js +9 -13
- package/dist/cjs/worker/expo/taskRunner.js +2 -11
- package/dist/esm/contracts/arkcontract.js +0 -2
- package/dist/esm/contracts/contractManager.js +113 -201
- package/dist/esm/contracts/contractWatcher.js +86 -115
- package/dist/esm/repositories/indexedDB/manager.js +6 -3
- package/dist/esm/repositories/indexedDB/schema.js +46 -2
- package/dist/esm/repositories/indexedDB/walletRepository.js +21 -2
- package/dist/esm/repositories/realm/contractRepository.js +0 -4
- package/dist/esm/repositories/realm/index.js +1 -1
- package/dist/esm/repositories/realm/schemas.js +48 -0
- package/dist/esm/repositories/realm/walletRepository.js +8 -4
- package/dist/esm/repositories/scriptFromAddress.js +13 -0
- package/dist/esm/repositories/sqlite/contractRepository.js +2 -6
- package/dist/esm/repositories/sqlite/walletRepository.js +121 -33
- package/dist/esm/utils/syncCursors.js +47 -53
- package/dist/esm/wallet/expo/background.js +0 -13
- package/dist/esm/wallet/expo/wallet.js +2 -7
- package/dist/esm/wallet/serviceWorker/wallet-message-handler.js +17 -8
- package/dist/esm/wallet/serviceWorker/wallet.js +19 -0
- package/dist/esm/wallet/utils.js +41 -9
- package/dist/esm/wallet/vtxo-manager.js +153 -39
- package/dist/esm/wallet/wallet.js +87 -205
- package/dist/esm/worker/expo/processors/contractPollProcessor.js +9 -13
- package/dist/esm/worker/expo/taskRunner.js +3 -12
- package/dist/types/contracts/arkcontract.d.ts +0 -2
- package/dist/types/contracts/contractManager.d.ts +38 -12
- package/dist/types/contracts/contractWatcher.d.ts +22 -21
- package/dist/types/contracts/types.d.ts +0 -7
- package/dist/types/repositories/indexedDB/manager.d.ts +5 -2
- package/dist/types/repositories/indexedDB/schema.d.ts +3 -2
- package/dist/types/repositories/realm/index.d.ts +1 -1
- package/dist/types/repositories/realm/schemas.d.ts +41 -0
- package/dist/types/repositories/scriptFromAddress.d.ts +9 -0
- package/dist/types/repositories/serialization.d.ts +1 -1
- package/dist/types/repositories/sqlite/walletRepository.d.ts +22 -0
- package/dist/types/repositories/walletRepository.d.ts +10 -2
- package/dist/types/utils/syncCursors.d.ts +25 -23
- package/dist/types/wallet/index.d.ts +1 -1
- package/dist/types/wallet/serviceWorker/wallet-message-handler.d.ts +15 -3
- package/dist/types/wallet/utils.d.ts +20 -4
- package/dist/types/wallet/vtxo-manager.d.ts +16 -6
- package/dist/types/wallet/wallet.d.ts +5 -17
- package/dist/types/worker/expo/processors/contractPollProcessor.d.ts +9 -4
- package/dist/types/worker/expo/taskRunner.d.ts +6 -3
- package/package.json +1 -1
|
@@ -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:
|
|
140
|
-
type:
|
|
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,12 @@ 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
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
return vtxos.filter((vtxo) => {
|
|
306
|
+
const contractManager = await this.getContractManager();
|
|
307
|
+
const vtxos = await contractManager.getContractsWithVtxos();
|
|
308
|
+
return vtxos
|
|
309
|
+
.flatMap((_) => _.vtxos)
|
|
310
|
+
.filter((vtxo) => {
|
|
303
311
|
if ((0, _1.isSpendable)(vtxo)) {
|
|
304
312
|
if (!f.withRecoverable &&
|
|
305
313
|
((0, _1.isRecoverable)(vtxo) || (0, _1.isExpired)(vtxo))) {
|
|
@@ -314,11 +322,9 @@ class ReadonlyWallet {
|
|
|
314
322
|
* Return wallet transaction history derived from Arkade state and boarding transactions.
|
|
315
323
|
*/
|
|
316
324
|
async getTransactionHistory() {
|
|
317
|
-
|
|
318
|
-
const
|
|
319
|
-
const allVtxos =
|
|
320
|
-
? await this.walletRepository.getVtxos(address)
|
|
321
|
-
: fetchedExtended;
|
|
325
|
+
const contractManager = await this.getContractManager();
|
|
326
|
+
const response = await contractManager.getContractsWithVtxos();
|
|
327
|
+
const allVtxos = response.flatMap((_) => _.vtxos);
|
|
322
328
|
const { boardingTxs, commitmentsToIgnore } = await this.getBoardingTxs();
|
|
323
329
|
const getTxCreatedAt = (txid) => this.indexerProvider
|
|
324
330
|
.getVtxos({ outpoints: [{ txid, vout: 0 }] })
|
|
@@ -326,166 +332,11 @@ class ReadonlyWallet {
|
|
|
326
332
|
return (0, transactionHistory_1.buildTransactionHistory)(allVtxos, boardingTxs, commitmentsToIgnore, getTxCreatedAt);
|
|
327
333
|
}
|
|
328
334
|
/**
|
|
329
|
-
*
|
|
330
|
-
* cursor, or do a full bootstrap when no cursor exists. Upserts
|
|
331
|
-
* the result into the cache and advances the sync cursors.
|
|
332
|
-
*
|
|
333
|
-
* Concurrent calls are deduplicated: if a sync is already in flight,
|
|
334
|
-
* subsequent callers receive the same promise instead of triggering
|
|
335
|
-
* a second network round-trip.
|
|
336
|
-
*/
|
|
337
|
-
syncVtxos() {
|
|
338
|
-
if (this._syncVtxosInflight)
|
|
339
|
-
return this._syncVtxosInflight;
|
|
340
|
-
const p = this.doSyncVtxos().finally(() => {
|
|
341
|
-
this._syncVtxosInflight = undefined;
|
|
342
|
-
});
|
|
343
|
-
this._syncVtxosInflight = p;
|
|
344
|
-
return p;
|
|
345
|
-
}
|
|
346
|
-
async doSyncVtxos() {
|
|
347
|
-
const address = await this.getAddress();
|
|
348
|
-
// Batch cursor read with script map to avoid extra async hops
|
|
349
|
-
// before the fetch (background operations may run between hops).
|
|
350
|
-
const [scriptMap, cursors] = await Promise.all([
|
|
351
|
-
this.getScriptMap(),
|
|
352
|
-
(0, syncCursors_1.getAllSyncCursors)(this.walletRepository),
|
|
353
|
-
]);
|
|
354
|
-
const allScripts = [...scriptMap.keys()];
|
|
355
|
-
// Partition scripts into bootstrap (no cursor) and delta (has cursor).
|
|
356
|
-
const bootstrapScripts = [];
|
|
357
|
-
const deltaScripts = [];
|
|
358
|
-
for (const s of allScripts) {
|
|
359
|
-
if (cursors[s] === undefined) {
|
|
360
|
-
bootstrapScripts.push(s);
|
|
361
|
-
}
|
|
362
|
-
else {
|
|
363
|
-
deltaScripts.push(s);
|
|
364
|
-
}
|
|
365
|
-
}
|
|
366
|
-
const requestStartedAt = Date.now();
|
|
367
|
-
const allVtxos = [];
|
|
368
|
-
const extendWithScript = (vtxo) => {
|
|
369
|
-
const vtxoScript = vtxo.script
|
|
370
|
-
? scriptMap.get(vtxo.script)
|
|
371
|
-
: undefined;
|
|
372
|
-
if (!vtxoScript)
|
|
373
|
-
return undefined;
|
|
374
|
-
return {
|
|
375
|
-
...vtxo,
|
|
376
|
-
forfeitTapLeafScript: vtxoScript.forfeit(),
|
|
377
|
-
intentTapLeafScript: vtxoScript.forfeit(),
|
|
378
|
-
tapTree: vtxoScript.encode(),
|
|
379
|
-
};
|
|
380
|
-
};
|
|
381
|
-
// Full fetch for scripts with no cursor.
|
|
382
|
-
if (bootstrapScripts.length > 0) {
|
|
383
|
-
const response = await this.indexerProvider.getVtxos({
|
|
384
|
-
scripts: bootstrapScripts,
|
|
385
|
-
});
|
|
386
|
-
allVtxos.push(...response.vtxos);
|
|
387
|
-
}
|
|
388
|
-
// Delta fetch for scripts with an existing cursor.
|
|
389
|
-
let hasDelta = false;
|
|
390
|
-
if (deltaScripts.length > 0) {
|
|
391
|
-
const minCursor = Math.min(...deltaScripts.map((s) => cursors[s]));
|
|
392
|
-
const window = (0, syncCursors_1.computeSyncWindow)(minCursor);
|
|
393
|
-
if (window) {
|
|
394
|
-
hasDelta = true;
|
|
395
|
-
const response = await this.indexerProvider.getVtxos({
|
|
396
|
-
scripts: deltaScripts,
|
|
397
|
-
after: window.after,
|
|
398
|
-
});
|
|
399
|
-
allVtxos.push(...response.vtxos);
|
|
400
|
-
}
|
|
401
|
-
}
|
|
402
|
-
// Extend every fetched virtual output and upsert into the cache.
|
|
403
|
-
const fetchedExtended = [];
|
|
404
|
-
for (const vtxo of allVtxos) {
|
|
405
|
-
const extended = extendWithScript(vtxo);
|
|
406
|
-
if (extended)
|
|
407
|
-
fetchedExtended.push(extended);
|
|
408
|
-
}
|
|
409
|
-
// Save virtual outputs first, then advance cursors only on success.
|
|
410
|
-
const cutoff = (0, syncCursors_1.cursorCutoff)(requestStartedAt);
|
|
411
|
-
await this.walletRepository.saveVtxos(address, fetchedExtended);
|
|
412
|
-
await (0, syncCursors_1.advanceSyncCursors)(this.walletRepository, Object.fromEntries(allScripts.map((s) => [s, cutoff])));
|
|
413
|
-
// Delta-sync reconciliation: full re-fetch for delta scripts.
|
|
414
|
-
//
|
|
415
|
-
// The delta fetch (above) only returns virtual outputs changed after the
|
|
416
|
-
// cursor, so it can miss preconfirmed virtual outputs that were consumed
|
|
417
|
-
// by a round between syncs. Rather than layering targeted
|
|
418
|
-
// queries (pendingOnly, spendableOnly) with pagination guards
|
|
419
|
-
// and set algebra, we perform a single unfiltered re-fetch for
|
|
420
|
-
// delta scripts. This is slightly more data over the wire but
|
|
421
|
-
// gives us complete, authoritative state in one call and keeps
|
|
422
|
-
// the reconciliation logic simple.
|
|
423
|
-
//
|
|
424
|
-
// Any cached non-spent virtual output that is absent from the full
|
|
425
|
-
// result set is marked spent; any virtual output whose state changed
|
|
426
|
-
// (e.g. preconfirmed → settled) is updated in place.
|
|
427
|
-
if (hasDelta) {
|
|
428
|
-
const { vtxos: fullVtxos, page: fullPage } = await this.indexerProvider.getVtxos({
|
|
429
|
-
scripts: deltaScripts,
|
|
430
|
-
});
|
|
431
|
-
// Reconciliation is best-effort: if the response is
|
|
432
|
-
// paginated we don't have a complete picture, so we skip
|
|
433
|
-
// rather than act on partial data. Wallets with enough
|
|
434
|
-
// virtual outputs to exceed a single page rely solely on the
|
|
435
|
-
// cursor-based delta mechanism for state updates.
|
|
436
|
-
const fullSetComplete = !fullPage || fullPage.total <= 1;
|
|
437
|
-
if (fullSetComplete) {
|
|
438
|
-
const fullOutpoints = new Map(fullVtxos.map((v) => [`${v.txid}:${v.vout}`, v]));
|
|
439
|
-
const deltaScriptSet = new Set(deltaScripts);
|
|
440
|
-
const cachedVtxos = await this.walletRepository.getVtxos(address);
|
|
441
|
-
const reconciledExtended = [];
|
|
442
|
-
for (const cached of cachedVtxos) {
|
|
443
|
-
if (!cached.script ||
|
|
444
|
-
!deltaScriptSet.has(cached.script) ||
|
|
445
|
-
cached.isSpent) {
|
|
446
|
-
continue;
|
|
447
|
-
}
|
|
448
|
-
const outpoint = `${cached.txid}:${cached.vout}`;
|
|
449
|
-
const fresh = fullOutpoints.get(outpoint);
|
|
450
|
-
if (!fresh) {
|
|
451
|
-
// Server no longer knows about this virtual output —
|
|
452
|
-
// it was spent between syncs.
|
|
453
|
-
reconciledExtended.push({
|
|
454
|
-
...cached,
|
|
455
|
-
isSpent: true,
|
|
456
|
-
});
|
|
457
|
-
continue;
|
|
458
|
-
}
|
|
459
|
-
const extended = extendWithScript(fresh);
|
|
460
|
-
if (extended &&
|
|
461
|
-
extended.virtualStatus.state !==
|
|
462
|
-
cached.virtualStatus.state) {
|
|
463
|
-
// State transitioned (e.g. preconfirmed →
|
|
464
|
-
// settled) — update the cached entry.
|
|
465
|
-
reconciledExtended.push(extended);
|
|
466
|
-
}
|
|
467
|
-
}
|
|
468
|
-
if (reconciledExtended.length > 0) {
|
|
469
|
-
console.warn(`[ark-sdk] delta sync: reconciled ${reconciledExtended.length} stale VTXO(s) via full re-fetch`);
|
|
470
|
-
await this.walletRepository.saveVtxos(address, reconciledExtended);
|
|
471
|
-
}
|
|
472
|
-
}
|
|
473
|
-
else {
|
|
474
|
-
console.warn("[ark-sdk] delta sync: skipping reconciliation — full re-fetch was paginated");
|
|
475
|
-
}
|
|
476
|
-
}
|
|
477
|
-
return {
|
|
478
|
-
isDelta: hasDelta || bootstrapScripts.length === 0,
|
|
479
|
-
fetchedExtended,
|
|
480
|
-
address,
|
|
481
|
-
};
|
|
482
|
-
}
|
|
483
|
-
/**
|
|
484
|
-
* Clear all virtual output sync cursors, forcing a full re-bootstrap on next sync.
|
|
335
|
+
* Clear the global VTXO sync cursor, forcing a full re-bootstrap on next sync.
|
|
485
336
|
* Useful for recovery after indexer reprocessing or debugging.
|
|
486
337
|
*/
|
|
487
|
-
async
|
|
488
|
-
await (0, syncCursors_1.
|
|
338
|
+
async clearSyncCursor() {
|
|
339
|
+
await (0, syncCursors_1.clearSyncCursor)(this.walletRepository);
|
|
489
340
|
}
|
|
490
341
|
/**
|
|
491
342
|
* Build a transaction history view for the wallet's boarding address.
|
|
@@ -528,6 +379,7 @@ class ReadonlyWallet {
|
|
|
528
379
|
createdAt: tx.status.confirmed
|
|
529
380
|
? new Date(tx.status.block_time * 1000)
|
|
530
381
|
: new Date(0),
|
|
382
|
+
script: base_1.hex.encode(this.boardingTapscript.pkScript),
|
|
531
383
|
});
|
|
532
384
|
}
|
|
533
385
|
}
|
|
@@ -617,22 +469,41 @@ class ReadonlyWallet {
|
|
|
617
469
|
await this.indexerProvider?.unsubscribeForScripts(subscriptionId);
|
|
618
470
|
};
|
|
619
471
|
// Handle subscription updates asynchronously without blocking.
|
|
620
|
-
//
|
|
621
|
-
//
|
|
622
|
-
//
|
|
623
|
-
//
|
|
624
|
-
//
|
|
472
|
+
// Subscription covers all wallet scripts (default + delegate) plus
|
|
473
|
+
// any additional registered contracts. Virtual outputs carry a
|
|
474
|
+
// `script` field from the indexer which the contract manager
|
|
475
|
+
// resolves to the owning contract so the extension uses the
|
|
476
|
+
// correct forfeit/intent tapscripts.
|
|
625
477
|
(async () => {
|
|
626
478
|
try {
|
|
479
|
+
const cm = await this.getContractManager();
|
|
627
480
|
for await (const update of subscription) {
|
|
628
|
-
if (update.newVtxos?.length
|
|
629
|
-
update.spentVtxos?.length
|
|
481
|
+
if (update.newVtxos?.length === 0 &&
|
|
482
|
+
update.spentVtxos?.length === 0) {
|
|
483
|
+
continue;
|
|
484
|
+
}
|
|
485
|
+
// Isolate per-update annotation failures (e.g. a VTXO
|
|
486
|
+
// arriving for a contract we haven't registered yet).
|
|
487
|
+
// Without this a single bad update would kill the
|
|
488
|
+
// for-await loop and silently drop every subsequent
|
|
489
|
+
// subscription event for the session.
|
|
490
|
+
try {
|
|
491
|
+
// Default to `[]` so a one-sided update (e.g.
|
|
492
|
+
// only `newVtxos`) doesn't pass `undefined` into
|
|
493
|
+
// annotateVtxos and throw on `.length`.
|
|
494
|
+
const [newVtxos, spentVtxos] = await Promise.all([
|
|
495
|
+
cm.annotateVtxos(update.newVtxos ?? []),
|
|
496
|
+
cm.annotateVtxos(update.spentVtxos ?? []),
|
|
497
|
+
]);
|
|
630
498
|
eventCallback({
|
|
631
499
|
type: "vtxo",
|
|
632
|
-
newVtxos
|
|
633
|
-
spentVtxos
|
|
500
|
+
newVtxos,
|
|
501
|
+
spentVtxos,
|
|
634
502
|
});
|
|
635
503
|
}
|
|
504
|
+
catch (error) {
|
|
505
|
+
console.warn("Dropping subscription update after annotation failed; next sync will reconcile:", error);
|
|
506
|
+
}
|
|
636
507
|
}
|
|
637
508
|
}
|
|
638
509
|
catch (error) {
|
|
@@ -778,7 +649,6 @@ class ReadonlyWallet {
|
|
|
778
649
|
indexerProvider: this.indexerProvider,
|
|
779
650
|
contractRepository: this.contractRepository,
|
|
780
651
|
walletRepository: this.walletRepository,
|
|
781
|
-
getDefaultAddress: () => this.getAddress(),
|
|
782
652
|
watcherConfig: this.watcherConfig,
|
|
783
653
|
});
|
|
784
654
|
// Register the wallet's current address as a contract
|
|
@@ -904,11 +774,10 @@ class Wallet extends ReadonlyWallet {
|
|
|
904
774
|
}
|
|
905
775
|
});
|
|
906
776
|
}
|
|
907
|
-
constructor(identity, network,
|
|
777
|
+
constructor(identity, network, onchainProvider, arkProvider, indexerProvider, arkServerPublicKey, offchainTapscript, boardingTapscript, serverUnrollScript, forfeitOutputScript, forfeitPubkey, dustAmount, walletRepository, contractRepository,
|
|
908
778
|
/** @deprecated Use settlementConfig */
|
|
909
779
|
renewalConfig, delegatorProvider, watcherConfig, settlementConfig) {
|
|
910
780
|
super(identity, network, onchainProvider, indexerProvider, arkServerPublicKey, offchainTapscript, boardingTapscript, dustAmount, walletRepository, contractRepository, delegatorProvider, watcherConfig);
|
|
911
|
-
this.networkName = networkName;
|
|
912
781
|
this.arkProvider = arkProvider;
|
|
913
782
|
this.serverUnrollScript = serverUnrollScript;
|
|
914
783
|
this.forfeitOutputScript = forfeitOutputScript;
|
|
@@ -1028,7 +897,7 @@ class Wallet extends ReadonlyWallet {
|
|
|
1028
897
|
const forfeitPubkey = base_1.hex.decode(setup.info.forfeitPubkey).slice(1);
|
|
1029
898
|
const forfeitAddress = (0, btc_signer_1.Address)(setup.network).decode(setup.info.forfeitAddress);
|
|
1030
899
|
const forfeitOutputScript = btc_signer_1.OutScript.encode(forfeitAddress);
|
|
1031
|
-
const wallet = new Wallet(config.identity, setup.network, setup.
|
|
900
|
+
const wallet = new Wallet(config.identity, setup.network, setup.onchainProvider, setup.arkProvider, setup.indexerProvider, setup.serverPubKey, setup.offchainTapscript, setup.boardingTapscript, serverUnrollScript, forfeitOutputScript, forfeitPubkey, setup.dustAmount, setup.walletRepository, setup.contractRepository, config.renewalConfig, config.delegatorProvider, config.watcherConfig, config.settlementConfig);
|
|
1032
901
|
await wallet.getVtxoManager();
|
|
1033
902
|
return wallet;
|
|
1034
903
|
}
|
|
@@ -1291,7 +1160,7 @@ class Wallet extends ReadonlyWallet {
|
|
|
1291
1160
|
const abortController = new AbortController();
|
|
1292
1161
|
try {
|
|
1293
1162
|
const stream = this.arkProvider.getEventStream(abortController.signal, topics);
|
|
1294
|
-
const intentId = await this.safeRegisterIntent(intent);
|
|
1163
|
+
const intentId = await this.safeRegisterIntent(intent, params.inputs);
|
|
1295
1164
|
const handler = this.createBatchHandler(intentId, params.inputs, recipients, session);
|
|
1296
1165
|
const commitmentTxid = await batch_1.Batch.join(stream, handler, {
|
|
1297
1166
|
abortController,
|
|
@@ -1304,8 +1173,16 @@ class Wallet extends ReadonlyWallet {
|
|
|
1304
1173
|
return commitmentTxid;
|
|
1305
1174
|
}
|
|
1306
1175
|
catch (error) {
|
|
1307
|
-
// delete the intent to not be stuck in the queue
|
|
1308
|
-
|
|
1176
|
+
// delete the intent to not be stuck in the queue. If deletion fails
|
|
1177
|
+
// the intent stays on the server and the next settle will hit
|
|
1178
|
+
// "duplicated input" in safeRegisterIntent — surface the failure
|
|
1179
|
+
// rather than silently swallowing it.
|
|
1180
|
+
const inputIds = params.inputs
|
|
1181
|
+
.map((i) => `${i.txid}:${i.vout}`)
|
|
1182
|
+
.join(",");
|
|
1183
|
+
await this.arkProvider.deleteIntent(deleteIntent).catch((e) => {
|
|
1184
|
+
console.warn(`Failed to delete intent after settle failure for inputs [${inputIds}]; intent may linger on server and cause 'duplicated input' on next settle`, e);
|
|
1185
|
+
});
|
|
1309
1186
|
throw error;
|
|
1310
1187
|
}
|
|
1311
1188
|
finally {
|
|
@@ -1495,7 +1372,7 @@ class Wallet extends ReadonlyWallet {
|
|
|
1495
1372
|
},
|
|
1496
1373
|
};
|
|
1497
1374
|
}
|
|
1498
|
-
async safeRegisterIntent(intent) {
|
|
1375
|
+
async safeRegisterIntent(intent, inputs) {
|
|
1499
1376
|
try {
|
|
1500
1377
|
return await this.arkProvider.registerIntent(intent);
|
|
1501
1378
|
}
|
|
@@ -1504,11 +1381,13 @@ class Wallet extends ReadonlyWallet {
|
|
|
1504
1381
|
if (error instanceof errors_1.ArkError &&
|
|
1505
1382
|
error.code === 0 &&
|
|
1506
1383
|
error.message.includes("duplicated input")) {
|
|
1507
|
-
//
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
1384
|
+
// Clear any queued intent spending these exact inputs. The
|
|
1385
|
+
// previous implementation signed a proof over getVtxos() only,
|
|
1386
|
+
// which misses boarding UTXOs — the most common trigger for
|
|
1387
|
+
// "duplicated input" on the auto-settle path. Signing the
|
|
1388
|
+
// caller's own inputs keeps the proof surgical and correct
|
|
1389
|
+
// regardless of whether the stuck input is a VTXO or boarding.
|
|
1390
|
+
const deleteIntent = await this.makeDeleteIntentSignature(inputs);
|
|
1512
1391
|
await this.arkProvider.deleteIntent(deleteIntent);
|
|
1513
1392
|
// try again
|
|
1514
1393
|
return this.arkProvider.registerIntent(intent);
|
|
@@ -1576,9 +1455,7 @@ class Wallet extends ReadonlyWallet {
|
|
|
1576
1455
|
scripts: allScripts,
|
|
1577
1456
|
});
|
|
1578
1457
|
for (const vtxo of fetchedVtxos) {
|
|
1579
|
-
const vtxoScript = vtxo.script
|
|
1580
|
-
? scriptMap.get(vtxo.script)
|
|
1581
|
-
: undefined;
|
|
1458
|
+
const vtxoScript = scriptMap.get(vtxo.script);
|
|
1582
1459
|
if (!vtxoScript)
|
|
1583
1460
|
continue;
|
|
1584
1461
|
if (vtxo.virtualStatus.state === "swept" ||
|
|
@@ -1881,8 +1758,9 @@ class Wallet extends ReadonlyWallet {
|
|
|
1881
1758
|
console.warn(`updateDbAfterOffchainTx: inputs length (${inputs.length}) differs from signedCheckpointTxs length (${signedCheckpointTxs.length})`);
|
|
1882
1759
|
}
|
|
1883
1760
|
const safeLength = Math.min(inputs.length, signedCheckpointTxs.length);
|
|
1884
|
-
|
|
1885
|
-
|
|
1761
|
+
const cm = await this.getContractManager();
|
|
1762
|
+
const annotatedInputs = await cm.annotateVtxos(inputs);
|
|
1763
|
+
for (const [inputIndex, vtxo] of annotatedInputs.entries()) {
|
|
1886
1764
|
if (inputIndex < safeLength &&
|
|
1887
1765
|
signedCheckpointTxs[inputIndex]) {
|
|
1888
1766
|
const checkpoint = btc_signer_1.Transaction.fromPSBT(base_1.base64.decode(signedCheckpointTxs[inputIndex]));
|
|
@@ -1942,6 +1820,7 @@ class Wallet extends ReadonlyWallet {
|
|
|
1942
1820
|
confirmed: false,
|
|
1943
1821
|
},
|
|
1944
1822
|
assets: changeAssets,
|
|
1823
|
+
script: base_1.hex.encode(this.offchainTapscript.pkScript),
|
|
1945
1824
|
};
|
|
1946
1825
|
}
|
|
1947
1826
|
await this.walletRepository.saveVtxos(addr, changeVtxo ? [...spentVtxos, changeVtxo] : spentVtxos);
|
|
@@ -1972,10 +1851,14 @@ class Wallet extends ReadonlyWallet {
|
|
|
1972
1851
|
const inputArkTxIds = new Set();
|
|
1973
1852
|
const boardingUtxoToRemove = new Set();
|
|
1974
1853
|
const isVtxo = (input) => "virtualStatus" in input;
|
|
1854
|
+
const vtxoInputs = inputs.filter(isVtxo);
|
|
1855
|
+
const cm = await this.getContractManager();
|
|
1856
|
+
const annotatedVtxos = await cm.annotateVtxos(vtxoInputs);
|
|
1857
|
+
const annotatedByKey = new Map(annotatedVtxos.map((v) => [`${v.txid}:${v.vout}`, v]));
|
|
1975
1858
|
for (const input of inputs) {
|
|
1976
1859
|
if (isVtxo(input)) {
|
|
1977
1860
|
// virtual output = mark it settled
|
|
1978
|
-
const vtxo =
|
|
1861
|
+
const vtxo = annotatedByKey.get(`${input.txid}:${input.vout}`);
|
|
1979
1862
|
if (vtxo.arkTxId) {
|
|
1980
1863
|
inputArkTxIds.add(vtxo.arkTxId);
|
|
1981
1864
|
}
|
|
@@ -2062,7 +1945,7 @@ function selectVirtualCoins(coins, targetAmount) {
|
|
|
2062
1945
|
*/
|
|
2063
1946
|
async function waitForIncomingFunds(wallet) {
|
|
2064
1947
|
let stopFunc;
|
|
2065
|
-
|
|
1948
|
+
return new Promise((resolve) => {
|
|
2066
1949
|
wallet
|
|
2067
1950
|
.notifyIncomingFunds((coins) => {
|
|
2068
1951
|
resolve(coins);
|
|
@@ -2073,5 +1956,4 @@ async function waitForIncomingFunds(wallet) {
|
|
|
2073
1956
|
stopFunc = stop;
|
|
2074
1957
|
});
|
|
2075
1958
|
});
|
|
2076
|
-
return promise;
|
|
2077
1959
|
}
|
|
@@ -8,27 +8,24 @@ exports.CONTRACT_POLL_TASK_TYPE = "contract-poll";
|
|
|
8
8
|
*
|
|
9
9
|
* Replicates the polling subset of @see ContractManager.initialize:
|
|
10
10
|
* 1. Load all contracts from the contract repository.
|
|
11
|
-
* 2.
|
|
12
|
-
* 3.
|
|
13
|
-
* 4.
|
|
14
|
-
*
|
|
11
|
+
* 2. Paginated fetch of every VTXO (including spent) from the indexer.
|
|
12
|
+
* 3. Extend each VTXO with tapscript data.
|
|
13
|
+
* 4. Save to the wallet repository.
|
|
14
|
+
*
|
|
15
|
+
* NOTE: the indexer query deliberately omits `spendableOnly`. Every
|
|
16
|
+
* repository implements `saveVtxos` as an upsert with no batch delete,
|
|
17
|
+
* so filtering to spendable-only would leave VTXOs that became spent
|
|
18
|
+
* between polls marked as spendable forever. Fetching the full set lets
|
|
19
|
+
* the upsert overwrite stale records with their latest state.
|
|
15
20
|
*/
|
|
16
21
|
exports.contractPollProcessor = {
|
|
17
22
|
taskType: exports.CONTRACT_POLL_TASK_TYPE,
|
|
18
23
|
async execute(item, deps) {
|
|
19
24
|
const { contractRepository, walletRepository, indexerProvider, extendVtxo, } = deps;
|
|
20
25
|
const contracts = await contractRepository.getContracts();
|
|
21
|
-
const now = Date.now();
|
|
22
26
|
let contractsProcessed = 0;
|
|
23
27
|
let vtxosSaved = 0;
|
|
24
28
|
for (const contract of contracts) {
|
|
25
|
-
// Mark expired active contracts as inactive
|
|
26
|
-
if (contract.state === "active" &&
|
|
27
|
-
contract.expiresAt &&
|
|
28
|
-
contract.expiresAt <= now) {
|
|
29
|
-
contract.state = "inactive";
|
|
30
|
-
await contractRepository.saveContract(contract);
|
|
31
|
-
}
|
|
32
29
|
// Paginated fetch of spendable virtual outputs
|
|
33
30
|
const pageSize = 100;
|
|
34
31
|
let pageIndex = 0;
|
|
@@ -37,7 +34,6 @@ exports.contractPollProcessor = {
|
|
|
37
34
|
while (hasMore) {
|
|
38
35
|
const { vtxos, page } = await indexerProvider.getVtxos({
|
|
39
36
|
scripts: [contract.script],
|
|
40
|
-
spendableOnly: true,
|
|
41
37
|
pageIndex,
|
|
42
38
|
pageSize,
|
|
43
39
|
});
|
|
@@ -66,17 +66,8 @@ async function runTasks(queue, processors, deps) {
|
|
|
66
66
|
* can build deps without depending on Expo.
|
|
67
67
|
*/
|
|
68
68
|
function createTaskDependencies(options) {
|
|
69
|
-
const { walletRepository, contractRepository, indexerProvider, arkProvider, offchainTapscript, } = options;
|
|
70
69
|
return {
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
indexerProvider,
|
|
74
|
-
arkProvider,
|
|
75
|
-
extendVtxo: (vtxo, contract) => {
|
|
76
|
-
if (contract) {
|
|
77
|
-
return (0, utils_1.extendVtxoFromContract)(vtxo, contract);
|
|
78
|
-
}
|
|
79
|
-
return (0, utils_1.extendVirtualCoin)({ offchainTapscript }, vtxo);
|
|
80
|
-
},
|
|
70
|
+
...options,
|
|
71
|
+
extendVtxo: (vtxo, contract) => (0, utils_1.extendVirtualCoinForContract)(vtxo, contract),
|
|
81
72
|
};
|
|
82
73
|
}
|
|
@@ -103,7 +103,6 @@ export function contractFromArkContract(encoded, options = {}) {
|
|
|
103
103
|
params,
|
|
104
104
|
state: options.state || "active",
|
|
105
105
|
createdAt: Date.now(),
|
|
106
|
-
expiresAt: options.expiresAt,
|
|
107
106
|
metadata: options.metadata,
|
|
108
107
|
};
|
|
109
108
|
}
|
|
@@ -129,7 +128,6 @@ export function contractFromArkContractWithAddress(encoded, serverPubKey, addres
|
|
|
129
128
|
address: vtxoScript.address(addressPrefix, serverPubKey).encode(),
|
|
130
129
|
state: options.state || "active",
|
|
131
130
|
createdAt: Date.now(),
|
|
132
|
-
expiresAt: options.expiresAt,
|
|
133
131
|
metadata: options.metadata,
|
|
134
132
|
};
|
|
135
133
|
}
|