@arkade-os/sdk 0.0.16

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 (103) hide show
  1. package/README.md +312 -0
  2. package/dist/cjs/arknote/index.js +86 -0
  3. package/dist/cjs/forfeit.js +38 -0
  4. package/dist/cjs/identity/inMemoryKey.js +40 -0
  5. package/dist/cjs/identity/index.js +2 -0
  6. package/dist/cjs/index.js +48 -0
  7. package/dist/cjs/musig2/index.js +10 -0
  8. package/dist/cjs/musig2/keys.js +57 -0
  9. package/dist/cjs/musig2/nonces.js +44 -0
  10. package/dist/cjs/musig2/sign.js +102 -0
  11. package/dist/cjs/networks.js +26 -0
  12. package/dist/cjs/package.json +3 -0
  13. package/dist/cjs/providers/ark.js +530 -0
  14. package/dist/cjs/providers/onchain.js +61 -0
  15. package/dist/cjs/script/address.js +45 -0
  16. package/dist/cjs/script/base.js +51 -0
  17. package/dist/cjs/script/default.js +40 -0
  18. package/dist/cjs/script/tapscript.js +528 -0
  19. package/dist/cjs/script/vhtlc.js +84 -0
  20. package/dist/cjs/tree/signingSession.js +238 -0
  21. package/dist/cjs/tree/validation.js +184 -0
  22. package/dist/cjs/tree/vtxoTree.js +197 -0
  23. package/dist/cjs/utils/bip21.js +114 -0
  24. package/dist/cjs/utils/coinselect.js +73 -0
  25. package/dist/cjs/utils/psbt.js +124 -0
  26. package/dist/cjs/utils/transactionHistory.js +148 -0
  27. package/dist/cjs/utils/txSizeEstimator.js +95 -0
  28. package/dist/cjs/wallet/index.js +8 -0
  29. package/dist/cjs/wallet/serviceWorker/db/vtxo/idb.js +153 -0
  30. package/dist/cjs/wallet/serviceWorker/db/vtxo/index.js +2 -0
  31. package/dist/cjs/wallet/serviceWorker/request.js +75 -0
  32. package/dist/cjs/wallet/serviceWorker/response.js +187 -0
  33. package/dist/cjs/wallet/serviceWorker/wallet.js +332 -0
  34. package/dist/cjs/wallet/serviceWorker/worker.js +452 -0
  35. package/dist/cjs/wallet/wallet.js +720 -0
  36. package/dist/esm/arknote/index.js +81 -0
  37. package/dist/esm/forfeit.js +35 -0
  38. package/dist/esm/identity/inMemoryKey.js +36 -0
  39. package/dist/esm/identity/index.js +1 -0
  40. package/dist/esm/index.js +39 -0
  41. package/dist/esm/musig2/index.js +3 -0
  42. package/dist/esm/musig2/keys.js +21 -0
  43. package/dist/esm/musig2/nonces.js +8 -0
  44. package/dist/esm/musig2/sign.js +63 -0
  45. package/dist/esm/networks.js +22 -0
  46. package/dist/esm/package.json +3 -0
  47. package/dist/esm/providers/ark.js +526 -0
  48. package/dist/esm/providers/onchain.js +57 -0
  49. package/dist/esm/script/address.js +41 -0
  50. package/dist/esm/script/base.js +46 -0
  51. package/dist/esm/script/default.js +37 -0
  52. package/dist/esm/script/tapscript.js +491 -0
  53. package/dist/esm/script/vhtlc.js +81 -0
  54. package/dist/esm/tree/signingSession.js +200 -0
  55. package/dist/esm/tree/validation.js +179 -0
  56. package/dist/esm/tree/vtxoTree.js +157 -0
  57. package/dist/esm/utils/bip21.js +110 -0
  58. package/dist/esm/utils/coinselect.js +69 -0
  59. package/dist/esm/utils/psbt.js +118 -0
  60. package/dist/esm/utils/transactionHistory.js +145 -0
  61. package/dist/esm/utils/txSizeEstimator.js +91 -0
  62. package/dist/esm/wallet/index.js +5 -0
  63. package/dist/esm/wallet/serviceWorker/db/vtxo/idb.js +149 -0
  64. package/dist/esm/wallet/serviceWorker/db/vtxo/index.js +1 -0
  65. package/dist/esm/wallet/serviceWorker/request.js +72 -0
  66. package/dist/esm/wallet/serviceWorker/response.js +184 -0
  67. package/dist/esm/wallet/serviceWorker/wallet.js +328 -0
  68. package/dist/esm/wallet/serviceWorker/worker.js +448 -0
  69. package/dist/esm/wallet/wallet.js +716 -0
  70. package/dist/types/arknote/index.d.ts +17 -0
  71. package/dist/types/forfeit.d.ts +15 -0
  72. package/dist/types/identity/inMemoryKey.d.ts +12 -0
  73. package/dist/types/identity/index.d.ts +7 -0
  74. package/dist/types/index.d.ts +22 -0
  75. package/dist/types/musig2/index.d.ts +4 -0
  76. package/dist/types/musig2/keys.d.ts +9 -0
  77. package/dist/types/musig2/nonces.d.ts +13 -0
  78. package/dist/types/musig2/sign.d.ts +27 -0
  79. package/dist/types/networks.d.ts +16 -0
  80. package/dist/types/providers/ark.d.ts +126 -0
  81. package/dist/types/providers/onchain.d.ts +36 -0
  82. package/dist/types/script/address.d.ts +10 -0
  83. package/dist/types/script/base.d.ts +26 -0
  84. package/dist/types/script/default.d.ts +19 -0
  85. package/dist/types/script/tapscript.d.ts +94 -0
  86. package/dist/types/script/vhtlc.d.ts +31 -0
  87. package/dist/types/tree/signingSession.d.ts +32 -0
  88. package/dist/types/tree/validation.d.ts +22 -0
  89. package/dist/types/tree/vtxoTree.d.ts +32 -0
  90. package/dist/types/utils/bip21.d.ts +21 -0
  91. package/dist/types/utils/coinselect.d.ts +21 -0
  92. package/dist/types/utils/psbt.d.ts +11 -0
  93. package/dist/types/utils/transactionHistory.d.ts +2 -0
  94. package/dist/types/utils/txSizeEstimator.d.ts +27 -0
  95. package/dist/types/wallet/index.d.ts +122 -0
  96. package/dist/types/wallet/serviceWorker/db/vtxo/idb.d.ts +18 -0
  97. package/dist/types/wallet/serviceWorker/db/vtxo/index.d.ts +12 -0
  98. package/dist/types/wallet/serviceWorker/request.d.ts +68 -0
  99. package/dist/types/wallet/serviceWorker/response.d.ts +107 -0
  100. package/dist/types/wallet/serviceWorker/wallet.d.ts +23 -0
  101. package/dist/types/wallet/serviceWorker/worker.d.ts +26 -0
  102. package/dist/types/wallet/wallet.d.ts +42 -0
  103. package/package.json +88 -0
@@ -0,0 +1,720 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.Wallet = void 0;
4
+ const base_1 = require("@scure/base");
5
+ const payment_1 = require("@scure/btc-signer/payment");
6
+ const psbt_1 = require("@scure/btc-signer/psbt");
7
+ const transactionHistory_1 = require("../utils/transactionHistory");
8
+ const bip21_1 = require("../utils/bip21");
9
+ const address_1 = require("../script/address");
10
+ const default_1 = require("../script/default");
11
+ const coinselect_1 = require("../utils/coinselect");
12
+ const networks_1 = require("../networks");
13
+ const onchain_1 = require("../providers/onchain");
14
+ const ark_1 = require("../providers/ark");
15
+ const forfeit_1 = require("../forfeit");
16
+ const txSizeEstimator_1 = require("../utils/txSizeEstimator");
17
+ const validation_1 = require("../tree/validation");
18
+ const _1 = require(".");
19
+ const base_2 = require("../script/base");
20
+ const tapscript_1 = require("../script/tapscript");
21
+ const psbt_2 = require("../utils/psbt");
22
+ const btc_signer_1 = require("@scure/btc-signer");
23
+ const arknote_1 = require("../arknote");
24
+ // Wallet does not store any data and rely on the Ark and onchain providers to fetch utxos and vtxos
25
+ class Wallet {
26
+ constructor(identity, network, onchainProvider, onchainP2TR, arkProvider, arkServerPublicKey, offchainTapscript, boardingTapscript) {
27
+ this.identity = identity;
28
+ this.network = network;
29
+ this.onchainProvider = onchainProvider;
30
+ this.onchainP2TR = onchainP2TR;
31
+ this.arkProvider = arkProvider;
32
+ this.arkServerPublicKey = arkServerPublicKey;
33
+ this.offchainTapscript = offchainTapscript;
34
+ this.boardingTapscript = boardingTapscript;
35
+ }
36
+ static async create(config) {
37
+ const network = (0, networks_1.getNetwork)(config.network);
38
+ const onchainProvider = new onchain_1.EsploraProvider(config.esploraUrl || onchain_1.ESPLORA_URL[config.network]);
39
+ // Derive onchain address
40
+ const pubkey = config.identity.xOnlyPublicKey();
41
+ if (!pubkey) {
42
+ throw new Error("Invalid configured public key");
43
+ }
44
+ let arkProvider;
45
+ if (config.arkServerUrl) {
46
+ arkProvider = new ark_1.RestArkProvider(config.arkServerUrl);
47
+ }
48
+ // Save onchain Taproot address key-path only
49
+ const onchainP2TR = (0, payment_1.p2tr)(pubkey, undefined, network);
50
+ if (arkProvider) {
51
+ const info = await arkProvider.getInfo();
52
+ if (info.network !== config.network) {
53
+ throw new Error(`The Ark Server URL expects ${info.network} but ${config.network} was configured`);
54
+ }
55
+ const exitTimelock = {
56
+ value: info.unilateralExitDelay,
57
+ type: info.unilateralExitDelay < 512n ? "blocks" : "seconds",
58
+ };
59
+ const boardingTimelock = {
60
+ value: info.unilateralExitDelay * 2n,
61
+ type: info.unilateralExitDelay * 2n < 512n ? "blocks" : "seconds",
62
+ };
63
+ // Generate tapscripts for offchain and boarding address
64
+ const serverPubKey = base_1.hex.decode(info.pubkey).slice(1);
65
+ const bareVtxoTapscript = new default_1.DefaultVtxo.Script({
66
+ pubKey: pubkey,
67
+ serverPubKey,
68
+ csvTimelock: exitTimelock,
69
+ });
70
+ const boardingTapscript = new default_1.DefaultVtxo.Script({
71
+ pubKey: pubkey,
72
+ serverPubKey,
73
+ csvTimelock: boardingTimelock,
74
+ });
75
+ // Save tapscripts
76
+ const offchainTapscript = bareVtxoTapscript;
77
+ return new Wallet(config.identity, network, onchainProvider, onchainP2TR, arkProvider, serverPubKey, offchainTapscript, boardingTapscript);
78
+ }
79
+ return new Wallet(config.identity, network, onchainProvider, onchainP2TR);
80
+ }
81
+ get onchainAddress() {
82
+ return this.onchainP2TR.address || "";
83
+ }
84
+ get boardingAddress() {
85
+ if (!this.boardingTapscript || !this.arkServerPublicKey) {
86
+ throw new Error("Boarding address not configured");
87
+ }
88
+ return this.boardingTapscript.address(this.network.hrp, this.arkServerPublicKey);
89
+ }
90
+ get boardingOnchainAddress() {
91
+ if (!this.boardingTapscript) {
92
+ throw new Error("Boarding address not configured");
93
+ }
94
+ return this.boardingTapscript.onchainAddress(this.network);
95
+ }
96
+ get offchainAddress() {
97
+ if (!this.offchainTapscript || !this.arkServerPublicKey) {
98
+ throw new Error("Offchain address not configured");
99
+ }
100
+ return this.offchainTapscript.address(this.network.hrp, this.arkServerPublicKey);
101
+ }
102
+ getAddress() {
103
+ const addressInfo = {
104
+ onchain: this.onchainAddress,
105
+ bip21: bip21_1.BIP21.create({
106
+ address: this.onchainAddress,
107
+ }),
108
+ };
109
+ // Only include Ark-related fields if Ark provider is configured and address is available
110
+ if (this.arkProvider &&
111
+ this.offchainTapscript &&
112
+ this.boardingTapscript &&
113
+ this.arkServerPublicKey) {
114
+ const offchainAddress = this.offchainAddress.encode();
115
+ addressInfo.offchain = offchainAddress;
116
+ addressInfo.bip21 = bip21_1.BIP21.create({
117
+ address: this.onchainP2TR.address,
118
+ ark: offchainAddress,
119
+ });
120
+ addressInfo.boarding = this.boardingOnchainAddress;
121
+ }
122
+ return Promise.resolve(addressInfo);
123
+ }
124
+ getAddressInfo() {
125
+ if (!this.arkProvider ||
126
+ !this.offchainTapscript ||
127
+ !this.boardingTapscript ||
128
+ !this.arkServerPublicKey) {
129
+ throw new Error("Ark provider not configured");
130
+ }
131
+ const offchainAddress = this.offchainAddress.encode();
132
+ const boardingAddress = this.boardingOnchainAddress;
133
+ return Promise.resolve({
134
+ offchain: {
135
+ address: offchainAddress,
136
+ scripts: {
137
+ exit: [this.offchainTapscript.exitScript],
138
+ forfeit: [this.offchainTapscript.forfeitScript],
139
+ },
140
+ },
141
+ boarding: {
142
+ address: boardingAddress,
143
+ scripts: {
144
+ exit: [this.boardingTapscript.exitScript],
145
+ forfeit: [this.boardingTapscript.forfeitScript],
146
+ },
147
+ },
148
+ });
149
+ }
150
+ async getBalance() {
151
+ // Get onchain coins
152
+ const coins = await this.getCoins();
153
+ const onchainConfirmed = coins
154
+ .filter((coin) => coin.status.confirmed)
155
+ .reduce((sum, coin) => sum + coin.value, 0);
156
+ const onchainUnconfirmed = coins
157
+ .filter((coin) => !coin.status.confirmed)
158
+ .reduce((sum, coin) => sum + coin.value, 0);
159
+ const onchainTotal = onchainConfirmed + onchainUnconfirmed;
160
+ // Get offchain coins if Ark provider is configured
161
+ let offchainSettled = 0;
162
+ let offchainPending = 0;
163
+ let offchainSwept = 0;
164
+ if (this.arkProvider) {
165
+ const vtxos = await this.getVirtualCoins();
166
+ offchainSettled = vtxos
167
+ .filter((coin) => coin.virtualStatus.state === "settled")
168
+ .reduce((sum, coin) => sum + coin.value, 0);
169
+ offchainPending = vtxos
170
+ .filter((coin) => coin.virtualStatus.state === "pending")
171
+ .reduce((sum, coin) => sum + coin.value, 0);
172
+ offchainSwept = vtxos
173
+ .filter((coin) => coin.virtualStatus.state === "swept")
174
+ .reduce((sum, coin) => sum + coin.value, 0);
175
+ }
176
+ const offchainTotal = offchainSettled + offchainPending;
177
+ return {
178
+ onchain: {
179
+ confirmed: onchainConfirmed,
180
+ unconfirmed: onchainUnconfirmed,
181
+ total: onchainTotal,
182
+ },
183
+ offchain: {
184
+ swept: offchainSwept,
185
+ settled: offchainSettled,
186
+ pending: offchainPending,
187
+ total: offchainTotal,
188
+ },
189
+ total: onchainTotal + offchainTotal,
190
+ };
191
+ }
192
+ async getCoins() {
193
+ // TODO: add caching logic to lower the number of requests to provider
194
+ const address = await this.getAddress();
195
+ return this.onchainProvider.getCoins(address.onchain);
196
+ }
197
+ async getVtxos() {
198
+ if (!this.arkProvider || !this.offchainTapscript) {
199
+ return [];
200
+ }
201
+ const address = await this.getAddress();
202
+ if (!address.offchain) {
203
+ return [];
204
+ }
205
+ const { spendableVtxos } = await this.arkProvider.getVirtualCoins(address.offchain);
206
+ const encodedOffchainTapscript = this.offchainTapscript.encode();
207
+ const forfeit = this.offchainTapscript.forfeit();
208
+ return spendableVtxos.map((vtxo) => ({
209
+ ...vtxo,
210
+ tapLeafScript: forfeit,
211
+ scripts: encodedOffchainTapscript,
212
+ }));
213
+ }
214
+ async getVirtualCoins() {
215
+ if (!this.arkProvider) {
216
+ return [];
217
+ }
218
+ const address = await this.getAddress();
219
+ if (!address.offchain) {
220
+ return [];
221
+ }
222
+ return this.arkProvider
223
+ .getVirtualCoins(address.offchain)
224
+ .then(({ spendableVtxos }) => spendableVtxos);
225
+ }
226
+ async getTransactionHistory() {
227
+ if (!this.arkProvider) {
228
+ return [];
229
+ }
230
+ const { spendableVtxos, spentVtxos } = await this.arkProvider.getVirtualCoins(this.offchainAddress.encode());
231
+ const { boardingTxs, roundsToIgnore } = await this.getBoardingTxs();
232
+ // convert VTXOs to offchain transactions
233
+ const offchainTxs = (0, transactionHistory_1.vtxosToTxs)(spendableVtxos, spentVtxos, roundsToIgnore);
234
+ const txs = [...boardingTxs, ...offchainTxs];
235
+ // sort transactions by creation time in descending order (newest first)
236
+ txs.sort(
237
+ // place createdAt = 0 (unconfirmed txs) first, then descending
238
+ (a, b) => {
239
+ if (a.createdAt === 0)
240
+ return -1;
241
+ if (b.createdAt === 0)
242
+ return 1;
243
+ return b.createdAt - a.createdAt;
244
+ });
245
+ return txs;
246
+ }
247
+ async getBoardingTxs() {
248
+ if (!this.boardingAddress) {
249
+ return { boardingTxs: [], roundsToIgnore: new Set() };
250
+ }
251
+ const boardingAddress = this.boardingOnchainAddress;
252
+ const txs = await this.onchainProvider.getTransactions(boardingAddress);
253
+ const utxos = [];
254
+ const roundsToIgnore = new Set();
255
+ for (const tx of txs) {
256
+ for (let i = 0; i < tx.vout.length; i++) {
257
+ const vout = tx.vout[i];
258
+ if (vout.scriptpubkey_address === boardingAddress) {
259
+ const spentStatuses = await this.onchainProvider.getTxOutspends(tx.txid);
260
+ const spentStatus = spentStatuses[i];
261
+ if (spentStatus?.spent) {
262
+ roundsToIgnore.add(spentStatus.txid);
263
+ }
264
+ utxos.push({
265
+ txid: tx.txid,
266
+ vout: i,
267
+ value: Number(vout.value),
268
+ status: {
269
+ confirmed: tx.status.confirmed,
270
+ block_time: tx.status.block_time,
271
+ },
272
+ virtualStatus: {
273
+ state: spentStatus?.spent ? "swept" : "pending",
274
+ batchTxID: spentStatus?.spent
275
+ ? spentStatus.txid
276
+ : undefined,
277
+ },
278
+ createdAt: tx.status.confirmed
279
+ ? new Date(tx.status.block_time * 1000)
280
+ : new Date(0),
281
+ });
282
+ }
283
+ }
284
+ }
285
+ const unconfirmedTxs = [];
286
+ const confirmedTxs = [];
287
+ for (const utxo of utxos) {
288
+ const tx = {
289
+ key: {
290
+ boardingTxid: utxo.txid,
291
+ roundTxid: "",
292
+ redeemTxid: "",
293
+ },
294
+ amount: utxo.value,
295
+ type: _1.TxType.TxReceived,
296
+ settled: utxo.virtualStatus.state === "swept",
297
+ createdAt: utxo.status.block_time
298
+ ? new Date(utxo.status.block_time * 1000).getTime()
299
+ : 0,
300
+ };
301
+ if (!utxo.status.block_time) {
302
+ unconfirmedTxs.push(tx);
303
+ }
304
+ else {
305
+ confirmedTxs.push(tx);
306
+ }
307
+ }
308
+ return {
309
+ boardingTxs: [...unconfirmedTxs, ...confirmedTxs],
310
+ roundsToIgnore,
311
+ };
312
+ }
313
+ async getBoardingUtxos() {
314
+ if (!this.boardingAddress || !this.boardingTapscript) {
315
+ throw new Error("Boarding address not configured");
316
+ }
317
+ const boardingUtxos = await this.onchainProvider.getCoins(this.boardingOnchainAddress);
318
+ const encodedBoardingTapscript = this.boardingTapscript.encode();
319
+ const forfeit = this.boardingTapscript.forfeit();
320
+ return boardingUtxos.map((utxo) => ({
321
+ ...utxo,
322
+ tapLeafScript: forfeit,
323
+ scripts: encodedBoardingTapscript,
324
+ }));
325
+ }
326
+ async sendBitcoin(params, zeroFee = true) {
327
+ if (params.amount <= 0) {
328
+ throw new Error("Amount must be positive");
329
+ }
330
+ if (params.amount < Wallet.DUST_AMOUNT) {
331
+ throw new Error("Amount is below dust limit");
332
+ }
333
+ // If Ark is configured and amount is suitable, send via offchain
334
+ if (this.arkProvider && this.isOffchainSuitable(params.address)) {
335
+ return this.sendOffchain(params, zeroFee);
336
+ }
337
+ // Otherwise, send via onchain
338
+ return this.sendOnchain(params);
339
+ }
340
+ isOffchainSuitable(address) {
341
+ try {
342
+ address_1.ArkAddress.decode(address);
343
+ return true;
344
+ }
345
+ catch (e) {
346
+ return false;
347
+ }
348
+ }
349
+ async sendOnchain(params) {
350
+ const coins = await this.getCoins();
351
+ const feeRate = params.feeRate || Wallet.FEE_RATE;
352
+ // Ensure fee is an integer by rounding up
353
+ const estimatedFee = Math.ceil(174 * feeRate);
354
+ const totalNeeded = params.amount + estimatedFee;
355
+ // Select coins
356
+ const selected = (0, coinselect_1.selectCoins)(coins, totalNeeded);
357
+ if (!selected.inputs) {
358
+ throw new Error("Insufficient funds");
359
+ }
360
+ // Create transaction
361
+ let tx = new btc_signer_1.Transaction();
362
+ // Add inputs
363
+ for (const input of selected.inputs) {
364
+ tx.addInput({
365
+ txid: input.txid,
366
+ index: input.vout,
367
+ witnessUtxo: {
368
+ script: this.onchainP2TR.script,
369
+ amount: BigInt(input.value),
370
+ },
371
+ tapInternalKey: this.onchainP2TR.tapInternalKey,
372
+ tapMerkleRoot: this.onchainP2TR.tapMerkleRoot,
373
+ });
374
+ }
375
+ // Add payment output
376
+ tx.addOutputAddress(params.address, BigInt(params.amount), this.network);
377
+ // Add change output if needed
378
+ if (selected.changeAmount > 0) {
379
+ tx.addOutputAddress(this.onchainAddress, BigInt(selected.changeAmount), this.network);
380
+ }
381
+ // Sign inputs and Finalize
382
+ tx = await this.identity.sign(tx);
383
+ tx.finalize();
384
+ // Broadcast
385
+ const txid = await this.onchainProvider.broadcastTransaction(tx.hex);
386
+ return txid;
387
+ }
388
+ async sendOffchain(params, zeroFee = true) {
389
+ if (!this.arkProvider ||
390
+ !this.offchainAddress ||
391
+ !this.offchainTapscript) {
392
+ throw new Error("wallet not initialized");
393
+ }
394
+ const virtualCoins = await this.getVirtualCoins();
395
+ const estimatedFee = zeroFee
396
+ ? 0
397
+ : Math.ceil(174 * (params.feeRate || Wallet.FEE_RATE));
398
+ const totalNeeded = params.amount + estimatedFee;
399
+ const selected = (0, coinselect_1.selectVirtualCoins)(virtualCoins, totalNeeded);
400
+ if (!selected || !selected.inputs) {
401
+ throw new Error("Insufficient funds");
402
+ }
403
+ const selectedLeaf = this.offchainTapscript.forfeit();
404
+ if (!selectedLeaf) {
405
+ throw new Error("Selected leaf not found");
406
+ }
407
+ const outputs = [
408
+ {
409
+ address: params.address,
410
+ amount: BigInt(params.amount),
411
+ },
412
+ ];
413
+ // add change output if needed
414
+ if (selected.changeAmount > 0) {
415
+ outputs.push({
416
+ address: this.offchainAddress.encode(),
417
+ amount: BigInt(selected.changeAmount),
418
+ });
419
+ }
420
+ const scripts = this.offchainTapscript.encode();
421
+ let tx = (0, psbt_2.createVirtualTx)(selected.inputs.map((input) => ({
422
+ ...input,
423
+ tapLeafScript: selectedLeaf,
424
+ scripts,
425
+ })), outputs);
426
+ tx = await this.identity.sign(tx);
427
+ const psbt = base_1.base64.encode(tx.toPSBT());
428
+ return this.arkProvider.submitVirtualTx(psbt);
429
+ }
430
+ async settle(params, eventCallback) {
431
+ if (!this.arkProvider) {
432
+ throw new Error("Ark provider not configured");
433
+ }
434
+ // validate arknotes inputs
435
+ if (params?.inputs) {
436
+ for (const input of params.inputs) {
437
+ if (typeof input === "string") {
438
+ try {
439
+ arknote_1.ArkNote.fromString(input);
440
+ }
441
+ catch (e) {
442
+ throw new Error(`Invalid arknote "${input}"`);
443
+ }
444
+ }
445
+ }
446
+ }
447
+ // if no params are provided, use all boarding and offchain utxos as inputs
448
+ // and send all to the offchain address
449
+ if (!params) {
450
+ if (!this.offchainAddress) {
451
+ throw new Error("Offchain address not configured");
452
+ }
453
+ let amount = 0;
454
+ const boardingUtxos = await this.getBoardingUtxos();
455
+ amount += boardingUtxos.reduce((sum, input) => sum + input.value, 0);
456
+ const vtxos = await this.getVtxos();
457
+ amount += vtxos.reduce((sum, input) => sum + input.value, 0);
458
+ const inputs = [...boardingUtxos, ...vtxos];
459
+ if (inputs.length === 0) {
460
+ throw new Error("No inputs found");
461
+ }
462
+ params = {
463
+ inputs,
464
+ outputs: [
465
+ {
466
+ address: this.offchainAddress.encode(),
467
+ amount: BigInt(amount),
468
+ },
469
+ ],
470
+ };
471
+ }
472
+ // register inputs
473
+ const { requestId } = await this.arkProvider.registerInputsForNextRound(params.inputs.map((input) => {
474
+ if (typeof input === "string") {
475
+ return input;
476
+ }
477
+ return {
478
+ outpoint: input,
479
+ tapscripts: input.scripts,
480
+ };
481
+ }));
482
+ const hasOffchainOutputs = params.outputs.some((output) => this.isOffchainSuitable(output.address));
483
+ // session holds the state of the musig2 signing process of the vtxo tree
484
+ let session;
485
+ const signingPublicKeys = [];
486
+ if (hasOffchainOutputs) {
487
+ session = this.identity.signerSession();
488
+ signingPublicKeys.push(base_1.hex.encode(session.getPublicKey()));
489
+ }
490
+ // register outputs
491
+ await this.arkProvider.registerOutputsForNextRound(requestId, params.outputs, signingPublicKeys);
492
+ // start pinging every seconds
493
+ const interval = setInterval(() => {
494
+ this.arkProvider?.ping(requestId).catch(stopPing);
495
+ }, 1000);
496
+ let pingRunning = true;
497
+ const stopPing = () => {
498
+ if (pingRunning) {
499
+ pingRunning = false;
500
+ clearInterval(interval);
501
+ }
502
+ };
503
+ const abortController = new AbortController();
504
+ // listen to settlement events
505
+ try {
506
+ const settlementStream = this.arkProvider.getEventStream(abortController.signal);
507
+ let step;
508
+ if (!hasOffchainOutputs) {
509
+ // if there are no offchain outputs, we don't have to handle musig2 tree signatures
510
+ // we can directly advance to the finalization step
511
+ step = ark_1.SettlementEventType.SigningNoncesGenerated;
512
+ }
513
+ const info = await this.arkProvider.getInfo();
514
+ const sweepTapscript = tapscript_1.CSVMultisigTapscript.encode({
515
+ timelock: {
516
+ value: info.batchExpiry,
517
+ type: info.batchExpiry >= 512n ? "seconds" : "blocks",
518
+ },
519
+ pubkeys: [base_1.hex.decode(info.pubkey).slice(1)],
520
+ }).script;
521
+ const sweepTapTreeRoot = (0, payment_1.tapLeafHash)(sweepTapscript);
522
+ for await (const event of settlementStream) {
523
+ if (eventCallback) {
524
+ eventCallback(event);
525
+ }
526
+ switch (event.type) {
527
+ // the settlement failed
528
+ case ark_1.SettlementEventType.Failed:
529
+ if (step === undefined) {
530
+ continue;
531
+ }
532
+ stopPing();
533
+ throw new Error(event.reason);
534
+ // the server has started the signing process of the vtxo tree transactions
535
+ // the server expects the partial musig2 nonces for each tx
536
+ case ark_1.SettlementEventType.SigningStart:
537
+ if (step !== undefined) {
538
+ continue;
539
+ }
540
+ stopPing();
541
+ if (hasOffchainOutputs) {
542
+ if (!session) {
543
+ throw new Error("Signing session not found");
544
+ }
545
+ await this.handleSettlementSigningEvent(event, sweepTapTreeRoot, session);
546
+ }
547
+ break;
548
+ // the musig2 nonces of the vtxo tree transactions are generated
549
+ // the server expects now the partial musig2 signatures
550
+ case ark_1.SettlementEventType.SigningNoncesGenerated:
551
+ if (step !== ark_1.SettlementEventType.SigningStart) {
552
+ continue;
553
+ }
554
+ stopPing();
555
+ if (hasOffchainOutputs) {
556
+ if (!session) {
557
+ throw new Error("Signing session not found");
558
+ }
559
+ await this.handleSettlementSigningNoncesGeneratedEvent(event, session);
560
+ }
561
+ break;
562
+ // the vtxo tree is signed, craft, sign and submit forfeit transactions
563
+ // if any boarding utxos are involved, the settlement tx is also signed
564
+ case ark_1.SettlementEventType.Finalization:
565
+ if (step !== ark_1.SettlementEventType.SigningNoncesGenerated) {
566
+ continue;
567
+ }
568
+ stopPing();
569
+ await this.handleSettlementFinalizationEvent(event, params.inputs, info);
570
+ break;
571
+ // the settlement is done, last event to be received
572
+ case ark_1.SettlementEventType.Finalized:
573
+ if (step !== ark_1.SettlementEventType.Finalization) {
574
+ continue;
575
+ }
576
+ abortController.abort();
577
+ return event.roundTxid;
578
+ }
579
+ step = event.type;
580
+ }
581
+ }
582
+ catch (error) {
583
+ abortController.abort();
584
+ throw error;
585
+ }
586
+ throw new Error("Settlement failed");
587
+ }
588
+ // validates the vtxo tree, creates a signing session and generates the musig2 nonces
589
+ async handleSettlementSigningEvent(event, sweepTapTreeRoot, session) {
590
+ const vtxoTree = event.unsignedVtxoTree;
591
+ if (!this.arkProvider) {
592
+ throw new Error("Ark provider not configured");
593
+ }
594
+ // validate the unsigned vtxo tree
595
+ (0, validation_1.validateVtxoTree)(event.unsignedSettlementTx, vtxoTree, sweepTapTreeRoot);
596
+ // TODO check if our registered outputs are in the vtxo tree
597
+ const settlementPsbt = base_1.base64.decode(event.unsignedSettlementTx);
598
+ const settlementTx = btc_signer_1.Transaction.fromPSBT(settlementPsbt);
599
+ const sharedOutput = settlementTx.getOutput(0);
600
+ if (!sharedOutput?.amount) {
601
+ throw new Error("Shared output not found");
602
+ }
603
+ session.init(vtxoTree, sweepTapTreeRoot, sharedOutput.amount);
604
+ await this.arkProvider.submitTreeNonces(event.id, base_1.hex.encode(session.getPublicKey()), session.getNonces());
605
+ }
606
+ async handleSettlementSigningNoncesGeneratedEvent(event, session) {
607
+ if (!this.arkProvider) {
608
+ throw new Error("Ark provider not configured");
609
+ }
610
+ session.setAggregatedNonces(event.treeNonces);
611
+ const signatures = session.sign();
612
+ await this.arkProvider.submitTreeSignatures(event.id, base_1.hex.encode(session.getPublicKey()), signatures);
613
+ }
614
+ async handleSettlementFinalizationEvent(event, inputs, infos) {
615
+ if (!this.arkProvider) {
616
+ throw new Error("Ark provider not configured");
617
+ }
618
+ // parse the server forfeit address
619
+ // server is expecting funds to be sent to this address
620
+ const forfeitAddress = (0, payment_1.Address)(this.network).decode(infos.forfeitAddress);
621
+ const serverPkScript = payment_1.OutScript.encode(forfeitAddress);
622
+ // the signed forfeits transactions to submit
623
+ const signedForfeits = [];
624
+ const vtxos = await this.getVirtualCoins();
625
+ let settlementPsbt = btc_signer_1.Transaction.fromPSBT(base_1.base64.decode(event.roundTx));
626
+ let hasBoardingUtxos = false;
627
+ let connectorsTreeValid = false;
628
+ for (const input of inputs) {
629
+ if (typeof input === "string")
630
+ continue; // skip notes
631
+ // check if the input is an offchain "virtual" coin
632
+ const vtxo = vtxos.find((vtxo) => vtxo.txid === input.txid && vtxo.vout === input.vout);
633
+ // boarding utxo, we need to sign the settlement tx
634
+ if (!vtxo) {
635
+ hasBoardingUtxos = true;
636
+ const inputIndexes = [];
637
+ for (let i = 0; i < settlementPsbt.inputsLength; i++) {
638
+ const settlementInput = settlementPsbt.getInput(i);
639
+ if (!settlementInput.txid ||
640
+ settlementInput.index === undefined) {
641
+ throw new Error("The server returned incomplete data. No settlement input found in the PSBT");
642
+ }
643
+ const inputTxId = base_1.hex.encode(settlementInput.txid);
644
+ if (inputTxId !== input.txid)
645
+ continue;
646
+ if (settlementInput.index !== input.vout)
647
+ continue;
648
+ // input found in the settlement tx, sign it
649
+ settlementPsbt.updateInput(i, {
650
+ tapLeafScript: [input.tapLeafScript],
651
+ });
652
+ inputIndexes.push(i);
653
+ }
654
+ settlementPsbt = await this.identity.sign(settlementPsbt, inputIndexes);
655
+ continue;
656
+ }
657
+ if (!connectorsTreeValid) {
658
+ // validate that the connectors tree is valid and contains our expected connectors
659
+ (0, validation_1.validateConnectorsTree)(event.roundTx, event.connectors);
660
+ connectorsTreeValid = true;
661
+ }
662
+ const forfeitControlBlock = psbt_1.TaprootControlBlock.encode(input.tapLeafScript[0]);
663
+ const tapscript = (0, tapscript_1.decodeTapscript)((0, base_2.scriptFromTapLeafScript)(input.tapLeafScript));
664
+ const fees = txSizeEstimator_1.TxWeightEstimator.create()
665
+ .addKeySpendInput() // connector
666
+ .addTapscriptInput(tapscript.witnessSize(100), // TODO: handle conditional script
667
+ input.tapLeafScript[1].length - 1, forfeitControlBlock.length)
668
+ .addP2WKHOutput()
669
+ .vsize()
670
+ .fee(event.minRelayFeeRate);
671
+ const connectorsLeaves = event.connectors.leaves();
672
+ const connectorOutpoint = event.connectorsIndex.get(`${vtxo.txid}:${vtxo.vout}`);
673
+ if (!connectorOutpoint) {
674
+ throw new Error("Connector outpoint not found");
675
+ }
676
+ let connectorOutput;
677
+ for (const leaf of connectorsLeaves) {
678
+ if (leaf.txid === connectorOutpoint.txid) {
679
+ try {
680
+ const connectorTx = btc_signer_1.Transaction.fromPSBT(base_1.base64.decode(leaf.tx));
681
+ connectorOutput = connectorTx.getOutput(connectorOutpoint.vout);
682
+ break;
683
+ }
684
+ catch {
685
+ throw new Error("Invalid connector tx");
686
+ }
687
+ }
688
+ }
689
+ if (!connectorOutput ||
690
+ !connectorOutput.amount ||
691
+ !connectorOutput.script) {
692
+ throw new Error("Connector output not found");
693
+ }
694
+ let forfeitTx = (0, forfeit_1.buildForfeitTx)({
695
+ connectorInput: connectorOutpoint,
696
+ connectorAmount: connectorOutput.amount,
697
+ feeAmount: fees,
698
+ serverPkScript,
699
+ connectorPkScript: connectorOutput.script,
700
+ vtxoAmount: BigInt(vtxo.value),
701
+ vtxoInput: input,
702
+ vtxoPkScript: base_2.VtxoScript.decode(input.scripts).pkScript,
703
+ });
704
+ // add the tapscript
705
+ forfeitTx.updateInput(1, {
706
+ tapLeafScript: [input.tapLeafScript],
707
+ });
708
+ // do not sign the connector input
709
+ forfeitTx = await this.identity.sign(forfeitTx, [1]);
710
+ signedForfeits.push(base_1.base64.encode(forfeitTx.toPSBT()));
711
+ }
712
+ await this.arkProvider.submitSignedForfeitTxs(signedForfeits, hasBoardingUtxos
713
+ ? base_1.base64.encode(settlementPsbt.toPSBT())
714
+ : undefined);
715
+ }
716
+ }
717
+ exports.Wallet = Wallet;
718
+ // TODO get dust from ark server?
719
+ Wallet.DUST_AMOUNT = BigInt(546); // Bitcoin dust limit in satoshis = 546
720
+ Wallet.FEE_RATE = 1; // sats/vbyte