@arkade-os/sdk 0.3.0-alpha.8 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +48 -14
- package/dist/cjs/arknote/index.js +3 -3
- package/dist/cjs/forfeit.js +2 -2
- package/dist/cjs/identity/singleKey.js +8 -8
- package/dist/cjs/index.js +13 -5
- package/dist/cjs/{bip322 → intent}/index.js +38 -61
- package/dist/cjs/musig2/index.js +2 -1
- package/dist/cjs/musig2/nonces.js +4 -0
- package/dist/cjs/providers/ark.js +76 -45
- package/dist/cjs/providers/errors.js +59 -0
- package/dist/cjs/providers/expoArk.js +15 -170
- package/dist/cjs/providers/expoIndexer.js +22 -111
- package/dist/cjs/providers/expoUtils.js +124 -0
- package/dist/cjs/providers/onchain.js +19 -20
- package/dist/cjs/repositories/walletRepository.js +64 -28
- package/dist/cjs/script/base.js +15 -7
- package/dist/cjs/script/tapscript.js +20 -21
- package/dist/cjs/script/vhtlc.js +2 -2
- package/dist/cjs/tree/signingSession.js +44 -11
- package/dist/cjs/tree/txTree.js +3 -4
- package/dist/cjs/tree/validation.js +2 -3
- package/dist/cjs/utils/arkTransaction.js +105 -15
- package/dist/cjs/utils/transaction.js +28 -0
- package/dist/cjs/utils/unknownFields.js +7 -7
- package/dist/cjs/wallet/onchain.js +6 -7
- package/dist/cjs/wallet/serviceWorker/response.js +32 -0
- package/dist/cjs/wallet/serviceWorker/utils.js +2 -0
- package/dist/cjs/wallet/serviceWorker/wallet.js +7 -8
- package/dist/cjs/wallet/serviceWorker/worker.js +46 -27
- package/dist/cjs/wallet/unroll.js +7 -9
- package/dist/cjs/wallet/utils.js +9 -0
- package/dist/cjs/wallet/vtxo-manager.js +323 -0
- package/dist/cjs/wallet/wallet.js +98 -125
- package/dist/esm/arknote/index.js +2 -2
- package/dist/esm/forfeit.js +1 -1
- package/dist/esm/identity/singleKey.js +9 -9
- package/dist/esm/index.js +14 -10
- package/dist/esm/{bip322 → intent}/index.js +32 -54
- package/dist/esm/musig2/index.js +1 -1
- package/dist/esm/musig2/nonces.js +3 -0
- package/dist/esm/providers/ark.js +76 -45
- package/dist/esm/providers/errors.js +54 -0
- package/dist/esm/providers/expoArk.js +15 -137
- package/dist/esm/providers/expoIndexer.js +22 -78
- package/dist/esm/providers/expoUtils.js +87 -0
- package/dist/esm/providers/onchain.js +19 -20
- package/dist/esm/repositories/walletRepository.js +64 -28
- package/dist/esm/script/base.js +12 -4
- package/dist/esm/script/tapscript.js +1 -2
- package/dist/esm/script/vhtlc.js +1 -1
- package/dist/esm/tree/signingSession.js +45 -12
- package/dist/esm/tree/txTree.js +3 -4
- package/dist/esm/tree/validation.js +2 -3
- package/dist/esm/utils/arkTransaction.js +97 -8
- package/dist/esm/utils/transaction.js +24 -0
- package/dist/esm/utils/unknownFields.js +3 -3
- package/dist/esm/wallet/onchain.js +3 -4
- package/dist/esm/wallet/serviceWorker/response.js +32 -0
- package/dist/esm/wallet/serviceWorker/utils.js +1 -0
- package/dist/esm/wallet/serviceWorker/wallet.js +8 -9
- package/dist/esm/wallet/serviceWorker/worker.js +48 -29
- package/dist/esm/wallet/unroll.js +5 -7
- package/dist/esm/wallet/utils.js +8 -0
- package/dist/esm/wallet/vtxo-manager.js +317 -0
- package/dist/esm/wallet/wallet.js +92 -119
- package/dist/types/arknote/index.d.ts +1 -1
- package/dist/types/forfeit.d.ts +2 -2
- package/dist/types/identity/index.d.ts +2 -2
- package/dist/types/identity/singleKey.d.ts +2 -2
- package/dist/types/index.d.ts +9 -7
- package/dist/types/intent/index.d.ts +41 -0
- package/dist/types/musig2/index.d.ts +1 -1
- package/dist/types/musig2/nonces.d.ts +1 -0
- package/dist/types/providers/ark.d.ts +62 -26
- package/dist/types/providers/errors.d.ts +13 -0
- package/dist/types/providers/expoIndexer.d.ts +2 -10
- package/dist/types/providers/expoUtils.d.ts +18 -0
- package/dist/types/providers/indexer.d.ts +1 -9
- package/dist/types/providers/onchain.d.ts +6 -2
- package/dist/types/repositories/walletRepository.d.ts +9 -5
- package/dist/types/script/base.d.ts +5 -2
- package/dist/types/tree/signingSession.d.ts +16 -11
- package/dist/types/utils/anchor.d.ts +2 -2
- package/dist/types/utils/arkTransaction.d.ts +12 -4
- package/dist/types/utils/transaction.d.ts +13 -0
- package/dist/types/utils/unknownFields.d.ts +4 -4
- package/dist/types/wallet/index.d.ts +6 -4
- package/dist/types/wallet/onchain.d.ts +1 -1
- package/dist/types/wallet/serviceWorker/response.d.ts +16 -2
- package/dist/types/wallet/serviceWorker/utils.d.ts +1 -0
- package/dist/types/wallet/serviceWorker/wallet.d.ts +2 -2
- package/dist/types/wallet/serviceWorker/worker.d.ts +7 -1
- package/dist/types/wallet/unroll.d.ts +1 -1
- package/dist/types/wallet/utils.d.ts +2 -1
- package/dist/types/wallet/vtxo-manager.d.ts +179 -0
- package/dist/types/wallet/wallet.d.ts +8 -4
- package/package.json +1 -2
- package/dist/cjs/bip322/errors.js +0 -13
- package/dist/esm/bip322/errors.js +0 -9
- package/dist/types/bip322/errors.d.ts +0 -6
- package/dist/types/bip322/index.d.ts +0 -57
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.DEFAULT_DB_NAME = void 0;
|
|
3
4
|
exports.setupServiceWorker = setupServiceWorker;
|
|
5
|
+
exports.DEFAULT_DB_NAME = "arkade-service-worker";
|
|
4
6
|
/**
|
|
5
7
|
* setupServiceWorker sets up the service worker.
|
|
6
8
|
* @param path - the path to the service worker script
|
|
@@ -25,7 +25,7 @@ class ServiceWorkerWallet {
|
|
|
25
25
|
}
|
|
26
26
|
static async create(options) {
|
|
27
27
|
// Default to IndexedDB for service worker context
|
|
28
|
-
const storage = options.
|
|
28
|
+
const storage = new indexedDB_1.IndexedDBStorageAdapter(options.dbName || utils_1.DEFAULT_DB_NAME, options.dbVersion);
|
|
29
29
|
// Create repositories
|
|
30
30
|
const walletRepo = new walletRepository_1.WalletRepositoryImpl(storage);
|
|
31
31
|
const contractRepo = new contractRepository_1.ContractRepositoryImpl(storage);
|
|
@@ -34,7 +34,7 @@ class ServiceWorkerWallet {
|
|
|
34
34
|
? options.identity
|
|
35
35
|
: null;
|
|
36
36
|
if (!identity) {
|
|
37
|
-
throw new Error("ServiceWorkerWallet.create() requires a Identity that can expose
|
|
37
|
+
throw new Error("ServiceWorkerWallet.create() requires a Identity that can expose a single private key");
|
|
38
38
|
}
|
|
39
39
|
// Extract private key for service worker initialization
|
|
40
40
|
const privateKey = identity.toHex();
|
|
@@ -77,13 +77,9 @@ class ServiceWorkerWallet {
|
|
|
77
77
|
// Register and setup the service worker
|
|
78
78
|
const serviceWorker = await (0, utils_1.setupServiceWorker)(options.serviceWorkerPath);
|
|
79
79
|
// Use the existing create method
|
|
80
|
-
return
|
|
81
|
-
|
|
82
|
-
arkServerUrl: options.arkServerUrl,
|
|
83
|
-
esploraUrl: options.esploraUrl,
|
|
84
|
-
identity: options.identity,
|
|
80
|
+
return ServiceWorkerWallet.create({
|
|
81
|
+
...options,
|
|
85
82
|
serviceWorker,
|
|
86
|
-
storage: options.storage,
|
|
87
83
|
});
|
|
88
84
|
}
|
|
89
85
|
// send a message and wait for a response
|
|
@@ -260,6 +256,9 @@ class ServiceWorkerWallet {
|
|
|
260
256
|
return new Promise((resolve, reject) => {
|
|
261
257
|
const messageHandler = (event) => {
|
|
262
258
|
const response = event.data;
|
|
259
|
+
if (response.id !== message.id) {
|
|
260
|
+
return;
|
|
261
|
+
}
|
|
263
262
|
if (!response.success) {
|
|
264
263
|
navigator.serviceWorker.removeEventListener("message", messageHandler);
|
|
265
264
|
reject(new Error(response.message));
|
|
@@ -14,14 +14,17 @@ const base_1 = require("@scure/base");
|
|
|
14
14
|
const indexedDB_1 = require("../../storage/indexedDB");
|
|
15
15
|
const walletRepository_1 = require("../../repositories/walletRepository");
|
|
16
16
|
const utils_1 = require("../utils");
|
|
17
|
+
const utils_2 = require("./utils");
|
|
17
18
|
/**
|
|
18
19
|
* Worker is a class letting to interact with ServiceWorkerWallet from the client
|
|
19
20
|
* it aims to be run in a service worker context
|
|
20
21
|
*/
|
|
21
22
|
class Worker {
|
|
22
|
-
constructor(messageCallback = () => { }) {
|
|
23
|
+
constructor(dbName = utils_2.DEFAULT_DB_NAME, dbVersion = 1, messageCallback = () => { }) {
|
|
24
|
+
this.dbName = dbName;
|
|
25
|
+
this.dbVersion = dbVersion;
|
|
23
26
|
this.messageCallback = messageCallback;
|
|
24
|
-
this.storage = new indexedDB_1.IndexedDBStorageAdapter(
|
|
27
|
+
this.storage = new indexedDB_1.IndexedDBStorageAdapter(dbName, dbVersion);
|
|
25
28
|
this.walletRepository = new walletRepository_1.WalletRepositoryImpl(this.storage);
|
|
26
29
|
}
|
|
27
30
|
/**
|
|
@@ -57,6 +60,15 @@ class Worker {
|
|
|
57
60
|
spent: allVtxos.filter((vtxo) => !(0, __1.isSpendable)(vtxo)),
|
|
58
61
|
};
|
|
59
62
|
}
|
|
63
|
+
/**
|
|
64
|
+
* Get all boarding utxos from wallet repository
|
|
65
|
+
*/
|
|
66
|
+
async getAllBoardingUtxos() {
|
|
67
|
+
if (!this.wallet)
|
|
68
|
+
return [];
|
|
69
|
+
const address = await this.wallet.getBoardingAddress();
|
|
70
|
+
return await this.walletRepository.getUtxos(address);
|
|
71
|
+
}
|
|
60
72
|
async start(withServiceWorkerUpdate = true) {
|
|
61
73
|
self.addEventListener("message", async (event) => {
|
|
62
74
|
await this.handleMessage(event);
|
|
@@ -107,6 +119,9 @@ class Worker {
|
|
|
107
119
|
const txs = await this.wallet.getTransactionHistory();
|
|
108
120
|
if (txs)
|
|
109
121
|
await this.walletRepository.saveTransactions(address, txs);
|
|
122
|
+
// unsubscribe previous subscription if any
|
|
123
|
+
if (this.incomingFundsSubscription)
|
|
124
|
+
this.incomingFundsSubscription();
|
|
110
125
|
// subscribe for incoming funds and notify all clients when new funds arrive
|
|
111
126
|
this.incomingFundsSubscription = await this.wallet.notifyIncomingFunds(async (funds) => {
|
|
112
127
|
if (funds.type === "vtxo") {
|
|
@@ -124,11 +139,19 @@ class Worker {
|
|
|
124
139
|
...spentVtxos,
|
|
125
140
|
]);
|
|
126
141
|
// notify all clients about the vtxo update
|
|
127
|
-
this.sendMessageToAllClients(
|
|
142
|
+
this.sendMessageToAllClients(response_1.Response.vtxoUpdate(newVtxos, spentVtxos));
|
|
128
143
|
}
|
|
129
144
|
if (funds.type === "utxo") {
|
|
145
|
+
const newUtxos = funds.coins.map((utxo) => (0, utils_1.extendCoin)(this.wallet, utxo));
|
|
146
|
+
if (newUtxos.length === 0) {
|
|
147
|
+
this.sendMessageToAllClients(response_1.Response.utxoUpdate([]));
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
const boardingAddress = await this.wallet?.getBoardingAddress();
|
|
151
|
+
// save utxos using unified repository
|
|
152
|
+
await this.walletRepository.saveUtxos(boardingAddress, newUtxos);
|
|
130
153
|
// notify all clients about the utxo update
|
|
131
|
-
this.sendMessageToAllClients(
|
|
154
|
+
this.sendMessageToAllClients(response_1.Response.utxoUpdate(funds.coins));
|
|
132
155
|
}
|
|
133
156
|
});
|
|
134
157
|
}
|
|
@@ -285,7 +308,7 @@ class Worker {
|
|
|
285
308
|
}
|
|
286
309
|
try {
|
|
287
310
|
const [boardingUtxos, spendableVtxos, sweptVtxos] = await Promise.all([
|
|
288
|
-
this.
|
|
311
|
+
this.getAllBoardingUtxos(),
|
|
289
312
|
this.getSpendableVtxos(),
|
|
290
313
|
this.getSweptVtxos(),
|
|
291
314
|
]);
|
|
@@ -353,22 +376,21 @@ class Worker {
|
|
|
353
376
|
return;
|
|
354
377
|
}
|
|
355
378
|
try {
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
event.source?.postMessage(response_1.Response.vtxos(message.id, vtxos));
|
|
379
|
+
const vtxos = await this.getSpendableVtxos();
|
|
380
|
+
const dustAmount = this.wallet.dustAmount;
|
|
381
|
+
const includeRecoverable = message.filter?.withRecoverable ?? false;
|
|
382
|
+
const filteredVtxos = includeRecoverable
|
|
383
|
+
? vtxos
|
|
384
|
+
: vtxos.filter((v) => {
|
|
385
|
+
if (dustAmount != null && (0, __1.isSubdust)(v, dustAmount)) {
|
|
386
|
+
return false;
|
|
387
|
+
}
|
|
388
|
+
if ((0, __1.isRecoverable)(v)) {
|
|
389
|
+
return false;
|
|
390
|
+
}
|
|
391
|
+
return true;
|
|
392
|
+
});
|
|
393
|
+
event.source?.postMessage(response_1.Response.vtxos(message.id, filteredVtxos));
|
|
372
394
|
}
|
|
373
395
|
catch (error) {
|
|
374
396
|
console.error("Error getting vtxos:", error);
|
|
@@ -391,7 +413,7 @@ class Worker {
|
|
|
391
413
|
return;
|
|
392
414
|
}
|
|
393
415
|
try {
|
|
394
|
-
const boardingUtxos = await this.
|
|
416
|
+
const boardingUtxos = await this.getAllBoardingUtxos();
|
|
395
417
|
event.source?.postMessage(response_1.Response.boardingUtxos(message.id, boardingUtxos));
|
|
396
418
|
}
|
|
397
419
|
catch (error) {
|
|
@@ -513,15 +535,12 @@ class Worker {
|
|
|
513
535
|
event.source?.postMessage(response_1.Response.error(message.id, "Unknown message type"));
|
|
514
536
|
}
|
|
515
537
|
}
|
|
516
|
-
async sendMessageToAllClients(
|
|
538
|
+
async sendMessageToAllClients(message) {
|
|
517
539
|
self.clients
|
|
518
540
|
.matchAll({ includeUncontrolled: true, type: "window" })
|
|
519
541
|
.then((clients) => {
|
|
520
542
|
clients.forEach((client) => {
|
|
521
|
-
client.postMessage(
|
|
522
|
-
type,
|
|
523
|
-
message,
|
|
524
|
-
});
|
|
543
|
+
client.postMessage(message);
|
|
525
544
|
});
|
|
526
545
|
});
|
|
527
546
|
}
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.Unroll = void 0;
|
|
4
|
-
const transaction_js_1 = require("@scure/btc-signer/transaction.js");
|
|
5
|
-
const indexer_1 = require("../providers/indexer");
|
|
6
4
|
const base_1 = require("@scure/base");
|
|
5
|
+
const btc_signer_1 = require("@scure/btc-signer");
|
|
6
|
+
const indexer_1 = require("../providers/indexer");
|
|
7
7
|
const base_2 = require("../script/base");
|
|
8
|
-
const psbt_js_1 = require("@scure/btc-signer/psbt.js");
|
|
9
8
|
const txSizeEstimator_1 = require("../utils/txSizeEstimator");
|
|
10
9
|
const wallet_1 = require("./wallet");
|
|
10
|
+
const transaction_1 = require("../utils/transaction");
|
|
11
11
|
var Unroll;
|
|
12
12
|
(function (Unroll) {
|
|
13
13
|
let StepType;
|
|
@@ -106,9 +106,7 @@ var Unroll;
|
|
|
106
106
|
if (virtualTxs.txs.length === 0) {
|
|
107
107
|
throw new Error(`Tx ${nextTxToBroadcast.txid} not found`);
|
|
108
108
|
}
|
|
109
|
-
const tx =
|
|
110
|
-
allowUnknownInputs: true,
|
|
111
|
-
});
|
|
109
|
+
const tx = transaction_1.Transaction.fromPSBT(base_1.base64.decode(virtualTxs.txs[0]));
|
|
112
110
|
// finalize the tree transaction
|
|
113
111
|
if (nextTxToBroadcast.type === indexer_1.ChainTxType.TREE) {
|
|
114
112
|
const input = tx.getInput(0);
|
|
@@ -197,11 +195,11 @@ var Unroll;
|
|
|
197
195
|
amount: BigInt(vtxo.value),
|
|
198
196
|
script: base_2.VtxoScript.decode(vtxo.tapTree).pkScript,
|
|
199
197
|
},
|
|
200
|
-
sighashType:
|
|
198
|
+
sighashType: btc_signer_1.SigHash.DEFAULT,
|
|
201
199
|
});
|
|
202
|
-
txWeightEstimator.addTapscriptInput(64, spendingLeaf[1].length,
|
|
200
|
+
txWeightEstimator.addTapscriptInput(64, spendingLeaf[1].length, btc_signer_1.TaprootControlBlock.encode(spendingLeaf[0]).length);
|
|
203
201
|
}
|
|
204
|
-
const tx = new
|
|
202
|
+
const tx = new transaction_1.Transaction({ version: 2 });
|
|
205
203
|
for (const input of inputs) {
|
|
206
204
|
tx.addInput(input);
|
|
207
205
|
}
|
package/dist/cjs/wallet/utils.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.extendVirtualCoin = extendVirtualCoin;
|
|
4
|
+
exports.extendCoin = extendCoin;
|
|
4
5
|
function extendVirtualCoin(wallet, vtxo) {
|
|
5
6
|
return {
|
|
6
7
|
...vtxo,
|
|
@@ -9,3 +10,11 @@ function extendVirtualCoin(wallet, vtxo) {
|
|
|
9
10
|
tapTree: wallet.offchainTapscript.encode(),
|
|
10
11
|
};
|
|
11
12
|
}
|
|
13
|
+
function extendCoin(wallet, utxo) {
|
|
14
|
+
return {
|
|
15
|
+
...utxo,
|
|
16
|
+
forfeitTapLeafScript: wallet.boardingTapscript.forfeit(),
|
|
17
|
+
intentTapLeafScript: wallet.boardingTapscript.exit(),
|
|
18
|
+
tapTree: wallet.boardingTapscript.encode(),
|
|
19
|
+
};
|
|
20
|
+
}
|
|
@@ -0,0 +1,323 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.VtxoManager = exports.DEFAULT_RENEWAL_CONFIG = void 0;
|
|
4
|
+
exports.isVtxoExpiringSoon = isVtxoExpiringSoon;
|
|
5
|
+
exports.getExpiringAndRecoverableVtxos = getExpiringAndRecoverableVtxos;
|
|
6
|
+
const _1 = require(".");
|
|
7
|
+
/**
|
|
8
|
+
* Default renewal configuration values
|
|
9
|
+
*/
|
|
10
|
+
exports.DEFAULT_RENEWAL_CONFIG = {
|
|
11
|
+
thresholdPercentage: 10,
|
|
12
|
+
};
|
|
13
|
+
function getDustAmount(wallet) {
|
|
14
|
+
return "dustAmount" in wallet ? wallet.dustAmount : 330n;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Filter VTXOs that are recoverable (swept and still spendable, or preconfirmed subdust)
|
|
18
|
+
*
|
|
19
|
+
* Recovery strategy:
|
|
20
|
+
* - Always recover swept VTXOs (they've been taken by the server)
|
|
21
|
+
* - Only recover subdust preconfirmed VTXOs (to avoid locking liquidity on settled VTXOs with long expiry)
|
|
22
|
+
*
|
|
23
|
+
* @param vtxos - Array of virtual coins to check
|
|
24
|
+
* @param dustAmount - Dust threshold to identify subdust
|
|
25
|
+
* @returns Array of recoverable VTXOs
|
|
26
|
+
*/
|
|
27
|
+
function getRecoverableVtxos(vtxos, dustAmount) {
|
|
28
|
+
return vtxos.filter((vtxo) => {
|
|
29
|
+
// Always recover swept VTXOs
|
|
30
|
+
if ((0, _1.isRecoverable)(vtxo)) {
|
|
31
|
+
return true;
|
|
32
|
+
}
|
|
33
|
+
// Recover preconfirmed subdust to consolidate small amounts
|
|
34
|
+
if (vtxo.virtualStatus.state === "preconfirmed" &&
|
|
35
|
+
(0, _1.isSubdust)(vtxo, dustAmount)) {
|
|
36
|
+
return true;
|
|
37
|
+
}
|
|
38
|
+
return false;
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Get recoverable VTXOs including subdust coins if the total value exceeds dust threshold.
|
|
43
|
+
*
|
|
44
|
+
* Decision is based on the combined total of ALL recoverable VTXOs (regular + subdust),
|
|
45
|
+
* not just the subdust portion alone.
|
|
46
|
+
*
|
|
47
|
+
* @param vtxos - Array of virtual coins to check
|
|
48
|
+
* @param dustAmount - Dust threshold amount in satoshis
|
|
49
|
+
* @returns Object containing recoverable VTXOs and whether subdust should be included
|
|
50
|
+
*/
|
|
51
|
+
function getRecoverableWithSubdust(vtxos, dustAmount) {
|
|
52
|
+
const recoverableVtxos = getRecoverableVtxos(vtxos, dustAmount);
|
|
53
|
+
// Separate subdust from regular recoverable
|
|
54
|
+
const subdust = [];
|
|
55
|
+
const regular = [];
|
|
56
|
+
for (const vtxo of recoverableVtxos) {
|
|
57
|
+
if ((0, _1.isSubdust)(vtxo, dustAmount)) {
|
|
58
|
+
subdust.push(vtxo);
|
|
59
|
+
}
|
|
60
|
+
else {
|
|
61
|
+
regular.push(vtxo);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
// Calculate totals
|
|
65
|
+
const regularTotal = regular.reduce((sum, vtxo) => sum + BigInt(vtxo.value), 0n);
|
|
66
|
+
const subdustTotal = subdust.reduce((sum, vtxo) => sum + BigInt(vtxo.value), 0n);
|
|
67
|
+
const combinedTotal = regularTotal + subdustTotal;
|
|
68
|
+
// Include subdust only if the combined total exceeds dust threshold
|
|
69
|
+
const shouldIncludeSubdust = combinedTotal >= dustAmount;
|
|
70
|
+
const vtxosToRecover = shouldIncludeSubdust ? recoverableVtxos : regular;
|
|
71
|
+
const totalAmount = vtxosToRecover.reduce((sum, vtxo) => sum + BigInt(vtxo.value), 0n);
|
|
72
|
+
return {
|
|
73
|
+
vtxosToRecover,
|
|
74
|
+
includesSubdust: shouldIncludeSubdust,
|
|
75
|
+
totalAmount,
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Check if a VTXO is expiring soon based on threshold
|
|
80
|
+
*
|
|
81
|
+
* @param vtxo - The virtual coin to check
|
|
82
|
+
* @param thresholdMs - Threshold in milliseconds from now
|
|
83
|
+
* @returns true if VTXO expires within threshold, false otherwise
|
|
84
|
+
*/
|
|
85
|
+
function isVtxoExpiringSoon(vtxo, percentage) {
|
|
86
|
+
const { batchExpiry } = vtxo.virtualStatus;
|
|
87
|
+
if (!batchExpiry) {
|
|
88
|
+
return false; // it doesn't expire
|
|
89
|
+
}
|
|
90
|
+
const now = Date.now();
|
|
91
|
+
if (batchExpiry <= now) {
|
|
92
|
+
return false; // already expired
|
|
93
|
+
}
|
|
94
|
+
// It shouldn't happen, but let's be safe
|
|
95
|
+
if (!vtxo.createdAt) {
|
|
96
|
+
return false;
|
|
97
|
+
}
|
|
98
|
+
const duration = batchExpiry - vtxo.createdAt.getTime();
|
|
99
|
+
const softExpiry = batchExpiry - (duration * percentage) / 100;
|
|
100
|
+
return softExpiry > 0 && softExpiry <= now;
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* Filter VTXOs that are expiring soon or are recoverable/subdust
|
|
104
|
+
*
|
|
105
|
+
* @param vtxos - Array of virtual coins to check
|
|
106
|
+
* @param thresholdMs - Threshold in milliseconds from now
|
|
107
|
+
* @param dustAmount - Dust threshold amount in satoshis
|
|
108
|
+
* @returns Array of VTXOs expiring within threshold
|
|
109
|
+
*/
|
|
110
|
+
function getExpiringAndRecoverableVtxos(vtxos, percentage, dustAmount) {
|
|
111
|
+
return vtxos.filter((vtxo) => isVtxoExpiringSoon(vtxo, percentage) ||
|
|
112
|
+
(0, _1.isRecoverable)(vtxo) ||
|
|
113
|
+
(0, _1.isSubdust)(vtxo, dustAmount));
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* VtxoManager is a unified class for managing VTXO lifecycle operations including
|
|
117
|
+
* recovery of swept/expired VTXOs and renewal to prevent expiration.
|
|
118
|
+
*
|
|
119
|
+
* Key Features:
|
|
120
|
+
* - **Recovery**: Reclaim swept or expired VTXOs back to the wallet
|
|
121
|
+
* - **Renewal**: Refresh VTXO expiration time before they expire
|
|
122
|
+
* - **Smart subdust handling**: Automatically includes subdust VTXOs when economically viable
|
|
123
|
+
* - **Expiry monitoring**: Check for VTXOs that are expiring soon
|
|
124
|
+
*
|
|
125
|
+
* VTXOs become recoverable when:
|
|
126
|
+
* - The Ark server sweeps them (virtualStatus.state === "swept") and they remain spendable
|
|
127
|
+
* - They are preconfirmed subdust (to consolidate small amounts without locking liquidity on settled VTXOs)
|
|
128
|
+
*
|
|
129
|
+
* @example
|
|
130
|
+
* ```typescript
|
|
131
|
+
* // Initialize with renewal config
|
|
132
|
+
* const manager = new VtxoManager(wallet, {
|
|
133
|
+
* enabled: true,
|
|
134
|
+
* thresholdPercentage: 10
|
|
135
|
+
* });
|
|
136
|
+
*
|
|
137
|
+
* // Check recoverable balance
|
|
138
|
+
* const balance = await manager.getRecoverableBalance();
|
|
139
|
+
* if (balance.recoverable > 0n) {
|
|
140
|
+
* console.log(`Can recover ${balance.recoverable} sats`);
|
|
141
|
+
* const txid = await manager.recoverVtxos();
|
|
142
|
+
* }
|
|
143
|
+
*
|
|
144
|
+
* // Check for expiring VTXOs
|
|
145
|
+
* const expiring = await manager.getExpiringVtxos();
|
|
146
|
+
* if (expiring.length > 0) {
|
|
147
|
+
* console.log(`${expiring.length} VTXOs expiring soon`);
|
|
148
|
+
* const txid = await manager.renewVtxos();
|
|
149
|
+
* }
|
|
150
|
+
* ```
|
|
151
|
+
*/
|
|
152
|
+
class VtxoManager {
|
|
153
|
+
constructor(wallet, renewalConfig) {
|
|
154
|
+
this.wallet = wallet;
|
|
155
|
+
this.renewalConfig = renewalConfig;
|
|
156
|
+
}
|
|
157
|
+
// ========== Recovery Methods ==========
|
|
158
|
+
/**
|
|
159
|
+
* Recover swept/expired VTXOs by settling them back to the wallet's Ark address.
|
|
160
|
+
*
|
|
161
|
+
* This method:
|
|
162
|
+
* 1. Fetches all VTXOs (including recoverable ones)
|
|
163
|
+
* 2. Filters for swept but still spendable VTXOs and preconfirmed subdust
|
|
164
|
+
* 3. Includes subdust VTXOs if the total value >= dust threshold
|
|
165
|
+
* 4. Settles everything back to the wallet's Ark address
|
|
166
|
+
*
|
|
167
|
+
* Note: Settled VTXOs with long expiry are NOT recovered to avoid locking liquidity unnecessarily.
|
|
168
|
+
* Only preconfirmed subdust is recovered to consolidate small amounts.
|
|
169
|
+
*
|
|
170
|
+
* @param eventCallback - Optional callback to receive settlement events
|
|
171
|
+
* @returns Settlement transaction ID
|
|
172
|
+
* @throws Error if no recoverable VTXOs found
|
|
173
|
+
*
|
|
174
|
+
* @example
|
|
175
|
+
* ```typescript
|
|
176
|
+
* const manager = new VtxoManager(wallet);
|
|
177
|
+
*
|
|
178
|
+
* // Simple recovery
|
|
179
|
+
* const txid = await manager.recoverVtxos();
|
|
180
|
+
*
|
|
181
|
+
* // With event callback
|
|
182
|
+
* const txid = await manager.recoverVtxos((event) => {
|
|
183
|
+
* console.log('Settlement event:', event.type);
|
|
184
|
+
* });
|
|
185
|
+
* ```
|
|
186
|
+
*/
|
|
187
|
+
async recoverVtxos(eventCallback) {
|
|
188
|
+
// Get all VTXOs including recoverable ones
|
|
189
|
+
const allVtxos = await this.wallet.getVtxos({
|
|
190
|
+
withRecoverable: true,
|
|
191
|
+
withUnrolled: false,
|
|
192
|
+
});
|
|
193
|
+
// Get dust amount from wallet
|
|
194
|
+
const dustAmount = getDustAmount(this.wallet);
|
|
195
|
+
// Filter recoverable VTXOs and handle subdust logic
|
|
196
|
+
const { vtxosToRecover, includesSubdust, totalAmount } = getRecoverableWithSubdust(allVtxos, dustAmount);
|
|
197
|
+
if (vtxosToRecover.length === 0) {
|
|
198
|
+
throw new Error("No recoverable VTXOs found");
|
|
199
|
+
}
|
|
200
|
+
const arkAddress = await this.wallet.getAddress();
|
|
201
|
+
// Settle all recoverable VTXOs back to the wallet
|
|
202
|
+
return this.wallet.settle({
|
|
203
|
+
inputs: vtxosToRecover,
|
|
204
|
+
outputs: [
|
|
205
|
+
{
|
|
206
|
+
address: arkAddress,
|
|
207
|
+
amount: totalAmount,
|
|
208
|
+
},
|
|
209
|
+
],
|
|
210
|
+
}, eventCallback);
|
|
211
|
+
}
|
|
212
|
+
/**
|
|
213
|
+
* Get information about recoverable balance without executing recovery.
|
|
214
|
+
*
|
|
215
|
+
* Useful for displaying to users before they decide to recover funds.
|
|
216
|
+
*
|
|
217
|
+
* @returns Object containing recoverable amounts and subdust information
|
|
218
|
+
*
|
|
219
|
+
* @example
|
|
220
|
+
* ```typescript
|
|
221
|
+
* const manager = new VtxoManager(wallet);
|
|
222
|
+
* const balance = await manager.getRecoverableBalance();
|
|
223
|
+
*
|
|
224
|
+
* if (balance.recoverable > 0n) {
|
|
225
|
+
* console.log(`You can recover ${balance.recoverable} sats`);
|
|
226
|
+
* if (balance.includesSubdust) {
|
|
227
|
+
* console.log(`This includes ${balance.subdust} sats from subdust VTXOs`);
|
|
228
|
+
* }
|
|
229
|
+
* }
|
|
230
|
+
* ```
|
|
231
|
+
*/
|
|
232
|
+
async getRecoverableBalance() {
|
|
233
|
+
const allVtxos = await this.wallet.getVtxos({
|
|
234
|
+
withRecoverable: true,
|
|
235
|
+
withUnrolled: false,
|
|
236
|
+
});
|
|
237
|
+
const dustAmount = getDustAmount(this.wallet);
|
|
238
|
+
const { vtxosToRecover, includesSubdust, totalAmount } = getRecoverableWithSubdust(allVtxos, dustAmount);
|
|
239
|
+
// Calculate subdust amount separately for reporting
|
|
240
|
+
const subdustAmount = vtxosToRecover
|
|
241
|
+
.filter((v) => BigInt(v.value) < dustAmount)
|
|
242
|
+
.reduce((sum, v) => sum + BigInt(v.value), 0n);
|
|
243
|
+
return {
|
|
244
|
+
recoverable: totalAmount,
|
|
245
|
+
subdust: subdustAmount,
|
|
246
|
+
includesSubdust,
|
|
247
|
+
vtxoCount: vtxosToRecover.length,
|
|
248
|
+
};
|
|
249
|
+
}
|
|
250
|
+
// ========== Renewal Methods ==========
|
|
251
|
+
/**
|
|
252
|
+
* Get VTXOs that are expiring soon based on renewal configuration
|
|
253
|
+
*
|
|
254
|
+
* @param thresholdPercentage - Optional override for threshold percentage (0-100)
|
|
255
|
+
* @returns Array of expiring VTXOs, empty array if renewal is disabled or no VTXOs expiring
|
|
256
|
+
*
|
|
257
|
+
* @example
|
|
258
|
+
* ```typescript
|
|
259
|
+
* const manager = new VtxoManager(wallet, { enabled: true, thresholdPercentage: 10 });
|
|
260
|
+
* const expiringVtxos = await manager.getExpiringVtxos();
|
|
261
|
+
* if (expiringVtxos.length > 0) {
|
|
262
|
+
* console.log(`${expiringVtxos.length} VTXOs expiring soon`);
|
|
263
|
+
* }
|
|
264
|
+
* ```
|
|
265
|
+
*/
|
|
266
|
+
async getExpiringVtxos(thresholdPercentage) {
|
|
267
|
+
const vtxos = await this.wallet.getVtxos({ withRecoverable: true });
|
|
268
|
+
const percentage = thresholdPercentage ??
|
|
269
|
+
this.renewalConfig?.thresholdPercentage ??
|
|
270
|
+
exports.DEFAULT_RENEWAL_CONFIG.thresholdPercentage;
|
|
271
|
+
return getExpiringAndRecoverableVtxos(vtxos, percentage, getDustAmount(this.wallet));
|
|
272
|
+
}
|
|
273
|
+
/**
|
|
274
|
+
* Renew expiring VTXOs by settling them back to the wallet's address
|
|
275
|
+
*
|
|
276
|
+
* This method collects all expiring spendable VTXOs (including recoverable ones) and settles
|
|
277
|
+
* them back to the wallet, effectively refreshing their expiration time. This is the
|
|
278
|
+
* primary way to prevent VTXOs from expiring.
|
|
279
|
+
*
|
|
280
|
+
* @param eventCallback - Optional callback for settlement events
|
|
281
|
+
* @returns Settlement transaction ID
|
|
282
|
+
* @throws Error if no VTXOs available to renew
|
|
283
|
+
* @throws Error if total amount is below dust threshold
|
|
284
|
+
*
|
|
285
|
+
* @example
|
|
286
|
+
* ```typescript
|
|
287
|
+
* const manager = new VtxoManager(wallet);
|
|
288
|
+
*
|
|
289
|
+
* // Simple renewal
|
|
290
|
+
* const txid = await manager.renewVtxos();
|
|
291
|
+
*
|
|
292
|
+
* // With event callback
|
|
293
|
+
* const txid = await manager.renewVtxos((event) => {
|
|
294
|
+
* console.log('Settlement event:', event.type);
|
|
295
|
+
* });
|
|
296
|
+
* ```
|
|
297
|
+
*/
|
|
298
|
+
async renewVtxos(eventCallback) {
|
|
299
|
+
// Get all VTXOs (including recoverable ones)
|
|
300
|
+
const vtxos = await this.getExpiringVtxos();
|
|
301
|
+
if (vtxos.length === 0) {
|
|
302
|
+
throw new Error("No VTXOs available to renew");
|
|
303
|
+
}
|
|
304
|
+
const totalAmount = vtxos.reduce((sum, vtxo) => sum + vtxo.value, 0);
|
|
305
|
+
// Get dust amount from wallet
|
|
306
|
+
const dustAmount = getDustAmount(this.wallet);
|
|
307
|
+
// Check if total amount is above dust threshold
|
|
308
|
+
if (BigInt(totalAmount) < dustAmount) {
|
|
309
|
+
throw new Error(`Total amount ${totalAmount} is below dust threshold ${dustAmount}`);
|
|
310
|
+
}
|
|
311
|
+
const arkAddress = await this.wallet.getAddress();
|
|
312
|
+
return this.wallet.settle({
|
|
313
|
+
inputs: vtxos,
|
|
314
|
+
outputs: [
|
|
315
|
+
{
|
|
316
|
+
address: arkAddress,
|
|
317
|
+
amount: BigInt(totalAmount),
|
|
318
|
+
},
|
|
319
|
+
],
|
|
320
|
+
}, eventCallback);
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
exports.VtxoManager = VtxoManager;
|