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