@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
package/README.md CHANGED
@@ -733,7 +733,7 @@ for await (const step of session) {
733
733
  console.log(`Waiting for transaction ${step.txid} to be confirmed`);
734
734
  break;
735
735
  case Unroll.StepType.UNROLL:
736
- console.log(`Broadcasting transaction ${step.tx.id}`);
736
+ console.log(`Transaction ${step.tx.id} unrolled`);
737
737
  break;
738
738
  case Unroll.StepType.DONE:
739
739
  console.log(`Unrolling complete for virtual output ${step.vtxoTxid}`);
@@ -749,6 +749,26 @@ The unrolling process works by:
749
749
  - Waiting for confirmations between steps
750
750
  - Using P2A (Pay-to-Anchor) transactions to pay for fees
751
751
 
752
+ Optionally, you can use `session.next()` to control the broadcasting process manually.
753
+
754
+ ```typescript
755
+ const step = await session.next();
756
+ switch (step.type) {
757
+ case Unroll.StepType.WAIT:
758
+ await step.do(); // wait for the transaction to be confirmed
759
+ break;
760
+ case Unroll.StepType.UNROLL:
761
+ const [parent, child] = step.pkg;
762
+ console.log(`Parent: ${parent}`)
763
+ console.log(`Child: ${child}`)
764
+ await step.do(); // broadcast the 1C1P package
765
+ break;
766
+ case Unroll.StepType.DONE:
767
+ console.log(`Unrolling complete for VTXO ${step.vtxoTxid}`);
768
+ break;
769
+ }
770
+ ```
771
+
752
772
  #### Step 2: Completing the Exit
753
773
 
754
774
  Once virtual outputs are fully unrolled and the unilateral exit timelock has expired, you can complete the exit:
@@ -6,6 +6,7 @@ const contractWatcher_1 = require("./contractWatcher");
6
6
  const handlers_1 = require("./handlers");
7
7
  const utils_1 = require("../wallet/utils");
8
8
  const syncCursors_1 = require("../utils/syncCursors");
9
+ const vtxoOwnership_1 = require("./vtxoOwnership");
9
10
  const DEFAULT_PAGE_SIZE = 500;
10
11
  /**
11
12
  * Central manager for contract lifecycle and operations.
@@ -357,11 +358,61 @@ class ContractManager {
357
358
  const contracts = opts?.scripts
358
359
  ? await this.getContracts({ script: opts.scripts })
359
360
  : undefined;
361
+ // Only forward an explicit window when the caller supplied one. An
362
+ // empty `{ after: undefined, before: undefined }` would short-circuit
363
+ // both the cursor-derived `?after=` query in `syncContracts` (because
364
+ // `??` doesn't fire on a non-nullish object) AND the cursor-advance
365
+ // gate (which requires `options.window === undefined`), turning every
366
+ // `refreshVtxos()` call into an unbounded full re-scan whose cursor
367
+ // never moves forward.
368
+ const hasExplicitWindow = opts?.after !== undefined || opts?.before !== undefined;
360
369
  await this.syncContracts({
361
370
  contracts,
362
- window: { after: opts?.after, before: opts?.before },
371
+ window: hasExplicitWindow
372
+ ? { after: opts?.after, before: opts?.before }
373
+ : undefined,
363
374
  });
364
375
  }
376
+ async refreshOutpoints(outpoints) {
377
+ if (outpoints.length === 0)
378
+ return;
379
+ const { vtxos } = await this.config.indexerProvider.getVtxos({
380
+ outpoints,
381
+ });
382
+ if (vtxos.length === 0)
383
+ return;
384
+ // Filter to outputs whose script we own. Map them to their owning
385
+ // contract so we can write through to the right per-address entry
386
+ // in the wallet repository.
387
+ const scripts = Array.from(new Set(vtxos.map((v) => v.script)));
388
+ const contracts = await this.config.contractRepository.getContracts({
389
+ script: scripts,
390
+ });
391
+ const scriptToContract = new Map(contracts.map((c) => [c.script, c]));
392
+ const owned = vtxos.filter((v) => scriptToContract.has(v.script));
393
+ if (owned.length === 0)
394
+ return;
395
+ const annotated = await this.annotateVtxos(owned);
396
+ const byAddress = new Map();
397
+ for (const vtxo of annotated) {
398
+ const contract = scriptToContract.get(vtxo.script);
399
+ if (!contract)
400
+ continue;
401
+ const address = contract.address;
402
+ const arr = byAddress.get(address) ?? [];
403
+ arr.push(vtxo);
404
+ byAddress.set(address, arr);
405
+ }
406
+ for (const [address, addressVtxos] of byAddress) {
407
+ const contract = contracts.find((c) => c.address === address);
408
+ if (contract) {
409
+ await (0, vtxoOwnership_1.saveVtxosForContract)(this.config.walletRepository, contract, addressVtxos);
410
+ }
411
+ else {
412
+ await this.config.walletRepository.saveVtxos(address, addressVtxos);
413
+ }
414
+ }
415
+ }
365
416
  /**
366
417
  * Check if currently watching.
367
418
  */
@@ -402,9 +453,9 @@ class ContractManager {
402
453
  this.emitEvent(event);
403
454
  }
404
455
  async getVtxosForContracts(contracts) {
405
- const res = await Promise.all(contracts.map(({ script, address }) => this.config.walletRepository.getVtxos(address).then((vtxos) => vtxos.map((vtxo) => ({
456
+ const res = await Promise.all(contracts.map((contract) => (0, vtxoOwnership_1.getVtxosForContract)(this.config.walletRepository, contract).then((vtxos) => vtxos.map((vtxo) => ({
406
457
  ...vtxo,
407
- contractScript: script,
458
+ contractScript: contract.script,
408
459
  })))));
409
460
  return res.flat();
410
461
  }
@@ -470,7 +521,14 @@ class ContractManager {
470
521
  });
471
522
  }
472
523
  for (const [addr, contractVtxos] of byContract) {
473
- await this.config.walletRepository.saveVtxos(addr, contractVtxos);
524
+ // The bucket is keyed by contract address, so the script filter
525
+ // here is the same as the contract's. Skip wrong-script rows
526
+ // rather than crash the reconcile loop.
527
+ const contract = contracts.find((c) => c.address === addr);
528
+ const filtered = (0, vtxoOwnership_1.warnAndFilterVtxosForScript)(contractVtxos, contract.script, "ContractManager.reconcilePendingFrontier");
529
+ if (filtered.length === 0)
530
+ continue;
531
+ await (0, vtxoOwnership_1.saveVtxosForContract)(this.config.walletRepository, contract, filtered);
474
532
  }
475
533
  }
476
534
  async fetchContractVxosFromIndexer(contracts, pageSize, syncWindow) {
@@ -480,7 +538,10 @@ class ContractManager {
480
538
  result.set(contractScript, vtxos);
481
539
  const contract = contracts.find((c) => c.script === contractScript);
482
540
  if (contract) {
483
- await this.config.walletRepository.saveVtxos(contract.address, vtxos);
541
+ const filtered = (0, vtxoOwnership_1.warnAndFilterVtxosForScript)(vtxos, contract.script, "ContractManager.fetchContractVxosFromIndexer");
542
+ if (filtered.length === 0)
543
+ continue;
544
+ await (0, vtxoOwnership_1.saveVtxosForContract)(this.config.walletRepository, contract, filtered);
484
545
  }
485
546
  }
486
547
  return result;
@@ -3,6 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.ContractWatcher = void 0;
4
4
  const utils_1 = require("../wallet/utils");
5
5
  const utils_2 = require("../providers/utils");
6
+ const vtxoOwnership_1 = require("./vtxoOwnership");
6
7
  /**
7
8
  * Watches multiple contracts for virtual output state changes with resilient connection handling.
8
9
  *
@@ -90,7 +91,10 @@ class ContractWatcher {
90
91
  */
91
92
  async seedLastKnownVtxos(state) {
92
93
  try {
93
- const cached = await this.config.walletRepository.getVtxos(state.contract.address);
94
+ // Apply the same script gate used by getContractVtxos so a legacy
95
+ // wrong-script row in the address bucket can't seed the baseline
96
+ // and then look "spent" on the first poll.
97
+ const cached = await (0, vtxoOwnership_1.getVtxosForContract)(this.config.walletRepository, state.contract);
94
98
  for (const vtxo of cached) {
95
99
  if (vtxo.isSpent)
96
100
  continue;
@@ -169,8 +173,10 @@ class ContractWatcher {
169
173
  return true;
170
174
  })
171
175
  .map(async (state) => {
172
- // Use contract address as cache key
173
- const cached = await repo.getVtxos(state.contract.address);
176
+ // Use contract address as cache key. Legacy address buckets
177
+ // can contain rows from other contracts; gate by script before
178
+ // converting so a wrong-script row never reaches the watcher.
179
+ const cached = await (0, vtxoOwnership_1.getVtxosForContract)(repo, state.contract);
174
180
  if (cached.length > 0) {
175
181
  // Convert to ContractVtxo with contractScript
176
182
  const contractVtxos = cached.map((v) => ({
@@ -4,6 +4,7 @@ exports.DefaultContractHandler = void 0;
4
4
  const base_1 = require("@scure/base");
5
5
  const default_1 = require("../../script/default");
6
6
  const helpers_1 = require("./helpers");
7
+ const timelock_1 = require("../../utils/timelock");
7
8
  const descriptor_1 = require("../../identity/descriptor");
8
9
  /**
9
10
  * Extract pubkey bytes from a descriptor or hex string.
@@ -28,12 +29,12 @@ exports.DefaultContractHandler = {
28
29
  return {
29
30
  pubKey: base_1.hex.encode(params.pubKey),
30
31
  serverPubKey: base_1.hex.encode(params.serverPubKey),
31
- csvTimelock: (0, helpers_1.timelockToSequence)(params.csvTimelock).toString(),
32
+ csvTimelock: (0, timelock_1.timelockToSequence)(params.csvTimelock).toString(),
32
33
  };
33
34
  },
34
35
  deserializeParams(params) {
35
36
  const csvTimelock = params.csvTimelock
36
- ? (0, helpers_1.sequenceToTimelock)(Number(params.csvTimelock))
37
+ ? (0, timelock_1.sequenceToTimelock)(Number(params.csvTimelock))
37
38
  : default_1.DefaultVtxo.Script.DEFAULT_TIMELOCK;
38
39
  return {
39
40
  pubKey: extractPubKeyBytes(params.pubKey),
@@ -5,6 +5,7 @@ const base_1 = require("@scure/base");
5
5
  const delegate_1 = require("../../script/delegate");
6
6
  const default_1 = require("../../script/default");
7
7
  const helpers_1 = require("./helpers");
8
+ const timelock_1 = require("../../utils/timelock");
8
9
  /**
9
10
  * Handler for delegate wallet virtual outputs.
10
11
  *
@@ -24,12 +25,12 @@ exports.DelegateContractHandler = {
24
25
  pubKey: base_1.hex.encode(params.pubKey),
25
26
  serverPubKey: base_1.hex.encode(params.serverPubKey),
26
27
  delegatePubKey: base_1.hex.encode(params.delegatePubKey),
27
- csvTimelock: (0, helpers_1.timelockToSequence)(params.csvTimelock).toString(),
28
+ csvTimelock: (0, timelock_1.timelockToSequence)(params.csvTimelock).toString(),
28
29
  };
29
30
  },
30
31
  deserializeParams(params) {
31
32
  const csvTimelock = params.csvTimelock
32
- ? (0, helpers_1.sequenceToTimelock)(Number(params.csvTimelock))
33
+ ? (0, timelock_1.sequenceToTimelock)(Number(params.csvTimelock))
33
34
  : default_1.DefaultVtxo.Script.DEFAULT_TIMELOCK;
34
35
  return {
35
36
  pubKey: base_1.hex.decode(params.pubKey),
@@ -1,44 +1,9 @@
1
1
  "use strict";
2
- var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
- if (k2 === undefined) k2 = k;
4
- var desc = Object.getOwnPropertyDescriptor(m, k);
5
- if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
- desc = { enumerable: true, get: function() { return m[k]; } };
7
- }
8
- Object.defineProperty(o, k2, desc);
9
- }) : (function(o, m, k, k2) {
10
- if (k2 === undefined) k2 = k;
11
- o[k2] = m[k];
12
- }));
13
- var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
- Object.defineProperty(o, "default", { enumerable: true, value: v });
15
- }) : function(o, v) {
16
- o["default"] = v;
17
- });
18
- var __importStar = (this && this.__importStar) || (function () {
19
- var ownKeys = function(o) {
20
- ownKeys = Object.getOwnPropertyNames || function (o) {
21
- var ar = [];
22
- for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
- return ar;
24
- };
25
- return ownKeys(o);
26
- };
27
- return function (mod) {
28
- if (mod && mod.__esModule) return mod;
29
- var result = {};
30
- if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
- __setModuleDefault(result, mod);
32
- return result;
33
- };
34
- })();
35
2
  Object.defineProperty(exports, "__esModule", { value: true });
36
- exports.timelockToSequence = timelockToSequence;
37
- exports.sequenceToTimelock = sequenceToTimelock;
38
3
  exports.resolveRole = resolveRole;
39
4
  exports.isCltvSatisfied = isCltvSatisfied;
40
5
  exports.isCsvSpendable = isCsvSpendable;
41
- const bip68 = __importStar(require("bip68"));
6
+ const timelock_1 = require("../../utils/timelock");
42
7
  const descriptor_1 = require("../../identity/descriptor");
43
8
  /**
44
9
  * Extract raw hex pubkey from a value that may be a descriptor or raw hex.
@@ -56,27 +21,6 @@ function extractRawPubKey(value) {
56
21
  return undefined;
57
22
  }
58
23
  }
59
- /**
60
- * Convert RelativeTimelock to BIP68 sequence number.
61
- */
62
- function timelockToSequence(timelock) {
63
- return bip68.encode(timelock.type === "blocks"
64
- ? { blocks: Number(timelock.value) }
65
- : { seconds: Number(timelock.value) });
66
- }
67
- /**
68
- * Convert BIP68 sequence number back to RelativeTimelock.
69
- */
70
- function sequenceToTimelock(sequence) {
71
- const decoded = bip68.decode(sequence);
72
- if ("blocks" in decoded && decoded.blocks !== undefined) {
73
- return { type: "blocks", value: BigInt(decoded.blocks) };
74
- }
75
- if ("seconds" in decoded && decoded.seconds !== undefined) {
76
- return { type: "seconds", value: BigInt(decoded.seconds) };
77
- }
78
- throw new Error(`Invalid BIP68 sequence: ${sequence}`);
79
- }
80
24
  /**
81
25
  * Resolve wallet's role from explicit role or by matching descriptor/pubkey.
82
26
  */
@@ -152,7 +96,7 @@ function isCsvSpendable(context, sequence) {
152
96
  return true;
153
97
  if (!context.vtxo)
154
98
  return false;
155
- const timelock = sequenceToTimelock(sequence);
99
+ const timelock = (0, timelock_1.sequenceToTimelock)(sequence);
156
100
  if (timelock.type === "blocks") {
157
101
  if (context.blockHeight === undefined ||
158
102
  context.vtxo.status.block_height === undefined) {
@@ -4,6 +4,7 @@ exports.VHTLCContractHandler = void 0;
4
4
  const base_1 = require("@scure/base");
5
5
  const vhtlc_1 = require("../../script/vhtlc");
6
6
  const helpers_1 = require("./helpers");
7
+ const timelock_1 = require("../../utils/timelock");
7
8
  /**
8
9
  * Handler for Virtual Hash Time Lock Contract (VHTLC).
9
10
  *
@@ -32,9 +33,9 @@ exports.VHTLCContractHandler = {
32
33
  server: base_1.hex.encode(params.server),
33
34
  hash: base_1.hex.encode(params.preimageHash),
34
35
  refundLocktime: params.refundLocktime.toString(),
35
- claimDelay: (0, helpers_1.timelockToSequence)(params.unilateralClaimDelay).toString(),
36
- refundDelay: (0, helpers_1.timelockToSequence)(params.unilateralRefundDelay).toString(),
37
- refundNoReceiverDelay: (0, helpers_1.timelockToSequence)(params.unilateralRefundWithoutReceiverDelay).toString(),
36
+ claimDelay: (0, timelock_1.timelockToSequence)(params.unilateralClaimDelay).toString(),
37
+ refundDelay: (0, timelock_1.timelockToSequence)(params.unilateralRefundDelay).toString(),
38
+ refundNoReceiverDelay: (0, timelock_1.timelockToSequence)(params.unilateralRefundWithoutReceiverDelay).toString(),
38
39
  };
39
40
  },
40
41
  deserializeParams(params) {
@@ -44,9 +45,9 @@ exports.VHTLCContractHandler = {
44
45
  server: base_1.hex.decode(params.server),
45
46
  preimageHash: base_1.hex.decode(params.hash),
46
47
  refundLocktime: BigInt(params.refundLocktime),
47
- unilateralClaimDelay: (0, helpers_1.sequenceToTimelock)(Number(params.claimDelay)),
48
- unilateralRefundDelay: (0, helpers_1.sequenceToTimelock)(Number(params.refundDelay)),
49
- unilateralRefundWithoutReceiverDelay: (0, helpers_1.sequenceToTimelock)(Number(params.refundNoReceiverDelay)),
48
+ unilateralClaimDelay: (0, timelock_1.sequenceToTimelock)(Number(params.claimDelay)),
49
+ unilateralRefundDelay: (0, timelock_1.sequenceToTimelock)(Number(params.refundDelay)),
50
+ unilateralRefundWithoutReceiverDelay: (0, timelock_1.sequenceToTimelock)(Number(params.refundNoReceiverDelay)),
50
51
  };
51
52
  },
52
53
  /**
@@ -0,0 +1,78 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.vtxoOutpoint = vtxoOutpoint;
4
+ exports.isVtxoForScript = isVtxoForScript;
5
+ exports.filterVtxosForScript = filterVtxosForScript;
6
+ exports.warnAndFilterVtxosForScript = warnAndFilterVtxosForScript;
7
+ exports.validateVtxosForScript = validateVtxosForScript;
8
+ exports.getVtxosForContract = getVtxosForContract;
9
+ exports.saveVtxosForContract = saveVtxosForContract;
10
+ /**
11
+ * Tier 1 helpers that enforce VTXO ownership at call sites that already know
12
+ * the intended contract script. Address-keyed repositories may still hand back
13
+ * legacy duplicate rows under the wrong bucket; these helpers gate reads and
14
+ * writes so a wrong-script row never wins.
15
+ *
16
+ * `script` is the authoritative ownership key. Equality is strict: a missing
17
+ * or empty `vtxo.script` never matches.
18
+ */
19
+ function vtxoOutpoint(vtxo) {
20
+ return `${vtxo.txid}:${vtxo.vout}`;
21
+ }
22
+ function isVtxoForScript(vtxo, script) {
23
+ return !!vtxo.script && vtxo.script === script;
24
+ }
25
+ function filterVtxosForScript(vtxos, script) {
26
+ return vtxos.filter((v) => isVtxoForScript(v, script));
27
+ }
28
+ /**
29
+ * Background/indexer sync flavour: drop wrong-script rows and log enough
30
+ * context to identify each rejection. Returns only matching rows so the
31
+ * caller can keep going.
32
+ */
33
+ function warnAndFilterVtxosForScript(vtxos, script, context) {
34
+ const matches = [];
35
+ const rejected = [];
36
+ for (const v of vtxos) {
37
+ if (isVtxoForScript(v, script)) {
38
+ matches.push(v);
39
+ }
40
+ else {
41
+ rejected.push(`${vtxoOutpoint(v)}(script=${v.script ?? ""})`);
42
+ }
43
+ }
44
+ if (rejected.length > 0) {
45
+ console.warn(`${context}: dropped ${rejected.length} wrong-script VTXO(s) for script ${script}: ${rejected.join(", ")}`);
46
+ }
47
+ return matches;
48
+ }
49
+ /**
50
+ * User-initiated transaction/signing flavour: throw before persisting or
51
+ * signing inconsistent ownership state. Silently skipping here would hide a
52
+ * serious bug in the wallet's spend path.
53
+ */
54
+ function validateVtxosForScript(vtxos, script, context) {
55
+ const mismatches = vtxos.filter((v) => !isVtxoForScript(v, script));
56
+ if (mismatches.length === 0)
57
+ return;
58
+ const detail = mismatches
59
+ .map((v) => `${vtxoOutpoint(v)}(script=${v.script ?? ""})`)
60
+ .join(", ");
61
+ throw new Error(`${context}: refusing to persist ${mismatches.length} VTXO(s) whose script does not match ${script}: ${detail}`);
62
+ }
63
+ /**
64
+ * Tier 2 dispatch helpers: route to script-scoped repository methods when
65
+ * available, falling back to Tier 1 address-based filtering otherwise.
66
+ */
67
+ async function getVtxosForContract(repo, contract) {
68
+ return repo.getVtxosForScript
69
+ ? repo.getVtxosForScript(contract.script)
70
+ : filterVtxosForScript(await repo.getVtxos(contract.address), contract.script);
71
+ }
72
+ async function saveVtxosForContract(repo, contract, vtxos) {
73
+ if (repo.saveVtxosForScript) {
74
+ return repo.saveVtxosForScript({ script: contract.script, address: contract.address }, vtxos);
75
+ }
76
+ validateVtxosForScript(vtxos, contract.script, "saveVtxosForContract");
77
+ return repo.saveVtxos(contract.address, vtxos);
78
+ }
package/dist/cjs/index.js CHANGED
@@ -183,9 +183,9 @@ Object.defineProperty(exports, "decodeArkContract", { enumerable: true, get: fun
183
183
  Object.defineProperty(exports, "contractFromArkContract", { enumerable: true, get: function () { return contracts_1.contractFromArkContract; } });
184
184
  Object.defineProperty(exports, "contractFromArkContractWithAddress", { enumerable: true, get: function () { return contracts_1.contractFromArkContractWithAddress; } });
185
185
  Object.defineProperty(exports, "isArkContract", { enumerable: true, get: function () { return contracts_1.isArkContract; } });
186
- const helpers_1 = require("./contracts/handlers/helpers");
187
- Object.defineProperty(exports, "timelockToSequence", { enumerable: true, get: function () { return helpers_1.timelockToSequence; } });
188
- Object.defineProperty(exports, "sequenceToTimelock", { enumerable: true, get: function () { return helpers_1.sequenceToTimelock; } });
186
+ const timelock_1 = require("./utils/timelock");
187
+ Object.defineProperty(exports, "timelockToSequence", { enumerable: true, get: function () { return timelock_1.timelockToSequence; } });
188
+ Object.defineProperty(exports, "sequenceToTimelock", { enumerable: true, get: function () { return timelock_1.sequenceToTimelock; } });
189
189
  const manager_1 = require("./repositories/indexedDB/manager");
190
190
  Object.defineProperty(exports, "closeDatabase", { enumerable: true, get: function () { return manager_1.closeDatabase; } });
191
191
  Object.defineProperty(exports, "openDatabase", { enumerable: true, get: function () { return manager_1.openDatabase; } });
@@ -1,6 +1,7 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.InMemoryWalletRepository = void 0;
4
+ const vtxoOwnership_1 = require("../../contracts/vtxoOwnership");
4
5
  /**
5
6
  * In-memory implementation of WalletRepository.
6
7
  * Data is ephemeral and scoped to the instance.
@@ -24,6 +25,40 @@ class InMemoryWalletRepository {
24
25
  async deleteVtxos(address) {
25
26
  this.vtxosByAddress.delete(address);
26
27
  }
28
+ async getVtxosForScript(script) {
29
+ const allMatches = [];
30
+ for (const bucket of this.vtxosByAddress.values()) {
31
+ for (const vtxo of bucket) {
32
+ if ((0, vtxoOwnership_1.isVtxoForScript)(vtxo, script)) {
33
+ allMatches.push(vtxo);
34
+ }
35
+ }
36
+ }
37
+ // Dedup by outpoint (last-write-wins across address buckets)
38
+ return mergeByKey([], allMatches, (item) => `${item.txid}:${item.vout}`);
39
+ }
40
+ async saveVtxosForScript(key, vtxos) {
41
+ if (!key.address) {
42
+ throw new Error("InMemoryWalletRepository requires an address");
43
+ }
44
+ for (const vtxo of vtxos) {
45
+ if (!(0, vtxoOwnership_1.isVtxoForScript)(vtxo, key.script)) {
46
+ throw new Error(`VTXO ${vtxo.txid}:${vtxo.vout} script mismatch: expected ${key.script}, got ${vtxo.script}`);
47
+ }
48
+ }
49
+ return this.saveVtxos(key.address, vtxos);
50
+ }
51
+ async deleteVtxosForScript(script) {
52
+ for (const [address, bucket] of this.vtxosByAddress.entries()) {
53
+ const next = bucket.filter((v) => !(0, vtxoOwnership_1.isVtxoForScript)(v, script));
54
+ if (next.length === 0) {
55
+ this.vtxosByAddress.delete(address);
56
+ }
57
+ else {
58
+ this.vtxosByAddress.set(address, next);
59
+ }
60
+ }
61
+ }
27
62
  async getUtxos(address) {
28
63
  return this.utxosByAddress.get(address) ?? [];
29
64
  }
@@ -6,6 +6,7 @@ const manager_1 = require("./manager");
6
6
  const schema_1 = require("./schema");
7
7
  const scriptFromAddress_1 = require("../scriptFromAddress");
8
8
  const utils_1 = require("../../worker/browser/utils");
9
+ const vtxoOwnership_1 = require("../../contracts/vtxoOwnership");
9
10
  /**
10
11
  * IndexedDB-based implementation of WalletRepository.
11
12
  */
@@ -144,6 +145,85 @@ class IndexedDBWalletRepository {
144
145
  throw error;
145
146
  }
146
147
  }
148
+ async getVtxosForScript(script) {
149
+ try {
150
+ const db = await this.getDB();
151
+ return new Promise((resolve, reject) => {
152
+ const transaction = db.transaction([db_1.STORE_VTXOS], "readonly");
153
+ const store = transaction.objectStore(db_1.STORE_VTXOS);
154
+ const index = store.index("script");
155
+ const request = index.getAll(script);
156
+ request.onerror = () => reject(request.error);
157
+ request.onsuccess = () => {
158
+ const results = request.result || [];
159
+ try {
160
+ // Defensive filter: only rows whose script matches.
161
+ const matching = results.filter((r) => r.script === script);
162
+ // Dedup same outpoint rows across address buckets.
163
+ // Work on raw rows so the address field is available
164
+ // for the canonicality tiebreaker.
165
+ const byOutpoint = new Map();
166
+ for (const row of matching) {
167
+ const outpoint = `${row.txid}:${row.vout}`;
168
+ const existing = byOutpoint.get(outpoint);
169
+ if (!existing) {
170
+ byOutpoint.set(outpoint, row);
171
+ continue;
172
+ }
173
+ if (shouldReplaceVtxo(existing, row)) {
174
+ byOutpoint.set(outpoint, row);
175
+ }
176
+ }
177
+ resolve(Array.from(byOutpoint.values()).map(deserializeVtxoWithBackfill));
178
+ }
179
+ catch (err) {
180
+ reject(err);
181
+ }
182
+ };
183
+ });
184
+ }
185
+ catch (error) {
186
+ console.error(`Failed to get VTXOs for script ${script}:`, error);
187
+ throw error;
188
+ }
189
+ }
190
+ async saveVtxosForScript(key, vtxos) {
191
+ if (!key.address) {
192
+ throw new Error("IndexedDBWalletRepository requires an address");
193
+ }
194
+ for (const vtxo of vtxos) {
195
+ if (!(0, vtxoOwnership_1.isVtxoForScript)(vtxo, key.script)) {
196
+ throw new Error(`VTXO ${vtxo.txid}:${vtxo.vout} script mismatch: expected ${key.script}, got ${vtxo.script}`);
197
+ }
198
+ }
199
+ return this.saveVtxos(key.address, vtxos);
200
+ }
201
+ async deleteVtxosForScript(script) {
202
+ try {
203
+ const db = await this.getDB();
204
+ return new Promise((resolve, reject) => {
205
+ const transaction = db.transaction([db_1.STORE_VTXOS], "readwrite");
206
+ const store = transaction.objectStore(db_1.STORE_VTXOS);
207
+ const index = store.index("script");
208
+ const request = index.openCursor(IDBKeyRange.only(script));
209
+ request.onerror = () => reject(request.error);
210
+ request.onsuccess = () => {
211
+ const cursor = request.result;
212
+ if (cursor) {
213
+ cursor.delete();
214
+ cursor.continue();
215
+ }
216
+ else {
217
+ resolve();
218
+ }
219
+ };
220
+ });
221
+ }
222
+ catch (error) {
223
+ console.error(`Failed to clear VTXOs for script ${script}:`, error);
224
+ throw error;
225
+ }
226
+ }
147
227
  async getUtxos(address) {
148
228
  try {
149
229
  const db = await this.getDB();
@@ -355,3 +435,40 @@ function deserializeVtxoWithBackfill(o) {
355
435
  }
356
436
  return (0, db_1.deserializeVtxo)(o);
357
437
  }
438
+ function isCanonicalRow(row) {
439
+ try {
440
+ return (0, scriptFromAddress_1.scriptFromArkAddress)(row.address) === row.script;
441
+ }
442
+ catch {
443
+ return false;
444
+ }
445
+ }
446
+ function shouldReplaceVtxo(existing, incoming) {
447
+ const existingCanonical = isCanonicalRow(existing);
448
+ const incomingCanonical = isCanonicalRow(incoming);
449
+ if (incomingCanonical && !existingCanonical)
450
+ return true;
451
+ if (existingCanonical && !incomingCanonical)
452
+ return false;
453
+ // Tie on canonicality, check lifecycle completeness
454
+ const existingWeight = getLifecycleWeight(existing);
455
+ const incomingWeight = getLifecycleWeight(incoming);
456
+ if (incomingWeight > existingWeight)
457
+ return true;
458
+ if (existingWeight > incomingWeight)
459
+ return false;
460
+ // Tie on weight, stable sort by address
461
+ return incoming.address < existing.address;
462
+ }
463
+ function getLifecycleWeight(v) {
464
+ let weight = 0;
465
+ if (v.isSpent !== undefined)
466
+ weight += 1;
467
+ if (v.spentBy)
468
+ weight += 2;
469
+ if (v.settledBy)
470
+ weight += 2;
471
+ if (v.arkTxId)
472
+ weight += 2;
473
+ return weight;
474
+ }
@@ -3,6 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.RealmWalletRepository = void 0;
4
4
  const serialization_1 = require("../serialization");
5
5
  const scriptFromAddress_1 = require("../scriptFromAddress");
6
+ const vtxoOwnership_1 = require("../../contracts/vtxoOwnership");
6
7
  /**
7
8
  * Realm-based implementation of WalletRepository.
8
9
  *
@@ -88,6 +89,33 @@ class RealmWalletRepository {
88
89
  this.realm.delete(toDelete);
89
90
  });
90
91
  }
92
+ async getVtxosForScript(script) {
93
+ await this.ensureInit();
94
+ const results = this.realm
95
+ .objects("ArkVtxo")
96
+ .filtered("script == $0", script);
97
+ return [...results].map(vtxoObjectToDomain);
98
+ }
99
+ async saveVtxosForScript(key, vtxos) {
100
+ if (!key.address) {
101
+ throw new Error("RealmWalletRepository requires an address");
102
+ }
103
+ for (const vtxo of vtxos) {
104
+ if (!(0, vtxoOwnership_1.isVtxoForScript)(vtxo, key.script)) {
105
+ throw new Error(`VTXO ${vtxo.txid}:${vtxo.vout} script mismatch: expected ${key.script}, got ${vtxo.script}`);
106
+ }
107
+ }
108
+ return this.saveVtxos(key.address, vtxos);
109
+ }
110
+ async deleteVtxosForScript(script) {
111
+ await this.ensureInit();
112
+ this.realm.write(() => {
113
+ const toDelete = this.realm
114
+ .objects("ArkVtxo")
115
+ .filtered("script == $0", script);
116
+ this.realm.delete(toDelete);
117
+ });
118
+ }
91
119
  // ── UTXO management ────────────────────────────────────────────────
92
120
  async getUtxos(address) {
93
121
  await this.ensureInit();