@arkade-os/sdk 0.4.23 → 0.4.24

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 (40) hide show
  1. package/README.md +21 -1
  2. package/dist/cjs/contracts/contractManager.js +29 -4
  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 +60 -0
  9. package/dist/cjs/index.js +3 -3
  10. package/dist/cjs/script/base.js +12 -47
  11. package/dist/cjs/script/tapscript.js +97 -73
  12. package/dist/cjs/utils/timelock.js +59 -0
  13. package/dist/cjs/utils/unknownFields.js +2 -39
  14. package/dist/cjs/wallet/serviceWorker/wallet-message-handler.js +59 -9
  15. package/dist/cjs/wallet/unroll.js +79 -67
  16. package/dist/cjs/wallet/wallet.js +78 -8
  17. package/dist/cjs/worker/expo/processors/contractPollProcessor.js +7 -2
  18. package/dist/esm/contracts/contractManager.js +29 -4
  19. package/dist/esm/contracts/contractWatcher.js +9 -3
  20. package/dist/esm/contracts/handlers/default.js +2 -1
  21. package/dist/esm/contracts/handlers/delegate.js +2 -1
  22. package/dist/esm/contracts/handlers/helpers.js +1 -22
  23. package/dist/esm/contracts/handlers/vhtlc.js +2 -1
  24. package/dist/esm/contracts/vtxoOwnership.js +53 -0
  25. package/dist/esm/index.js +1 -1
  26. package/dist/esm/script/base.js +12 -14
  27. package/dist/esm/script/tapscript.js +97 -40
  28. package/dist/esm/utils/timelock.js +22 -0
  29. package/dist/esm/utils/unknownFields.js +2 -6
  30. package/dist/esm/wallet/serviceWorker/wallet-message-handler.js +59 -9
  31. package/dist/esm/wallet/unroll.js +78 -67
  32. package/dist/esm/wallet/wallet.js +76 -6
  33. package/dist/esm/worker/expo/processors/contractPollProcessor.js +7 -2
  34. package/dist/types/contracts/handlers/helpers.d.ts +0 -9
  35. package/dist/types/contracts/vtxoOwnership.d.ts +25 -0
  36. package/dist/types/index.d.ts +1 -1
  37. package/dist/types/script/tapscript.d.ts +4 -0
  38. package/dist/types/utils/timelock.d.ts +9 -0
  39. package/dist/types/wallet/unroll.d.ts +10 -0
  40. package/package.json +1 -1
@@ -1,43 +1,10 @@
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
3
  exports.CLTVMultisigTapscript = exports.ConditionMultisigTapscript = exports.ConditionCSVMultisigTapscript = exports.CSVMultisigTapscript = exports.MultisigTapscript = exports.TapscriptType = void 0;
37
4
  exports.decodeTapscript = decodeTapscript;
38
- const bip68 = __importStar(require("bip68"));
39
5
  const btc_signer_1 = require("@scure/btc-signer");
40
6
  const base_1 = require("@scure/base");
7
+ const timelock_1 = require("../utils/timelock");
41
8
  const MinimalScriptNum = (0, btc_signer_1.ScriptNum)(undefined, true);
42
9
  var TapscriptType;
43
10
  (function (TapscriptType) {
@@ -271,9 +238,7 @@ var CSVMultisigTapscript;
271
238
  throw new Error(`Invalid pubkey length: expected 32, got ${pubkey.length}`);
272
239
  }
273
240
  }
274
- const sequence = MinimalScriptNum.encode(BigInt(bip68.encode(params.timelock.type === "blocks"
275
- ? { blocks: Number(params.timelock.value) }
276
- : { seconds: Number(params.timelock.value) })));
241
+ const sequence = MinimalScriptNum.encode(BigInt((0, timelock_1.timelockToSequence)(params.timelock)));
277
242
  const asm = [
278
243
  sequence.length === 1 ? sequence[0] : sequence,
279
244
  "CHECKSEQUENCEVERIFY",
@@ -296,17 +261,12 @@ var CSVMultisigTapscript;
296
261
  if (script.length === 0) {
297
262
  throw new Error("Failed to decode: script is empty");
298
263
  }
299
- const asm = btc_signer_1.Script.decode(script);
300
- if (asm.length < 3) {
301
- throw new Error(`Invalid script: too short (expected at least 3)`);
264
+ const isValid = isScriptValid(script);
265
+ if (isValid instanceof Error) {
266
+ throw isValid;
302
267
  }
268
+ const asm = btc_signer_1.Script.decode(script);
303
269
  const sequence = asm[0];
304
- if (typeof sequence === "string") {
305
- throw new Error("Invalid script: expected sequence number");
306
- }
307
- if (asm[1] !== "CHECKSEQUENCEVERIFY" || asm[2] !== "DROP") {
308
- throw new Error("Invalid script: expected CHECKSEQUENCEVERIFY DROP");
309
- }
310
270
  const multisigScript = new Uint8Array(btc_signer_1.Script.encode(asm.slice(3)));
311
271
  let multisig;
312
272
  try {
@@ -322,10 +282,7 @@ var CSVMultisigTapscript;
322
282
  else {
323
283
  sequenceNum = Number(MinimalScriptNum.decode(sequence));
324
284
  }
325
- const decodedTimelock = bip68.decode(sequenceNum);
326
- const timelock = decodedTimelock.blocks !== undefined
327
- ? { type: "blocks", value: BigInt(decodedTimelock.blocks) }
328
- : { type: "seconds", value: BigInt(decodedTimelock.seconds) };
285
+ const timelock = (0, timelock_1.sequenceToTimelock)(sequenceNum);
329
286
  const reconstructed = encode({
330
287
  timelock,
331
288
  ...multisig.params,
@@ -348,6 +305,21 @@ var CSVMultisigTapscript;
348
305
  return tapscript.type === TapscriptType.CSVMultisig;
349
306
  }
350
307
  CSVMultisigTapscript.is = is;
308
+ function isScriptValid(script) {
309
+ const asm = btc_signer_1.Script.decode(script);
310
+ if (asm.length < 3) {
311
+ return new Error(`Invalid script: too short (expected at least 3)`);
312
+ }
313
+ const sequence = asm[0];
314
+ if (typeof sequence === "string") {
315
+ return new Error("Invalid script: expected sequence number");
316
+ }
317
+ if (asm[1] !== "CHECKSEQUENCEVERIFY" || asm[2] !== "DROP") {
318
+ return new Error("Invalid script: expected CHECKSEQUENCEVERIFY DROP");
319
+ }
320
+ return true;
321
+ }
322
+ CSVMultisigTapscript.isScriptValid = isScriptValid;
351
323
  })(CSVMultisigTapscript || (exports.CSVMultisigTapscript = CSVMultisigTapscript = {}));
352
324
  /**
353
325
  * Combines a condition script with an exit closure. The resulting script requires
@@ -382,18 +354,14 @@ var ConditionCSVMultisigTapscript;
382
354
  if (script.length === 0) {
383
355
  throw new Error("Failed to decode: script is empty");
384
356
  }
385
- const asm = btc_signer_1.Script.decode(script);
386
- if (asm.length < 1) {
387
- throw new Error(`Invalid script: too short (expected at least 1)`);
388
- }
389
- let verifyIndex = -1;
390
- for (let i = asm.length - 1; i >= 0; i--) {
391
- if (asm[i] === "VERIFY") {
392
- verifyIndex = i;
393
- }
357
+ const isValid = isScriptValid(script);
358
+ if (isValid instanceof Error) {
359
+ throw isValid;
394
360
  }
361
+ const asm = btc_signer_1.Script.decode(script);
362
+ let verifyIndex = getVerifyIndex(asm);
395
363
  if (verifyIndex === -1) {
396
- throw new Error("Invalid script: missing VERIFY operation");
364
+ throw Error("Invalid script: missing VERIFY operation");
397
365
  }
398
366
  const conditionScript = new Uint8Array(btc_signer_1.Script.encode(asm.slice(0, verifyIndex)));
399
367
  const csvMultisigScript = new Uint8Array(btc_signer_1.Script.encode(asm.slice(verifyIndex + 1)));
@@ -426,6 +394,28 @@ var ConditionCSVMultisigTapscript;
426
394
  return tapscript.type === TapscriptType.ConditionCSVMultisig;
427
395
  }
428
396
  ConditionCSVMultisigTapscript.is = is;
397
+ function getVerifyIndex(asm) {
398
+ let verifyIndex = -1;
399
+ for (let i = asm.length - 1; i >= 0; i--) {
400
+ if (asm[i] === "VERIFY") {
401
+ verifyIndex = i;
402
+ return verifyIndex;
403
+ }
404
+ }
405
+ return verifyIndex;
406
+ }
407
+ function isScriptValid(script) {
408
+ const asm = btc_signer_1.Script.decode(script);
409
+ if (asm.length < 1) {
410
+ return new Error(`Invalid script: too short (expected at least 1)`);
411
+ }
412
+ let verifyIndex = getVerifyIndex(asm);
413
+ if (verifyIndex === -1) {
414
+ return new Error("Invalid script: missing VERIFY operation");
415
+ }
416
+ return true;
417
+ }
418
+ ConditionCSVMultisigTapscript.isScriptValid = isScriptValid;
429
419
  })(ConditionCSVMultisigTapscript || (exports.ConditionCSVMultisigTapscript = ConditionCSVMultisigTapscript = {}));
430
420
  /**
431
421
  * Combines a condition script with a forfeit closure. The resulting script requires
@@ -460,18 +450,14 @@ var ConditionMultisigTapscript;
460
450
  if (script.length === 0) {
461
451
  throw new Error("Failed to decode: script is empty");
462
452
  }
463
- const asm = btc_signer_1.Script.decode(script);
464
- if (asm.length < 1) {
465
- throw new Error(`Invalid script: too short (expected at least 1)`);
466
- }
467
- let verifyIndex = -1;
468
- for (let i = asm.length - 1; i >= 0; i--) {
469
- if (asm[i] === "VERIFY") {
470
- verifyIndex = i;
471
- }
453
+ const isValid = isScriptValid(script);
454
+ if (isValid instanceof Error) {
455
+ throw isValid;
472
456
  }
457
+ const asm = btc_signer_1.Script.decode(script);
458
+ let verifyIndex = getVerifyIndex(asm);
473
459
  if (verifyIndex === -1) {
474
- throw new Error("Invalid script: missing VERIFY operation");
460
+ throw Error("Invalid script: missing VERIFY operation");
475
461
  }
476
462
  const conditionScript = new Uint8Array(btc_signer_1.Script.encode(asm.slice(0, verifyIndex)));
477
463
  const multisigScript = new Uint8Array(btc_signer_1.Script.encode(asm.slice(verifyIndex + 1)));
@@ -504,6 +490,28 @@ var ConditionMultisigTapscript;
504
490
  return tapscript.type === TapscriptType.ConditionMultisig;
505
491
  }
506
492
  ConditionMultisigTapscript.is = is;
493
+ function getVerifyIndex(asm) {
494
+ let verifyIndex = -1;
495
+ for (let i = asm.length - 1; i >= 0; i--) {
496
+ if (asm[i] === "VERIFY") {
497
+ verifyIndex = i;
498
+ return verifyIndex;
499
+ }
500
+ }
501
+ return verifyIndex;
502
+ }
503
+ function isScriptValid(script) {
504
+ const asm = btc_signer_1.Script.decode(script);
505
+ if (asm.length < 1) {
506
+ return new Error(`Invalid script: too short (expected at least 1)`);
507
+ }
508
+ let verifyIndex = getVerifyIndex(asm);
509
+ if (verifyIndex === -1) {
510
+ return new Error("Invalid script: missing VERIFY operation");
511
+ }
512
+ return true;
513
+ }
514
+ ConditionMultisigTapscript.isScriptValid = isScriptValid;
507
515
  })(ConditionMultisigTapscript || (exports.ConditionMultisigTapscript = ConditionMultisigTapscript = {}));
508
516
  /**
509
517
  * Implements an absolute timelock (CLTV) script combined with a forfeit closure.
@@ -544,10 +552,11 @@ var CLTVMultisigTapscript;
544
552
  if (script.length === 0) {
545
553
  throw new Error("Failed to decode: script is empty");
546
554
  }
547
- const asm = btc_signer_1.Script.decode(script);
548
- if (asm.length < 3) {
549
- throw new Error(`Invalid script: too short (expected at least 3)`);
555
+ const isValid = isScriptValid(script);
556
+ if (isValid instanceof Error) {
557
+ throw isValid;
550
558
  }
559
+ const asm = btc_signer_1.Script.decode(script);
551
560
  const locktime = asm[0];
552
561
  if (typeof locktime === "string") {
553
562
  throw new Error("Invalid script: expected locktime number");
@@ -592,4 +601,19 @@ var CLTVMultisigTapscript;
592
601
  return tapscript.type === TapscriptType.CLTVMultisig;
593
602
  }
594
603
  CLTVMultisigTapscript.is = is;
604
+ function isScriptValid(script) {
605
+ const asm = btc_signer_1.Script.decode(script);
606
+ if (asm.length < 3) {
607
+ return new Error(`Invalid script: too short (expected at least 3)`);
608
+ }
609
+ const locktime = asm[0];
610
+ if (typeof locktime === "string") {
611
+ return new Error("Invalid script: expected locktime as number or bytes");
612
+ }
613
+ if (asm[1] !== "CHECKLOCKTIMEVERIFY" || asm[2] !== "DROP") {
614
+ return new Error("Invalid script: expected CHECKLOCKTIMEVERIFY DROP");
615
+ }
616
+ return true;
617
+ }
618
+ CLTVMultisigTapscript.isScriptValid = isScriptValid;
595
619
  })(CLTVMultisigTapscript || (exports.CLTVMultisigTapscript = CLTVMultisigTapscript = {}));
@@ -0,0 +1,59 @@
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
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.timelockToSequence = timelockToSequence;
37
+ exports.sequenceToTimelock = sequenceToTimelock;
38
+ const bip68 = __importStar(require("bip68"));
39
+ /**
40
+ * Convert RelativeTimelock to BIP68 sequence number.
41
+ */
42
+ function timelockToSequence(timelock) {
43
+ return bip68.encode(timelock.type === "blocks"
44
+ ? { blocks: Number(timelock.value) }
45
+ : { seconds: Number(timelock.value) });
46
+ }
47
+ /**
48
+ * Convert BIP68 sequence number back to RelativeTimelock.
49
+ */
50
+ function sequenceToTimelock(sequence) {
51
+ const decoded = bip68.decode(sequence);
52
+ if ("blocks" in decoded && decoded.blocks !== undefined) {
53
+ return { type: "blocks", value: BigInt(decoded.blocks) };
54
+ }
55
+ if ("seconds" in decoded && decoded.seconds !== undefined) {
56
+ return { type: "seconds", value: BigInt(decoded.seconds) };
57
+ }
58
+ throw new Error(`Invalid BIP68 sequence: ${sequence}`);
59
+ }
@@ -1,44 +1,11 @@
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
3
  exports.VtxoTreeExpiry = exports.CosignerPublicKey = exports.ConditionWitness = exports.VtxoTaprootTree = exports.ArkPsbtFieldKeyType = exports.ArkPsbtFieldKey = void 0;
37
4
  exports.setArkPsbtField = setArkPsbtField;
38
5
  exports.getArkPsbtFields = getArkPsbtFields;
39
- const bip68 = __importStar(require("bip68"));
40
6
  const btc_signer_1 = require("@scure/btc-signer");
41
7
  const base_1 = require("@scure/base");
8
+ const timelock_1 = require("./timelock");
42
9
  /**
43
10
  * ArkPsbtFieldKey are the available key names for the Arkade PSBT custom fields.
44
11
  */
@@ -184,11 +151,7 @@ exports.VtxoTreeExpiry = {
184
151
  const v = (0, btc_signer_1.ScriptNum)(6, true).decode(unknown[1]);
185
152
  if (!v)
186
153
  return null;
187
- const { blocks, seconds } = bip68.decode(Number(v));
188
- return {
189
- type: blocks ? "blocks" : "seconds",
190
- value: BigInt(blocks ?? seconds ?? 0),
191
- };
154
+ return (0, timelock_1.sequenceToTimelock)(Number(v));
192
155
  }),
193
156
  };
194
157
  const encodedPsbtFieldKey = Object.fromEntries(Object.values(ArkPsbtFieldKey).map((key) => [
@@ -5,6 +5,8 @@ const indexer_1 = require("../../providers/indexer");
5
5
  const index_1 = require("../index");
6
6
  const utils_1 = require("../utils");
7
7
  const transactionHistory_1 = require("../../utils/transactionHistory");
8
+ const vtxoOwnership_1 = require("../../contracts/vtxoOwnership");
9
+ const scriptFromAddress_1 = require("../../repositories/scriptFromAddress");
8
10
  class WalletNotInitializedError extends Error {
9
11
  constructor() {
10
12
  super("Wallet handler not initialized");
@@ -587,11 +589,45 @@ class WalletMessageHandler {
587
589
  const { newVtxos, spentVtxos } = funds;
588
590
  if (newVtxos.length + spentVtxos.length === 0)
589
591
  return;
590
- // save virtual outputs using unified repository
591
- await this.walletRepository?.saveVtxos(address, [
592
- ...newVtxos,
593
- ...spentVtxos,
594
- ]);
592
+ // Save virtual outputs using unified repository. The
593
+ // event may carry rows for several scripts (other
594
+ // contracts the wallet watches), so split by script and
595
+ // save each bucket under its own contract address rather
596
+ // than saving a mixed-script array under one address.
597
+ const byScript = new Map();
598
+ for (const v of [...newVtxos, ...spentVtxos]) {
599
+ if (!v.script) {
600
+ // Without a script we can't route the row to the
601
+ // right contract bucket; surface the drop instead
602
+ // of silently losing the VTXO.
603
+ console.warn(`WalletMessageHandler.notifyIncomingFunds: dropping VTXO without script ${v.txid}:${v.vout}`);
604
+ continue;
605
+ }
606
+ const arr = byScript.get(v.script) ?? [];
607
+ arr.push(v);
608
+ byScript.set(v.script, arr);
609
+ }
610
+ let walletScript;
611
+ try {
612
+ walletScript = (0, scriptFromAddress_1.scriptFromArkAddress)(address);
613
+ }
614
+ catch {
615
+ walletScript = undefined;
616
+ }
617
+ const cm = await this.readonlyWallet.getContractManager();
618
+ const contracts = await cm.getContracts();
619
+ const addrByScript = new Map(contracts.map((c) => [c.script, c.address]));
620
+ for (const [script, vtxos] of byScript) {
621
+ const filtered = (0, vtxoOwnership_1.warnAndFilterVtxosForScript)(vtxos, script, "WalletMessageHandler.notifyIncomingFunds");
622
+ if (filtered.length === 0)
623
+ continue;
624
+ const targetAddress = script === walletScript
625
+ ? address
626
+ : addrByScript.get(script);
627
+ if (!targetAddress)
628
+ continue;
629
+ await this.walletRepository?.saveVtxos(targetAddress, filtered);
630
+ }
595
631
  // notify all clients about the virtual output state update
596
632
  this.scheduleForNextTick(() => this.tagged({
597
633
  type: "VTXO_UPDATE",
@@ -803,17 +839,31 @@ class WalletMessageHandler {
803
839
  }
804
840
  }
805
841
  };
806
- // Aggregate virtual outputs from all contract addresses
842
+ // Aggregate virtual outputs from all contract addresses. Address
843
+ // buckets may carry legacy duplicate rows from other contracts; gate
844
+ // each bucket by its owning contract script before deduplication so a
845
+ // wrong-script row never wins the txid:vout race.
807
846
  const manager = await this.readonlyWallet.getContractManager();
808
847
  const contracts = await manager.getContracts();
809
848
  for (const contract of contracts) {
810
849
  const vtxos = await this.walletRepository.getVtxos(contract.address);
811
- addVtxos(vtxos);
850
+ addVtxos((0, vtxoOwnership_1.filterVtxosForScript)(vtxos, contract.script));
812
851
  }
813
- // Also check the wallet's primary address
852
+ // Also check the wallet's primary address. Decode it to its script
853
+ // and apply the same script gate. Failing to decode the wallet's own
854
+ // address is a structural bug — surfacing the error is safer than
855
+ // silently dropping the primary bucket and zeroing the user's
856
+ // visible balance.
814
857
  const walletAddress = await this.readonlyWallet.getAddress();
858
+ let walletScript;
859
+ try {
860
+ walletScript = (0, scriptFromAddress_1.scriptFromArkAddress)(walletAddress);
861
+ }
862
+ catch (e) {
863
+ throw new Error(`WalletMessageHandler.getVtxosFromRepo: failed to derive script from wallet address ${walletAddress}: ${e instanceof Error ? e.message : String(e)}`);
864
+ }
815
865
  const walletVtxos = await this.walletRepository.getVtxos(walletAddress);
816
- addVtxos(walletVtxos);
866
+ addVtxos((0, vtxoOwnership_1.filterVtxosForScript)(walletVtxos, walletScript));
817
867
  return allVtxos;
818
868
  }
819
869
  /**
@@ -1,9 +1,10 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.Unroll = void 0;
4
+ exports.prepareUnrollTransaction = prepareUnrollTransaction;
4
5
  const base_1 = require("@scure/base");
5
6
  const btc_signer_1 = require("@scure/btc-signer");
6
- const helpers_1 = require("../contracts/handlers/helpers");
7
+ const timelock_1 = require("../utils/timelock");
7
8
  const indexer_1 = require("../providers/indexer");
8
9
  const base_2 = require("../script/base");
9
10
  const txSizeEstimator_1 = require("../utils/txSizeEstimator");
@@ -129,10 +130,12 @@ var Unroll;
129
130
  // finalize Arkade transaction
130
131
  tx.finalize();
131
132
  }
133
+ const pkg = await this.bumper.bumpP2A(tx);
132
134
  return {
133
135
  type: StepType.UNROLL,
134
136
  tx,
135
- do: doUnroll(this.bumper, this.explorer, tx),
137
+ pkg,
138
+ do: doUnroll(this.explorer, pkg),
136
139
  };
137
140
  }
138
141
  /**
@@ -164,79 +167,88 @@ var Unroll;
164
167
  * @returns the txid of the transaction spending the unrolled funds
165
168
  */
166
169
  async function completeUnroll(wallet, vtxoTxids, outputAddress) {
167
- const chainTip = await wallet.onchainProvider.getChainTip();
168
- let vtxos = await wallet.getVtxos({ withUnrolled: true });
169
- vtxos = vtxos.filter((vtxo) => vtxoTxids.includes(vtxo.txid));
170
- if (vtxos.length === 0) {
171
- throw new Error("No vtxos to complete unroll");
172
- }
173
- const inputs = [];
174
- let totalAmount = 0n;
175
- const txWeightEstimator = txSizeEstimator_1.TxWeightEstimator.create();
176
- for (const vtxo of vtxos) {
177
- if (!vtxo.isUnrolled) {
178
- throw new Error(`Vtxo ${vtxo.txid}:${vtxo.vout} is not fully unrolled, use unroll first`);
179
- }
180
- const txStatus = await wallet.onchainProvider.getTxStatus(vtxo.txid);
181
- if (!txStatus.confirmed) {
182
- throw new Error(`tx ${vtxo.txid} is not confirmed`);
183
- }
184
- const exit = availableExitPath({ height: txStatus.blockHeight, time: txStatus.blockTime }, chainTip, vtxo);
185
- if (!exit) {
186
- throw new Error(`no available exit path found for vtxo ${vtxo.txid}:${vtxo.vout}`);
187
- }
188
- const spendingLeaf = base_2.VtxoScript.decode(vtxo.tapTree).findLeaf(base_1.hex.encode(exit.script));
189
- if (!spendingLeaf) {
190
- throw new Error(`spending leaf not found for vtxo ${vtxo.txid}:${vtxo.vout}`);
191
- }
192
- totalAmount += BigInt(vtxo.value);
193
- const sequence = (0, helpers_1.timelockToSequence)(exit.params.timelock);
194
- inputs.push({
195
- txid: vtxo.txid,
196
- index: vtxo.vout,
197
- tapLeafScript: [spendingLeaf],
198
- sequence,
199
- witnessUtxo: {
200
- amount: BigInt(vtxo.value),
201
- script: base_2.VtxoScript.decode(vtxo.tapTree).pkScript,
202
- },
203
- sighashType: btc_signer_1.SigHash.DEFAULT,
204
- });
205
- txWeightEstimator.addTapscriptInput(64, spendingLeaf[1].length, btc_signer_1.TaprootControlBlock.encode(spendingLeaf[0]).length);
206
- }
207
- const tx = new transaction_1.Transaction({ version: 2 });
208
- for (const input of inputs) {
209
- tx.addInput(input);
210
- }
211
- txWeightEstimator.addOutputAddress(outputAddress, wallet.network);
212
- let feeRate = await wallet.onchainProvider.getFeeRate();
213
- if (!feeRate || feeRate < wallet_1.Wallet.MIN_FEE_RATE) {
214
- feeRate = wallet_1.Wallet.MIN_FEE_RATE;
215
- }
216
- const feeAmount = txWeightEstimator.vsize().fee(BigInt(feeRate));
217
- if (feeAmount > totalAmount) {
218
- throw new Error("fee amount is greater than the total amount");
219
- }
220
- const sendAmount = totalAmount - feeAmount;
221
- if (sendAmount < BigInt(utils_1.DUST_AMOUNT)) {
222
- throw new Error("send amount is less than dust amount");
223
- }
224
- tx.addOutputAddress(outputAddress, sendAmount);
225
- const signedTx = await wallet.identity.sign(tx);
226
- signedTx.finalize();
170
+ const signedTx = await prepareUnrollTransaction(wallet, vtxoTxids, outputAddress);
227
171
  await wallet.onchainProvider.broadcastTransaction(signedTx.hex);
228
172
  return signedTx.id;
229
173
  }
230
174
  Unroll.completeUnroll = completeUnroll;
231
175
  })(Unroll || (exports.Unroll = Unroll = {}));
176
+ /**
177
+ * Prepares the transaction that spends the CSV path to complete unrolling a VTXO.
178
+ * @param wallet the wallet owning the VTXO(s)
179
+ * @param vtxoTxIds the txids of the VTXO(s) to complete unroll
180
+ * @param outputAddress the address to send the unrolled funds to
181
+ * @throws if the VTXO(s) are not fully unrolled, if the txids are not found, if the tx is not confirmed, if no exit path is found or not available
182
+ * @returns the transaction spending the unrolled funds
183
+ */
184
+ async function prepareUnrollTransaction(wallet, vtxoTxIds, outputAddress) {
185
+ const chainTip = await wallet.onchainProvider.getChainTip();
186
+ let vtxos = await wallet.getVtxos({ withUnrolled: true });
187
+ vtxos = vtxos.filter((vtxo) => vtxoTxIds.includes(vtxo.txid));
188
+ if (vtxos.length === 0) {
189
+ throw new Error("No vtxos to complete unroll");
190
+ }
191
+ const inputs = [];
192
+ let totalAmount = 0n;
193
+ const txWeightEstimator = txSizeEstimator_1.TxWeightEstimator.create();
194
+ for (const vtxo of vtxos) {
195
+ if (!vtxo.isUnrolled) {
196
+ throw new Error(`Vtxo ${vtxo.txid}:${vtxo.vout} is not fully unrolled, use unroll first`);
197
+ }
198
+ const txStatus = await wallet.onchainProvider.getTxStatus(vtxo.txid);
199
+ if (!txStatus.confirmed) {
200
+ throw new Error(`tx ${vtxo.txid} is not confirmed`);
201
+ }
202
+ const exit = availableExitPath({ height: txStatus.blockHeight, time: txStatus.blockTime }, chainTip, vtxo);
203
+ if (!exit) {
204
+ throw new Error(`no available exit path found for vtxo ${vtxo.txid}:${vtxo.vout}`);
205
+ }
206
+ const spendingLeaf = base_2.VtxoScript.decode(vtxo.tapTree).findLeaf(base_1.hex.encode(exit.script));
207
+ if (!spendingLeaf) {
208
+ throw new Error(`spending leaf not found for vtxo ${vtxo.txid}:${vtxo.vout}`);
209
+ }
210
+ totalAmount += BigInt(vtxo.value);
211
+ const sequence = (0, timelock_1.timelockToSequence)(exit.params.timelock);
212
+ inputs.push({
213
+ txid: vtxo.txid,
214
+ index: vtxo.vout,
215
+ tapLeafScript: [spendingLeaf],
216
+ sequence,
217
+ witnessUtxo: {
218
+ amount: BigInt(vtxo.value),
219
+ script: base_2.VtxoScript.decode(vtxo.tapTree).pkScript,
220
+ },
221
+ sighashType: btc_signer_1.SigHash.DEFAULT,
222
+ });
223
+ txWeightEstimator.addTapscriptInput(64, spendingLeaf[1].length, btc_signer_1.TaprootControlBlock.encode(spendingLeaf[0]).length);
224
+ }
225
+ const tx = new transaction_1.Transaction({ version: 2 });
226
+ for (const input of inputs) {
227
+ tx.addInput(input);
228
+ }
229
+ txWeightEstimator.addOutputAddress(outputAddress, wallet.network);
230
+ let feeRate = await wallet.onchainProvider.getFeeRate();
231
+ if (!feeRate || feeRate < wallet_1.Wallet.MIN_FEE_RATE) {
232
+ feeRate = wallet_1.Wallet.MIN_FEE_RATE;
233
+ }
234
+ const feeAmount = txWeightEstimator.vsize().fee(BigInt(feeRate));
235
+ if (feeAmount > totalAmount) {
236
+ throw new Error("fee amount is greater than the total amount");
237
+ }
238
+ const sendAmount = totalAmount - feeAmount;
239
+ if (sendAmount < BigInt(utils_1.DUST_AMOUNT)) {
240
+ throw new Error("send amount is less than dust amount");
241
+ }
242
+ tx.addOutputAddress(outputAddress, sendAmount, wallet.network);
243
+ const signedTx = await wallet.identity.sign(tx);
244
+ signedTx.finalize();
245
+ return signedTx;
246
+ }
232
247
  function sleep(ms) {
233
248
  return new Promise((resolve) => setTimeout(resolve, ms));
234
249
  }
235
- function doUnroll(bumper, onchainProvider, tx) {
236
- return async () => {
237
- const [parent, child] = await bumper.bumpP2A(tx);
238
- await onchainProvider.broadcastTransaction(parent, child);
239
- };
250
+ function doUnroll(onchainProvider, pkg) {
251
+ return () => onchainProvider.broadcastTransaction(...pkg).then(() => undefined);
240
252
  }
241
253
  function doWait(onchainProvider, txid) {
242
254
  return () => {