@arkade-os/sdk 0.3.0-alpha.8 → 0.3.1-alpha.1
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 +64 -14
- package/dist/cjs/arknote/index.js +3 -3
- package/dist/cjs/forfeit.js +5 -2
- package/dist/cjs/identity/singleKey.js +5 -4
- package/dist/cjs/index.js +6 -3
- package/dist/cjs/{bip322 → intent}/index.js +37 -55
- package/dist/cjs/providers/ark.js +62 -23
- 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/script/base.js +1 -2
- package/dist/cjs/script/tapscript.js +20 -21
- package/dist/cjs/script/vhtlc.js +2 -2
- package/dist/cjs/tree/signingSession.js +7 -8
- package/dist/cjs/tree/txTree.js +3 -4
- package/dist/cjs/tree/validation.js +2 -3
- package/dist/cjs/utils/arkTransaction.js +104 -12
- package/dist/cjs/utils/unknownFields.js +5 -5
- package/dist/cjs/wallet/onchain.js +4 -5
- package/dist/cjs/wallet/serviceWorker/utils.js +2 -0
- package/dist/cjs/wallet/serviceWorker/wallet.js +4 -8
- package/dist/cjs/wallet/serviceWorker/worker.js +23 -18
- package/dist/cjs/wallet/unroll.js +6 -7
- package/dist/cjs/wallet/vtxo-manager.js +381 -0
- package/dist/cjs/wallet/wallet.js +63 -94
- package/dist/esm/arknote/index.js +2 -2
- package/dist/esm/forfeit.js +4 -1
- package/dist/esm/identity/singleKey.js +7 -6
- package/dist/esm/index.js +7 -6
- package/dist/esm/{bip322 → intent}/index.js +31 -48
- package/dist/esm/providers/ark.js +62 -23
- 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/script/base.js +1 -2
- package/dist/esm/script/tapscript.js +1 -2
- package/dist/esm/script/vhtlc.js +1 -1
- package/dist/esm/tree/signingSession.js +8 -9
- package/dist/esm/tree/txTree.js +3 -4
- package/dist/esm/tree/validation.js +2 -3
- package/dist/esm/utils/arkTransaction.js +95 -4
- package/dist/esm/utils/unknownFields.js +1 -1
- package/dist/esm/wallet/onchain.js +1 -2
- package/dist/esm/wallet/serviceWorker/utils.js +1 -0
- package/dist/esm/wallet/serviceWorker/wallet.js +5 -9
- package/dist/esm/wallet/serviceWorker/worker.js +23 -18
- package/dist/esm/wallet/unroll.js +2 -3
- package/dist/esm/wallet/vtxo-manager.js +372 -0
- package/dist/esm/wallet/wallet.js +56 -87
- package/dist/types/arknote/index.d.ts +1 -1
- package/dist/types/forfeit.d.ts +2 -2
- package/dist/types/identity/index.d.ts +1 -1
- package/dist/types/identity/singleKey.d.ts +1 -1
- package/dist/types/index.d.ts +6 -5
- package/dist/types/intent/index.d.ts +41 -0
- package/dist/types/providers/ark.d.ts +55 -21
- 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/script/base.d.ts +3 -2
- package/dist/types/tree/signingSession.d.ts +10 -10
- package/dist/types/utils/anchor.d.ts +2 -2
- package/dist/types/utils/arkTransaction.d.ts +13 -3
- package/dist/types/utils/unknownFields.d.ts +2 -2
- package/dist/types/wallet/index.d.ts +6 -4
- package/dist/types/wallet/onchain.d.ts +1 -1
- 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 +3 -1
- package/dist/types/wallet/unroll.d.ts +1 -1
- package/dist/types/wallet/vtxo-manager.d.ts +207 -0
- package/dist/types/wallet/wallet.d.ts +7 -3
- 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
|
@@ -0,0 +1,372 @@
|
|
|
1
|
+
import { isRecoverable, isSubdust } from './index.js';
|
|
2
|
+
/**
|
|
3
|
+
* Default renewal configuration values
|
|
4
|
+
*/
|
|
5
|
+
export const DEFAULT_RENEWAL_CONFIG = {
|
|
6
|
+
thresholdPercentage: 10,
|
|
7
|
+
};
|
|
8
|
+
/**
|
|
9
|
+
* Filter VTXOs that are recoverable (swept and still spendable, or preconfirmed subdust)
|
|
10
|
+
*
|
|
11
|
+
* Recovery strategy:
|
|
12
|
+
* - Always recover swept VTXOs (they've been taken by the server)
|
|
13
|
+
* - Only recover subdust preconfirmed VTXOs (to avoid locking liquidity on settled VTXOs with long expiry)
|
|
14
|
+
*
|
|
15
|
+
* @param vtxos - Array of virtual coins to check
|
|
16
|
+
* @param dustAmount - Dust threshold to identify subdust
|
|
17
|
+
* @returns Array of recoverable VTXOs
|
|
18
|
+
*/
|
|
19
|
+
function getRecoverableVtxos(vtxos, dustAmount) {
|
|
20
|
+
return vtxos.filter((vtxo) => {
|
|
21
|
+
// Always recover swept VTXOs
|
|
22
|
+
if (isRecoverable(vtxo)) {
|
|
23
|
+
return true;
|
|
24
|
+
}
|
|
25
|
+
// Recover preconfirmed subdust to consolidate small amounts
|
|
26
|
+
if (vtxo.virtualStatus.state === "preconfirmed" &&
|
|
27
|
+
isSubdust(vtxo, dustAmount)) {
|
|
28
|
+
return true;
|
|
29
|
+
}
|
|
30
|
+
return false;
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Get recoverable VTXOs including subdust coins if the total value exceeds dust threshold.
|
|
35
|
+
*
|
|
36
|
+
* Decision is based on the combined total of ALL recoverable VTXOs (regular + subdust),
|
|
37
|
+
* not just the subdust portion alone.
|
|
38
|
+
*
|
|
39
|
+
* @param vtxos - Array of virtual coins to check
|
|
40
|
+
* @param dustAmount - Dust threshold amount in satoshis
|
|
41
|
+
* @returns Object containing recoverable VTXOs and whether subdust should be included
|
|
42
|
+
*/
|
|
43
|
+
function getRecoverableWithSubdust(vtxos, dustAmount) {
|
|
44
|
+
const recoverableVtxos = getRecoverableVtxos(vtxos, dustAmount);
|
|
45
|
+
// Separate subdust from regular recoverable
|
|
46
|
+
const subdust = [];
|
|
47
|
+
const regular = [];
|
|
48
|
+
for (const vtxo of recoverableVtxos) {
|
|
49
|
+
if (isSubdust(vtxo, dustAmount)) {
|
|
50
|
+
subdust.push(vtxo);
|
|
51
|
+
}
|
|
52
|
+
else {
|
|
53
|
+
regular.push(vtxo);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
// Calculate totals
|
|
57
|
+
const regularTotal = regular.reduce((sum, vtxo) => sum + BigInt(vtxo.value), 0n);
|
|
58
|
+
const subdustTotal = subdust.reduce((sum, vtxo) => sum + BigInt(vtxo.value), 0n);
|
|
59
|
+
const combinedTotal = regularTotal + subdustTotal;
|
|
60
|
+
// Include subdust only if the combined total exceeds dust threshold
|
|
61
|
+
const shouldIncludeSubdust = combinedTotal >= dustAmount;
|
|
62
|
+
const vtxosToRecover = shouldIncludeSubdust ? recoverableVtxos : regular;
|
|
63
|
+
const totalAmount = vtxosToRecover.reduce((sum, vtxo) => sum + BigInt(vtxo.value), 0n);
|
|
64
|
+
return {
|
|
65
|
+
vtxosToRecover,
|
|
66
|
+
includesSubdust: shouldIncludeSubdust,
|
|
67
|
+
totalAmount,
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Check if a VTXO is expiring soon based on threshold
|
|
72
|
+
*
|
|
73
|
+
* @param vtxo - The virtual coin to check
|
|
74
|
+
* @param thresholdMs - Threshold in milliseconds from now
|
|
75
|
+
* @returns true if VTXO expires within threshold, false otherwise
|
|
76
|
+
*/
|
|
77
|
+
export function isVtxoExpiringSoon(vtxo, thresholdMs) {
|
|
78
|
+
const { batchExpiry } = vtxo.virtualStatus;
|
|
79
|
+
// No expiry set means it doesn't expire
|
|
80
|
+
if (!batchExpiry) {
|
|
81
|
+
return false;
|
|
82
|
+
}
|
|
83
|
+
const now = Date.now();
|
|
84
|
+
const timeUntilExpiry = batchExpiry - now;
|
|
85
|
+
return timeUntilExpiry > 0 && timeUntilExpiry <= thresholdMs;
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Filter VTXOs that are expiring soon
|
|
89
|
+
*
|
|
90
|
+
* @param vtxos - Array of virtual coins to check
|
|
91
|
+
* @param thresholdMs - Threshold in milliseconds from now
|
|
92
|
+
* @returns Array of VTXOs expiring within threshold
|
|
93
|
+
*/
|
|
94
|
+
export function getExpiringVtxos(vtxos, thresholdMs) {
|
|
95
|
+
return vtxos.filter((vtxo) => isVtxoExpiringSoon(vtxo, thresholdMs));
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Calculate expiry threshold in milliseconds based on batch expiry and percentage
|
|
99
|
+
*
|
|
100
|
+
* @param batchExpiry - Batch expiry timestamp in milliseconds
|
|
101
|
+
* @param percentage - Percentage of total time (0-100)
|
|
102
|
+
* @returns Threshold timestamp in milliseconds from now
|
|
103
|
+
*
|
|
104
|
+
* @example
|
|
105
|
+
* // VTXO expires in 10 days, threshold is 10%
|
|
106
|
+
* const expiry = Date.now() + 10 * 24 * 60 * 60 * 1000;
|
|
107
|
+
* const threshold = calculateExpiryThreshold(expiry, 10);
|
|
108
|
+
* // Returns 1 day in milliseconds (10% of 10 days)
|
|
109
|
+
*/
|
|
110
|
+
export function calculateExpiryThreshold(batchExpiry, percentage) {
|
|
111
|
+
if (percentage < 0 || percentage > 100) {
|
|
112
|
+
throw new Error("Percentage must be between 0 and 100");
|
|
113
|
+
}
|
|
114
|
+
const now = Date.now();
|
|
115
|
+
const totalTime = batchExpiry - now;
|
|
116
|
+
if (totalTime <= 0) {
|
|
117
|
+
// Already expired
|
|
118
|
+
return 0;
|
|
119
|
+
}
|
|
120
|
+
// Calculate threshold as percentage of total time
|
|
121
|
+
return Math.floor((totalTime * percentage) / 100);
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* Get the minimum expiry time from a list of VTXOs
|
|
125
|
+
*
|
|
126
|
+
* @param vtxos - Array of virtual coins
|
|
127
|
+
* @returns Minimum batch expiry timestamp, or undefined if no VTXOs have expiry
|
|
128
|
+
*/
|
|
129
|
+
export function getMinimumExpiry(vtxos) {
|
|
130
|
+
const expiries = vtxos
|
|
131
|
+
.map((v) => v.virtualStatus.batchExpiry)
|
|
132
|
+
.filter((e) => e !== undefined);
|
|
133
|
+
if (expiries.length === 0) {
|
|
134
|
+
return undefined;
|
|
135
|
+
}
|
|
136
|
+
return Math.min(...expiries);
|
|
137
|
+
}
|
|
138
|
+
/**
|
|
139
|
+
* Calculate dynamic threshold based on the earliest expiring VTXO
|
|
140
|
+
*
|
|
141
|
+
* @param vtxos - Array of virtual coins
|
|
142
|
+
* @param percentage - Percentage of time until expiry (0-100)
|
|
143
|
+
* @returns Threshold in milliseconds, or undefined if no VTXOs have expiry
|
|
144
|
+
*/
|
|
145
|
+
export function calculateDynamicThreshold(vtxos, percentage) {
|
|
146
|
+
const minExpiry = getMinimumExpiry(vtxos);
|
|
147
|
+
if (!minExpiry) {
|
|
148
|
+
return undefined;
|
|
149
|
+
}
|
|
150
|
+
return calculateExpiryThreshold(minExpiry, percentage);
|
|
151
|
+
}
|
|
152
|
+
/**
|
|
153
|
+
* VtxoManager is a unified class for managing VTXO lifecycle operations including
|
|
154
|
+
* recovery of swept/expired VTXOs and renewal to prevent expiration.
|
|
155
|
+
*
|
|
156
|
+
* Key Features:
|
|
157
|
+
* - **Recovery**: Reclaim swept or expired VTXOs back to the wallet
|
|
158
|
+
* - **Renewal**: Refresh VTXO expiration time before they expire
|
|
159
|
+
* - **Smart subdust handling**: Automatically includes subdust VTXOs when economically viable
|
|
160
|
+
* - **Expiry monitoring**: Check for VTXOs that are expiring soon
|
|
161
|
+
*
|
|
162
|
+
* VTXOs become recoverable when:
|
|
163
|
+
* - The Ark server sweeps them (virtualStatus.state === "swept") and they remain spendable
|
|
164
|
+
* - They are preconfirmed subdust (to consolidate small amounts without locking liquidity on settled VTXOs)
|
|
165
|
+
*
|
|
166
|
+
* @example
|
|
167
|
+
* ```typescript
|
|
168
|
+
* // Initialize with renewal config
|
|
169
|
+
* const manager = new VtxoManager(wallet, {
|
|
170
|
+
* enabled: true,
|
|
171
|
+
* thresholdPercentage: 10
|
|
172
|
+
* });
|
|
173
|
+
*
|
|
174
|
+
* // Check recoverable balance
|
|
175
|
+
* const balance = await manager.getRecoverableBalance();
|
|
176
|
+
* if (balance.recoverable > 0n) {
|
|
177
|
+
* console.log(`Can recover ${balance.recoverable} sats`);
|
|
178
|
+
* const txid = await manager.recoverVtxos();
|
|
179
|
+
* }
|
|
180
|
+
*
|
|
181
|
+
* // Check for expiring VTXOs
|
|
182
|
+
* const expiring = await manager.getExpiringVtxos();
|
|
183
|
+
* if (expiring.length > 0) {
|
|
184
|
+
* console.log(`${expiring.length} VTXOs expiring soon`);
|
|
185
|
+
* const txid = await manager.renewVtxos();
|
|
186
|
+
* }
|
|
187
|
+
* ```
|
|
188
|
+
*/
|
|
189
|
+
export class VtxoManager {
|
|
190
|
+
constructor(wallet, renewalConfig) {
|
|
191
|
+
this.wallet = wallet;
|
|
192
|
+
this.renewalConfig = renewalConfig;
|
|
193
|
+
}
|
|
194
|
+
// ========== Recovery Methods ==========
|
|
195
|
+
/**
|
|
196
|
+
* Recover swept/expired VTXOs by settling them back to the wallet's Ark address.
|
|
197
|
+
*
|
|
198
|
+
* This method:
|
|
199
|
+
* 1. Fetches all VTXOs (including recoverable ones)
|
|
200
|
+
* 2. Filters for swept but still spendable VTXOs and preconfirmed subdust
|
|
201
|
+
* 3. Includes subdust VTXOs if the total value >= dust threshold
|
|
202
|
+
* 4. Settles everything back to the wallet's Ark address
|
|
203
|
+
*
|
|
204
|
+
* Note: Settled VTXOs with long expiry are NOT recovered to avoid locking liquidity unnecessarily.
|
|
205
|
+
* Only preconfirmed subdust is recovered to consolidate small amounts.
|
|
206
|
+
*
|
|
207
|
+
* @param eventCallback - Optional callback to receive settlement events
|
|
208
|
+
* @returns Settlement transaction ID
|
|
209
|
+
* @throws Error if no recoverable VTXOs found
|
|
210
|
+
*
|
|
211
|
+
* @example
|
|
212
|
+
* ```typescript
|
|
213
|
+
* const manager = new VtxoManager(wallet);
|
|
214
|
+
*
|
|
215
|
+
* // Simple recovery
|
|
216
|
+
* const txid = await manager.recoverVtxos();
|
|
217
|
+
*
|
|
218
|
+
* // With event callback
|
|
219
|
+
* const txid = await manager.recoverVtxos((event) => {
|
|
220
|
+
* console.log('Settlement event:', event.type);
|
|
221
|
+
* });
|
|
222
|
+
* ```
|
|
223
|
+
*/
|
|
224
|
+
async recoverVtxos(eventCallback) {
|
|
225
|
+
// Get all VTXOs including recoverable ones
|
|
226
|
+
const allVtxos = await this.wallet.getVtxos({
|
|
227
|
+
withRecoverable: true,
|
|
228
|
+
withUnrolled: false,
|
|
229
|
+
});
|
|
230
|
+
// Get dust amount from wallet
|
|
231
|
+
const dustAmount = "dustAmount" in this.wallet
|
|
232
|
+
? this.wallet.dustAmount
|
|
233
|
+
: 1000n;
|
|
234
|
+
// Filter recoverable VTXOs and handle subdust logic
|
|
235
|
+
const { vtxosToRecover, includesSubdust, totalAmount } = getRecoverableWithSubdust(allVtxos, dustAmount);
|
|
236
|
+
if (vtxosToRecover.length === 0) {
|
|
237
|
+
throw new Error("No recoverable VTXOs found");
|
|
238
|
+
}
|
|
239
|
+
const arkAddress = await this.wallet.getAddress();
|
|
240
|
+
// Settle all recoverable VTXOs back to the wallet
|
|
241
|
+
return this.wallet.settle({
|
|
242
|
+
inputs: vtxosToRecover,
|
|
243
|
+
outputs: [
|
|
244
|
+
{
|
|
245
|
+
address: arkAddress,
|
|
246
|
+
amount: totalAmount,
|
|
247
|
+
},
|
|
248
|
+
],
|
|
249
|
+
}, eventCallback);
|
|
250
|
+
}
|
|
251
|
+
/**
|
|
252
|
+
* Get information about recoverable balance without executing recovery.
|
|
253
|
+
*
|
|
254
|
+
* Useful for displaying to users before they decide to recover funds.
|
|
255
|
+
*
|
|
256
|
+
* @returns Object containing recoverable amounts and subdust information
|
|
257
|
+
*
|
|
258
|
+
* @example
|
|
259
|
+
* ```typescript
|
|
260
|
+
* const manager = new VtxoManager(wallet);
|
|
261
|
+
* const balance = await manager.getRecoverableBalance();
|
|
262
|
+
*
|
|
263
|
+
* if (balance.recoverable > 0n) {
|
|
264
|
+
* console.log(`You can recover ${balance.recoverable} sats`);
|
|
265
|
+
* if (balance.includesSubdust) {
|
|
266
|
+
* console.log(`This includes ${balance.subdust} sats from subdust VTXOs`);
|
|
267
|
+
* }
|
|
268
|
+
* }
|
|
269
|
+
* ```
|
|
270
|
+
*/
|
|
271
|
+
async getRecoverableBalance() {
|
|
272
|
+
const allVtxos = await this.wallet.getVtxos({
|
|
273
|
+
withRecoverable: true,
|
|
274
|
+
withUnrolled: false,
|
|
275
|
+
});
|
|
276
|
+
const dustAmount = "dustAmount" in this.wallet
|
|
277
|
+
? this.wallet.dustAmount
|
|
278
|
+
: 1000n;
|
|
279
|
+
const { vtxosToRecover, includesSubdust, totalAmount } = getRecoverableWithSubdust(allVtxos, dustAmount);
|
|
280
|
+
// Calculate subdust amount separately for reporting
|
|
281
|
+
const subdustAmount = vtxosToRecover
|
|
282
|
+
.filter((v) => BigInt(v.value) < dustAmount)
|
|
283
|
+
.reduce((sum, v) => sum + BigInt(v.value), 0n);
|
|
284
|
+
return {
|
|
285
|
+
recoverable: totalAmount,
|
|
286
|
+
subdust: subdustAmount,
|
|
287
|
+
includesSubdust,
|
|
288
|
+
vtxoCount: vtxosToRecover.length,
|
|
289
|
+
};
|
|
290
|
+
}
|
|
291
|
+
// ========== Renewal Methods ==========
|
|
292
|
+
/**
|
|
293
|
+
* Get VTXOs that are expiring soon based on renewal configuration
|
|
294
|
+
*
|
|
295
|
+
* @param thresholdPercentage - Optional override for threshold percentage (0-100)
|
|
296
|
+
* @returns Array of expiring VTXOs, empty array if renewal is disabled or no VTXOs expiring
|
|
297
|
+
*
|
|
298
|
+
* @example
|
|
299
|
+
* ```typescript
|
|
300
|
+
* const manager = new VtxoManager(wallet, { enabled: true, thresholdPercentage: 10 });
|
|
301
|
+
* const expiringVtxos = await manager.getExpiringVtxos();
|
|
302
|
+
* if (expiringVtxos.length > 0) {
|
|
303
|
+
* console.log(`${expiringVtxos.length} VTXOs expiring soon`);
|
|
304
|
+
* }
|
|
305
|
+
* ```
|
|
306
|
+
*/
|
|
307
|
+
async getExpiringVtxos(thresholdPercentage) {
|
|
308
|
+
if (!this.renewalConfig?.enabled) {
|
|
309
|
+
return [];
|
|
310
|
+
}
|
|
311
|
+
const vtxos = await this.wallet.getVtxos();
|
|
312
|
+
const percentage = thresholdPercentage ??
|
|
313
|
+
this.renewalConfig.thresholdPercentage ??
|
|
314
|
+
DEFAULT_RENEWAL_CONFIG.thresholdPercentage;
|
|
315
|
+
const threshold = calculateDynamicThreshold(vtxos, percentage);
|
|
316
|
+
if (!threshold) {
|
|
317
|
+
return [];
|
|
318
|
+
}
|
|
319
|
+
return getExpiringVtxos(vtxos, threshold);
|
|
320
|
+
}
|
|
321
|
+
/**
|
|
322
|
+
* Renew VTXOs by settling them back to the wallet's address
|
|
323
|
+
*
|
|
324
|
+
* This method collects all spendable VTXOs (including recoverable ones) and settles
|
|
325
|
+
* them back to the wallet, effectively refreshing their expiration time. This is the
|
|
326
|
+
* primary way to prevent VTXOs from expiring.
|
|
327
|
+
*
|
|
328
|
+
* @param eventCallback - Optional callback for settlement events
|
|
329
|
+
* @returns Settlement transaction ID
|
|
330
|
+
* @throws Error if no VTXOs available to renew
|
|
331
|
+
* @throws Error if total amount is below dust threshold
|
|
332
|
+
*
|
|
333
|
+
* @example
|
|
334
|
+
* ```typescript
|
|
335
|
+
* const manager = new VtxoManager(wallet);
|
|
336
|
+
*
|
|
337
|
+
* // Simple renewal
|
|
338
|
+
* const txid = await manager.renewVtxos();
|
|
339
|
+
*
|
|
340
|
+
* // With event callback
|
|
341
|
+
* const txid = await manager.renewVtxos((event) => {
|
|
342
|
+
* console.log('Settlement event:', event.type);
|
|
343
|
+
* });
|
|
344
|
+
* ```
|
|
345
|
+
*/
|
|
346
|
+
async renewVtxos(eventCallback) {
|
|
347
|
+
// Get all VTXOs (including recoverable ones)
|
|
348
|
+
const vtxos = await this.wallet.getVtxos({ withRecoverable: true });
|
|
349
|
+
if (vtxos.length === 0) {
|
|
350
|
+
throw new Error("No VTXOs available to renew");
|
|
351
|
+
}
|
|
352
|
+
const totalAmount = vtxos.reduce((sum, vtxo) => sum + vtxo.value, 0);
|
|
353
|
+
// Get dust amount from wallet
|
|
354
|
+
const dustAmount = "dustAmount" in this.wallet
|
|
355
|
+
? this.wallet.dustAmount
|
|
356
|
+
: 1000n;
|
|
357
|
+
// Check if total amount is above dust threshold
|
|
358
|
+
if (BigInt(totalAmount) < dustAmount) {
|
|
359
|
+
throw new Error(`Total amount ${totalAmount} is below dust threshold ${dustAmount}`);
|
|
360
|
+
}
|
|
361
|
+
const arkAddress = await this.wallet.getAddress();
|
|
362
|
+
return this.wallet.settle({
|
|
363
|
+
inputs: vtxos,
|
|
364
|
+
outputs: [
|
|
365
|
+
{
|
|
366
|
+
address: arkAddress,
|
|
367
|
+
amount: BigInt(totalAmount),
|
|
368
|
+
},
|
|
369
|
+
],
|
|
370
|
+
}, eventCallback);
|
|
371
|
+
}
|
|
372
|
+
}
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { base64, hex } from "@scure/base";
|
|
2
2
|
import * as bip68 from "bip68";
|
|
3
|
-
import {
|
|
4
|
-
import { SigHash, Transaction } from "@scure/btc-signer
|
|
5
|
-
import {
|
|
3
|
+
import { tapLeafHash } from "@scure/btc-signer/payment.js";
|
|
4
|
+
import { SigHash, Transaction, Address, OutScript, } from "@scure/btc-signer";
|
|
5
|
+
import { sha256 } from "@scure/btc-signer/utils.js";
|
|
6
6
|
import { vtxosToTxs } from '../utils/transactionHistory.js';
|
|
7
7
|
import { ArkAddress } from '../script/address.js';
|
|
8
8
|
import { DefaultVtxo } from '../script/default.js';
|
|
@@ -12,14 +12,15 @@ import { SettlementEventType, RestArkProvider, } from '../providers/ark.js';
|
|
|
12
12
|
import { buildForfeitTx } from '../forfeit.js';
|
|
13
13
|
import { validateConnectorsTxGraph, validateVtxoTxGraph, } from '../tree/validation.js';
|
|
14
14
|
import { isRecoverable, isSpendable, isSubdust, TxType, } from './index.js';
|
|
15
|
-
import { sha256, sha256x2 } from "@scure/btc-signer/utils.js";
|
|
16
15
|
import { VtxoScript } from '../script/base.js';
|
|
17
16
|
import { CSVMultisigTapscript } from '../script/tapscript.js';
|
|
18
17
|
import { buildOffchainTx, hasBoardingTxExpired } from '../utils/arkTransaction.js';
|
|
18
|
+
import { DEFAULT_RENEWAL_CONFIG } from './vtxo-manager.js';
|
|
19
19
|
import { ArkNote } from '../arknote/index.js';
|
|
20
|
-
import {
|
|
20
|
+
import { Intent } from '../intent/index.js';
|
|
21
21
|
import { RestIndexerProvider } from '../providers/indexer.js';
|
|
22
22
|
import { TxTree } from '../tree/txTree.js';
|
|
23
|
+
import { ConditionWitness, VtxoTaprootTree } from '../utils/unknownFields.js';
|
|
23
24
|
import { InMemoryStorageAdapter } from '../storage/inMemory.js';
|
|
24
25
|
import { WalletRepositoryImpl, } from '../repositories/walletRepository.js';
|
|
25
26
|
import { ContractRepositoryImpl, } from '../repositories/contractRepository.js';
|
|
@@ -58,7 +59,7 @@ import { extendVirtualCoin } from './utils.js';
|
|
|
58
59
|
* ```
|
|
59
60
|
*/
|
|
60
61
|
export class Wallet {
|
|
61
|
-
constructor(identity, network, networkName, onchainProvider, arkProvider, indexerProvider, arkServerPublicKey, offchainTapscript, boardingTapscript, serverUnrollScript, forfeitOutputScript, dustAmount, walletRepository, contractRepository) {
|
|
62
|
+
constructor(identity, network, networkName, onchainProvider, arkProvider, indexerProvider, arkServerPublicKey, offchainTapscript, boardingTapscript, serverUnrollScript, forfeitOutputScript, forfeitPubkey, dustAmount, walletRepository, contractRepository, renewalConfig) {
|
|
62
63
|
this.identity = identity;
|
|
63
64
|
this.network = network;
|
|
64
65
|
this.networkName = networkName;
|
|
@@ -70,9 +71,15 @@ export class Wallet {
|
|
|
70
71
|
this.boardingTapscript = boardingTapscript;
|
|
71
72
|
this.serverUnrollScript = serverUnrollScript;
|
|
72
73
|
this.forfeitOutputScript = forfeitOutputScript;
|
|
74
|
+
this.forfeitPubkey = forfeitPubkey;
|
|
73
75
|
this.dustAmount = dustAmount;
|
|
74
76
|
this.walletRepository = walletRepository;
|
|
75
77
|
this.contractRepository = contractRepository;
|
|
78
|
+
this.renewalConfig = {
|
|
79
|
+
enabled: renewalConfig?.enabled ?? false,
|
|
80
|
+
...DEFAULT_RENEWAL_CONFIG,
|
|
81
|
+
...renewalConfig,
|
|
82
|
+
};
|
|
76
83
|
}
|
|
77
84
|
static async create(config) {
|
|
78
85
|
const pubkey = await config.identity.xOnlyPublicKey();
|
|
@@ -102,6 +109,7 @@ export class Wallet {
|
|
|
102
109
|
const esploraUrl = config.esploraUrl || ESPLORA_URL[info.network];
|
|
103
110
|
// Use provided onchainProvider instance or create a new one
|
|
104
111
|
const onchainProvider = config.onchainProvider || new EsploraProvider(esploraUrl);
|
|
112
|
+
// Generate timelocks
|
|
105
113
|
const exitTimelock = {
|
|
106
114
|
value: info.unilateralExitDelay,
|
|
107
115
|
type: info.unilateralExitDelay < 512n ? "blocks" : "seconds",
|
|
@@ -127,21 +135,22 @@ export class Wallet {
|
|
|
127
135
|
// the serverUnrollScript is the one used to create output scripts of the checkpoint transactions
|
|
128
136
|
let serverUnrollScript;
|
|
129
137
|
try {
|
|
130
|
-
const raw = hex.decode(info.
|
|
138
|
+
const raw = hex.decode(info.checkpointTapscript);
|
|
131
139
|
serverUnrollScript = CSVMultisigTapscript.decode(raw);
|
|
132
140
|
}
|
|
133
141
|
catch (e) {
|
|
134
|
-
throw new Error("Invalid
|
|
142
|
+
throw new Error("Invalid checkpointTapscript from server");
|
|
135
143
|
}
|
|
136
144
|
// parse the server forfeit address
|
|
137
145
|
// server is expecting funds to be sent to this address
|
|
146
|
+
const forfeitPubkey = hex.decode(info.forfeitPubkey).slice(1);
|
|
138
147
|
const forfeitAddress = Address(network).decode(info.forfeitAddress);
|
|
139
148
|
const forfeitOutputScript = OutScript.encode(forfeitAddress);
|
|
140
149
|
// Set up storage and repositories
|
|
141
150
|
const storage = config.storage || new InMemoryStorageAdapter();
|
|
142
151
|
const walletRepository = new WalletRepositoryImpl(storage);
|
|
143
152
|
const contractRepository = new ContractRepositoryImpl(storage);
|
|
144
|
-
return new Wallet(config.identity, network, info.network, onchainProvider, arkProvider, indexerProvider, serverPubKey, offchainTapscript, boardingTapscript, serverUnrollScript, forfeitOutputScript, info.dust, walletRepository, contractRepository);
|
|
153
|
+
return new Wallet(config.identity, network, info.network, onchainProvider, arkProvider, indexerProvider, serverPubKey, offchainTapscript, boardingTapscript, serverUnrollScript, forfeitOutputScript, forfeitPubkey, info.dust, walletRepository, contractRepository, config.renewalConfig);
|
|
145
154
|
}
|
|
146
155
|
get arkAddress() {
|
|
147
156
|
return this.offchainTapscript.address(this.network.hrp, this.arkServerPublicKey);
|
|
@@ -453,7 +462,7 @@ export class Wallet {
|
|
|
453
462
|
const signingPublicKeys = [];
|
|
454
463
|
if (hasOffchainOutputs) {
|
|
455
464
|
session = this.identity.signerSession();
|
|
456
|
-
signingPublicKeys.push(hex.encode(session.getPublicKey()));
|
|
465
|
+
signingPublicKeys.push(hex.encode(await session.getPublicKey()));
|
|
457
466
|
}
|
|
458
467
|
const [intent, deleteIntent] = await Promise.all([
|
|
459
468
|
this.makeRegisterIntentSignature(params.inputs, outputs, onchainOutputIndexes, signingPublicKeys),
|
|
@@ -492,7 +501,7 @@ export class Wallet {
|
|
|
492
501
|
if (step !== undefined) {
|
|
493
502
|
continue;
|
|
494
503
|
}
|
|
495
|
-
const res = await this.handleBatchStartedEvent(event, intentId, this.
|
|
504
|
+
const res = await this.handleBatchStartedEvent(event, intentId, this.forfeitPubkey, this.forfeitOutputScript);
|
|
496
505
|
if (!res.skip) {
|
|
497
506
|
step = event.type;
|
|
498
507
|
sweepTapTreeRoot = res.sweepTapTreeRoot;
|
|
@@ -677,10 +686,10 @@ export class Wallet {
|
|
|
677
686
|
};
|
|
678
687
|
return stopFunc;
|
|
679
688
|
}
|
|
680
|
-
async handleBatchStartedEvent(event, intentId,
|
|
689
|
+
async handleBatchStartedEvent(event, intentId, forfeitPubKey, forfeitOutputScript) {
|
|
681
690
|
const utf8IntentId = new TextEncoder().encode(intentId);
|
|
682
691
|
const intentIdHash = sha256(utf8IntentId);
|
|
683
|
-
const intentIdHashStr = hex.encode(
|
|
692
|
+
const intentIdHashStr = hex.encode(intentIdHash);
|
|
684
693
|
let skip = true;
|
|
685
694
|
// check if our intent ID hash matches any in the event
|
|
686
695
|
for (const idHash of event.intentIdHashes) {
|
|
@@ -700,7 +709,7 @@ export class Wallet {
|
|
|
700
709
|
value: event.batchExpiry,
|
|
701
710
|
type: event.batchExpiry >= 512n ? "seconds" : "blocks",
|
|
702
711
|
},
|
|
703
|
-
pubkeys: [
|
|
712
|
+
pubkeys: [forfeitPubKey],
|
|
704
713
|
}).script;
|
|
705
714
|
const sweepTapTreeRoot = tapLeafHash(sweepTapscript);
|
|
706
715
|
return {
|
|
@@ -721,12 +730,15 @@ export class Wallet {
|
|
|
721
730
|
throw new Error("Shared output not found");
|
|
722
731
|
}
|
|
723
732
|
session.init(vtxoGraph, sweepTapTreeRoot, sharedOutput.amount);
|
|
724
|
-
|
|
733
|
+
const pubkey = hex.encode(await session.getPublicKey());
|
|
734
|
+
const nonces = await session.getNonces();
|
|
735
|
+
await this.arkProvider.submitTreeNonces(event.id, pubkey, nonces);
|
|
725
736
|
}
|
|
726
737
|
async handleSettlementSigningNoncesGeneratedEvent(event, session) {
|
|
727
738
|
session.setAggregatedNonces(event.treeNonces);
|
|
728
|
-
const signatures = session.sign();
|
|
729
|
-
|
|
739
|
+
const signatures = await session.sign();
|
|
740
|
+
const pubkey = hex.encode(await session.getPublicKey());
|
|
741
|
+
await this.arkProvider.submitTreeSignatures(event.id, pubkey, signatures);
|
|
730
742
|
}
|
|
731
743
|
async handleSettlementFinalizationEvent(event, inputs, forfeitOutputScript, connectorsGraph) {
|
|
732
744
|
// the signed forfeits transactions to submit
|
|
@@ -774,7 +786,7 @@ export class Wallet {
|
|
|
774
786
|
throw new Error("not enough connectors received");
|
|
775
787
|
}
|
|
776
788
|
const connectorLeaf = connectorsLeaves[connectorIndex];
|
|
777
|
-
const connectorTxId =
|
|
789
|
+
const connectorTxId = connectorLeaf.id;
|
|
778
790
|
const connectorOutput = connectorLeaf.getOutput(0);
|
|
779
791
|
if (!connectorOutput) {
|
|
780
792
|
throw new Error("connector output not found");
|
|
@@ -815,111 +827,68 @@ export class Wallet {
|
|
|
815
827
|
: undefined);
|
|
816
828
|
}
|
|
817
829
|
}
|
|
818
|
-
async makeRegisterIntentSignature(
|
|
830
|
+
async makeRegisterIntentSignature(coins, outputs, onchainOutputsIndexes, cosignerPubKeys) {
|
|
819
831
|
const nowSeconds = Math.floor(Date.now() / 1000);
|
|
820
|
-
const
|
|
832
|
+
const inputs = this.prepareIntentProofInputs(coins);
|
|
821
833
|
const message = {
|
|
822
834
|
type: "register",
|
|
823
|
-
input_tap_trees: inputTapTrees,
|
|
824
835
|
onchain_output_indexes: onchainOutputsIndexes,
|
|
825
836
|
valid_at: nowSeconds,
|
|
826
837
|
expire_at: nowSeconds + 2 * 60, // valid for 2 minutes
|
|
827
838
|
cosigners_public_keys: cosignerPubKeys,
|
|
828
839
|
};
|
|
829
840
|
const encodedMessage = JSON.stringify(message, null, 0);
|
|
830
|
-
const
|
|
841
|
+
const proof = Intent.create(encodedMessage, inputs, outputs);
|
|
842
|
+
const signedProof = await this.identity.sign(proof);
|
|
831
843
|
return {
|
|
832
|
-
|
|
844
|
+
proof: base64.encode(signedProof.toPSBT()),
|
|
833
845
|
message: encodedMessage,
|
|
834
846
|
};
|
|
835
847
|
}
|
|
836
|
-
async makeDeleteIntentSignature(
|
|
848
|
+
async makeDeleteIntentSignature(coins) {
|
|
837
849
|
const nowSeconds = Math.floor(Date.now() / 1000);
|
|
838
|
-
const
|
|
850
|
+
const inputs = this.prepareIntentProofInputs(coins);
|
|
839
851
|
const message = {
|
|
840
852
|
type: "delete",
|
|
841
853
|
expire_at: nowSeconds + 2 * 60, // valid for 2 minutes
|
|
842
854
|
};
|
|
843
855
|
const encodedMessage = JSON.stringify(message, null, 0);
|
|
844
|
-
const
|
|
856
|
+
const proof = Intent.create(encodedMessage, inputs, []);
|
|
857
|
+
const signedProof = await this.identity.sign(proof);
|
|
845
858
|
return {
|
|
846
|
-
|
|
859
|
+
proof: base64.encode(signedProof.toPSBT()),
|
|
847
860
|
message: encodedMessage,
|
|
848
861
|
};
|
|
849
862
|
}
|
|
850
|
-
|
|
863
|
+
prepareIntentProofInputs(coins) {
|
|
851
864
|
const inputs = [];
|
|
852
|
-
const
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
const
|
|
856
|
-
|
|
865
|
+
for (const input of coins) {
|
|
866
|
+
const vtxoScript = VtxoScript.decode(input.tapTree);
|
|
867
|
+
const sequence = getSequence(input);
|
|
868
|
+
const unknown = [VtxoTaprootTree.encode(input.tapTree)];
|
|
869
|
+
if (input.extraWitness) {
|
|
870
|
+
unknown.push(ConditionWitness.encode(input.extraWitness));
|
|
871
|
+
}
|
|
857
872
|
inputs.push({
|
|
858
|
-
txid: hex.decode(
|
|
859
|
-
index:
|
|
873
|
+
txid: hex.decode(input.txid),
|
|
874
|
+
index: input.vout,
|
|
860
875
|
witnessUtxo: {
|
|
861
|
-
amount: BigInt(
|
|
876
|
+
amount: BigInt(input.value),
|
|
862
877
|
script: vtxoScript.pkScript,
|
|
863
878
|
},
|
|
864
879
|
sequence,
|
|
865
|
-
tapLeafScript: [
|
|
880
|
+
tapLeafScript: [input.intentTapLeafScript],
|
|
881
|
+
unknown,
|
|
866
882
|
});
|
|
867
|
-
inputTapTrees.push(hex.encode(bip322Input.tapTree));
|
|
868
|
-
inputExtraWitnesses.push(bip322Input.extraWitness || []);
|
|
869
883
|
}
|
|
870
|
-
return
|
|
871
|
-
inputs,
|
|
872
|
-
inputTapTrees,
|
|
873
|
-
finalizer: finalizeWithExtraWitnesses(inputExtraWitnesses),
|
|
874
|
-
};
|
|
875
|
-
}
|
|
876
|
-
async makeBIP322Signature(message, inputs, finalizer, outputs) {
|
|
877
|
-
const proof = BIP322.create(message, inputs, outputs);
|
|
878
|
-
const signedProof = await this.identity.sign(proof);
|
|
879
|
-
return BIP322.signature(signedProof, finalizer);
|
|
884
|
+
return inputs;
|
|
880
885
|
}
|
|
881
886
|
}
|
|
882
887
|
Wallet.MIN_FEE_RATE = 1; // sats/vbyte
|
|
883
|
-
function
|
|
884
|
-
return function (tx) {
|
|
885
|
-
for (let i = 0; i < tx.inputsLength; i++) {
|
|
886
|
-
try {
|
|
887
|
-
tx.finalizeIdx(i);
|
|
888
|
-
}
|
|
889
|
-
catch (e) {
|
|
890
|
-
// handle empty witness error
|
|
891
|
-
if (e instanceof Error &&
|
|
892
|
-
e.message.includes("finalize/taproot: empty witness")) {
|
|
893
|
-
const tapLeaves = tx.getInput(i).tapLeafScript;
|
|
894
|
-
if (!tapLeaves || tapLeaves.length <= 0)
|
|
895
|
-
throw e;
|
|
896
|
-
const [cb, s] = tapLeaves[0];
|
|
897
|
-
const script = s.slice(0, -1);
|
|
898
|
-
tx.updateInput(i, {
|
|
899
|
-
finalScriptWitness: [
|
|
900
|
-
script,
|
|
901
|
-
TaprootControlBlock.encode(cb),
|
|
902
|
-
],
|
|
903
|
-
});
|
|
904
|
-
}
|
|
905
|
-
}
|
|
906
|
-
const finalScriptWitness = tx.getInput(i).finalScriptWitness;
|
|
907
|
-
if (!finalScriptWitness)
|
|
908
|
-
throw new Error("input not finalized");
|
|
909
|
-
// input 0 and 1 spend the same pkscript
|
|
910
|
-
const extra = inputExtraWitnesses[i === 0 ? 0 : i - 1];
|
|
911
|
-
if (extra && extra.length > 0) {
|
|
912
|
-
tx.updateInput(i, {
|
|
913
|
-
finalScriptWitness: [...extra, ...finalScriptWitness],
|
|
914
|
-
});
|
|
915
|
-
}
|
|
916
|
-
}
|
|
917
|
-
};
|
|
918
|
-
}
|
|
919
|
-
function getSequence(bip322Input) {
|
|
888
|
+
function getSequence(coin) {
|
|
920
889
|
let sequence = undefined;
|
|
921
890
|
try {
|
|
922
|
-
const scriptWithLeafVersion =
|
|
891
|
+
const scriptWithLeafVersion = coin.intentTapLeafScript[1];
|
|
923
892
|
const script = scriptWithLeafVersion.subarray(0, scriptWithLeafVersion.length - 1);
|
|
924
893
|
const params = CSVMultisigTapscript.decode(script).params;
|
|
925
894
|
sequence = bip68.encode(params.timelock.type === "blocks"
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { TapLeafScript, VtxoScript } from "../script/base";
|
|
2
1
|
import { Bytes } from "@scure/btc-signer/utils.js";
|
|
2
|
+
import { TapLeafScript, VtxoScript } from "../script/base";
|
|
3
3
|
import { ExtendedCoin, Status } from "../wallet";
|
|
4
4
|
/**
|
|
5
5
|
* ArkNotes are special virtual coins in the Ark protocol that can be created
|
package/dist/types/forfeit.d.ts
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
import { Transaction } from "@scure/btc-signer
|
|
2
|
-
import { TransactionInputUpdate } from "@scure/btc-signer/psbt";
|
|
1
|
+
import { Transaction } from "@scure/btc-signer";
|
|
2
|
+
import { TransactionInputUpdate } from "@scure/btc-signer/psbt.js";
|
|
3
3
|
export declare function buildForfeitTx(inputs: TransactionInputUpdate[], forfeitPkScript: Uint8Array, txLocktime?: number): Transaction;
|