@arkade-os/sdk 0.1.4 → 0.2.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 (114) hide show
  1. package/README.md +157 -174
  2. package/dist/cjs/arknote/index.js +61 -58
  3. package/dist/cjs/bip322/errors.js +13 -0
  4. package/dist/cjs/bip322/index.js +178 -0
  5. package/dist/cjs/forfeit.js +14 -25
  6. package/dist/cjs/identity/singleKey.js +68 -0
  7. package/dist/cjs/index.js +43 -17
  8. package/dist/cjs/providers/ark.js +261 -321
  9. package/dist/cjs/providers/indexer.js +525 -0
  10. package/dist/cjs/providers/onchain.js +193 -15
  11. package/dist/cjs/script/address.js +48 -17
  12. package/dist/cjs/script/base.js +120 -3
  13. package/dist/cjs/script/default.js +18 -4
  14. package/dist/cjs/script/tapscript.js +61 -20
  15. package/dist/cjs/script/vhtlc.js +85 -7
  16. package/dist/cjs/tree/signingSession.js +63 -106
  17. package/dist/cjs/tree/txTree.js +193 -0
  18. package/dist/cjs/tree/validation.js +79 -155
  19. package/dist/cjs/utils/anchor.js +35 -0
  20. package/dist/cjs/utils/arkTransaction.js +108 -0
  21. package/dist/cjs/utils/transactionHistory.js +84 -72
  22. package/dist/cjs/utils/txSizeEstimator.js +12 -0
  23. package/dist/cjs/utils/unknownFields.js +211 -0
  24. package/dist/cjs/wallet/index.js +12 -0
  25. package/dist/cjs/wallet/onchain.js +201 -0
  26. package/dist/cjs/wallet/ramps.js +95 -0
  27. package/dist/cjs/wallet/serviceWorker/db/vtxo/idb.js +32 -0
  28. package/dist/cjs/wallet/serviceWorker/request.js +15 -12
  29. package/dist/cjs/wallet/serviceWorker/response.js +22 -27
  30. package/dist/cjs/wallet/serviceWorker/utils.js +8 -0
  31. package/dist/cjs/wallet/serviceWorker/wallet.js +61 -34
  32. package/dist/cjs/wallet/serviceWorker/worker.js +120 -108
  33. package/dist/cjs/wallet/unroll.js +270 -0
  34. package/dist/cjs/wallet/wallet.js +701 -454
  35. package/dist/esm/arknote/index.js +61 -57
  36. package/dist/esm/bip322/errors.js +9 -0
  37. package/dist/esm/bip322/index.js +174 -0
  38. package/dist/esm/forfeit.js +15 -26
  39. package/dist/esm/identity/singleKey.js +64 -0
  40. package/dist/esm/index.js +31 -12
  41. package/dist/esm/providers/ark.js +259 -320
  42. package/dist/esm/providers/indexer.js +521 -0
  43. package/dist/esm/providers/onchain.js +193 -15
  44. package/dist/esm/script/address.js +48 -17
  45. package/dist/esm/script/base.js +120 -3
  46. package/dist/esm/script/default.js +18 -4
  47. package/dist/esm/script/tapscript.js +61 -20
  48. package/dist/esm/script/vhtlc.js +85 -7
  49. package/dist/esm/tree/signingSession.js +65 -108
  50. package/dist/esm/tree/txTree.js +189 -0
  51. package/dist/esm/tree/validation.js +75 -152
  52. package/dist/esm/utils/anchor.js +31 -0
  53. package/dist/esm/utils/arkTransaction.js +105 -0
  54. package/dist/esm/utils/transactionHistory.js +84 -72
  55. package/dist/esm/utils/txSizeEstimator.js +12 -0
  56. package/dist/esm/utils/unknownFields.js +173 -0
  57. package/dist/esm/wallet/index.js +9 -0
  58. package/dist/esm/wallet/onchain.js +196 -0
  59. package/dist/esm/wallet/ramps.js +91 -0
  60. package/dist/esm/wallet/serviceWorker/db/vtxo/idb.js +32 -0
  61. package/dist/esm/wallet/serviceWorker/request.js +15 -12
  62. package/dist/esm/wallet/serviceWorker/response.js +22 -27
  63. package/dist/esm/wallet/serviceWorker/utils.js +8 -0
  64. package/dist/esm/wallet/serviceWorker/wallet.js +62 -35
  65. package/dist/esm/wallet/serviceWorker/worker.js +120 -108
  66. package/dist/esm/wallet/unroll.js +267 -0
  67. package/dist/esm/wallet/wallet.js +674 -461
  68. package/dist/types/arknote/index.d.ts +40 -13
  69. package/dist/types/bip322/errors.d.ts +6 -0
  70. package/dist/types/bip322/index.d.ts +57 -0
  71. package/dist/types/forfeit.d.ts +2 -14
  72. package/dist/types/identity/singleKey.d.ts +27 -0
  73. package/dist/types/index.d.ts +24 -12
  74. package/dist/types/providers/ark.d.ts +114 -95
  75. package/dist/types/providers/indexer.d.ts +186 -0
  76. package/dist/types/providers/onchain.d.ts +41 -11
  77. package/dist/types/script/address.d.ts +26 -2
  78. package/dist/types/script/base.d.ts +13 -3
  79. package/dist/types/script/default.d.ts +22 -0
  80. package/dist/types/script/tapscript.d.ts +61 -5
  81. package/dist/types/script/vhtlc.d.ts +27 -0
  82. package/dist/types/tree/signingSession.d.ts +5 -5
  83. package/dist/types/tree/txTree.d.ts +28 -0
  84. package/dist/types/tree/validation.d.ts +15 -22
  85. package/dist/types/utils/anchor.d.ts +19 -0
  86. package/dist/types/utils/arkTransaction.d.ts +27 -0
  87. package/dist/types/utils/transactionHistory.d.ts +7 -1
  88. package/dist/types/utils/txSizeEstimator.d.ts +3 -0
  89. package/dist/types/utils/unknownFields.d.ts +83 -0
  90. package/dist/types/wallet/index.d.ts +51 -50
  91. package/dist/types/wallet/onchain.d.ts +49 -0
  92. package/dist/types/wallet/ramps.d.ts +32 -0
  93. package/dist/types/wallet/serviceWorker/db/vtxo/idb.d.ts +2 -0
  94. package/dist/types/wallet/serviceWorker/db/vtxo/index.d.ts +2 -0
  95. package/dist/types/wallet/serviceWorker/request.d.ts +14 -16
  96. package/dist/types/wallet/serviceWorker/response.d.ts +17 -19
  97. package/dist/types/wallet/serviceWorker/utils.d.ts +8 -0
  98. package/dist/types/wallet/serviceWorker/wallet.d.ts +36 -8
  99. package/dist/types/wallet/serviceWorker/worker.d.ts +7 -3
  100. package/dist/types/wallet/unroll.d.ts +102 -0
  101. package/dist/types/wallet/wallet.d.ts +71 -25
  102. package/package.json +37 -35
  103. package/dist/cjs/identity/inMemoryKey.js +0 -40
  104. package/dist/cjs/tree/vtxoTree.js +0 -231
  105. package/dist/cjs/utils/coinselect.js +0 -73
  106. package/dist/cjs/utils/psbt.js +0 -137
  107. package/dist/esm/identity/inMemoryKey.js +0 -36
  108. package/dist/esm/tree/vtxoTree.js +0 -191
  109. package/dist/esm/utils/coinselect.js +0 -69
  110. package/dist/esm/utils/psbt.js +0 -131
  111. package/dist/types/identity/inMemoryKey.d.ts +0 -12
  112. package/dist/types/tree/vtxoTree.d.ts +0 -33
  113. package/dist/types/utils/coinselect.d.ts +0 -21
  114. package/dist/types/utils/psbt.d.ts +0 -11
@@ -2,16 +2,21 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.Worker = void 0;
4
4
  /// <reference lib="webworker" />
5
- const inMemoryKey_1 = require("../../identity/inMemoryKey");
5
+ const singleKey_1 = require("../../identity/singleKey");
6
+ const __1 = require("..");
6
7
  const wallet_1 = require("../wallet");
7
8
  const request_1 = require("./request");
8
9
  const response_1 = require("./response");
9
10
  const ark_1 = require("../../providers/ark");
10
- const default_1 = require("../../script/default");
11
11
  const idb_1 = require("./db/vtxo/idb");
12
12
  const transactionHistory_1 = require("../../utils/transactionHistory");
13
- // Worker is a class letting to interact with ServiceWorkerWallet from the client
14
- // it aims to be run in a service worker context
13
+ const indexer_1 = require("../../providers/indexer");
14
+ const base_1 = require("@scure/base");
15
+ const btc_signer_1 = require("@scure/btc-signer");
16
+ /**
17
+ * Worker is a class letting to interact with ServiceWorkerWallet from the client
18
+ * it aims to be run in a service worker context
19
+ */
15
20
  class Worker {
16
21
  constructor(vtxoRepository = new idb_1.IndexedDBVtxoRepository(), messageCallback = () => { }) {
17
22
  this.vtxoRepository = vtxoRepository;
@@ -39,41 +44,48 @@ class Worker {
39
44
  await this.vtxoRepository.close();
40
45
  this.wallet = undefined;
41
46
  this.arkProvider = undefined;
47
+ this.indexerProvider = undefined;
42
48
  this.vtxoSubscription = undefined;
43
49
  }
44
50
  async onWalletInitialized() {
45
51
  if (!this.wallet ||
46
52
  !this.arkProvider ||
53
+ !this.indexerProvider ||
47
54
  !this.wallet.offchainTapscript ||
48
55
  !this.wallet.boardingTapscript) {
49
56
  return;
50
57
  }
51
58
  // subscribe to address updates
52
- const addressInfo = await this.wallet.getAddressInfo();
53
- if (!addressInfo.offchain) {
54
- return;
55
- }
56
59
  await this.vtxoRepository.open();
57
- // set the initial vtxos state
58
- const { spendableVtxos, spentVtxos } = await this.arkProvider.getVirtualCoins(addressInfo.offchain.address);
59
60
  const encodedOffchainTapscript = this.wallet.offchainTapscript.encode();
60
61
  const forfeit = this.wallet.offchainTapscript.forfeit();
61
- const vtxos = [...spendableVtxos, ...spentVtxos].map((vtxo) => ({
62
+ const exit = this.wallet.offchainTapscript.exit();
63
+ const script = base_1.hex.encode(this.wallet.offchainTapscript.pkScript);
64
+ // set the initial vtxos state
65
+ const response = await this.indexerProvider.getVtxos({
66
+ scripts: [script],
67
+ });
68
+ const vtxos = response.vtxos.map((vtxo) => ({
62
69
  ...vtxo,
63
- tapLeafScript: forfeit,
64
- scripts: encodedOffchainTapscript,
70
+ forfeitTapLeafScript: forfeit,
71
+ intentTapLeafScript: exit,
72
+ tapTree: encodedOffchainTapscript,
65
73
  }));
66
74
  await this.vtxoRepository.addOrUpdate(vtxos);
67
- this.processVtxoSubscription(addressInfo.offchain);
75
+ this.processVtxoSubscription({
76
+ script,
77
+ vtxoScript: this.wallet.offchainTapscript,
78
+ });
68
79
  }
69
- async processVtxoSubscription({ address, scripts, }) {
80
+ async processVtxoSubscription({ script, vtxoScript, }) {
70
81
  try {
71
- const addressScripts = [...scripts.exit, ...scripts.forfeit];
72
- const vtxoScript = default_1.DefaultVtxo.Script.decode(addressScripts);
73
- const tapLeafScript = vtxoScript.findLeaf(scripts.forfeit[0]);
82
+ const forfeitTapLeafScript = vtxoScript.forfeit();
83
+ const intentTapLeafScript = vtxoScript.exit();
74
84
  const abortController = new AbortController();
75
- const subscription = this.arkProvider.subscribeForAddress(address, abortController.signal);
85
+ const subscriptionId = await this.indexerProvider.subscribeForScripts([script]);
86
+ const subscription = this.indexerProvider.getSubscription(subscriptionId, abortController.signal);
76
87
  this.vtxoSubscription = abortController;
88
+ const tapTree = vtxoScript.encode();
77
89
  for await (const update of subscription) {
78
90
  const vtxos = [...update.newVtxos, ...update.spentVtxos];
79
91
  if (vtxos.length === 0) {
@@ -81,8 +93,9 @@ class Worker {
81
93
  }
82
94
  const extendedVtxos = vtxos.map((vtxo) => ({
83
95
  ...vtxo,
84
- tapLeafScript,
85
- scripts: addressScripts,
96
+ forfeitTapLeafScript,
97
+ intentTapLeafScript,
98
+ tapTree,
86
99
  }));
87
100
  await this.vtxoRepository.addOrUpdate(extendedVtxos);
88
101
  }
@@ -106,9 +119,9 @@ class Worker {
106
119
  }
107
120
  try {
108
121
  this.arkProvider = new ark_1.RestArkProvider(message.arkServerUrl);
122
+ this.indexerProvider = new indexer_1.RestIndexerProvider(message.arkServerUrl);
109
123
  this.wallet = await wallet_1.Wallet.create({
110
- network: message.network,
111
- identity: inMemoryKey_1.InMemoryKey.fromHex(message.privateKey),
124
+ identity: singleKey_1.SingleKey.fromHex(message.privateKey),
112
125
  arkServerUrl: message.arkServerUrl,
113
126
  arkServerPublicKey: message.arkServerPublicKey,
114
127
  });
@@ -162,7 +175,7 @@ class Worker {
162
175
  return;
163
176
  }
164
177
  try {
165
- const txid = await this.wallet.sendBitcoin(message.params, message.zeroFee);
178
+ const txid = await this.wallet.sendBitcoin(message.params);
166
179
  event.source?.postMessage(response_1.Response.sendBitcoinSuccess(message.id, txid));
167
180
  }
168
181
  catch (error) {
@@ -186,8 +199,8 @@ class Worker {
186
199
  return;
187
200
  }
188
201
  try {
189
- const addresses = await this.wallet.getAddress();
190
- event.source?.postMessage(response_1.Response.addresses(message.id, addresses));
202
+ const address = await this.wallet.getAddress();
203
+ event.source?.postMessage(response_1.Response.address(message.id, address));
191
204
  }
192
205
  catch (error) {
193
206
  console.error("Error getting address:", error);
@@ -197,11 +210,11 @@ class Worker {
197
210
  event.source?.postMessage(response_1.Response.error(message.id, errorMessage));
198
211
  }
199
212
  }
200
- async handleGetAddressInfo(event) {
213
+ async handleGetBoardingAddress(event) {
201
214
  const message = event.data;
202
- if (!request_1.Request.isGetAddressInfo(message)) {
203
- console.error("Invalid GET_ADDRESS_INFO message format", message);
204
- event.source?.postMessage(response_1.Response.error(message.id, "Invalid GET_ADDRESS_INFO message format"));
215
+ if (!request_1.Request.isGetBoardingAddress(message)) {
216
+ console.error("Invalid GET_BOARDING_ADDRESS message format", message);
217
+ event.source?.postMessage(response_1.Response.error(message.id, "Invalid GET_BOARDING_ADDRESS message format"));
205
218
  return;
206
219
  }
207
220
  if (!this.wallet) {
@@ -210,11 +223,11 @@ class Worker {
210
223
  return;
211
224
  }
212
225
  try {
213
- const addressInfo = await this.wallet.getAddressInfo();
214
- event.source?.postMessage(response_1.Response.addressInfo(message.id, addressInfo));
226
+ const address = await this.wallet.getBoardingAddress();
227
+ event.source?.postMessage(response_1.Response.boardingAddress(message.id, address));
215
228
  }
216
229
  catch (error) {
217
- console.error("Error getting address info:", error);
230
+ console.error("Error getting boarding address:", error);
218
231
  const errorMessage = error instanceof Error
219
232
  ? error.message
220
233
  : "Unknown error occurred";
@@ -234,40 +247,52 @@ class Worker {
234
247
  return;
235
248
  }
236
249
  try {
237
- const coins = await this.wallet.getCoins();
238
- const onchainConfirmed = coins
239
- .filter((coin) => coin.status.confirmed)
240
- .reduce((sum, coin) => sum + coin.value, 0);
241
- const onchainUnconfirmed = coins
242
- .filter((coin) => !coin.status.confirmed)
243
- .reduce((sum, coin) => sum + coin.value, 0);
244
- const onchainTotal = onchainConfirmed + onchainUnconfirmed;
245
- const spendableVtxos = await this.vtxoRepository.getSpendableVtxos();
246
- const offchainSettledBalance = spendableVtxos.reduce((sum, vtxo) => vtxo.virtualStatus.state === "settled"
247
- ? sum + vtxo.value
248
- : sum, 0);
249
- const offchainPendingBalance = spendableVtxos.reduce((sum, vtxo) => vtxo.virtualStatus.state === "pending"
250
- ? sum + vtxo.value
251
- : sum, 0);
252
- const offchainSweptBalance = spendableVtxos.reduce((sum, vtxo) => vtxo.virtualStatus.state === "swept"
253
- ? sum + vtxo.value
254
- : sum, 0);
255
- const offchainTotal = offchainSettledBalance +
256
- offchainPendingBalance +
257
- offchainSweptBalance;
250
+ const [boardingUtxos, spendableVtxos, sweptVtxos] = await Promise.all([
251
+ this.wallet.getBoardingUtxos(),
252
+ this.vtxoRepository.getSpendableVtxos(),
253
+ this.vtxoRepository.getSweptVtxos(),
254
+ ]);
255
+ // boarding
256
+ let confirmed = 0;
257
+ let unconfirmed = 0;
258
+ for (const utxo of boardingUtxos) {
259
+ if (utxo.status.confirmed) {
260
+ confirmed += utxo.value;
261
+ }
262
+ else {
263
+ unconfirmed += utxo.value;
264
+ }
265
+ }
266
+ // offchain
267
+ let settled = 0;
268
+ let preconfirmed = 0;
269
+ let recoverable = 0;
270
+ for (const vtxo of spendableVtxos) {
271
+ if (vtxo.virtualStatus.state === "settled") {
272
+ settled += vtxo.value;
273
+ }
274
+ else if (vtxo.virtualStatus.state === "preconfirmed") {
275
+ preconfirmed += vtxo.value;
276
+ }
277
+ }
278
+ for (const vtxo of sweptVtxos) {
279
+ if ((0, __1.isSpendable)(vtxo)) {
280
+ recoverable += vtxo.value;
281
+ }
282
+ }
283
+ const totalBoarding = confirmed + unconfirmed;
284
+ const totalOffchain = settled + preconfirmed + recoverable;
258
285
  event.source?.postMessage(response_1.Response.balance(message.id, {
259
- onchain: {
260
- confirmed: onchainConfirmed,
261
- unconfirmed: onchainUnconfirmed,
262
- total: onchainTotal,
286
+ boarding: {
287
+ confirmed,
288
+ unconfirmed,
289
+ total: totalBoarding,
263
290
  },
264
- offchain: {
265
- swept: offchainSweptBalance,
266
- settled: offchainSettledBalance,
267
- pending: offchainPendingBalance,
268
- total: offchainTotal,
269
- },
270
- total: onchainTotal + offchainTotal,
291
+ settled,
292
+ preconfirmed,
293
+ available: settled + preconfirmed,
294
+ recoverable,
295
+ total: totalBoarding + totalOffchain,
271
296
  }));
272
297
  }
273
298
  catch (error) {
@@ -278,30 +303,6 @@ class Worker {
278
303
  event.source?.postMessage(response_1.Response.error(message.id, errorMessage));
279
304
  }
280
305
  }
281
- async handleGetCoins(event) {
282
- const message = event.data;
283
- if (!request_1.Request.isGetCoins(message)) {
284
- console.error("Invalid GET_COINS message format", message);
285
- event.source?.postMessage(response_1.Response.error(message.id, "Invalid GET_COINS message format"));
286
- return;
287
- }
288
- if (!this.wallet) {
289
- console.error("Wallet not initialized");
290
- event.source?.postMessage(response_1.Response.error(message.id, "Wallet not initialized"));
291
- return;
292
- }
293
- try {
294
- const coins = await this.wallet.getCoins();
295
- event.source?.postMessage(response_1.Response.coins(message.id, coins));
296
- }
297
- catch (error) {
298
- console.error("Error getting coins:", error);
299
- const errorMessage = error instanceof Error
300
- ? error.message
301
- : "Unknown error occurred";
302
- event.source?.postMessage(response_1.Response.error(message.id, errorMessage));
303
- }
304
- }
305
306
  async handleGetVtxos(event) {
306
307
  const message = event.data;
307
308
  if (!request_1.Request.isGetVtxos(message)) {
@@ -315,7 +316,18 @@ class Worker {
315
316
  return;
316
317
  }
317
318
  try {
318
- const vtxos = await this.vtxoRepository.getSpendableVtxos();
319
+ let vtxos = await this.vtxoRepository.getSpendableVtxos();
320
+ if (!message.filter?.withRecoverable) {
321
+ if (!this.wallet)
322
+ throw new Error("Wallet not initialized");
323
+ // exclude subdust is we don't want recoverable
324
+ vtxos = vtxos.filter((v) => !(0, __1.isSubdust)(v, this.wallet.dustAmount));
325
+ }
326
+ if (message.filter?.withRecoverable) {
327
+ // get also swept and spendable vtxos
328
+ const sweptVtxos = await this.vtxoRepository.getSweptVtxos();
329
+ vtxos.push(...sweptVtxos.filter(__1.isSpendable));
330
+ }
319
331
  event.source?.postMessage(response_1.Response.vtxos(message.id, vtxos));
320
332
  }
321
333
  catch (error) {
@@ -363,7 +375,7 @@ class Worker {
363
375
  return;
364
376
  }
365
377
  try {
366
- const { boardingTxs, roundsToIgnore } = await this.wallet.getBoardingTxs();
378
+ const { boardingTxs, commitmentsToIgnore: roundsToIgnore } = await this.wallet.getBoardingTxs();
367
379
  const { spendable, spent } = await this.vtxoRepository.getAllVtxos();
368
380
  // convert VTXOs to offchain transactions
369
381
  const offchainTxs = (0, transactionHistory_1.vtxosToTxs)(spendable, spent, roundsToIgnore);
@@ -397,11 +409,11 @@ class Worker {
397
409
  }
398
410
  event.source?.postMessage(response_1.Response.walletStatus(message.id, this.wallet !== undefined));
399
411
  }
400
- async handleExit(event) {
412
+ async handleSign(event) {
401
413
  const message = event.data;
402
- if (!request_1.Request.isExit(message)) {
403
- console.error("Invalid EXIT message format", message);
404
- event.source?.postMessage(response_1.Response.error(message.id, "Invalid EXIT message format"));
414
+ if (!request_1.Request.isSign(message)) {
415
+ console.error("Invalid SIGN message format", message);
416
+ event.source?.postMessage(response_1.Response.error(message.id, "Invalid SIGN message format"));
405
417
  return;
406
418
  }
407
419
  if (!this.wallet) {
@@ -410,11 +422,15 @@ class Worker {
410
422
  return;
411
423
  }
412
424
  try {
413
- await this.wallet.exit(message.outpoints);
414
- event.source?.postMessage(response_1.Response.exitSuccess(message.id));
425
+ const tx = btc_signer_1.Transaction.fromPSBT(base_1.base64.decode(message.tx), {
426
+ allowUnknown: true,
427
+ allowUnknownInputs: true,
428
+ });
429
+ const signedTx = await this.wallet.identity.sign(tx, message.inputIndexes);
430
+ event.source?.postMessage(response_1.Response.signSuccess(message.id, base_1.base64.encode(signedTx.toPSBT())));
415
431
  }
416
432
  catch (error) {
417
- console.error("Error exiting:", error);
433
+ console.error("Error signing:", error);
418
434
  const errorMessage = error instanceof Error
419
435
  ? error.message
420
436
  : "Unknown error occurred";
@@ -446,18 +462,14 @@ class Worker {
446
462
  await this.handleGetAddress(event);
447
463
  break;
448
464
  }
449
- case "GET_ADDRESS_INFO": {
450
- await this.handleGetAddressInfo(event);
465
+ case "GET_BOARDING_ADDRESS": {
466
+ await this.handleGetBoardingAddress(event);
451
467
  break;
452
468
  }
453
469
  case "GET_BALANCE": {
454
470
  await this.handleGetBalance(event);
455
471
  break;
456
472
  }
457
- case "GET_COINS": {
458
- await this.handleGetCoins(event);
459
- break;
460
- }
461
473
  case "GET_VTXOS": {
462
474
  await this.handleGetVtxos(event);
463
475
  break;
@@ -474,14 +486,14 @@ class Worker {
474
486
  await this.handleGetStatus(event);
475
487
  break;
476
488
  }
477
- case "EXIT": {
478
- await this.handleExit(event);
479
- break;
480
- }
481
489
  case "CLEAR": {
482
490
  await this.handleClear(event);
483
491
  break;
484
492
  }
493
+ case "SIGN": {
494
+ await this.handleSign(event);
495
+ break;
496
+ }
485
497
  default:
486
498
  event.source?.postMessage(response_1.Response.error(message.id, "Unknown message type"));
487
499
  }
@@ -0,0 +1,270 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.Unroll = void 0;
4
+ const btc_signer_1 = require("@scure/btc-signer");
5
+ const indexer_1 = require("../providers/indexer");
6
+ const base_1 = require("@scure/base");
7
+ const base_2 = require("../script/base");
8
+ const psbt_1 = require("@scure/btc-signer/psbt");
9
+ const txSizeEstimator_1 = require("../utils/txSizeEstimator");
10
+ const wallet_1 = require("./wallet");
11
+ var Unroll;
12
+ (function (Unroll) {
13
+ let StepType;
14
+ (function (StepType) {
15
+ StepType[StepType["UNROLL"] = 0] = "UNROLL";
16
+ StepType[StepType["WAIT"] = 1] = "WAIT";
17
+ StepType[StepType["DONE"] = 2] = "DONE";
18
+ })(StepType = Unroll.StepType || (Unroll.StepType = {}));
19
+ /**
20
+ * Manages the unrolling process of a VTXO back to the Bitcoin blockchain.
21
+ *
22
+ * The Session class implements an async iterator that processes the unrolling steps:
23
+ * 1. **WAIT**: Waits for a transaction to be confirmed onchain (if it's in mempool)
24
+ * 2. **UNROLL**: Broadcasts the next transaction in the chain to the blockchain
25
+ * 3. **DONE**: Indicates the unrolling process is complete
26
+ *
27
+ * The unrolling process works by traversing the transaction chain from the root (most recent)
28
+ * to the leaf (oldest), broadcasting each transaction that isn't already onchain.
29
+ *
30
+ * @example
31
+ * ```typescript
32
+ * const session = await Unroll.Session.create(vtxoOutpoint, bumper, explorer, indexer);
33
+ *
34
+ * // iterate over the steps
35
+ * for await (const doneStep of session) {
36
+ * switch (doneStep.type) {
37
+ * case Unroll.StepType.WAIT:
38
+ * console.log(`Transaction ${doneStep.txid} confirmed`);
39
+ * break;
40
+ * case Unroll.StepType.UNROLL:
41
+ * console.log(`Broadcasting transaction ${doneStep.tx.id}`);
42
+ * break;
43
+ * case Unroll.StepType.DONE:
44
+ * console.log(`Unrolling complete for VTXO ${doneStep.vtxoTxid}`);
45
+ * break;
46
+ * }
47
+ * }
48
+ * ```
49
+ **/
50
+ class Session {
51
+ constructor(toUnroll, bumper, explorer, indexer) {
52
+ this.toUnroll = toUnroll;
53
+ this.bumper = bumper;
54
+ this.explorer = explorer;
55
+ this.indexer = indexer;
56
+ }
57
+ static async create(toUnroll, bumper, explorer, indexer) {
58
+ const { chain } = await indexer.getVtxoChain(toUnroll);
59
+ return new Session({ ...toUnroll, chain }, bumper, explorer, indexer);
60
+ }
61
+ /**
62
+ * Get the next step to be executed
63
+ * @returns The next step to be executed + the function to execute it
64
+ */
65
+ async next() {
66
+ let nextTxToBroadcast;
67
+ const chain = this.toUnroll.chain;
68
+ // Iterate through the chain from the end (root) to the beginning (leaf)
69
+ for (let i = chain.length - 1; i >= 0; i--) {
70
+ const chainTx = chain[i];
71
+ // Skip commitment transactions as they are always onchain
72
+ if (chainTx.type === indexer_1.ChainTxType.COMMITMENT ||
73
+ chainTx.type === indexer_1.ChainTxType.UNSPECIFIED) {
74
+ continue;
75
+ }
76
+ try {
77
+ // Check if the transaction is confirmed onchain
78
+ const txInfo = await this.explorer.getTxStatus(chainTx.txid);
79
+ // If found but not confirmed, it means the tx is in the mempool
80
+ // An unilateral exit is running, we must wait for it to be confirmed
81
+ if (!txInfo.confirmed) {
82
+ return {
83
+ type: StepType.WAIT,
84
+ txid: chainTx.txid,
85
+ do: doWait(this.explorer, chainTx.txid),
86
+ };
87
+ }
88
+ }
89
+ catch (e) {
90
+ // If the tx is not found, it's offchain, let's break
91
+ nextTxToBroadcast = chainTx;
92
+ break;
93
+ }
94
+ }
95
+ if (!nextTxToBroadcast) {
96
+ return {
97
+ type: StepType.DONE,
98
+ vtxoTxid: this.toUnroll.txid,
99
+ do: () => Promise.resolve(),
100
+ };
101
+ }
102
+ // Get the virtual transaction data
103
+ const virtualTxs = await this.indexer.getVirtualTxs([
104
+ nextTxToBroadcast.txid,
105
+ ]);
106
+ if (virtualTxs.txs.length === 0) {
107
+ throw new Error(`Tx ${nextTxToBroadcast.txid} not found`);
108
+ }
109
+ const tx = btc_signer_1.Transaction.fromPSBT(base_1.base64.decode(virtualTxs.txs[0]), {
110
+ allowUnknownInputs: true,
111
+ });
112
+ // finalize the tree transaction
113
+ if (nextTxToBroadcast.type === indexer_1.ChainTxType.TREE) {
114
+ const input = tx.getInput(0);
115
+ if (!input) {
116
+ throw new Error("Input not found");
117
+ }
118
+ const tapKeySig = input.tapKeySig;
119
+ if (!tapKeySig) {
120
+ throw new Error("Tap key sig not found");
121
+ }
122
+ tx.updateInput(0, {
123
+ finalScriptWitness: [tapKeySig],
124
+ });
125
+ }
126
+ else {
127
+ // finalize ark transaction
128
+ tx.finalize();
129
+ }
130
+ return {
131
+ type: StepType.UNROLL,
132
+ tx,
133
+ do: doUnroll(this.bumper, this.explorer, tx),
134
+ };
135
+ }
136
+ /**
137
+ * Iterate over the steps to be executed and execute them
138
+ * @returns An async iterator over the executed steps
139
+ */
140
+ async *[Symbol.asyncIterator]() {
141
+ let lastStep;
142
+ do {
143
+ if (lastStep !== undefined) {
144
+ // wait 1 second before trying the next step in order to give time to the
145
+ // explorer to update the tx status
146
+ await sleep(1000);
147
+ }
148
+ const step = await this.next();
149
+ await step.do();
150
+ yield step;
151
+ lastStep = step.type;
152
+ } while (lastStep !== StepType.DONE);
153
+ }
154
+ }
155
+ Unroll.Session = Session;
156
+ /**
157
+ * Complete the unroll of a VTXO by broadcasting the transaction that spends the CSV path.
158
+ * @param wallet the wallet owning the VTXO(s)
159
+ * @param vtxoTxids the txids of the VTXO(s) to complete unroll
160
+ * @param outputAddress the address to send the unrolled funds to
161
+ * @throws if the VTXO(s) are not fully unrolled, if the txids are not found, if the tx is not confirmed, if no exit path is found or not available
162
+ * @returns the txid of the transaction spending the unrolled funds
163
+ */
164
+ async function completeUnroll(wallet, vtxoTxids, outputAddress) {
165
+ const chainTip = await wallet.onchainProvider.getChainTip();
166
+ let vtxos = await wallet.getVtxos({ withUnrolled: true });
167
+ vtxos = vtxos.filter((vtxo) => vtxoTxids.includes(vtxo.txid));
168
+ if (vtxos.length === 0) {
169
+ throw new Error("No vtxos to complete unroll");
170
+ }
171
+ const inputs = [];
172
+ let totalAmount = 0n;
173
+ const txWeightEstimator = txSizeEstimator_1.TxWeightEstimator.create();
174
+ for (const vtxo of vtxos) {
175
+ if (!vtxo.isUnrolled) {
176
+ throw new Error(`Vtxo ${vtxo.txid}:${vtxo.vout} is not fully unrolled, use unroll first`);
177
+ }
178
+ const txStatus = await wallet.onchainProvider.getTxStatus(vtxo.txid);
179
+ if (!txStatus.confirmed) {
180
+ throw new Error(`tx ${vtxo.txid} is not confirmed`);
181
+ }
182
+ const exit = availableExitPath({ height: txStatus.blockHeight, time: txStatus.blockTime }, chainTip, vtxo);
183
+ if (!exit) {
184
+ throw new Error(`no available exit path found for vtxo ${vtxo.txid}:${vtxo.vout}`);
185
+ }
186
+ const spendingLeaf = base_2.VtxoScript.decode(vtxo.tapTree).findLeaf(base_1.hex.encode(exit.script));
187
+ if (!spendingLeaf) {
188
+ throw new Error(`spending leaf not found for vtxo ${vtxo.txid}:${vtxo.vout}`);
189
+ }
190
+ totalAmount += BigInt(vtxo.value);
191
+ inputs.push({
192
+ txid: vtxo.txid,
193
+ index: vtxo.vout,
194
+ tapLeafScript: [spendingLeaf],
195
+ sequence: 0xffffffff - 1,
196
+ witnessUtxo: {
197
+ amount: BigInt(vtxo.value),
198
+ script: base_2.VtxoScript.decode(vtxo.tapTree).pkScript,
199
+ },
200
+ sighashType: btc_signer_1.SigHash.DEFAULT,
201
+ });
202
+ txWeightEstimator.addTapscriptInput(64, spendingLeaf[1].length, psbt_1.TaprootControlBlock.encode(spendingLeaf[0]).length);
203
+ }
204
+ const tx = new btc_signer_1.Transaction({ allowUnknownInputs: true, version: 2 });
205
+ for (const input of inputs) {
206
+ tx.addInput(input);
207
+ }
208
+ txWeightEstimator.addP2TROutput();
209
+ let feeRate = await wallet.onchainProvider.getFeeRate();
210
+ if (!feeRate || feeRate < wallet_1.Wallet.MIN_FEE_RATE) {
211
+ feeRate = wallet_1.Wallet.MIN_FEE_RATE;
212
+ }
213
+ const feeAmount = txWeightEstimator.vsize().fee(BigInt(feeRate));
214
+ if (feeAmount > totalAmount) {
215
+ throw new Error("fee amount is greater than the total amount");
216
+ }
217
+ tx.addOutputAddress(outputAddress, totalAmount - feeAmount);
218
+ const signedTx = await wallet.identity.sign(tx);
219
+ signedTx.finalize();
220
+ await wallet.onchainProvider.broadcastTransaction(signedTx.hex);
221
+ return signedTx.id;
222
+ }
223
+ Unroll.completeUnroll = completeUnroll;
224
+ })(Unroll || (exports.Unroll = Unroll = {}));
225
+ function sleep(ms) {
226
+ return new Promise((resolve) => setTimeout(resolve, ms));
227
+ }
228
+ function doUnroll(bumper, onchainProvider, tx) {
229
+ return async () => {
230
+ const [parent, child] = await bumper.bumpP2A(tx);
231
+ await onchainProvider.broadcastTransaction(parent, child);
232
+ };
233
+ }
234
+ function doWait(onchainProvider, txid) {
235
+ return () => {
236
+ return new Promise((resolve, reject) => {
237
+ const interval = setInterval(async () => {
238
+ try {
239
+ const txInfo = await onchainProvider.getTxStatus(txid);
240
+ if (txInfo.confirmed) {
241
+ clearInterval(interval);
242
+ resolve();
243
+ }
244
+ }
245
+ catch (e) {
246
+ clearInterval(interval);
247
+ reject(e);
248
+ }
249
+ }, 5000);
250
+ });
251
+ };
252
+ }
253
+ function availableExitPath(confirmedAt, current, vtxo) {
254
+ const exits = base_2.VtxoScript.decode(vtxo.tapTree).exitPaths();
255
+ for (const exit of exits) {
256
+ if (exit.params.timelock.type === "blocks") {
257
+ if (current.height >=
258
+ confirmedAt.height + Number(exit.params.timelock.value)) {
259
+ return exit;
260
+ }
261
+ }
262
+ else {
263
+ if (current.time >=
264
+ confirmedAt.time + Number(exit.params.timelock.value)) {
265
+ return exit;
266
+ }
267
+ }
268
+ }
269
+ return undefined;
270
+ }