@arkade-os/sdk 0.4.7 → 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/identity/seedIdentity.js +2 -2
- package/dist/cjs/index.js +9 -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 +249 -36
- package/dist/cjs/wallet/serviceWorker/wallet.js +286 -34
- package/dist/cjs/wallet/vtxo-manager.js +123 -86
- package/dist/cjs/wallet/wallet.js +140 -68
- package/dist/cjs/worker/errors.js +17 -0
- package/dist/cjs/worker/messageBus.js +14 -2
- package/dist/esm/contracts/contractManager.js +59 -11
- package/dist/esm/contracts/contractWatcher.js +21 -2
- package/dist/esm/identity/seedIdentity.js +2 -2
- package/dist/esm/index.js +3 -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 +245 -35
- package/dist/esm/wallet/serviceWorker/wallet.js +286 -34
- package/dist/esm/wallet/vtxo-manager.js +123 -86
- package/dist/esm/wallet/wallet.js +140 -68
- package/dist/esm/worker/errors.js +12 -0
- package/dist/esm/worker/messageBus.js +14 -2
- package/dist/types/contracts/contractManager.d.ts +10 -0
- package/dist/types/identity/seedIdentity.d.ts +5 -2
- package/dist/types/index.d.ts +5 -4
- 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 +101 -7
- package/dist/types/wallet/serviceWorker/wallet.d.ts +16 -0
- package/dist/types/wallet/vtxo-manager.d.ts +29 -2
- package/dist/types/wallet/wallet.d.ts +10 -0
- package/dist/types/worker/errors.d.ts +6 -0
- package/dist/types/worker/messageBus.d.ts +6 -0
- package/package.json +1 -1
|
@@ -74,9 +74,10 @@ class ContractManager {
|
|
|
74
74
|
}
|
|
75
75
|
// Load persisted contracts
|
|
76
76
|
const contracts = await this.config.contractRepository.getContracts();
|
|
77
|
-
// fetch
|
|
77
|
+
// fetch all VTXOs (including spent/swept) for all contracts,
|
|
78
|
+
// so the repository has full history for transaction history and balance
|
|
78
79
|
// TODO: what if the user has 1k contracts?
|
|
79
|
-
await this.
|
|
80
|
+
await this.fetchContractVxosFromIndexer(contracts, true);
|
|
80
81
|
// add all contracts to the watcher
|
|
81
82
|
const now = Date.now();
|
|
82
83
|
for (const contract of contracts) {
|
|
@@ -139,8 +140,8 @@ class ContractManager {
|
|
|
139
140
|
};
|
|
140
141
|
// Persist
|
|
141
142
|
await this.config.contractRepository.saveContract(contract);
|
|
142
|
-
//
|
|
143
|
-
await this.
|
|
143
|
+
// fetch all VTXOs (including spent/swept) for this contract
|
|
144
|
+
await this.fetchContractVxosFromIndexer([contract], true);
|
|
144
145
|
// Add to watcher
|
|
145
146
|
await this.watcher.addContract(contract);
|
|
146
147
|
return contract;
|
|
@@ -305,6 +306,14 @@ class ContractManager {
|
|
|
305
306
|
this.eventCallbacks.delete(callback);
|
|
306
307
|
};
|
|
307
308
|
}
|
|
309
|
+
/**
|
|
310
|
+
* Force a full VTXO refresh from the indexer for all contracts.
|
|
311
|
+
* Populates the wallet repository with complete VTXO history.
|
|
312
|
+
*/
|
|
313
|
+
async refreshVtxos() {
|
|
314
|
+
const contracts = await this.config.contractRepository.getContracts();
|
|
315
|
+
await this.fetchContractVxosFromIndexer(contracts, true);
|
|
316
|
+
}
|
|
308
317
|
/**
|
|
309
318
|
* Check if currently watching.
|
|
310
319
|
*/
|
|
@@ -334,11 +343,13 @@ class ContractManager {
|
|
|
334
343
|
case "vtxo_spent":
|
|
335
344
|
await this.fetchContractVxosFromIndexer([event.contract], true);
|
|
336
345
|
break;
|
|
337
|
-
case "connection_reset":
|
|
338
|
-
// Refetch all VTXOs for all active
|
|
346
|
+
case "connection_reset": {
|
|
347
|
+
// Refetch all VTXOs (including spent/swept) for all active
|
|
348
|
+
// contracts so the repo stays consistent with bootstrap state
|
|
339
349
|
const activeWatchedContracts = this.watcher.getActiveContracts();
|
|
340
|
-
await this.fetchContractVxosFromIndexer(activeWatchedContracts,
|
|
350
|
+
await this.fetchContractVxosFromIndexer(activeWatchedContracts, true);
|
|
341
351
|
break;
|
|
352
|
+
}
|
|
342
353
|
case "contract_expired":
|
|
343
354
|
// just update DB
|
|
344
355
|
await this.config.contractRepository.saveContract(event.contract);
|
|
@@ -365,11 +376,48 @@ class ContractManager {
|
|
|
365
376
|
return result;
|
|
366
377
|
}
|
|
367
378
|
async fetchContractVtxosBulk(contracts, includeSpent) {
|
|
368
|
-
|
|
369
|
-
|
|
379
|
+
if (contracts.length === 0) {
|
|
380
|
+
return new Map();
|
|
381
|
+
}
|
|
382
|
+
// For a single contract, use the paginated path directly.
|
|
383
|
+
if (contracts.length === 1) {
|
|
384
|
+
const contract = contracts[0];
|
|
370
385
|
const vtxos = await this.fetchContractVtxosPaginated(contract, includeSpent);
|
|
371
|
-
|
|
372
|
-
}
|
|
386
|
+
return new Map([[contract.script, vtxos]]);
|
|
387
|
+
}
|
|
388
|
+
// For multiple contracts, batch all scripts into a single indexer call
|
|
389
|
+
// per page to minimise round-trips. Results are keyed by script so we
|
|
390
|
+
// can distribute them back to the correct contract afterwards.
|
|
391
|
+
const scriptToContract = new Map(contracts.map((c) => [c.script, c]));
|
|
392
|
+
const result = new Map(contracts.map((c) => [c.script, []]));
|
|
393
|
+
const scripts = contracts.map((c) => c.script);
|
|
394
|
+
const pageSize = 100;
|
|
395
|
+
const opts = includeSpent ? {} : { spendableOnly: true };
|
|
396
|
+
let pageIndex = 0;
|
|
397
|
+
let hasMore = true;
|
|
398
|
+
while (hasMore) {
|
|
399
|
+
const { vtxos, page } = await this.config.indexerProvider.getVtxos({
|
|
400
|
+
scripts,
|
|
401
|
+
...opts,
|
|
402
|
+
pageIndex,
|
|
403
|
+
pageSize,
|
|
404
|
+
});
|
|
405
|
+
for (const vtxo of vtxos) {
|
|
406
|
+
// Match the VTXO back to its contract via the script field
|
|
407
|
+
// populated by the indexer.
|
|
408
|
+
if (!vtxo.script)
|
|
409
|
+
continue;
|
|
410
|
+
const contract = scriptToContract.get(vtxo.script);
|
|
411
|
+
if (!contract)
|
|
412
|
+
continue;
|
|
413
|
+
result.get(contract.script).push({
|
|
414
|
+
...(0, utils_1.extendVtxoFromContract)(vtxo, contract),
|
|
415
|
+
contractScript: contract.script,
|
|
416
|
+
});
|
|
417
|
+
}
|
|
418
|
+
hasMore = page ? vtxos.length === pageSize : false;
|
|
419
|
+
pageIndex++;
|
|
420
|
+
}
|
|
373
421
|
return result;
|
|
374
422
|
}
|
|
375
423
|
async fetchContractVtxosPaginated(contract, includeSpent) {
|
|
@@ -413,8 +413,27 @@ class ContractWatcher {
|
|
|
413
413
|
}
|
|
414
414
|
return;
|
|
415
415
|
}
|
|
416
|
-
|
|
417
|
-
|
|
416
|
+
try {
|
|
417
|
+
this.subscriptionId =
|
|
418
|
+
await this.config.indexerProvider.subscribeForScripts(scriptsToWatch, this.subscriptionId);
|
|
419
|
+
}
|
|
420
|
+
catch (error) {
|
|
421
|
+
// If we sent a stale subscription ID that the server no longer
|
|
422
|
+
// recognises, clear it and retry to create a fresh subscription.
|
|
423
|
+
// The server currently returns HTTP 500 with a JSON body whose
|
|
424
|
+
// message field looks like "subscription <uuid> not found".
|
|
425
|
+
// All other errors (network failures, parse errors, etc.) are rethrown.
|
|
426
|
+
const isStale = error instanceof Error &&
|
|
427
|
+
/subscription\s+\S+\s+not\s+found/i.test(error.message);
|
|
428
|
+
if (this.subscriptionId && isStale) {
|
|
429
|
+
this.subscriptionId = undefined;
|
|
430
|
+
this.subscriptionId =
|
|
431
|
+
await this.config.indexerProvider.subscribeForScripts(scriptsToWatch);
|
|
432
|
+
}
|
|
433
|
+
else {
|
|
434
|
+
throw error;
|
|
435
|
+
}
|
|
436
|
+
}
|
|
418
437
|
}
|
|
419
438
|
/**
|
|
420
439
|
* Main listening loop for subscription events.
|
|
@@ -105,7 +105,7 @@ class SeedIdentity {
|
|
|
105
105
|
static fromSeed(seed, opts) {
|
|
106
106
|
const descriptor = hasDescriptor(opts)
|
|
107
107
|
? opts.descriptor
|
|
108
|
-
: buildDescriptor(seed, opts.isMainnet);
|
|
108
|
+
: buildDescriptor(seed, opts.isMainnet ?? true);
|
|
109
109
|
return new SeedIdentity(seed, descriptor);
|
|
110
110
|
}
|
|
111
111
|
async xOnlyPublicKey() {
|
|
@@ -194,7 +194,7 @@ class MnemonicIdentity extends SeedIdentity {
|
|
|
194
194
|
const seed = (0, bip39_1.mnemonicToSeedSync)(phrase, passphrase);
|
|
195
195
|
const descriptor = hasDescriptor(opts)
|
|
196
196
|
? opts.descriptor
|
|
197
|
-
: buildDescriptor(seed, opts.isMainnet);
|
|
197
|
+
: buildDescriptor(seed, opts.isMainnet ?? true);
|
|
198
198
|
return new MnemonicIdentity(seed, descriptor);
|
|
199
199
|
}
|
|
200
200
|
}
|
package/dist/cjs/index.js
CHANGED
|
@@ -36,8 +36,9 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
36
36
|
};
|
|
37
37
|
})();
|
|
38
38
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
39
|
-
exports.
|
|
40
|
-
exports.
|
|
39
|
+
exports.VtxoTaprootTree = exports.VtxoTreeExpiry = exports.CosignerPublicKey = exports.getArkPsbtFields = exports.setArkPsbtField = exports.ArkPsbtFieldKeyType = exports.ArkPsbtFieldKey = exports.TapTreeCoder = exports.CLTVMultisigTapscript = exports.ConditionMultisigTapscript = exports.ConditionCSVMultisigTapscript = exports.CSVMultisigTapscript = exports.MultisigTapscript = exports.decodeTapscript = exports.ServiceWorkerReadonlyWallet = exports.ServiceWorkerWallet = exports.ServiceWorkerTimeoutError = exports.MessageBusNotInitializedError = exports.DelegatorNotConfiguredError = exports.ReadonlyWalletError = exports.WalletNotInitializedError = exports.WalletMessageHandler = exports.MessageBus = exports.setupServiceWorker = exports.SettlementEventType = exports.ChainTxType = exports.IndexerTxType = exports.TxType = exports.VHTLC = exports.VtxoScript = exports.DelegateVtxo = exports.DefaultVtxo = exports.ArkAddress = exports.RestIndexerProvider = exports.RestArkProvider = exports.EsploraProvider = exports.ESPLORA_URL = exports.RestDelegatorProvider = exports.DelegatorManagerImpl = exports.VtxoManager = exports.Ramps = exports.OnchainWallet = exports.ReadonlyDescriptorIdentity = exports.MnemonicIdentity = exports.SeedIdentity = exports.ReadonlySingleKey = exports.SingleKey = exports.ReadonlyWallet = exports.Wallet = exports.asset = void 0;
|
|
40
|
+
exports.contractFromArkContractWithAddress = exports.contractFromArkContract = exports.decodeArkContract = exports.encodeArkContract = exports.VHTLCContractHandler = exports.DelegateContractHandler = exports.DefaultContractHandler = exports.contractHandlers = exports.ContractWatcher = exports.ContractManager = exports.getSequence = exports.isExpired = exports.isSubdust = exports.isSpendable = exports.isRecoverable = exports.buildForfeitTx = exports.validateConnectorsTxGraph = exports.validateVtxoTxGraph = exports.Batch = exports.maybeArkError = exports.ArkError = exports.Transaction = exports.Unroll = exports.P2A = exports.TxTree = exports.BIP322 = exports.Intent = exports.ContractRepositoryImpl = exports.WalletRepositoryImpl = exports.rollbackMigration = exports.getMigrationStatus = exports.requiresMigration = exports.migrateWalletRepository = exports.MIGRATION_KEY = exports.InMemoryContractRepository = exports.InMemoryWalletRepository = exports.IndexedDBContractRepository = exports.IndexedDBWalletRepository = exports.openDatabase = exports.closeDatabase = exports.networks = exports.ArkNote = exports.isValidArkAddress = exports.isVtxoExpiringSoon = exports.combineTapscriptSigs = exports.hasBoardingTxExpired = exports.waitForIncomingFunds = exports.verifyTapscriptSignatures = exports.buildOffchainTx = exports.ConditionWitness = void 0;
|
|
41
|
+
exports.isArkContract = void 0;
|
|
41
42
|
const transaction_1 = require("./utils/transaction");
|
|
42
43
|
Object.defineProperty(exports, "Transaction", { enumerable: true, get: function () { return transaction_1.Transaction; } });
|
|
43
44
|
const singleKey_1 = require("./identity/singleKey");
|
|
@@ -175,3 +176,9 @@ Object.defineProperty(exports, "closeDatabase", { enumerable: true, get: functio
|
|
|
175
176
|
Object.defineProperty(exports, "openDatabase", { enumerable: true, get: function () { return manager_1.openDatabase; } });
|
|
176
177
|
const wallet_message_handler_1 = require("./wallet/serviceWorker/wallet-message-handler");
|
|
177
178
|
Object.defineProperty(exports, "WalletMessageHandler", { enumerable: true, get: function () { return wallet_message_handler_1.WalletMessageHandler; } });
|
|
179
|
+
Object.defineProperty(exports, "WalletNotInitializedError", { enumerable: true, get: function () { return wallet_message_handler_1.WalletNotInitializedError; } });
|
|
180
|
+
Object.defineProperty(exports, "ReadonlyWalletError", { enumerable: true, get: function () { return wallet_message_handler_1.ReadonlyWalletError; } });
|
|
181
|
+
Object.defineProperty(exports, "DelegatorNotConfiguredError", { enumerable: true, get: function () { return wallet_message_handler_1.DelegatorNotConfiguredError; } });
|
|
182
|
+
const errors_2 = require("./worker/errors");
|
|
183
|
+
Object.defineProperty(exports, "MessageBusNotInitializedError", { enumerable: true, get: function () { return errors_2.MessageBusNotInitializedError; } });
|
|
184
|
+
Object.defineProperty(exports, "ServiceWorkerTimeoutError", { enumerable: true, get: function () { return errors_2.ServiceWorkerTimeoutError; } });
|
|
@@ -406,6 +406,7 @@ function convertVtxo(vtxo) {
|
|
|
406
406
|
createdAt: new Date(Number(vtxo.createdAt) * 1000),
|
|
407
407
|
isUnrolled: vtxo.isUnrolled,
|
|
408
408
|
isSpent: vtxo.isSpent,
|
|
409
|
+
script: vtxo.script,
|
|
409
410
|
assets: vtxo.assets?.map((a) => ({
|
|
410
411
|
assetId: a.assetId,
|
|
411
412
|
amount: Number(a.amount),
|
|
@@ -118,7 +118,8 @@ async function buildTransactionHistory(vtxos, allBoardingTxs, commitmentsToIgnor
|
|
|
118
118
|
txAmount = spentAmount;
|
|
119
119
|
// TODO: fetch the vtxo with /v1/indexer/vtxos?outpoints=<vtxo.arkTxid:0> to know when the tx was made
|
|
120
120
|
txTime = getTxCreatedAt
|
|
121
|
-
? await getTxCreatedAt(vtxo.arkTxId)
|
|
121
|
+
? ((await getTxCreatedAt(vtxo.arkTxId)) ??
|
|
122
|
+
vtxo.createdAt.getTime() + 1)
|
|
122
123
|
: vtxo.createdAt.getTime() + 1;
|
|
123
124
|
}
|
|
124
125
|
const assets = subtractAssets(allSpent, changes);
|
|
@@ -1,9 +1,31 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.WalletMessageHandler = exports.DEFAULT_MESSAGE_TAG = void 0;
|
|
3
|
+
exports.WalletMessageHandler = exports.DEFAULT_MESSAGE_TAG = exports.DelegatorNotConfiguredError = exports.ReadonlyWalletError = exports.WalletNotInitializedError = void 0;
|
|
4
4
|
const indexer_1 = require("../../providers/indexer");
|
|
5
5
|
const index_1 = require("../index");
|
|
6
6
|
const utils_1 = require("../utils");
|
|
7
|
+
const transactionHistory_1 = require("../../utils/transactionHistory");
|
|
8
|
+
class WalletNotInitializedError extends Error {
|
|
9
|
+
constructor() {
|
|
10
|
+
super("Wallet handler not initialized");
|
|
11
|
+
this.name = "WalletNotInitializedError";
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
exports.WalletNotInitializedError = WalletNotInitializedError;
|
|
15
|
+
class ReadonlyWalletError extends Error {
|
|
16
|
+
constructor() {
|
|
17
|
+
super("Read-only wallet: operation requires signing");
|
|
18
|
+
this.name = "ReadonlyWalletError";
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
exports.ReadonlyWalletError = ReadonlyWalletError;
|
|
22
|
+
class DelegatorNotConfiguredError extends Error {
|
|
23
|
+
constructor() {
|
|
24
|
+
super("Delegator not configured");
|
|
25
|
+
this.name = "DelegatorNotConfiguredError";
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
exports.DelegatorNotConfiguredError = DelegatorNotConfiguredError;
|
|
7
29
|
exports.DEFAULT_MESSAGE_TAG = "WALLET_UPDATER";
|
|
8
30
|
class WalletMessageHandler {
|
|
9
31
|
/**
|
|
@@ -24,7 +46,31 @@ class WalletMessageHandler {
|
|
|
24
46
|
this.walletRepository = repositories.walletRepository;
|
|
25
47
|
}
|
|
26
48
|
async stop() {
|
|
27
|
-
|
|
49
|
+
if (this.incomingFundsSubscription) {
|
|
50
|
+
this.incomingFundsSubscription();
|
|
51
|
+
this.incomingFundsSubscription = undefined;
|
|
52
|
+
}
|
|
53
|
+
if (this.contractEventsSubscription) {
|
|
54
|
+
this.contractEventsSubscription();
|
|
55
|
+
this.contractEventsSubscription = undefined;
|
|
56
|
+
}
|
|
57
|
+
// Dispose the wallet to stop VtxoManager background tasks
|
|
58
|
+
// (auto-renewal, boarding UTXO polling) and ContractWatcher.
|
|
59
|
+
try {
|
|
60
|
+
if (this.wallet) {
|
|
61
|
+
await this.wallet.dispose();
|
|
62
|
+
}
|
|
63
|
+
else if (this.readonlyWallet) {
|
|
64
|
+
await this.readonlyWallet.dispose();
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
catch (_) {
|
|
68
|
+
// best-effort teardown
|
|
69
|
+
}
|
|
70
|
+
this.wallet = undefined;
|
|
71
|
+
this.readonlyWallet = undefined;
|
|
72
|
+
this.arkProvider = undefined;
|
|
73
|
+
this.indexerProvider = undefined;
|
|
28
74
|
}
|
|
29
75
|
async tick(_now) {
|
|
30
76
|
const results = await Promise.allSettled(this.onNextTick.map((fn) => fn()));
|
|
@@ -47,7 +93,7 @@ class WalletMessageHandler {
|
|
|
47
93
|
}
|
|
48
94
|
requireWallet() {
|
|
49
95
|
if (!this.wallet) {
|
|
50
|
-
throw new
|
|
96
|
+
throw new ReadonlyWalletError();
|
|
51
97
|
}
|
|
52
98
|
return this.wallet;
|
|
53
99
|
}
|
|
@@ -69,7 +115,7 @@ class WalletMessageHandler {
|
|
|
69
115
|
if (!this.readonlyWallet) {
|
|
70
116
|
return this.tagged({
|
|
71
117
|
id,
|
|
72
|
-
error: new
|
|
118
|
+
error: new WalletNotInitializedError(),
|
|
73
119
|
});
|
|
74
120
|
}
|
|
75
121
|
try {
|
|
@@ -130,7 +176,8 @@ class WalletMessageHandler {
|
|
|
130
176
|
});
|
|
131
177
|
}
|
|
132
178
|
case "GET_TRANSACTION_HISTORY": {
|
|
133
|
-
const
|
|
179
|
+
const allVtxos = await this.getVtxosFromRepo();
|
|
180
|
+
const transactions = (await this.buildTransactionHistoryFromCache(allVtxos)) ?? [];
|
|
134
181
|
return this.tagged({
|
|
135
182
|
id,
|
|
136
183
|
type: "TRANSACTION_HISTORY",
|
|
@@ -157,7 +204,7 @@ class WalletMessageHandler {
|
|
|
157
204
|
});
|
|
158
205
|
}
|
|
159
206
|
case "RELOAD_WALLET": {
|
|
160
|
-
await this.
|
|
207
|
+
await this.reloadWallet();
|
|
161
208
|
return this.tagged({
|
|
162
209
|
id,
|
|
163
210
|
type: "RELOAD_SUCCESS",
|
|
@@ -243,6 +290,14 @@ class WalletMessageHandler {
|
|
|
243
290
|
payload: { isWatching },
|
|
244
291
|
});
|
|
245
292
|
}
|
|
293
|
+
case "REFRESH_VTXOS": {
|
|
294
|
+
const manager = await this.readonlyWallet.getContractManager();
|
|
295
|
+
await manager.refreshVtxos();
|
|
296
|
+
return this.tagged({
|
|
297
|
+
id,
|
|
298
|
+
type: "REFRESH_VTXOS_SUCCESS",
|
|
299
|
+
});
|
|
300
|
+
}
|
|
246
301
|
case "SEND": {
|
|
247
302
|
const { recipients } = message.payload;
|
|
248
303
|
const txid = await this.wallet.send(...recipients);
|
|
@@ -297,7 +352,7 @@ class WalletMessageHandler {
|
|
|
297
352
|
const wallet = this.requireWallet();
|
|
298
353
|
const delegatorManager = await wallet.getDelegatorManager();
|
|
299
354
|
if (!delegatorManager) {
|
|
300
|
-
throw new
|
|
355
|
+
throw new DelegatorNotConfiguredError();
|
|
301
356
|
}
|
|
302
357
|
const info = await delegatorManager.getDelegateInfo();
|
|
303
358
|
return this.tagged({
|
|
@@ -306,6 +361,83 @@ class WalletMessageHandler {
|
|
|
306
361
|
payload: { info },
|
|
307
362
|
});
|
|
308
363
|
}
|
|
364
|
+
case "RECOVER_VTXOS": {
|
|
365
|
+
const wallet = this.requireWallet();
|
|
366
|
+
const vtxoManager = await wallet.getVtxoManager();
|
|
367
|
+
const txid = await vtxoManager.recoverVtxos((e) => {
|
|
368
|
+
this.scheduleForNextTick(() => this.tagged({
|
|
369
|
+
id,
|
|
370
|
+
type: "RECOVER_VTXOS_EVENT",
|
|
371
|
+
payload: e,
|
|
372
|
+
}));
|
|
373
|
+
});
|
|
374
|
+
return this.tagged({
|
|
375
|
+
id,
|
|
376
|
+
type: "RECOVER_VTXOS_SUCCESS",
|
|
377
|
+
payload: { txid },
|
|
378
|
+
});
|
|
379
|
+
}
|
|
380
|
+
case "GET_RECOVERABLE_BALANCE": {
|
|
381
|
+
const wallet = this.requireWallet();
|
|
382
|
+
const vtxoManager = await wallet.getVtxoManager();
|
|
383
|
+
const balance = await vtxoManager.getRecoverableBalance();
|
|
384
|
+
return this.tagged({
|
|
385
|
+
id,
|
|
386
|
+
type: "RECOVERABLE_BALANCE",
|
|
387
|
+
payload: {
|
|
388
|
+
recoverable: balance.recoverable.toString(),
|
|
389
|
+
subdust: balance.subdust.toString(),
|
|
390
|
+
includesSubdust: balance.includesSubdust,
|
|
391
|
+
vtxoCount: balance.vtxoCount,
|
|
392
|
+
},
|
|
393
|
+
});
|
|
394
|
+
}
|
|
395
|
+
case "GET_EXPIRING_VTXOS": {
|
|
396
|
+
const wallet = this.requireWallet();
|
|
397
|
+
const vtxoManager = await wallet.getVtxoManager();
|
|
398
|
+
const vtxos = await vtxoManager.getExpiringVtxos(message.payload.thresholdMs);
|
|
399
|
+
return this.tagged({
|
|
400
|
+
id,
|
|
401
|
+
type: "EXPIRING_VTXOS",
|
|
402
|
+
payload: { vtxos },
|
|
403
|
+
});
|
|
404
|
+
}
|
|
405
|
+
case "RENEW_VTXOS": {
|
|
406
|
+
const wallet = this.requireWallet();
|
|
407
|
+
const vtxoManager = await wallet.getVtxoManager();
|
|
408
|
+
const txid = await vtxoManager.renewVtxos((e) => {
|
|
409
|
+
this.scheduleForNextTick(() => this.tagged({
|
|
410
|
+
id,
|
|
411
|
+
type: "RENEW_VTXOS_EVENT",
|
|
412
|
+
payload: e,
|
|
413
|
+
}));
|
|
414
|
+
});
|
|
415
|
+
return this.tagged({
|
|
416
|
+
id,
|
|
417
|
+
type: "RENEW_VTXOS_SUCCESS",
|
|
418
|
+
payload: { txid },
|
|
419
|
+
});
|
|
420
|
+
}
|
|
421
|
+
case "GET_EXPIRED_BOARDING_UTXOS": {
|
|
422
|
+
const wallet = this.requireWallet();
|
|
423
|
+
const vtxoManager = await wallet.getVtxoManager();
|
|
424
|
+
const utxos = await vtxoManager.getExpiredBoardingUtxos();
|
|
425
|
+
return this.tagged({
|
|
426
|
+
id,
|
|
427
|
+
type: "EXPIRED_BOARDING_UTXOS",
|
|
428
|
+
payload: { utxos },
|
|
429
|
+
});
|
|
430
|
+
}
|
|
431
|
+
case "SWEEP_EXPIRED_BOARDING_UTXOS": {
|
|
432
|
+
const wallet = this.requireWallet();
|
|
433
|
+
const vtxoManager = await wallet.getVtxoManager();
|
|
434
|
+
const txid = await vtxoManager.sweepExpiredBoardingUtxos();
|
|
435
|
+
return this.tagged({
|
|
436
|
+
id,
|
|
437
|
+
type: "SWEEP_EXPIRED_BOARDING_UTXOS_SUCCESS",
|
|
438
|
+
payload: { txid },
|
|
439
|
+
});
|
|
440
|
+
}
|
|
309
441
|
default:
|
|
310
442
|
console.error("Unknown message type", message);
|
|
311
443
|
throw new Error("Unknown message");
|
|
@@ -322,10 +454,9 @@ class WalletMessageHandler {
|
|
|
322
454
|
await this.onWalletInitialized();
|
|
323
455
|
}
|
|
324
456
|
async handleGetBalance() {
|
|
325
|
-
const [boardingUtxos,
|
|
457
|
+
const [boardingUtxos, allVtxos] = await Promise.all([
|
|
326
458
|
this.getAllBoardingUtxos(),
|
|
327
|
-
this.
|
|
328
|
-
this.getSweptVtxos(),
|
|
459
|
+
this.getVtxosFromRepo(),
|
|
329
460
|
]);
|
|
330
461
|
// boarding
|
|
331
462
|
let confirmed = 0;
|
|
@@ -338,7 +469,9 @@ class WalletMessageHandler {
|
|
|
338
469
|
unconfirmed += utxo.value;
|
|
339
470
|
}
|
|
340
471
|
}
|
|
341
|
-
// offchain
|
|
472
|
+
// offchain — split spendable vs swept from single repo read
|
|
473
|
+
const spendableVtxos = allVtxos.filter(index_1.isSpendable);
|
|
474
|
+
const sweptVtxos = allVtxos.filter((vtxo) => vtxo.virtualStatus.state === "swept");
|
|
342
475
|
let settled = 0;
|
|
343
476
|
let preconfirmed = 0;
|
|
344
477
|
let recoverable = 0;
|
|
@@ -388,23 +521,12 @@ class WalletMessageHandler {
|
|
|
388
521
|
return this.readonlyWallet.getBoardingUtxos();
|
|
389
522
|
}
|
|
390
523
|
/**
|
|
391
|
-
* Get spendable vtxos
|
|
524
|
+
* Get spendable vtxos from the repository
|
|
392
525
|
*/
|
|
393
526
|
async getSpendableVtxos() {
|
|
394
|
-
|
|
395
|
-
return [];
|
|
396
|
-
const vtxos = await this.readonlyWallet.getVtxos();
|
|
527
|
+
const vtxos = await this.getVtxosFromRepo();
|
|
397
528
|
return vtxos.filter(index_1.isSpendable);
|
|
398
529
|
}
|
|
399
|
-
/**
|
|
400
|
-
* Get swept vtxos for the current wallet address
|
|
401
|
-
*/
|
|
402
|
-
async getSweptVtxos() {
|
|
403
|
-
if (!this.readonlyWallet)
|
|
404
|
-
return [];
|
|
405
|
-
const vtxos = await this.readonlyWallet.getVtxos();
|
|
406
|
-
return vtxos.filter((vtxo) => vtxo.virtualStatus.state === "swept");
|
|
407
|
-
}
|
|
408
530
|
async onWalletInitialized() {
|
|
409
531
|
if (!this.readonlyWallet ||
|
|
410
532
|
!this.arkProvider ||
|
|
@@ -412,10 +534,11 @@ class WalletMessageHandler {
|
|
|
412
534
|
!this.walletRepository) {
|
|
413
535
|
return;
|
|
414
536
|
}
|
|
415
|
-
//
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
537
|
+
// Initialize contract manager FIRST — this populates the repository
|
|
538
|
+
// with full VTXO history for all contracts (one indexer call per contract)
|
|
539
|
+
await this.ensureContractEventBroadcasting();
|
|
540
|
+
// Read VTXOs from repository (now populated by contract manager)
|
|
541
|
+
const vtxos = await this.getVtxosFromRepo();
|
|
419
542
|
if (this.wallet) {
|
|
420
543
|
try {
|
|
421
544
|
// recover pending transactions if possible
|
|
@@ -427,15 +550,13 @@ class WalletMessageHandler {
|
|
|
427
550
|
console.error("Error recovering pending transactions:", error);
|
|
428
551
|
}
|
|
429
552
|
}
|
|
430
|
-
// Get wallet address and save vtxos using unified repository
|
|
431
|
-
const address = await this.readonlyWallet.getAddress();
|
|
432
|
-
await this.walletRepository.saveVtxos(address, vtxos);
|
|
433
553
|
// Fetch boarding utxos and save using unified repository
|
|
434
554
|
const boardingAddress = await this.readonlyWallet.getBoardingAddress();
|
|
435
555
|
const coins = await this.readonlyWallet.onchainProvider.getCoins(boardingAddress);
|
|
436
556
|
await this.walletRepository.saveUtxos(boardingAddress, coins.map((utxo) => (0, utils_1.extendCoin)(this.readonlyWallet, utxo)));
|
|
437
|
-
//
|
|
438
|
-
const
|
|
557
|
+
// Build transaction history from cached VTXOs (no indexer call)
|
|
558
|
+
const address = await this.readonlyWallet.getAddress();
|
|
559
|
+
const txs = await this.buildTransactionHistoryFromCache(vtxos);
|
|
439
560
|
if (txs)
|
|
440
561
|
await this.walletRepository.saveTransactions(address, txs);
|
|
441
562
|
// unsubscribe previous subscription if any
|
|
@@ -480,7 +601,28 @@ class WalletMessageHandler {
|
|
|
480
601
|
}));
|
|
481
602
|
}
|
|
482
603
|
});
|
|
483
|
-
|
|
604
|
+
// Eagerly start the VtxoManager so its background tasks (auto-renewal,
|
|
605
|
+
// boarding UTXO polling/sweep) run inside the service worker without
|
|
606
|
+
// waiting for a client to send a vtxo-manager message first.
|
|
607
|
+
if (this.wallet) {
|
|
608
|
+
try {
|
|
609
|
+
await this.wallet.getVtxoManager();
|
|
610
|
+
}
|
|
611
|
+
catch (error) {
|
|
612
|
+
console.error("Error starting VtxoManager:", error);
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
/**
|
|
617
|
+
* Force a full VTXO refresh from the indexer, then re-run bootstrap.
|
|
618
|
+
* Used by RELOAD_WALLET to ensure fresh data.
|
|
619
|
+
*/
|
|
620
|
+
async reloadWallet() {
|
|
621
|
+
if (!this.readonlyWallet)
|
|
622
|
+
return;
|
|
623
|
+
const manager = await this.readonlyWallet.getContractManager();
|
|
624
|
+
await manager.refreshVtxos();
|
|
625
|
+
await this.onWalletInitialized();
|
|
484
626
|
}
|
|
485
627
|
async handleSettle(message) {
|
|
486
628
|
const wallet = this.requireWallet();
|
|
@@ -523,7 +665,7 @@ class WalletMessageHandler {
|
|
|
523
665
|
const wallet = this.requireWallet();
|
|
524
666
|
const delegatorManager = await wallet.getDelegatorManager();
|
|
525
667
|
if (!delegatorManager) {
|
|
526
|
-
throw new
|
|
668
|
+
throw new DelegatorNotConfiguredError();
|
|
527
669
|
}
|
|
528
670
|
const { vtxoOutpoints, destination, delegateAt } = message.payload;
|
|
529
671
|
const allVtxos = await wallet.getVtxos();
|
|
@@ -550,7 +692,7 @@ class WalletMessageHandler {
|
|
|
550
692
|
}
|
|
551
693
|
async handleGetVtxos(message) {
|
|
552
694
|
if (!this.readonlyWallet) {
|
|
553
|
-
throw new
|
|
695
|
+
throw new WalletNotInitializedError();
|
|
554
696
|
}
|
|
555
697
|
const vtxos = await this.getSpendableVtxos();
|
|
556
698
|
const dustAmount = this.readonlyWallet.dustAmount;
|
|
@@ -605,6 +747,77 @@ class WalletMessageHandler {
|
|
|
605
747
|
this.arkProvider = undefined;
|
|
606
748
|
this.indexerProvider = undefined;
|
|
607
749
|
}
|
|
750
|
+
/**
|
|
751
|
+
* Read all VTXOs from the repository, aggregated across all contract
|
|
752
|
+
* addresses and the wallet's primary address, with deduplication.
|
|
753
|
+
*/
|
|
754
|
+
async getVtxosFromRepo() {
|
|
755
|
+
if (!this.walletRepository || !this.readonlyWallet)
|
|
756
|
+
return [];
|
|
757
|
+
const seen = new Set();
|
|
758
|
+
const allVtxos = [];
|
|
759
|
+
const addVtxos = (vtxos) => {
|
|
760
|
+
for (const vtxo of vtxos) {
|
|
761
|
+
const key = `${vtxo.txid}:${vtxo.vout}`;
|
|
762
|
+
if (!seen.has(key)) {
|
|
763
|
+
seen.add(key);
|
|
764
|
+
allVtxos.push(vtxo);
|
|
765
|
+
}
|
|
766
|
+
}
|
|
767
|
+
};
|
|
768
|
+
// Aggregate VTXOs from all contract addresses
|
|
769
|
+
const manager = await this.readonlyWallet.getContractManager();
|
|
770
|
+
const contracts = await manager.getContracts();
|
|
771
|
+
for (const contract of contracts) {
|
|
772
|
+
const vtxos = await this.walletRepository.getVtxos(contract.address);
|
|
773
|
+
addVtxos(vtxos);
|
|
774
|
+
}
|
|
775
|
+
// Also check the wallet's primary address
|
|
776
|
+
const walletAddress = await this.readonlyWallet.getAddress();
|
|
777
|
+
const walletVtxos = await this.walletRepository.getVtxos(walletAddress);
|
|
778
|
+
addVtxos(walletVtxos);
|
|
779
|
+
return allVtxos;
|
|
780
|
+
}
|
|
781
|
+
/**
|
|
782
|
+
* Build transaction history from cached VTXOs without hitting the indexer.
|
|
783
|
+
* Falls back to indexer only for uncached transaction timestamps.
|
|
784
|
+
*/
|
|
785
|
+
async buildTransactionHistoryFromCache(vtxos) {
|
|
786
|
+
if (!this.readonlyWallet)
|
|
787
|
+
return null;
|
|
788
|
+
const { boardingTxs, commitmentsToIgnore } = await this.readonlyWallet.getBoardingTxs();
|
|
789
|
+
// Build a lookup for cached VTXO timestamps, keyed by txid.
|
|
790
|
+
// Multiple VTXOs can share a txid (different vouts) — we keep the
|
|
791
|
+
// earliest createdAt so the history ordering is stable.
|
|
792
|
+
const vtxoCreatedAt = new Map();
|
|
793
|
+
for (const vtxo of vtxos) {
|
|
794
|
+
const existing = vtxoCreatedAt.get(vtxo.txid);
|
|
795
|
+
const ts = vtxo.createdAt.getTime();
|
|
796
|
+
if (existing === undefined || ts < existing) {
|
|
797
|
+
vtxoCreatedAt.set(vtxo.txid, ts);
|
|
798
|
+
}
|
|
799
|
+
}
|
|
800
|
+
// getTxCreatedAt resolves the creation timestamp of a transaction.
|
|
801
|
+
// buildTransactionHistory calls this for spent-offchain VTXOs with
|
|
802
|
+
// no change outputs to determine the time of the sending tx.
|
|
803
|
+
// Returns undefined on miss so buildTransactionHistory uses its
|
|
804
|
+
// own fallback (vtxo.createdAt + 1) rather than epoch 0.
|
|
805
|
+
// The vout:0 lookup in the indexer fallback mirrors the pre-existing
|
|
806
|
+
// convention in ReadonlyWallet.getTransactionHistory().
|
|
807
|
+
const getTxCreatedAt = async (txid) => {
|
|
808
|
+
const cached = vtxoCreatedAt.get(txid);
|
|
809
|
+
if (cached !== undefined)
|
|
810
|
+
return cached;
|
|
811
|
+
if (this.indexerProvider) {
|
|
812
|
+
const res = await this.indexerProvider.getVtxos({
|
|
813
|
+
outpoints: [{ txid, vout: 0 }],
|
|
814
|
+
});
|
|
815
|
+
return res.vtxos[0]?.createdAt.getTime();
|
|
816
|
+
}
|
|
817
|
+
return undefined;
|
|
818
|
+
};
|
|
819
|
+
return (0, transactionHistory_1.buildTransactionHistory)(vtxos, boardingTxs, commitmentsToIgnore, getTxCreatedAt);
|
|
820
|
+
}
|
|
608
821
|
async ensureContractEventBroadcasting() {
|
|
609
822
|
if (!this.readonlyWallet)
|
|
610
823
|
return;
|