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