@arkade-os/sdk 0.3.0-alpha.7 → 0.3.0

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 (109) hide show
  1. package/README.md +99 -14
  2. package/dist/cjs/adapters/expo.js +8 -0
  3. package/dist/cjs/arknote/index.js +3 -3
  4. package/dist/cjs/forfeit.js +2 -2
  5. package/dist/cjs/identity/singleKey.js +8 -8
  6. package/dist/cjs/index.js +14 -5
  7. package/dist/cjs/{bip322 → intent}/index.js +38 -61
  8. package/dist/cjs/musig2/index.js +2 -1
  9. package/dist/cjs/musig2/nonces.js +4 -0
  10. package/dist/cjs/providers/ark.js +76 -45
  11. package/dist/cjs/providers/errors.js +59 -0
  12. package/dist/cjs/providers/expoArk.js +82 -0
  13. package/dist/cjs/providers/expoIndexer.js +105 -0
  14. package/dist/cjs/providers/expoUtils.js +124 -0
  15. package/dist/cjs/providers/indexer.js +3 -1
  16. package/dist/cjs/providers/onchain.js +19 -20
  17. package/dist/cjs/repositories/walletRepository.js +64 -28
  18. package/dist/cjs/script/base.js +15 -7
  19. package/dist/cjs/script/tapscript.js +20 -21
  20. package/dist/cjs/script/vhtlc.js +2 -2
  21. package/dist/cjs/tree/signingSession.js +44 -11
  22. package/dist/cjs/tree/txTree.js +3 -4
  23. package/dist/cjs/tree/validation.js +2 -3
  24. package/dist/cjs/utils/arkTransaction.js +118 -15
  25. package/dist/cjs/utils/transaction.js +28 -0
  26. package/dist/cjs/utils/unknownFields.js +7 -7
  27. package/dist/cjs/wallet/index.js +1 -1
  28. package/dist/cjs/wallet/onchain.js +6 -7
  29. package/dist/cjs/wallet/serviceWorker/response.js +32 -0
  30. package/dist/cjs/wallet/serviceWorker/utils.js +2 -9
  31. package/dist/cjs/wallet/serviceWorker/wallet.js +7 -8
  32. package/dist/cjs/wallet/serviceWorker/worker.js +48 -32
  33. package/dist/cjs/wallet/unroll.js +7 -9
  34. package/dist/cjs/wallet/utils.js +20 -0
  35. package/dist/cjs/wallet/vtxo-manager.js +323 -0
  36. package/dist/cjs/wallet/wallet.js +165 -174
  37. package/dist/esm/adapters/expo.js +3 -0
  38. package/dist/esm/arknote/index.js +2 -2
  39. package/dist/esm/forfeit.js +1 -1
  40. package/dist/esm/identity/singleKey.js +9 -9
  41. package/dist/esm/index.js +14 -10
  42. package/dist/esm/{bip322 → intent}/index.js +32 -54
  43. package/dist/esm/musig2/index.js +1 -1
  44. package/dist/esm/musig2/nonces.js +3 -0
  45. package/dist/esm/providers/ark.js +76 -45
  46. package/dist/esm/providers/errors.js +54 -0
  47. package/dist/esm/providers/expoArk.js +78 -0
  48. package/dist/esm/providers/expoIndexer.js +101 -0
  49. package/dist/esm/providers/expoUtils.js +87 -0
  50. package/dist/esm/providers/indexer.js +3 -1
  51. package/dist/esm/providers/onchain.js +19 -20
  52. package/dist/esm/repositories/walletRepository.js +64 -28
  53. package/dist/esm/script/base.js +12 -4
  54. package/dist/esm/script/tapscript.js +1 -2
  55. package/dist/esm/script/vhtlc.js +1 -1
  56. package/dist/esm/tree/signingSession.js +45 -12
  57. package/dist/esm/tree/txTree.js +3 -4
  58. package/dist/esm/tree/validation.js +2 -3
  59. package/dist/esm/utils/arkTransaction.js +110 -9
  60. package/dist/esm/utils/transaction.js +24 -0
  61. package/dist/esm/utils/unknownFields.js +3 -3
  62. package/dist/esm/wallet/index.js +1 -1
  63. package/dist/esm/wallet/onchain.js +3 -4
  64. package/dist/esm/wallet/serviceWorker/response.js +32 -0
  65. package/dist/esm/wallet/serviceWorker/utils.js +1 -8
  66. package/dist/esm/wallet/serviceWorker/wallet.js +8 -9
  67. package/dist/esm/wallet/serviceWorker/worker.js +49 -33
  68. package/dist/esm/wallet/unroll.js +5 -7
  69. package/dist/esm/wallet/utils.js +16 -0
  70. package/dist/esm/wallet/vtxo-manager.js +317 -0
  71. package/dist/esm/wallet/wallet.js +159 -168
  72. package/dist/types/adapters/expo.d.ts +4 -0
  73. package/dist/types/arknote/index.d.ts +1 -1
  74. package/dist/types/forfeit.d.ts +2 -2
  75. package/dist/types/identity/index.d.ts +2 -2
  76. package/dist/types/identity/singleKey.d.ts +2 -2
  77. package/dist/types/index.d.ts +11 -9
  78. package/dist/types/intent/index.d.ts +41 -0
  79. package/dist/types/musig2/index.d.ts +1 -1
  80. package/dist/types/musig2/nonces.d.ts +1 -0
  81. package/dist/types/providers/ark.d.ts +197 -27
  82. package/dist/types/providers/errors.d.ts +13 -0
  83. package/dist/types/providers/expoArk.d.ts +22 -0
  84. package/dist/types/providers/expoIndexer.d.ts +18 -0
  85. package/dist/types/providers/expoUtils.d.ts +18 -0
  86. package/dist/types/providers/indexer.d.ts +8 -8
  87. package/dist/types/providers/onchain.d.ts +6 -2
  88. package/dist/types/repositories/walletRepository.d.ts +9 -5
  89. package/dist/types/script/base.d.ts +5 -2
  90. package/dist/types/tree/signingSession.d.ts +16 -11
  91. package/dist/types/utils/anchor.d.ts +2 -2
  92. package/dist/types/utils/arkTransaction.d.ts +15 -5
  93. package/dist/types/utils/transaction.d.ts +13 -0
  94. package/dist/types/utils/unknownFields.d.ts +4 -4
  95. package/dist/types/wallet/index.d.ts +47 -7
  96. package/dist/types/wallet/onchain.d.ts +1 -1
  97. package/dist/types/wallet/serviceWorker/response.d.ts +16 -2
  98. package/dist/types/wallet/serviceWorker/utils.d.ts +1 -2
  99. package/dist/types/wallet/serviceWorker/wallet.d.ts +2 -2
  100. package/dist/types/wallet/serviceWorker/worker.d.ts +7 -1
  101. package/dist/types/wallet/unroll.d.ts +1 -1
  102. package/dist/types/wallet/utils.d.ts +3 -0
  103. package/dist/types/wallet/vtxo-manager.d.ts +179 -0
  104. package/dist/types/wallet/wallet.d.ts +17 -5
  105. package/package.json +11 -3
  106. package/dist/cjs/bip322/errors.js +0 -13
  107. package/dist/esm/bip322/errors.js +0 -9
  108. package/dist/types/bip322/errors.d.ts +0 -6
  109. package/dist/types/bip322/index.d.ts +0 -57
@@ -1,7 +1,6 @@
1
1
  import { Transaction } from "@scure/btc-signer/transaction.js";
2
2
  import { base64 } from "@scure/base";
3
3
  import { hex } from "@scure/base";
4
- import { sha256x2 } from "@scure/btc-signer/utils.js";
5
4
  /**
6
5
  * TxTree is a graph of bitcoin transactions.
7
6
  * It is used to represent batch tree created during settlement session
@@ -19,7 +18,7 @@ export class TxTree {
19
18
  const chunksByTxid = new Map();
20
19
  for (const chunk of chunks) {
21
20
  const decodedChunk = decodeNode(chunk);
22
- const txid = hex.encode(sha256x2(decodedChunk.tx.toBytes(true)).reverse());
21
+ const txid = decodedChunk.tx.id;
23
22
  chunksByTxid.set(txid, decodedChunk);
24
23
  }
25
24
  // Find the root chunks (the ones that aren't referenced as a child)
@@ -88,7 +87,7 @@ export class TxTree {
88
87
  }
89
88
  child.validate();
90
89
  const childInput = child.root.getInput(0);
91
- const parentTxid = hex.encode(sha256x2(this.root.toBytes(true)).reverse());
90
+ const parentTxid = this.root.id;
92
91
  // verify the input of the child is the output of the parent
93
92
  if (!childInput.txid ||
94
93
  hex.encode(childInput.txid) !== parentTxid ||
@@ -123,7 +122,7 @@ export class TxTree {
123
122
  return leaves;
124
123
  }
125
124
  get txid() {
126
- return hex.encode(sha256x2(this.root.toBytes(true)).reverse());
125
+ return this.root.id;
127
126
  }
128
127
  find(txid) {
129
128
  if (txid === this.txid) {
@@ -1,7 +1,6 @@
1
1
  import { hex } from "@scure/base";
2
2
  import { Transaction } from "@scure/btc-signer/transaction.js";
3
3
  import { base64 } from "@scure/base";
4
- import { sha256x2 } from "@scure/btc-signer/utils.js";
5
4
  import { aggregateKeys } from '../musig2/index.js';
6
5
  import { CosignerPublicKey, getArkPsbtFields } from '../utils/unknownFields.js';
7
6
  export const ErrInvalidSettlementTx = (tx) => new Error(`invalid settlement transaction: ${tx}`);
@@ -25,7 +24,7 @@ export function validateConnectorsTxGraph(settlementTxB64, connectorsGraph) {
25
24
  const settlementTx = Transaction.fromPSBT(base64.decode(settlementTxB64));
26
25
  if (settlementTx.outputsLength <= BATCH_OUTPUT_CONNECTORS_INDEX)
27
26
  throw ErrInvalidSettlementTxOutputs;
28
- const expectedRootTxid = hex.encode(sha256x2(settlementTx.toBytes(true)).reverse());
27
+ const expectedRootTxid = settlementTx.id;
29
28
  if (!rootInput.txid)
30
29
  throw ErrWrongSettlementTxid;
31
30
  if (hex.encode(rootInput.txid) !== expectedRootTxid)
@@ -52,7 +51,7 @@ export function validateVtxoTxGraph(graph, roundTransaction, sweepTapTreeRoot) {
52
51
  throw ErrEmptyTree;
53
52
  }
54
53
  const rootInput = graph.root.getInput(0);
55
- const commitmentTxid = hex.encode(sha256x2(roundTransaction.toBytes(true)).reverse());
54
+ const commitmentTxid = roundTransaction.id;
56
55
  if (!rootInput.txid ||
57
56
  hex.encode(rootInput.txid) !== commitmentTxid ||
58
57
  rootInput.index !== BATCH_OUTPUT_VTXO_INDEX) {
@@ -1,10 +1,12 @@
1
- import { DEFAULT_SEQUENCE, Transaction, } from "@scure/btc-signer/transaction.js";
2
- import { CLTVMultisigTapscript, decodeTapscript } from '../script/tapscript.js';
1
+ import { schnorr } from "@noble/curves/secp256k1.js";
2
+ import { hex } from "@scure/base";
3
+ import { DEFAULT_SEQUENCE, SigHash } from "@scure/btc-signer";
4
+ import { tapLeafHash } from "@scure/btc-signer/payment.js";
5
+ import { CLTVMultisigTapscript, decodeTapscript, } from '../script/tapscript.js';
3
6
  import { scriptFromTapLeafScript, VtxoScript, } from '../script/base.js';
4
7
  import { P2A } from './anchor.js';
5
- import { hex } from "@scure/base";
6
- import { sha256x2 } from "@scure/btc-signer/utils.js";
7
8
  import { setArkPsbtField, VtxoTaprootTree } from './unknownFields.js';
9
+ import { Transaction } from './transaction.js';
8
10
  /**
9
11
  * Builds an offchain transaction with checkpoint transactions.
10
12
  *
@@ -44,8 +46,6 @@ function buildVirtualTx(inputs, outputs) {
44
46
  }
45
47
  const tx = new Transaction({
46
48
  version: 3,
47
- allowUnknown: true,
48
- allowUnknownOutputs: true,
49
49
  lockTime: Number(lockTime),
50
50
  });
51
51
  for (const [i, input] of inputs.entries()) {
@@ -70,8 +70,7 @@ function buildVirtualTx(inputs, outputs) {
70
70
  }
71
71
  function buildCheckpointTx(vtxo, serverUnrollScript) {
72
72
  // create the checkpoint vtxo script from collaborative closure
73
- const collaborativeClosure = decodeTapscript(vtxo.checkpointTapLeafScript ??
74
- scriptFromTapLeafScript(vtxo.tapLeafScript));
73
+ const collaborativeClosure = decodeTapscript(scriptFromTapLeafScript(vtxo.tapLeafScript));
75
74
  // create the checkpoint vtxo script combining collaborative closure and server unroll script
76
75
  const checkpointVtxoScript = new VtxoScript([
77
76
  serverUnrollScript.script,
@@ -88,7 +87,7 @@ function buildCheckpointTx(vtxo, serverUnrollScript) {
88
87
  const collaborativeLeafProof = checkpointVtxoScript.findLeaf(hex.encode(collaborativeClosure.script));
89
88
  // create the checkpoint input that will be used as input of the virtual tx
90
89
  const checkpointInput = {
91
- txid: hex.encode(sha256x2(checkpointTx.toBytes(true)).reverse()),
90
+ txid: checkpointTx.id,
92
91
  vout: 0,
93
92
  value: vtxo.value,
94
93
  tapLeafScript: collaborativeLeafProof,
@@ -103,3 +102,105 @@ const nLocktimeMinSeconds = 500000000n;
103
102
  function isSeconds(locktime) {
104
103
  return locktime >= nLocktimeMinSeconds;
105
104
  }
105
+ export function hasBoardingTxExpired(coin, boardingTimelock) {
106
+ if (!coin.status.block_time)
107
+ return false;
108
+ if (boardingTimelock.value === 0n)
109
+ return true;
110
+ if (boardingTimelock.type !== "blocks")
111
+ return false; // TODO: handle get chain tip
112
+ // validate expiry in terms of seconds
113
+ const now = BigInt(Math.floor(Date.now() / 1000));
114
+ const blockTime = BigInt(Math.floor(coin.status.block_time));
115
+ return blockTime + boardingTimelock.value <= now;
116
+ }
117
+ /**
118
+ * Formats a sighash type as a hex string (e.g., 0x01)
119
+ */
120
+ function formatSighash(type) {
121
+ return `0x${type.toString(16).padStart(2, "0")}`;
122
+ }
123
+ /**
124
+ * Verify tapscript signatures on a transaction input
125
+ * @param tx Transaction to verify
126
+ * @param inputIndex Index of the input to verify
127
+ * @param requiredSigners List of required signer pubkeys (hex encoded)
128
+ * @param excludePubkeys List of pubkeys to exclude from verification (hex encoded, e.g., server key not yet signed)
129
+ * @param allowedSighashTypes List of allowed sighash types (defaults to [SigHash.DEFAULT])
130
+ * @throws Error if verification fails
131
+ */
132
+ export function verifyTapscriptSignatures(tx, inputIndex, requiredSigners, excludePubkeys = [], allowedSighashTypes = [SigHash.DEFAULT]) {
133
+ const input = tx.getInput(inputIndex);
134
+ // Collect prevout scripts and amounts for ALL inputs (required for preimageWitnessV1)
135
+ const prevoutScripts = [];
136
+ const prevoutAmounts = [];
137
+ for (let i = 0; i < tx.inputsLength; i++) {
138
+ const inp = tx.getInput(i);
139
+ if (!inp.witnessUtxo) {
140
+ throw new Error(`Input ${i} is missing witnessUtxo`);
141
+ }
142
+ prevoutScripts.push(inp.witnessUtxo.script);
143
+ prevoutAmounts.push(inp.witnessUtxo.amount);
144
+ }
145
+ // Verify tapScriptSig signatures
146
+ if (!input.tapScriptSig || input.tapScriptSig.length === 0) {
147
+ throw new Error(`Input ${inputIndex} is missing tapScriptSig`);
148
+ }
149
+ // Verify each signature in tapScriptSig
150
+ for (const [tapScriptSigData, signature] of input.tapScriptSig) {
151
+ const pubKey = tapScriptSigData.pubKey;
152
+ const pubKeyHex = hex.encode(pubKey);
153
+ // Skip verification for excluded pubkeys
154
+ if (excludePubkeys.includes(pubKeyHex)) {
155
+ continue;
156
+ }
157
+ // Extract sighash type from signature
158
+ // Schnorr signatures are 64 bytes, with optional 1-byte sighash appended
159
+ const sighashType = signature.length === 65 ? signature[64] : SigHash.DEFAULT;
160
+ const sig = signature.subarray(0, 64);
161
+ // Verify sighash type is allowed
162
+ if (!allowedSighashTypes.includes(sighashType)) {
163
+ const sighashName = formatSighash(sighashType);
164
+ throw new Error(`Unallowed sighash type ${sighashName} for input ${inputIndex}, pubkey ${pubKeyHex}.`);
165
+ }
166
+ // Find the tapLeafScript that matches this signature's leafHash
167
+ if (!input.tapLeafScript || input.tapLeafScript.length === 0) {
168
+ throw new Error();
169
+ }
170
+ // Search for the leaf that matches the leafHash in tapScriptSigData
171
+ const leafHash = tapScriptSigData.leafHash;
172
+ const leafHashHex = hex.encode(leafHash);
173
+ let matchingScript;
174
+ let matchingVersion;
175
+ for (const [_, scriptWithVersion] of input.tapLeafScript) {
176
+ const script = scriptWithVersion.subarray(0, -1);
177
+ const version = scriptWithVersion[scriptWithVersion.length - 1];
178
+ // Compute the leaf hash for this script and compare as hex strings
179
+ const computedLeafHash = tapLeafHash(script, version);
180
+ const computedHex = hex.encode(computedLeafHash);
181
+ if (computedHex === leafHashHex) {
182
+ matchingScript = script;
183
+ matchingVersion = version;
184
+ break;
185
+ }
186
+ }
187
+ if (!matchingScript || matchingVersion === undefined) {
188
+ throw new Error(`Input ${inputIndex}: No tapLeafScript found matching leafHash ${hex.encode(leafHash)}`);
189
+ }
190
+ // Reconstruct the message that was signed
191
+ // Note: preimageWitnessV1 requires ALL input prevout scripts and amounts
192
+ const message = tx.preimageWitnessV1(inputIndex, prevoutScripts, sighashType, prevoutAmounts, undefined, matchingScript, matchingVersion);
193
+ // Verify the schnorr signature
194
+ const isValid = schnorr.verify(sig, message, pubKey);
195
+ if (!isValid) {
196
+ throw new Error(`Invalid signature for input ${inputIndex}, pubkey ${pubKeyHex}`);
197
+ }
198
+ }
199
+ // Verify we have signatures from all required signers (excluding those we're skipping)
200
+ const signedPubkeys = input.tapScriptSig.map(([data]) => hex.encode(data.pubKey));
201
+ const requiredNotExcluded = requiredSigners.filter((pk) => !excludePubkeys.includes(pk));
202
+ const missingSigners = requiredNotExcluded.filter((pk) => !signedPubkeys.includes(pk));
203
+ if (missingSigners.length > 0) {
204
+ throw new Error(`Missing signatures from: ${missingSigners.map((pk) => pk.slice(0, 16)).join(", ")}...`);
205
+ }
206
+ }
@@ -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,5 +1,5 @@
1
1
  import * as bip68 from "bip68";
2
- import { RawWitness, ScriptNum } from "@scure/btc-signer/script.js";
2
+ import { RawWitness, ScriptNum } from "@scure/btc-signer";
3
3
  import { hex } from "@scure/base";
4
4
  /**
5
5
  * ArkPsbtFieldKey is the key values for ark psbt fields.
@@ -13,9 +13,9 @@ export var ArkPsbtFieldKey;
13
13
  })(ArkPsbtFieldKey || (ArkPsbtFieldKey = {}));
14
14
  /**
15
15
  * ArkPsbtFieldKeyType is the type of the ark psbt field key.
16
- * Every ark psbt field has key type 255.
16
+ * Every ark psbt field has key type 222.
17
17
  */
18
- export const ArkPsbtFieldKeyType = 255;
18
+ export const ArkPsbtFieldKeyType = 222;
19
19
  /**
20
20
  * setArkPsbtField appends a new unknown field to the input at inputIndex
21
21
  *
@@ -4,7 +4,7 @@ export var TxType;
4
4
  TxType["TxReceived"] = "RECEIVED";
5
5
  })(TxType || (TxType = {}));
6
6
  export function isSpendable(vtxo) {
7
- return vtxo.spentBy === undefined || vtxo.spentBy === "";
7
+ return !vtxo.isSpent;
8
8
  }
9
9
  export function isRecoverable(vtxo) {
10
10
  return vtxo.virtualStatus.state === "swept" && isSpendable(vtxo);
@@ -1,9 +1,9 @@
1
- import { p2tr } from "@scure/btc-signer/payment.js";
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
- import { Transaction } from "@scure/btc-signer/transaction.js";
5
4
  import { findP2AOutput, P2A } from '../utils/anchor.js';
6
5
  import { TxWeightEstimator } from '../utils/txSizeEstimator.js';
6
+ import { Transaction } from '../utils/transaction.js';
7
7
  /**
8
8
  * Onchain Bitcoin wallet implementation for traditional Bitcoin transactions.
9
9
  *
@@ -105,9 +105,8 @@ export class OnchainWallet {
105
105
  async bumpP2A(parent) {
106
106
  const parentVsize = parent.vsize;
107
107
  let child = new Transaction({
108
- allowUnknownInputs: true,
109
- allowLegacyWitnessUtxo: true,
110
108
  version: 3,
109
+ allowLegacyWitnessUtxo: true,
111
110
  });
112
111
  child.addInput(findP2AOutput(parent)); // throws if not found
113
112
  const childVsize = TxWeightEstimator.create()
@@ -1,3 +1,8 @@
1
+ import { hex } from "@scure/base";
2
+ function getRandomId() {
3
+ const randomValue = crypto.getRandomValues(new Uint8Array(16));
4
+ return hex.encode(randomValue);
5
+ }
1
6
  /**
2
7
  * Response is the namespace that contains the response types for the service worker.
3
8
  */
@@ -184,4 +189,31 @@ export var Response;
184
189
  };
185
190
  }
186
191
  Response.walletReloaded = walletReloaded;
192
+ function isVtxoUpdate(response) {
193
+ return response.type === "VTXO_UPDATE";
194
+ }
195
+ Response.isVtxoUpdate = isVtxoUpdate;
196
+ function vtxoUpdate(newVtxos, spentVtxos) {
197
+ return {
198
+ type: "VTXO_UPDATE",
199
+ id: getRandomId(), // spontaneous update, not tied to a request
200
+ success: true,
201
+ spentVtxos,
202
+ newVtxos,
203
+ };
204
+ }
205
+ Response.vtxoUpdate = vtxoUpdate;
206
+ function isUtxoUpdate(response) {
207
+ return response.type === "UTXO_UPDATE";
208
+ }
209
+ Response.isUtxoUpdate = isUtxoUpdate;
210
+ function utxoUpdate(coins) {
211
+ return {
212
+ type: "UTXO_UPDATE",
213
+ id: getRandomId(), // spontaneous update, not tied to a request
214
+ success: true,
215
+ coins,
216
+ };
217
+ }
218
+ Response.utxoUpdate = utxoUpdate;
187
219
  })(Response || (Response = {}));
@@ -1,3 +1,4 @@
1
+ export const DEFAULT_DB_NAME = "arkade-service-worker";
1
2
  /**
2
3
  * setupServiceWorker sets up the service worker.
3
4
  * @param path - the path to the service worker script
@@ -44,11 +45,3 @@ export async function setupServiceWorker(path) {
44
45
  navigator.serviceWorker.addEventListener("error", onError);
45
46
  });
46
47
  }
47
- export function extendVirtualCoin(wallet, vtxo) {
48
- return {
49
- ...vtxo,
50
- forfeitTapLeafScript: wallet.offchainTapscript.forfeit(),
51
- intentTapLeafScript: wallet.offchainTapscript.exit(),
52
- tapTree: wallet.offchainTapscript.encode(),
53
- };
54
- }
@@ -3,7 +3,7 @@ import { hex } from "@scure/base";
3
3
  import { IndexedDBStorageAdapter } from '../../storage/indexedDB.js';
4
4
  import { WalletRepositoryImpl } from '../../repositories/walletRepository.js';
5
5
  import { ContractRepositoryImpl } from '../../repositories/contractRepository.js';
6
- import { setupServiceWorker } from './utils.js';
6
+ import { DEFAULT_DB_NAME, setupServiceWorker } from './utils.js';
7
7
  const isPrivateKeyIdentity = (identity) => {
8
8
  return typeof identity.toHex === "function";
9
9
  };
@@ -22,7 +22,7 @@ export class ServiceWorkerWallet {
22
22
  }
23
23
  static async create(options) {
24
24
  // Default to IndexedDB for service worker context
25
- const storage = options.storage || new IndexedDBStorageAdapter("wallet-db");
25
+ const storage = new IndexedDBStorageAdapter(options.dbName || DEFAULT_DB_NAME, options.dbVersion);
26
26
  // Create repositories
27
27
  const walletRepo = new WalletRepositoryImpl(storage);
28
28
  const contractRepo = new ContractRepositoryImpl(storage);
@@ -31,7 +31,7 @@ export class ServiceWorkerWallet {
31
31
  ? options.identity
32
32
  : null;
33
33
  if (!identity) {
34
- throw new Error("ServiceWorkerWallet.create() requires a Identity that can expose its private key");
34
+ throw new Error("ServiceWorkerWallet.create() requires a Identity that can expose a single private key");
35
35
  }
36
36
  // Extract private key for service worker initialization
37
37
  const privateKey = identity.toHex();
@@ -74,13 +74,9 @@ export class ServiceWorkerWallet {
74
74
  // Register and setup the service worker
75
75
  const serviceWorker = await setupServiceWorker(options.serviceWorkerPath);
76
76
  // Use the existing create method
77
- return await ServiceWorkerWallet.create({
78
- arkServerPublicKey: options.arkServerPublicKey,
79
- arkServerUrl: options.arkServerUrl,
80
- esploraUrl: options.esploraUrl,
81
- identity: options.identity,
77
+ return ServiceWorkerWallet.create({
78
+ ...options,
82
79
  serviceWorker,
83
- storage: options.storage,
84
80
  });
85
81
  }
86
82
  // send a message and wait for a response
@@ -257,6 +253,9 @@ export class ServiceWorkerWallet {
257
253
  return new Promise((resolve, reject) => {
258
254
  const messageHandler = (event) => {
259
255
  const response = event.data;
256
+ if (response.id !== message.id) {
257
+ return;
258
+ }
260
259
  if (!response.success) {
261
260
  navigator.serviceWorker.removeEventListener("message", messageHandler);
262
261
  reject(new Error(response.message));
@@ -1,6 +1,6 @@
1
1
  /// <reference lib="webworker" />
2
2
  import { SingleKey } from '../../identity/singleKey.js';
3
- import { 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,15 +10,18 @@ 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
+ import { DEFAULT_DB_NAME } from './utils.js';
14
15
  /**
15
16
  * Worker is a class letting to interact with ServiceWorkerWallet from the client
16
17
  * it aims to be run in a service worker context
17
18
  */
18
19
  export class Worker {
19
- constructor(messageCallback = () => { }) {
20
+ constructor(dbName = DEFAULT_DB_NAME, dbVersion = 1, messageCallback = () => { }) {
21
+ this.dbName = dbName;
22
+ this.dbVersion = dbVersion;
20
23
  this.messageCallback = messageCallback;
21
- this.storage = new IndexedDBStorageAdapter("arkade-service-worker", 1);
24
+ this.storage = new IndexedDBStorageAdapter(dbName, dbVersion);
22
25
  this.walletRepository = new WalletRepositoryImpl(this.storage);
23
26
  }
24
27
  /**
@@ -54,6 +57,15 @@ export class Worker {
54
57
  spent: allVtxos.filter((vtxo) => !isSpendable(vtxo)),
55
58
  };
56
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
+ }
57
69
  async start(withServiceWorkerUpdate = true) {
58
70
  self.addEventListener("message", async (event) => {
59
71
  await this.handleMessage(event);
@@ -74,6 +86,8 @@ export class Worker {
74
86
  this.incomingFundsSubscription();
75
87
  // Clear storage - this replaces vtxoRepository.close()
76
88
  await this.storage.clear();
89
+ // Reset in-memory caches by recreating the repository
90
+ this.walletRepository = new WalletRepositoryImpl(this.storage);
77
91
  this.wallet = undefined;
78
92
  this.arkProvider = undefined;
79
93
  this.indexerProvider = undefined;
@@ -102,7 +116,7 @@ export class Worker {
102
116
  const txs = await this.wallet.getTransactionHistory();
103
117
  if (txs)
104
118
  await this.walletRepository.saveTransactions(address, txs);
105
- // stop previous subscriptions if any
119
+ // unsubscribe previous subscription if any
106
120
  if (this.incomingFundsSubscription)
107
121
  this.incomingFundsSubscription();
108
122
  // subscribe for incoming funds and notify all clients when new funds arrive
@@ -122,11 +136,19 @@ export class Worker {
122
136
  ...spentVtxos,
123
137
  ]);
124
138
  // notify all clients about the vtxo update
125
- this.sendMessageToAllClients("VTXO_UPDATE", JSON.stringify({ newVtxos, spentVtxos }));
139
+ this.sendMessageToAllClients(Response.vtxoUpdate(newVtxos, spentVtxos));
126
140
  }
127
- if (funds.type === "utxo" && funds.coins.length > 0) {
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);
128
150
  // notify all clients about the utxo update
129
- this.sendMessageToAllClients("UTXO_UPDATE", JSON.stringify(funds.coins));
151
+ this.sendMessageToAllClients(Response.utxoUpdate(funds.coins));
130
152
  }
131
153
  });
132
154
  }
@@ -283,7 +305,7 @@ export class Worker {
283
305
  }
284
306
  try {
285
307
  const [boardingUtxos, spendableVtxos, sweptVtxos] = await Promise.all([
286
- this.wallet.getBoardingUtxos(),
308
+ this.getAllBoardingUtxos(),
287
309
  this.getSpendableVtxos(),
288
310
  this.getSweptVtxos(),
289
311
  ]);
@@ -351,23 +373,21 @@ export class Worker {
351
373
  return;
352
374
  }
353
375
  try {
354
- let vtxos = await this.getSpendableVtxos();
355
- if (!message.filter?.withRecoverable) {
356
- if (!this.wallet)
357
- throw new Error("Wallet not initialized");
358
- // exclude subdust is we don't want recoverable
359
- const dustAmount = this.wallet?.dustAmount;
360
- vtxos =
361
- dustAmount == null
362
- ? vtxos
363
- : vtxos.filter((v) => !isSubdust(v, dustAmount));
364
- }
365
- if (message.filter?.withRecoverable) {
366
- // get also swept and spendable vtxos
367
- const sweptVtxos = await this.getSweptVtxos();
368
- vtxos.push(...sweptVtxos.filter(isSpendable));
369
- }
370
- event.source?.postMessage(Response.vtxos(message.id, vtxos));
376
+ const vtxos = await this.getSpendableVtxos();
377
+ const dustAmount = this.wallet.dustAmount;
378
+ const includeRecoverable = message.filter?.withRecoverable ?? false;
379
+ const filteredVtxos = includeRecoverable
380
+ ? vtxos
381
+ : vtxos.filter((v) => {
382
+ if (dustAmount != null && isSubdust(v, dustAmount)) {
383
+ return false;
384
+ }
385
+ if (isRecoverable(v)) {
386
+ return false;
387
+ }
388
+ return true;
389
+ });
390
+ event.source?.postMessage(Response.vtxos(message.id, filteredVtxos));
371
391
  }
372
392
  catch (error) {
373
393
  console.error("Error getting vtxos:", error);
@@ -390,7 +410,7 @@ export class Worker {
390
410
  return;
391
411
  }
392
412
  try {
393
- const boardingUtxos = await this.wallet.getBoardingUtxos();
413
+ const boardingUtxos = await this.getAllBoardingUtxos();
394
414
  event.source?.postMessage(Response.boardingUtxos(message.id, boardingUtxos));
395
415
  }
396
416
  catch (error) {
@@ -512,21 +532,17 @@ export class Worker {
512
532
  event.source?.postMessage(Response.error(message.id, "Unknown message type"));
513
533
  }
514
534
  }
515
- async sendMessageToAllClients(type, message) {
535
+ async sendMessageToAllClients(message) {
516
536
  self.clients
517
537
  .matchAll({ includeUncontrolled: true, type: "window" })
518
538
  .then((clients) => {
519
539
  clients.forEach((client) => {
520
- client.postMessage({
521
- type,
522
- message,
523
- });
540
+ client.postMessage(message);
524
541
  });
525
542
  });
526
543
  }
527
544
  async handleReloadWallet(event) {
528
545
  const message = event.data;
529
- console.log("RELOAD_WALLET message received", message);
530
546
  if (!Request.isReloadWallet(message)) {
531
547
  console.error("Invalid RELOAD_WALLET message format", message);
532
548
  event.source?.postMessage(Response.error(message.id, "Invalid RELOAD_WALLET message format"));
@@ -1,10 +1,10 @@
1
- import { SigHash, Transaction } from "@scure/btc-signer/transaction.js";
2
- import { ChainTxType } from '../providers/indexer.js';
3
1
  import { base64, hex } from "@scure/base";
2
+ import { SigHash, TaprootControlBlock } from "@scure/btc-signer";
3
+ import { ChainTxType } from '../providers/indexer.js';
4
4
  import { VtxoScript } from '../script/base.js';
5
- import { TaprootControlBlock, } from "@scure/btc-signer/psbt.js";
6
5
  import { TxWeightEstimator } from '../utils/txSizeEstimator.js';
7
6
  import { Wallet } from './wallet.js';
7
+ import { Transaction } from '../utils/transaction.js';
8
8
  export var Unroll;
9
9
  (function (Unroll) {
10
10
  let StepType;
@@ -103,9 +103,7 @@ export var Unroll;
103
103
  if (virtualTxs.txs.length === 0) {
104
104
  throw new Error(`Tx ${nextTxToBroadcast.txid} not found`);
105
105
  }
106
- const tx = Transaction.fromPSBT(base64.decode(virtualTxs.txs[0]), {
107
- allowUnknownInputs: true,
108
- });
106
+ const tx = Transaction.fromPSBT(base64.decode(virtualTxs.txs[0]));
109
107
  // finalize the tree transaction
110
108
  if (nextTxToBroadcast.type === ChainTxType.TREE) {
111
109
  const input = tx.getInput(0);
@@ -198,7 +196,7 @@ export var Unroll;
198
196
  });
199
197
  txWeightEstimator.addTapscriptInput(64, spendingLeaf[1].length, TaprootControlBlock.encode(spendingLeaf[0]).length);
200
198
  }
201
- const tx = new Transaction({ allowUnknownInputs: true, version: 2 });
199
+ const tx = new Transaction({ version: 2 });
202
200
  for (const input of inputs) {
203
201
  tx.addInput(input);
204
202
  }
@@ -0,0 +1,16 @@
1
+ export function extendVirtualCoin(wallet, vtxo) {
2
+ return {
3
+ ...vtxo,
4
+ forfeitTapLeafScript: wallet.offchainTapscript.forfeit(),
5
+ intentTapLeafScript: wallet.offchainTapscript.exit(),
6
+ tapTree: wallet.offchainTapscript.encode(),
7
+ };
8
+ }
9
+ export function extendCoin(wallet, utxo) {
10
+ return {
11
+ ...utxo,
12
+ forfeitTapLeafScript: wallet.boardingTapscript.forfeit(),
13
+ intentTapLeafScript: wallet.boardingTapscript.exit(),
14
+ tapTree: wallet.boardingTapscript.encode(),
15
+ };
16
+ }