@arkade-os/sdk 0.3.4 → 0.3.6
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 +11 -5
- package/dist/cjs/index.js +3 -2
- package/dist/cjs/providers/ark.js +8 -1
- package/dist/cjs/utils/transactionHistory.js +29 -13
- package/dist/cjs/wallet/ramps.js +7 -1
- package/dist/cjs/wallet/vtxo-manager.js +19 -24
- package/dist/cjs/wallet/wallet.js +75 -4
- package/dist/esm/index.js +2 -2
- package/dist/esm/providers/ark.js +8 -1
- package/dist/esm/utils/transactionHistory.js +29 -13
- package/dist/esm/wallet/ramps.js +7 -1
- package/dist/esm/wallet/vtxo-manager.js +18 -23
- package/dist/esm/wallet/wallet.js +75 -4
- package/dist/types/index.d.ts +4 -4
- package/dist/types/providers/ark.d.ts +2 -2
- package/dist/types/wallet/ramps.d.ts +3 -2
- package/dist/types/wallet/vtxo-manager.d.ts +11 -10
- package/dist/types/wallet/wallet.d.ts +1 -1
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -120,8 +120,8 @@ import { VtxoManager } from '@arkade-os/sdk'
|
|
|
120
120
|
|
|
121
121
|
// Create manager with optional renewal configuration
|
|
122
122
|
const manager = new VtxoManager(wallet, {
|
|
123
|
-
enabled: true,
|
|
124
|
-
|
|
123
|
+
enabled: true, // Enable expiration monitoring
|
|
124
|
+
thresholdMs: 24 * 60 * 60 * 1000 // Alert when 24h hours % of lifetime remains (default)
|
|
125
125
|
})
|
|
126
126
|
```
|
|
127
127
|
|
|
@@ -137,8 +137,8 @@ console.log('Renewed:', txid)
|
|
|
137
137
|
|
|
138
138
|
// Check which VTXOs are expiring soon
|
|
139
139
|
const expiringVtxos = await manager.getExpiringVtxos()
|
|
140
|
-
// Override
|
|
141
|
-
const urgentlyExpiring = await manager.getExpiringVtxos(
|
|
140
|
+
// Override thresholdMs (e.g., renew when 5 seconds of time remains)
|
|
141
|
+
const urgentlyExpiring = await manager.getExpiringVtxos(5_000)
|
|
142
142
|
```
|
|
143
143
|
|
|
144
144
|
|
|
@@ -171,7 +171,13 @@ Collaborative exit or "offboarding" allows you to withdraw your virtual funds to
|
|
|
171
171
|
```typescript
|
|
172
172
|
import { Ramps } from '@arkade-os/sdk'
|
|
173
173
|
|
|
174
|
-
|
|
174
|
+
// Get fee information from the server
|
|
175
|
+
const info = await wallet.arkProvider.getInfo();
|
|
176
|
+
|
|
177
|
+
const exitTxid = await new Ramps(wallet).offboard(
|
|
178
|
+
onchainAddress,
|
|
179
|
+
info.fees
|
|
180
|
+
);
|
|
175
181
|
```
|
|
176
182
|
|
|
177
183
|
### Unilateral Exit
|
package/dist/cjs/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.
|
|
4
|
-
exports.maybeArkError = exports.ArkError = exports.Transaction = void 0;
|
|
3
|
+
exports.P2A = exports.TxTree = exports.Intent = exports.ContractRepositoryImpl = exports.WalletRepositoryImpl = exports.networks = exports.ArkNote = exports.isVtxoExpiringSoon = exports.combineTapscriptSigs = exports.hasBoardingTxExpired = exports.waitForIncomingFunds = exports.verifyTapscriptSignatures = exports.buildOffchainTx = exports.ConditionWitness = exports.VtxoTaprootTree = exports.VtxoTreeExpiry = exports.CosignerPublicKey = exports.getArkPsbtFields = exports.setArkPsbtField = exports.ArkPsbtFieldKeyType = exports.ArkPsbtFieldKey = exports.TapTreeCoder = exports.CLTVMultisigTapscript = exports.ConditionMultisigTapscript = exports.ConditionCSVMultisigTapscript = exports.CSVMultisigTapscript = exports.MultisigTapscript = exports.decodeTapscript = exports.Response = exports.Request = exports.ServiceWorkerWallet = exports.Worker = exports.setupServiceWorker = exports.SettlementEventType = exports.ChainTxType = exports.IndexerTxType = exports.TxType = exports.VHTLC = exports.VtxoScript = exports.DefaultVtxo = exports.ArkAddress = exports.RestIndexerProvider = exports.RestArkProvider = exports.EsploraProvider = exports.ESPLORA_URL = exports.VtxoManager = exports.Ramps = exports.OnchainWallet = exports.SingleKey = exports.Wallet = void 0;
|
|
4
|
+
exports.maybeArkError = exports.ArkError = exports.Transaction = exports.Unroll = void 0;
|
|
5
5
|
const transaction_1 = require("./utils/transaction");
|
|
6
6
|
Object.defineProperty(exports, "Transaction", { enumerable: true, get: function () { return transaction_1.Transaction; } });
|
|
7
7
|
const singleKey_1 = require("./identity/singleKey");
|
|
@@ -25,6 +25,7 @@ Object.defineProperty(exports, "TxTree", { enumerable: true, get: function () {
|
|
|
25
25
|
const ramps_1 = require("./wallet/ramps");
|
|
26
26
|
Object.defineProperty(exports, "Ramps", { enumerable: true, get: function () { return ramps_1.Ramps; } });
|
|
27
27
|
const vtxo_manager_1 = require("./wallet/vtxo-manager");
|
|
28
|
+
Object.defineProperty(exports, "isVtxoExpiringSoon", { enumerable: true, get: function () { return vtxo_manager_1.isVtxoExpiringSoon; } });
|
|
28
29
|
Object.defineProperty(exports, "VtxoManager", { enumerable: true, get: function () { return vtxo_manager_1.VtxoManager; } });
|
|
29
30
|
const wallet_3 = require("./wallet/serviceWorker/wallet");
|
|
30
31
|
Object.defineProperty(exports, "ServiceWorkerWallet", { enumerable: true, get: function () { return wallet_3.ServiceWorkerWallet; } });
|
|
@@ -46,7 +46,14 @@ class RestArkProvider {
|
|
|
46
46
|
})) ?? [],
|
|
47
47
|
digest: fromServer.digest ?? "",
|
|
48
48
|
dust: BigInt(fromServer.dust ?? 0),
|
|
49
|
-
fees:
|
|
49
|
+
fees: {
|
|
50
|
+
intentFee: {
|
|
51
|
+
...fromServer.fees?.intentFee,
|
|
52
|
+
onchainInput: BigInt(fromServer.fees?.intentFee?.onchainInput ?? 0),
|
|
53
|
+
onchainOutput: BigInt(fromServer.fees?.intentFee?.onchainOutput ?? 0),
|
|
54
|
+
},
|
|
55
|
+
txFeeRate: fromServer?.fees?.txFeeRate ?? "",
|
|
56
|
+
},
|
|
50
57
|
forfeitAddress: fromServer.forfeitAddress ?? "",
|
|
51
58
|
forfeitPubkey: fromServer.forfeitPubkey ?? "",
|
|
52
59
|
network: fromServer.network ?? "",
|
|
@@ -14,6 +14,10 @@ function vtxosToTxs(spendable, spent, boardingBatchTxids) {
|
|
|
14
14
|
// All vtxos are received unless:
|
|
15
15
|
// - they resulted from a settlement (either boarding or refresh)
|
|
16
16
|
// - they are the change of a spend tx
|
|
17
|
+
// - they were spent in a payment (have arkTxId set)
|
|
18
|
+
// - they resulted from a payment (their txid matches an arkTxId of a spent vtxo)
|
|
19
|
+
// First, collect all arkTxIds from spent vtxos to identify payment transactions
|
|
20
|
+
const paymentArkTxIds = new Set(spent.filter((v) => v.arkTxId).map((v) => v.arkTxId));
|
|
17
21
|
let vtxosLeftToCheck = [...spent];
|
|
18
22
|
for (const vtxo of [...spendable, ...spent]) {
|
|
19
23
|
if (vtxo.virtualStatus.state !== "preconfirmed" &&
|
|
@@ -21,6 +25,16 @@ function vtxosToTxs(spendable, spent, boardingBatchTxids) {
|
|
|
21
25
|
vtxo.virtualStatus.commitmentTxIds.some((txid) => boardingBatchTxids.has(txid))) {
|
|
22
26
|
continue;
|
|
23
27
|
}
|
|
28
|
+
// Skip vtxos that were spent in a payment transaction
|
|
29
|
+
// These will be handled in the sent transaction section below
|
|
30
|
+
if (vtxo.arkTxId) {
|
|
31
|
+
continue;
|
|
32
|
+
}
|
|
33
|
+
// Skip vtxos that resulted from a payment transaction
|
|
34
|
+
// (their txid matches an arkTxId from a spent vtxo)
|
|
35
|
+
if (paymentArkTxIds.has(vtxo.txid)) {
|
|
36
|
+
continue;
|
|
37
|
+
}
|
|
24
38
|
const settleVtxos = findVtxosSpentInSettlement(vtxosLeftToCheck, vtxo);
|
|
25
39
|
vtxosLeftToCheck = removeVtxosFromList(vtxosLeftToCheck, settleVtxos);
|
|
26
40
|
const settleAmount = reduceVtxosAmount(settleVtxos);
|
|
@@ -56,21 +70,17 @@ function vtxosToTxs(spendable, spent, boardingBatchTxids) {
|
|
|
56
70
|
// vtxos by settled by or ark txid
|
|
57
71
|
const vtxosByTxid = new Map();
|
|
58
72
|
for (const v of spent) {
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
const currentVtxos = vtxosByTxid.get(v.settledBy);
|
|
64
|
-
vtxosByTxid.set(v.settledBy, [...currentVtxos, v]);
|
|
65
|
-
}
|
|
66
|
-
if (!v.arkTxId) {
|
|
73
|
+
// Prefer arkTxId over settledBy to avoid duplicates
|
|
74
|
+
// A vtxo should only be grouped once
|
|
75
|
+
const groupKey = v.arkTxId || v.settledBy;
|
|
76
|
+
if (!groupKey) {
|
|
67
77
|
continue;
|
|
68
78
|
}
|
|
69
|
-
if (!vtxosByTxid.has(
|
|
70
|
-
vtxosByTxid.set(
|
|
79
|
+
if (!vtxosByTxid.has(groupKey)) {
|
|
80
|
+
vtxosByTxid.set(groupKey, []);
|
|
71
81
|
}
|
|
72
|
-
const currentVtxos = vtxosByTxid.get(
|
|
73
|
-
vtxosByTxid.set(
|
|
82
|
+
const currentVtxos = vtxosByTxid.get(groupKey);
|
|
83
|
+
vtxosByTxid.set(groupKey, [...currentVtxos, v]);
|
|
74
84
|
}
|
|
75
85
|
for (const [sb, vtxos] of vtxosByTxid) {
|
|
76
86
|
const resultedVtxos = findVtxosResultedFromTxid([...spendable, ...spent], sb);
|
|
@@ -85,7 +95,13 @@ function vtxosToTxs(spendable, spent, boardingBatchTxids) {
|
|
|
85
95
|
boardingTxid: "",
|
|
86
96
|
arkTxid: "",
|
|
87
97
|
};
|
|
88
|
-
|
|
98
|
+
// Use the grouping key (sb) as arkTxid if it looks like an arkTxId
|
|
99
|
+
// (i.e., if the spent vtxos had arkTxId set, use that instead of result vtxo's txid)
|
|
100
|
+
const isArkTxId = vtxos.some((v) => v.arkTxId === sb);
|
|
101
|
+
if (isArkTxId) {
|
|
102
|
+
txKey.arkTxid = sb;
|
|
103
|
+
}
|
|
104
|
+
else if (vtxo.virtualStatus.state === "preconfirmed") {
|
|
89
105
|
txKey.arkTxid = vtxo.txid;
|
|
90
106
|
}
|
|
91
107
|
txs.push({
|
package/dist/cjs/wallet/ramps.js
CHANGED
|
@@ -56,10 +56,11 @@ class Ramps {
|
|
|
56
56
|
* Offboard vtxos, or "collaborative exit" vtxos to onchain address.
|
|
57
57
|
*
|
|
58
58
|
* @param destinationAddress - The destination address to offboard to.
|
|
59
|
+
* @param feeInfo - The fee info to deduct from the offboard amount.
|
|
59
60
|
* @param amount - The amount to offboard. If not provided, the total amount of vtxos will be offboarded.
|
|
60
61
|
* @param eventCallback - The callback to receive settlement events. optional.
|
|
61
62
|
*/
|
|
62
|
-
async offboard(destinationAddress, amount, eventCallback) {
|
|
63
|
+
async offboard(destinationAddress, feeInfo, amount, eventCallback) {
|
|
63
64
|
const vtxos = await this.wallet.getVtxos({
|
|
64
65
|
withRecoverable: true,
|
|
65
66
|
withUnrolled: false,
|
|
@@ -73,6 +74,11 @@ class Ramps {
|
|
|
73
74
|
change = totalAmount - amount;
|
|
74
75
|
}
|
|
75
76
|
amount = amount ?? totalAmount;
|
|
77
|
+
const fees = feeInfo.intentFee.onchainOutput;
|
|
78
|
+
if (fees > amount) {
|
|
79
|
+
throw new Error(`can't deduct fees from offboard amount (${fees} > ${amount})`);
|
|
80
|
+
}
|
|
81
|
+
amount -= fees;
|
|
76
82
|
const outputs = [
|
|
77
83
|
{
|
|
78
84
|
address: destinationAddress,
|
|
@@ -1,14 +1,15 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.VtxoManager = exports.DEFAULT_RENEWAL_CONFIG = void 0;
|
|
3
|
+
exports.VtxoManager = exports.DEFAULT_RENEWAL_CONFIG = exports.DEFAULT_THRESHOLD_MS = void 0;
|
|
4
4
|
exports.isVtxoExpiringSoon = isVtxoExpiringSoon;
|
|
5
5
|
exports.getExpiringAndRecoverableVtxos = getExpiringAndRecoverableVtxos;
|
|
6
6
|
const _1 = require(".");
|
|
7
|
+
exports.DEFAULT_THRESHOLD_MS = 3 * 24 * 60 * 60 * 1000; // 3 days
|
|
7
8
|
/**
|
|
8
9
|
* Default renewal configuration values
|
|
9
10
|
*/
|
|
10
11
|
exports.DEFAULT_RENEWAL_CONFIG = {
|
|
11
|
-
|
|
12
|
+
thresholdMs: exports.DEFAULT_THRESHOLD_MS, // 3 days
|
|
12
13
|
};
|
|
13
14
|
function getDustAmount(wallet) {
|
|
14
15
|
return "dustAmount" in wallet ? wallet.dustAmount : 330n;
|
|
@@ -82,22 +83,16 @@ function getRecoverableWithSubdust(vtxos, dustAmount) {
|
|
|
82
83
|
* @param thresholdMs - Threshold in milliseconds from now
|
|
83
84
|
* @returns true if VTXO expires within threshold, false otherwise
|
|
84
85
|
*/
|
|
85
|
-
function isVtxoExpiringSoon(vtxo,
|
|
86
|
+
function isVtxoExpiringSoon(vtxo, thresholdMs // in milliseconds
|
|
87
|
+
) {
|
|
88
|
+
const realThresholdMs = thresholdMs <= 100 ? exports.DEFAULT_THRESHOLD_MS : thresholdMs;
|
|
86
89
|
const { batchExpiry } = vtxo.virtualStatus;
|
|
87
|
-
if (!batchExpiry)
|
|
90
|
+
if (!batchExpiry)
|
|
88
91
|
return false; // it doesn't expire
|
|
89
|
-
}
|
|
90
92
|
const now = Date.now();
|
|
91
|
-
if (batchExpiry <= now)
|
|
93
|
+
if (batchExpiry <= now)
|
|
92
94
|
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;
|
|
95
|
+
return batchExpiry - now <= realThresholdMs;
|
|
101
96
|
}
|
|
102
97
|
/**
|
|
103
98
|
* Filter VTXOs that are expiring soon or are recoverable/subdust
|
|
@@ -107,8 +102,8 @@ function isVtxoExpiringSoon(vtxo, percentage) {
|
|
|
107
102
|
* @param dustAmount - Dust threshold amount in satoshis
|
|
108
103
|
* @returns Array of VTXOs expiring within threshold
|
|
109
104
|
*/
|
|
110
|
-
function getExpiringAndRecoverableVtxos(vtxos,
|
|
111
|
-
return vtxos.filter((vtxo) => isVtxoExpiringSoon(vtxo,
|
|
105
|
+
function getExpiringAndRecoverableVtxos(vtxos, thresholdMs, dustAmount) {
|
|
106
|
+
return vtxos.filter((vtxo) => isVtxoExpiringSoon(vtxo, thresholdMs) ||
|
|
112
107
|
(0, _1.isRecoverable)(vtxo) ||
|
|
113
108
|
(0, _1.isSubdust)(vtxo, dustAmount));
|
|
114
109
|
}
|
|
@@ -131,7 +126,7 @@ function getExpiringAndRecoverableVtxos(vtxos, percentage, dustAmount) {
|
|
|
131
126
|
* // Initialize with renewal config
|
|
132
127
|
* const manager = new VtxoManager(wallet, {
|
|
133
128
|
* enabled: true,
|
|
134
|
-
*
|
|
129
|
+
* thresholdMs: 86400000
|
|
135
130
|
* });
|
|
136
131
|
*
|
|
137
132
|
* // Check recoverable balance
|
|
@@ -251,24 +246,24 @@ class VtxoManager {
|
|
|
251
246
|
/**
|
|
252
247
|
* Get VTXOs that are expiring soon based on renewal configuration
|
|
253
248
|
*
|
|
254
|
-
* @param
|
|
249
|
+
* @param thresholdMs - Optional override for threshold in milliseconds
|
|
255
250
|
* @returns Array of expiring VTXOs, empty array if renewal is disabled or no VTXOs expiring
|
|
256
251
|
*
|
|
257
252
|
* @example
|
|
258
253
|
* ```typescript
|
|
259
|
-
* const manager = new VtxoManager(wallet, { enabled: true,
|
|
254
|
+
* const manager = new VtxoManager(wallet, { enabled: true, thresholdMs: 86400000 });
|
|
260
255
|
* const expiringVtxos = await manager.getExpiringVtxos();
|
|
261
256
|
* if (expiringVtxos.length > 0) {
|
|
262
257
|
* console.log(`${expiringVtxos.length} VTXOs expiring soon`);
|
|
263
258
|
* }
|
|
264
259
|
* ```
|
|
265
260
|
*/
|
|
266
|
-
async getExpiringVtxos(
|
|
261
|
+
async getExpiringVtxos(thresholdMs) {
|
|
267
262
|
const vtxos = await this.wallet.getVtxos({ withRecoverable: true });
|
|
268
|
-
const
|
|
269
|
-
this.renewalConfig?.
|
|
270
|
-
exports.DEFAULT_RENEWAL_CONFIG.
|
|
271
|
-
return getExpiringAndRecoverableVtxos(vtxos,
|
|
263
|
+
const threshold = thresholdMs ??
|
|
264
|
+
this.renewalConfig?.thresholdMs ??
|
|
265
|
+
exports.DEFAULT_RENEWAL_CONFIG.thresholdMs;
|
|
266
|
+
return getExpiringAndRecoverableVtxos(vtxos, threshold, getDustAmount(this.wallet));
|
|
272
267
|
}
|
|
273
268
|
/**
|
|
274
269
|
* Renew expiring VTXOs by settling them back to the wallet's address
|
|
@@ -431,14 +431,13 @@ class Wallet {
|
|
|
431
431
|
});
|
|
432
432
|
}
|
|
433
433
|
const tapTree = this.offchainTapscript.encode();
|
|
434
|
-
|
|
434
|
+
const offchainTx = (0, arkTransaction_1.buildOffchainTx)(selected.inputs.map((input) => ({
|
|
435
435
|
...input,
|
|
436
436
|
tapLeafScript: selectedLeaf,
|
|
437
437
|
tapTree,
|
|
438
438
|
})), outputs, this.serverUnrollScript);
|
|
439
439
|
const signedVirtualTx = await this.identity.sign(offchainTx.arkTx);
|
|
440
440
|
const { arkTxid, signedCheckpointTxs } = await this.arkProvider.submitTx(base_1.base64.encode(signedVirtualTx.toPSBT()), offchainTx.checkpoints.map((c) => base_1.base64.encode(c.toPSBT())));
|
|
441
|
-
// TODO persist final virtual tx and checkpoints to repository
|
|
442
441
|
// sign the checkpoints
|
|
443
442
|
const finalCheckpoints = await Promise.all(signedCheckpointTxs.map(async (c) => {
|
|
444
443
|
const tx = btc_signer_1.Transaction.fromPSBT(base_1.base64.decode(c));
|
|
@@ -446,7 +445,78 @@ class Wallet {
|
|
|
446
445
|
return base_1.base64.encode(signedCheckpoint.toPSBT());
|
|
447
446
|
}));
|
|
448
447
|
await this.arkProvider.finalizeTx(arkTxid, finalCheckpoints);
|
|
449
|
-
|
|
448
|
+
try {
|
|
449
|
+
// mark VTXOs as spent and optionally add the change VTXO
|
|
450
|
+
const spentVtxos = [];
|
|
451
|
+
const commitmentTxIds = new Set();
|
|
452
|
+
let batchExpiry = Number.MAX_SAFE_INTEGER;
|
|
453
|
+
for (const [inputIndex, input] of selected.inputs.entries()) {
|
|
454
|
+
const vtxo = (0, utils_1.extendVirtualCoin)(this, input);
|
|
455
|
+
const checkpointB64 = signedCheckpointTxs[inputIndex];
|
|
456
|
+
const checkpoint = btc_signer_1.Transaction.fromPSBT(base_1.base64.decode(checkpointB64));
|
|
457
|
+
spentVtxos.push({
|
|
458
|
+
...vtxo,
|
|
459
|
+
virtualStatus: { ...vtxo.virtualStatus, state: "spent" },
|
|
460
|
+
spentBy: checkpoint.id,
|
|
461
|
+
arkTxId: arkTxid,
|
|
462
|
+
isSpent: true,
|
|
463
|
+
});
|
|
464
|
+
if (vtxo.virtualStatus.commitmentTxIds) {
|
|
465
|
+
for (const commitmentTxId of vtxo.virtualStatus
|
|
466
|
+
.commitmentTxIds) {
|
|
467
|
+
commitmentTxIds.add(commitmentTxId);
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
if (vtxo.virtualStatus.batchExpiry) {
|
|
471
|
+
batchExpiry = Math.min(batchExpiry, vtxo.virtualStatus.batchExpiry);
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
const createdAt = Date.now();
|
|
475
|
+
const addr = this.arkAddress.encode();
|
|
476
|
+
if (selected.changeAmount > 0n &&
|
|
477
|
+
batchExpiry !== Number.MAX_SAFE_INTEGER) {
|
|
478
|
+
const changeVtxo = {
|
|
479
|
+
txid: arkTxid,
|
|
480
|
+
vout: outputs.length - 1,
|
|
481
|
+
createdAt: new Date(createdAt),
|
|
482
|
+
forfeitTapLeafScript: this.offchainTapscript.forfeit(),
|
|
483
|
+
intentTapLeafScript: this.offchainTapscript.exit(),
|
|
484
|
+
isUnrolled: false,
|
|
485
|
+
isSpent: false,
|
|
486
|
+
tapTree: this.offchainTapscript.encode(),
|
|
487
|
+
value: Number(selected.changeAmount),
|
|
488
|
+
virtualStatus: {
|
|
489
|
+
state: "preconfirmed",
|
|
490
|
+
commitmentTxIds: Array.from(commitmentTxIds),
|
|
491
|
+
batchExpiry,
|
|
492
|
+
},
|
|
493
|
+
status: {
|
|
494
|
+
confirmed: false,
|
|
495
|
+
},
|
|
496
|
+
};
|
|
497
|
+
await this.walletRepository.saveVtxos(addr, [changeVtxo]);
|
|
498
|
+
}
|
|
499
|
+
await this.walletRepository.saveVtxos(addr, spentVtxos);
|
|
500
|
+
await this.walletRepository.saveTransactions(addr, [
|
|
501
|
+
{
|
|
502
|
+
key: {
|
|
503
|
+
boardingTxid: "",
|
|
504
|
+
commitmentTxid: "",
|
|
505
|
+
arkTxid: arkTxid,
|
|
506
|
+
},
|
|
507
|
+
amount: params.amount,
|
|
508
|
+
type: _1.TxType.TxSent,
|
|
509
|
+
settled: false,
|
|
510
|
+
createdAt: Date.now(),
|
|
511
|
+
},
|
|
512
|
+
]);
|
|
513
|
+
}
|
|
514
|
+
catch (e) {
|
|
515
|
+
console.warn("error saving offchain tx to repository", e);
|
|
516
|
+
}
|
|
517
|
+
finally {
|
|
518
|
+
return arkTxid;
|
|
519
|
+
}
|
|
450
520
|
}
|
|
451
521
|
async settle(params, eventCallback) {
|
|
452
522
|
if (params?.inputs) {
|
|
@@ -719,7 +789,8 @@ class Wallet {
|
|
|
719
789
|
(async () => {
|
|
720
790
|
try {
|
|
721
791
|
for await (const update of subscription) {
|
|
722
|
-
if (update.newVtxos?.length > 0
|
|
792
|
+
if (update.newVtxos?.length > 0 ||
|
|
793
|
+
update.spentVtxos?.length > 0) {
|
|
723
794
|
eventCallback({
|
|
724
795
|
type: "vtxo",
|
|
725
796
|
newVtxos: update.newVtxos.map((vtxo) => (0, utils_1.extendVirtualCoin)(this, vtxo)),
|
package/dist/esm/index.js
CHANGED
|
@@ -8,7 +8,7 @@ import { TxType, } from './wallet/index.js';
|
|
|
8
8
|
import { Wallet, waitForIncomingFunds } from './wallet/wallet.js';
|
|
9
9
|
import { TxTree } from './tree/txTree.js';
|
|
10
10
|
import { Ramps } from './wallet/ramps.js';
|
|
11
|
-
import { VtxoManager } from './wallet/vtxo-manager.js';
|
|
11
|
+
import { isVtxoExpiringSoon, VtxoManager } from './wallet/vtxo-manager.js';
|
|
12
12
|
import { ServiceWorkerWallet } from './wallet/serviceWorker/wallet.js';
|
|
13
13
|
import { OnchainWallet } from './wallet/onchain.js';
|
|
14
14
|
import { setupServiceWorker } from './wallet/serviceWorker/utils.js';
|
|
@@ -45,7 +45,7 @@ decodeTapscript, MultisigTapscript, CSVMultisigTapscript, ConditionCSVMultisigTa
|
|
|
45
45
|
// Ark PSBT fields
|
|
46
46
|
ArkPsbtFieldKey, ArkPsbtFieldKeyType, setArkPsbtField, getArkPsbtFields, CosignerPublicKey, VtxoTreeExpiry, VtxoTaprootTree, ConditionWitness,
|
|
47
47
|
// Utils
|
|
48
|
-
buildOffchainTx, verifyTapscriptSignatures, waitForIncomingFunds, hasBoardingTxExpired, combineTapscriptSigs,
|
|
48
|
+
buildOffchainTx, verifyTapscriptSignatures, waitForIncomingFunds, hasBoardingTxExpired, combineTapscriptSigs, isVtxoExpiringSoon,
|
|
49
49
|
// Arknote
|
|
50
50
|
ArkNote,
|
|
51
51
|
// Network
|
|
@@ -42,7 +42,14 @@ export class RestArkProvider {
|
|
|
42
42
|
})) ?? [],
|
|
43
43
|
digest: fromServer.digest ?? "",
|
|
44
44
|
dust: BigInt(fromServer.dust ?? 0),
|
|
45
|
-
fees:
|
|
45
|
+
fees: {
|
|
46
|
+
intentFee: {
|
|
47
|
+
...fromServer.fees?.intentFee,
|
|
48
|
+
onchainInput: BigInt(fromServer.fees?.intentFee?.onchainInput ?? 0),
|
|
49
|
+
onchainOutput: BigInt(fromServer.fees?.intentFee?.onchainOutput ?? 0),
|
|
50
|
+
},
|
|
51
|
+
txFeeRate: fromServer?.fees?.txFeeRate ?? "",
|
|
52
|
+
},
|
|
46
53
|
forfeitAddress: fromServer.forfeitAddress ?? "",
|
|
47
54
|
forfeitPubkey: fromServer.forfeitPubkey ?? "",
|
|
48
55
|
network: fromServer.network ?? "",
|
|
@@ -11,6 +11,10 @@ export function vtxosToTxs(spendable, spent, boardingBatchTxids) {
|
|
|
11
11
|
// All vtxos are received unless:
|
|
12
12
|
// - they resulted from a settlement (either boarding or refresh)
|
|
13
13
|
// - they are the change of a spend tx
|
|
14
|
+
// - they were spent in a payment (have arkTxId set)
|
|
15
|
+
// - they resulted from a payment (their txid matches an arkTxId of a spent vtxo)
|
|
16
|
+
// First, collect all arkTxIds from spent vtxos to identify payment transactions
|
|
17
|
+
const paymentArkTxIds = new Set(spent.filter((v) => v.arkTxId).map((v) => v.arkTxId));
|
|
14
18
|
let vtxosLeftToCheck = [...spent];
|
|
15
19
|
for (const vtxo of [...spendable, ...spent]) {
|
|
16
20
|
if (vtxo.virtualStatus.state !== "preconfirmed" &&
|
|
@@ -18,6 +22,16 @@ export function vtxosToTxs(spendable, spent, boardingBatchTxids) {
|
|
|
18
22
|
vtxo.virtualStatus.commitmentTxIds.some((txid) => boardingBatchTxids.has(txid))) {
|
|
19
23
|
continue;
|
|
20
24
|
}
|
|
25
|
+
// Skip vtxos that were spent in a payment transaction
|
|
26
|
+
// These will be handled in the sent transaction section below
|
|
27
|
+
if (vtxo.arkTxId) {
|
|
28
|
+
continue;
|
|
29
|
+
}
|
|
30
|
+
// Skip vtxos that resulted from a payment transaction
|
|
31
|
+
// (their txid matches an arkTxId from a spent vtxo)
|
|
32
|
+
if (paymentArkTxIds.has(vtxo.txid)) {
|
|
33
|
+
continue;
|
|
34
|
+
}
|
|
21
35
|
const settleVtxos = findVtxosSpentInSettlement(vtxosLeftToCheck, vtxo);
|
|
22
36
|
vtxosLeftToCheck = removeVtxosFromList(vtxosLeftToCheck, settleVtxos);
|
|
23
37
|
const settleAmount = reduceVtxosAmount(settleVtxos);
|
|
@@ -53,21 +67,17 @@ export function vtxosToTxs(spendable, spent, boardingBatchTxids) {
|
|
|
53
67
|
// vtxos by settled by or ark txid
|
|
54
68
|
const vtxosByTxid = new Map();
|
|
55
69
|
for (const v of spent) {
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
const currentVtxos = vtxosByTxid.get(v.settledBy);
|
|
61
|
-
vtxosByTxid.set(v.settledBy, [...currentVtxos, v]);
|
|
62
|
-
}
|
|
63
|
-
if (!v.arkTxId) {
|
|
70
|
+
// Prefer arkTxId over settledBy to avoid duplicates
|
|
71
|
+
// A vtxo should only be grouped once
|
|
72
|
+
const groupKey = v.arkTxId || v.settledBy;
|
|
73
|
+
if (!groupKey) {
|
|
64
74
|
continue;
|
|
65
75
|
}
|
|
66
|
-
if (!vtxosByTxid.has(
|
|
67
|
-
vtxosByTxid.set(
|
|
76
|
+
if (!vtxosByTxid.has(groupKey)) {
|
|
77
|
+
vtxosByTxid.set(groupKey, []);
|
|
68
78
|
}
|
|
69
|
-
const currentVtxos = vtxosByTxid.get(
|
|
70
|
-
vtxosByTxid.set(
|
|
79
|
+
const currentVtxos = vtxosByTxid.get(groupKey);
|
|
80
|
+
vtxosByTxid.set(groupKey, [...currentVtxos, v]);
|
|
71
81
|
}
|
|
72
82
|
for (const [sb, vtxos] of vtxosByTxid) {
|
|
73
83
|
const resultedVtxos = findVtxosResultedFromTxid([...spendable, ...spent], sb);
|
|
@@ -82,7 +92,13 @@ export function vtxosToTxs(spendable, spent, boardingBatchTxids) {
|
|
|
82
92
|
boardingTxid: "",
|
|
83
93
|
arkTxid: "",
|
|
84
94
|
};
|
|
85
|
-
|
|
95
|
+
// Use the grouping key (sb) as arkTxid if it looks like an arkTxId
|
|
96
|
+
// (i.e., if the spent vtxos had arkTxId set, use that instead of result vtxo's txid)
|
|
97
|
+
const isArkTxId = vtxos.some((v) => v.arkTxId === sb);
|
|
98
|
+
if (isArkTxId) {
|
|
99
|
+
txKey.arkTxid = sb;
|
|
100
|
+
}
|
|
101
|
+
else if (vtxo.virtualStatus.state === "preconfirmed") {
|
|
86
102
|
txKey.arkTxid = vtxo.txid;
|
|
87
103
|
}
|
|
88
104
|
txs.push({
|
package/dist/esm/wallet/ramps.js
CHANGED
|
@@ -53,10 +53,11 @@ export class Ramps {
|
|
|
53
53
|
* Offboard vtxos, or "collaborative exit" vtxos to onchain address.
|
|
54
54
|
*
|
|
55
55
|
* @param destinationAddress - The destination address to offboard to.
|
|
56
|
+
* @param feeInfo - The fee info to deduct from the offboard amount.
|
|
56
57
|
* @param amount - The amount to offboard. If not provided, the total amount of vtxos will be offboarded.
|
|
57
58
|
* @param eventCallback - The callback to receive settlement events. optional.
|
|
58
59
|
*/
|
|
59
|
-
async offboard(destinationAddress, amount, eventCallback) {
|
|
60
|
+
async offboard(destinationAddress, feeInfo, amount, eventCallback) {
|
|
60
61
|
const vtxos = await this.wallet.getVtxos({
|
|
61
62
|
withRecoverable: true,
|
|
62
63
|
withUnrolled: false,
|
|
@@ -70,6 +71,11 @@ export class Ramps {
|
|
|
70
71
|
change = totalAmount - amount;
|
|
71
72
|
}
|
|
72
73
|
amount = amount ?? totalAmount;
|
|
74
|
+
const fees = feeInfo.intentFee.onchainOutput;
|
|
75
|
+
if (fees > amount) {
|
|
76
|
+
throw new Error(`can't deduct fees from offboard amount (${fees} > ${amount})`);
|
|
77
|
+
}
|
|
78
|
+
amount -= fees;
|
|
73
79
|
const outputs = [
|
|
74
80
|
{
|
|
75
81
|
address: destinationAddress,
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { isRecoverable, isSubdust } from './index.js';
|
|
2
|
+
export const DEFAULT_THRESHOLD_MS = 3 * 24 * 60 * 60 * 1000; // 3 days
|
|
2
3
|
/**
|
|
3
4
|
* Default renewal configuration values
|
|
4
5
|
*/
|
|
5
6
|
export const DEFAULT_RENEWAL_CONFIG = {
|
|
6
|
-
|
|
7
|
+
thresholdMs: DEFAULT_THRESHOLD_MS, // 3 days
|
|
7
8
|
};
|
|
8
9
|
function getDustAmount(wallet) {
|
|
9
10
|
return "dustAmount" in wallet ? wallet.dustAmount : 330n;
|
|
@@ -77,22 +78,16 @@ function getRecoverableWithSubdust(vtxos, dustAmount) {
|
|
|
77
78
|
* @param thresholdMs - Threshold in milliseconds from now
|
|
78
79
|
* @returns true if VTXO expires within threshold, false otherwise
|
|
79
80
|
*/
|
|
80
|
-
export function isVtxoExpiringSoon(vtxo,
|
|
81
|
+
export function isVtxoExpiringSoon(vtxo, thresholdMs // in milliseconds
|
|
82
|
+
) {
|
|
83
|
+
const realThresholdMs = thresholdMs <= 100 ? DEFAULT_THRESHOLD_MS : thresholdMs;
|
|
81
84
|
const { batchExpiry } = vtxo.virtualStatus;
|
|
82
|
-
if (!batchExpiry)
|
|
85
|
+
if (!batchExpiry)
|
|
83
86
|
return false; // it doesn't expire
|
|
84
|
-
}
|
|
85
87
|
const now = Date.now();
|
|
86
|
-
if (batchExpiry <= now)
|
|
88
|
+
if (batchExpiry <= now)
|
|
87
89
|
return false; // already expired
|
|
88
|
-
|
|
89
|
-
// It shouldn't happen, but let's be safe
|
|
90
|
-
if (!vtxo.createdAt) {
|
|
91
|
-
return false;
|
|
92
|
-
}
|
|
93
|
-
const duration = batchExpiry - vtxo.createdAt.getTime();
|
|
94
|
-
const softExpiry = batchExpiry - (duration * percentage) / 100;
|
|
95
|
-
return softExpiry > 0 && softExpiry <= now;
|
|
90
|
+
return batchExpiry - now <= realThresholdMs;
|
|
96
91
|
}
|
|
97
92
|
/**
|
|
98
93
|
* Filter VTXOs that are expiring soon or are recoverable/subdust
|
|
@@ -102,8 +97,8 @@ export function isVtxoExpiringSoon(vtxo, percentage) {
|
|
|
102
97
|
* @param dustAmount - Dust threshold amount in satoshis
|
|
103
98
|
* @returns Array of VTXOs expiring within threshold
|
|
104
99
|
*/
|
|
105
|
-
export function getExpiringAndRecoverableVtxos(vtxos,
|
|
106
|
-
return vtxos.filter((vtxo) => isVtxoExpiringSoon(vtxo,
|
|
100
|
+
export function getExpiringAndRecoverableVtxos(vtxos, thresholdMs, dustAmount) {
|
|
101
|
+
return vtxos.filter((vtxo) => isVtxoExpiringSoon(vtxo, thresholdMs) ||
|
|
107
102
|
isRecoverable(vtxo) ||
|
|
108
103
|
isSubdust(vtxo, dustAmount));
|
|
109
104
|
}
|
|
@@ -126,7 +121,7 @@ export function getExpiringAndRecoverableVtxos(vtxos, percentage, dustAmount) {
|
|
|
126
121
|
* // Initialize with renewal config
|
|
127
122
|
* const manager = new VtxoManager(wallet, {
|
|
128
123
|
* enabled: true,
|
|
129
|
-
*
|
|
124
|
+
* thresholdMs: 86400000
|
|
130
125
|
* });
|
|
131
126
|
*
|
|
132
127
|
* // Check recoverable balance
|
|
@@ -246,24 +241,24 @@ export class VtxoManager {
|
|
|
246
241
|
/**
|
|
247
242
|
* Get VTXOs that are expiring soon based on renewal configuration
|
|
248
243
|
*
|
|
249
|
-
* @param
|
|
244
|
+
* @param thresholdMs - Optional override for threshold in milliseconds
|
|
250
245
|
* @returns Array of expiring VTXOs, empty array if renewal is disabled or no VTXOs expiring
|
|
251
246
|
*
|
|
252
247
|
* @example
|
|
253
248
|
* ```typescript
|
|
254
|
-
* const manager = new VtxoManager(wallet, { enabled: true,
|
|
249
|
+
* const manager = new VtxoManager(wallet, { enabled: true, thresholdMs: 86400000 });
|
|
255
250
|
* const expiringVtxos = await manager.getExpiringVtxos();
|
|
256
251
|
* if (expiringVtxos.length > 0) {
|
|
257
252
|
* console.log(`${expiringVtxos.length} VTXOs expiring soon`);
|
|
258
253
|
* }
|
|
259
254
|
* ```
|
|
260
255
|
*/
|
|
261
|
-
async getExpiringVtxos(
|
|
256
|
+
async getExpiringVtxos(thresholdMs) {
|
|
262
257
|
const vtxos = await this.wallet.getVtxos({ withRecoverable: true });
|
|
263
|
-
const
|
|
264
|
-
this.renewalConfig?.
|
|
265
|
-
DEFAULT_RENEWAL_CONFIG.
|
|
266
|
-
return getExpiringAndRecoverableVtxos(vtxos,
|
|
258
|
+
const threshold = thresholdMs ??
|
|
259
|
+
this.renewalConfig?.thresholdMs ??
|
|
260
|
+
DEFAULT_RENEWAL_CONFIG.thresholdMs;
|
|
261
|
+
return getExpiringAndRecoverableVtxos(vtxos, threshold, getDustAmount(this.wallet));
|
|
267
262
|
}
|
|
268
263
|
/**
|
|
269
264
|
* Renew expiring VTXOs by settling them back to the wallet's address
|
|
@@ -394,14 +394,13 @@ export class Wallet {
|
|
|
394
394
|
});
|
|
395
395
|
}
|
|
396
396
|
const tapTree = this.offchainTapscript.encode();
|
|
397
|
-
|
|
397
|
+
const offchainTx = buildOffchainTx(selected.inputs.map((input) => ({
|
|
398
398
|
...input,
|
|
399
399
|
tapLeafScript: selectedLeaf,
|
|
400
400
|
tapTree,
|
|
401
401
|
})), outputs, this.serverUnrollScript);
|
|
402
402
|
const signedVirtualTx = await this.identity.sign(offchainTx.arkTx);
|
|
403
403
|
const { arkTxid, signedCheckpointTxs } = await this.arkProvider.submitTx(base64.encode(signedVirtualTx.toPSBT()), offchainTx.checkpoints.map((c) => base64.encode(c.toPSBT())));
|
|
404
|
-
// TODO persist final virtual tx and checkpoints to repository
|
|
405
404
|
// sign the checkpoints
|
|
406
405
|
const finalCheckpoints = await Promise.all(signedCheckpointTxs.map(async (c) => {
|
|
407
406
|
const tx = Transaction.fromPSBT(base64.decode(c));
|
|
@@ -409,7 +408,78 @@ export class Wallet {
|
|
|
409
408
|
return base64.encode(signedCheckpoint.toPSBT());
|
|
410
409
|
}));
|
|
411
410
|
await this.arkProvider.finalizeTx(arkTxid, finalCheckpoints);
|
|
412
|
-
|
|
411
|
+
try {
|
|
412
|
+
// mark VTXOs as spent and optionally add the change VTXO
|
|
413
|
+
const spentVtxos = [];
|
|
414
|
+
const commitmentTxIds = new Set();
|
|
415
|
+
let batchExpiry = Number.MAX_SAFE_INTEGER;
|
|
416
|
+
for (const [inputIndex, input] of selected.inputs.entries()) {
|
|
417
|
+
const vtxo = extendVirtualCoin(this, input);
|
|
418
|
+
const checkpointB64 = signedCheckpointTxs[inputIndex];
|
|
419
|
+
const checkpoint = Transaction.fromPSBT(base64.decode(checkpointB64));
|
|
420
|
+
spentVtxos.push({
|
|
421
|
+
...vtxo,
|
|
422
|
+
virtualStatus: { ...vtxo.virtualStatus, state: "spent" },
|
|
423
|
+
spentBy: checkpoint.id,
|
|
424
|
+
arkTxId: arkTxid,
|
|
425
|
+
isSpent: true,
|
|
426
|
+
});
|
|
427
|
+
if (vtxo.virtualStatus.commitmentTxIds) {
|
|
428
|
+
for (const commitmentTxId of vtxo.virtualStatus
|
|
429
|
+
.commitmentTxIds) {
|
|
430
|
+
commitmentTxIds.add(commitmentTxId);
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
if (vtxo.virtualStatus.batchExpiry) {
|
|
434
|
+
batchExpiry = Math.min(batchExpiry, vtxo.virtualStatus.batchExpiry);
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
const createdAt = Date.now();
|
|
438
|
+
const addr = this.arkAddress.encode();
|
|
439
|
+
if (selected.changeAmount > 0n &&
|
|
440
|
+
batchExpiry !== Number.MAX_SAFE_INTEGER) {
|
|
441
|
+
const changeVtxo = {
|
|
442
|
+
txid: arkTxid,
|
|
443
|
+
vout: outputs.length - 1,
|
|
444
|
+
createdAt: new Date(createdAt),
|
|
445
|
+
forfeitTapLeafScript: this.offchainTapscript.forfeit(),
|
|
446
|
+
intentTapLeafScript: this.offchainTapscript.exit(),
|
|
447
|
+
isUnrolled: false,
|
|
448
|
+
isSpent: false,
|
|
449
|
+
tapTree: this.offchainTapscript.encode(),
|
|
450
|
+
value: Number(selected.changeAmount),
|
|
451
|
+
virtualStatus: {
|
|
452
|
+
state: "preconfirmed",
|
|
453
|
+
commitmentTxIds: Array.from(commitmentTxIds),
|
|
454
|
+
batchExpiry,
|
|
455
|
+
},
|
|
456
|
+
status: {
|
|
457
|
+
confirmed: false,
|
|
458
|
+
},
|
|
459
|
+
};
|
|
460
|
+
await this.walletRepository.saveVtxos(addr, [changeVtxo]);
|
|
461
|
+
}
|
|
462
|
+
await this.walletRepository.saveVtxos(addr, spentVtxos);
|
|
463
|
+
await this.walletRepository.saveTransactions(addr, [
|
|
464
|
+
{
|
|
465
|
+
key: {
|
|
466
|
+
boardingTxid: "",
|
|
467
|
+
commitmentTxid: "",
|
|
468
|
+
arkTxid: arkTxid,
|
|
469
|
+
},
|
|
470
|
+
amount: params.amount,
|
|
471
|
+
type: TxType.TxSent,
|
|
472
|
+
settled: false,
|
|
473
|
+
createdAt: Date.now(),
|
|
474
|
+
},
|
|
475
|
+
]);
|
|
476
|
+
}
|
|
477
|
+
catch (e) {
|
|
478
|
+
console.warn("error saving offchain tx to repository", e);
|
|
479
|
+
}
|
|
480
|
+
finally {
|
|
481
|
+
return arkTxid;
|
|
482
|
+
}
|
|
413
483
|
}
|
|
414
484
|
async settle(params, eventCallback) {
|
|
415
485
|
if (params?.inputs) {
|
|
@@ -682,7 +752,8 @@ export class Wallet {
|
|
|
682
752
|
(async () => {
|
|
683
753
|
try {
|
|
684
754
|
for await (const update of subscription) {
|
|
685
|
-
if (update.newVtxos?.length > 0
|
|
755
|
+
if (update.newVtxos?.length > 0 ||
|
|
756
|
+
update.spentVtxos?.length > 0) {
|
|
686
757
|
eventCallback({
|
|
687
758
|
type: "vtxo",
|
|
688
759
|
newVtxos: update.newVtxos.map((vtxo) => extendVirtualCoin(this, vtxo)),
|
package/dist/types/index.d.ts
CHANGED
|
@@ -10,7 +10,7 @@ import { Wallet, waitForIncomingFunds, IncomingFunds } from "./wallet/wallet";
|
|
|
10
10
|
import { TxTree, TxTreeNode } from "./tree/txTree";
|
|
11
11
|
import { SignerSession, TreeNonces, TreePartialSigs } from "./tree/signingSession";
|
|
12
12
|
import { Ramps } from "./wallet/ramps";
|
|
13
|
-
import { VtxoManager } from "./wallet/vtxo-manager";
|
|
13
|
+
import { isVtxoExpiringSoon, VtxoManager } from "./wallet/vtxo-manager";
|
|
14
14
|
import { ServiceWorkerWallet } from "./wallet/serviceWorker/wallet";
|
|
15
15
|
import { OnchainWallet } from "./wallet/onchain";
|
|
16
16
|
import { setupServiceWorker } from "./wallet/serviceWorker/utils";
|
|
@@ -18,7 +18,7 @@ import { Worker } from "./wallet/serviceWorker/worker";
|
|
|
18
18
|
import { Request } from "./wallet/serviceWorker/request";
|
|
19
19
|
import { Response } from "./wallet/serviceWorker/response";
|
|
20
20
|
import { ESPLORA_URL, EsploraProvider, OnchainProvider, ExplorerTransaction } from "./providers/onchain";
|
|
21
|
-
import { RestArkProvider, ArkProvider, SettlementEvent, SettlementEventType, ArkInfo, SignedIntent, Output, TxNotification, BatchFinalizationEvent, BatchFinalizedEvent, BatchFailedEvent, TreeSigningStartedEvent, TreeNoncesEvent, BatchStartedEvent, TreeTxEvent, TreeSignatureEvent, ScheduledSession } from "./providers/ark";
|
|
21
|
+
import { RestArkProvider, ArkProvider, SettlementEvent, SettlementEventType, ArkInfo, SignedIntent, Output, TxNotification, BatchFinalizationEvent, BatchFinalizedEvent, BatchFailedEvent, TreeSigningStartedEvent, TreeNoncesEvent, BatchStartedEvent, TreeTxEvent, TreeSignatureEvent, ScheduledSession, FeeInfo } from "./providers/ark";
|
|
22
22
|
import { CLTVMultisigTapscript, ConditionCSVMultisigTapscript, ConditionMultisigTapscript, CSVMultisigTapscript, decodeTapscript, MultisigTapscript, TapscriptType, ArkTapscript, RelativeTimelock } from "./script/tapscript";
|
|
23
23
|
import { hasBoardingTxExpired, buildOffchainTx, verifyTapscriptSignatures, ArkTxInput, OffchainTx, combineTapscriptSigs } from "./utils/arkTransaction";
|
|
24
24
|
import { VtxoTaprootTree, ConditionWitness, getArkPsbtFields, setArkPsbtField, ArkPsbtFieldCoder, ArkPsbtFieldKey, ArkPsbtFieldKeyType, CosignerPublicKey, VtxoTreeExpiry } from "./utils/unknownFields";
|
|
@@ -33,5 +33,5 @@ import { Unroll } from "./wallet/unroll";
|
|
|
33
33
|
import { WalletRepositoryImpl } from "./repositories/walletRepository";
|
|
34
34
|
import { ContractRepositoryImpl } from "./repositories/contractRepository";
|
|
35
35
|
import { ArkError, maybeArkError } from "./providers/errors";
|
|
36
|
-
export { Wallet, SingleKey, OnchainWallet, Ramps, VtxoManager, ESPLORA_URL, EsploraProvider, RestArkProvider, RestIndexerProvider, ArkAddress, DefaultVtxo, VtxoScript, VHTLC, TxType, IndexerTxType, ChainTxType, SettlementEventType, setupServiceWorker, Worker, ServiceWorkerWallet, Request, Response, decodeTapscript, MultisigTapscript, CSVMultisigTapscript, ConditionCSVMultisigTapscript, ConditionMultisigTapscript, CLTVMultisigTapscript, TapTreeCoder, ArkPsbtFieldKey, ArkPsbtFieldKeyType, setArkPsbtField, getArkPsbtFields, CosignerPublicKey, VtxoTreeExpiry, VtxoTaprootTree, ConditionWitness, buildOffchainTx, verifyTapscriptSignatures, waitForIncomingFunds, hasBoardingTxExpired, combineTapscriptSigs, ArkNote, networks, WalletRepositoryImpl, ContractRepositoryImpl, Intent, TxTree, P2A, Unroll, Transaction, ArkError, maybeArkError, };
|
|
37
|
-
export type { Identity, IWallet, WalletConfig, ProviderClass, ArkTransaction, Coin, ExtendedCoin, ExtendedVirtualCoin, WalletBalance, SendBitcoinParams, Recipient, SettleParams, Status, VirtualStatus, Outpoint, VirtualCoin, TxKey, TapscriptType, ArkTxInput, OffchainTx, TapLeaves, IncomingFunds, IndexerProvider, PageResponse, Batch, ChainTx, CommitmentTx, TxHistoryRecord, Vtxo, VtxoChain, Tx, OnchainProvider, ArkProvider, SettlementEvent, ArkInfo, SignedIntent, Output, TxNotification, ExplorerTransaction, BatchFinalizationEvent, BatchFinalizedEvent, BatchFailedEvent, TreeSigningStartedEvent, TreeNoncesEvent, BatchStartedEvent, TreeTxEvent, TreeSignatureEvent, ScheduledSession, PaginationOptions, SubscriptionResponse, SubscriptionHeartbeat, SubscriptionEvent, Network, NetworkName, ArkTapscript, RelativeTimelock, EncodedVtxoScript, TapLeafScript, SignerSession, TreeNonces, TreePartialSigs, GetVtxosFilter, Nonces, PartialSig, ArkPsbtFieldCoder, TxTreeNode, AnchorBumper, };
|
|
36
|
+
export { Wallet, SingleKey, OnchainWallet, Ramps, VtxoManager, ESPLORA_URL, EsploraProvider, RestArkProvider, RestIndexerProvider, ArkAddress, DefaultVtxo, VtxoScript, VHTLC, TxType, IndexerTxType, ChainTxType, SettlementEventType, setupServiceWorker, Worker, ServiceWorkerWallet, Request, Response, decodeTapscript, MultisigTapscript, CSVMultisigTapscript, ConditionCSVMultisigTapscript, ConditionMultisigTapscript, CLTVMultisigTapscript, TapTreeCoder, ArkPsbtFieldKey, ArkPsbtFieldKeyType, setArkPsbtField, getArkPsbtFields, CosignerPublicKey, VtxoTreeExpiry, VtxoTaprootTree, ConditionWitness, buildOffchainTx, verifyTapscriptSignatures, waitForIncomingFunds, hasBoardingTxExpired, combineTapscriptSigs, isVtxoExpiringSoon, ArkNote, networks, WalletRepositoryImpl, ContractRepositoryImpl, Intent, TxTree, P2A, Unroll, Transaction, ArkError, maybeArkError, };
|
|
37
|
+
export type { Identity, IWallet, WalletConfig, ProviderClass, ArkTransaction, Coin, ExtendedCoin, ExtendedVirtualCoin, WalletBalance, SendBitcoinParams, Recipient, SettleParams, Status, VirtualStatus, Outpoint, VirtualCoin, TxKey, TapscriptType, ArkTxInput, OffchainTx, TapLeaves, IncomingFunds, IndexerProvider, PageResponse, Batch, ChainTx, CommitmentTx, TxHistoryRecord, Vtxo, VtxoChain, Tx, OnchainProvider, ArkProvider, SettlementEvent, FeeInfo, ArkInfo, SignedIntent, Output, TxNotification, ExplorerTransaction, BatchFinalizationEvent, BatchFinalizedEvent, BatchFailedEvent, TreeSigningStartedEvent, TreeNoncesEvent, BatchStartedEvent, TreeTxEvent, TreeSignatureEvent, ScheduledSession, PaginationOptions, SubscriptionResponse, SubscriptionHeartbeat, SubscriptionEvent, Network, NetworkName, ArkTapscript, RelativeTimelock, EncodedVtxoScript, TapLeafScript, SignerSession, TreeNonces, TreePartialSigs, GetVtxosFilter, Nonces, PartialSig, ArkPsbtFieldCoder, TxTreeNode, AnchorBumper, };
|
|
@@ -75,8 +75,8 @@ export interface ScheduledSession {
|
|
|
75
75
|
export interface IntentFeeInfo {
|
|
76
76
|
offchainInput: string;
|
|
77
77
|
offchainOutput: string;
|
|
78
|
-
onchainInput:
|
|
79
|
-
onchainOutput:
|
|
78
|
+
onchainInput: bigint;
|
|
79
|
+
onchainOutput: bigint;
|
|
80
80
|
}
|
|
81
81
|
export interface FeeInfo {
|
|
82
82
|
intentFee: IntentFeeInfo;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { ExtendedCoin, IWallet } from ".";
|
|
2
|
-
import { SettlementEvent } from "../providers/ark";
|
|
2
|
+
import { FeeInfo, SettlementEvent } from "../providers/ark";
|
|
3
3
|
/**
|
|
4
4
|
* Ramps is a class wrapping IWallet.settle method to provide a more convenient interface for onboarding and offboarding operations.
|
|
5
5
|
*
|
|
@@ -25,8 +25,9 @@ export declare class Ramps {
|
|
|
25
25
|
* Offboard vtxos, or "collaborative exit" vtxos to onchain address.
|
|
26
26
|
*
|
|
27
27
|
* @param destinationAddress - The destination address to offboard to.
|
|
28
|
+
* @param feeInfo - The fee info to deduct from the offboard amount.
|
|
28
29
|
* @param amount - The amount to offboard. If not provided, the total amount of vtxos will be offboarded.
|
|
29
30
|
* @param eventCallback - The callback to receive settlement events. optional.
|
|
30
31
|
*/
|
|
31
|
-
offboard(destinationAddress: string, amount?: bigint, eventCallback?: (event: SettlementEvent) => void): ReturnType<IWallet["settle"]>;
|
|
32
|
+
offboard(destinationAddress: string, feeInfo: FeeInfo, amount?: bigint, eventCallback?: (event: SettlementEvent) => void): ReturnType<IWallet["settle"]>;
|
|
32
33
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { ExtendedVirtualCoin, IWallet } from ".";
|
|
2
2
|
import { SettlementEvent } from "../providers/ark";
|
|
3
|
+
export declare const DEFAULT_THRESHOLD_MS: number;
|
|
3
4
|
/**
|
|
4
5
|
* Configuration options for automatic VTXO renewal
|
|
5
6
|
*/
|
|
@@ -10,11 +11,11 @@ export interface RenewalConfig {
|
|
|
10
11
|
*/
|
|
11
12
|
enabled?: boolean;
|
|
12
13
|
/**
|
|
13
|
-
*
|
|
14
|
-
* E.g.,
|
|
15
|
-
* @default
|
|
14
|
+
* Threshold in milliseconds to use as threshold for renewal
|
|
15
|
+
* E.g., 86400000 means renew when 24 hours until expiry remains
|
|
16
|
+
* @default 86400000 (24 hours)
|
|
16
17
|
*/
|
|
17
|
-
|
|
18
|
+
thresholdMs?: number;
|
|
18
19
|
}
|
|
19
20
|
/**
|
|
20
21
|
* Default renewal configuration values
|
|
@@ -27,7 +28,7 @@ export declare const DEFAULT_RENEWAL_CONFIG: Required<Omit<RenewalConfig, "enabl
|
|
|
27
28
|
* @param thresholdMs - Threshold in milliseconds from now
|
|
28
29
|
* @returns true if VTXO expires within threshold, false otherwise
|
|
29
30
|
*/
|
|
30
|
-
export declare function isVtxoExpiringSoon(vtxo: ExtendedVirtualCoin,
|
|
31
|
+
export declare function isVtxoExpiringSoon(vtxo: ExtendedVirtualCoin, thresholdMs: number): boolean;
|
|
31
32
|
/**
|
|
32
33
|
* Filter VTXOs that are expiring soon or are recoverable/subdust
|
|
33
34
|
*
|
|
@@ -36,7 +37,7 @@ export declare function isVtxoExpiringSoon(vtxo: ExtendedVirtualCoin, percentage
|
|
|
36
37
|
* @param dustAmount - Dust threshold amount in satoshis
|
|
37
38
|
* @returns Array of VTXOs expiring within threshold
|
|
38
39
|
*/
|
|
39
|
-
export declare function getExpiringAndRecoverableVtxos(vtxos: ExtendedVirtualCoin[],
|
|
40
|
+
export declare function getExpiringAndRecoverableVtxos(vtxos: ExtendedVirtualCoin[], thresholdMs: number, dustAmount: bigint): ExtendedVirtualCoin[];
|
|
40
41
|
/**
|
|
41
42
|
* VtxoManager is a unified class for managing VTXO lifecycle operations including
|
|
42
43
|
* recovery of swept/expired VTXOs and renewal to prevent expiration.
|
|
@@ -56,7 +57,7 @@ export declare function getExpiringAndRecoverableVtxos(vtxos: ExtendedVirtualCoi
|
|
|
56
57
|
* // Initialize with renewal config
|
|
57
58
|
* const manager = new VtxoManager(wallet, {
|
|
58
59
|
* enabled: true,
|
|
59
|
-
*
|
|
60
|
+
* thresholdMs: 86400000
|
|
60
61
|
* });
|
|
61
62
|
*
|
|
62
63
|
* // Check recoverable balance
|
|
@@ -137,19 +138,19 @@ export declare class VtxoManager {
|
|
|
137
138
|
/**
|
|
138
139
|
* Get VTXOs that are expiring soon based on renewal configuration
|
|
139
140
|
*
|
|
140
|
-
* @param
|
|
141
|
+
* @param thresholdMs - Optional override for threshold in milliseconds
|
|
141
142
|
* @returns Array of expiring VTXOs, empty array if renewal is disabled or no VTXOs expiring
|
|
142
143
|
*
|
|
143
144
|
* @example
|
|
144
145
|
* ```typescript
|
|
145
|
-
* const manager = new VtxoManager(wallet, { enabled: true,
|
|
146
|
+
* const manager = new VtxoManager(wallet, { enabled: true, thresholdMs: 86400000 });
|
|
146
147
|
* const expiringVtxos = await manager.getExpiringVtxos();
|
|
147
148
|
* if (expiringVtxos.length > 0) {
|
|
148
149
|
* console.log(`${expiringVtxos.length} VTXOs expiring soon`);
|
|
149
150
|
* }
|
|
150
151
|
* ```
|
|
151
152
|
*/
|
|
152
|
-
getExpiringVtxos(
|
|
153
|
+
getExpiringVtxos(thresholdMs?: number): Promise<ExtendedVirtualCoin[]>;
|
|
153
154
|
/**
|
|
154
155
|
* Renew expiring VTXOs by settling them back to the wallet's address
|
|
155
156
|
*
|
|
@@ -71,7 +71,7 @@ export declare class Wallet implements IWallet {
|
|
|
71
71
|
readonly contractRepository: ContractRepository;
|
|
72
72
|
readonly renewalConfig: Required<Omit<WalletConfig["renewalConfig"], "enabled">> & {
|
|
73
73
|
enabled: boolean;
|
|
74
|
-
|
|
74
|
+
thresholdMs: number;
|
|
75
75
|
};
|
|
76
76
|
private constructor();
|
|
77
77
|
static create(config: WalletConfig): Promise<Wallet>;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@arkade-os/sdk",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.6",
|
|
4
4
|
"description": "Bitcoin wallet SDK with Taproot and Ark integration",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/cjs/index.js",
|
|
@@ -63,8 +63,8 @@
|
|
|
63
63
|
"@types/node": "24.3.1",
|
|
64
64
|
"@vitest/coverage-v8": "3.2.4",
|
|
65
65
|
"esbuild": "^0.25.9",
|
|
66
|
-
"expo": "~52.0.47",
|
|
67
66
|
"eventsource": "4.0.0",
|
|
67
|
+
"expo": "~52.0.47",
|
|
68
68
|
"glob": "11.0.3",
|
|
69
69
|
"husky": "9.1.7",
|
|
70
70
|
"prettier": "3.6.2",
|