@arkade-os/sdk 0.3.1-alpha.4 → 0.3.1-alpha.6

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 (47) hide show
  1. package/README.md +11 -27
  2. package/dist/cjs/forfeit.js +2 -5
  3. package/dist/cjs/identity/singleKey.js +4 -5
  4. package/dist/cjs/index.js +5 -4
  5. package/dist/cjs/intent/index.js +3 -8
  6. package/dist/cjs/providers/onchain.js +19 -20
  7. package/dist/cjs/repositories/walletRepository.js +64 -2
  8. package/dist/cjs/script/base.js +14 -5
  9. package/dist/cjs/utils/arkTransaction.js +3 -5
  10. package/dist/cjs/utils/transaction.js +28 -0
  11. package/dist/cjs/wallet/onchain.js +4 -4
  12. package/dist/cjs/wallet/serviceWorker/worker.js +19 -2
  13. package/dist/cjs/wallet/unroll.js +3 -4
  14. package/dist/cjs/wallet/utils.js +9 -0
  15. package/dist/cjs/wallet/vtxo-manager.js +31 -89
  16. package/dist/cjs/wallet/wallet.js +11 -13
  17. package/dist/esm/forfeit.js +1 -4
  18. package/dist/esm/identity/singleKey.js +3 -4
  19. package/dist/esm/index.js +3 -3
  20. package/dist/esm/intent/index.js +2 -7
  21. package/dist/esm/providers/onchain.js +19 -20
  22. package/dist/esm/repositories/walletRepository.js +64 -2
  23. package/dist/esm/script/base.js +11 -2
  24. package/dist/esm/utils/arkTransaction.js +3 -5
  25. package/dist/esm/utils/transaction.js +24 -0
  26. package/dist/esm/wallet/onchain.js +3 -3
  27. package/dist/esm/wallet/serviceWorker/worker.js +21 -4
  28. package/dist/esm/wallet/unroll.js +4 -5
  29. package/dist/esm/wallet/utils.js +8 -0
  30. package/dist/esm/wallet/vtxo-manager.js +30 -85
  31. package/dist/esm/wallet/wallet.js +12 -14
  32. package/dist/types/forfeit.d.ts +1 -1
  33. package/dist/types/identity/index.d.ts +1 -1
  34. package/dist/types/identity/singleKey.d.ts +1 -1
  35. package/dist/types/index.d.ts +3 -3
  36. package/dist/types/intent/index.d.ts +1 -1
  37. package/dist/types/providers/onchain.d.ts +6 -2
  38. package/dist/types/repositories/walletRepository.d.ts +9 -1
  39. package/dist/types/script/base.d.ts +2 -0
  40. package/dist/types/utils/arkTransaction.d.ts +1 -3
  41. package/dist/types/utils/transaction.d.ts +13 -0
  42. package/dist/types/wallet/onchain.d.ts +1 -1
  43. package/dist/types/wallet/serviceWorker/worker.d.ts +4 -0
  44. package/dist/types/wallet/unroll.d.ts +1 -1
  45. package/dist/types/wallet/utils.d.ts +2 -1
  46. package/dist/types/wallet/vtxo-manager.d.ts +7 -35
  47. package/package.json +1 -1
@@ -2,10 +2,7 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.VtxoManager = exports.DEFAULT_RENEWAL_CONFIG = void 0;
4
4
  exports.isVtxoExpiringSoon = isVtxoExpiringSoon;
5
- exports.getExpiringVtxos = getExpiringVtxos;
6
- exports.calculateExpiryThreshold = calculateExpiryThreshold;
7
- exports.getMinimumExpiry = getMinimumExpiry;
8
- exports.calculateDynamicThreshold = calculateDynamicThreshold;
5
+ exports.getExpiringAndRecoverableVtxos = getExpiringAndRecoverableVtxos;
9
6
  const _1 = require(".");
10
7
  /**
11
8
  * Default renewal configuration values
@@ -13,6 +10,9 @@ const _1 = require(".");
13
10
  exports.DEFAULT_RENEWAL_CONFIG = {
14
11
  thresholdPercentage: 10,
15
12
  };
13
+ function getDustAmount(wallet) {
14
+ return "dustAmount" in wallet ? wallet.dustAmount : 330n;
15
+ }
16
16
  /**
17
17
  * Filter VTXOs that are recoverable (swept and still spendable, or preconfirmed subdust)
18
18
  *
@@ -82,80 +82,35 @@ function getRecoverableWithSubdust(vtxos, dustAmount) {
82
82
  * @param thresholdMs - Threshold in milliseconds from now
83
83
  * @returns true if VTXO expires within threshold, false otherwise
84
84
  */
85
- function isVtxoExpiringSoon(vtxo, thresholdMs) {
85
+ function isVtxoExpiringSoon(vtxo, percentage) {
86
86
  const { batchExpiry } = vtxo.virtualStatus;
87
- // No expiry set means it doesn't expire
88
87
  if (!batchExpiry) {
89
- return false;
88
+ return false; // it doesn't expire
90
89
  }
91
90
  const now = Date.now();
92
- const timeUntilExpiry = batchExpiry - now;
93
- return timeUntilExpiry > 0 && timeUntilExpiry <= thresholdMs;
91
+ if (batchExpiry <= now) {
92
+ return false; // already expired
93
+ }
94
+ // It shouldn't happen, but let's be safe
95
+ if (!vtxo.createdAt) {
96
+ return false;
97
+ }
98
+ const duration = batchExpiry - vtxo.createdAt.getTime();
99
+ const softExpiry = batchExpiry - (duration * percentage) / 100;
100
+ return softExpiry > 0 && softExpiry <= now;
94
101
  }
95
102
  /**
96
- * Filter VTXOs that are expiring soon
103
+ * Filter VTXOs that are expiring soon or are recoverable/subdust
97
104
  *
98
105
  * @param vtxos - Array of virtual coins to check
99
106
  * @param thresholdMs - Threshold in milliseconds from now
107
+ * @param dustAmount - Dust threshold amount in satoshis
100
108
  * @returns Array of VTXOs expiring within threshold
101
109
  */
102
- function getExpiringVtxos(vtxos, thresholdMs) {
103
- return vtxos.filter((vtxo) => isVtxoExpiringSoon(vtxo, thresholdMs));
104
- }
105
- /**
106
- * Calculate expiry threshold in milliseconds based on batch expiry and percentage
107
- *
108
- * @param batchExpiry - Batch expiry timestamp in milliseconds
109
- * @param percentage - Percentage of total time (0-100)
110
- * @returns Threshold timestamp in milliseconds from now
111
- *
112
- * @example
113
- * // VTXO expires in 10 days, threshold is 10%
114
- * const expiry = Date.now() + 10 * 24 * 60 * 60 * 1000;
115
- * const threshold = calculateExpiryThreshold(expiry, 10);
116
- * // Returns 1 day in milliseconds (10% of 10 days)
117
- */
118
- function calculateExpiryThreshold(batchExpiry, percentage) {
119
- if (percentage < 0 || percentage > 100) {
120
- throw new Error("Percentage must be between 0 and 100");
121
- }
122
- const now = Date.now();
123
- const totalTime = batchExpiry - now;
124
- if (totalTime <= 0) {
125
- // Already expired
126
- return 0;
127
- }
128
- // Calculate threshold as percentage of total time
129
- return Math.floor((totalTime * percentage) / 100);
130
- }
131
- /**
132
- * Get the minimum expiry time from a list of VTXOs
133
- *
134
- * @param vtxos - Array of virtual coins
135
- * @returns Minimum batch expiry timestamp, or undefined if no VTXOs have expiry
136
- */
137
- function getMinimumExpiry(vtxos) {
138
- const expiries = vtxos
139
- .map((v) => v.virtualStatus.batchExpiry)
140
- .filter((e) => e !== undefined);
141
- if (expiries.length === 0) {
142
- return undefined;
143
- }
144
- return Math.min(...expiries);
145
- }
146
- /**
147
- * Calculate dynamic threshold based on the earliest expiring VTXO
148
- *
149
- * @param vtxos - Array of virtual coins
150
- * @param percentage - Percentage of time until expiry (0-100)
151
- * @returns Threshold in milliseconds, or undefined if no VTXOs have expiry
152
- */
153
- function calculateDynamicThreshold(vtxos, percentage) {
154
- const minExpiry = getMinimumExpiry(vtxos);
155
- if (!minExpiry) {
156
- return undefined;
157
- }
158
- return calculateExpiryThreshold(minExpiry, percentage);
110
+ function getExpiringAndRecoverableVtxos(vtxos, percentage, dustAmount) {
111
+ return vtxos.filter((vtxo) => isVtxoExpiringSoon(vtxo, percentage) ||
112
+ (0, _1.isRecoverable)(vtxo) ||
113
+ (0, _1.isSubdust)(vtxo, dustAmount));
159
114
  }
160
115
  /**
161
116
  * VtxoManager is a unified class for managing VTXO lifecycle operations including
@@ -236,9 +191,7 @@ class VtxoManager {
236
191
  withUnrolled: false,
237
192
  });
238
193
  // Get dust amount from wallet
239
- const dustAmount = "dustAmount" in this.wallet
240
- ? this.wallet.dustAmount
241
- : 1000n;
194
+ const dustAmount = getDustAmount(this.wallet);
242
195
  // Filter recoverable VTXOs and handle subdust logic
243
196
  const { vtxosToRecover, includesSubdust, totalAmount } = getRecoverableWithSubdust(allVtxos, dustAmount);
244
197
  if (vtxosToRecover.length === 0) {
@@ -281,9 +234,7 @@ class VtxoManager {
281
234
  withRecoverable: true,
282
235
  withUnrolled: false,
283
236
  });
284
- const dustAmount = "dustAmount" in this.wallet
285
- ? this.wallet.dustAmount
286
- : 1000n;
237
+ const dustAmount = getDustAmount(this.wallet);
287
238
  const { vtxosToRecover, includesSubdust, totalAmount } = getRecoverableWithSubdust(allVtxos, dustAmount);
288
239
  // Calculate subdust amount separately for reporting
289
240
  const subdustAmount = vtxosToRecover
@@ -313,23 +264,16 @@ class VtxoManager {
313
264
  * ```
314
265
  */
315
266
  async getExpiringVtxos(thresholdPercentage) {
316
- if (!this.renewalConfig?.enabled) {
317
- return [];
318
- }
319
- const vtxos = await this.wallet.getVtxos();
267
+ const vtxos = await this.wallet.getVtxos({ withRecoverable: true });
320
268
  const percentage = thresholdPercentage ??
321
- this.renewalConfig.thresholdPercentage ??
269
+ this.renewalConfig?.thresholdPercentage ??
322
270
  exports.DEFAULT_RENEWAL_CONFIG.thresholdPercentage;
323
- const threshold = calculateDynamicThreshold(vtxos, percentage);
324
- if (!threshold) {
325
- return [];
326
- }
327
- return getExpiringVtxos(vtxos, threshold);
271
+ return getExpiringAndRecoverableVtxos(vtxos, percentage, getDustAmount(this.wallet));
328
272
  }
329
273
  /**
330
- * Renew VTXOs by settling them back to the wallet's address
274
+ * Renew expiring VTXOs by settling them back to the wallet's address
331
275
  *
332
- * This method collects all spendable VTXOs (including recoverable ones) and settles
276
+ * This method collects all expiring spendable VTXOs (including recoverable ones) and settles
333
277
  * them back to the wallet, effectively refreshing their expiration time. This is the
334
278
  * primary way to prevent VTXOs from expiring.
335
279
  *
@@ -353,15 +297,13 @@ class VtxoManager {
353
297
  */
354
298
  async renewVtxos(eventCallback) {
355
299
  // Get all VTXOs (including recoverable ones)
356
- const vtxos = await this.wallet.getVtxos({ withRecoverable: true });
300
+ const vtxos = await this.getExpiringVtxos();
357
301
  if (vtxos.length === 0) {
358
302
  throw new Error("No VTXOs available to renew");
359
303
  }
360
304
  const totalAmount = vtxos.reduce((sum, vtxo) => sum + vtxo.value, 0);
361
305
  // Get dust amount from wallet
362
- const dustAmount = "dustAmount" in this.wallet
363
- ? this.wallet.dustAmount
364
- : 1000n;
306
+ const dustAmount = getDustAmount(this.wallet);
365
307
  // Check if total amount is above dust threshold
366
308
  if (BigInt(totalAmount) < dustAmount) {
367
309
  throw new Error(`Total amount ${totalAmount} is below dust threshold ${dustAmount}`);
@@ -370,15 +370,12 @@ class Wallet {
370
370
  async getBoardingUtxos() {
371
371
  const boardingAddress = await this.getBoardingAddress();
372
372
  const boardingUtxos = await this.onchainProvider.getCoins(boardingAddress);
373
- const encodedBoardingTapscript = this.boardingTapscript.encode();
374
- const forfeit = this.boardingTapscript.forfeit();
375
- const exit = this.boardingTapscript.exit();
376
- return boardingUtxos.map((utxo) => ({
377
- ...utxo,
378
- forfeitTapLeafScript: forfeit,
379
- intentTapLeafScript: exit,
380
- tapTree: encodedBoardingTapscript,
381
- }));
373
+ const utxos = boardingUtxos.map((utxo) => {
374
+ return (0, utils_1.extendCoin)(this, utxo);
375
+ });
376
+ // Save boardingUtxos using unified repository
377
+ await this.walletRepository.saveUtxos(boardingAddress, utxos);
378
+ return utxos;
382
379
  }
383
380
  async sendBitcoin(params) {
384
381
  if (params.amount <= 0) {
@@ -798,8 +795,6 @@ class Wallet {
798
795
  const vtxo = vtxos.find((vtxo) => vtxo.txid === input.txid && vtxo.vout === input.vout);
799
796
  // boarding utxo, we need to sign the settlement tx
800
797
  if (!vtxo) {
801
- hasBoardingUtxos = true;
802
- const inputIndexes = [];
803
798
  for (let i = 0; i < settlementPsbt.inputsLength; i++) {
804
799
  const settlementInput = settlementPsbt.getInput(i);
805
800
  if (!settlementInput.txid ||
@@ -815,9 +810,12 @@ class Wallet {
815
810
  settlementPsbt.updateInput(i, {
816
811
  tapLeafScript: [input.forfeitTapLeafScript],
817
812
  });
818
- inputIndexes.push(i);
813
+ settlementPsbt = await this.identity.sign(settlementPsbt, [
814
+ i,
815
+ ]);
816
+ hasBoardingUtxos = true;
817
+ break;
819
818
  }
820
- settlementPsbt = await this.identity.sign(settlementPsbt, inputIndexes);
821
819
  continue;
822
820
  }
823
821
  if ((0, _1.isRecoverable)(vtxo) || (0, _1.isSubdust)(vtxo, this.dustAmount)) {
@@ -1,12 +1,9 @@
1
- import { Transaction } from "@scure/btc-signer";
1
+ import { Transaction } from './utils/transaction.js';
2
2
  import { P2A } from './utils/anchor.js';
3
3
  export function buildForfeitTx(inputs, forfeitPkScript, txLocktime) {
4
4
  const tx = new Transaction({
5
5
  version: 3,
6
6
  lockTime: txLocktime,
7
- allowUnknownOutputs: true,
8
- allowUnknown: true,
9
- allowUnknownInputs: true,
10
7
  });
11
8
  let amount = 0n;
12
9
  for (const input of inputs) {
@@ -1,9 +1,8 @@
1
1
  import { pubECDSA, pubSchnorr, randomPrivateKeyBytes, } from "@scure/btc-signer/utils.js";
2
- import { SigHash } from "@scure/btc-signer/transaction.js";
2
+ import { SigHash } from "@scure/btc-signer";
3
3
  import { hex } from "@scure/base";
4
4
  import { TreeSignerSession } from '../tree/signingSession.js';
5
5
  import { schnorr, sign } from "@noble/secp256k1";
6
- const ZERO_32 = new Uint8Array(32).fill(0);
7
6
  const ALL_SIGHASH = Object.values(SigHash).filter((x) => typeof x === "number");
8
7
  /**
9
8
  * In-memory single key implementation for Bitcoin transaction signing.
@@ -48,7 +47,7 @@ export class SingleKey {
48
47
  const txCpy = tx.clone();
49
48
  if (!inputIndexes) {
50
49
  try {
51
- if (!txCpy.sign(this.key, ALL_SIGHASH, ZERO_32)) {
50
+ if (!txCpy.sign(this.key, ALL_SIGHASH)) {
52
51
  throw new Error("Failed to sign transaction");
53
52
  }
54
53
  }
@@ -64,7 +63,7 @@ export class SingleKey {
64
63
  return txCpy;
65
64
  }
66
65
  for (const inputIndex of inputIndexes) {
67
- if (!txCpy.signIdx(this.key, inputIndex, ALL_SIGHASH, ZERO_32)) {
66
+ if (!txCpy.signIdx(this.key, inputIndex, ALL_SIGHASH)) {
68
67
  throw new Error(`Failed to sign input #${inputIndex}`);
69
68
  }
70
69
  }
package/dist/esm/index.js CHANGED
@@ -1,9 +1,9 @@
1
- import { Transaction } from "@scure/btc-signer/transaction.js";
1
+ import { Transaction } from './utils/transaction.js';
2
2
  import { SingleKey } from './identity/singleKey.js';
3
3
  import { ArkAddress } from './script/address.js';
4
4
  import { VHTLC } from './script/vhtlc.js';
5
5
  import { DefaultVtxo } from './script/default.js';
6
- import { VtxoScript } from './script/base.js';
6
+ import { VtxoScript, TapTreeCoder, } from './script/base.js';
7
7
  import { TxType, } from './wallet/index.js';
8
8
  import { Wallet, waitForIncomingFunds } from './wallet/wallet.js';
9
9
  import { TxTree } from './tree/txTree.js';
@@ -41,7 +41,7 @@ TxType, IndexerTxType, ChainTxType, SettlementEventType,
41
41
  // Service Worker
42
42
  setupServiceWorker, Worker, ServiceWorkerWallet, Request, Response,
43
43
  // Tapscript
44
- decodeTapscript, MultisigTapscript, CSVMultisigTapscript, ConditionCSVMultisigTapscript, ConditionMultisigTapscript, CLTVMultisigTapscript,
44
+ decodeTapscript, MultisigTapscript, CSVMultisigTapscript, ConditionCSVMultisigTapscript, ConditionMultisigTapscript, CLTVMultisigTapscript, TapTreeCoder,
45
45
  // Ark PSBT fields
46
46
  ArkPsbtFieldKey, ArkPsbtFieldKeyType, setArkPsbtField, getArkPsbtFields, CosignerPublicKey, VtxoTreeExpiry, VtxoTaprootTree, ConditionWitness,
47
47
  // Utils
@@ -1,5 +1,6 @@
1
- import { OP, Transaction, Script, SigHash } from "@scure/btc-signer";
1
+ import { OP, Script, SigHash } from "@scure/btc-signer";
2
2
  import { schnorr } from "@noble/curves/secp256k1.js";
3
+ import { Transaction } from '../utils/transaction.js';
3
4
  /**
4
5
  * Intent proof implementation for Bitcoin message signing.
5
6
  *
@@ -84,9 +85,6 @@ function craftToSpendTx(message, pkScript) {
84
85
  const messageHash = hashMessage(message);
85
86
  const tx = new Transaction({
86
87
  version: 0,
87
- allowUnknownOutputs: true,
88
- allowUnknown: true,
89
- allowUnknownInputs: true,
90
88
  });
91
89
  // add input with zero hash and max index
92
90
  tx.addInput({
@@ -109,9 +107,6 @@ function craftToSignTx(toSpend, inputs, outputs) {
109
107
  const firstInput = inputs[0];
110
108
  const tx = new Transaction({
111
109
  version: 2,
112
- allowUnknownOutputs: outputs.length === 0,
113
- allowUnknown: true,
114
- allowUnknownInputs: true,
115
110
  lockTime: 0,
116
111
  });
117
112
  // add the first "toSpend" input
@@ -18,9 +18,10 @@ export const ESPLORA_URL = {
18
18
  * ```
19
19
  */
20
20
  export class EsploraProvider {
21
- constructor(baseUrl) {
21
+ constructor(baseUrl, opts) {
22
22
  this.baseUrl = baseUrl;
23
- this.polling = false;
23
+ this.pollingInterval = opts?.pollingInterval ?? 15000;
24
+ this.forcePolling = opts?.forcePolling ?? false;
24
25
  }
25
26
  async getCoins(address) {
26
27
  const response = await fetch(`${this.baseUrl}/address/${address}/utxo`);
@@ -91,13 +92,9 @@ export class EsploraProvider {
91
92
  let intervalId = null;
92
93
  const wsUrl = this.baseUrl.replace(/^http(s)?:/, "ws$1:") + "/v1/ws";
93
94
  const poll = async () => {
94
- if (this.polling)
95
- return;
96
- this.polling = true;
97
- // websocket is not reliable, so we will fallback to polling
98
- const pollingInterval = 5000; // 5 seconds
99
- const getAllTxs = () => {
100
- return Promise.all(addresses.map((address) => this.getTransactions(address))).then((txArrays) => txArrays.flat());
95
+ const getAllTxs = async () => {
96
+ const txArrays = await Promise.all(addresses.map((address) => this.getTransactions(address)));
97
+ return txArrays.flat();
101
98
  };
102
99
  // initial fetch to get existing transactions
103
100
  const initialTxs = await getAllTxs();
@@ -122,9 +119,19 @@ export class EsploraProvider {
122
119
  catch (error) {
123
120
  console.error("Error in polling mechanism:", error);
124
121
  }
125
- }, pollingInterval);
122
+ }, this.pollingInterval);
126
123
  };
127
124
  let ws = null;
125
+ const stopFunc = () => {
126
+ if (ws)
127
+ ws.close();
128
+ if (intervalId)
129
+ clearInterval(intervalId);
130
+ };
131
+ if (this.forcePolling) {
132
+ await poll();
133
+ return stopFunc;
134
+ }
128
135
  try {
129
136
  ws = new WebSocket(wsUrl);
130
137
  ws.addEventListener("open", () => {
@@ -171,13 +178,6 @@ export class EsploraProvider {
171
178
  // if websocket is not available, fallback to polling
172
179
  await poll();
173
180
  }
174
- const stopFunc = () => {
175
- if (ws && ws.readyState === WebSocket.OPEN)
176
- ws.close();
177
- if (intervalId)
178
- clearInterval(intervalId);
179
- this.polling = false;
180
- };
181
181
  return stopFunc;
182
182
  }
183
183
  async getChainTip() {
@@ -245,8 +245,7 @@ const isExplorerTransaction = (tx) => {
245
245
  return (typeof tx.txid === "string" &&
246
246
  Array.isArray(tx.vout) &&
247
247
  tx.vout.every((vout) => typeof vout.scriptpubkey_address === "string" &&
248
- typeof vout.value === "string") &&
248
+ typeof vout.value === "number") &&
249
249
  typeof tx.status === "object" &&
250
- typeof tx.status.confirmed === "boolean" &&
251
- typeof tx.status.block_time === "number");
250
+ typeof tx.status.confirmed === "boolean");
252
251
  };
@@ -12,7 +12,14 @@ const serializeVtxo = (v) => ({
12
12
  tapTree: toHex(v.tapTree),
13
13
  forfeitTapLeafScript: serializeTapLeaf(v.forfeitTapLeafScript),
14
14
  intentTapLeafScript: serializeTapLeaf(v.intentTapLeafScript),
15
- extraWitness: v.extraWitness?.map((w) => toHex(w)),
15
+ extraWitness: v.extraWitness?.map(toHex),
16
+ });
17
+ const serializeUtxo = (u) => ({
18
+ ...u,
19
+ tapTree: toHex(u.tapTree),
20
+ forfeitTapLeafScript: serializeTapLeaf(u.forfeitTapLeafScript),
21
+ intentTapLeafScript: serializeTapLeaf(u.intentTapLeafScript),
22
+ extraWitness: u.extraWitness?.map(toHex),
16
23
  });
17
24
  const deserializeTapLeaf = (t) => {
18
25
  const cb = TaprootControlBlock.decode(fromHex(t.cb));
@@ -24,13 +31,21 @@ const deserializeVtxo = (o) => ({
24
31
  tapTree: fromHex(o.tapTree),
25
32
  forfeitTapLeafScript: deserializeTapLeaf(o.forfeitTapLeafScript),
26
33
  intentTapLeafScript: deserializeTapLeaf(o.intentTapLeafScript),
27
- extraWitness: o.extraWitness?.map((w) => fromHex(w)),
34
+ extraWitness: o.extraWitness?.map(fromHex),
35
+ });
36
+ const deserializeUtxo = (o) => ({
37
+ ...o,
38
+ tapTree: fromHex(o.tapTree),
39
+ forfeitTapLeafScript: deserializeTapLeaf(o.forfeitTapLeafScript),
40
+ intentTapLeafScript: deserializeTapLeaf(o.intentTapLeafScript),
41
+ extraWitness: o.extraWitness?.map(fromHex),
28
42
  });
29
43
  export class WalletRepositoryImpl {
30
44
  constructor(storage) {
31
45
  this.storage = storage;
32
46
  this.cache = {
33
47
  vtxos: new Map(),
48
+ utxos: new Map(),
34
49
  transactions: new Map(),
35
50
  walletState: null,
36
51
  initialized: new Set(),
@@ -83,6 +98,53 @@ export class WalletRepositoryImpl {
83
98
  this.cache.vtxos.set(address, []);
84
99
  await this.storage.removeItem(`vtxos:${address}`);
85
100
  }
101
+ async getUtxos(address) {
102
+ const cacheKey = `utxos:${address}`;
103
+ if (this.cache.utxos.has(address)) {
104
+ return this.cache.utxos.get(address);
105
+ }
106
+ const stored = await this.storage.getItem(cacheKey);
107
+ if (!stored) {
108
+ this.cache.utxos.set(address, []);
109
+ return [];
110
+ }
111
+ try {
112
+ const parsed = JSON.parse(stored);
113
+ const utxos = parsed.map(deserializeUtxo);
114
+ this.cache.utxos.set(address, utxos.slice());
115
+ return utxos.slice();
116
+ }
117
+ catch (error) {
118
+ console.error(`Failed to parse UTXOs for address ${address}:`, error);
119
+ this.cache.utxos.set(address, []);
120
+ return [];
121
+ }
122
+ }
123
+ async saveUtxos(address, utxos) {
124
+ const storedUtxos = await this.getUtxos(address);
125
+ utxos.forEach((utxo) => {
126
+ const existing = storedUtxos.findIndex((u) => u.txid === utxo.txid && u.vout === utxo.vout);
127
+ if (existing !== -1) {
128
+ storedUtxos[existing] = utxo;
129
+ }
130
+ else {
131
+ storedUtxos.push(utxo);
132
+ }
133
+ });
134
+ this.cache.utxos.set(address, storedUtxos.slice());
135
+ await this.storage.setItem(`utxos:${address}`, JSON.stringify(storedUtxos.map(serializeUtxo)));
136
+ }
137
+ async removeUtxo(address, utxoId) {
138
+ const utxos = await this.getUtxos(address);
139
+ const [txid, vout] = utxoId.split(":");
140
+ const filtered = utxos.filter((v) => !(v.txid === txid && v.vout === parseInt(vout, 10)));
141
+ this.cache.utxos.set(address, filtered.slice());
142
+ await this.storage.setItem(`utxos:${address}`, JSON.stringify(filtered.map(serializeUtxo)));
143
+ }
144
+ async clearUtxos(address) {
145
+ this.cache.utxos.set(address, []);
146
+ await this.storage.removeItem(`utxos:${address}`);
147
+ }
86
148
  async getTransactionHistory(address) {
87
149
  const cacheKey = `tx:${address}`;
88
150
  if (this.cache.transactions.has(address)) {
@@ -4,7 +4,7 @@ import { PSBTOutput } from "@scure/btc-signer/psbt.js";
4
4
  import { hex } from "@scure/base";
5
5
  import { ArkAddress } from './address.js';
6
6
  import { ConditionCSVMultisigTapscript, CSVMultisigTapscript, } from './tapscript.js';
7
- const TapTreeCoder = PSBTOutput.tapTree[2];
7
+ export const TapTreeCoder = PSBTOutput.tapTree[2];
8
8
  export function scriptFromTapLeafScript(leaf) {
9
9
  return leaf[1].subarray(0, leaf[1].length - 1); // remove the version byte
10
10
  }
@@ -24,7 +24,16 @@ export class VtxoScript {
24
24
  }
25
25
  constructor(scripts) {
26
26
  this.scripts = scripts;
27
- const tapTree = taprootListToTree(scripts.map((script) => ({ script, leafVersion: TAP_LEAF_VERSION })));
27
+ // reverse the scripts if the number of scripts is odd
28
+ // this is to be compatible with arkd algorithm computing taproot tree from list of tapscripts
29
+ // the scripts must be reversed only HERE while we compute the tweaked public key
30
+ // but the original order should be preserved while encoding as taptree
31
+ // note: .slice().reverse() is used instead of .reverse() to avoid mutating the original array
32
+ const list = scripts.length % 2 !== 0 ? scripts.slice().reverse() : scripts;
33
+ const tapTree = taprootListToTree(list.map((script) => ({
34
+ script,
35
+ leafVersion: TAP_LEAF_VERSION,
36
+ })));
28
37
  const payment = p2tr(TAPROOT_UNSPENDABLE_KEY, tapTree, undefined, true);
29
38
  if (!payment.tapLeafScript ||
30
39
  payment.tapLeafScript.length !== scripts.length) {
@@ -1,11 +1,12 @@
1
1
  import { schnorr } from "@noble/curves/secp256k1.js";
2
2
  import { hex } from "@scure/base";
3
- import { DEFAULT_SEQUENCE, Transaction, SigHash } from "@scure/btc-signer";
3
+ import { DEFAULT_SEQUENCE, SigHash } from "@scure/btc-signer";
4
4
  import { tapLeafHash } from "@scure/btc-signer/payment.js";
5
5
  import { CLTVMultisigTapscript, decodeTapscript, } from '../script/tapscript.js';
6
6
  import { scriptFromTapLeafScript, VtxoScript, } from '../script/base.js';
7
7
  import { P2A } from './anchor.js';
8
8
  import { setArkPsbtField, VtxoTaprootTree } from './unknownFields.js';
9
+ import { Transaction } from './transaction.js';
9
10
  /**
10
11
  * Builds an offchain transaction with checkpoint transactions.
11
12
  *
@@ -45,8 +46,6 @@ function buildVirtualTx(inputs, outputs) {
45
46
  }
46
47
  const tx = new Transaction({
47
48
  version: 3,
48
- allowUnknown: true,
49
- allowUnknownOutputs: true,
50
49
  lockTime: Number(lockTime),
51
50
  });
52
51
  for (const [i, input] of inputs.entries()) {
@@ -71,8 +70,7 @@ function buildVirtualTx(inputs, outputs) {
71
70
  }
72
71
  function buildCheckpointTx(vtxo, serverUnrollScript) {
73
72
  // create the checkpoint vtxo script from collaborative closure
74
- const collaborativeClosure = decodeTapscript(vtxo.checkpointTapLeafScript ??
75
- scriptFromTapLeafScript(vtxo.tapLeafScript));
73
+ const collaborativeClosure = decodeTapscript(scriptFromTapLeafScript(vtxo.tapLeafScript));
76
74
  // create the checkpoint vtxo script combining collaborative closure and server unroll script
77
75
  const checkpointVtxoScript = new VtxoScript([
78
76
  serverUnrollScript.script,
@@ -0,0 +1,24 @@
1
+ import { Transaction as BtcSignerTransaction } from "@scure/btc-signer";
2
+ /**
3
+ * Transaction is a wrapper around the @scure/btc-signer Transaction class.
4
+ * It adds the Ark protocol specific options to the transaction.
5
+ */
6
+ export class Transaction extends BtcSignerTransaction {
7
+ constructor(opts) {
8
+ super(withArkOpts(opts));
9
+ }
10
+ static fromPSBT(psbt_, opts) {
11
+ return BtcSignerTransaction.fromPSBT(psbt_, withArkOpts(opts));
12
+ }
13
+ static fromRaw(raw, opts) {
14
+ return BtcSignerTransaction.fromRaw(raw, withArkOpts(opts));
15
+ }
16
+ }
17
+ Transaction.ARK_TX_OPTS = {
18
+ allowUnknown: true,
19
+ allowUnknownOutputs: true,
20
+ allowUnknownInputs: true,
21
+ };
22
+ function withArkOpts(opts) {
23
+ return { ...Transaction.ARK_TX_OPTS, ...opts };
24
+ }
@@ -1,8 +1,9 @@
1
- import { Transaction, p2tr } from "@scure/btc-signer";
1
+ import { p2tr } from "@scure/btc-signer";
2
2
  import { getNetwork } from '../networks.js';
3
3
  import { ESPLORA_URL, EsploraProvider, } from '../providers/onchain.js';
4
4
  import { findP2AOutput, P2A } from '../utils/anchor.js';
5
5
  import { TxWeightEstimator } from '../utils/txSizeEstimator.js';
6
+ import { Transaction } from '../utils/transaction.js';
6
7
  /**
7
8
  * Onchain Bitcoin wallet implementation for traditional Bitcoin transactions.
8
9
  *
@@ -104,9 +105,8 @@ export class OnchainWallet {
104
105
  async bumpP2A(parent) {
105
106
  const parentVsize = parent.vsize;
106
107
  let child = new Transaction({
107
- allowUnknownInputs: true,
108
- allowLegacyWitnessUtxo: true,
109
108
  version: 3,
109
+ allowLegacyWitnessUtxo: true,
110
110
  });
111
111
  child.addInput(findP2AOutput(parent)); // throws if not found
112
112
  const childVsize = TxWeightEstimator.create()
@@ -1,6 +1,6 @@
1
1
  /// <reference lib="webworker" />
2
2
  import { SingleKey } from '../../identity/singleKey.js';
3
- import { isRecoverable, isSpendable, isSubdust } from '../index.js';
3
+ import { isRecoverable, isSpendable, isSubdust, } from '../index.js';
4
4
  import { Wallet } from '../wallet.js';
5
5
  import { Request } from './request.js';
6
6
  import { Response } from './response.js';
@@ -10,7 +10,7 @@ import { RestIndexerProvider } from '../../providers/indexer.js';
10
10
  import { hex } from "@scure/base";
11
11
  import { IndexedDBStorageAdapter } from '../../storage/indexedDB.js';
12
12
  import { WalletRepositoryImpl, } from '../../repositories/walletRepository.js';
13
- import { extendVirtualCoin } from '../utils.js';
13
+ import { extendCoin, extendVirtualCoin } from '../utils.js';
14
14
  import { DEFAULT_DB_NAME } from './utils.js';
15
15
  /**
16
16
  * Worker is a class letting to interact with ServiceWorkerWallet from the client
@@ -57,6 +57,15 @@ export class Worker {
57
57
  spent: allVtxos.filter((vtxo) => !isSpendable(vtxo)),
58
58
  };
59
59
  }
60
+ /**
61
+ * Get all boarding utxos from wallet repository
62
+ */
63
+ async getAllBoardingUtxos() {
64
+ if (!this.wallet)
65
+ return [];
66
+ const address = await this.wallet.getBoardingAddress();
67
+ return await this.walletRepository.getUtxos(address);
68
+ }
60
69
  async start(withServiceWorkerUpdate = true) {
61
70
  self.addEventListener("message", async (event) => {
62
71
  await this.handleMessage(event);
@@ -130,6 +139,14 @@ export class Worker {
130
139
  this.sendMessageToAllClients(Response.vtxoUpdate(newVtxos, spentVtxos));
131
140
  }
132
141
  if (funds.type === "utxo") {
142
+ const newUtxos = funds.coins.map((utxo) => extendCoin(this.wallet, utxo));
143
+ if (newUtxos.length === 0) {
144
+ this.sendMessageToAllClients(Response.utxoUpdate([]));
145
+ return;
146
+ }
147
+ const boardingAddress = await this.wallet?.getBoardingAddress();
148
+ // save utxos using unified repository
149
+ await this.walletRepository.saveUtxos(boardingAddress, newUtxos);
133
150
  // notify all clients about the utxo update
134
151
  this.sendMessageToAllClients(Response.utxoUpdate(funds.coins));
135
152
  }
@@ -288,7 +305,7 @@ export class Worker {
288
305
  }
289
306
  try {
290
307
  const [boardingUtxos, spendableVtxos, sweptVtxos] = await Promise.all([
291
- this.wallet.getBoardingUtxos(),
308
+ this.getAllBoardingUtxos(),
292
309
  this.getSpendableVtxos(),
293
310
  this.getSweptVtxos(),
294
311
  ]);
@@ -393,7 +410,7 @@ export class Worker {
393
410
  return;
394
411
  }
395
412
  try {
396
- const boardingUtxos = await this.wallet.getBoardingUtxos();
413
+ const boardingUtxos = await this.getAllBoardingUtxos();
397
414
  event.source?.postMessage(Response.boardingUtxos(message.id, boardingUtxos));
398
415
  }
399
416
  catch (error) {