@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.
Files changed (77) hide show
  1. package/README.md +64 -14
  2. package/dist/cjs/arknote/index.js +3 -3
  3. package/dist/cjs/forfeit.js +5 -2
  4. package/dist/cjs/identity/singleKey.js +5 -4
  5. package/dist/cjs/index.js +6 -3
  6. package/dist/cjs/{bip322 → intent}/index.js +37 -55
  7. package/dist/cjs/providers/ark.js +62 -23
  8. package/dist/cjs/providers/expoArk.js +15 -170
  9. package/dist/cjs/providers/expoIndexer.js +22 -111
  10. package/dist/cjs/providers/expoUtils.js +124 -0
  11. package/dist/cjs/script/base.js +1 -2
  12. package/dist/cjs/script/tapscript.js +20 -21
  13. package/dist/cjs/script/vhtlc.js +2 -2
  14. package/dist/cjs/tree/signingSession.js +7 -8
  15. package/dist/cjs/tree/txTree.js +3 -4
  16. package/dist/cjs/tree/validation.js +2 -3
  17. package/dist/cjs/utils/arkTransaction.js +104 -12
  18. package/dist/cjs/utils/unknownFields.js +5 -5
  19. package/dist/cjs/wallet/onchain.js +4 -5
  20. package/dist/cjs/wallet/serviceWorker/utils.js +2 -0
  21. package/dist/cjs/wallet/serviceWorker/wallet.js +4 -8
  22. package/dist/cjs/wallet/serviceWorker/worker.js +23 -18
  23. package/dist/cjs/wallet/unroll.js +6 -7
  24. package/dist/cjs/wallet/vtxo-manager.js +381 -0
  25. package/dist/cjs/wallet/wallet.js +63 -94
  26. package/dist/esm/arknote/index.js +2 -2
  27. package/dist/esm/forfeit.js +4 -1
  28. package/dist/esm/identity/singleKey.js +7 -6
  29. package/dist/esm/index.js +7 -6
  30. package/dist/esm/{bip322 → intent}/index.js +31 -48
  31. package/dist/esm/providers/ark.js +62 -23
  32. package/dist/esm/providers/expoArk.js +15 -137
  33. package/dist/esm/providers/expoIndexer.js +22 -78
  34. package/dist/esm/providers/expoUtils.js +87 -0
  35. package/dist/esm/script/base.js +1 -2
  36. package/dist/esm/script/tapscript.js +1 -2
  37. package/dist/esm/script/vhtlc.js +1 -1
  38. package/dist/esm/tree/signingSession.js +8 -9
  39. package/dist/esm/tree/txTree.js +3 -4
  40. package/dist/esm/tree/validation.js +2 -3
  41. package/dist/esm/utils/arkTransaction.js +95 -4
  42. package/dist/esm/utils/unknownFields.js +1 -1
  43. package/dist/esm/wallet/onchain.js +1 -2
  44. package/dist/esm/wallet/serviceWorker/utils.js +1 -0
  45. package/dist/esm/wallet/serviceWorker/wallet.js +5 -9
  46. package/dist/esm/wallet/serviceWorker/worker.js +23 -18
  47. package/dist/esm/wallet/unroll.js +2 -3
  48. package/dist/esm/wallet/vtxo-manager.js +372 -0
  49. package/dist/esm/wallet/wallet.js +56 -87
  50. package/dist/types/arknote/index.d.ts +1 -1
  51. package/dist/types/forfeit.d.ts +2 -2
  52. package/dist/types/identity/index.d.ts +1 -1
  53. package/dist/types/identity/singleKey.d.ts +1 -1
  54. package/dist/types/index.d.ts +6 -5
  55. package/dist/types/intent/index.d.ts +41 -0
  56. package/dist/types/providers/ark.d.ts +55 -21
  57. package/dist/types/providers/expoIndexer.d.ts +2 -10
  58. package/dist/types/providers/expoUtils.d.ts +18 -0
  59. package/dist/types/providers/indexer.d.ts +1 -9
  60. package/dist/types/script/base.d.ts +3 -2
  61. package/dist/types/tree/signingSession.d.ts +10 -10
  62. package/dist/types/utils/anchor.d.ts +2 -2
  63. package/dist/types/utils/arkTransaction.d.ts +13 -3
  64. package/dist/types/utils/unknownFields.d.ts +2 -2
  65. package/dist/types/wallet/index.d.ts +6 -4
  66. package/dist/types/wallet/onchain.d.ts +1 -1
  67. package/dist/types/wallet/serviceWorker/utils.d.ts +1 -0
  68. package/dist/types/wallet/serviceWorker/wallet.d.ts +2 -2
  69. package/dist/types/wallet/serviceWorker/worker.d.ts +3 -1
  70. package/dist/types/wallet/unroll.d.ts +1 -1
  71. package/dist/types/wallet/vtxo-manager.d.ts +207 -0
  72. package/dist/types/wallet/wallet.d.ts +7 -3
  73. package/package.json +1 -2
  74. package/dist/cjs/bip322/errors.js +0 -13
  75. package/dist/esm/bip322/errors.js +0 -9
  76. package/dist/types/bip322/errors.d.ts +0 -6
  77. 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 { Address, OutScript, tapLeafHash } from "@scure/btc-signer/payment.js";
4
- import { SigHash, Transaction } from "@scure/btc-signer/transaction.js";
5
- import { TaprootControlBlock, } from "@scure/btc-signer/psbt.js";
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 { BIP322 } from '../bip322/index.js';
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.checkpointExitClosure);
138
+ const raw = hex.decode(info.checkpointTapscript);
131
139
  serverUnrollScript = CSVMultisigTapscript.decode(raw);
132
140
  }
133
141
  catch (e) {
134
- throw new Error("Invalid checkpointExitClosure from server");
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.arkServerPublicKey, this.forfeitOutputScript);
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, serverPubKey, forfeitOutputScript) {
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(new Uint8Array(intentIdHash));
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: [serverPubKey],
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
- await this.arkProvider.submitTreeNonces(event.id, hex.encode(session.getPublicKey()), session.getNonces());
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
- await this.arkProvider.submitTreeSignatures(event.id, hex.encode(session.getPublicKey()), signatures);
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 = hex.encode(sha256x2(connectorLeaf.toBytes(true)).reverse());
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(bip322Inputs, outputs, onchainOutputsIndexes, cosignerPubKeys) {
830
+ async makeRegisterIntentSignature(coins, outputs, onchainOutputsIndexes, cosignerPubKeys) {
819
831
  const nowSeconds = Math.floor(Date.now() / 1000);
820
- const { inputs, inputTapTrees, finalizer } = this.prepareBIP322Inputs(bip322Inputs);
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 signature = await this.makeBIP322Signature(encodedMessage, inputs, finalizer, outputs);
841
+ const proof = Intent.create(encodedMessage, inputs, outputs);
842
+ const signedProof = await this.identity.sign(proof);
831
843
  return {
832
- signature,
844
+ proof: base64.encode(signedProof.toPSBT()),
833
845
  message: encodedMessage,
834
846
  };
835
847
  }
836
- async makeDeleteIntentSignature(bip322Inputs) {
848
+ async makeDeleteIntentSignature(coins) {
837
849
  const nowSeconds = Math.floor(Date.now() / 1000);
838
- const { inputs, finalizer } = this.prepareBIP322Inputs(bip322Inputs);
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 signature = await this.makeBIP322Signature(encodedMessage, inputs, finalizer);
856
+ const proof = Intent.create(encodedMessage, inputs, []);
857
+ const signedProof = await this.identity.sign(proof);
845
858
  return {
846
- signature,
859
+ proof: base64.encode(signedProof.toPSBT()),
847
860
  message: encodedMessage,
848
861
  };
849
862
  }
850
- prepareBIP322Inputs(bip322Inputs) {
863
+ prepareIntentProofInputs(coins) {
851
864
  const inputs = [];
852
- const inputTapTrees = [];
853
- const inputExtraWitnesses = [];
854
- for (const bip322Input of bip322Inputs) {
855
- const vtxoScript = VtxoScript.decode(bip322Input.tapTree);
856
- const sequence = getSequence(bip322Input);
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(bip322Input.txid),
859
- index: bip322Input.vout,
873
+ txid: hex.decode(input.txid),
874
+ index: input.vout,
860
875
  witnessUtxo: {
861
- amount: BigInt(bip322Input.value),
876
+ amount: BigInt(input.value),
862
877
  script: vtxoScript.pkScript,
863
878
  },
864
879
  sequence,
865
- tapLeafScript: [bip322Input.intentTapLeafScript],
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 finalizeWithExtraWitnesses(inputExtraWitnesses) {
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 = bip322Input.intentTapLeafScript[1];
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
@@ -1,3 +1,3 @@
1
- import { Transaction } from "@scure/btc-signer/transaction.js";
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;