@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
|
@@ -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,11 @@ 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
|
+
// Hardcoded unilateral exit delay for mainnet (~7 days in seconds).
|
|
38
|
+
// Pinned here so that address derivation stays stable for existing mainnet
|
|
39
|
+
// wallets even after the server lowers the delay it advertises.
|
|
40
|
+
const MAINNET_UNILATERAL_EXIT_DELAY = 605184n;
|
|
37
41
|
/**
|
|
38
42
|
* Type guard function to check if an identity has a toReadonly method.
|
|
39
43
|
*/
|
|
@@ -129,10 +133,16 @@ export class ReadonlyWallet {
|
|
|
129
133
|
throw new Error("invalid exitTimelock");
|
|
130
134
|
}
|
|
131
135
|
}
|
|
136
|
+
// On mainnet, pin the unilateral exit delay to the historical value so
|
|
137
|
+
// that addresses derived by existing wallets remain stable even if the
|
|
138
|
+
// server starts advertising a shorter delay.
|
|
139
|
+
const unilateralExitDelay = info.network === "bitcoin"
|
|
140
|
+
? MAINNET_UNILATERAL_EXIT_DELAY
|
|
141
|
+
: info.unilateralExitDelay;
|
|
132
142
|
// create unilateral exit timelock
|
|
133
143
|
const exitTimelock = config.exitTimelock ?? {
|
|
134
|
-
value:
|
|
135
|
-
type:
|
|
144
|
+
value: unilateralExitDelay,
|
|
145
|
+
type: unilateralExitDelay < 512n ? "blocks" : "seconds",
|
|
136
146
|
};
|
|
137
147
|
// validate boarding timelock passed in config if any
|
|
138
148
|
if (config.boardingTimelock) {
|
|
@@ -287,14 +297,12 @@ export class ReadonlyWallet {
|
|
|
287
297
|
* @param filter - Optional flags controlling whether recoverable or unrolled VTXOs are included
|
|
288
298
|
*/
|
|
289
299
|
async getVtxos(filter) {
|
|
290
|
-
const { isDelta, fetchedExtended, address } = await this.syncVtxos();
|
|
291
300
|
const f = filter ?? { withRecoverable: true, withUnrolled: false };
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
return vtxos.filter((vtxo) => {
|
|
301
|
+
const contractManager = await this.getContractManager();
|
|
302
|
+
const vtxos = await contractManager.getContractsWithVtxos();
|
|
303
|
+
return vtxos
|
|
304
|
+
.flatMap((_) => _.vtxos)
|
|
305
|
+
.filter((vtxo) => {
|
|
298
306
|
if (isSpendable(vtxo)) {
|
|
299
307
|
if (!f.withRecoverable &&
|
|
300
308
|
(isRecoverable(vtxo) || isExpired(vtxo))) {
|
|
@@ -309,11 +317,9 @@ export class ReadonlyWallet {
|
|
|
309
317
|
* Return wallet transaction history derived from Arkade state and boarding transactions.
|
|
310
318
|
*/
|
|
311
319
|
async getTransactionHistory() {
|
|
312
|
-
|
|
313
|
-
const
|
|
314
|
-
const allVtxos =
|
|
315
|
-
? await this.walletRepository.getVtxos(address)
|
|
316
|
-
: fetchedExtended;
|
|
320
|
+
const contractManager = await this.getContractManager();
|
|
321
|
+
const response = await contractManager.getContractsWithVtxos();
|
|
322
|
+
const allVtxos = response.flatMap((_) => _.vtxos);
|
|
317
323
|
const { boardingTxs, commitmentsToIgnore } = await this.getBoardingTxs();
|
|
318
324
|
const getTxCreatedAt = (txid) => this.indexerProvider
|
|
319
325
|
.getVtxos({ outpoints: [{ txid, vout: 0 }] })
|
|
@@ -321,166 +327,11 @@ export class ReadonlyWallet {
|
|
|
321
327
|
return buildTransactionHistory(allVtxos, boardingTxs, commitmentsToIgnore, getTxCreatedAt);
|
|
322
328
|
}
|
|
323
329
|
/**
|
|
324
|
-
*
|
|
325
|
-
* cursor, or do a full bootstrap when no cursor exists. Upserts
|
|
326
|
-
* the result into the cache and advances the sync cursors.
|
|
327
|
-
*
|
|
328
|
-
* Concurrent calls are deduplicated: if a sync is already in flight,
|
|
329
|
-
* subsequent callers receive the same promise instead of triggering
|
|
330
|
-
* a second network round-trip.
|
|
331
|
-
*/
|
|
332
|
-
syncVtxos() {
|
|
333
|
-
if (this._syncVtxosInflight)
|
|
334
|
-
return this._syncVtxosInflight;
|
|
335
|
-
const p = this.doSyncVtxos().finally(() => {
|
|
336
|
-
this._syncVtxosInflight = undefined;
|
|
337
|
-
});
|
|
338
|
-
this._syncVtxosInflight = p;
|
|
339
|
-
return p;
|
|
340
|
-
}
|
|
341
|
-
async doSyncVtxos() {
|
|
342
|
-
const address = await this.getAddress();
|
|
343
|
-
// Batch cursor read with script map to avoid extra async hops
|
|
344
|
-
// before the fetch (background operations may run between hops).
|
|
345
|
-
const [scriptMap, cursors] = await Promise.all([
|
|
346
|
-
this.getScriptMap(),
|
|
347
|
-
getAllSyncCursors(this.walletRepository),
|
|
348
|
-
]);
|
|
349
|
-
const allScripts = [...scriptMap.keys()];
|
|
350
|
-
// Partition scripts into bootstrap (no cursor) and delta (has cursor).
|
|
351
|
-
const bootstrapScripts = [];
|
|
352
|
-
const deltaScripts = [];
|
|
353
|
-
for (const s of allScripts) {
|
|
354
|
-
if (cursors[s] === undefined) {
|
|
355
|
-
bootstrapScripts.push(s);
|
|
356
|
-
}
|
|
357
|
-
else {
|
|
358
|
-
deltaScripts.push(s);
|
|
359
|
-
}
|
|
360
|
-
}
|
|
361
|
-
const requestStartedAt = Date.now();
|
|
362
|
-
const allVtxos = [];
|
|
363
|
-
const extendWithScript = (vtxo) => {
|
|
364
|
-
const vtxoScript = vtxo.script
|
|
365
|
-
? scriptMap.get(vtxo.script)
|
|
366
|
-
: undefined;
|
|
367
|
-
if (!vtxoScript)
|
|
368
|
-
return undefined;
|
|
369
|
-
return {
|
|
370
|
-
...vtxo,
|
|
371
|
-
forfeitTapLeafScript: vtxoScript.forfeit(),
|
|
372
|
-
intentTapLeafScript: vtxoScript.forfeit(),
|
|
373
|
-
tapTree: vtxoScript.encode(),
|
|
374
|
-
};
|
|
375
|
-
};
|
|
376
|
-
// Full fetch for scripts with no cursor.
|
|
377
|
-
if (bootstrapScripts.length > 0) {
|
|
378
|
-
const response = await this.indexerProvider.getVtxos({
|
|
379
|
-
scripts: bootstrapScripts,
|
|
380
|
-
});
|
|
381
|
-
allVtxos.push(...response.vtxos);
|
|
382
|
-
}
|
|
383
|
-
// Delta fetch for scripts with an existing cursor.
|
|
384
|
-
let hasDelta = false;
|
|
385
|
-
if (deltaScripts.length > 0) {
|
|
386
|
-
const minCursor = Math.min(...deltaScripts.map((s) => cursors[s]));
|
|
387
|
-
const window = computeSyncWindow(minCursor);
|
|
388
|
-
if (window) {
|
|
389
|
-
hasDelta = true;
|
|
390
|
-
const response = await this.indexerProvider.getVtxos({
|
|
391
|
-
scripts: deltaScripts,
|
|
392
|
-
after: window.after,
|
|
393
|
-
});
|
|
394
|
-
allVtxos.push(...response.vtxos);
|
|
395
|
-
}
|
|
396
|
-
}
|
|
397
|
-
// Extend every fetched virtual output and upsert into the cache.
|
|
398
|
-
const fetchedExtended = [];
|
|
399
|
-
for (const vtxo of allVtxos) {
|
|
400
|
-
const extended = extendWithScript(vtxo);
|
|
401
|
-
if (extended)
|
|
402
|
-
fetchedExtended.push(extended);
|
|
403
|
-
}
|
|
404
|
-
// Save virtual outputs first, then advance cursors only on success.
|
|
405
|
-
const cutoff = cursorCutoff(requestStartedAt);
|
|
406
|
-
await this.walletRepository.saveVtxos(address, fetchedExtended);
|
|
407
|
-
await advanceSyncCursors(this.walletRepository, Object.fromEntries(allScripts.map((s) => [s, cutoff])));
|
|
408
|
-
// Delta-sync reconciliation: full re-fetch for delta scripts.
|
|
409
|
-
//
|
|
410
|
-
// The delta fetch (above) only returns virtual outputs changed after the
|
|
411
|
-
// cursor, so it can miss preconfirmed virtual outputs that were consumed
|
|
412
|
-
// by a round between syncs. Rather than layering targeted
|
|
413
|
-
// queries (pendingOnly, spendableOnly) with pagination guards
|
|
414
|
-
// and set algebra, we perform a single unfiltered re-fetch for
|
|
415
|
-
// delta scripts. This is slightly more data over the wire but
|
|
416
|
-
// gives us complete, authoritative state in one call and keeps
|
|
417
|
-
// the reconciliation logic simple.
|
|
418
|
-
//
|
|
419
|
-
// Any cached non-spent virtual output that is absent from the full
|
|
420
|
-
// result set is marked spent; any virtual output whose state changed
|
|
421
|
-
// (e.g. preconfirmed → settled) is updated in place.
|
|
422
|
-
if (hasDelta) {
|
|
423
|
-
const { vtxos: fullVtxos, page: fullPage } = await this.indexerProvider.getVtxos({
|
|
424
|
-
scripts: deltaScripts,
|
|
425
|
-
});
|
|
426
|
-
// Reconciliation is best-effort: if the response is
|
|
427
|
-
// paginated we don't have a complete picture, so we skip
|
|
428
|
-
// rather than act on partial data. Wallets with enough
|
|
429
|
-
// virtual outputs to exceed a single page rely solely on the
|
|
430
|
-
// cursor-based delta mechanism for state updates.
|
|
431
|
-
const fullSetComplete = !fullPage || fullPage.total <= 1;
|
|
432
|
-
if (fullSetComplete) {
|
|
433
|
-
const fullOutpoints = new Map(fullVtxos.map((v) => [`${v.txid}:${v.vout}`, v]));
|
|
434
|
-
const deltaScriptSet = new Set(deltaScripts);
|
|
435
|
-
const cachedVtxos = await this.walletRepository.getVtxos(address);
|
|
436
|
-
const reconciledExtended = [];
|
|
437
|
-
for (const cached of cachedVtxos) {
|
|
438
|
-
if (!cached.script ||
|
|
439
|
-
!deltaScriptSet.has(cached.script) ||
|
|
440
|
-
cached.isSpent) {
|
|
441
|
-
continue;
|
|
442
|
-
}
|
|
443
|
-
const outpoint = `${cached.txid}:${cached.vout}`;
|
|
444
|
-
const fresh = fullOutpoints.get(outpoint);
|
|
445
|
-
if (!fresh) {
|
|
446
|
-
// Server no longer knows about this virtual output —
|
|
447
|
-
// it was spent between syncs.
|
|
448
|
-
reconciledExtended.push({
|
|
449
|
-
...cached,
|
|
450
|
-
isSpent: true,
|
|
451
|
-
});
|
|
452
|
-
continue;
|
|
453
|
-
}
|
|
454
|
-
const extended = extendWithScript(fresh);
|
|
455
|
-
if (extended &&
|
|
456
|
-
extended.virtualStatus.state !==
|
|
457
|
-
cached.virtualStatus.state) {
|
|
458
|
-
// State transitioned (e.g. preconfirmed →
|
|
459
|
-
// settled) — update the cached entry.
|
|
460
|
-
reconciledExtended.push(extended);
|
|
461
|
-
}
|
|
462
|
-
}
|
|
463
|
-
if (reconciledExtended.length > 0) {
|
|
464
|
-
console.warn(`[ark-sdk] delta sync: reconciled ${reconciledExtended.length} stale VTXO(s) via full re-fetch`);
|
|
465
|
-
await this.walletRepository.saveVtxos(address, reconciledExtended);
|
|
466
|
-
}
|
|
467
|
-
}
|
|
468
|
-
else {
|
|
469
|
-
console.warn("[ark-sdk] delta sync: skipping reconciliation — full re-fetch was paginated");
|
|
470
|
-
}
|
|
471
|
-
}
|
|
472
|
-
return {
|
|
473
|
-
isDelta: hasDelta || bootstrapScripts.length === 0,
|
|
474
|
-
fetchedExtended,
|
|
475
|
-
address,
|
|
476
|
-
};
|
|
477
|
-
}
|
|
478
|
-
/**
|
|
479
|
-
* 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.
|
|
480
331
|
* Useful for recovery after indexer reprocessing or debugging.
|
|
481
332
|
*/
|
|
482
|
-
async
|
|
483
|
-
await
|
|
333
|
+
async clearSyncCursor() {
|
|
334
|
+
await clearSyncCursor(this.walletRepository);
|
|
484
335
|
}
|
|
485
336
|
/**
|
|
486
337
|
* Build a transaction history view for the wallet's boarding address.
|
|
@@ -523,6 +374,7 @@ export class ReadonlyWallet {
|
|
|
523
374
|
createdAt: tx.status.confirmed
|
|
524
375
|
? new Date(tx.status.block_time * 1000)
|
|
525
376
|
: new Date(0),
|
|
377
|
+
script: hex.encode(this.boardingTapscript.pkScript),
|
|
526
378
|
});
|
|
527
379
|
}
|
|
528
380
|
}
|
|
@@ -612,22 +464,41 @@ export class ReadonlyWallet {
|
|
|
612
464
|
await this.indexerProvider?.unsubscribeForScripts(subscriptionId);
|
|
613
465
|
};
|
|
614
466
|
// Handle subscription updates asynchronously without blocking.
|
|
615
|
-
//
|
|
616
|
-
//
|
|
617
|
-
//
|
|
618
|
-
//
|
|
619
|
-
//
|
|
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.
|
|
620
472
|
(async () => {
|
|
621
473
|
try {
|
|
474
|
+
const cm = await this.getContractManager();
|
|
622
475
|
for await (const update of subscription) {
|
|
623
|
-
if (update.newVtxos?.length
|
|
624
|
-
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
|
+
]);
|
|
625
493
|
eventCallback({
|
|
626
494
|
type: "vtxo",
|
|
627
|
-
newVtxos
|
|
628
|
-
spentVtxos
|
|
495
|
+
newVtxos,
|
|
496
|
+
spentVtxos,
|
|
629
497
|
});
|
|
630
498
|
}
|
|
499
|
+
catch (error) {
|
|
500
|
+
console.warn("Dropping subscription update after annotation failed; next sync will reconcile:", error);
|
|
501
|
+
}
|
|
631
502
|
}
|
|
632
503
|
}
|
|
633
504
|
catch (error) {
|
|
@@ -773,7 +644,6 @@ export class ReadonlyWallet {
|
|
|
773
644
|
indexerProvider: this.indexerProvider,
|
|
774
645
|
contractRepository: this.contractRepository,
|
|
775
646
|
walletRepository: this.walletRepository,
|
|
776
|
-
getDefaultAddress: () => this.getAddress(),
|
|
777
647
|
watcherConfig: this.watcherConfig,
|
|
778
648
|
});
|
|
779
649
|
// Register the wallet's current address as a contract
|
|
@@ -898,11 +768,10 @@ export class Wallet extends ReadonlyWallet {
|
|
|
898
768
|
}
|
|
899
769
|
});
|
|
900
770
|
}
|
|
901
|
-
constructor(identity, network,
|
|
771
|
+
constructor(identity, network, onchainProvider, arkProvider, indexerProvider, arkServerPublicKey, offchainTapscript, boardingTapscript, serverUnrollScript, forfeitOutputScript, forfeitPubkey, dustAmount, walletRepository, contractRepository,
|
|
902
772
|
/** @deprecated Use settlementConfig */
|
|
903
773
|
renewalConfig, delegatorProvider, watcherConfig, settlementConfig) {
|
|
904
774
|
super(identity, network, onchainProvider, indexerProvider, arkServerPublicKey, offchainTapscript, boardingTapscript, dustAmount, walletRepository, contractRepository, delegatorProvider, watcherConfig);
|
|
905
|
-
this.networkName = networkName;
|
|
906
775
|
this.arkProvider = arkProvider;
|
|
907
776
|
this.serverUnrollScript = serverUnrollScript;
|
|
908
777
|
this.forfeitOutputScript = forfeitOutputScript;
|
|
@@ -1022,7 +891,7 @@ export class Wallet extends ReadonlyWallet {
|
|
|
1022
891
|
const forfeitPubkey = hex.decode(setup.info.forfeitPubkey).slice(1);
|
|
1023
892
|
const forfeitAddress = Address(setup.network).decode(setup.info.forfeitAddress);
|
|
1024
893
|
const forfeitOutputScript = OutScript.encode(forfeitAddress);
|
|
1025
|
-
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);
|
|
1026
895
|
await wallet.getVtxoManager();
|
|
1027
896
|
return wallet;
|
|
1028
897
|
}
|
|
@@ -1285,7 +1154,7 @@ export class Wallet extends ReadonlyWallet {
|
|
|
1285
1154
|
const abortController = new AbortController();
|
|
1286
1155
|
try {
|
|
1287
1156
|
const stream = this.arkProvider.getEventStream(abortController.signal, topics);
|
|
1288
|
-
const intentId = await this.safeRegisterIntent(intent);
|
|
1157
|
+
const intentId = await this.safeRegisterIntent(intent, params.inputs);
|
|
1289
1158
|
const handler = this.createBatchHandler(intentId, params.inputs, recipients, session);
|
|
1290
1159
|
const commitmentTxid = await Batch.join(stream, handler, {
|
|
1291
1160
|
abortController,
|
|
@@ -1298,8 +1167,16 @@ export class Wallet extends ReadonlyWallet {
|
|
|
1298
1167
|
return commitmentTxid;
|
|
1299
1168
|
}
|
|
1300
1169
|
catch (error) {
|
|
1301
|
-
// delete the intent to not be stuck in the queue
|
|
1302
|
-
|
|
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
|
+
});
|
|
1303
1180
|
throw error;
|
|
1304
1181
|
}
|
|
1305
1182
|
finally {
|
|
@@ -1489,7 +1366,7 @@ export class Wallet extends ReadonlyWallet {
|
|
|
1489
1366
|
},
|
|
1490
1367
|
};
|
|
1491
1368
|
}
|
|
1492
|
-
async safeRegisterIntent(intent) {
|
|
1369
|
+
async safeRegisterIntent(intent, inputs) {
|
|
1493
1370
|
try {
|
|
1494
1371
|
return await this.arkProvider.registerIntent(intent);
|
|
1495
1372
|
}
|
|
@@ -1498,11 +1375,13 @@ export class Wallet extends ReadonlyWallet {
|
|
|
1498
1375
|
if (error instanceof ArkError &&
|
|
1499
1376
|
error.code === 0 &&
|
|
1500
1377
|
error.message.includes("duplicated input")) {
|
|
1501
|
-
//
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
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);
|
|
1506
1385
|
await this.arkProvider.deleteIntent(deleteIntent);
|
|
1507
1386
|
// try again
|
|
1508
1387
|
return this.arkProvider.registerIntent(intent);
|
|
@@ -1570,9 +1449,7 @@ export class Wallet extends ReadonlyWallet {
|
|
|
1570
1449
|
scripts: allScripts,
|
|
1571
1450
|
});
|
|
1572
1451
|
for (const vtxo of fetchedVtxos) {
|
|
1573
|
-
const vtxoScript = vtxo.script
|
|
1574
|
-
? scriptMap.get(vtxo.script)
|
|
1575
|
-
: undefined;
|
|
1452
|
+
const vtxoScript = scriptMap.get(vtxo.script);
|
|
1576
1453
|
if (!vtxoScript)
|
|
1577
1454
|
continue;
|
|
1578
1455
|
if (vtxo.virtualStatus.state === "swept" ||
|
|
@@ -1875,8 +1752,9 @@ export class Wallet extends ReadonlyWallet {
|
|
|
1875
1752
|
console.warn(`updateDbAfterOffchainTx: inputs length (${inputs.length}) differs from signedCheckpointTxs length (${signedCheckpointTxs.length})`);
|
|
1876
1753
|
}
|
|
1877
1754
|
const safeLength = Math.min(inputs.length, signedCheckpointTxs.length);
|
|
1878
|
-
|
|
1879
|
-
|
|
1755
|
+
const cm = await this.getContractManager();
|
|
1756
|
+
const annotatedInputs = await cm.annotateVtxos(inputs);
|
|
1757
|
+
for (const [inputIndex, vtxo] of annotatedInputs.entries()) {
|
|
1880
1758
|
if (inputIndex < safeLength &&
|
|
1881
1759
|
signedCheckpointTxs[inputIndex]) {
|
|
1882
1760
|
const checkpoint = Transaction.fromPSBT(base64.decode(signedCheckpointTxs[inputIndex]));
|
|
@@ -1936,6 +1814,7 @@ export class Wallet extends ReadonlyWallet {
|
|
|
1936
1814
|
confirmed: false,
|
|
1937
1815
|
},
|
|
1938
1816
|
assets: changeAssets,
|
|
1817
|
+
script: hex.encode(this.offchainTapscript.pkScript),
|
|
1939
1818
|
};
|
|
1940
1819
|
}
|
|
1941
1820
|
await this.walletRepository.saveVtxos(addr, changeVtxo ? [...spentVtxos, changeVtxo] : spentVtxos);
|
|
@@ -1966,10 +1845,14 @@ export class Wallet extends ReadonlyWallet {
|
|
|
1966
1845
|
const inputArkTxIds = new Set();
|
|
1967
1846
|
const boardingUtxoToRemove = new Set();
|
|
1968
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]));
|
|
1969
1852
|
for (const input of inputs) {
|
|
1970
1853
|
if (isVtxo(input)) {
|
|
1971
1854
|
// virtual output = mark it settled
|
|
1972
|
-
const vtxo =
|
|
1855
|
+
const vtxo = annotatedByKey.get(`${input.txid}:${input.vout}`);
|
|
1973
1856
|
if (vtxo.arkTxId) {
|
|
1974
1857
|
inputArkTxIds.add(vtxo.arkTxId);
|
|
1975
1858
|
}
|
|
@@ -2055,7 +1938,7 @@ export function selectVirtualCoins(coins, targetAmount) {
|
|
|
2055
1938
|
*/
|
|
2056
1939
|
export async function waitForIncomingFunds(wallet) {
|
|
2057
1940
|
let stopFunc;
|
|
2058
|
-
|
|
1941
|
+
return new Promise((resolve) => {
|
|
2059
1942
|
wallet
|
|
2060
1943
|
.notifyIncomingFunds((coins) => {
|
|
2061
1944
|
resolve(coins);
|
|
@@ -2066,5 +1949,4 @@ export async function waitForIncomingFunds(wallet) {
|
|
|
2066
1949
|
stopFunc = stop;
|
|
2067
1950
|
});
|
|
2068
1951
|
});
|
|
2069
|
-
return promise;
|
|
2070
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
|
/**
|