@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
package/dist/cjs/wallet/asset.js
CHANGED
|
@@ -21,7 +21,7 @@ function createAssetPacket(assetInputs, receivers, changeReceiver) {
|
|
|
21
21
|
const existing = inputsByAssetId.get(asset.assetId);
|
|
22
22
|
inputsByAssetId.set(asset.assetId, [
|
|
23
23
|
...(existing ?? []),
|
|
24
|
-
asset_1.AssetInput.create(inputIndex,
|
|
24
|
+
asset_1.AssetInput.create(inputIndex, asset.amount),
|
|
25
25
|
]);
|
|
26
26
|
}
|
|
27
27
|
}
|
|
@@ -35,7 +35,7 @@ function createAssetPacket(assetInputs, receivers, changeReceiver) {
|
|
|
35
35
|
const existing = outputsByAssetId.get(asset.assetId);
|
|
36
36
|
outputsByAssetId.set(asset.assetId, [
|
|
37
37
|
...(existing ?? []),
|
|
38
|
-
asset_1.AssetOutput.create(outputIndex,
|
|
38
|
+
asset_1.AssetOutput.create(outputIndex, asset.amount),
|
|
39
39
|
]);
|
|
40
40
|
}
|
|
41
41
|
}
|
|
@@ -47,7 +47,7 @@ function createAssetPacket(assetInputs, receivers, changeReceiver) {
|
|
|
47
47
|
const existing = outputsByAssetId.get(asset.assetId);
|
|
48
48
|
outputsByAssetId.set(asset.assetId, [
|
|
49
49
|
...(existing ?? []),
|
|
50
|
-
asset_1.AssetOutput.create(outputIndex,
|
|
50
|
+
asset_1.AssetOutput.create(outputIndex, asset.amount),
|
|
51
51
|
]);
|
|
52
52
|
}
|
|
53
53
|
}
|
|
@@ -75,9 +75,11 @@ function selectCoinsWithAsset(coins, assetId, requiredAmount) {
|
|
|
75
75
|
const coinsWithAsset = coins.filter((coin) => coin.assets?.some((a) => a.assetId === assetId));
|
|
76
76
|
// sort by asset amount (smallest first for better selection)
|
|
77
77
|
coinsWithAsset.sort((a, b) => {
|
|
78
|
-
const amountA = a.assets?.find((asset) => asset.assetId === assetId)?.amount ??
|
|
79
|
-
const amountB = b.assets?.find((asset) => asset.assetId === assetId)?.amount ??
|
|
80
|
-
|
|
78
|
+
const amountA = a.assets?.find((asset) => asset.assetId === assetId)?.amount ?? 0n;
|
|
79
|
+
const amountB = b.assets?.find((asset) => asset.assetId === assetId)?.amount ?? 0n;
|
|
80
|
+
// Array.sort callback returns number; reduce the bigint diff to
|
|
81
|
+
// -1/0/1 (the only thing sort actually consults).
|
|
82
|
+
return amountA < amountB ? -1 : amountA > amountB ? 1 : 0;
|
|
81
83
|
});
|
|
82
84
|
const selected = [];
|
|
83
85
|
let totalAssetAmount = 0n;
|
|
@@ -85,8 +87,8 @@ function selectCoinsWithAsset(coins, assetId, requiredAmount) {
|
|
|
85
87
|
if (totalAssetAmount >= requiredAmount)
|
|
86
88
|
break;
|
|
87
89
|
selected.push(coin);
|
|
88
|
-
const assetAmount = coin.assets?.find((a) => a.assetId === assetId)?.amount ??
|
|
89
|
-
totalAssetAmount +=
|
|
90
|
+
const assetAmount = coin.assets?.find((a) => a.assetId === assetId)?.amount ?? 0n;
|
|
91
|
+
totalAssetAmount += assetAmount;
|
|
90
92
|
}
|
|
91
93
|
if (totalAssetAmount < requiredAmount) {
|
|
92
94
|
throw new Error(`Insufficient asset balance: have ${totalAssetAmount}, need ${requiredAmount}`);
|
|
@@ -245,12 +245,12 @@ async function makeSignedDelegateIntent(identity, coins, outputs, onchainOutputs
|
|
|
245
245
|
for (const [, assets] of assetInputs) {
|
|
246
246
|
for (const asset of assets) {
|
|
247
247
|
const existing = allAssets.get(asset.assetId) ?? 0n;
|
|
248
|
-
allAssets.set(asset.assetId, existing +
|
|
248
|
+
allAssets.set(asset.assetId, existing + asset.amount);
|
|
249
249
|
}
|
|
250
250
|
}
|
|
251
251
|
outputAssets = [];
|
|
252
252
|
for (const [assetId, amount] of allAssets) {
|
|
253
|
-
outputAssets.push({ assetId, amount
|
|
253
|
+
outputAssets.push({ assetId, amount });
|
|
254
254
|
}
|
|
255
255
|
}
|
|
256
256
|
const recipients = outputs.map((output, i) => ({
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.HDDescriptorProvider = void 0;
|
|
4
|
+
const descriptors_scure_1 = require("@bitcoinerlab/descriptors-scure");
|
|
5
|
+
const descriptor_1 = require("../identity/descriptor");
|
|
6
|
+
const syncCursors_1 = require("../utils/syncCursors");
|
|
7
|
+
/** Settings key under {@link WalletState.settings} where HD state lives. */
|
|
8
|
+
const HD_SETTINGS_KEY = "hd";
|
|
9
|
+
/**
|
|
10
|
+
* HD-wallet {@link DescriptorProvider} that allocates a fresh signing
|
|
11
|
+
* descriptor on every call. The provider holds no notion of "current" — it
|
|
12
|
+
* is a pure rotating allocator. The question of "which descriptor is the
|
|
13
|
+
* wallet currently bound to?" is answered by querying the contract
|
|
14
|
+
* repository for active contracts, not by asking this provider.
|
|
15
|
+
*
|
|
16
|
+
* State is persisted under `WalletRepository.getWalletState().settings.hd` so
|
|
17
|
+
* that no storage-schema migration is required when switching a wallet from
|
|
18
|
+
* single-key to HD. The provider is backed by an {@link HDCapableIdentity},
|
|
19
|
+
* which carries the wildcard account descriptor template (for derivation)
|
|
20
|
+
* and the signing primitives.
|
|
21
|
+
*
|
|
22
|
+
* The read-modify-write of the persisted index runs inside the shared per-
|
|
23
|
+
* repo `updateWalletState` mutex, so two `getNextSigningDescriptor` callers
|
|
24
|
+
* — including those driving separate `HDDescriptorProvider` instances on
|
|
25
|
+
* the same repo — can never observe the same index.
|
|
26
|
+
*
|
|
27
|
+
* @example
|
|
28
|
+
* ```ts
|
|
29
|
+
* const provider = await HDDescriptorProvider.create(identity, walletRepo);
|
|
30
|
+
* const descriptor = await provider.getNextSigningDescriptor();
|
|
31
|
+
* // descriptor: tr([fp/86'/0'/0']xpub/0/0)
|
|
32
|
+
* const next = await provider.getNextSigningDescriptor();
|
|
33
|
+
* // next: tr([fp/86'/0'/0']xpub/0/1)
|
|
34
|
+
* ```
|
|
35
|
+
*/
|
|
36
|
+
class HDDescriptorProvider {
|
|
37
|
+
constructor(identity, walletRepository) {
|
|
38
|
+
this.identity = identity;
|
|
39
|
+
this.walletRepository = walletRepository;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Construct an HDDescriptorProvider. No I/O is performed here;
|
|
43
|
+
* persisted state is read lazily on the first call to
|
|
44
|
+
* `getNextSigningDescriptor`. A descriptor-mismatch error surfaces on
|
|
45
|
+
* first use rather than at boot.
|
|
46
|
+
*/
|
|
47
|
+
static async create(identity, walletRepository) {
|
|
48
|
+
return new HDDescriptorProvider(identity, walletRepository);
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Allocate the next descriptor and return it. The first call on a fresh
|
|
52
|
+
* wallet returns descriptor at index 0; subsequent calls return 1, 2, 3,
|
|
53
|
+
* ... in order. Each call is atomic with respect to other rotations on
|
|
54
|
+
* the same repo: two concurrent callers can never observe the same
|
|
55
|
+
* index.
|
|
56
|
+
*/
|
|
57
|
+
async getNextSigningDescriptor() {
|
|
58
|
+
return this.mutate((settings) => {
|
|
59
|
+
const next = settings.lastIndexUsed === undefined
|
|
60
|
+
? 0
|
|
61
|
+
: settings.lastIndexUsed + 1;
|
|
62
|
+
settings.lastIndexUsed = next;
|
|
63
|
+
return this.materializeAt(next);
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Returns true when the given descriptor is derivable from this wallet's
|
|
68
|
+
* seed. Delegates to the underlying identity, which handles both HD and
|
|
69
|
+
* simple `tr(pubkey)` descriptors.
|
|
70
|
+
*/
|
|
71
|
+
isOurs(descriptor) {
|
|
72
|
+
return this.identity.isOurs(descriptor);
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Signs each request with the key derived from its descriptor. Delegates
|
|
76
|
+
* to the identity's signing primitives — the identity, not the provider,
|
|
77
|
+
* holds the seed.
|
|
78
|
+
*/
|
|
79
|
+
async signWithDescriptor(requests) {
|
|
80
|
+
return this.identity.signWithDescriptor(requests);
|
|
81
|
+
}
|
|
82
|
+
/** Signs a message using the key derived from `descriptor`. */
|
|
83
|
+
async signMessageWithDescriptor(descriptor, message, signatureType = "schnorr") {
|
|
84
|
+
return this.identity.signMessageWithDescriptor(descriptor, message, signatureType);
|
|
85
|
+
}
|
|
86
|
+
// ── internals ────────────────────────────────────────────────────
|
|
87
|
+
/**
|
|
88
|
+
* Substitute the wildcard in the identity's account-descriptor template
|
|
89
|
+
* with a concrete index, going through the descriptors-scure parser
|
|
90
|
+
* rather than ad-hoc string substitution. The parser's `expand({ index })`
|
|
91
|
+
* call validates that the input is a ranged template AND produces a
|
|
92
|
+
* canonical materialized key expression at the given index.
|
|
93
|
+
*/
|
|
94
|
+
materializeAt(index) {
|
|
95
|
+
const descriptor = this.identity.descriptor;
|
|
96
|
+
const network = (0, descriptor_1.isMainnetDescriptor)(descriptor)
|
|
97
|
+
? descriptors_scure_1.networks.bitcoin
|
|
98
|
+
: descriptors_scure_1.networks.testnet;
|
|
99
|
+
const expansion = (0, descriptors_scure_1.expand)({ descriptor, network, index });
|
|
100
|
+
const keyInfo = expansion.expansionMap?.["@0"];
|
|
101
|
+
if (!keyInfo?.keyExpression) {
|
|
102
|
+
throw new Error(`HDDescriptorProvider: cannot materialize descriptor at index ${index}`);
|
|
103
|
+
}
|
|
104
|
+
return `tr(${keyInfo.keyExpression})`;
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* Run the read-modify-write of HD settings inside the shared per-repo
|
|
108
|
+
* wallet-state mutex. The closure receives a freshly-validated settings
|
|
109
|
+
* snapshot, mutates it, and returns whatever value the caller wants to
|
|
110
|
+
* surface; the mutated settings are then persisted as part of the same
|
|
111
|
+
* atomic update.
|
|
112
|
+
*
|
|
113
|
+
* Doing the read inside the lock is what prevents two providers (or two
|
|
114
|
+
* concurrent callers on the same provider) from racing on a stale index.
|
|
115
|
+
*/
|
|
116
|
+
async mutate(fn) {
|
|
117
|
+
let result;
|
|
118
|
+
await (0, syncCursors_1.updateWalletState)(this.walletRepository, (state) => {
|
|
119
|
+
const settings = this.parseSettings(state);
|
|
120
|
+
result = fn(settings);
|
|
121
|
+
return {
|
|
122
|
+
...state,
|
|
123
|
+
settings: {
|
|
124
|
+
...(state.settings ?? {}),
|
|
125
|
+
[HD_SETTINGS_KEY]: settings,
|
|
126
|
+
},
|
|
127
|
+
};
|
|
128
|
+
});
|
|
129
|
+
return result;
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* Validate the persisted HD settings (or initialize a fresh record when
|
|
133
|
+
* absent) and return a clone safe for the caller to mutate.
|
|
134
|
+
*
|
|
135
|
+
* The cast to `HDWalletSettings` trusts storage; a corrupted or
|
|
136
|
+
* partially-migrated repo could otherwise produce `NaN` descriptors.
|
|
137
|
+
* Fail loud rather than silently derive garbage.
|
|
138
|
+
*/
|
|
139
|
+
parseSettings(state) {
|
|
140
|
+
const stored = state.settings?.[HD_SETTINGS_KEY];
|
|
141
|
+
const expected = this.identity.descriptor;
|
|
142
|
+
if (!stored) {
|
|
143
|
+
return { descriptor: expected };
|
|
144
|
+
}
|
|
145
|
+
if (stored.descriptor !== expected) {
|
|
146
|
+
throw new Error(`HD descriptor mismatch: stored "${stored.descriptor}", expected "${expected}". ` +
|
|
147
|
+
`Refusing to reuse HD state from a different identity.`);
|
|
148
|
+
}
|
|
149
|
+
if (stored.lastIndexUsed !== undefined &&
|
|
150
|
+
(typeof stored.lastIndexUsed !== "number" ||
|
|
151
|
+
!Number.isInteger(stored.lastIndexUsed) ||
|
|
152
|
+
stored.lastIndexUsed < 0)) {
|
|
153
|
+
throw new Error(`Corrupt HD settings: lastIndexUsed is not a non-negative integer (got ${String(stored.lastIndexUsed)}).`);
|
|
154
|
+
}
|
|
155
|
+
// Shallow clone so the closure may mutate without aliasing the repo's copy.
|
|
156
|
+
return { ...stored };
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
exports.HDDescriptorProvider = HDDescriptorProvider;
|
package/dist/cjs/wallet/index.js
CHANGED
|
@@ -1,10 +1,14 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.TxType = void 0;
|
|
3
|
+
exports.TxType = exports.DEFAULT_NETWORK_NAME = exports.DEFAULT_ARKADE_HRP = exports.DEFAULT_ARKADE_SERVER_URL = void 0;
|
|
4
4
|
exports.isSpendable = isSpendable;
|
|
5
5
|
exports.isRecoverable = isRecoverable;
|
|
6
6
|
exports.isExpired = isExpired;
|
|
7
7
|
exports.isSubdust = isSubdust;
|
|
8
|
+
/** Defaults */
|
|
9
|
+
exports.DEFAULT_ARKADE_SERVER_URL = "https://arkade.computer";
|
|
10
|
+
exports.DEFAULT_ARKADE_HRP = "ark";
|
|
11
|
+
exports.DEFAULT_NETWORK_NAME = "bitcoin";
|
|
8
12
|
/** Wallet transaction direction. */
|
|
9
13
|
var TxType;
|
|
10
14
|
(function (TxType) {
|
|
@@ -3,6 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.OnchainWallet = void 0;
|
|
4
4
|
exports.selectCoins = selectCoins;
|
|
5
5
|
const btc_signer_1 = require("@scure/btc-signer");
|
|
6
|
+
const _1 = require(".");
|
|
6
7
|
const networks_1 = require("../networks");
|
|
7
8
|
const onchain_1 = require("../providers/onchain");
|
|
8
9
|
const anchor_1 = require("../utils/anchor");
|
|
@@ -43,7 +44,7 @@ class OnchainWallet {
|
|
|
43
44
|
* @defaultValue `provider = new EsploraProvider('https://mempool.space/api')`
|
|
44
45
|
* @throws Error if the configured identity cannot produce a valid x-only public key
|
|
45
46
|
*/
|
|
46
|
-
static async create(identity, networkName, provider) {
|
|
47
|
+
static async create(identity, networkName = _1.DEFAULT_NETWORK_NAME, provider) {
|
|
47
48
|
const pubkey = await identity.xOnlyPublicKey();
|
|
48
49
|
if (!pubkey) {
|
|
49
50
|
throw new Error("Invalid configured public 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");
|
|
@@ -514,7 +516,7 @@ class WalletMessageHandler {
|
|
|
514
516
|
for (const vtxo of spendableVtxos) {
|
|
515
517
|
if (vtxo.assets) {
|
|
516
518
|
for (const a of vtxo.assets) {
|
|
517
|
-
const current = assetBalances.get(a.assetId) ??
|
|
519
|
+
const current = assetBalances.get(a.assetId) ?? 0n;
|
|
518
520
|
assetBalances.set(a.assetId, current + a.amount);
|
|
519
521
|
}
|
|
520
522
|
}
|
|
@@ -587,11 +589,45 @@ class WalletMessageHandler {
|
|
|
587
589
|
const { newVtxos, spentVtxos } = funds;
|
|
588
590
|
if (newVtxos.length + spentVtxos.length === 0)
|
|
589
591
|
return;
|
|
590
|
-
//
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
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
|
/**
|
|
@@ -8,6 +8,7 @@ const repositories_1 = require("../../repositories");
|
|
|
8
8
|
const wallet_message_handler_1 = require("./wallet-message-handler");
|
|
9
9
|
const utils_2 = require("../utils");
|
|
10
10
|
const errors_1 = require("../../worker/errors");
|
|
11
|
+
const wallet_1 = require("../wallet");
|
|
11
12
|
// Check by error message content instead of instanceof because postMessage uses the
|
|
12
13
|
// structured clone algorithm which strips the prototype chain — the page
|
|
13
14
|
// receives a plain Error, not the original MessageBusNotInitializedError.
|
|
@@ -214,7 +215,7 @@ class ServiceWorkerReadonlyWallet {
|
|
|
214
215
|
.then(base_1.hex.encode);
|
|
215
216
|
const initWalletPayload = {
|
|
216
217
|
key: { publicKey },
|
|
217
|
-
arkServerUrl: options
|
|
218
|
+
arkServerUrl: (0, wallet_1.getArkadeServerUrl)(options),
|
|
218
219
|
arkServerPublicKey: options.arkServerPublicKey,
|
|
219
220
|
delegatorUrl: options.delegatorUrl,
|
|
220
221
|
};
|
|
@@ -229,7 +230,7 @@ class ServiceWorkerReadonlyWallet {
|
|
|
229
230
|
const busInitConfig = {
|
|
230
231
|
wallet: serializedWallet,
|
|
231
232
|
arkServer: {
|
|
232
|
-
url: options
|
|
233
|
+
url: (0, wallet_1.getArkadeServerUrl)(options),
|
|
233
234
|
publicKey: options.arkServerPublicKey,
|
|
234
235
|
},
|
|
235
236
|
delegatorUrl: options.delegatorUrl,
|
|
@@ -888,7 +889,7 @@ class ServiceWorkerWallet extends ServiceWorkerReadonlyWallet {
|
|
|
888
889
|
: null;
|
|
889
890
|
const initWalletPayload = {
|
|
890
891
|
key: legacyPrivateKey ? { privateKey: legacyPrivateKey } : {},
|
|
891
|
-
arkServerUrl: options
|
|
892
|
+
arkServerUrl: (0, wallet_1.getArkadeServerUrl)(options),
|
|
892
893
|
arkServerPublicKey: options.arkServerPublicKey,
|
|
893
894
|
delegatorUrl: options.delegatorUrl,
|
|
894
895
|
};
|
|
@@ -903,7 +904,7 @@ class ServiceWorkerWallet extends ServiceWorkerReadonlyWallet {
|
|
|
903
904
|
const busInitConfig = {
|
|
904
905
|
wallet: serializedWallet,
|
|
905
906
|
arkServer: {
|
|
906
|
-
url: options
|
|
907
|
+
url: (0, wallet_1.getArkadeServerUrl)(options),
|
|
907
908
|
publicKey: options.arkServerPublicKey,
|
|
908
909
|
},
|
|
909
910
|
delegatorUrl: options.delegatorUrl,
|
|
@@ -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
|
|
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
|
-
|
|
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
|
|
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(
|
|
236
|
-
return
|
|
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 () => {
|
|
@@ -148,8 +148,7 @@ function validateAssetGroupOutput(packet, outputIndex, assetId, expectedAmount)
|
|
|
148
148
|
if (!assetOutput) {
|
|
149
149
|
throw (0, exports.ErrAssetOutputNotFound)(assetId, outputIndex);
|
|
150
150
|
}
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
throw (0, exports.ErrInvalidAssetOutputAmount)(assetOutput.amount, expectedAmountBigInt, assetId);
|
|
151
|
+
if (assetOutput.amount !== expectedAmount) {
|
|
152
|
+
throw (0, exports.ErrInvalidAssetOutputAmount)(assetOutput.amount, expectedAmount, assetId);
|
|
154
153
|
}
|
|
155
154
|
}
|