@arkade-os/sdk 0.4.22 → 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 +116 -13
- package/dist/cjs/contracts/arkcontract.js +2 -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/identity/descriptor.js +75 -4
- package/dist/cjs/identity/hdCapableIdentity.js +2 -0
- package/dist/cjs/identity/seedIdentity.js +225 -103
- package/dist/cjs/identity/serialize.js +5 -0
- package/dist/cjs/identity/staticDescriptorProvider.js +1 -1
- package/dist/cjs/index.js +12 -3
- package/dist/cjs/providers/electrum.js +285 -79
- package/dist/cjs/providers/expoIndexer.js +1 -1
- package/dist/cjs/providers/indexer.js +2 -2
- package/dist/cjs/providers/onchain.js +9 -3
- package/dist/cjs/repositories/migrations/walletRepositoryImpl.js +6 -2
- package/dist/cjs/repositories/realm/walletRepository.js +2 -2
- package/dist/cjs/repositories/serialization.js +34 -1
- package/dist/cjs/repositories/sqlite/walletRepository.js +4 -2
- package/dist/cjs/script/address.js +2 -1
- 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/transactionHistory.js +4 -4
- package/dist/cjs/utils/unknownFields.js +2 -39
- package/dist/cjs/wallet/asset-manager.js +18 -18
- package/dist/cjs/wallet/asset.js +10 -8
- package/dist/cjs/wallet/delegator.js +2 -2
- package/dist/cjs/wallet/hdDescriptorProvider.js +159 -0
- package/dist/cjs/wallet/index.js +5 -1
- package/dist/cjs/wallet/onchain.js +2 -1
- package/dist/cjs/wallet/serviceWorker/wallet-message-handler.js +60 -10
- package/dist/cjs/wallet/serviceWorker/wallet.js +5 -4
- package/dist/cjs/wallet/unroll.js +79 -67
- package/dist/cjs/wallet/validation.js +2 -3
- package/dist/cjs/wallet/wallet.js +91 -22
- package/dist/cjs/worker/expo/processors/contractPollProcessor.js +7 -2
- package/dist/esm/contracts/arkcontract.js +2 -1
- 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/identity/descriptor.js +74 -5
- package/dist/esm/identity/hdCapableIdentity.js +1 -0
- package/dist/esm/identity/seedIdentity.js +225 -103
- package/dist/esm/identity/serialize.js +5 -0
- package/dist/esm/identity/staticDescriptorProvider.js +1 -1
- package/dist/esm/index.js +7 -4
- package/dist/esm/providers/electrum.js +284 -78
- package/dist/esm/providers/expoIndexer.js +1 -1
- package/dist/esm/providers/indexer.js +2 -2
- package/dist/esm/providers/onchain.js +9 -3
- package/dist/esm/repositories/migrations/walletRepositoryImpl.js +6 -2
- package/dist/esm/repositories/realm/walletRepository.js +3 -3
- package/dist/esm/repositories/serialization.js +27 -0
- package/dist/esm/repositories/sqlite/walletRepository.js +5 -3
- package/dist/esm/script/address.js +2 -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/transactionHistory.js +4 -4
- package/dist/esm/utils/unknownFields.js +2 -6
- package/dist/esm/wallet/asset-manager.js +18 -18
- package/dist/esm/wallet/asset.js +10 -8
- package/dist/esm/wallet/delegator.js +2 -2
- package/dist/esm/wallet/hdDescriptorProvider.js +155 -0
- package/dist/esm/wallet/index.js +4 -0
- package/dist/esm/wallet/onchain.js +2 -1
- package/dist/esm/wallet/serviceWorker/wallet-message-handler.js +60 -10
- package/dist/esm/wallet/serviceWorker/wallet.js +5 -4
- package/dist/esm/wallet/unroll.js +78 -67
- package/dist/esm/wallet/validation.js +2 -3
- package/dist/esm/wallet/wallet.js +88 -20
- package/dist/esm/worker/expo/processors/contractPollProcessor.js +7 -2
- package/dist/types/contracts/arkcontract.d.ts +1 -1
- package/dist/types/contracts/handlers/helpers.d.ts +0 -9
- package/dist/types/contracts/vtxoOwnership.d.ts +25 -0
- package/dist/types/identity/descriptor.d.ts +26 -0
- package/dist/types/identity/descriptorProvider.d.ts +11 -4
- package/dist/types/identity/hdCapableIdentity.d.ts +44 -0
- package/dist/types/identity/index.d.ts +1 -0
- package/dist/types/identity/seedIdentity.d.ts +113 -29
- package/dist/types/identity/serialize.d.ts +12 -0
- package/dist/types/identity/staticDescriptorProvider.d.ts +1 -1
- package/dist/types/index.d.ts +6 -3
- package/dist/types/providers/electrum.d.ts +115 -15
- package/dist/types/providers/onchain.d.ts +6 -0
- package/dist/types/repositories/serialization.d.ts +26 -2
- package/dist/types/script/address.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/hdDescriptorProvider.d.ts +93 -0
- package/dist/types/wallet/index.d.ts +19 -10
- package/dist/types/wallet/onchain.d.ts +1 -1
- package/dist/types/wallet/serviceWorker/wallet.d.ts +1 -1
- package/dist/types/wallet/unroll.d.ts +10 -0
- package/dist/types/wallet/wallet.d.ts +4 -1
- package/package.json +1 -1
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.Wallet = exports.ReadonlyWallet = void 0;
|
|
3
|
+
exports.Wallet = exports.ReadonlyWallet = exports.getArkadeServerUrl = void 0;
|
|
4
4
|
exports.selectVirtualCoins = selectVirtualCoins;
|
|
5
5
|
exports.waitForIncomingFunds = waitForIncomingFunds;
|
|
6
6
|
const base_1 = require("@scure/base");
|
|
@@ -37,8 +37,11 @@ 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");
|
|
43
|
+
const getArkadeServerUrl = ({ arkServerUrl, }) => arkServerUrl || _1.DEFAULT_ARKADE_SERVER_URL;
|
|
44
|
+
exports.getArkadeServerUrl = getArkadeServerUrl;
|
|
42
45
|
// Historical unilateral exit delay for mainnet (~7 days in seconds).
|
|
43
46
|
// Kept so existing wallets can still discover and spend VTXOs sent to the
|
|
44
47
|
// legacy address after arkd starts advertising a different delay.
|
|
@@ -53,7 +56,7 @@ function dedupeTimelocks(timelocks) {
|
|
|
53
56
|
const seen = new Set();
|
|
54
57
|
const deduped = [];
|
|
55
58
|
for (const timelock of timelocks) {
|
|
56
|
-
const sequence = (0,
|
|
59
|
+
const sequence = (0, timelock_1.timelockToSequence)(timelock).toString();
|
|
57
60
|
if (seen.has(sequence))
|
|
58
61
|
continue;
|
|
59
62
|
seen.add(sequence);
|
|
@@ -125,10 +128,7 @@ class ReadonlyWallet {
|
|
|
125
128
|
// Use provided arkProvider instance or create a new one from arkServerUrl
|
|
126
129
|
const arkProvider = config.arkProvider ||
|
|
127
130
|
(() => {
|
|
128
|
-
|
|
129
|
-
throw new Error("Either arkProvider or arkServerUrl must be provided");
|
|
130
|
-
}
|
|
131
|
-
return new ark_1.RestArkProvider(config.arkServerUrl);
|
|
131
|
+
return new ark_1.RestArkProvider((0, exports.getArkadeServerUrl)(config));
|
|
132
132
|
})();
|
|
133
133
|
// Extract arkServerUrl from provider if not explicitly provided
|
|
134
134
|
const arkServerUrl = config.arkServerUrl || arkProvider.serverUrl;
|
|
@@ -307,7 +307,7 @@ class ReadonlyWallet {
|
|
|
307
307
|
continue;
|
|
308
308
|
if (vtxo.assets) {
|
|
309
309
|
for (const a of vtxo.assets) {
|
|
310
|
-
const current = assetBalances.get(a.assetId) ??
|
|
310
|
+
const current = assetBalances.get(a.assetId) ?? 0n;
|
|
311
311
|
assetBalances.set(a.assetId, current + a.amount);
|
|
312
312
|
}
|
|
313
313
|
}
|
|
@@ -649,7 +649,7 @@ class ReadonlyWallet {
|
|
|
649
649
|
watcherConfig: this.watcherConfig,
|
|
650
650
|
});
|
|
651
651
|
for (const csvTimelock of this.walletContractTimelocks) {
|
|
652
|
-
const csvTimelockStr = (0,
|
|
652
|
+
const csvTimelockStr = (0, timelock_1.timelockToSequence)(csvTimelock).toString();
|
|
653
653
|
const defaultScript = new default_1.DefaultVtxo.Script({
|
|
654
654
|
pubKey: this.offchainTapscript.options.pubKey,
|
|
655
655
|
serverPubKey: this.offchainTapscript.options.serverPubKey,
|
|
@@ -1129,12 +1129,12 @@ class Wallet extends ReadonlyWallet {
|
|
|
1129
1129
|
for (const [, assets] of assetInputs) {
|
|
1130
1130
|
for (const asset of assets) {
|
|
1131
1131
|
const existing = allAssets.get(asset.assetId) ?? 0n;
|
|
1132
|
-
allAssets.set(asset.assetId, existing +
|
|
1132
|
+
allAssets.set(asset.assetId, existing + asset.amount);
|
|
1133
1133
|
}
|
|
1134
1134
|
}
|
|
1135
1135
|
outputAssets = [];
|
|
1136
1136
|
for (const [assetId, amount] of allAssets) {
|
|
1137
|
-
outputAssets.push({ assetId, amount
|
|
1137
|
+
outputAssets.push({ assetId, amount });
|
|
1138
1138
|
}
|
|
1139
1139
|
}
|
|
1140
1140
|
const recipients = params.outputs.map((output, i) => ({
|
|
@@ -1569,7 +1569,7 @@ class Wallet extends ReadonlyWallet {
|
|
|
1569
1569
|
* const txid = await wallet.send({
|
|
1570
1570
|
* address: 'ark1q...',
|
|
1571
1571
|
* amount: 1000, // (optional, default to dust) btc amount to send to the output
|
|
1572
|
-
* assets: [{ assetId: 'abc123...', amount:
|
|
1572
|
+
* assets: [{ assetId: 'abc123...', amount: 50n }] // (optional) list of assets to send
|
|
1573
1573
|
* });
|
|
1574
1574
|
* ```
|
|
1575
1575
|
*/
|
|
@@ -1600,7 +1600,7 @@ class Wallet extends ReadonlyWallet {
|
|
|
1600
1600
|
continue;
|
|
1601
1601
|
}
|
|
1602
1602
|
for (const receiverAsset of recipient.assets) {
|
|
1603
|
-
let amountToSelect =
|
|
1603
|
+
let amountToSelect = receiverAsset.amount;
|
|
1604
1604
|
// check if existing change covers the needed amount
|
|
1605
1605
|
const existingChange = assetChanges.get(receiverAsset.assetId) ?? 0n;
|
|
1606
1606
|
if (existingChange >= amountToSelect) {
|
|
@@ -1627,7 +1627,7 @@ class Wallet extends ReadonlyWallet {
|
|
|
1627
1627
|
continue;
|
|
1628
1628
|
}
|
|
1629
1629
|
const existing = assetChanges.get(a.assetId) ?? 0n;
|
|
1630
|
-
assetChanges.set(a.assetId, existing +
|
|
1630
|
+
assetChanges.set(a.assetId, existing + a.amount);
|
|
1631
1631
|
}
|
|
1632
1632
|
}
|
|
1633
1633
|
}
|
|
@@ -1647,7 +1647,7 @@ class Wallet extends ReadonlyWallet {
|
|
|
1647
1647
|
if (coin.assets) {
|
|
1648
1648
|
for (const asset of coin.assets) {
|
|
1649
1649
|
const existing = assetChanges.get(asset.assetId) ?? 0n;
|
|
1650
|
-
assetChanges.set(asset.assetId, existing +
|
|
1650
|
+
assetChanges.set(asset.assetId, existing + asset.amount);
|
|
1651
1651
|
}
|
|
1652
1652
|
}
|
|
1653
1653
|
}
|
|
@@ -1669,7 +1669,7 @@ class Wallet extends ReadonlyWallet {
|
|
|
1669
1669
|
if (coin.assets) {
|
|
1670
1670
|
for (const asset of coin.assets) {
|
|
1671
1671
|
const existing = assetChanges.get(asset.assetId) ?? 0n;
|
|
1672
|
-
assetChanges.set(asset.assetId, existing +
|
|
1672
|
+
assetChanges.set(asset.assetId, existing + asset.amount);
|
|
1673
1673
|
}
|
|
1674
1674
|
}
|
|
1675
1675
|
}
|
|
@@ -1684,7 +1684,7 @@ class Wallet extends ReadonlyWallet {
|
|
|
1684
1684
|
const changeAssets = [];
|
|
1685
1685
|
for (const [assetId, amount] of assetChanges) {
|
|
1686
1686
|
if (amount > 0n) {
|
|
1687
|
-
changeAssets.push({ assetId, amount
|
|
1687
|
+
changeAssets.push({ assetId, amount });
|
|
1688
1688
|
}
|
|
1689
1689
|
}
|
|
1690
1690
|
changeIndex = outputs.length;
|
|
@@ -1831,7 +1831,7 @@ class Wallet extends ReadonlyWallet {
|
|
|
1831
1831
|
}
|
|
1832
1832
|
}
|
|
1833
1833
|
const createdAt = Date.now();
|
|
1834
|
-
const
|
|
1834
|
+
const primaryAddr = this.arkAddress.encode();
|
|
1835
1835
|
// Only save a change virtual output for preconfirmed coins (those with a batchExpiry).
|
|
1836
1836
|
// Inputs without a batchExpiry are already settled/unrolled and don't need tracking.
|
|
1837
1837
|
let changeVtxo;
|
|
@@ -1858,8 +1858,45 @@ class Wallet extends ReadonlyWallet {
|
|
|
1858
1858
|
script: base_1.hex.encode(this.offchainTapscript.pkScript),
|
|
1859
1859
|
};
|
|
1860
1860
|
}
|
|
1861
|
-
|
|
1862
|
-
|
|
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, [
|
|
1863
1900
|
{
|
|
1864
1901
|
key: {
|
|
1865
1902
|
boardingTxid: "",
|
|
@@ -1875,12 +1912,12 @@ class Wallet extends ReadonlyWallet {
|
|
|
1875
1912
|
}
|
|
1876
1913
|
catch (e) {
|
|
1877
1914
|
console.warn("error saving offchain tx to repository", e);
|
|
1915
|
+
throw e;
|
|
1878
1916
|
}
|
|
1879
1917
|
}
|
|
1880
1918
|
// mark virtual outputs as spent/settled, remove boarding inputs
|
|
1881
1919
|
async updateDbAfterSettle(inputs, commitmentTxid) {
|
|
1882
1920
|
try {
|
|
1883
|
-
const addr = this.arkAddress.encode();
|
|
1884
1921
|
const boardingAddress = await this.getBoardingAddress();
|
|
1885
1922
|
const spentVtxos = [];
|
|
1886
1923
|
const inputArkTxIds = new Set();
|
|
@@ -1913,7 +1950,38 @@ class Wallet extends ReadonlyWallet {
|
|
|
1913
1950
|
}
|
|
1914
1951
|
}
|
|
1915
1952
|
if (spentVtxos.length > 0) {
|
|
1916
|
-
|
|
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
|
+
}
|
|
1917
1985
|
}
|
|
1918
1986
|
if (boardingUtxoToRemove.size > 0) {
|
|
1919
1987
|
const currentUtxos = await this.walletRepository.getUtxos(boardingAddress);
|
|
@@ -1927,6 +1995,7 @@ class Wallet extends ReadonlyWallet {
|
|
|
1927
1995
|
}
|
|
1928
1996
|
catch (e) {
|
|
1929
1997
|
console.warn("error updating repository after settle", e);
|
|
1998
|
+
throw e;
|
|
1930
1999
|
}
|
|
1931
2000
|
}
|
|
1932
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 {
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { hex } from "@scure/base";
|
|
2
2
|
import { contractHandlers } from './handlers/index.js';
|
|
3
|
+
import { DEFAULT_ARKADE_HRP } from '../wallet/index.js';
|
|
3
4
|
/**
|
|
4
5
|
* Prefix for arkcontract strings.
|
|
5
6
|
*/
|
|
@@ -115,7 +116,7 @@ export function contractFromArkContract(encoded, options = {}) {
|
|
|
115
116
|
* @param options - Additional options
|
|
116
117
|
* @returns A complete Contract object
|
|
117
118
|
*/
|
|
118
|
-
export function contractFromArkContractWithAddress(encoded, serverPubKey, addressPrefix, options = {}) {
|
|
119
|
+
export function contractFromArkContractWithAddress(encoded, serverPubKey, addressPrefix = DEFAULT_ARKADE_HRP, options = {}) {
|
|
119
120
|
const parsed = decodeArkContract(encoded);
|
|
120
121
|
const handler = contractHandlers.getOrThrow(parsed.type);
|
|
121
122
|
const params = parsed.data;
|
|
@@ -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
|
+
}
|
|
@@ -1,7 +1,72 @@
|
|
|
1
|
-
import { expand, networks
|
|
1
|
+
import { expand, networks } from "@bitcoinerlab/descriptors-scure";
|
|
2
2
|
import { hex } from "@scure/base";
|
|
3
|
-
|
|
4
|
-
|
|
3
|
+
/**
|
|
4
|
+
* True iff `descriptor` is a Bitcoin mainnet descriptor (xpub-prefixed
|
|
5
|
+
* BIP32 key).
|
|
6
|
+
*
|
|
7
|
+
* Note: testnet, signet, regtest, mutinynet, and other non-mainnet
|
|
8
|
+
* networks all share the same `tpub` BIP32 version bytes — they cannot
|
|
9
|
+
* be distinguished from one another at the descriptor level. Callers
|
|
10
|
+
* that need a `Network` constants object for `expand()` pick
|
|
11
|
+
* `networks.bitcoin` vs `networks.testnet` themselves; callers that
|
|
12
|
+
* need the *actual* network the wallet is on must track that
|
|
13
|
+
* out-of-band.
|
|
14
|
+
*/
|
|
15
|
+
export function isMainnetDescriptor(descriptor) {
|
|
16
|
+
return !descriptor.includes("tpub");
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Shared "does `candidate` belong to the identity backed by
|
|
20
|
+
* `ourDescriptor`?" predicate.
|
|
21
|
+
*
|
|
22
|
+
* - HD descriptors (expanding to a `bip32` key) match by account xpub
|
|
23
|
+
* on both sides — index-agnostic, so a wildcard template and any
|
|
24
|
+
* concrete index under it all collapse to the same xpub.
|
|
25
|
+
* - Bare `tr(pubkey)` candidates fall back to comparing the candidate
|
|
26
|
+
* pubkey against `ourXOnlyPubkey` (the cached pubkey on the identity
|
|
27
|
+
* side, since pulling it from `ourDescriptor` would require an index
|
|
28
|
+
* substitution the caller already performed).
|
|
29
|
+
*/
|
|
30
|
+
export function descriptorIsOurs(candidate, ourDescriptor, ourXOnlyPubkey) {
|
|
31
|
+
if (!isDescriptor(candidate))
|
|
32
|
+
return false;
|
|
33
|
+
try {
|
|
34
|
+
const candidateInfo = expand({
|
|
35
|
+
descriptor: candidate,
|
|
36
|
+
network: isMainnetDescriptor(candidate)
|
|
37
|
+
? networks.bitcoin
|
|
38
|
+
: networks.testnet,
|
|
39
|
+
}).expansionMap?.["@0"];
|
|
40
|
+
if (!candidateInfo)
|
|
41
|
+
return false;
|
|
42
|
+
if (candidateInfo.bip32) {
|
|
43
|
+
const ourBip32 = expand({
|
|
44
|
+
descriptor: ourDescriptor,
|
|
45
|
+
network: isMainnetDescriptor(ourDescriptor)
|
|
46
|
+
? networks.bitcoin
|
|
47
|
+
: networks.testnet,
|
|
48
|
+
}).expansionMap?.["@0"]?.bip32;
|
|
49
|
+
if (!ourBip32)
|
|
50
|
+
return false;
|
|
51
|
+
return ourBip32.toBase58() === candidateInfo.bip32.toBase58();
|
|
52
|
+
}
|
|
53
|
+
if (candidateInfo.pubkey) {
|
|
54
|
+
// For tr() the library hands back a 32-byte x-only key, but
|
|
55
|
+
// strip a leading parity byte defensively so a 33-byte
|
|
56
|
+
// compressed key (mismatched length) doesn't silently
|
|
57
|
+
// false-negative against our 32-byte x-only side.
|
|
58
|
+
const candidatePub = candidateInfo.pubkey.length === 33
|
|
59
|
+
? candidateInfo.pubkey.subarray(1)
|
|
60
|
+
: candidateInfo.pubkey;
|
|
61
|
+
if (candidatePub.length !== ourXOnlyPubkey.length)
|
|
62
|
+
return false;
|
|
63
|
+
return hex.encode(candidatePub) === hex.encode(ourXOnlyPubkey);
|
|
64
|
+
}
|
|
65
|
+
return false;
|
|
66
|
+
}
|
|
67
|
+
catch {
|
|
68
|
+
return false;
|
|
69
|
+
}
|
|
5
70
|
}
|
|
6
71
|
/**
|
|
7
72
|
* Check if a string is a descriptor of the shape `tr(...)`.
|
|
@@ -43,7 +108,9 @@ export function extractPubKey(descriptor) {
|
|
|
43
108
|
if (!isDescriptor(descriptor)) {
|
|
44
109
|
return descriptor;
|
|
45
110
|
}
|
|
46
|
-
const network =
|
|
111
|
+
const network = isMainnetDescriptor(descriptor)
|
|
112
|
+
? networks.bitcoin
|
|
113
|
+
: networks.testnet;
|
|
47
114
|
const expansion = expand({ descriptor, network });
|
|
48
115
|
if (!expansion.expansionMap) {
|
|
49
116
|
throw new Error("Cannot extract pubkey from descriptor: expansion failed.");
|
|
@@ -70,7 +137,9 @@ export function parseHDDescriptor(descriptor) {
|
|
|
70
137
|
}
|
|
71
138
|
let expansion;
|
|
72
139
|
try {
|
|
73
|
-
const network =
|
|
140
|
+
const network = isMainnetDescriptor(descriptor)
|
|
141
|
+
? networks.bitcoin
|
|
142
|
+
: networks.testnet;
|
|
74
143
|
expansion = expand({ descriptor, network });
|
|
75
144
|
}
|
|
76
145
|
catch {
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|