@arkade-os/sdk 0.4.17 → 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 -215
- 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 +72 -195
- 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 -217
- 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 +75 -198
- 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 -9
- 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
|
@@ -12,7 +12,7 @@ import { validateConnectorsTxGraph, validateVtxoTxGraph, } from '../tree/validat
|
|
|
12
12
|
import { validateBatchRecipients } from './validation.js';
|
|
13
13
|
import { isBatchSignable } from '../identity/index.js';
|
|
14
14
|
import { isExpired, isRecoverable, isSpendable, isSubdust, TxType, } from './index.js';
|
|
15
|
-
import { createAssetPacket,
|
|
15
|
+
import { createAssetPacket, selectCoinsWithAsset, selectedCoinsToAssetInputs, } from './asset.js';
|
|
16
16
|
import { VtxoScript } from '../script/base.js';
|
|
17
17
|
import { CSVMultisigTapscript } from '../script/tapscript.js';
|
|
18
18
|
import { buildOffchainTx, combineTapscriptSigs, hasBoardingTxExpired, isValidArkAddress, } from '../utils/arkTransaction.js';
|
|
@@ -20,7 +20,7 @@ import { DEFAULT_RENEWAL_CONFIG, DEFAULT_SETTLEMENT_CONFIG, VtxoManager, } from
|
|
|
20
20
|
import { ArkNote } from '../arknote/index.js';
|
|
21
21
|
import { Intent } from '../intent/index.js';
|
|
22
22
|
import { RestIndexerProvider } from '../providers/indexer.js';
|
|
23
|
-
import { extendCoin,
|
|
23
|
+
import { extendCoin, validateRecipients } from './utils.js';
|
|
24
24
|
import { ArkError } from '../providers/errors.js';
|
|
25
25
|
import { Batch } from './batch.js';
|
|
26
26
|
import { Estimator } from '../arkfee/index.js';
|
|
@@ -33,7 +33,7 @@ import { IndexedDBContractRepository, IndexedDBWalletRepository, } from '../repo
|
|
|
33
33
|
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
|
-
import {
|
|
36
|
+
import { clearSyncCursor, updateWalletState } from '../utils/syncCursors.js';
|
|
37
37
|
// Hardcoded unilateral exit delay for mainnet (~7 days in seconds).
|
|
38
38
|
// Pinned here so that address derivation stays stable for existing mainnet
|
|
39
39
|
// wallets even after the server lowers the delay it advertises.
|
|
@@ -299,8 +299,10 @@ export class ReadonlyWallet {
|
|
|
299
299
|
async getVtxos(filter) {
|
|
300
300
|
const f = filter ?? { withRecoverable: true, withUnrolled: false };
|
|
301
301
|
const contractManager = await this.getContractManager();
|
|
302
|
-
const
|
|
303
|
-
return
|
|
302
|
+
const vtxos = await contractManager.getContractsWithVtxos();
|
|
303
|
+
return vtxos
|
|
304
|
+
.flatMap((_) => _.vtxos)
|
|
305
|
+
.filter((vtxo) => {
|
|
304
306
|
if (isSpendable(vtxo)) {
|
|
305
307
|
if (!f.withRecoverable &&
|
|
306
308
|
(isRecoverable(vtxo) || isExpired(vtxo))) {
|
|
@@ -309,17 +311,15 @@ export class ReadonlyWallet {
|
|
|
309
311
|
return true;
|
|
310
312
|
}
|
|
311
313
|
return !!(f.withUnrolled && vtxo.isUnrolled);
|
|
312
|
-
})
|
|
314
|
+
});
|
|
313
315
|
}
|
|
314
316
|
/**
|
|
315
317
|
* Return wallet transaction history derived from Arkade state and boarding transactions.
|
|
316
318
|
*/
|
|
317
319
|
async getTransactionHistory() {
|
|
318
|
-
|
|
319
|
-
const
|
|
320
|
-
const allVtxos =
|
|
321
|
-
? await this.walletRepository.getVtxos(address)
|
|
322
|
-
: fetchedExtended;
|
|
320
|
+
const contractManager = await this.getContractManager();
|
|
321
|
+
const response = await contractManager.getContractsWithVtxos();
|
|
322
|
+
const allVtxos = response.flatMap((_) => _.vtxos);
|
|
323
323
|
const { boardingTxs, commitmentsToIgnore } = await this.getBoardingTxs();
|
|
324
324
|
const getTxCreatedAt = (txid) => this.indexerProvider
|
|
325
325
|
.getVtxos({ outpoints: [{ txid, vout: 0 }] })
|
|
@@ -327,166 +327,11 @@ export class ReadonlyWallet {
|
|
|
327
327
|
return buildTransactionHistory(allVtxos, boardingTxs, commitmentsToIgnore, getTxCreatedAt);
|
|
328
328
|
}
|
|
329
329
|
/**
|
|
330
|
-
*
|
|
331
|
-
* cursor, or do a full bootstrap when no cursor exists. Upserts
|
|
332
|
-
* the result into the cache and advances the sync cursors.
|
|
333
|
-
*
|
|
334
|
-
* Concurrent calls are deduplicated: if a sync is already in flight,
|
|
335
|
-
* subsequent callers receive the same promise instead of triggering
|
|
336
|
-
* a second network round-trip.
|
|
337
|
-
*/
|
|
338
|
-
syncVtxos() {
|
|
339
|
-
if (this._syncVtxosInflight)
|
|
340
|
-
return this._syncVtxosInflight;
|
|
341
|
-
const p = this.doSyncVtxos().finally(() => {
|
|
342
|
-
this._syncVtxosInflight = undefined;
|
|
343
|
-
});
|
|
344
|
-
this._syncVtxosInflight = p;
|
|
345
|
-
return p;
|
|
346
|
-
}
|
|
347
|
-
async doSyncVtxos() {
|
|
348
|
-
const address = await this.getAddress();
|
|
349
|
-
// Batch cursor read with script map to avoid extra async hops
|
|
350
|
-
// before the fetch (background operations may run between hops).
|
|
351
|
-
const [scriptMap, cursors] = await Promise.all([
|
|
352
|
-
this.getScriptMap(),
|
|
353
|
-
getAllSyncCursors(this.walletRepository),
|
|
354
|
-
]);
|
|
355
|
-
const allScripts = [...scriptMap.keys()];
|
|
356
|
-
// Partition scripts into bootstrap (no cursor) and delta (has cursor).
|
|
357
|
-
const bootstrapScripts = [];
|
|
358
|
-
const deltaScripts = [];
|
|
359
|
-
for (const s of allScripts) {
|
|
360
|
-
if (cursors[s] === undefined) {
|
|
361
|
-
bootstrapScripts.push(s);
|
|
362
|
-
}
|
|
363
|
-
else {
|
|
364
|
-
deltaScripts.push(s);
|
|
365
|
-
}
|
|
366
|
-
}
|
|
367
|
-
const requestStartedAt = Date.now();
|
|
368
|
-
const allVtxos = [];
|
|
369
|
-
const extendWithScript = (vtxo) => {
|
|
370
|
-
const vtxoScript = vtxo.script
|
|
371
|
-
? scriptMap.get(vtxo.script)
|
|
372
|
-
: undefined;
|
|
373
|
-
if (!vtxoScript)
|
|
374
|
-
return undefined;
|
|
375
|
-
return {
|
|
376
|
-
...vtxo,
|
|
377
|
-
forfeitTapLeafScript: vtxoScript.forfeit(),
|
|
378
|
-
intentTapLeafScript: vtxoScript.forfeit(),
|
|
379
|
-
tapTree: vtxoScript.encode(),
|
|
380
|
-
};
|
|
381
|
-
};
|
|
382
|
-
// Full fetch for scripts with no cursor.
|
|
383
|
-
if (bootstrapScripts.length > 0) {
|
|
384
|
-
const response = await this.indexerProvider.getVtxos({
|
|
385
|
-
scripts: bootstrapScripts,
|
|
386
|
-
});
|
|
387
|
-
allVtxos.push(...response.vtxos);
|
|
388
|
-
}
|
|
389
|
-
// Delta fetch for scripts with an existing cursor.
|
|
390
|
-
let hasDelta = false;
|
|
391
|
-
if (deltaScripts.length > 0) {
|
|
392
|
-
const minCursor = Math.min(...deltaScripts.map((s) => cursors[s]));
|
|
393
|
-
const window = computeSyncWindow(minCursor);
|
|
394
|
-
if (window) {
|
|
395
|
-
hasDelta = true;
|
|
396
|
-
const response = await this.indexerProvider.getVtxos({
|
|
397
|
-
scripts: deltaScripts,
|
|
398
|
-
after: window.after,
|
|
399
|
-
});
|
|
400
|
-
allVtxos.push(...response.vtxos);
|
|
401
|
-
}
|
|
402
|
-
}
|
|
403
|
-
// Extend every fetched virtual output and upsert into the cache.
|
|
404
|
-
const fetchedExtended = [];
|
|
405
|
-
for (const vtxo of allVtxos) {
|
|
406
|
-
const extended = extendWithScript(vtxo);
|
|
407
|
-
if (extended)
|
|
408
|
-
fetchedExtended.push(extended);
|
|
409
|
-
}
|
|
410
|
-
// Save virtual outputs first, then advance cursors only on success.
|
|
411
|
-
const cutoff = cursorCutoff(requestStartedAt);
|
|
412
|
-
await this.walletRepository.saveVtxos(address, fetchedExtended);
|
|
413
|
-
await advanceSyncCursors(this.walletRepository, Object.fromEntries(allScripts.map((s) => [s, cutoff])));
|
|
414
|
-
// Delta-sync reconciliation: full re-fetch for delta scripts.
|
|
415
|
-
//
|
|
416
|
-
// The delta fetch (above) only returns virtual outputs changed after the
|
|
417
|
-
// cursor, so it can miss preconfirmed virtual outputs that were consumed
|
|
418
|
-
// by a round between syncs. Rather than layering targeted
|
|
419
|
-
// queries (pendingOnly, spendableOnly) with pagination guards
|
|
420
|
-
// and set algebra, we perform a single unfiltered re-fetch for
|
|
421
|
-
// delta scripts. This is slightly more data over the wire but
|
|
422
|
-
// gives us complete, authoritative state in one call and keeps
|
|
423
|
-
// the reconciliation logic simple.
|
|
424
|
-
//
|
|
425
|
-
// Any cached non-spent virtual output that is absent from the full
|
|
426
|
-
// result set is marked spent; any virtual output whose state changed
|
|
427
|
-
// (e.g. preconfirmed → settled) is updated in place.
|
|
428
|
-
if (hasDelta) {
|
|
429
|
-
const { vtxos: fullVtxos, page: fullPage } = await this.indexerProvider.getVtxos({
|
|
430
|
-
scripts: deltaScripts,
|
|
431
|
-
});
|
|
432
|
-
// Reconciliation is best-effort: if the response is
|
|
433
|
-
// paginated we don't have a complete picture, so we skip
|
|
434
|
-
// rather than act on partial data. Wallets with enough
|
|
435
|
-
// virtual outputs to exceed a single page rely solely on the
|
|
436
|
-
// cursor-based delta mechanism for state updates.
|
|
437
|
-
const fullSetComplete = !fullPage || fullPage.total <= 1;
|
|
438
|
-
if (fullSetComplete) {
|
|
439
|
-
const fullOutpoints = new Map(fullVtxos.map((v) => [`${v.txid}:${v.vout}`, v]));
|
|
440
|
-
const deltaScriptSet = new Set(deltaScripts);
|
|
441
|
-
const cachedVtxos = await this.walletRepository.getVtxos(address);
|
|
442
|
-
const reconciledExtended = [];
|
|
443
|
-
for (const cached of cachedVtxos) {
|
|
444
|
-
if (!cached.script ||
|
|
445
|
-
!deltaScriptSet.has(cached.script) ||
|
|
446
|
-
cached.isSpent) {
|
|
447
|
-
continue;
|
|
448
|
-
}
|
|
449
|
-
const outpoint = `${cached.txid}:${cached.vout}`;
|
|
450
|
-
const fresh = fullOutpoints.get(outpoint);
|
|
451
|
-
if (!fresh) {
|
|
452
|
-
// Server no longer knows about this virtual output —
|
|
453
|
-
// it was spent between syncs.
|
|
454
|
-
reconciledExtended.push({
|
|
455
|
-
...cached,
|
|
456
|
-
isSpent: true,
|
|
457
|
-
});
|
|
458
|
-
continue;
|
|
459
|
-
}
|
|
460
|
-
const extended = extendWithScript(fresh);
|
|
461
|
-
if (extended &&
|
|
462
|
-
extended.virtualStatus.state !==
|
|
463
|
-
cached.virtualStatus.state) {
|
|
464
|
-
// State transitioned (e.g. preconfirmed →
|
|
465
|
-
// settled) — update the cached entry.
|
|
466
|
-
reconciledExtended.push(extended);
|
|
467
|
-
}
|
|
468
|
-
}
|
|
469
|
-
if (reconciledExtended.length > 0) {
|
|
470
|
-
console.warn(`[ark-sdk] delta sync: reconciled ${reconciledExtended.length} stale VTXO(s) via full re-fetch`);
|
|
471
|
-
await this.walletRepository.saveVtxos(address, reconciledExtended);
|
|
472
|
-
}
|
|
473
|
-
}
|
|
474
|
-
else {
|
|
475
|
-
console.warn("[ark-sdk] delta sync: skipping reconciliation — full re-fetch was paginated");
|
|
476
|
-
}
|
|
477
|
-
}
|
|
478
|
-
return {
|
|
479
|
-
isDelta: hasDelta || bootstrapScripts.length === 0,
|
|
480
|
-
fetchedExtended,
|
|
481
|
-
address,
|
|
482
|
-
};
|
|
483
|
-
}
|
|
484
|
-
/**
|
|
485
|
-
* Clear all virtual output sync cursors, forcing a full re-bootstrap on next sync.
|
|
330
|
+
* Clear the global VTXO sync cursor, forcing a full re-bootstrap on next sync.
|
|
486
331
|
* Useful for recovery after indexer reprocessing or debugging.
|
|
487
332
|
*/
|
|
488
|
-
async
|
|
489
|
-
await
|
|
333
|
+
async clearSyncCursor() {
|
|
334
|
+
await clearSyncCursor(this.walletRepository);
|
|
490
335
|
}
|
|
491
336
|
/**
|
|
492
337
|
* Build a transaction history view for the wallet's boarding address.
|
|
@@ -529,6 +374,7 @@ export class ReadonlyWallet {
|
|
|
529
374
|
createdAt: tx.status.confirmed
|
|
530
375
|
? new Date(tx.status.block_time * 1000)
|
|
531
376
|
: new Date(0),
|
|
377
|
+
script: hex.encode(this.boardingTapscript.pkScript),
|
|
532
378
|
});
|
|
533
379
|
}
|
|
534
380
|
}
|
|
@@ -618,22 +464,41 @@ export class ReadonlyWallet {
|
|
|
618
464
|
await this.indexerProvider?.unsubscribeForScripts(subscriptionId);
|
|
619
465
|
};
|
|
620
466
|
// Handle subscription updates asynchronously without blocking.
|
|
621
|
-
//
|
|
622
|
-
//
|
|
623
|
-
//
|
|
624
|
-
//
|
|
625
|
-
//
|
|
467
|
+
// Subscription covers all wallet scripts (default + delegate) plus
|
|
468
|
+
// any additional registered contracts. Virtual outputs carry a
|
|
469
|
+
// `script` field from the indexer which the contract manager
|
|
470
|
+
// resolves to the owning contract so the extension uses the
|
|
471
|
+
// correct forfeit/intent tapscripts.
|
|
626
472
|
(async () => {
|
|
627
473
|
try {
|
|
474
|
+
const cm = await this.getContractManager();
|
|
628
475
|
for await (const update of subscription) {
|
|
629
|
-
if (update.newVtxos?.length
|
|
630
|
-
update.spentVtxos?.length
|
|
476
|
+
if (update.newVtxos?.length === 0 &&
|
|
477
|
+
update.spentVtxos?.length === 0) {
|
|
478
|
+
continue;
|
|
479
|
+
}
|
|
480
|
+
// Isolate per-update annotation failures (e.g. a VTXO
|
|
481
|
+
// arriving for a contract we haven't registered yet).
|
|
482
|
+
// Without this a single bad update would kill the
|
|
483
|
+
// for-await loop and silently drop every subsequent
|
|
484
|
+
// subscription event for the session.
|
|
485
|
+
try {
|
|
486
|
+
// Default to `[]` so a one-sided update (e.g.
|
|
487
|
+
// only `newVtxos`) doesn't pass `undefined` into
|
|
488
|
+
// annotateVtxos and throw on `.length`.
|
|
489
|
+
const [newVtxos, spentVtxos] = await Promise.all([
|
|
490
|
+
cm.annotateVtxos(update.newVtxos ?? []),
|
|
491
|
+
cm.annotateVtxos(update.spentVtxos ?? []),
|
|
492
|
+
]);
|
|
631
493
|
eventCallback({
|
|
632
494
|
type: "vtxo",
|
|
633
|
-
newVtxos
|
|
634
|
-
spentVtxos
|
|
495
|
+
newVtxos,
|
|
496
|
+
spentVtxos,
|
|
635
497
|
});
|
|
636
498
|
}
|
|
499
|
+
catch (error) {
|
|
500
|
+
console.warn("Dropping subscription update after annotation failed; next sync will reconcile:", error);
|
|
501
|
+
}
|
|
637
502
|
}
|
|
638
503
|
}
|
|
639
504
|
catch (error) {
|
|
@@ -903,11 +768,10 @@ export class Wallet extends ReadonlyWallet {
|
|
|
903
768
|
}
|
|
904
769
|
});
|
|
905
770
|
}
|
|
906
|
-
constructor(identity, network,
|
|
771
|
+
constructor(identity, network, onchainProvider, arkProvider, indexerProvider, arkServerPublicKey, offchainTapscript, boardingTapscript, serverUnrollScript, forfeitOutputScript, forfeitPubkey, dustAmount, walletRepository, contractRepository,
|
|
907
772
|
/** @deprecated Use settlementConfig */
|
|
908
773
|
renewalConfig, delegatorProvider, watcherConfig, settlementConfig) {
|
|
909
774
|
super(identity, network, onchainProvider, indexerProvider, arkServerPublicKey, offchainTapscript, boardingTapscript, dustAmount, walletRepository, contractRepository, delegatorProvider, watcherConfig);
|
|
910
|
-
this.networkName = networkName;
|
|
911
775
|
this.arkProvider = arkProvider;
|
|
912
776
|
this.serverUnrollScript = serverUnrollScript;
|
|
913
777
|
this.forfeitOutputScript = forfeitOutputScript;
|
|
@@ -1027,7 +891,7 @@ export class Wallet extends ReadonlyWallet {
|
|
|
1027
891
|
const forfeitPubkey = hex.decode(setup.info.forfeitPubkey).slice(1);
|
|
1028
892
|
const forfeitAddress = Address(setup.network).decode(setup.info.forfeitAddress);
|
|
1029
893
|
const forfeitOutputScript = OutScript.encode(forfeitAddress);
|
|
1030
|
-
const wallet = new Wallet(config.identity, setup.network, setup.
|
|
894
|
+
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);
|
|
1031
895
|
await wallet.getVtxoManager();
|
|
1032
896
|
return wallet;
|
|
1033
897
|
}
|
|
@@ -1290,7 +1154,7 @@ export class Wallet extends ReadonlyWallet {
|
|
|
1290
1154
|
const abortController = new AbortController();
|
|
1291
1155
|
try {
|
|
1292
1156
|
const stream = this.arkProvider.getEventStream(abortController.signal, topics);
|
|
1293
|
-
const intentId = await this.safeRegisterIntent(intent);
|
|
1157
|
+
const intentId = await this.safeRegisterIntent(intent, params.inputs);
|
|
1294
1158
|
const handler = this.createBatchHandler(intentId, params.inputs, recipients, session);
|
|
1295
1159
|
const commitmentTxid = await Batch.join(stream, handler, {
|
|
1296
1160
|
abortController,
|
|
@@ -1303,8 +1167,16 @@ export class Wallet extends ReadonlyWallet {
|
|
|
1303
1167
|
return commitmentTxid;
|
|
1304
1168
|
}
|
|
1305
1169
|
catch (error) {
|
|
1306
|
-
// delete the intent to not be stuck in the queue
|
|
1307
|
-
|
|
1170
|
+
// delete the intent to not be stuck in the queue. If deletion fails
|
|
1171
|
+
// the intent stays on the server and the next settle will hit
|
|
1172
|
+
// "duplicated input" in safeRegisterIntent — surface the failure
|
|
1173
|
+
// rather than silently swallowing it.
|
|
1174
|
+
const inputIds = params.inputs
|
|
1175
|
+
.map((i) => `${i.txid}:${i.vout}`)
|
|
1176
|
+
.join(",");
|
|
1177
|
+
await this.arkProvider.deleteIntent(deleteIntent).catch((e) => {
|
|
1178
|
+
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);
|
|
1179
|
+
});
|
|
1308
1180
|
throw error;
|
|
1309
1181
|
}
|
|
1310
1182
|
finally {
|
|
@@ -1494,7 +1366,7 @@ export class Wallet extends ReadonlyWallet {
|
|
|
1494
1366
|
},
|
|
1495
1367
|
};
|
|
1496
1368
|
}
|
|
1497
|
-
async safeRegisterIntent(intent) {
|
|
1369
|
+
async safeRegisterIntent(intent, inputs) {
|
|
1498
1370
|
try {
|
|
1499
1371
|
return await this.arkProvider.registerIntent(intent);
|
|
1500
1372
|
}
|
|
@@ -1503,11 +1375,13 @@ export class Wallet extends ReadonlyWallet {
|
|
|
1503
1375
|
if (error instanceof ArkError &&
|
|
1504
1376
|
error.code === 0 &&
|
|
1505
1377
|
error.message.includes("duplicated input")) {
|
|
1506
|
-
//
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
1378
|
+
// Clear any queued intent spending these exact inputs. The
|
|
1379
|
+
// previous implementation signed a proof over getVtxos() only,
|
|
1380
|
+
// which misses boarding UTXOs — the most common trigger for
|
|
1381
|
+
// "duplicated input" on the auto-settle path. Signing the
|
|
1382
|
+
// caller's own inputs keeps the proof surgical and correct
|
|
1383
|
+
// regardless of whether the stuck input is a VTXO or boarding.
|
|
1384
|
+
const deleteIntent = await this.makeDeleteIntentSignature(inputs);
|
|
1511
1385
|
await this.arkProvider.deleteIntent(deleteIntent);
|
|
1512
1386
|
// try again
|
|
1513
1387
|
return this.arkProvider.registerIntent(intent);
|
|
@@ -1575,9 +1449,7 @@ export class Wallet extends ReadonlyWallet {
|
|
|
1575
1449
|
scripts: allScripts,
|
|
1576
1450
|
});
|
|
1577
1451
|
for (const vtxo of fetchedVtxos) {
|
|
1578
|
-
const vtxoScript = vtxo.script
|
|
1579
|
-
? scriptMap.get(vtxo.script)
|
|
1580
|
-
: undefined;
|
|
1452
|
+
const vtxoScript = scriptMap.get(vtxo.script);
|
|
1581
1453
|
if (!vtxoScript)
|
|
1582
1454
|
continue;
|
|
1583
1455
|
if (vtxo.virtualStatus.state === "swept" ||
|
|
@@ -1880,8 +1752,9 @@ export class Wallet extends ReadonlyWallet {
|
|
|
1880
1752
|
console.warn(`updateDbAfterOffchainTx: inputs length (${inputs.length}) differs from signedCheckpointTxs length (${signedCheckpointTxs.length})`);
|
|
1881
1753
|
}
|
|
1882
1754
|
const safeLength = Math.min(inputs.length, signedCheckpointTxs.length);
|
|
1883
|
-
|
|
1884
|
-
|
|
1755
|
+
const cm = await this.getContractManager();
|
|
1756
|
+
const annotatedInputs = await cm.annotateVtxos(inputs);
|
|
1757
|
+
for (const [inputIndex, vtxo] of annotatedInputs.entries()) {
|
|
1885
1758
|
if (inputIndex < safeLength &&
|
|
1886
1759
|
signedCheckpointTxs[inputIndex]) {
|
|
1887
1760
|
const checkpoint = Transaction.fromPSBT(base64.decode(signedCheckpointTxs[inputIndex]));
|
|
@@ -1941,6 +1814,7 @@ export class Wallet extends ReadonlyWallet {
|
|
|
1941
1814
|
confirmed: false,
|
|
1942
1815
|
},
|
|
1943
1816
|
assets: changeAssets,
|
|
1817
|
+
script: hex.encode(this.offchainTapscript.pkScript),
|
|
1944
1818
|
};
|
|
1945
1819
|
}
|
|
1946
1820
|
await this.walletRepository.saveVtxos(addr, changeVtxo ? [...spentVtxos, changeVtxo] : spentVtxos);
|
|
@@ -1971,10 +1845,14 @@ export class Wallet extends ReadonlyWallet {
|
|
|
1971
1845
|
const inputArkTxIds = new Set();
|
|
1972
1846
|
const boardingUtxoToRemove = new Set();
|
|
1973
1847
|
const isVtxo = (input) => "virtualStatus" in input;
|
|
1848
|
+
const vtxoInputs = inputs.filter(isVtxo);
|
|
1849
|
+
const cm = await this.getContractManager();
|
|
1850
|
+
const annotatedVtxos = await cm.annotateVtxos(vtxoInputs);
|
|
1851
|
+
const annotatedByKey = new Map(annotatedVtxos.map((v) => [`${v.txid}:${v.vout}`, v]));
|
|
1974
1852
|
for (const input of inputs) {
|
|
1975
1853
|
if (isVtxo(input)) {
|
|
1976
1854
|
// virtual output = mark it settled
|
|
1977
|
-
const vtxo =
|
|
1855
|
+
const vtxo = annotatedByKey.get(`${input.txid}:${input.vout}`);
|
|
1978
1856
|
if (vtxo.arkTxId) {
|
|
1979
1857
|
inputArkTxIds.add(vtxo.arkTxId);
|
|
1980
1858
|
}
|
|
@@ -2060,7 +1938,7 @@ export function selectVirtualCoins(coins, targetAmount) {
|
|
|
2060
1938
|
*/
|
|
2061
1939
|
export async function waitForIncomingFunds(wallet) {
|
|
2062
1940
|
let stopFunc;
|
|
2063
|
-
|
|
1941
|
+
return new Promise((resolve) => {
|
|
2064
1942
|
wallet
|
|
2065
1943
|
.notifyIncomingFunds((coins) => {
|
|
2066
1944
|
resolve(coins);
|
|
@@ -2071,5 +1949,4 @@ export async function waitForIncomingFunds(wallet) {
|
|
|
2071
1949
|
stopFunc = stop;
|
|
2072
1950
|
});
|
|
2073
1951
|
});
|
|
2074
|
-
return promise;
|
|
2075
1952
|
}
|
|
@@ -5,27 +5,24 @@ export const CONTRACT_POLL_TASK_TYPE = "contract-poll";
|
|
|
5
5
|
*
|
|
6
6
|
* Replicates the polling subset of @see ContractManager.initialize:
|
|
7
7
|
* 1. Load all contracts from the contract repository.
|
|
8
|
-
* 2.
|
|
9
|
-
* 3.
|
|
10
|
-
* 4.
|
|
11
|
-
*
|
|
8
|
+
* 2. Paginated fetch of every VTXO (including spent) from the indexer.
|
|
9
|
+
* 3. Extend each VTXO with tapscript data.
|
|
10
|
+
* 4. Save to the wallet repository.
|
|
11
|
+
*
|
|
12
|
+
* NOTE: the indexer query deliberately omits `spendableOnly`. Every
|
|
13
|
+
* repository implements `saveVtxos` as an upsert with no batch delete,
|
|
14
|
+
* so filtering to spendable-only would leave VTXOs that became spent
|
|
15
|
+
* between polls marked as spendable forever. Fetching the full set lets
|
|
16
|
+
* the upsert overwrite stale records with their latest state.
|
|
12
17
|
*/
|
|
13
18
|
export const contractPollProcessor = {
|
|
14
19
|
taskType: CONTRACT_POLL_TASK_TYPE,
|
|
15
20
|
async execute(item, deps) {
|
|
16
21
|
const { contractRepository, walletRepository, indexerProvider, extendVtxo, } = deps;
|
|
17
22
|
const contracts = await contractRepository.getContracts();
|
|
18
|
-
const now = Date.now();
|
|
19
23
|
let contractsProcessed = 0;
|
|
20
24
|
let vtxosSaved = 0;
|
|
21
25
|
for (const contract of contracts) {
|
|
22
|
-
// Mark expired active contracts as inactive
|
|
23
|
-
if (contract.state === "active" &&
|
|
24
|
-
contract.expiresAt &&
|
|
25
|
-
contract.expiresAt <= now) {
|
|
26
|
-
contract.state = "inactive";
|
|
27
|
-
await contractRepository.saveContract(contract);
|
|
28
|
-
}
|
|
29
26
|
// Paginated fetch of spendable virtual outputs
|
|
30
27
|
const pageSize = 100;
|
|
31
28
|
let pageIndex = 0;
|
|
@@ -34,7 +31,6 @@ export const contractPollProcessor = {
|
|
|
34
31
|
while (hasMore) {
|
|
35
32
|
const { vtxos, page } = await indexerProvider.getVtxos({
|
|
36
33
|
scripts: [contract.script],
|
|
37
|
-
spendableOnly: true,
|
|
38
34
|
pageIndex,
|
|
39
35
|
pageSize,
|
|
40
36
|
});
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { getRandomId,
|
|
1
|
+
import { getRandomId, extendVirtualCoinForContract } from '../../wallet/utils.js';
|
|
2
2
|
/**
|
|
3
3
|
* Run all pending tasks from the queue through matching processors.
|
|
4
4
|
*
|
|
@@ -62,17 +62,8 @@ export async function runTasks(queue, processors, deps) {
|
|
|
62
62
|
* can build deps without depending on Expo.
|
|
63
63
|
*/
|
|
64
64
|
export function createTaskDependencies(options) {
|
|
65
|
-
const { walletRepository, contractRepository, indexerProvider, arkProvider, offchainTapscript, } = options;
|
|
66
65
|
return {
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
indexerProvider,
|
|
70
|
-
arkProvider,
|
|
71
|
-
extendVtxo: (vtxo, contract) => {
|
|
72
|
-
if (contract) {
|
|
73
|
-
return extendVtxoFromContract(vtxo, contract);
|
|
74
|
-
}
|
|
75
|
-
return extendVirtualCoin({ offchainTapscript }, vtxo);
|
|
76
|
-
},
|
|
66
|
+
...options,
|
|
67
|
+
extendVtxo: (vtxo, contract) => extendVirtualCoinForContract(vtxo, contract),
|
|
77
68
|
};
|
|
78
69
|
}
|
|
@@ -74,7 +74,6 @@ export declare function decodeArkContract(encoded: string): ParsedArkContract;
|
|
|
74
74
|
export declare function contractFromArkContract(encoded: string, options?: {
|
|
75
75
|
label?: string;
|
|
76
76
|
state?: "active" | "inactive";
|
|
77
|
-
expiresAt?: number;
|
|
78
77
|
metadata?: Record<string, unknown>;
|
|
79
78
|
}): Omit<Contract, "script" | "address"> & {
|
|
80
79
|
script?: string;
|
|
@@ -92,7 +91,6 @@ export declare function contractFromArkContract(encoded: string, options?: {
|
|
|
92
91
|
export declare function contractFromArkContractWithAddress(encoded: string, serverPubKey: Uint8Array, addressPrefix: string, options?: {
|
|
93
92
|
label?: string;
|
|
94
93
|
state?: "active" | "inactive";
|
|
95
|
-
expiresAt?: number;
|
|
96
94
|
metadata?: Record<string, unknown>;
|
|
97
95
|
}): Contract;
|
|
98
96
|
/**
|
|
@@ -2,7 +2,7 @@ import { IndexerProvider } from "../providers/indexer";
|
|
|
2
2
|
import { WalletRepository } from "../repositories/walletRepository";
|
|
3
3
|
import { Contract, ContractEventCallback, ContractState, ContractWithVtxos, GetContractsFilter, PathSelection } from "./types";
|
|
4
4
|
import { ContractWatcherConfig } from "./contractWatcher";
|
|
5
|
-
import { VirtualCoin } from "../wallet";
|
|
5
|
+
import { ExtendedVirtualCoin, VirtualCoin } from "../wallet";
|
|
6
6
|
import { ContractRepository } from "../repositories";
|
|
7
7
|
export type RefreshVtxosOptions = {
|
|
8
8
|
scripts?: string[];
|
|
@@ -36,6 +36,19 @@ export interface IContractManager extends Disposable {
|
|
|
36
36
|
* If no filter is provided, returns all contracts with their virtual outputs.
|
|
37
37
|
*/
|
|
38
38
|
getContractsWithVtxos(filter?: GetContractsFilter): Promise<ContractWithVtxos[]>;
|
|
39
|
+
/**
|
|
40
|
+
* Stamp raw virtual outputs with the correct per-contract tapscripts
|
|
41
|
+
* (forfeit, intent, tap tree).
|
|
42
|
+
*
|
|
43
|
+
* Resolves each vtxo's `script` to its owning contract via the contract
|
|
44
|
+
* repository and attaches the matching tapscripts. Throws when any vtxo
|
|
45
|
+
* references a script with no registered contract — callers are expected
|
|
46
|
+
* to register the contract before asking for annotation. This is the
|
|
47
|
+
* single shared path that replaces scattered `extendVirtualCoin*` calls
|
|
48
|
+
* in wallet/handler code, and keeps the wallet from silently stamping the
|
|
49
|
+
* default tapscript onto a non-default vtxo.
|
|
50
|
+
*/
|
|
51
|
+
annotateVtxos(vtxos: VirtualCoin[]): Promise<ExtendedVirtualCoin[]>;
|
|
39
52
|
/**
|
|
40
53
|
* Update mutable contract fields.
|
|
41
54
|
*
|
|
@@ -135,7 +148,7 @@ export type CreateContractParams = Omit<Contract, "createdAt" | "state"> & {
|
|
|
135
148
|
* - Create and persist contracts
|
|
136
149
|
* - Query stored contracts (optionally with their virtual outputs)
|
|
137
150
|
* - Provide spendable path selection for a contract
|
|
138
|
-
* - Emit contract-related events (virtual output received/spent
|
|
151
|
+
* - Emit contract-related events (virtual output received/spent, connection reset)
|
|
139
152
|
*
|
|
140
153
|
* Notes:
|
|
141
154
|
* - Implementations typically start watching automatically during initialization
|
|
@@ -181,7 +194,6 @@ export declare class ContractManager implements IContractManager {
|
|
|
181
194
|
private initialized;
|
|
182
195
|
private eventCallbacks;
|
|
183
196
|
private stopWatcherFn?;
|
|
184
|
-
private syncVtxosCallInflight?;
|
|
185
197
|
private constructor();
|
|
186
198
|
/**
|
|
187
199
|
* Static factory method for creating a new ContractManager.
|
|
@@ -194,6 +206,17 @@ export declare class ContractManager implements IContractManager {
|
|
|
194
206
|
*/
|
|
195
207
|
static create(config: ContractManagerConfig): Promise<ContractManager>;
|
|
196
208
|
private initialize;
|
|
209
|
+
/**
|
|
210
|
+
* Delta-sync the full watched set and reconcile the pending frontier.
|
|
211
|
+
*
|
|
212
|
+
* Shared recovery path used on initial boot and after a subscription
|
|
213
|
+
* reconnect. `syncContracts({})` scopes to the current watched set
|
|
214
|
+
* (see {@link ContractWatcher.getWatchedContracts}), uses the
|
|
215
|
+
* cursor-derived delta window, and advances the cursor on success.
|
|
216
|
+
* `reconcilePendingFrontier` catches not-yet-finalized virtual
|
|
217
|
+
* outputs that could sit outside any delta window.
|
|
218
|
+
*/
|
|
219
|
+
private reconcileWatched;
|
|
197
220
|
/**
|
|
198
221
|
* Create and register a new contract.
|
|
199
222
|
*
|
|
@@ -218,6 +241,7 @@ export declare class ContractManager implements IContractManager {
|
|
|
218
241
|
*/
|
|
219
242
|
getContracts(filter?: GetContractsFilter): Promise<Contract[]>;
|
|
220
243
|
getContractsWithVtxos(filter?: GetContractsFilter, pageSize?: number): Promise<ContractWithVtxos[]>;
|
|
244
|
+
annotateVtxos(vtxos: VirtualCoin[]): Promise<ExtendedVirtualCoin[]>;
|
|
221
245
|
private buildContractsDbFilter;
|
|
222
246
|
/**
|
|
223
247
|
* Update a contract.
|
|
@@ -281,8 +305,9 @@ export declare class ContractManager implements IContractManager {
|
|
|
281
305
|
/**
|
|
282
306
|
* Force refresh virtual outputs from the indexer.
|
|
283
307
|
*
|
|
284
|
-
* Without options,
|
|
308
|
+
* Without options, re-fetches every contract and advances the global cursor.
|
|
285
309
|
* With options, narrows the refresh to specific scripts and/or a time window.
|
|
310
|
+
* Subset refreshes (scripts filter) intentionally do not advance the cursor.
|
|
286
311
|
*/
|
|
287
312
|
refreshVtxos(opts?: RefreshVtxosOptions): Promise<void>;
|
|
288
313
|
/**
|
|
@@ -299,11 +324,16 @@ export declare class ContractManager implements IContractManager {
|
|
|
299
324
|
private handleContractEvent;
|
|
300
325
|
private getVtxosForContracts;
|
|
301
326
|
/**
|
|
302
|
-
*
|
|
303
|
-
*
|
|
304
|
-
*
|
|
327
|
+
* Sync virtual outputs for the given contracts against the indexer.
|
|
328
|
+
*
|
|
329
|
+
* When `options.contracts` is omitted the sync covers the full
|
|
330
|
+
* watched set (active contracts plus any inactive contracts still
|
|
331
|
+
* holding cached VTXOs) and the global cursor is advanced on
|
|
332
|
+
* success. Passing an explicit subset leaves the cursor alone so a
|
|
333
|
+
* narrow poll can't hide data that other contracts still need to
|
|
334
|
+
* pick up.
|
|
305
335
|
*/
|
|
306
|
-
private
|
|
336
|
+
private syncContracts;
|
|
307
337
|
/**
|
|
308
338
|
* Fetch all pending (unfinalized) virtual outputs and upsert them into the
|
|
309
339
|
* repository. This catches virtual outputs whose state changed outside the delta
|
|
@@ -312,7 +342,6 @@ export declare class ContractManager implements IContractManager {
|
|
|
312
342
|
private reconcilePendingFrontier;
|
|
313
343
|
private fetchContractVxosFromIndexer;
|
|
314
344
|
private fetchContractVtxosBulk;
|
|
315
|
-
private fetchContractVtxosPaginated;
|
|
316
345
|
/**
|
|
317
346
|
* Dispose of the ContractManager and release all resources.
|
|
318
347
|
*
|