@arkade-os/sdk 0.4.18 → 0.4.20
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/contractWatcher.js +7 -1
- package/dist/cjs/contracts/handlers/default.js +10 -3
- package/dist/cjs/contracts/handlers/helpers.js +47 -5
- package/dist/cjs/contracts/handlers/vhtlc.js +4 -2
- package/dist/cjs/identity/descriptor.js +98 -0
- package/dist/cjs/identity/descriptorProvider.js +2 -0
- package/dist/cjs/identity/index.js +15 -1
- package/dist/cjs/identity/seedIdentity.js +91 -6
- package/dist/cjs/identity/serialize.js +166 -0
- package/dist/cjs/identity/staticDescriptorProvider.js +65 -0
- package/dist/cjs/index.js +6 -3
- package/dist/cjs/providers/ark.js +45 -34
- package/dist/cjs/providers/electrum.js +663 -0
- package/dist/cjs/providers/indexer.js +5 -1
- package/dist/cjs/providers/utils.js +4 -0
- package/dist/cjs/wallet/ramps.js +1 -1
- package/dist/cjs/wallet/serviceWorker/wallet-message-handler.js +10 -0
- package/dist/cjs/wallet/serviceWorker/wallet.js +137 -91
- package/dist/cjs/wallet/vtxo-manager.js +133 -17
- package/dist/cjs/wallet/wallet.js +80 -19
- package/dist/cjs/worker/messageBus.js +200 -56
- package/dist/esm/contracts/contractWatcher.js +7 -1
- package/dist/esm/contracts/handlers/default.js +10 -3
- package/dist/esm/contracts/handlers/helpers.js +47 -5
- package/dist/esm/contracts/handlers/vhtlc.js +4 -2
- package/dist/esm/identity/descriptor.js +92 -0
- package/dist/esm/identity/descriptorProvider.js +1 -0
- package/dist/esm/identity/index.js +6 -1
- package/dist/esm/identity/seedIdentity.js +89 -6
- package/dist/esm/identity/serialize.js +159 -0
- package/dist/esm/identity/staticDescriptorProvider.js +61 -0
- package/dist/esm/index.js +2 -1
- package/dist/esm/providers/ark.js +46 -35
- package/dist/esm/providers/electrum.js +658 -0
- package/dist/esm/providers/indexer.js +6 -2
- package/dist/esm/providers/utils.js +3 -0
- package/dist/esm/wallet/ramps.js +1 -1
- package/dist/esm/wallet/serviceWorker/wallet-message-handler.js +10 -0
- package/dist/esm/wallet/serviceWorker/wallet.js +137 -91
- package/dist/esm/wallet/vtxo-manager.js +133 -17
- package/dist/esm/wallet/wallet.js +80 -19
- package/dist/esm/worker/messageBus.js +201 -57
- package/dist/types/contracts/handlers/default.d.ts +1 -1
- package/dist/types/contracts/handlers/helpers.d.ts +1 -1
- package/dist/types/contracts/types.d.ts +11 -3
- package/dist/types/identity/descriptor.d.ts +35 -0
- package/dist/types/identity/descriptorProvider.d.ts +28 -0
- package/dist/types/identity/index.d.ts +7 -1
- package/dist/types/identity/seedIdentity.d.ts +41 -4
- package/dist/types/identity/serialize.d.ts +84 -0
- package/dist/types/identity/staticDescriptorProvider.d.ts +18 -0
- package/dist/types/index.d.ts +4 -2
- package/dist/types/providers/electrum.d.ts +212 -0
- package/dist/types/providers/utils.d.ts +1 -0
- package/dist/types/wallet/serviceWorker/wallet-message-handler.d.ts +11 -2
- package/dist/types/wallet/serviceWorker/wallet.d.ts +27 -10
- package/dist/types/wallet/vtxo-manager.d.ts +15 -0
- package/dist/types/wallet/wallet.d.ts +3 -0
- package/dist/types/worker/messageBus.d.ts +68 -8
- package/package.json +3 -2
package/dist/cjs/wallet/ramps.js
CHANGED
|
@@ -142,7 +142,7 @@ class Ramps {
|
|
|
142
142
|
weight: 0,
|
|
143
143
|
birth: vtxo.createdAt,
|
|
144
144
|
expiry: vtxo.virtualStatus.batchExpiry
|
|
145
|
-
? new Date(vtxo.virtualStatus.batchExpiry
|
|
145
|
+
? new Date(vtxo.virtualStatus.batchExpiry)
|
|
146
146
|
: undefined,
|
|
147
147
|
});
|
|
148
148
|
if (inputFee.satoshis >= vtxo.value) {
|
|
@@ -103,6 +103,16 @@ class WalletMessageHandler {
|
|
|
103
103
|
tag: this.messageTag,
|
|
104
104
|
};
|
|
105
105
|
}
|
|
106
|
+
// Flows that surrender control to the Ark server and the other participants
|
|
107
|
+
// in a batch round: quiet gaps between protocol events can easily exceed
|
|
108
|
+
// the bus-level messageTimeoutMs. Liveness is covered out-of-band by the
|
|
109
|
+
// page-side PING / MESSAGE_BUS_NOT_INITIALIZED path triggered by concurrent
|
|
110
|
+
// short requests (GET_STATUS, GET_BALANCE, ...).
|
|
111
|
+
isLongRunning(message) {
|
|
112
|
+
return (message.type === "SETTLE" ||
|
|
113
|
+
message.type === "RECOVER_VTXOS" ||
|
|
114
|
+
message.type === "RENEW_VTXOS");
|
|
115
|
+
}
|
|
106
116
|
async handleMessage(message) {
|
|
107
117
|
const id = message.id;
|
|
108
118
|
if (message.type === "INIT_WALLET") {
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.ServiceWorkerWallet = exports.ServiceWorkerReadonlyWallet = exports.DEFAULT_MESSAGE_TIMEOUTS = void 0;
|
|
4
4
|
const base_1 = require("@scure/base");
|
|
5
|
+
const identity_1 = require("../../identity");
|
|
5
6
|
const utils_1 = require("../../worker/browser/utils");
|
|
6
7
|
const repositories_1 = require("../../repositories");
|
|
7
8
|
const wallet_message_handler_1 = require("./wallet-message-handler");
|
|
@@ -36,7 +37,10 @@ exports.DEFAULT_MESSAGE_TIMEOUTS = {
|
|
|
36
37
|
GET_EXPIRED_BOARDING_UTXOS: 20000,
|
|
37
38
|
GET_RECOVERABLE_BALANCE: 20000,
|
|
38
39
|
RELOAD_WALLET: 20000,
|
|
39
|
-
// Transactions — need more headroom
|
|
40
|
+
// Transactions — need more headroom.
|
|
41
|
+
// SETTLE / RECOVER_VTXOS / RENEW_VTXOS go through the streaming path and
|
|
42
|
+
// are treated as long-running on both sides of the bus: the values below
|
|
43
|
+
// are retained only for type completeness and are never enforced.
|
|
40
44
|
SEND_BITCOIN: 50000,
|
|
41
45
|
SEND: 50000,
|
|
42
46
|
SETTLE: 50000,
|
|
@@ -81,9 +85,12 @@ function getRequestDedupKey(request) {
|
|
|
81
85
|
const { id, tag, ...rest } = request;
|
|
82
86
|
return JSON.stringify(rest);
|
|
83
87
|
}
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
88
|
+
function isSigningCapable(identity) {
|
|
89
|
+
const candidate = identity;
|
|
90
|
+
return (typeof candidate.signMessage === "function" &&
|
|
91
|
+
typeof candidate.sign === "function" &&
|
|
92
|
+
typeof candidate.signerSession === "function");
|
|
93
|
+
}
|
|
87
94
|
class ServiceWorkerReadonlyAssetManager {
|
|
88
95
|
constructor(sendMessage, messageTag) {
|
|
89
96
|
this.sendMessage = sendMessage;
|
|
@@ -199,55 +206,54 @@ class ServiceWorkerReadonlyWallet {
|
|
|
199
206
|
const messageTag = options.walletUpdaterTag ?? wallet_message_handler_1.DEFAULT_MESSAGE_TAG;
|
|
200
207
|
// Create the wallet instance
|
|
201
208
|
const wallet = new ServiceWorkerReadonlyWallet(options.serviceWorker, options.identity, walletRepository, contractRepository, messageTag);
|
|
209
|
+
const serializedWallet = await (0, identity_1.serializeReadonlyIdentity)(options.identity);
|
|
210
|
+
// INIT_WALLET retains the legacy `key` payload for wire compatibility
|
|
211
|
+
// with older workers; the current handler does not read it.
|
|
202
212
|
const publicKey = await options.identity
|
|
203
213
|
.compressedPublicKey()
|
|
204
214
|
.then(base_1.hex.encode);
|
|
205
|
-
const
|
|
215
|
+
const initWalletPayload = {
|
|
206
216
|
key: { publicKey },
|
|
207
217
|
arkServerUrl: options.arkServerUrl,
|
|
208
218
|
arkServerPublicKey: options.arkServerPublicKey,
|
|
209
219
|
delegatorUrl: options.delegatorUrl,
|
|
210
220
|
};
|
|
211
|
-
//
|
|
212
|
-
|
|
213
|
-
|
|
221
|
+
// Precompute the merged timeout map so page-side waiting and
|
|
222
|
+
// worker-side enforcement are derived from the same source.
|
|
223
|
+
const messageTimeouts = options.messageTimeouts
|
|
224
|
+
? {
|
|
225
|
+
...exports.DEFAULT_MESSAGE_TIMEOUTS,
|
|
226
|
+
...options.messageTimeouts,
|
|
227
|
+
}
|
|
228
|
+
: exports.DEFAULT_MESSAGE_TIMEOUTS;
|
|
229
|
+
const busInitConfig = {
|
|
230
|
+
wallet: serializedWallet,
|
|
214
231
|
arkServer: {
|
|
215
|
-
url:
|
|
216
|
-
publicKey:
|
|
232
|
+
url: options.arkServerUrl,
|
|
233
|
+
publicKey: options.arkServerPublicKey,
|
|
217
234
|
},
|
|
218
|
-
delegatorUrl:
|
|
235
|
+
delegatorUrl: options.delegatorUrl,
|
|
219
236
|
indexerUrl: options.indexerUrl,
|
|
220
237
|
esploraUrl: options.esploraUrl,
|
|
221
|
-
timeoutMs: options.messageBusTimeoutMs,
|
|
222
238
|
watcherConfig: options.watcherConfig,
|
|
223
|
-
|
|
239
|
+
messageTimeouts,
|
|
240
|
+
};
|
|
241
|
+
// Bootstrap the MessageBus in the service worker
|
|
242
|
+
await initializeMessageBus(options.serviceWorker, { ...busInitConfig, timeoutMs: options.messageBusTimeoutMs }, options.messageBusTimeoutMs);
|
|
224
243
|
// Initialize the wallet handler
|
|
225
244
|
const initMessage = {
|
|
226
245
|
tag: messageTag,
|
|
227
246
|
type: "INIT_WALLET",
|
|
228
247
|
id: (0, utils_2.getRandomId)(),
|
|
229
|
-
payload:
|
|
248
|
+
payload: initWalletPayload,
|
|
230
249
|
};
|
|
231
250
|
await wallet.sendMessage(initMessage);
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
publicKey: initConfig.arkServerPublicKey,
|
|
237
|
-
},
|
|
238
|
-
delegatorUrl: initConfig.delegatorUrl,
|
|
239
|
-
indexerUrl: options.indexerUrl,
|
|
240
|
-
esploraUrl: options.esploraUrl,
|
|
241
|
-
watcherConfig: options.watcherConfig,
|
|
242
|
-
};
|
|
243
|
-
wallet.initWalletPayload = initConfig;
|
|
251
|
+
// Persist the full init config (including messageTimeouts) so
|
|
252
|
+
// reinitialize() re-sends the same map to a restarted worker.
|
|
253
|
+
wallet.initConfig = busInitConfig;
|
|
254
|
+
wallet.initWalletPayload = initWalletPayload;
|
|
244
255
|
wallet.messageBusTimeoutMs = options.messageBusTimeoutMs;
|
|
245
|
-
|
|
246
|
-
wallet.messageTimeouts = {
|
|
247
|
-
...exports.DEFAULT_MESSAGE_TIMEOUTS,
|
|
248
|
-
...options.messageTimeouts,
|
|
249
|
-
};
|
|
250
|
-
}
|
|
256
|
+
wallet.messageTimeouts = messageTimeouts;
|
|
251
257
|
return wallet;
|
|
252
258
|
}
|
|
253
259
|
/**
|
|
@@ -306,24 +312,16 @@ class ServiceWorkerReadonlyWallet {
|
|
|
306
312
|
}
|
|
307
313
|
// Like sendMessageDirect but supports streaming responses: intermediate
|
|
308
314
|
// messages are forwarded via onEvent while the promise resolves on the
|
|
309
|
-
// first response for which isComplete returns true.
|
|
310
|
-
//
|
|
311
|
-
//
|
|
312
|
-
|
|
315
|
+
// first response for which isComplete returns true. No inactivity deadline:
|
|
316
|
+
// settlement-class flows surrender control to remote peers and can sit
|
|
317
|
+
// idle for long stretches between protocol events. Service-worker death
|
|
318
|
+
// is detected out-of-band via concurrent short requests that surface
|
|
319
|
+
// MESSAGE_BUS_NOT_INITIALIZED.
|
|
320
|
+
sendMessageStreaming(request, onEvent, isComplete) {
|
|
313
321
|
return new Promise((resolve, reject) => {
|
|
314
|
-
const resetTimeout = () => {
|
|
315
|
-
clearTimeout(timeoutId);
|
|
316
|
-
timeoutId = setTimeout(() => {
|
|
317
|
-
cleanup();
|
|
318
|
-
reject(new errors_1.ServiceWorkerTimeoutError(`Service worker message timed out (${request.type})`));
|
|
319
|
-
}, timeoutMs);
|
|
320
|
-
};
|
|
321
322
|
const cleanup = () => {
|
|
322
|
-
clearTimeout(timeoutId);
|
|
323
323
|
navigator.serviceWorker.removeEventListener("message", messageHandler);
|
|
324
324
|
};
|
|
325
|
-
let timeoutId;
|
|
326
|
-
resetTimeout();
|
|
327
325
|
const messageHandler = (event) => {
|
|
328
326
|
const response = event.data;
|
|
329
327
|
if (request.id !== response.id)
|
|
@@ -338,7 +336,6 @@ class ServiceWorkerReadonlyWallet {
|
|
|
338
336
|
resolve(response);
|
|
339
337
|
}
|
|
340
338
|
else {
|
|
341
|
-
resetTimeout();
|
|
342
339
|
onEvent(response);
|
|
343
340
|
}
|
|
344
341
|
};
|
|
@@ -428,11 +425,10 @@ class ServiceWorkerReadonlyWallet {
|
|
|
428
425
|
await this.reinitialize();
|
|
429
426
|
}
|
|
430
427
|
}
|
|
431
|
-
const timeoutMs = this.getTimeoutForRequest(request);
|
|
432
428
|
const maxRetries = 2;
|
|
433
429
|
for (let attempt = 0;; attempt++) {
|
|
434
430
|
try {
|
|
435
|
-
return await this.sendMessageStreaming(request, onEvent, isComplete
|
|
431
|
+
return await this.sendMessageStreaming(request, onEvent, isComplete);
|
|
436
432
|
}
|
|
437
433
|
catch (error) {
|
|
438
434
|
if (!isMessageBusNotInitializedError(error) ||
|
|
@@ -443,19 +439,68 @@ class ServiceWorkerReadonlyWallet {
|
|
|
443
439
|
}
|
|
444
440
|
}
|
|
445
441
|
}
|
|
442
|
+
/**
|
|
443
|
+
* Produce a serialized envelope for the wallet's identity. The base
|
|
444
|
+
* class always emits a readonly envelope; `ServiceWorkerWallet`
|
|
445
|
+
* overrides to emit a signing envelope.
|
|
446
|
+
*/
|
|
447
|
+
async serializeIdentity() {
|
|
448
|
+
return (0, identity_1.serializeReadonlyIdentity)(this.identity);
|
|
449
|
+
}
|
|
450
|
+
/**
|
|
451
|
+
* Return the cached init config, or rebuild one from live instance
|
|
452
|
+
* state when the cache was never populated. Recovery path for
|
|
453
|
+
* SDK-factory-created wallets; manual constructor bypasses do not
|
|
454
|
+
* retain enough state here and will hit the "never initialized" throw.
|
|
455
|
+
*/
|
|
456
|
+
async buildInitConfig() {
|
|
457
|
+
if (this.initConfig)
|
|
458
|
+
return this.initConfig;
|
|
459
|
+
if (!this.arkServerUrl) {
|
|
460
|
+
throw new Error("Cannot re-initialize: wallet was not initialized via the SDK factory");
|
|
461
|
+
}
|
|
462
|
+
const wallet = await this.serializeIdentity();
|
|
463
|
+
this.initConfig = {
|
|
464
|
+
wallet,
|
|
465
|
+
arkServer: {
|
|
466
|
+
url: this.arkServerUrl,
|
|
467
|
+
publicKey: this.arkServerPublicKey,
|
|
468
|
+
},
|
|
469
|
+
delegatorUrl: this.delegatorUrl,
|
|
470
|
+
indexerUrl: this.indexerUrl,
|
|
471
|
+
esploraUrl: this.esploraUrl,
|
|
472
|
+
watcherConfig: this.watcherConfig,
|
|
473
|
+
settlementConfig: this.settlementConfig,
|
|
474
|
+
};
|
|
475
|
+
return this.initConfig;
|
|
476
|
+
}
|
|
477
|
+
/** Minimal INIT_WALLET payload used on reinitialize when the cache is gone. */
|
|
478
|
+
buildInitWalletPayload() {
|
|
479
|
+
if (this.initWalletPayload)
|
|
480
|
+
return this.initWalletPayload;
|
|
481
|
+
if (!this.arkServerUrl) {
|
|
482
|
+
throw new Error("Cannot re-initialize: wallet was not initialized via the SDK factory");
|
|
483
|
+
}
|
|
484
|
+
this.initWalletPayload = {
|
|
485
|
+
// `key` is deprecated and ignored by the current handler.
|
|
486
|
+
key: {},
|
|
487
|
+
arkServerUrl: this.arkServerUrl,
|
|
488
|
+
arkServerPublicKey: this.arkServerPublicKey,
|
|
489
|
+
};
|
|
490
|
+
return this.initWalletPayload;
|
|
491
|
+
}
|
|
446
492
|
async reinitialize() {
|
|
447
493
|
if (this.reinitPromise)
|
|
448
494
|
return this.reinitPromise;
|
|
449
495
|
this.reinitPromise = (async () => {
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
await initializeMessageBus(this.serviceWorker, this.initConfig, this.messageBusTimeoutMs);
|
|
496
|
+
const config = await this.buildInitConfig();
|
|
497
|
+
const payload = this.buildInitWalletPayload();
|
|
498
|
+
await initializeMessageBus(this.serviceWorker, config, this.messageBusTimeoutMs);
|
|
454
499
|
const initMessage = {
|
|
455
500
|
tag: this.messageTag,
|
|
456
501
|
type: "INIT_WALLET",
|
|
457
502
|
id: (0, utils_2.getRandomId)(),
|
|
458
|
-
payload
|
|
503
|
+
payload,
|
|
459
504
|
};
|
|
460
505
|
await this.sendMessageDirect(initMessage, this.getTimeoutForRequest(initMessage));
|
|
461
506
|
})().finally(() => {
|
|
@@ -818,71 +863,72 @@ class ServiceWorkerWallet extends ServiceWorkerReadonlyWallet {
|
|
|
818
863
|
get assetManager() {
|
|
819
864
|
return this._assetManager;
|
|
820
865
|
}
|
|
866
|
+
async serializeIdentity() {
|
|
867
|
+
return (0, identity_1.serializeSigningIdentity)(this.identity);
|
|
868
|
+
}
|
|
821
869
|
static async create(options) {
|
|
822
870
|
const walletRepository = options.storage?.walletRepository ??
|
|
823
871
|
new repositories_1.IndexedDBWalletRepository();
|
|
824
872
|
const contractRepository = options.storage?.contractRepository ??
|
|
825
873
|
new repositories_1.IndexedDBContractRepository();
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
? options.identity
|
|
829
|
-
: null;
|
|
830
|
-
if (!identity) {
|
|
831
|
-
throw new Error("ServiceWorkerWallet.create() requires a Identity that can expose a single private key");
|
|
874
|
+
if (!isSigningCapable(options.identity)) {
|
|
875
|
+
throw new Error("ServiceWorkerWallet.create() requires a signing Identity; got a ReadonlyIdentity");
|
|
832
876
|
}
|
|
833
|
-
|
|
834
|
-
const
|
|
877
|
+
const identity = options.identity;
|
|
878
|
+
const serializedWallet = (0, identity_1.serializeSigningIdentity)(identity);
|
|
835
879
|
const messageTag = options.walletUpdaterTag ?? wallet_message_handler_1.DEFAULT_MESSAGE_TAG;
|
|
836
880
|
// Create the wallet instance
|
|
837
881
|
const wallet = new ServiceWorkerWallet(options.serviceWorker, identity, walletRepository, contractRepository, messageTag, !!options.delegatorUrl);
|
|
838
|
-
|
|
839
|
-
|
|
882
|
+
// INIT_WALLET retains the legacy `key` payload for wire compatibility
|
|
883
|
+
// with older workers; the current handler does not read it, and only
|
|
884
|
+
// SingleKey-style identities can populate it. Kept optional so seed /
|
|
885
|
+
// mnemonic identities simply omit it.
|
|
886
|
+
const legacyPrivateKey = serializedWallet.type === "single-key"
|
|
887
|
+
? serializedWallet.privateKey
|
|
888
|
+
: null;
|
|
889
|
+
const initWalletPayload = {
|
|
890
|
+
key: legacyPrivateKey ? { privateKey: legacyPrivateKey } : {},
|
|
840
891
|
arkServerUrl: options.arkServerUrl,
|
|
841
892
|
arkServerPublicKey: options.arkServerPublicKey,
|
|
842
893
|
delegatorUrl: options.delegatorUrl,
|
|
843
894
|
};
|
|
844
|
-
|
|
845
|
-
|
|
895
|
+
// Precompute the merged timeout map so page-side waiting and
|
|
896
|
+
// worker-side enforcement are derived from the same source.
|
|
897
|
+
const messageTimeouts = options.messageTimeouts
|
|
898
|
+
? {
|
|
899
|
+
...exports.DEFAULT_MESSAGE_TIMEOUTS,
|
|
900
|
+
...options.messageTimeouts,
|
|
901
|
+
}
|
|
902
|
+
: exports.DEFAULT_MESSAGE_TIMEOUTS;
|
|
903
|
+
const busInitConfig = {
|
|
904
|
+
wallet: serializedWallet,
|
|
846
905
|
arkServer: {
|
|
847
|
-
url:
|
|
848
|
-
publicKey:
|
|
906
|
+
url: options.arkServerUrl,
|
|
907
|
+
publicKey: options.arkServerPublicKey,
|
|
849
908
|
},
|
|
850
|
-
delegatorUrl:
|
|
909
|
+
delegatorUrl: options.delegatorUrl,
|
|
851
910
|
indexerUrl: options.indexerUrl,
|
|
852
911
|
esploraUrl: options.esploraUrl,
|
|
853
|
-
timeoutMs: options.messageBusTimeoutMs,
|
|
854
912
|
settlementConfig: options.settlementConfig,
|
|
855
913
|
watcherConfig: options.watcherConfig,
|
|
856
|
-
|
|
914
|
+
messageTimeouts,
|
|
915
|
+
};
|
|
916
|
+
await initializeMessageBus(options.serviceWorker, { ...busInitConfig, timeoutMs: options.messageBusTimeoutMs }, options.messageBusTimeoutMs);
|
|
857
917
|
// Initialize the service worker with the config
|
|
858
918
|
const initMessage = {
|
|
859
919
|
tag: messageTag,
|
|
860
920
|
type: "INIT_WALLET",
|
|
861
921
|
id: (0, utils_2.getRandomId)(),
|
|
862
|
-
payload:
|
|
922
|
+
payload: initWalletPayload,
|
|
863
923
|
};
|
|
864
924
|
// Initialize the service worker
|
|
865
925
|
await wallet.sendMessage(initMessage);
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
publicKey: initConfig.arkServerPublicKey,
|
|
871
|
-
},
|
|
872
|
-
delegatorUrl: initConfig.delegatorUrl,
|
|
873
|
-
indexerUrl: options.indexerUrl,
|
|
874
|
-
esploraUrl: options.esploraUrl,
|
|
875
|
-
settlementConfig: options.settlementConfig,
|
|
876
|
-
watcherConfig: options.watcherConfig,
|
|
877
|
-
};
|
|
878
|
-
wallet.initWalletPayload = initConfig;
|
|
926
|
+
// Persist the full init config (including messageTimeouts) so
|
|
927
|
+
// reinitialize() re-sends the same map to a restarted worker.
|
|
928
|
+
wallet.initConfig = busInitConfig;
|
|
929
|
+
wallet.initWalletPayload = initWalletPayload;
|
|
879
930
|
wallet.messageBusTimeoutMs = options.messageBusTimeoutMs;
|
|
880
|
-
|
|
881
|
-
wallet.messageTimeouts = {
|
|
882
|
-
...exports.DEFAULT_MESSAGE_TIMEOUTS,
|
|
883
|
-
...options.messageTimeouts,
|
|
884
|
-
};
|
|
885
|
-
}
|
|
931
|
+
wallet.messageTimeouts = messageTimeouts;
|
|
886
932
|
return wallet;
|
|
887
933
|
}
|
|
888
934
|
/**
|
|
@@ -10,6 +10,8 @@ const base_1 = require("@scure/base");
|
|
|
10
10
|
const base_2 = require("../script/base");
|
|
11
11
|
const transaction_1 = require("../utils/transaction");
|
|
12
12
|
const txSizeEstimator_1 = require("../utils/txSizeEstimator");
|
|
13
|
+
const arkfee_1 = require("../arkfee");
|
|
14
|
+
const address_1 = require("../script/address");
|
|
13
15
|
/**
|
|
14
16
|
* Return whether a wallet exposes the properties required for boarding input sweep operations.
|
|
15
17
|
*
|
|
@@ -19,6 +21,7 @@ const txSizeEstimator_1 = require("../utils/txSizeEstimator");
|
|
|
19
21
|
function isSweepCapable(wallet) {
|
|
20
22
|
return ("boardingTapscript" in wallet &&
|
|
21
23
|
"onchainProvider" in wallet &&
|
|
24
|
+
"arkProvider" in wallet &&
|
|
22
25
|
"network" in wallet);
|
|
23
26
|
}
|
|
24
27
|
/**
|
|
@@ -29,7 +32,7 @@ function isSweepCapable(wallet) {
|
|
|
29
32
|
*/
|
|
30
33
|
function assertSweepCapable(wallet) {
|
|
31
34
|
if (!isSweepCapable(wallet)) {
|
|
32
|
-
throw new Error("Boarding UTXO sweep requires a Wallet instance with boardingTapscript, onchainProvider, and network");
|
|
35
|
+
throw new Error("Boarding UTXO sweep requires a Wallet instance with boardingTapscript, onchainProvider, arkProvider, and network");
|
|
33
36
|
}
|
|
34
37
|
}
|
|
35
38
|
/**
|
|
@@ -232,6 +235,11 @@ class VtxoManager {
|
|
|
232
235
|
// because they now ride on the same settle intent.
|
|
233
236
|
this.lastPeriodicSettleTimestamp = 0;
|
|
234
237
|
this.consecutivePeriodicSettleFailures = 0;
|
|
238
|
+
// Throttle for the VTXO_ALREADY_SPENT -> refreshVtxos() reconciliation.
|
|
239
|
+
// The server's authoritative view says our local cache is stale, so we
|
|
240
|
+
// trigger a full refresh to advance the global sync cursor. Rate-limit
|
|
241
|
+
// to guard against a buggy indexer cycling us into a refresh storm.
|
|
242
|
+
this.lastVtxoSpentRefreshTimestamp = 0;
|
|
235
243
|
// Normalize: prefer settlementConfig, fall back to renewalConfig, default to enabled
|
|
236
244
|
if (settlementConfig !== undefined) {
|
|
237
245
|
this.settlementConfig = settlementConfig;
|
|
@@ -622,6 +630,10 @@ class VtxoManager {
|
|
|
622
630
|
getOnchainProvider() {
|
|
623
631
|
return this.getSweepWallet().onchainProvider;
|
|
624
632
|
}
|
|
633
|
+
/** Returns the Ark provider for intent fee and server info lookups. */
|
|
634
|
+
getArkProvider() {
|
|
635
|
+
return this.getSweepWallet().arkProvider;
|
|
636
|
+
}
|
|
625
637
|
/** Returns the Bitcoin network configuration from the wallet. */
|
|
626
638
|
getNetwork() {
|
|
627
639
|
return this.getSweepWallet().network;
|
|
@@ -667,7 +679,6 @@ class VtxoManager {
|
|
|
667
679
|
return;
|
|
668
680
|
}
|
|
669
681
|
if (e.message.includes("VTXO_ALREADY_REGISTERED") ||
|
|
670
|
-
e.message.includes("VTXO_ALREADY_SPENT") ||
|
|
671
682
|
e.message.includes("duplicated input")) {
|
|
672
683
|
// Virtual output is already being used in a concurrent
|
|
673
684
|
// user-initiated operation. Skip silently — the
|
|
@@ -675,6 +686,14 @@ class VtxoManager {
|
|
|
675
686
|
// renewal will retry on the next cycle.
|
|
676
687
|
return;
|
|
677
688
|
}
|
|
689
|
+
if (e.message.includes("VTXO_ALREADY_SPENT")) {
|
|
690
|
+
// Our local VTXO cache is stale vs. the
|
|
691
|
+
// server's authoritative view. Trigger a
|
|
692
|
+
// throttled refresh to reconcile, then skip
|
|
693
|
+
// — the next cycle will see fresh data.
|
|
694
|
+
void this.maybeRefreshAfterVtxoSpent();
|
|
695
|
+
return;
|
|
696
|
+
}
|
|
678
697
|
}
|
|
679
698
|
console.error("Error renewing VTXOs:", e);
|
|
680
699
|
});
|
|
@@ -692,6 +711,39 @@ class VtxoManager {
|
|
|
692
711
|
return undefined;
|
|
693
712
|
}
|
|
694
713
|
}
|
|
714
|
+
/**
|
|
715
|
+
* VTXO_ALREADY_SPENT means the server's authoritative view of VTXO state
|
|
716
|
+
* is ahead of ours — cross-instance race, pre-lock snapshot drift, or an
|
|
717
|
+
* SSE gap left stale data in the local cache. Silent-swallowing guarantees
|
|
718
|
+
* the same error on the next cycle because nothing reconciles the cache,
|
|
719
|
+
* so instead we trigger a full refreshVtxos() to advance the global sync
|
|
720
|
+
* cursor. Throttled to prevent a buggy indexer from causing a refresh
|
|
721
|
+
* storm.
|
|
722
|
+
*/
|
|
723
|
+
maybeRefreshAfterVtxoSpent() {
|
|
724
|
+
if (this.vtxoSpentRefreshPromise) {
|
|
725
|
+
return this.vtxoSpentRefreshPromise;
|
|
726
|
+
}
|
|
727
|
+
const now = Date.now();
|
|
728
|
+
if (now - this.lastVtxoSpentRefreshTimestamp <
|
|
729
|
+
VtxoManager.VTXO_SPENT_REFRESH_COOLDOWN_MS) {
|
|
730
|
+
return Promise.resolve();
|
|
731
|
+
}
|
|
732
|
+
this.lastVtxoSpentRefreshTimestamp = now;
|
|
733
|
+
this.vtxoSpentRefreshPromise = (async () => {
|
|
734
|
+
try {
|
|
735
|
+
const contractManager = await this.wallet.getContractManager();
|
|
736
|
+
await contractManager.refreshVtxos();
|
|
737
|
+
}
|
|
738
|
+
catch (e) {
|
|
739
|
+
console.error("Error refreshing VTXOs after VTXO_ALREADY_SPENT:", e);
|
|
740
|
+
}
|
|
741
|
+
finally {
|
|
742
|
+
this.vtxoSpentRefreshPromise = undefined;
|
|
743
|
+
}
|
|
744
|
+
})();
|
|
745
|
+
return this.vtxoSpentRefreshPromise;
|
|
746
|
+
}
|
|
695
747
|
/** Computes the next poll delay, applying exponential backoff on failures. */
|
|
696
748
|
getNextPollDelay() {
|
|
697
749
|
if (this.settlementConfig === false)
|
|
@@ -824,7 +876,8 @@ class VtxoManager {
|
|
|
824
876
|
catch (e) {
|
|
825
877
|
throw e instanceof Error ? e : new Error(String(e));
|
|
826
878
|
}
|
|
827
|
-
const unsettledBoarding = boardingUtxos.filter((u) =>
|
|
879
|
+
const unsettledBoarding = boardingUtxos.filter((u) => u.status.confirmed &&
|
|
880
|
+
!this.knownBoardingUtxos.has(`${u.txid}:${u.vout}`) &&
|
|
828
881
|
!expiredSet.has(`${u.txid}:${u.vout}`));
|
|
829
882
|
// Collect near-expiry VTXOs unless the event-driven path is mid-renewal.
|
|
830
883
|
// Skipping when renewalInProgress avoids double-submitting the same VTXOs.
|
|
@@ -850,13 +903,54 @@ class VtxoManager {
|
|
|
850
903
|
return;
|
|
851
904
|
}
|
|
852
905
|
const dustAmount = getDustAmount(this.wallet);
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
906
|
+
// Fetch server intent-fee config so each input/output can be priced.
|
|
907
|
+
// Without this, settle sends `outputAmount = sum(inputs)` and the
|
|
908
|
+
// server rejects with INTENT_INSUFFICIENT_FEE whenever the operator
|
|
909
|
+
// charges non-zero intent fees.
|
|
910
|
+
const { fees } = await this.getArkProvider().getInfo();
|
|
911
|
+
const estimator = new arkfee_1.Estimator(fees.intentFee);
|
|
912
|
+
let totalAmount = 0n;
|
|
913
|
+
const filteredBoarding = [];
|
|
914
|
+
for (const u of unsettledBoarding) {
|
|
915
|
+
const inputFee = estimator.evalOnchainInput({
|
|
916
|
+
amount: BigInt(u.value),
|
|
917
|
+
});
|
|
918
|
+
if (inputFee.value >= BigInt(u.value)) {
|
|
919
|
+
// Fee exceeds input value — including it would drain the output.
|
|
920
|
+
continue;
|
|
921
|
+
}
|
|
922
|
+
filteredBoarding.push(u);
|
|
923
|
+
totalAmount += BigInt(u.value) - BigInt(inputFee.satoshis);
|
|
924
|
+
}
|
|
925
|
+
const filteredVtxos = [];
|
|
926
|
+
for (const v of expiringVtxos) {
|
|
927
|
+
const inputFee = estimator.evalOffchainInput({
|
|
928
|
+
amount: BigInt(v.value),
|
|
929
|
+
type: v.virtualStatus.state === "swept" ? "recoverable" : "vtxo",
|
|
930
|
+
weight: 0,
|
|
931
|
+
birth: v.createdAt,
|
|
932
|
+
expiry: v.virtualStatus.batchExpiry
|
|
933
|
+
? new Date(v.virtualStatus.batchExpiry)
|
|
934
|
+
: undefined,
|
|
935
|
+
});
|
|
936
|
+
if (inputFee.satoshis >= v.value) {
|
|
937
|
+
continue;
|
|
938
|
+
}
|
|
939
|
+
filteredVtxos.push(v);
|
|
940
|
+
totalAmount += BigInt(v.value) - BigInt(inputFee.satoshis);
|
|
941
|
+
}
|
|
942
|
+
if (filteredBoarding.length === 0 && filteredVtxos.length === 0) {
|
|
857
943
|
return;
|
|
944
|
+
}
|
|
858
945
|
const arkAddress = await this.wallet.getAddress();
|
|
859
|
-
const
|
|
946
|
+
const outputFee = estimator.evalOffchainOutput({
|
|
947
|
+
amount: totalAmount,
|
|
948
|
+
script: base_1.hex.encode(address_1.ArkAddress.decode(arkAddress).pkScript),
|
|
949
|
+
});
|
|
950
|
+
totalAmount -= BigInt(outputFee.satoshis);
|
|
951
|
+
if (totalAmount < dustAmount)
|
|
952
|
+
return;
|
|
953
|
+
const includesVtxos = filteredVtxos.length > 0;
|
|
860
954
|
// Block the event-driven renewal path while this settle is in flight
|
|
861
955
|
// when VTXOs are part of the intent. Mirrors renewVtxos()'s guard so
|
|
862
956
|
// the two paths can't race on the same VTXO inputs.
|
|
@@ -864,16 +958,34 @@ class VtxoManager {
|
|
|
864
958
|
this.renewalInProgress = true;
|
|
865
959
|
}
|
|
866
960
|
let success = false;
|
|
961
|
+
let staleCacheSkip = false;
|
|
867
962
|
try {
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
963
|
+
try {
|
|
964
|
+
await this.wallet.settle({
|
|
965
|
+
inputs: [...filteredBoarding, ...filteredVtxos],
|
|
966
|
+
outputs: [{ address: arkAddress, amount: totalAmount }],
|
|
967
|
+
});
|
|
968
|
+
// Mark boarding inputs as known only after successful settle.
|
|
969
|
+
for (const u of filteredBoarding) {
|
|
970
|
+
this.knownBoardingUtxos.add(`${u.txid}:${u.vout}`);
|
|
971
|
+
}
|
|
972
|
+
success = true;
|
|
973
|
+
}
|
|
974
|
+
catch (e) {
|
|
975
|
+
if (e instanceof Error &&
|
|
976
|
+
e.message.includes("VTXO_ALREADY_SPENT")) {
|
|
977
|
+
// Local VTXO cache is stale vs. the server's
|
|
978
|
+
// authoritative view — not a transient failure.
|
|
979
|
+
// Trigger a throttled refresh and skip this cycle
|
|
980
|
+
// without bumping the failure counter, so the next
|
|
981
|
+
// poll can retry once the cache reconciles.
|
|
982
|
+
staleCacheSkip = true;
|
|
983
|
+
void this.maybeRefreshAfterVtxoSpent();
|
|
984
|
+
}
|
|
985
|
+
else {
|
|
986
|
+
throw e;
|
|
987
|
+
}
|
|
875
988
|
}
|
|
876
|
-
success = true;
|
|
877
989
|
}
|
|
878
990
|
finally {
|
|
879
991
|
this.lastPeriodicSettleTimestamp = Date.now();
|
|
@@ -888,7 +1000,10 @@ class VtxoManager {
|
|
|
888
1000
|
if (success) {
|
|
889
1001
|
this.consecutivePeriodicSettleFailures = 0;
|
|
890
1002
|
}
|
|
891
|
-
else {
|
|
1003
|
+
else if (!staleCacheSkip) {
|
|
1004
|
+
// Don't bump on stale-cache skip: it's not a transient
|
|
1005
|
+
// failure, and the next cycle should try immediately
|
|
1006
|
+
// after the refresh lands.
|
|
892
1007
|
this.consecutivePeriodicSettleFailures++;
|
|
893
1008
|
}
|
|
894
1009
|
}
|
|
@@ -926,3 +1041,4 @@ VtxoManager.MAX_BACKOFF_MS = 5 * 60 * 1000; // 5 minutes
|
|
|
926
1041
|
VtxoManager.RENEWAL_COOLDOWN_MS = 30000; // 30 seconds
|
|
927
1042
|
VtxoManager.PERIODIC_SETTLE_COOLDOWN_MS = 30000;
|
|
928
1043
|
VtxoManager.PERIODIC_SETTLE_MAX_BACKOFF_MS = 5 * 60 * 1000;
|
|
1044
|
+
VtxoManager.VTXO_SPENT_REFRESH_COOLDOWN_MS = 30000;
|