@arkade-os/sdk 0.4.8 → 0.4.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cjs/contracts/contractManager.js +59 -11
- package/dist/cjs/contracts/contractWatcher.js +21 -2
- package/dist/cjs/providers/expoIndexer.js +1 -0
- package/dist/cjs/providers/indexer.js +1 -0
- package/dist/cjs/utils/transactionHistory.js +2 -1
- package/dist/cjs/wallet/serviceWorker/wallet-message-handler.js +109 -29
- package/dist/cjs/wallet/serviceWorker/wallet.js +22 -0
- package/dist/cjs/wallet/vtxo-manager.js +81 -50
- package/dist/cjs/wallet/wallet.js +46 -34
- package/dist/cjs/worker/messageBus.js +7 -0
- package/dist/esm/contracts/contractManager.js +59 -11
- package/dist/esm/contracts/contractWatcher.js +21 -2
- package/dist/esm/providers/expoIndexer.js +1 -0
- package/dist/esm/providers/indexer.js +1 -0
- package/dist/esm/utils/transactionHistory.js +2 -1
- package/dist/esm/wallet/serviceWorker/wallet-message-handler.js +109 -29
- package/dist/esm/wallet/serviceWorker/wallet.js +22 -0
- package/dist/esm/wallet/vtxo-manager.js +81 -50
- package/dist/esm/wallet/wallet.js +46 -34
- package/dist/esm/worker/messageBus.js +7 -0
- package/dist/types/contracts/contractManager.d.ts +10 -0
- package/dist/types/repositories/serialization.d.ts +1 -0
- package/dist/types/utils/transactionHistory.d.ts +1 -1
- package/dist/types/wallet/index.d.ts +2 -0
- package/dist/types/wallet/serviceWorker/wallet-message-handler.d.ts +23 -6
- package/dist/types/wallet/serviceWorker/wallet.d.ts +9 -1
- package/dist/types/wallet/vtxo-manager.d.ts +5 -0
- package/dist/types/worker/messageBus.d.ts +6 -0
- package/package.json +1 -1
|
@@ -278,27 +278,33 @@ class ReadonlyWallet {
|
|
|
278
278
|
const scriptMap = await this.getScriptMap();
|
|
279
279
|
const f = filter ?? { withRecoverable: true, withUnrolled: false };
|
|
280
280
|
const allExtended = [];
|
|
281
|
-
//
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
if (
|
|
291
|
-
|
|
292
|
-
|
|
281
|
+
// Batch all scripts into a single indexer call
|
|
282
|
+
const allScripts = [...scriptMap.keys()];
|
|
283
|
+
const response = await this.indexerProvider.getVtxos({
|
|
284
|
+
scripts: allScripts,
|
|
285
|
+
});
|
|
286
|
+
for (const vtxo of response.vtxos) {
|
|
287
|
+
const vtxoScript = vtxo.script
|
|
288
|
+
? scriptMap.get(vtxo.script)
|
|
289
|
+
: undefined;
|
|
290
|
+
if (!vtxoScript)
|
|
291
|
+
continue;
|
|
292
|
+
if ((0, _1.isSpendable)(vtxo)) {
|
|
293
|
+
if (!f.withRecoverable &&
|
|
294
|
+
((0, _1.isRecoverable)(vtxo) || (0, _1.isExpired)(vtxo))) {
|
|
295
|
+
continue;
|
|
296
|
+
}
|
|
293
297
|
}
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
forfeitTapLeafScript: vtxoScript.forfeit(),
|
|
298
|
-
intentTapLeafScript: vtxoScript.forfeit(),
|
|
299
|
-
tapTree: vtxoScript.encode(),
|
|
300
|
-
});
|
|
298
|
+
else {
|
|
299
|
+
if (!f.withUnrolled || !vtxo.isUnrolled)
|
|
300
|
+
continue;
|
|
301
301
|
}
|
|
302
|
+
allExtended.push({
|
|
303
|
+
...vtxo,
|
|
304
|
+
forfeitTapLeafScript: vtxoScript.forfeit(),
|
|
305
|
+
intentTapLeafScript: vtxoScript.forfeit(),
|
|
306
|
+
tapTree: vtxoScript.encode(),
|
|
307
|
+
});
|
|
302
308
|
}
|
|
303
309
|
// Update cache with fresh data
|
|
304
310
|
await this.walletRepository.saveVtxos(address, allExtended);
|
|
@@ -310,7 +316,7 @@ class ReadonlyWallet {
|
|
|
310
316
|
const { boardingTxs, commitmentsToIgnore } = await this.getBoardingTxs();
|
|
311
317
|
const getTxCreatedAt = (txid) => this.indexerProvider
|
|
312
318
|
.getVtxos({ outpoints: [{ txid, vout: 0 }] })
|
|
313
|
-
.then((res) => res.vtxos[0]?.createdAt.getTime()
|
|
319
|
+
.then((res) => res.vtxos[0]?.createdAt.getTime());
|
|
314
320
|
return (0, transactionHistory_1.buildTransactionHistory)(response.vtxos, boardingTxs, commitmentsToIgnore, getTxCreatedAt);
|
|
315
321
|
}
|
|
316
322
|
async getBoardingTxs() {
|
|
@@ -1344,23 +1350,29 @@ class Wallet extends ReadonlyWallet {
|
|
|
1344
1350
|
async finalizePendingTxs(vtxos) {
|
|
1345
1351
|
const MAX_INPUTS_PER_INTENT = 20;
|
|
1346
1352
|
if (!vtxos || vtxos.length === 0) {
|
|
1347
|
-
//
|
|
1353
|
+
// Batch all scripts into a single indexer call
|
|
1348
1354
|
const scriptMap = await this.getScriptMap();
|
|
1349
1355
|
const allExtended = [];
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1356
|
+
const allScripts = [...scriptMap.keys()];
|
|
1357
|
+
const { vtxos: fetchedVtxos } = await this.indexerProvider.getVtxos({
|
|
1358
|
+
scripts: allScripts,
|
|
1359
|
+
});
|
|
1360
|
+
for (const vtxo of fetchedVtxos) {
|
|
1361
|
+
const vtxoScript = vtxo.script
|
|
1362
|
+
? scriptMap.get(vtxo.script)
|
|
1363
|
+
: undefined;
|
|
1364
|
+
if (!vtxoScript)
|
|
1365
|
+
continue;
|
|
1366
|
+
if (vtxo.virtualStatus.state === "swept" ||
|
|
1367
|
+
vtxo.virtualStatus.state === "settled") {
|
|
1368
|
+
continue;
|
|
1363
1369
|
}
|
|
1370
|
+
allExtended.push({
|
|
1371
|
+
...vtxo,
|
|
1372
|
+
forfeitTapLeafScript: vtxoScript.forfeit(),
|
|
1373
|
+
intentTapLeafScript: vtxoScript.forfeit(),
|
|
1374
|
+
tapTree: vtxoScript.encode(),
|
|
1375
|
+
});
|
|
1364
1376
|
}
|
|
1365
1377
|
if (allExtended.length === 0) {
|
|
1366
1378
|
return { finalized: [], pending: [] };
|
|
@@ -152,8 +152,12 @@ class MessageBus {
|
|
|
152
152
|
identity,
|
|
153
153
|
arkServerUrl: config.arkServer.url,
|
|
154
154
|
arkServerPublicKey: config.arkServer.publicKey,
|
|
155
|
+
indexerUrl: config.indexerUrl,
|
|
156
|
+
esploraUrl: config.esploraUrl,
|
|
155
157
|
storage,
|
|
156
158
|
delegatorProvider,
|
|
159
|
+
settlementConfig: config.settlementConfig,
|
|
160
|
+
watcherConfig: config.watcherConfig,
|
|
157
161
|
});
|
|
158
162
|
return { wallet, arkProvider, readonlyWallet: wallet };
|
|
159
163
|
}
|
|
@@ -163,8 +167,11 @@ class MessageBus {
|
|
|
163
167
|
identity,
|
|
164
168
|
arkServerUrl: config.arkServer.url,
|
|
165
169
|
arkServerPublicKey: config.arkServer.publicKey,
|
|
170
|
+
indexerUrl: config.indexerUrl,
|
|
171
|
+
esploraUrl: config.esploraUrl,
|
|
166
172
|
storage,
|
|
167
173
|
delegatorProvider,
|
|
174
|
+
watcherConfig: config.watcherConfig,
|
|
168
175
|
});
|
|
169
176
|
return { readonlyWallet, arkProvider };
|
|
170
177
|
}
|
|
@@ -71,9 +71,10 @@ export class ContractManager {
|
|
|
71
71
|
}
|
|
72
72
|
// Load persisted contracts
|
|
73
73
|
const contracts = await this.config.contractRepository.getContracts();
|
|
74
|
-
// fetch
|
|
74
|
+
// fetch all VTXOs (including spent/swept) for all contracts,
|
|
75
|
+
// so the repository has full history for transaction history and balance
|
|
75
76
|
// TODO: what if the user has 1k contracts?
|
|
76
|
-
await this.
|
|
77
|
+
await this.fetchContractVxosFromIndexer(contracts, true);
|
|
77
78
|
// add all contracts to the watcher
|
|
78
79
|
const now = Date.now();
|
|
79
80
|
for (const contract of contracts) {
|
|
@@ -136,8 +137,8 @@ export class ContractManager {
|
|
|
136
137
|
};
|
|
137
138
|
// Persist
|
|
138
139
|
await this.config.contractRepository.saveContract(contract);
|
|
139
|
-
//
|
|
140
|
-
await this.
|
|
140
|
+
// fetch all VTXOs (including spent/swept) for this contract
|
|
141
|
+
await this.fetchContractVxosFromIndexer([contract], true);
|
|
141
142
|
// Add to watcher
|
|
142
143
|
await this.watcher.addContract(contract);
|
|
143
144
|
return contract;
|
|
@@ -302,6 +303,14 @@ export class ContractManager {
|
|
|
302
303
|
this.eventCallbacks.delete(callback);
|
|
303
304
|
};
|
|
304
305
|
}
|
|
306
|
+
/**
|
|
307
|
+
* Force a full VTXO refresh from the indexer for all contracts.
|
|
308
|
+
* Populates the wallet repository with complete VTXO history.
|
|
309
|
+
*/
|
|
310
|
+
async refreshVtxos() {
|
|
311
|
+
const contracts = await this.config.contractRepository.getContracts();
|
|
312
|
+
await this.fetchContractVxosFromIndexer(contracts, true);
|
|
313
|
+
}
|
|
305
314
|
/**
|
|
306
315
|
* Check if currently watching.
|
|
307
316
|
*/
|
|
@@ -331,11 +340,13 @@ export class ContractManager {
|
|
|
331
340
|
case "vtxo_spent":
|
|
332
341
|
await this.fetchContractVxosFromIndexer([event.contract], true);
|
|
333
342
|
break;
|
|
334
|
-
case "connection_reset":
|
|
335
|
-
// Refetch all VTXOs for all active
|
|
343
|
+
case "connection_reset": {
|
|
344
|
+
// Refetch all VTXOs (including spent/swept) for all active
|
|
345
|
+
// contracts so the repo stays consistent with bootstrap state
|
|
336
346
|
const activeWatchedContracts = this.watcher.getActiveContracts();
|
|
337
|
-
await this.fetchContractVxosFromIndexer(activeWatchedContracts,
|
|
347
|
+
await this.fetchContractVxosFromIndexer(activeWatchedContracts, true);
|
|
338
348
|
break;
|
|
349
|
+
}
|
|
339
350
|
case "contract_expired":
|
|
340
351
|
// just update DB
|
|
341
352
|
await this.config.contractRepository.saveContract(event.contract);
|
|
@@ -362,11 +373,48 @@ export class ContractManager {
|
|
|
362
373
|
return result;
|
|
363
374
|
}
|
|
364
375
|
async fetchContractVtxosBulk(contracts, includeSpent) {
|
|
365
|
-
|
|
366
|
-
|
|
376
|
+
if (contracts.length === 0) {
|
|
377
|
+
return new Map();
|
|
378
|
+
}
|
|
379
|
+
// For a single contract, use the paginated path directly.
|
|
380
|
+
if (contracts.length === 1) {
|
|
381
|
+
const contract = contracts[0];
|
|
367
382
|
const vtxos = await this.fetchContractVtxosPaginated(contract, includeSpent);
|
|
368
|
-
|
|
369
|
-
}
|
|
383
|
+
return new Map([[contract.script, vtxos]]);
|
|
384
|
+
}
|
|
385
|
+
// For multiple contracts, batch all scripts into a single indexer call
|
|
386
|
+
// per page to minimise round-trips. Results are keyed by script so we
|
|
387
|
+
// can distribute them back to the correct contract afterwards.
|
|
388
|
+
const scriptToContract = new Map(contracts.map((c) => [c.script, c]));
|
|
389
|
+
const result = new Map(contracts.map((c) => [c.script, []]));
|
|
390
|
+
const scripts = contracts.map((c) => c.script);
|
|
391
|
+
const pageSize = 100;
|
|
392
|
+
const opts = includeSpent ? {} : { spendableOnly: true };
|
|
393
|
+
let pageIndex = 0;
|
|
394
|
+
let hasMore = true;
|
|
395
|
+
while (hasMore) {
|
|
396
|
+
const { vtxos, page } = await this.config.indexerProvider.getVtxos({
|
|
397
|
+
scripts,
|
|
398
|
+
...opts,
|
|
399
|
+
pageIndex,
|
|
400
|
+
pageSize,
|
|
401
|
+
});
|
|
402
|
+
for (const vtxo of vtxos) {
|
|
403
|
+
// Match the VTXO back to its contract via the script field
|
|
404
|
+
// populated by the indexer.
|
|
405
|
+
if (!vtxo.script)
|
|
406
|
+
continue;
|
|
407
|
+
const contract = scriptToContract.get(vtxo.script);
|
|
408
|
+
if (!contract)
|
|
409
|
+
continue;
|
|
410
|
+
result.get(contract.script).push({
|
|
411
|
+
...extendVtxoFromContract(vtxo, contract),
|
|
412
|
+
contractScript: contract.script,
|
|
413
|
+
});
|
|
414
|
+
}
|
|
415
|
+
hasMore = page ? vtxos.length === pageSize : false;
|
|
416
|
+
pageIndex++;
|
|
417
|
+
}
|
|
370
418
|
return result;
|
|
371
419
|
}
|
|
372
420
|
async fetchContractVtxosPaginated(contract, includeSpent) {
|
|
@@ -410,8 +410,27 @@ export class ContractWatcher {
|
|
|
410
410
|
}
|
|
411
411
|
return;
|
|
412
412
|
}
|
|
413
|
-
|
|
414
|
-
|
|
413
|
+
try {
|
|
414
|
+
this.subscriptionId =
|
|
415
|
+
await this.config.indexerProvider.subscribeForScripts(scriptsToWatch, this.subscriptionId);
|
|
416
|
+
}
|
|
417
|
+
catch (error) {
|
|
418
|
+
// If we sent a stale subscription ID that the server no longer
|
|
419
|
+
// recognises, clear it and retry to create a fresh subscription.
|
|
420
|
+
// The server currently returns HTTP 500 with a JSON body whose
|
|
421
|
+
// message field looks like "subscription <uuid> not found".
|
|
422
|
+
// All other errors (network failures, parse errors, etc.) are rethrown.
|
|
423
|
+
const isStale = error instanceof Error &&
|
|
424
|
+
/subscription\s+\S+\s+not\s+found/i.test(error.message);
|
|
425
|
+
if (this.subscriptionId && isStale) {
|
|
426
|
+
this.subscriptionId = undefined;
|
|
427
|
+
this.subscriptionId =
|
|
428
|
+
await this.config.indexerProvider.subscribeForScripts(scriptsToWatch);
|
|
429
|
+
}
|
|
430
|
+
else {
|
|
431
|
+
throw error;
|
|
432
|
+
}
|
|
433
|
+
}
|
|
415
434
|
}
|
|
416
435
|
/**
|
|
417
436
|
* Main listening loop for subscription events.
|
|
@@ -402,6 +402,7 @@ function convertVtxo(vtxo) {
|
|
|
402
402
|
createdAt: new Date(Number(vtxo.createdAt) * 1000),
|
|
403
403
|
isUnrolled: vtxo.isUnrolled,
|
|
404
404
|
isSpent: vtxo.isSpent,
|
|
405
|
+
script: vtxo.script,
|
|
405
406
|
assets: vtxo.assets?.map((a) => ({
|
|
406
407
|
assetId: a.assetId,
|
|
407
408
|
amount: Number(a.amount),
|
|
@@ -115,7 +115,8 @@ export async function buildTransactionHistory(vtxos, allBoardingTxs, commitments
|
|
|
115
115
|
txAmount = spentAmount;
|
|
116
116
|
// TODO: fetch the vtxo with /v1/indexer/vtxos?outpoints=<vtxo.arkTxid:0> to know when the tx was made
|
|
117
117
|
txTime = getTxCreatedAt
|
|
118
|
-
? await getTxCreatedAt(vtxo.arkTxId)
|
|
118
|
+
? ((await getTxCreatedAt(vtxo.arkTxId)) ??
|
|
119
|
+
vtxo.createdAt.getTime() + 1)
|
|
119
120
|
: vtxo.createdAt.getTime() + 1;
|
|
120
121
|
}
|
|
121
122
|
const assets = subtractAssets(allSpent, changes);
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { RestIndexerProvider } from '../../providers/indexer.js';
|
|
2
2
|
import { isExpired, isRecoverable, isSpendable, isSubdust, } from '../index.js';
|
|
3
3
|
import { extendCoin, extendVirtualCoin } from '../utils.js';
|
|
4
|
+
import { buildTransactionHistory } from '../../utils/transactionHistory.js';
|
|
4
5
|
export class WalletNotInitializedError extends Error {
|
|
5
6
|
constructor() {
|
|
6
7
|
super("Wallet handler not initialized");
|
|
@@ -169,7 +170,8 @@ export class WalletMessageHandler {
|
|
|
169
170
|
});
|
|
170
171
|
}
|
|
171
172
|
case "GET_TRANSACTION_HISTORY": {
|
|
172
|
-
const
|
|
173
|
+
const allVtxos = await this.getVtxosFromRepo();
|
|
174
|
+
const transactions = (await this.buildTransactionHistoryFromCache(allVtxos)) ?? [];
|
|
173
175
|
return this.tagged({
|
|
174
176
|
id,
|
|
175
177
|
type: "TRANSACTION_HISTORY",
|
|
@@ -196,7 +198,7 @@ export class WalletMessageHandler {
|
|
|
196
198
|
});
|
|
197
199
|
}
|
|
198
200
|
case "RELOAD_WALLET": {
|
|
199
|
-
await this.
|
|
201
|
+
await this.reloadWallet();
|
|
200
202
|
return this.tagged({
|
|
201
203
|
id,
|
|
202
204
|
type: "RELOAD_SUCCESS",
|
|
@@ -282,6 +284,14 @@ export class WalletMessageHandler {
|
|
|
282
284
|
payload: { isWatching },
|
|
283
285
|
});
|
|
284
286
|
}
|
|
287
|
+
case "REFRESH_VTXOS": {
|
|
288
|
+
const manager = await this.readonlyWallet.getContractManager();
|
|
289
|
+
await manager.refreshVtxos();
|
|
290
|
+
return this.tagged({
|
|
291
|
+
id,
|
|
292
|
+
type: "REFRESH_VTXOS_SUCCESS",
|
|
293
|
+
});
|
|
294
|
+
}
|
|
285
295
|
case "SEND": {
|
|
286
296
|
const { recipients } = message.payload;
|
|
287
297
|
const txid = await this.wallet.send(...recipients);
|
|
@@ -438,10 +448,9 @@ export class WalletMessageHandler {
|
|
|
438
448
|
await this.onWalletInitialized();
|
|
439
449
|
}
|
|
440
450
|
async handleGetBalance() {
|
|
441
|
-
const [boardingUtxos,
|
|
451
|
+
const [boardingUtxos, allVtxos] = await Promise.all([
|
|
442
452
|
this.getAllBoardingUtxos(),
|
|
443
|
-
this.
|
|
444
|
-
this.getSweptVtxos(),
|
|
453
|
+
this.getVtxosFromRepo(),
|
|
445
454
|
]);
|
|
446
455
|
// boarding
|
|
447
456
|
let confirmed = 0;
|
|
@@ -454,7 +463,9 @@ export class WalletMessageHandler {
|
|
|
454
463
|
unconfirmed += utxo.value;
|
|
455
464
|
}
|
|
456
465
|
}
|
|
457
|
-
// offchain
|
|
466
|
+
// offchain — split spendable vs swept from single repo read
|
|
467
|
+
const spendableVtxos = allVtxos.filter(isSpendable);
|
|
468
|
+
const sweptVtxos = allVtxos.filter((vtxo) => vtxo.virtualStatus.state === "swept");
|
|
458
469
|
let settled = 0;
|
|
459
470
|
let preconfirmed = 0;
|
|
460
471
|
let recoverable = 0;
|
|
@@ -504,23 +515,12 @@ export class WalletMessageHandler {
|
|
|
504
515
|
return this.readonlyWallet.getBoardingUtxos();
|
|
505
516
|
}
|
|
506
517
|
/**
|
|
507
|
-
* Get spendable vtxos
|
|
518
|
+
* Get spendable vtxos from the repository
|
|
508
519
|
*/
|
|
509
520
|
async getSpendableVtxos() {
|
|
510
|
-
|
|
511
|
-
return [];
|
|
512
|
-
const vtxos = await this.readonlyWallet.getVtxos();
|
|
521
|
+
const vtxos = await this.getVtxosFromRepo();
|
|
513
522
|
return vtxos.filter(isSpendable);
|
|
514
523
|
}
|
|
515
|
-
/**
|
|
516
|
-
* Get swept vtxos for the current wallet address
|
|
517
|
-
*/
|
|
518
|
-
async getSweptVtxos() {
|
|
519
|
-
if (!this.readonlyWallet)
|
|
520
|
-
return [];
|
|
521
|
-
const vtxos = await this.readonlyWallet.getVtxos();
|
|
522
|
-
return vtxos.filter((vtxo) => vtxo.virtualStatus.state === "swept");
|
|
523
|
-
}
|
|
524
524
|
async onWalletInitialized() {
|
|
525
525
|
if (!this.readonlyWallet ||
|
|
526
526
|
!this.arkProvider ||
|
|
@@ -528,10 +528,11 @@ export class WalletMessageHandler {
|
|
|
528
528
|
!this.walletRepository) {
|
|
529
529
|
return;
|
|
530
530
|
}
|
|
531
|
-
//
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
531
|
+
// Initialize contract manager FIRST — this populates the repository
|
|
532
|
+
// with full VTXO history for all contracts (one indexer call per contract)
|
|
533
|
+
await this.ensureContractEventBroadcasting();
|
|
534
|
+
// Read VTXOs from repository (now populated by contract manager)
|
|
535
|
+
const vtxos = await this.getVtxosFromRepo();
|
|
535
536
|
if (this.wallet) {
|
|
536
537
|
try {
|
|
537
538
|
// recover pending transactions if possible
|
|
@@ -543,15 +544,13 @@ export class WalletMessageHandler {
|
|
|
543
544
|
console.error("Error recovering pending transactions:", error);
|
|
544
545
|
}
|
|
545
546
|
}
|
|
546
|
-
// Get wallet address and save vtxos using unified repository
|
|
547
|
-
const address = await this.readonlyWallet.getAddress();
|
|
548
|
-
await this.walletRepository.saveVtxos(address, vtxos);
|
|
549
547
|
// Fetch boarding utxos and save using unified repository
|
|
550
548
|
const boardingAddress = await this.readonlyWallet.getBoardingAddress();
|
|
551
549
|
const coins = await this.readonlyWallet.onchainProvider.getCoins(boardingAddress);
|
|
552
550
|
await this.walletRepository.saveUtxos(boardingAddress, coins.map((utxo) => extendCoin(this.readonlyWallet, utxo)));
|
|
553
|
-
//
|
|
554
|
-
const
|
|
551
|
+
// Build transaction history from cached VTXOs (no indexer call)
|
|
552
|
+
const address = await this.readonlyWallet.getAddress();
|
|
553
|
+
const txs = await this.buildTransactionHistoryFromCache(vtxos);
|
|
555
554
|
if (txs)
|
|
556
555
|
await this.walletRepository.saveTransactions(address, txs);
|
|
557
556
|
// unsubscribe previous subscription if any
|
|
@@ -596,7 +595,6 @@ export class WalletMessageHandler {
|
|
|
596
595
|
}));
|
|
597
596
|
}
|
|
598
597
|
});
|
|
599
|
-
await this.ensureContractEventBroadcasting();
|
|
600
598
|
// Eagerly start the VtxoManager so its background tasks (auto-renewal,
|
|
601
599
|
// boarding UTXO polling/sweep) run inside the service worker without
|
|
602
600
|
// waiting for a client to send a vtxo-manager message first.
|
|
@@ -609,6 +607,17 @@ export class WalletMessageHandler {
|
|
|
609
607
|
}
|
|
610
608
|
}
|
|
611
609
|
}
|
|
610
|
+
/**
|
|
611
|
+
* Force a full VTXO refresh from the indexer, then re-run bootstrap.
|
|
612
|
+
* Used by RELOAD_WALLET to ensure fresh data.
|
|
613
|
+
*/
|
|
614
|
+
async reloadWallet() {
|
|
615
|
+
if (!this.readonlyWallet)
|
|
616
|
+
return;
|
|
617
|
+
const manager = await this.readonlyWallet.getContractManager();
|
|
618
|
+
await manager.refreshVtxos();
|
|
619
|
+
await this.onWalletInitialized();
|
|
620
|
+
}
|
|
612
621
|
async handleSettle(message) {
|
|
613
622
|
const wallet = this.requireWallet();
|
|
614
623
|
const txid = await wallet.settle(message.payload.params, (e) => {
|
|
@@ -732,6 +741,77 @@ export class WalletMessageHandler {
|
|
|
732
741
|
this.arkProvider = undefined;
|
|
733
742
|
this.indexerProvider = undefined;
|
|
734
743
|
}
|
|
744
|
+
/**
|
|
745
|
+
* Read all VTXOs from the repository, aggregated across all contract
|
|
746
|
+
* addresses and the wallet's primary address, with deduplication.
|
|
747
|
+
*/
|
|
748
|
+
async getVtxosFromRepo() {
|
|
749
|
+
if (!this.walletRepository || !this.readonlyWallet)
|
|
750
|
+
return [];
|
|
751
|
+
const seen = new Set();
|
|
752
|
+
const allVtxos = [];
|
|
753
|
+
const addVtxos = (vtxos) => {
|
|
754
|
+
for (const vtxo of vtxos) {
|
|
755
|
+
const key = `${vtxo.txid}:${vtxo.vout}`;
|
|
756
|
+
if (!seen.has(key)) {
|
|
757
|
+
seen.add(key);
|
|
758
|
+
allVtxos.push(vtxo);
|
|
759
|
+
}
|
|
760
|
+
}
|
|
761
|
+
};
|
|
762
|
+
// Aggregate VTXOs from all contract addresses
|
|
763
|
+
const manager = await this.readonlyWallet.getContractManager();
|
|
764
|
+
const contracts = await manager.getContracts();
|
|
765
|
+
for (const contract of contracts) {
|
|
766
|
+
const vtxos = await this.walletRepository.getVtxos(contract.address);
|
|
767
|
+
addVtxos(vtxos);
|
|
768
|
+
}
|
|
769
|
+
// Also check the wallet's primary address
|
|
770
|
+
const walletAddress = await this.readonlyWallet.getAddress();
|
|
771
|
+
const walletVtxos = await this.walletRepository.getVtxos(walletAddress);
|
|
772
|
+
addVtxos(walletVtxos);
|
|
773
|
+
return allVtxos;
|
|
774
|
+
}
|
|
775
|
+
/**
|
|
776
|
+
* Build transaction history from cached VTXOs without hitting the indexer.
|
|
777
|
+
* Falls back to indexer only for uncached transaction timestamps.
|
|
778
|
+
*/
|
|
779
|
+
async buildTransactionHistoryFromCache(vtxos) {
|
|
780
|
+
if (!this.readonlyWallet)
|
|
781
|
+
return null;
|
|
782
|
+
const { boardingTxs, commitmentsToIgnore } = await this.readonlyWallet.getBoardingTxs();
|
|
783
|
+
// Build a lookup for cached VTXO timestamps, keyed by txid.
|
|
784
|
+
// Multiple VTXOs can share a txid (different vouts) — we keep the
|
|
785
|
+
// earliest createdAt so the history ordering is stable.
|
|
786
|
+
const vtxoCreatedAt = new Map();
|
|
787
|
+
for (const vtxo of vtxos) {
|
|
788
|
+
const existing = vtxoCreatedAt.get(vtxo.txid);
|
|
789
|
+
const ts = vtxo.createdAt.getTime();
|
|
790
|
+
if (existing === undefined || ts < existing) {
|
|
791
|
+
vtxoCreatedAt.set(vtxo.txid, ts);
|
|
792
|
+
}
|
|
793
|
+
}
|
|
794
|
+
// getTxCreatedAt resolves the creation timestamp of a transaction.
|
|
795
|
+
// buildTransactionHistory calls this for spent-offchain VTXOs with
|
|
796
|
+
// no change outputs to determine the time of the sending tx.
|
|
797
|
+
// Returns undefined on miss so buildTransactionHistory uses its
|
|
798
|
+
// own fallback (vtxo.createdAt + 1) rather than epoch 0.
|
|
799
|
+
// The vout:0 lookup in the indexer fallback mirrors the pre-existing
|
|
800
|
+
// convention in ReadonlyWallet.getTransactionHistory().
|
|
801
|
+
const getTxCreatedAt = async (txid) => {
|
|
802
|
+
const cached = vtxoCreatedAt.get(txid);
|
|
803
|
+
if (cached !== undefined)
|
|
804
|
+
return cached;
|
|
805
|
+
if (this.indexerProvider) {
|
|
806
|
+
const res = await this.indexerProvider.getVtxos({
|
|
807
|
+
outpoints: [{ txid, vout: 0 }],
|
|
808
|
+
});
|
|
809
|
+
return res.vtxos[0]?.createdAt.getTime();
|
|
810
|
+
}
|
|
811
|
+
return undefined;
|
|
812
|
+
};
|
|
813
|
+
return buildTransactionHistory(vtxos, boardingTxs, commitmentsToIgnore, getTxCreatedAt);
|
|
814
|
+
}
|
|
735
815
|
async ensureContractEventBroadcasting() {
|
|
736
816
|
if (!this.readonlyWallet)
|
|
737
817
|
return;
|
|
@@ -158,7 +158,10 @@ export class ServiceWorkerReadonlyWallet {
|
|
|
158
158
|
publicKey: initConfig.arkServerPublicKey,
|
|
159
159
|
},
|
|
160
160
|
delegatorUrl: initConfig.delegatorUrl,
|
|
161
|
+
indexerUrl: options.indexerUrl,
|
|
162
|
+
esploraUrl: options.esploraUrl,
|
|
161
163
|
timeoutMs: options.messageBusTimeoutMs,
|
|
164
|
+
watcherConfig: options.watcherConfig,
|
|
162
165
|
}, options.messageBusTimeoutMs);
|
|
163
166
|
// Initialize the wallet handler
|
|
164
167
|
const initMessage = {
|
|
@@ -175,6 +178,9 @@ export class ServiceWorkerReadonlyWallet {
|
|
|
175
178
|
publicKey: initConfig.arkServerPublicKey,
|
|
176
179
|
},
|
|
177
180
|
delegatorUrl: initConfig.delegatorUrl,
|
|
181
|
+
indexerUrl: options.indexerUrl,
|
|
182
|
+
esploraUrl: options.esploraUrl,
|
|
183
|
+
watcherConfig: options.watcherConfig,
|
|
178
184
|
};
|
|
179
185
|
wallet.initWalletPayload = initConfig;
|
|
180
186
|
wallet.messageBusTimeoutMs = options.messageBusTimeoutMs;
|
|
@@ -675,6 +681,14 @@ export class ServiceWorkerReadonlyWallet {
|
|
|
675
681
|
navigator.serviceWorker.removeEventListener("message", messageHandler);
|
|
676
682
|
};
|
|
677
683
|
},
|
|
684
|
+
async refreshVtxos() {
|
|
685
|
+
const message = {
|
|
686
|
+
type: "REFRESH_VTXOS",
|
|
687
|
+
id: getRandomId(),
|
|
688
|
+
tag: messageTag,
|
|
689
|
+
};
|
|
690
|
+
await sendContractMessage(message);
|
|
691
|
+
},
|
|
678
692
|
async isWatching() {
|
|
679
693
|
const message = {
|
|
680
694
|
type: "IS_CONTRACT_MANAGER_WATCHING",
|
|
@@ -744,7 +758,11 @@ export class ServiceWorkerWallet extends ServiceWorkerReadonlyWallet {
|
|
|
744
758
|
publicKey: initConfig.arkServerPublicKey,
|
|
745
759
|
},
|
|
746
760
|
delegatorUrl: initConfig.delegatorUrl,
|
|
761
|
+
indexerUrl: options.indexerUrl,
|
|
762
|
+
esploraUrl: options.esploraUrl,
|
|
747
763
|
timeoutMs: options.messageBusTimeoutMs,
|
|
764
|
+
settlementConfig: options.settlementConfig,
|
|
765
|
+
watcherConfig: options.watcherConfig,
|
|
748
766
|
}, options.messageBusTimeoutMs);
|
|
749
767
|
// Initialize the service worker with the config
|
|
750
768
|
const initMessage = {
|
|
@@ -762,6 +780,10 @@ export class ServiceWorkerWallet extends ServiceWorkerReadonlyWallet {
|
|
|
762
780
|
publicKey: initConfig.arkServerPublicKey,
|
|
763
781
|
},
|
|
764
782
|
delegatorUrl: initConfig.delegatorUrl,
|
|
783
|
+
indexerUrl: options.indexerUrl,
|
|
784
|
+
esploraUrl: options.esploraUrl,
|
|
785
|
+
settlementConfig: options.settlementConfig,
|
|
786
|
+
watcherConfig: options.watcherConfig,
|
|
765
787
|
};
|
|
766
788
|
wallet.initWalletPayload = initConfig;
|
|
767
789
|
wallet.messageBusTimeoutMs = options.messageBusTimeoutMs;
|