@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.
- package/README.md +21 -1
- package/dist/cjs/contracts/contractManager.js +29 -4
- package/dist/cjs/contracts/contractWatcher.js +9 -3
- package/dist/cjs/contracts/handlers/default.js +3 -2
- package/dist/cjs/contracts/handlers/delegate.js +3 -2
- package/dist/cjs/contracts/handlers/helpers.js +2 -58
- package/dist/cjs/contracts/handlers/vhtlc.js +7 -6
- package/dist/cjs/contracts/vtxoOwnership.js +60 -0
- package/dist/cjs/index.js +3 -3
- package/dist/cjs/script/base.js +12 -47
- package/dist/cjs/script/tapscript.js +97 -73
- package/dist/cjs/utils/timelock.js +59 -0
- package/dist/cjs/utils/unknownFields.js +2 -39
- package/dist/cjs/wallet/serviceWorker/wallet-message-handler.js +59 -9
- package/dist/cjs/wallet/unroll.js +79 -67
- package/dist/cjs/wallet/wallet.js +78 -8
- package/dist/cjs/worker/expo/processors/contractPollProcessor.js +7 -2
- package/dist/esm/contracts/contractManager.js +29 -4
- package/dist/esm/contracts/contractWatcher.js +9 -3
- package/dist/esm/contracts/handlers/default.js +2 -1
- package/dist/esm/contracts/handlers/delegate.js +2 -1
- package/dist/esm/contracts/handlers/helpers.js +1 -22
- package/dist/esm/contracts/handlers/vhtlc.js +2 -1
- package/dist/esm/contracts/vtxoOwnership.js +53 -0
- package/dist/esm/index.js +1 -1
- package/dist/esm/script/base.js +12 -14
- package/dist/esm/script/tapscript.js +97 -40
- package/dist/esm/utils/timelock.js +22 -0
- package/dist/esm/utils/unknownFields.js +2 -6
- package/dist/esm/wallet/serviceWorker/wallet-message-handler.js +59 -9
- package/dist/esm/wallet/unroll.js +78 -67
- package/dist/esm/wallet/wallet.js +76 -6
- package/dist/esm/worker/expo/processors/contractPollProcessor.js +7 -2
- package/dist/types/contracts/handlers/helpers.d.ts +0 -9
- package/dist/types/contracts/vtxoOwnership.d.ts +25 -0
- package/dist/types/index.d.ts +1 -1
- package/dist/types/script/tapscript.d.ts +4 -0
- package/dist/types/utils/timelock.d.ts +9 -0
- package/dist/types/wallet/unroll.d.ts +10 -0
- package/package.json +1 -1
|
@@ -37,8 +37,9 @@ const delegator_1 = require("./delegator");
|
|
|
37
37
|
const repositories_1 = require("../repositories");
|
|
38
38
|
const contractManager_1 = require("../contracts/contractManager");
|
|
39
39
|
const handlers_1 = require("../contracts/handlers");
|
|
40
|
-
const
|
|
40
|
+
const timelock_1 = require("../utils/timelock");
|
|
41
41
|
const syncCursors_1 = require("../utils/syncCursors");
|
|
42
|
+
const vtxoOwnership_1 = require("../contracts/vtxoOwnership");
|
|
42
43
|
const getArkadeServerUrl = ({ arkServerUrl, }) => arkServerUrl || _1.DEFAULT_ARKADE_SERVER_URL;
|
|
43
44
|
exports.getArkadeServerUrl = getArkadeServerUrl;
|
|
44
45
|
// Historical unilateral exit delay for mainnet (~7 days in seconds).
|
|
@@ -55,7 +56,7 @@ function dedupeTimelocks(timelocks) {
|
|
|
55
56
|
const seen = new Set();
|
|
56
57
|
const deduped = [];
|
|
57
58
|
for (const timelock of timelocks) {
|
|
58
|
-
const sequence = (0,
|
|
59
|
+
const sequence = (0, timelock_1.timelockToSequence)(timelock).toString();
|
|
59
60
|
if (seen.has(sequence))
|
|
60
61
|
continue;
|
|
61
62
|
seen.add(sequence);
|
|
@@ -648,7 +649,7 @@ class ReadonlyWallet {
|
|
|
648
649
|
watcherConfig: this.watcherConfig,
|
|
649
650
|
});
|
|
650
651
|
for (const csvTimelock of this.walletContractTimelocks) {
|
|
651
|
-
const csvTimelockStr = (0,
|
|
652
|
+
const csvTimelockStr = (0, timelock_1.timelockToSequence)(csvTimelock).toString();
|
|
652
653
|
const defaultScript = new default_1.DefaultVtxo.Script({
|
|
653
654
|
pubKey: this.offchainTapscript.options.pubKey,
|
|
654
655
|
serverPubKey: this.offchainTapscript.options.serverPubKey,
|
|
@@ -1830,7 +1831,7 @@ class Wallet extends ReadonlyWallet {
|
|
|
1830
1831
|
}
|
|
1831
1832
|
}
|
|
1832
1833
|
const createdAt = Date.now();
|
|
1833
|
-
const
|
|
1834
|
+
const primaryAddr = this.arkAddress.encode();
|
|
1834
1835
|
// Only save a change virtual output for preconfirmed coins (those with a batchExpiry).
|
|
1835
1836
|
// Inputs without a batchExpiry are already settled/unrolled and don't need tracking.
|
|
1836
1837
|
let changeVtxo;
|
|
@@ -1857,8 +1858,45 @@ class Wallet extends ReadonlyWallet {
|
|
|
1857
1858
|
script: base_1.hex.encode(this.offchainTapscript.pkScript),
|
|
1858
1859
|
};
|
|
1859
1860
|
}
|
|
1860
|
-
|
|
1861
|
-
|
|
1861
|
+
// Route spent rows to their owning contract bucket. The wallet's
|
|
1862
|
+
// primary contract is registered with the manager at boot, so
|
|
1863
|
+
// `addrByScript` already includes it; in a multi-contract spend
|
|
1864
|
+
// each input may belong to a different contract.
|
|
1865
|
+
const contracts = await cm.getContracts();
|
|
1866
|
+
const addrByScript = new Map(contracts.map((c) => [c.script, c.address]));
|
|
1867
|
+
const spentByScript = new Map();
|
|
1868
|
+
for (const v of spentVtxos) {
|
|
1869
|
+
if (!v.script) {
|
|
1870
|
+
throw new Error(`Wallet.updateDbAfterOffchainTx: spent VTXO ${v.txid}:${v.vout} has no script`);
|
|
1871
|
+
}
|
|
1872
|
+
const arr = spentByScript.get(v.script) ?? [];
|
|
1873
|
+
arr.push(v);
|
|
1874
|
+
spentByScript.set(v.script, arr);
|
|
1875
|
+
}
|
|
1876
|
+
const byAddress = new Map();
|
|
1877
|
+
for (const [script, vtxos] of spentByScript) {
|
|
1878
|
+
// User-initiated send path: a wrong-script row here means the
|
|
1879
|
+
// wallet is about to record ownership against the wrong
|
|
1880
|
+
// contract — fail loudly rather than persist inconsistent state.
|
|
1881
|
+
(0, vtxoOwnership_1.validateVtxosForScript)(vtxos, script, "Wallet.updateDbAfterOffchainTx");
|
|
1882
|
+
const targetAddr = addrByScript.get(script);
|
|
1883
|
+
if (!targetAddr) {
|
|
1884
|
+
throw new Error(`Wallet.updateDbAfterOffchainTx: no contract owns script ${script}`);
|
|
1885
|
+
}
|
|
1886
|
+
const bucket = byAddress.get(targetAddr) ?? [];
|
|
1887
|
+
bucket.push(...vtxos);
|
|
1888
|
+
byAddress.set(targetAddr, bucket);
|
|
1889
|
+
}
|
|
1890
|
+
// Change is always primary-script by construction.
|
|
1891
|
+
if (changeVtxo) {
|
|
1892
|
+
const bucket = byAddress.get(primaryAddr) ?? [];
|
|
1893
|
+
bucket.push(changeVtxo);
|
|
1894
|
+
byAddress.set(primaryAddr, bucket);
|
|
1895
|
+
}
|
|
1896
|
+
for (const [addr, vtxos] of byAddress) {
|
|
1897
|
+
await this.walletRepository.saveVtxos(addr, vtxos);
|
|
1898
|
+
}
|
|
1899
|
+
await this.walletRepository.saveTransactions(primaryAddr, [
|
|
1862
1900
|
{
|
|
1863
1901
|
key: {
|
|
1864
1902
|
boardingTxid: "",
|
|
@@ -1874,12 +1912,12 @@ class Wallet extends ReadonlyWallet {
|
|
|
1874
1912
|
}
|
|
1875
1913
|
catch (e) {
|
|
1876
1914
|
console.warn("error saving offchain tx to repository", e);
|
|
1915
|
+
throw e;
|
|
1877
1916
|
}
|
|
1878
1917
|
}
|
|
1879
1918
|
// mark virtual outputs as spent/settled, remove boarding inputs
|
|
1880
1919
|
async updateDbAfterSettle(inputs, commitmentTxid) {
|
|
1881
1920
|
try {
|
|
1882
|
-
const addr = this.arkAddress.encode();
|
|
1883
1921
|
const boardingAddress = await this.getBoardingAddress();
|
|
1884
1922
|
const spentVtxos = [];
|
|
1885
1923
|
const inputArkTxIds = new Set();
|
|
@@ -1912,7 +1950,38 @@ class Wallet extends ReadonlyWallet {
|
|
|
1912
1950
|
}
|
|
1913
1951
|
}
|
|
1914
1952
|
if (spentVtxos.length > 0) {
|
|
1915
|
-
|
|
1953
|
+
// Route settled rows to their owning contract bucket. In a
|
|
1954
|
+
// multi-contract settle the inputs may belong to several
|
|
1955
|
+
// contracts; the wallet's primary contract is registered with
|
|
1956
|
+
// the manager at boot, so its address is in `addrByScript`
|
|
1957
|
+
// alongside the rest.
|
|
1958
|
+
const contracts = await cm.getContracts();
|
|
1959
|
+
const addrByScript = new Map(contracts.map((c) => [c.script, c.address]));
|
|
1960
|
+
const byAddress = new Map();
|
|
1961
|
+
const byScript = new Map();
|
|
1962
|
+
for (const v of spentVtxos) {
|
|
1963
|
+
if (!v.script) {
|
|
1964
|
+
throw new Error(`Wallet.updateDbAfterSettle: spent VTXO ${v.txid}:${v.vout} has no script`);
|
|
1965
|
+
}
|
|
1966
|
+
const arr = byScript.get(v.script) ?? [];
|
|
1967
|
+
arr.push(v);
|
|
1968
|
+
byScript.set(v.script, arr);
|
|
1969
|
+
}
|
|
1970
|
+
for (const [script, vtxos] of byScript) {
|
|
1971
|
+
// User-initiated settle path: refuse to record a settle
|
|
1972
|
+
// against the wrong script.
|
|
1973
|
+
(0, vtxoOwnership_1.validateVtxosForScript)(vtxos, script, "Wallet.updateDbAfterSettle");
|
|
1974
|
+
const targetAddr = addrByScript.get(script);
|
|
1975
|
+
if (!targetAddr) {
|
|
1976
|
+
throw new Error(`Wallet.updateDbAfterSettle: no contract owns script ${script}`);
|
|
1977
|
+
}
|
|
1978
|
+
const bucket = byAddress.get(targetAddr) ?? [];
|
|
1979
|
+
bucket.push(...vtxos);
|
|
1980
|
+
byAddress.set(targetAddr, bucket);
|
|
1981
|
+
}
|
|
1982
|
+
for (const [bucketAddr, vtxos] of byAddress) {
|
|
1983
|
+
await this.walletRepository.saveVtxos(bucketAddr, vtxos);
|
|
1984
|
+
}
|
|
1916
1985
|
}
|
|
1917
1986
|
if (boardingUtxoToRemove.size > 0) {
|
|
1918
1987
|
const currentUtxos = await this.walletRepository.getUtxos(boardingAddress);
|
|
@@ -1926,6 +1995,7 @@ class Wallet extends ReadonlyWallet {
|
|
|
1926
1995
|
}
|
|
1927
1996
|
catch (e) {
|
|
1928
1997
|
console.warn("error updating repository after settle", e);
|
|
1998
|
+
throw e;
|
|
1929
1999
|
}
|
|
1930
2000
|
}
|
|
1931
2001
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.contractPollProcessor = exports.CONTRACT_POLL_TASK_TYPE = void 0;
|
|
4
|
+
const vtxoOwnership_1 = require("../../../contracts/vtxoOwnership");
|
|
4
5
|
exports.CONTRACT_POLL_TASK_TYPE = "contract-poll";
|
|
5
6
|
/**
|
|
6
7
|
* Polls the indexer for the latest VTXO state of every contract and
|
|
@@ -43,8 +44,12 @@ exports.contractPollProcessor = {
|
|
|
43
44
|
hasMore = page ? vtxos.length === pageSize : false;
|
|
44
45
|
pageIndex++;
|
|
45
46
|
}
|
|
46
|
-
|
|
47
|
-
|
|
47
|
+
// Skip wrong-script rows (legacy duplicates or indexer drift)
|
|
48
|
+
// before persisting; the loop must keep going for the remaining
|
|
49
|
+
// contracts even when one row is rejected.
|
|
50
|
+
const filtered = (0, vtxoOwnership_1.warnAndFilterVtxosForScript)(allVtxos, contract.script, "contractPollProcessor");
|
|
51
|
+
await walletRepository.saveVtxos(contract.address, filtered);
|
|
52
|
+
vtxosSaved += filtered.length;
|
|
48
53
|
contractsProcessed++;
|
|
49
54
|
}
|
|
50
55
|
return {
|
|
@@ -3,6 +3,7 @@ import { ContractWatcher } from './contractWatcher.js';
|
|
|
3
3
|
import { contractHandlers } from './handlers/index.js';
|
|
4
4
|
import { extendVirtualCoinForContract } from '../wallet/utils.js';
|
|
5
5
|
import { advanceSyncCursor, computeSyncWindow, cursorCutoff, getSyncCursor, } from '../utils/syncCursors.js';
|
|
6
|
+
import { filterVtxosForScript, warnAndFilterVtxosForScript, } from './vtxoOwnership.js';
|
|
6
7
|
const DEFAULT_PAGE_SIZE = 500;
|
|
7
8
|
/**
|
|
8
9
|
* Central manager for contract lifecycle and operations.
|
|
@@ -354,9 +355,19 @@ export class ContractManager {
|
|
|
354
355
|
const contracts = opts?.scripts
|
|
355
356
|
? await this.getContracts({ script: opts.scripts })
|
|
356
357
|
: undefined;
|
|
358
|
+
// Only forward an explicit window when the caller supplied one. An
|
|
359
|
+
// empty `{ after: undefined, before: undefined }` would short-circuit
|
|
360
|
+
// both the cursor-derived `?after=` query in `syncContracts` (because
|
|
361
|
+
// `??` doesn't fire on a non-nullish object) AND the cursor-advance
|
|
362
|
+
// gate (which requires `options.window === undefined`), turning every
|
|
363
|
+
// `refreshVtxos()` call into an unbounded full re-scan whose cursor
|
|
364
|
+
// never moves forward.
|
|
365
|
+
const hasExplicitWindow = opts?.after !== undefined || opts?.before !== undefined;
|
|
357
366
|
await this.syncContracts({
|
|
358
367
|
contracts,
|
|
359
|
-
window:
|
|
368
|
+
window: hasExplicitWindow
|
|
369
|
+
? { after: opts?.after, before: opts?.before }
|
|
370
|
+
: undefined,
|
|
360
371
|
});
|
|
361
372
|
}
|
|
362
373
|
/**
|
|
@@ -399,7 +410,11 @@ export class ContractManager {
|
|
|
399
410
|
this.emitEvent(event);
|
|
400
411
|
}
|
|
401
412
|
async getVtxosForContracts(contracts) {
|
|
402
|
-
const res = await Promise.all(contracts.map(({ script, address }) => this.config.walletRepository.getVtxos(address).then((vtxos) =>
|
|
413
|
+
const res = await Promise.all(contracts.map(({ script, address }) => this.config.walletRepository.getVtxos(address).then((vtxos) =>
|
|
414
|
+
// Address buckets may carry legacy duplicate rows from
|
|
415
|
+
// other contracts that once shared the same address —
|
|
416
|
+
// gate by script so the wrong-script row never wins.
|
|
417
|
+
filterVtxosForScript(vtxos, script).map((vtxo) => ({
|
|
403
418
|
...vtxo,
|
|
404
419
|
contractScript: script,
|
|
405
420
|
})))));
|
|
@@ -467,7 +482,14 @@ export class ContractManager {
|
|
|
467
482
|
});
|
|
468
483
|
}
|
|
469
484
|
for (const [addr, contractVtxos] of byContract) {
|
|
470
|
-
|
|
485
|
+
// The bucket is keyed by contract address, so the script filter
|
|
486
|
+
// here is the same as the contract's. Skip wrong-script rows
|
|
487
|
+
// rather than crash the reconcile loop.
|
|
488
|
+
const contract = contracts.find((c) => c.address === addr);
|
|
489
|
+
const filtered = warnAndFilterVtxosForScript(contractVtxos, contract.script, "ContractManager.reconcilePendingFrontier");
|
|
490
|
+
if (filtered.length === 0)
|
|
491
|
+
continue;
|
|
492
|
+
await this.config.walletRepository.saveVtxos(addr, filtered);
|
|
471
493
|
}
|
|
472
494
|
}
|
|
473
495
|
async fetchContractVxosFromIndexer(contracts, pageSize, syncWindow) {
|
|
@@ -477,7 +499,10 @@ export class ContractManager {
|
|
|
477
499
|
result.set(contractScript, vtxos);
|
|
478
500
|
const contract = contracts.find((c) => c.script === contractScript);
|
|
479
501
|
if (contract) {
|
|
480
|
-
|
|
502
|
+
const filtered = warnAndFilterVtxosForScript(vtxos, contract.script, "ContractManager.fetchContractVxosFromIndexer");
|
|
503
|
+
if (filtered.length === 0)
|
|
504
|
+
continue;
|
|
505
|
+
await this.config.walletRepository.saveVtxos(contract.address, filtered);
|
|
481
506
|
}
|
|
482
507
|
}
|
|
483
508
|
return result;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { extendVirtualCoinForContract } from '../wallet/utils.js';
|
|
2
2
|
import { isEventSourceError } from '../providers/utils.js';
|
|
3
|
+
import { filterVtxosForScript } from './vtxoOwnership.js';
|
|
3
4
|
/**
|
|
4
5
|
* Watches multiple contracts for virtual output state changes with resilient connection handling.
|
|
5
6
|
*
|
|
@@ -87,7 +88,10 @@ export class ContractWatcher {
|
|
|
87
88
|
*/
|
|
88
89
|
async seedLastKnownVtxos(state) {
|
|
89
90
|
try {
|
|
90
|
-
|
|
91
|
+
// Apply the same script gate used by getContractVtxos so a legacy
|
|
92
|
+
// wrong-script row in the address bucket can't seed the baseline
|
|
93
|
+
// and then look "spent" on the first poll.
|
|
94
|
+
const cached = filterVtxosForScript(await this.config.walletRepository.getVtxos(state.contract.address), state.contract.script);
|
|
91
95
|
for (const vtxo of cached) {
|
|
92
96
|
if (vtxo.isSpent)
|
|
93
97
|
continue;
|
|
@@ -166,8 +170,10 @@ export class ContractWatcher {
|
|
|
166
170
|
return true;
|
|
167
171
|
})
|
|
168
172
|
.map(async (state) => {
|
|
169
|
-
// Use contract address as cache key
|
|
170
|
-
|
|
173
|
+
// Use contract address as cache key. Legacy address buckets
|
|
174
|
+
// can contain rows from other contracts; gate by script before
|
|
175
|
+
// converting so a wrong-script row never reaches the watcher.
|
|
176
|
+
const cached = filterVtxosForScript(await repo.getVtxos(state.contract.address), state.contract.script);
|
|
171
177
|
if (cached.length > 0) {
|
|
172
178
|
// Convert to ContractVtxo with contractScript
|
|
173
179
|
const contractVtxos = cached.map((v) => ({
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { hex } from "@scure/base";
|
|
2
2
|
import { DefaultVtxo } from '../../script/default.js';
|
|
3
|
-
import { isCsvSpendable
|
|
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
|
|
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
|
|
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
|
|
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,53 @@
|
|
|
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
|
+
}
|
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
|
|
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';
|
package/dist/esm/script/base.js
CHANGED
|
@@ -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
|
|
129
|
-
|
|
130
|
-
|
|
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
|
-
|
|
138
|
-
|
|
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 =
|
|
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;
|