@arkade-os/sdk 0.4.17 → 0.4.19
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/providers/ark.js +36 -33
- 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 +222 -40
- package/dist/cjs/wallet/wallet.js +149 -211
- 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/providers/ark.js +36 -33
- 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 +222 -40
- package/dist/esm/wallet/wallet.js +152 -214
- 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 +29 -6
- package/dist/types/wallet/wallet.d.ts +8 -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.
|
|
@@ -63,6 +63,12 @@ export class ReadonlyWallet {
|
|
|
63
63
|
this.walletRepository = walletRepository;
|
|
64
64
|
this.contractRepository = contractRepository;
|
|
65
65
|
this.delegatorProvider = delegatorProvider;
|
|
66
|
+
// Outpoints ("txid:vout") committed to an in-flight settle/send. Filtered
|
|
67
|
+
// from getVtxos() so concurrent callers (UI, VtxoManager auto-renewal,
|
|
68
|
+
// another send/settle racing the _txLock) can't reselect coins that are
|
|
69
|
+
// already on their way out. The set is in-memory only: a process crash
|
|
70
|
+
// clears it, and a stale entry only hides a VTXO (never spends one).
|
|
71
|
+
this._pendingSpendOutpoints = new Set();
|
|
66
72
|
// Guard: detect identity/server network mismatch for descriptor-based identities.
|
|
67
73
|
// This duplicates the check in setupWalletConfig() so that subclasses
|
|
68
74
|
// bypassing the factory still get the safety net.
|
|
@@ -299,8 +305,13 @@ export class ReadonlyWallet {
|
|
|
299
305
|
async getVtxos(filter) {
|
|
300
306
|
const f = filter ?? { withRecoverable: true, withUnrolled: false };
|
|
301
307
|
const contractManager = await this.getContractManager();
|
|
302
|
-
const
|
|
303
|
-
return
|
|
308
|
+
const vtxos = await contractManager.getContractsWithVtxos();
|
|
309
|
+
return vtxos
|
|
310
|
+
.flatMap((_) => _.vtxos)
|
|
311
|
+
.filter((vtxo) => {
|
|
312
|
+
if (this._pendingSpendOutpoints.has(`${vtxo.txid}:${vtxo.vout}`)) {
|
|
313
|
+
return false;
|
|
314
|
+
}
|
|
304
315
|
if (isSpendable(vtxo)) {
|
|
305
316
|
if (!f.withRecoverable &&
|
|
306
317
|
(isRecoverable(vtxo) || isExpired(vtxo))) {
|
|
@@ -309,17 +320,15 @@ export class ReadonlyWallet {
|
|
|
309
320
|
return true;
|
|
310
321
|
}
|
|
311
322
|
return !!(f.withUnrolled && vtxo.isUnrolled);
|
|
312
|
-
})
|
|
323
|
+
});
|
|
313
324
|
}
|
|
314
325
|
/**
|
|
315
326
|
* Return wallet transaction history derived from Arkade state and boarding transactions.
|
|
316
327
|
*/
|
|
317
328
|
async getTransactionHistory() {
|
|
318
|
-
|
|
319
|
-
const
|
|
320
|
-
const allVtxos =
|
|
321
|
-
? await this.walletRepository.getVtxos(address)
|
|
322
|
-
: fetchedExtended;
|
|
329
|
+
const contractManager = await this.getContractManager();
|
|
330
|
+
const response = await contractManager.getContractsWithVtxos();
|
|
331
|
+
const allVtxos = response.flatMap((_) => _.vtxos);
|
|
323
332
|
const { boardingTxs, commitmentsToIgnore } = await this.getBoardingTxs();
|
|
324
333
|
const getTxCreatedAt = (txid) => this.indexerProvider
|
|
325
334
|
.getVtxos({ outpoints: [{ txid, vout: 0 }] })
|
|
@@ -327,166 +336,11 @@ export class ReadonlyWallet {
|
|
|
327
336
|
return buildTransactionHistory(allVtxos, boardingTxs, commitmentsToIgnore, getTxCreatedAt);
|
|
328
337
|
}
|
|
329
338
|
/**
|
|
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.
|
|
339
|
+
* Clear the global VTXO sync cursor, forcing a full re-bootstrap on next sync.
|
|
486
340
|
* Useful for recovery after indexer reprocessing or debugging.
|
|
487
341
|
*/
|
|
488
|
-
async
|
|
489
|
-
await
|
|
342
|
+
async clearSyncCursor() {
|
|
343
|
+
await clearSyncCursor(this.walletRepository);
|
|
490
344
|
}
|
|
491
345
|
/**
|
|
492
346
|
* Build a transaction history view for the wallet's boarding address.
|
|
@@ -529,6 +383,7 @@ export class ReadonlyWallet {
|
|
|
529
383
|
createdAt: tx.status.confirmed
|
|
530
384
|
? new Date(tx.status.block_time * 1000)
|
|
531
385
|
: new Date(0),
|
|
386
|
+
script: hex.encode(this.boardingTapscript.pkScript),
|
|
532
387
|
});
|
|
533
388
|
}
|
|
534
389
|
}
|
|
@@ -618,22 +473,41 @@ export class ReadonlyWallet {
|
|
|
618
473
|
await this.indexerProvider?.unsubscribeForScripts(subscriptionId);
|
|
619
474
|
};
|
|
620
475
|
// Handle subscription updates asynchronously without blocking.
|
|
621
|
-
//
|
|
622
|
-
//
|
|
623
|
-
//
|
|
624
|
-
//
|
|
625
|
-
//
|
|
476
|
+
// Subscription covers all wallet scripts (default + delegate) plus
|
|
477
|
+
// any additional registered contracts. Virtual outputs carry a
|
|
478
|
+
// `script` field from the indexer which the contract manager
|
|
479
|
+
// resolves to the owning contract so the extension uses the
|
|
480
|
+
// correct forfeit/intent tapscripts.
|
|
626
481
|
(async () => {
|
|
627
482
|
try {
|
|
483
|
+
const cm = await this.getContractManager();
|
|
628
484
|
for await (const update of subscription) {
|
|
629
|
-
if (update.newVtxos?.length
|
|
630
|
-
update.spentVtxos?.length
|
|
485
|
+
if (update.newVtxos?.length === 0 &&
|
|
486
|
+
update.spentVtxos?.length === 0) {
|
|
487
|
+
continue;
|
|
488
|
+
}
|
|
489
|
+
// Isolate per-update annotation failures (e.g. a VTXO
|
|
490
|
+
// arriving for a contract we haven't registered yet).
|
|
491
|
+
// Without this a single bad update would kill the
|
|
492
|
+
// for-await loop and silently drop every subsequent
|
|
493
|
+
// subscription event for the session.
|
|
494
|
+
try {
|
|
495
|
+
// Default to `[]` so a one-sided update (e.g.
|
|
496
|
+
// only `newVtxos`) doesn't pass `undefined` into
|
|
497
|
+
// annotateVtxos and throw on `.length`.
|
|
498
|
+
const [newVtxos, spentVtxos] = await Promise.all([
|
|
499
|
+
cm.annotateVtxos(update.newVtxos ?? []),
|
|
500
|
+
cm.annotateVtxos(update.spentVtxos ?? []),
|
|
501
|
+
]);
|
|
631
502
|
eventCallback({
|
|
632
503
|
type: "vtxo",
|
|
633
|
-
newVtxos
|
|
634
|
-
spentVtxos
|
|
504
|
+
newVtxos,
|
|
505
|
+
spentVtxos,
|
|
635
506
|
});
|
|
636
507
|
}
|
|
508
|
+
catch (error) {
|
|
509
|
+
console.warn("Dropping subscription update after annotation failed; next sync will reconcile:", error);
|
|
510
|
+
}
|
|
637
511
|
}
|
|
638
512
|
}
|
|
639
513
|
catch (error) {
|
|
@@ -889,6 +763,20 @@ export class ReadonlyWallet {
|
|
|
889
763
|
* ```
|
|
890
764
|
*/
|
|
891
765
|
export class Wallet extends ReadonlyWallet {
|
|
766
|
+
_addPendingSpends(inputs) {
|
|
767
|
+
for (const input of inputs) {
|
|
768
|
+
if ("virtualStatus" in input) {
|
|
769
|
+
this._pendingSpendOutpoints.add(`${input.txid}:${input.vout}`);
|
|
770
|
+
}
|
|
771
|
+
}
|
|
772
|
+
}
|
|
773
|
+
_removePendingSpends(inputs) {
|
|
774
|
+
for (const input of inputs) {
|
|
775
|
+
if ("virtualStatus" in input) {
|
|
776
|
+
this._pendingSpendOutpoints.delete(`${input.txid}:${input.vout}`);
|
|
777
|
+
}
|
|
778
|
+
}
|
|
779
|
+
}
|
|
892
780
|
_withTxLock(fn) {
|
|
893
781
|
let release;
|
|
894
782
|
const lock = new Promise((r) => (release = r));
|
|
@@ -903,11 +791,10 @@ export class Wallet extends ReadonlyWallet {
|
|
|
903
791
|
}
|
|
904
792
|
});
|
|
905
793
|
}
|
|
906
|
-
constructor(identity, network,
|
|
794
|
+
constructor(identity, network, onchainProvider, arkProvider, indexerProvider, arkServerPublicKey, offchainTapscript, boardingTapscript, serverUnrollScript, forfeitOutputScript, forfeitPubkey, dustAmount, walletRepository, contractRepository,
|
|
907
795
|
/** @deprecated Use settlementConfig */
|
|
908
796
|
renewalConfig, delegatorProvider, watcherConfig, settlementConfig) {
|
|
909
797
|
super(identity, network, onchainProvider, indexerProvider, arkServerPublicKey, offchainTapscript, boardingTapscript, dustAmount, walletRepository, contractRepository, delegatorProvider, watcherConfig);
|
|
910
|
-
this.networkName = networkName;
|
|
911
798
|
this.arkProvider = arkProvider;
|
|
912
799
|
this.serverUnrollScript = serverUnrollScript;
|
|
913
800
|
this.forfeitOutputScript = forfeitOutputScript;
|
|
@@ -1027,7 +914,7 @@ export class Wallet extends ReadonlyWallet {
|
|
|
1027
914
|
const forfeitPubkey = hex.decode(setup.info.forfeitPubkey).slice(1);
|
|
1028
915
|
const forfeitAddress = Address(setup.network).decode(setup.info.forfeitAddress);
|
|
1029
916
|
const forfeitOutputScript = OutScript.encode(forfeitAddress);
|
|
1030
|
-
const wallet = new Wallet(config.identity, setup.network, setup.
|
|
917
|
+
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
918
|
await wallet.getVtxoManager();
|
|
1032
919
|
return wallet;
|
|
1033
920
|
}
|
|
@@ -1105,9 +992,15 @@ export class Wallet extends ReadonlyWallet {
|
|
|
1105
992
|
amount: BigInt(selected.changeAmount),
|
|
1106
993
|
});
|
|
1107
994
|
}
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
995
|
+
this._addPendingSpends(selected.inputs);
|
|
996
|
+
try {
|
|
997
|
+
const { arkTxid, signedCheckpointTxs } = await this.buildAndSubmitOffchainTx(selected.inputs, outputs);
|
|
998
|
+
await this.updateDbAfterOffchainTx(selected.inputs, arkTxid, signedCheckpointTxs, params.amount, selected.changeAmount, selected.changeAmount > 0n ? outputs.length - 1 : 0);
|
|
999
|
+
return arkTxid;
|
|
1000
|
+
}
|
|
1001
|
+
finally {
|
|
1002
|
+
this._removePendingSpends(selected.inputs);
|
|
1003
|
+
}
|
|
1111
1004
|
});
|
|
1112
1005
|
}
|
|
1113
1006
|
return this.send({
|
|
@@ -1153,7 +1046,8 @@ export class Wallet extends ReadonlyWallet {
|
|
|
1153
1046
|
const tip = await this.onchainProvider.getChainTip();
|
|
1154
1047
|
chainTipHeight = tip.height;
|
|
1155
1048
|
}
|
|
1156
|
-
const boardingUtxos = (await this.getBoardingUtxos()).filter((utxo) =>
|
|
1049
|
+
const boardingUtxos = (await this.getBoardingUtxos()).filter((utxo) => utxo.status.confirmed &&
|
|
1050
|
+
!hasBoardingTxExpired(utxo, boardingTimelock, chainTipHeight));
|
|
1157
1051
|
const filteredBoardingUtxos = [];
|
|
1158
1052
|
for (const utxo of boardingUtxos) {
|
|
1159
1053
|
const inputFee = estimator.evalOnchainInput({
|
|
@@ -1288,11 +1182,29 @@ export class Wallet extends ReadonlyWallet {
|
|
|
1288
1182
|
...params.inputs.map((input) => `${input.txid}:${input.vout}`),
|
|
1289
1183
|
];
|
|
1290
1184
|
const abortController = new AbortController();
|
|
1185
|
+
let stream;
|
|
1186
|
+
// Optimistically hide these inputs from concurrent getVtxos() callers
|
|
1187
|
+
// while the settlement is in flight. Set before safeRegisterIntent so
|
|
1188
|
+
// there's no window between intent registration and coin-visibility.
|
|
1189
|
+
this._addPendingSpends(params.inputs);
|
|
1291
1190
|
try {
|
|
1292
|
-
|
|
1293
|
-
|
|
1191
|
+
stream = this.arkProvider.getEventStream(abortController.signal, topics);
|
|
1192
|
+
// Prime the iterator so the provider opens the SSE subscription
|
|
1193
|
+
// before safeRegisterIntent can trigger server-side batch events.
|
|
1194
|
+
const firstNext = stream.next();
|
|
1195
|
+
// If settle exits before Batch.join consumes the primed result,
|
|
1196
|
+
// keep the orphaned promise from surfacing as an unhandled rejection.
|
|
1197
|
+
void firstNext.catch(() => { });
|
|
1198
|
+
const primedStream = (async function* () {
|
|
1199
|
+
const first = await firstNext;
|
|
1200
|
+
if (!first.done) {
|
|
1201
|
+
yield first.value;
|
|
1202
|
+
}
|
|
1203
|
+
yield* stream;
|
|
1204
|
+
})();
|
|
1205
|
+
const intentId = await this.safeRegisterIntent(intent, params.inputs);
|
|
1294
1206
|
const handler = this.createBatchHandler(intentId, params.inputs, recipients, session);
|
|
1295
|
-
const commitmentTxid = await Batch.join(
|
|
1207
|
+
const commitmentTxid = await Batch.join(primedStream, handler, {
|
|
1296
1208
|
abortController,
|
|
1297
1209
|
skipVtxoTreeSigning: !hasOffchainOutputs,
|
|
1298
1210
|
eventCallback: eventCallback
|
|
@@ -1303,28 +1215,41 @@ export class Wallet extends ReadonlyWallet {
|
|
|
1303
1215
|
return commitmentTxid;
|
|
1304
1216
|
}
|
|
1305
1217
|
catch (error) {
|
|
1306
|
-
// delete the intent to not be stuck in the queue
|
|
1307
|
-
|
|
1218
|
+
// delete the intent to not be stuck in the queue. If deletion fails
|
|
1219
|
+
// the intent stays on the server and the next settle will hit
|
|
1220
|
+
// "duplicated input" in safeRegisterIntent — surface the failure
|
|
1221
|
+
// rather than silently swallowing it.
|
|
1222
|
+
const inputIds = params.inputs
|
|
1223
|
+
.map((i) => `${i.txid}:${i.vout}`)
|
|
1224
|
+
.join(",");
|
|
1225
|
+
await this.arkProvider.deleteIntent(deleteIntent).catch((e) => {
|
|
1226
|
+
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);
|
|
1227
|
+
});
|
|
1308
1228
|
throw error;
|
|
1309
1229
|
}
|
|
1310
1230
|
finally {
|
|
1311
|
-
//
|
|
1231
|
+
// Clear state first so a synchronous handler firing from abort()
|
|
1232
|
+
// never observes a stale pending-spend set.
|
|
1233
|
+
this._removePendingSpends(params.inputs);
|
|
1234
|
+
// close the stream — abort() fires the in-body handler if the
|
|
1235
|
+
// generator has started iterating; return() also releases the
|
|
1236
|
+
// eager resource if the body is still suspended or never ran
|
|
1237
|
+
// (e.g. safeRegisterIntent threw before Batch.join was called).
|
|
1312
1238
|
abortController.abort();
|
|
1239
|
+
await stream?.return?.().catch(() => { });
|
|
1313
1240
|
}
|
|
1314
1241
|
}
|
|
1315
1242
|
async handleSettlementFinalizationEvent(event, inputs, forfeitOutputScript, connectorsGraph) {
|
|
1316
1243
|
// the signed forfeits transactions to submit
|
|
1317
1244
|
const signedForfeits = [];
|
|
1318
|
-
const
|
|
1245
|
+
const isVtxo = (input) => "virtualStatus" in input;
|
|
1319
1246
|
let settlementPsbt = Transaction.fromPSBT(base64.decode(event.commitmentTx));
|
|
1320
1247
|
let hasBoardingUtxos = false;
|
|
1321
1248
|
let connectorIndex = 0;
|
|
1322
1249
|
const connectorsLeaves = connectorsGraph?.leaves() || [];
|
|
1323
1250
|
for (const input of inputs) {
|
|
1324
|
-
// check if the input is an offchain "virtual" coin
|
|
1325
|
-
const vtxo = vtxos.find((vtxo) => vtxo.txid === input.txid && vtxo.vout === input.vout);
|
|
1326
1251
|
// boarding input, we need to sign the settlement tx
|
|
1327
|
-
if (!
|
|
1252
|
+
if (!isVtxo(input)) {
|
|
1328
1253
|
for (let i = 0; i < settlementPsbt.inputsLength; i++) {
|
|
1329
1254
|
const settlementInput = settlementPsbt.getInput(i);
|
|
1330
1255
|
if (!settlementInput.txid ||
|
|
@@ -1348,7 +1273,7 @@ export class Wallet extends ReadonlyWallet {
|
|
|
1348
1273
|
}
|
|
1349
1274
|
continue;
|
|
1350
1275
|
}
|
|
1351
|
-
if (isRecoverable(
|
|
1276
|
+
if (isRecoverable(input) || isSubdust(input, this.dustAmount)) {
|
|
1352
1277
|
// recoverable or subdust coin, we don't need to create a forfeit tx
|
|
1353
1278
|
continue;
|
|
1354
1279
|
}
|
|
@@ -1375,7 +1300,7 @@ export class Wallet extends ReadonlyWallet {
|
|
|
1375
1300
|
txid: input.txid,
|
|
1376
1301
|
index: input.vout,
|
|
1377
1302
|
witnessUtxo: {
|
|
1378
|
-
amount: BigInt(
|
|
1303
|
+
amount: BigInt(input.value),
|
|
1379
1304
|
script: VtxoScript.decode(input.tapTree).pkScript,
|
|
1380
1305
|
},
|
|
1381
1306
|
sighashType: SigHash.DEFAULT,
|
|
@@ -1494,7 +1419,7 @@ export class Wallet extends ReadonlyWallet {
|
|
|
1494
1419
|
},
|
|
1495
1420
|
};
|
|
1496
1421
|
}
|
|
1497
|
-
async safeRegisterIntent(intent) {
|
|
1422
|
+
async safeRegisterIntent(intent, inputs) {
|
|
1498
1423
|
try {
|
|
1499
1424
|
return await this.arkProvider.registerIntent(intent);
|
|
1500
1425
|
}
|
|
@@ -1503,11 +1428,13 @@ export class Wallet extends ReadonlyWallet {
|
|
|
1503
1428
|
if (error instanceof ArkError &&
|
|
1504
1429
|
error.code === 0 &&
|
|
1505
1430
|
error.message.includes("duplicated input")) {
|
|
1506
|
-
//
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
1431
|
+
// Clear any queued intent spending these exact inputs. The
|
|
1432
|
+
// previous implementation signed a proof over getVtxos() only,
|
|
1433
|
+
// which misses boarding UTXOs — the most common trigger for
|
|
1434
|
+
// "duplicated input" on the auto-settle path. Signing the
|
|
1435
|
+
// caller's own inputs keeps the proof surgical and correct
|
|
1436
|
+
// regardless of whether the stuck input is a VTXO or boarding.
|
|
1437
|
+
const deleteIntent = await this.makeDeleteIntentSignature(inputs);
|
|
1511
1438
|
await this.arkProvider.deleteIntent(deleteIntent);
|
|
1512
1439
|
// try again
|
|
1513
1440
|
return this.arkProvider.registerIntent(intent);
|
|
@@ -1575,9 +1502,7 @@ export class Wallet extends ReadonlyWallet {
|
|
|
1575
1502
|
scripts: allScripts,
|
|
1576
1503
|
});
|
|
1577
1504
|
for (const vtxo of fetchedVtxos) {
|
|
1578
|
-
const vtxoScript = vtxo.script
|
|
1579
|
-
? scriptMap.get(vtxo.script)
|
|
1580
|
-
: undefined;
|
|
1505
|
+
const vtxoScript = scriptMap.get(vtxo.script);
|
|
1581
1506
|
if (!vtxoScript)
|
|
1582
1507
|
continue;
|
|
1583
1508
|
if (vtxo.virtualStatus.state === "swept" ||
|
|
@@ -1804,9 +1729,17 @@ export class Wallet extends ReadonlyWallet {
|
|
|
1804
1729
|
outputs.push(Extension.create([assetPacket]).txOut());
|
|
1805
1730
|
}
|
|
1806
1731
|
const sentAmount = recipients.reduce((sum, r) => sum + r.amount, 0);
|
|
1807
|
-
|
|
1808
|
-
|
|
1809
|
-
|
|
1732
|
+
// Optimistically hide selected coins from concurrent getVtxos() while
|
|
1733
|
+
// the offchain tx is in flight.
|
|
1734
|
+
this._addPendingSpends(selectedCoins);
|
|
1735
|
+
try {
|
|
1736
|
+
const { arkTxid, signedCheckpointTxs } = await this.buildAndSubmitOffchainTx(selectedCoins, outputs);
|
|
1737
|
+
await this.updateDbAfterOffchainTx(selectedCoins, arkTxid, signedCheckpointTxs, sentAmount, BigInt(changeAmount), changeReceiver ? changeIndex : 0, changeReceiver?.assets);
|
|
1738
|
+
return arkTxid;
|
|
1739
|
+
}
|
|
1740
|
+
finally {
|
|
1741
|
+
this._removePendingSpends(selectedCoins);
|
|
1742
|
+
}
|
|
1810
1743
|
}
|
|
1811
1744
|
/**
|
|
1812
1745
|
* Build an offchain transaction from the given inputs and outputs,
|
|
@@ -1880,8 +1813,9 @@ export class Wallet extends ReadonlyWallet {
|
|
|
1880
1813
|
console.warn(`updateDbAfterOffchainTx: inputs length (${inputs.length}) differs from signedCheckpointTxs length (${signedCheckpointTxs.length})`);
|
|
1881
1814
|
}
|
|
1882
1815
|
const safeLength = Math.min(inputs.length, signedCheckpointTxs.length);
|
|
1883
|
-
|
|
1884
|
-
|
|
1816
|
+
const cm = await this.getContractManager();
|
|
1817
|
+
const annotatedInputs = await cm.annotateVtxos(inputs);
|
|
1818
|
+
for (const [inputIndex, vtxo] of annotatedInputs.entries()) {
|
|
1885
1819
|
if (inputIndex < safeLength &&
|
|
1886
1820
|
signedCheckpointTxs[inputIndex]) {
|
|
1887
1821
|
const checkpoint = Transaction.fromPSBT(base64.decode(signedCheckpointTxs[inputIndex]));
|
|
@@ -1941,6 +1875,7 @@ export class Wallet extends ReadonlyWallet {
|
|
|
1941
1875
|
confirmed: false,
|
|
1942
1876
|
},
|
|
1943
1877
|
assets: changeAssets,
|
|
1878
|
+
script: hex.encode(this.offchainTapscript.pkScript),
|
|
1944
1879
|
};
|
|
1945
1880
|
}
|
|
1946
1881
|
await this.walletRepository.saveVtxos(addr, changeVtxo ? [...spentVtxos, changeVtxo] : spentVtxos);
|
|
@@ -1971,10 +1906,14 @@ export class Wallet extends ReadonlyWallet {
|
|
|
1971
1906
|
const inputArkTxIds = new Set();
|
|
1972
1907
|
const boardingUtxoToRemove = new Set();
|
|
1973
1908
|
const isVtxo = (input) => "virtualStatus" in input;
|
|
1909
|
+
const vtxoInputs = inputs.filter(isVtxo);
|
|
1910
|
+
const cm = await this.getContractManager();
|
|
1911
|
+
const annotatedVtxos = await cm.annotateVtxos(vtxoInputs);
|
|
1912
|
+
const annotatedByKey = new Map(annotatedVtxos.map((v) => [`${v.txid}:${v.vout}`, v]));
|
|
1974
1913
|
for (const input of inputs) {
|
|
1975
1914
|
if (isVtxo(input)) {
|
|
1976
1915
|
// virtual output = mark it settled
|
|
1977
|
-
const vtxo =
|
|
1916
|
+
const vtxo = annotatedByKey.get(`${input.txid}:${input.vout}`);
|
|
1978
1917
|
if (vtxo.arkTxId) {
|
|
1979
1918
|
inputArkTxIds.add(vtxo.arkTxId);
|
|
1980
1919
|
}
|
|
@@ -2060,7 +1999,7 @@ export function selectVirtualCoins(coins, targetAmount) {
|
|
|
2060
1999
|
*/
|
|
2061
2000
|
export async function waitForIncomingFunds(wallet) {
|
|
2062
2001
|
let stopFunc;
|
|
2063
|
-
|
|
2002
|
+
return new Promise((resolve) => {
|
|
2064
2003
|
wallet
|
|
2065
2004
|
.notifyIncomingFunds((coins) => {
|
|
2066
2005
|
resolve(coins);
|
|
@@ -2071,5 +2010,4 @@ export async function waitForIncomingFunds(wallet) {
|
|
|
2071
2010
|
stopFunc = stop;
|
|
2072
2011
|
});
|
|
2073
2012
|
});
|
|
2074
|
-
return promise;
|
|
2075
2013
|
}
|
|
@@ -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
|
}
|