@arkade-os/sdk 0.4.23 → 0.4.25

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 (60) hide show
  1. package/README.md +21 -1
  2. package/dist/cjs/contracts/contractManager.js +66 -5
  3. package/dist/cjs/contracts/contractWatcher.js +9 -3
  4. package/dist/cjs/contracts/handlers/default.js +3 -2
  5. package/dist/cjs/contracts/handlers/delegate.js +3 -2
  6. package/dist/cjs/contracts/handlers/helpers.js +2 -58
  7. package/dist/cjs/contracts/handlers/vhtlc.js +7 -6
  8. package/dist/cjs/contracts/vtxoOwnership.js +78 -0
  9. package/dist/cjs/index.js +3 -3
  10. package/dist/cjs/repositories/inMemory/walletRepository.js +35 -0
  11. package/dist/cjs/repositories/indexedDB/walletRepository.js +117 -0
  12. package/dist/cjs/repositories/realm/walletRepository.js +28 -0
  13. package/dist/cjs/repositories/sqlite/walletRepository.js +23 -0
  14. package/dist/cjs/script/base.js +12 -47
  15. package/dist/cjs/script/tapscript.js +97 -73
  16. package/dist/cjs/utils/timelock.js +59 -0
  17. package/dist/cjs/utils/unknownFields.js +2 -39
  18. package/dist/cjs/wallet/serviceWorker/wallet-message-handler.js +71 -10
  19. package/dist/cjs/wallet/serviceWorker/wallet.js +10 -0
  20. package/dist/cjs/wallet/unroll.js +79 -67
  21. package/dist/cjs/wallet/vtxo-manager.js +112 -16
  22. package/dist/cjs/wallet/wallet.js +64 -8
  23. package/dist/cjs/worker/expo/processors/contractPollProcessor.js +7 -2
  24. package/dist/esm/contracts/contractManager.js +66 -5
  25. package/dist/esm/contracts/contractWatcher.js +9 -3
  26. package/dist/esm/contracts/handlers/default.js +2 -1
  27. package/dist/esm/contracts/handlers/delegate.js +2 -1
  28. package/dist/esm/contracts/handlers/helpers.js +1 -22
  29. package/dist/esm/contracts/handlers/vhtlc.js +2 -1
  30. package/dist/esm/contracts/vtxoOwnership.js +69 -0
  31. package/dist/esm/index.js +1 -1
  32. package/dist/esm/repositories/inMemory/walletRepository.js +35 -0
  33. package/dist/esm/repositories/indexedDB/walletRepository.js +117 -0
  34. package/dist/esm/repositories/realm/walletRepository.js +28 -0
  35. package/dist/esm/repositories/sqlite/walletRepository.js +23 -0
  36. package/dist/esm/script/base.js +12 -14
  37. package/dist/esm/script/tapscript.js +97 -40
  38. package/dist/esm/utils/timelock.js +22 -0
  39. package/dist/esm/utils/unknownFields.js +2 -6
  40. package/dist/esm/wallet/serviceWorker/wallet-message-handler.js +71 -10
  41. package/dist/esm/wallet/serviceWorker/wallet.js +10 -0
  42. package/dist/esm/wallet/unroll.js +78 -67
  43. package/dist/esm/wallet/vtxo-manager.js +112 -16
  44. package/dist/esm/wallet/wallet.js +62 -6
  45. package/dist/esm/worker/expo/processors/contractPollProcessor.js +7 -2
  46. package/dist/types/contracts/contractManager.d.ts +17 -1
  47. package/dist/types/contracts/handlers/helpers.d.ts +0 -9
  48. package/dist/types/contracts/vtxoOwnership.d.ts +33 -0
  49. package/dist/types/index.d.ts +1 -1
  50. package/dist/types/repositories/inMemory/walletRepository.d.ts +4 -1
  51. package/dist/types/repositories/indexedDB/walletRepository.d.ts +4 -1
  52. package/dist/types/repositories/realm/walletRepository.d.ts +4 -1
  53. package/dist/types/repositories/sqlite/walletRepository.d.ts +4 -1
  54. package/dist/types/repositories/walletRepository.d.ts +21 -0
  55. package/dist/types/script/tapscript.d.ts +4 -0
  56. package/dist/types/utils/timelock.d.ts +9 -0
  57. package/dist/types/wallet/serviceWorker/wallet-message-handler.d.ts +14 -2
  58. package/dist/types/wallet/unroll.d.ts +10 -0
  59. package/dist/types/wallet/vtxo-manager.d.ts +32 -5
  60. package/package.json +1 -1
@@ -1,6 +1,7 @@
1
1
  import { hex } from "@scure/base";
2
2
  import { DefaultVtxo } from '../../script/default.js';
3
- import { isCsvSpendable, sequenceToTimelock, timelockToSequence, } from './helpers.js';
3
+ import { isCsvSpendable } from './helpers.js';
4
+ import { sequenceToTimelock, timelockToSequence } from '../../utils/timelock.js';
4
5
  import { normalizeToDescriptor, extractPubKey, } from '../../identity/descriptor.js';
5
6
  /**
6
7
  * Extract pubkey bytes from a descriptor or hex string.
@@ -1,7 +1,8 @@
1
1
  import { hex } from "@scure/base";
2
2
  import { DelegateVtxo } from '../../script/delegate.js';
3
3
  import { DefaultVtxo } from '../../script/default.js';
4
- import { isCsvSpendable, sequenceToTimelock, timelockToSequence, } from './helpers.js';
4
+ import { isCsvSpendable } from './helpers.js';
5
+ import { sequenceToTimelock, timelockToSequence } from '../../utils/timelock.js';
5
6
  /**
6
7
  * Handler for delegate wallet virtual outputs.
7
8
  *
@@ -1,4 +1,4 @@
1
- import * as bip68 from "bip68";
1
+ import { sequenceToTimelock } from '../../utils/timelock.js';
2
2
  import { isDescriptor, extractPubKey } from '../../identity/descriptor.js';
3
3
  /**
4
4
  * Extract raw hex pubkey from a value that may be a descriptor or raw hex.
@@ -16,27 +16,6 @@ function extractRawPubKey(value) {
16
16
  return undefined;
17
17
  }
18
18
  }
19
- /**
20
- * Convert RelativeTimelock to BIP68 sequence number.
21
- */
22
- export function timelockToSequence(timelock) {
23
- return bip68.encode(timelock.type === "blocks"
24
- ? { blocks: Number(timelock.value) }
25
- : { seconds: Number(timelock.value) });
26
- }
27
- /**
28
- * Convert BIP68 sequence number back to RelativeTimelock.
29
- */
30
- export function sequenceToTimelock(sequence) {
31
- const decoded = bip68.decode(sequence);
32
- if ("blocks" in decoded && decoded.blocks !== undefined) {
33
- return { type: "blocks", value: BigInt(decoded.blocks) };
34
- }
35
- if ("seconds" in decoded && decoded.seconds !== undefined) {
36
- return { type: "seconds", value: BigInt(decoded.seconds) };
37
- }
38
- throw new Error(`Invalid BIP68 sequence: ${sequence}`);
39
- }
40
19
  /**
41
20
  * Resolve wallet's role from explicit role or by matching descriptor/pubkey.
42
21
  */
@@ -1,6 +1,7 @@
1
1
  import { hex } from "@scure/base";
2
2
  import { VHTLC } from '../../script/vhtlc.js';
3
- import { isCltvSatisfied, isCsvSpendable, resolveRole, sequenceToTimelock, timelockToSequence, } from './helpers.js';
3
+ import { isCltvSatisfied, isCsvSpendable, resolveRole } from './helpers.js';
4
+ import { sequenceToTimelock, timelockToSequence } from '../../utils/timelock.js';
4
5
  /**
5
6
  * Handler for Virtual Hash Time Lock Contract (VHTLC).
6
7
  *
@@ -0,0 +1,69 @@
1
+ /**
2
+ * Tier 1 helpers that enforce VTXO ownership at call sites that already know
3
+ * the intended contract script. Address-keyed repositories may still hand back
4
+ * legacy duplicate rows under the wrong bucket; these helpers gate reads and
5
+ * writes so a wrong-script row never wins.
6
+ *
7
+ * `script` is the authoritative ownership key. Equality is strict: a missing
8
+ * or empty `vtxo.script` never matches.
9
+ */
10
+ export function vtxoOutpoint(vtxo) {
11
+ return `${vtxo.txid}:${vtxo.vout}`;
12
+ }
13
+ export function isVtxoForScript(vtxo, script) {
14
+ return !!vtxo.script && vtxo.script === script;
15
+ }
16
+ export function filterVtxosForScript(vtxos, script) {
17
+ return vtxos.filter((v) => isVtxoForScript(v, script));
18
+ }
19
+ /**
20
+ * Background/indexer sync flavour: drop wrong-script rows and log enough
21
+ * context to identify each rejection. Returns only matching rows so the
22
+ * caller can keep going.
23
+ */
24
+ export function warnAndFilterVtxosForScript(vtxos, script, context) {
25
+ const matches = [];
26
+ const rejected = [];
27
+ for (const v of vtxos) {
28
+ if (isVtxoForScript(v, script)) {
29
+ matches.push(v);
30
+ }
31
+ else {
32
+ rejected.push(`${vtxoOutpoint(v)}(script=${v.script ?? ""})`);
33
+ }
34
+ }
35
+ if (rejected.length > 0) {
36
+ console.warn(`${context}: dropped ${rejected.length} wrong-script VTXO(s) for script ${script}: ${rejected.join(", ")}`);
37
+ }
38
+ return matches;
39
+ }
40
+ /**
41
+ * User-initiated transaction/signing flavour: throw before persisting or
42
+ * signing inconsistent ownership state. Silently skipping here would hide a
43
+ * serious bug in the wallet's spend path.
44
+ */
45
+ export function validateVtxosForScript(vtxos, script, context) {
46
+ const mismatches = vtxos.filter((v) => !isVtxoForScript(v, script));
47
+ if (mismatches.length === 0)
48
+ return;
49
+ const detail = mismatches
50
+ .map((v) => `${vtxoOutpoint(v)}(script=${v.script ?? ""})`)
51
+ .join(", ");
52
+ throw new Error(`${context}: refusing to persist ${mismatches.length} VTXO(s) whose script does not match ${script}: ${detail}`);
53
+ }
54
+ /**
55
+ * Tier 2 dispatch helpers: route to script-scoped repository methods when
56
+ * available, falling back to Tier 1 address-based filtering otherwise.
57
+ */
58
+ export async function getVtxosForContract(repo, contract) {
59
+ return repo.getVtxosForScript
60
+ ? repo.getVtxosForScript(contract.script)
61
+ : filterVtxosForScript(await repo.getVtxos(contract.address), contract.script);
62
+ }
63
+ export async function saveVtxosForContract(repo, contract, vtxos) {
64
+ if (repo.saveVtxosForScript) {
65
+ return repo.saveVtxosForScript({ script: contract.script, address: contract.address }, vtxos);
66
+ }
67
+ validateVtxosForScript(vtxos, contract.script, "saveVtxosForContract");
68
+ return repo.saveVtxos(contract.address, vtxos);
69
+ }
package/dist/esm/index.js CHANGED
@@ -42,7 +42,7 @@ export * from './arkfee/index.js';
42
42
  export * as asset from './extension/asset/index.js';
43
43
  // Contracts
44
44
  import { ContractManager, ContractWatcher, contractHandlers, DefaultContractHandler, DelegateContractHandler, VHTLCContractHandler, encodeArkContract, decodeArkContract, contractFromArkContract, contractFromArkContractWithAddress, isArkContract, } from './contracts/index.js';
45
- import { timelockToSequence, sequenceToTimelock, } from './contracts/handlers/helpers.js';
45
+ import { timelockToSequence, sequenceToTimelock } from './utils/timelock.js';
46
46
  import { closeDatabase, openDatabase } from './repositories/indexedDB/manager.js';
47
47
  import { WalletMessageHandler, WalletNotInitializedError, ReadonlyWalletError, DelegatorNotConfiguredError, } from './wallet/serviceWorker/wallet-message-handler.js';
48
48
  import { MESSAGE_BUS_NOT_INITIALIZED, MessageBusNotInitializedError, ServiceWorkerTimeoutError, } from './worker/errors.js';
@@ -1,3 +1,4 @@
1
+ import { isVtxoForScript } from '../../contracts/vtxoOwnership.js';
1
2
  /**
2
3
  * In-memory implementation of WalletRepository.
3
4
  * Data is ephemeral and scoped to the instance.
@@ -21,6 +22,40 @@ export class InMemoryWalletRepository {
21
22
  async deleteVtxos(address) {
22
23
  this.vtxosByAddress.delete(address);
23
24
  }
25
+ async getVtxosForScript(script) {
26
+ const allMatches = [];
27
+ for (const bucket of this.vtxosByAddress.values()) {
28
+ for (const vtxo of bucket) {
29
+ if (isVtxoForScript(vtxo, script)) {
30
+ allMatches.push(vtxo);
31
+ }
32
+ }
33
+ }
34
+ // Dedup by outpoint (last-write-wins across address buckets)
35
+ return mergeByKey([], allMatches, (item) => `${item.txid}:${item.vout}`);
36
+ }
37
+ async saveVtxosForScript(key, vtxos) {
38
+ if (!key.address) {
39
+ throw new Error("InMemoryWalletRepository requires an address");
40
+ }
41
+ for (const vtxo of vtxos) {
42
+ if (!isVtxoForScript(vtxo, key.script)) {
43
+ throw new Error(`VTXO ${vtxo.txid}:${vtxo.vout} script mismatch: expected ${key.script}, got ${vtxo.script}`);
44
+ }
45
+ }
46
+ return this.saveVtxos(key.address, vtxos);
47
+ }
48
+ async deleteVtxosForScript(script) {
49
+ for (const [address, bucket] of this.vtxosByAddress.entries()) {
50
+ const next = bucket.filter((v) => !isVtxoForScript(v, script));
51
+ if (next.length === 0) {
52
+ this.vtxosByAddress.delete(address);
53
+ }
54
+ else {
55
+ this.vtxosByAddress.set(address, next);
56
+ }
57
+ }
58
+ }
24
59
  async getUtxos(address) {
25
60
  return this.utxosByAddress.get(address) ?? [];
26
61
  }
@@ -3,6 +3,7 @@ import { closeDatabase, openDatabase } from './manager.js';
3
3
  import { initDatabase } from './schema.js';
4
4
  import { scriptFromArkAddress } from '../scriptFromAddress.js';
5
5
  import { DEFAULT_DB_NAME } from '../../worker/browser/utils.js';
6
+ import { isVtxoForScript } from '../../contracts/vtxoOwnership.js';
6
7
  /**
7
8
  * IndexedDB-based implementation of WalletRepository.
8
9
  */
@@ -141,6 +142,85 @@ export class IndexedDBWalletRepository {
141
142
  throw error;
142
143
  }
143
144
  }
145
+ async getVtxosForScript(script) {
146
+ try {
147
+ const db = await this.getDB();
148
+ return new Promise((resolve, reject) => {
149
+ const transaction = db.transaction([STORE_VTXOS], "readonly");
150
+ const store = transaction.objectStore(STORE_VTXOS);
151
+ const index = store.index("script");
152
+ const request = index.getAll(script);
153
+ request.onerror = () => reject(request.error);
154
+ request.onsuccess = () => {
155
+ const results = request.result || [];
156
+ try {
157
+ // Defensive filter: only rows whose script matches.
158
+ const matching = results.filter((r) => r.script === script);
159
+ // Dedup same outpoint rows across address buckets.
160
+ // Work on raw rows so the address field is available
161
+ // for the canonicality tiebreaker.
162
+ const byOutpoint = new Map();
163
+ for (const row of matching) {
164
+ const outpoint = `${row.txid}:${row.vout}`;
165
+ const existing = byOutpoint.get(outpoint);
166
+ if (!existing) {
167
+ byOutpoint.set(outpoint, row);
168
+ continue;
169
+ }
170
+ if (shouldReplaceVtxo(existing, row)) {
171
+ byOutpoint.set(outpoint, row);
172
+ }
173
+ }
174
+ resolve(Array.from(byOutpoint.values()).map(deserializeVtxoWithBackfill));
175
+ }
176
+ catch (err) {
177
+ reject(err);
178
+ }
179
+ };
180
+ });
181
+ }
182
+ catch (error) {
183
+ console.error(`Failed to get VTXOs for script ${script}:`, error);
184
+ throw error;
185
+ }
186
+ }
187
+ async saveVtxosForScript(key, vtxos) {
188
+ if (!key.address) {
189
+ throw new Error("IndexedDBWalletRepository requires an address");
190
+ }
191
+ for (const vtxo of vtxos) {
192
+ if (!isVtxoForScript(vtxo, key.script)) {
193
+ throw new Error(`VTXO ${vtxo.txid}:${vtxo.vout} script mismatch: expected ${key.script}, got ${vtxo.script}`);
194
+ }
195
+ }
196
+ return this.saveVtxos(key.address, vtxos);
197
+ }
198
+ async deleteVtxosForScript(script) {
199
+ try {
200
+ const db = await this.getDB();
201
+ return new Promise((resolve, reject) => {
202
+ const transaction = db.transaction([STORE_VTXOS], "readwrite");
203
+ const store = transaction.objectStore(STORE_VTXOS);
204
+ const index = store.index("script");
205
+ const request = index.openCursor(IDBKeyRange.only(script));
206
+ request.onerror = () => reject(request.error);
207
+ request.onsuccess = () => {
208
+ const cursor = request.result;
209
+ if (cursor) {
210
+ cursor.delete();
211
+ cursor.continue();
212
+ }
213
+ else {
214
+ resolve();
215
+ }
216
+ };
217
+ });
218
+ }
219
+ catch (error) {
220
+ console.error(`Failed to clear VTXOs for script ${script}:`, error);
221
+ throw error;
222
+ }
223
+ }
144
224
  async getUtxos(address) {
145
225
  try {
146
226
  const db = await this.getDB();
@@ -351,3 +431,40 @@ function deserializeVtxoWithBackfill(o) {
351
431
  }
352
432
  return deserializeVtxo(o);
353
433
  }
434
+ function isCanonicalRow(row) {
435
+ try {
436
+ return scriptFromArkAddress(row.address) === row.script;
437
+ }
438
+ catch {
439
+ return false;
440
+ }
441
+ }
442
+ function shouldReplaceVtxo(existing, incoming) {
443
+ const existingCanonical = isCanonicalRow(existing);
444
+ const incomingCanonical = isCanonicalRow(incoming);
445
+ if (incomingCanonical && !existingCanonical)
446
+ return true;
447
+ if (existingCanonical && !incomingCanonical)
448
+ return false;
449
+ // Tie on canonicality, check lifecycle completeness
450
+ const existingWeight = getLifecycleWeight(existing);
451
+ const incomingWeight = getLifecycleWeight(incoming);
452
+ if (incomingWeight > existingWeight)
453
+ return true;
454
+ if (existingWeight > incomingWeight)
455
+ return false;
456
+ // Tie on weight, stable sort by address
457
+ return incoming.address < existing.address;
458
+ }
459
+ function getLifecycleWeight(v) {
460
+ let weight = 0;
461
+ if (v.isSpent !== undefined)
462
+ weight += 1;
463
+ if (v.spentBy)
464
+ weight += 2;
465
+ if (v.settledBy)
466
+ weight += 2;
467
+ if (v.arkTxId)
468
+ weight += 2;
469
+ return weight;
470
+ }
@@ -1,5 +1,6 @@
1
1
  import { serializeVtxo, serializeUtxo, deserializeVtxo, deserializeUtxo, serializeAssets, deserializeAssets, } from '../serialization.js';
2
2
  import { scriptFromArkAddress } from '../scriptFromAddress.js';
3
+ import { isVtxoForScript } from '../../contracts/vtxoOwnership.js';
3
4
  /**
4
5
  * Realm-based implementation of WalletRepository.
5
6
  *
@@ -85,6 +86,33 @@ export class RealmWalletRepository {
85
86
  this.realm.delete(toDelete);
86
87
  });
87
88
  }
89
+ async getVtxosForScript(script) {
90
+ await this.ensureInit();
91
+ const results = this.realm
92
+ .objects("ArkVtxo")
93
+ .filtered("script == $0", script);
94
+ return [...results].map(vtxoObjectToDomain);
95
+ }
96
+ async saveVtxosForScript(key, vtxos) {
97
+ if (!key.address) {
98
+ throw new Error("RealmWalletRepository requires an address");
99
+ }
100
+ for (const vtxo of vtxos) {
101
+ if (!isVtxoForScript(vtxo, key.script)) {
102
+ throw new Error(`VTXO ${vtxo.txid}:${vtxo.vout} script mismatch: expected ${key.script}, got ${vtxo.script}`);
103
+ }
104
+ }
105
+ return this.saveVtxos(key.address, vtxos);
106
+ }
107
+ async deleteVtxosForScript(script) {
108
+ await this.ensureInit();
109
+ this.realm.write(() => {
110
+ const toDelete = this.realm
111
+ .objects("ArkVtxo")
112
+ .filtered("script == $0", script);
113
+ this.realm.delete(toDelete);
114
+ });
115
+ }
88
116
  // ── UTXO management ────────────────────────────────────────────────
89
117
  async getUtxos(address) {
90
118
  await this.ensureInit();
@@ -1,5 +1,6 @@
1
1
  import { serializeVtxo, serializeUtxo, deserializeVtxo, deserializeUtxo, serializeAssets, deserializeAssets, } from '../serialization.js';
2
2
  import { scriptFromArkAddress } from '../scriptFromAddress.js';
3
+ import { isVtxoForScript } from '../../contracts/vtxoOwnership.js';
3
4
  /**
4
5
  * SQLite-based implementation of WalletRepository.
5
6
  *
@@ -242,6 +243,28 @@ export class SQLiteWalletRepository {
242
243
  await this.ensureInit();
243
244
  await this.db.run(`DELETE FROM ${this.tables.vtxos} WHERE address = ?`, [address]);
244
245
  }
246
+ async getVtxosForScript(script) {
247
+ await this.ensureInit();
248
+ const rows = await this.db.all(`SELECT * FROM ${this.tables.vtxos} WHERE script = ?`, [script]);
249
+ return rows.map(vtxoRowToDomain);
250
+ }
251
+ async saveVtxosForScript(key, vtxos) {
252
+ if (!key.address) {
253
+ throw new Error("SQLiteWalletRepository requires an address");
254
+ }
255
+ for (const vtxo of vtxos) {
256
+ if (!isVtxoForScript(vtxo, key.script)) {
257
+ throw new Error(`VTXO ${vtxo.txid}:${vtxo.vout} script mismatch: expected ${key.script}, got ${vtxo.script}`);
258
+ }
259
+ }
260
+ return this.saveVtxos(key.address, vtxos);
261
+ }
262
+ async deleteVtxosForScript(script) {
263
+ await this.ensureInit();
264
+ await this.db.run(`DELETE FROM ${this.tables.vtxos} WHERE script = ?`, [
265
+ script,
266
+ ]);
267
+ }
245
268
  // ── UTXO management ────────────────────────────────────────────────
246
269
  async getUtxos(address) {
247
270
  await this.ensureInit();
@@ -1,9 +1,9 @@
1
1
  import { Script, Address, p2tr, taprootListToTree, TAPROOT_UNSPENDABLE_KEY, } from "@scure/btc-signer";
2
- import * as bip68 from "bip68";
3
2
  import { TAP_LEAF_VERSION } from "@scure/btc-signer/payment.js";
4
3
  import { PSBTOutput } from "@scure/btc-signer/psbt.js";
5
4
  import { hex } from "@scure/base";
6
5
  import { ArkAddress } from './address.js';
6
+ import { timelockToSequence } from '../utils/timelock.js';
7
7
  import { CLTVMultisigTapscript, ConditionCSVMultisigTapscript, CSVMultisigTapscript, } from './tapscript.js';
8
8
  export const TapTreeCoder = PSBTOutput.tapTree[2];
9
9
  export function scriptFromTapLeafScript(leaf) {
@@ -125,19 +125,19 @@ export class VtxoScript {
125
125
  const paths = [];
126
126
  for (const leaf of this.leaves) {
127
127
  try {
128
- const tapscript = CSVMultisigTapscript.decode(scriptFromTapLeafScript(leaf));
129
- paths.push(tapscript);
130
- continue;
131
- }
132
- catch (e) {
133
- try {
134
- const tapscript = ConditionCSVMultisigTapscript.decode(scriptFromTapLeafScript(leaf));
135
- paths.push(tapscript);
128
+ const script = scriptFromTapLeafScript(leaf);
129
+ if (CSVMultisigTapscript.isScriptValid(script) === true) {
130
+ const tapScript = CSVMultisigTapscript.decode(script);
131
+ paths.push(tapScript);
136
132
  }
137
- catch (e) {
138
- continue;
133
+ else if (ConditionCSVMultisigTapscript.isScriptValid(script) === true) {
134
+ const tapScript = ConditionCSVMultisigTapscript.decode(script);
135
+ paths.push(tapScript);
139
136
  }
140
137
  }
138
+ catch (e) {
139
+ console.debug("Failed to decode script", e);
140
+ }
141
141
  }
142
142
  return paths;
143
143
  }
@@ -167,9 +167,7 @@ export function getSequence(tapLeafScript) {
167
167
  const script = scriptWithLeafVersion.subarray(0, scriptWithLeafVersion.length - 1);
168
168
  try {
169
169
  const params = CSVMultisigTapscript.decode(script).params;
170
- sequence = bip68.encode(params.timelock.type === "blocks"
171
- ? { blocks: Number(params.timelock.value) }
172
- : { seconds: Number(params.timelock.value) });
170
+ sequence = timelockToSequence(params.timelock);
173
171
  }
174
172
  catch {
175
173
  const params = CLTVMultisigTapscript.decode(script).params;
@@ -1,6 +1,6 @@
1
- import * as bip68 from "bip68";
2
1
  import { Script, ScriptNum, p2tr_ms } from "@scure/btc-signer";
3
2
  import { hex } from "@scure/base";
3
+ import { sequenceToTimelock, timelockToSequence } from '../utils/timelock.js';
4
4
  const MinimalScriptNum = ScriptNum(undefined, true);
5
5
  export var TapscriptType;
6
6
  (function (TapscriptType) {
@@ -234,9 +234,7 @@ export var CSVMultisigTapscript;
234
234
  throw new Error(`Invalid pubkey length: expected 32, got ${pubkey.length}`);
235
235
  }
236
236
  }
237
- const sequence = MinimalScriptNum.encode(BigInt(bip68.encode(params.timelock.type === "blocks"
238
- ? { blocks: Number(params.timelock.value) }
239
- : { seconds: Number(params.timelock.value) })));
237
+ const sequence = MinimalScriptNum.encode(BigInt(timelockToSequence(params.timelock)));
240
238
  const asm = [
241
239
  sequence.length === 1 ? sequence[0] : sequence,
242
240
  "CHECKSEQUENCEVERIFY",
@@ -259,17 +257,12 @@ export var CSVMultisigTapscript;
259
257
  if (script.length === 0) {
260
258
  throw new Error("Failed to decode: script is empty");
261
259
  }
262
- const asm = Script.decode(script);
263
- if (asm.length < 3) {
264
- throw new Error(`Invalid script: too short (expected at least 3)`);
260
+ const isValid = isScriptValid(script);
261
+ if (isValid instanceof Error) {
262
+ throw isValid;
265
263
  }
264
+ const asm = Script.decode(script);
266
265
  const sequence = asm[0];
267
- if (typeof sequence === "string") {
268
- throw new Error("Invalid script: expected sequence number");
269
- }
270
- if (asm[1] !== "CHECKSEQUENCEVERIFY" || asm[2] !== "DROP") {
271
- throw new Error("Invalid script: expected CHECKSEQUENCEVERIFY DROP");
272
- }
273
266
  const multisigScript = new Uint8Array(Script.encode(asm.slice(3)));
274
267
  let multisig;
275
268
  try {
@@ -285,10 +278,7 @@ export var CSVMultisigTapscript;
285
278
  else {
286
279
  sequenceNum = Number(MinimalScriptNum.decode(sequence));
287
280
  }
288
- const decodedTimelock = bip68.decode(sequenceNum);
289
- const timelock = decodedTimelock.blocks !== undefined
290
- ? { type: "blocks", value: BigInt(decodedTimelock.blocks) }
291
- : { type: "seconds", value: BigInt(decodedTimelock.seconds) };
281
+ const timelock = sequenceToTimelock(sequenceNum);
292
282
  const reconstructed = encode({
293
283
  timelock,
294
284
  ...multisig.params,
@@ -311,6 +301,21 @@ export var CSVMultisigTapscript;
311
301
  return tapscript.type === TapscriptType.CSVMultisig;
312
302
  }
313
303
  CSVMultisigTapscript.is = is;
304
+ function isScriptValid(script) {
305
+ const asm = Script.decode(script);
306
+ if (asm.length < 3) {
307
+ return new Error(`Invalid script: too short (expected at least 3)`);
308
+ }
309
+ const sequence = asm[0];
310
+ if (typeof sequence === "string") {
311
+ return new Error("Invalid script: expected sequence number");
312
+ }
313
+ if (asm[1] !== "CHECKSEQUENCEVERIFY" || asm[2] !== "DROP") {
314
+ return new Error("Invalid script: expected CHECKSEQUENCEVERIFY DROP");
315
+ }
316
+ return true;
317
+ }
318
+ CSVMultisigTapscript.isScriptValid = isScriptValid;
314
319
  })(CSVMultisigTapscript || (CSVMultisigTapscript = {}));
315
320
  /**
316
321
  * Combines a condition script with an exit closure. The resulting script requires
@@ -345,18 +350,14 @@ export var ConditionCSVMultisigTapscript;
345
350
  if (script.length === 0) {
346
351
  throw new Error("Failed to decode: script is empty");
347
352
  }
348
- const asm = Script.decode(script);
349
- if (asm.length < 1) {
350
- throw new Error(`Invalid script: too short (expected at least 1)`);
351
- }
352
- let verifyIndex = -1;
353
- for (let i = asm.length - 1; i >= 0; i--) {
354
- if (asm[i] === "VERIFY") {
355
- verifyIndex = i;
356
- }
353
+ const isValid = isScriptValid(script);
354
+ if (isValid instanceof Error) {
355
+ throw isValid;
357
356
  }
357
+ const asm = Script.decode(script);
358
+ let verifyIndex = getVerifyIndex(asm);
358
359
  if (verifyIndex === -1) {
359
- throw new Error("Invalid script: missing VERIFY operation");
360
+ throw Error("Invalid script: missing VERIFY operation");
360
361
  }
361
362
  const conditionScript = new Uint8Array(Script.encode(asm.slice(0, verifyIndex)));
362
363
  const csvMultisigScript = new Uint8Array(Script.encode(asm.slice(verifyIndex + 1)));
@@ -389,6 +390,28 @@ export var ConditionCSVMultisigTapscript;
389
390
  return tapscript.type === TapscriptType.ConditionCSVMultisig;
390
391
  }
391
392
  ConditionCSVMultisigTapscript.is = is;
393
+ function getVerifyIndex(asm) {
394
+ let verifyIndex = -1;
395
+ for (let i = asm.length - 1; i >= 0; i--) {
396
+ if (asm[i] === "VERIFY") {
397
+ verifyIndex = i;
398
+ return verifyIndex;
399
+ }
400
+ }
401
+ return verifyIndex;
402
+ }
403
+ function isScriptValid(script) {
404
+ const asm = Script.decode(script);
405
+ if (asm.length < 1) {
406
+ return new Error(`Invalid script: too short (expected at least 1)`);
407
+ }
408
+ let verifyIndex = getVerifyIndex(asm);
409
+ if (verifyIndex === -1) {
410
+ return new Error("Invalid script: missing VERIFY operation");
411
+ }
412
+ return true;
413
+ }
414
+ ConditionCSVMultisigTapscript.isScriptValid = isScriptValid;
392
415
  })(ConditionCSVMultisigTapscript || (ConditionCSVMultisigTapscript = {}));
393
416
  /**
394
417
  * Combines a condition script with a forfeit closure. The resulting script requires
@@ -423,18 +446,14 @@ export var ConditionMultisigTapscript;
423
446
  if (script.length === 0) {
424
447
  throw new Error("Failed to decode: script is empty");
425
448
  }
426
- const asm = Script.decode(script);
427
- if (asm.length < 1) {
428
- throw new Error(`Invalid script: too short (expected at least 1)`);
429
- }
430
- let verifyIndex = -1;
431
- for (let i = asm.length - 1; i >= 0; i--) {
432
- if (asm[i] === "VERIFY") {
433
- verifyIndex = i;
434
- }
449
+ const isValid = isScriptValid(script);
450
+ if (isValid instanceof Error) {
451
+ throw isValid;
435
452
  }
453
+ const asm = Script.decode(script);
454
+ let verifyIndex = getVerifyIndex(asm);
436
455
  if (verifyIndex === -1) {
437
- throw new Error("Invalid script: missing VERIFY operation");
456
+ throw Error("Invalid script: missing VERIFY operation");
438
457
  }
439
458
  const conditionScript = new Uint8Array(Script.encode(asm.slice(0, verifyIndex)));
440
459
  const multisigScript = new Uint8Array(Script.encode(asm.slice(verifyIndex + 1)));
@@ -467,6 +486,28 @@ export var ConditionMultisigTapscript;
467
486
  return tapscript.type === TapscriptType.ConditionMultisig;
468
487
  }
469
488
  ConditionMultisigTapscript.is = is;
489
+ function getVerifyIndex(asm) {
490
+ let verifyIndex = -1;
491
+ for (let i = asm.length - 1; i >= 0; i--) {
492
+ if (asm[i] === "VERIFY") {
493
+ verifyIndex = i;
494
+ return verifyIndex;
495
+ }
496
+ }
497
+ return verifyIndex;
498
+ }
499
+ function isScriptValid(script) {
500
+ const asm = Script.decode(script);
501
+ if (asm.length < 1) {
502
+ return new Error(`Invalid script: too short (expected at least 1)`);
503
+ }
504
+ let verifyIndex = getVerifyIndex(asm);
505
+ if (verifyIndex === -1) {
506
+ return new Error("Invalid script: missing VERIFY operation");
507
+ }
508
+ return true;
509
+ }
510
+ ConditionMultisigTapscript.isScriptValid = isScriptValid;
470
511
  })(ConditionMultisigTapscript || (ConditionMultisigTapscript = {}));
471
512
  /**
472
513
  * Implements an absolute timelock (CLTV) script combined with a forfeit closure.
@@ -507,10 +548,11 @@ export var CLTVMultisigTapscript;
507
548
  if (script.length === 0) {
508
549
  throw new Error("Failed to decode: script is empty");
509
550
  }
510
- const asm = Script.decode(script);
511
- if (asm.length < 3) {
512
- throw new Error(`Invalid script: too short (expected at least 3)`);
551
+ const isValid = isScriptValid(script);
552
+ if (isValid instanceof Error) {
553
+ throw isValid;
513
554
  }
555
+ const asm = Script.decode(script);
514
556
  const locktime = asm[0];
515
557
  if (typeof locktime === "string") {
516
558
  throw new Error("Invalid script: expected locktime number");
@@ -555,4 +597,19 @@ export var CLTVMultisigTapscript;
555
597
  return tapscript.type === TapscriptType.CLTVMultisig;
556
598
  }
557
599
  CLTVMultisigTapscript.is = is;
600
+ function isScriptValid(script) {
601
+ const asm = Script.decode(script);
602
+ if (asm.length < 3) {
603
+ return new Error(`Invalid script: too short (expected at least 3)`);
604
+ }
605
+ const locktime = asm[0];
606
+ if (typeof locktime === "string") {
607
+ return new Error("Invalid script: expected locktime as number or bytes");
608
+ }
609
+ if (asm[1] !== "CHECKLOCKTIMEVERIFY" || asm[2] !== "DROP") {
610
+ return new Error("Invalid script: expected CHECKLOCKTIMEVERIFY DROP");
611
+ }
612
+ return true;
613
+ }
614
+ CLTVMultisigTapscript.isScriptValid = isScriptValid;
558
615
  })(CLTVMultisigTapscript || (CLTVMultisigTapscript = {}));