@qntx/wallet-engine 0.4.0 → 0.5.2
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/LICENSE-APACHE +201 -0
- package/LICENSE-MIT +21 -0
- package/README.md +59 -0
- package/dist/index.d.mts +271 -10
- package/dist/index.mjs +2603 -649
- package/dist/index.mjs.map +1 -1
- package/package.json +22 -10
package/dist/index.mjs
CHANGED
|
@@ -2,11 +2,25 @@ import { createStore } from "zustand/vanilla";
|
|
|
2
2
|
import { SparkInputError, bitcoinAddressScript, createSparkClient, decodeBolt11, decodeSparkAddress, encodeSparkAddress, parseSparkInvoice, uuidv7 } from "@qntx/spark";
|
|
3
3
|
import { nostr } from "@qntx/wallet/nostr";
|
|
4
4
|
import { spark } from "@qntx/wallet/spark";
|
|
5
|
+
import { createPublicClient, decodeFunctionResult, encodeFunctionData, fallback, http, isAddress, isHex, numberToHex } from "viem";
|
|
6
|
+
import { bitcoinAddressFromXpub, bitcoinNetwork, bitcoinPubkeyFromXpub, decodeBitcoinPsbt } from "@qntx/wallet-chains/bitcoin";
|
|
7
|
+
import { chains } from "@qntx/wallet/chains";
|
|
8
|
+
import { sha256 } from "@noble/hashes/sha2.js";
|
|
9
|
+
import { Address, OutScript, Transaction } from "@scure/btc-signer";
|
|
10
|
+
import { checksumEvmAddress, decodeEvm } from "@qntx/wallet-chains/evm";
|
|
11
|
+
import { bytesToHex, utf8ToBytes } from "@noble/hashes/utils.js";
|
|
5
12
|
import { parseBackupHeader } from "@qntx/wallet";
|
|
6
13
|
import { signedMatchesUnsigned, verifyEvent } from "@qntx/nostr";
|
|
14
|
+
import { base64, hex } from "@scure/base";
|
|
15
|
+
import { p2wpkh } from "@scure/btc-signer/payment.js";
|
|
16
|
+
import { selectUTXO } from "@scure/btc-signer/utxo.js";
|
|
7
17
|
import { decodeLnurl, encodeLnurl, lightningAddressUrl, registerLightningAddress, requestInvoice, resolveLnurl, submitWithdraw, unregisterLightningAddress } from "@qntx/spark/lnurl";
|
|
8
18
|
import { makeZapRequest } from "@qntx/nostr/nips/nip57";
|
|
9
|
-
import {
|
|
19
|
+
import { PROGRAM_IDS, decodeSolana, netAfterTransferFee, parseMintExtensions } from "@qntx/wallet-chains/solana";
|
|
20
|
+
import { getSetComputeUnitLimitInstruction, getSetComputeUnitPriceInstruction } from "@solana-program/compute-budget";
|
|
21
|
+
import { getTransferSolInstruction } from "@solana-program/system";
|
|
22
|
+
import { TOKEN_PROGRAM_ADDRESS, findAssociatedTokenPda, getCreateAssociatedTokenIdempotentInstruction, getTransferCheckedInstruction } from "@solana-program/token";
|
|
23
|
+
import { address, appendTransactionMessageInstructions, blockhash, compileTransactionMessage, createNoopSigner, createTransactionMessage, getCompiledTransactionMessageEncoder, setTransactionMessageFeePayer, setTransactionMessageLifetimeUsingBlockhash } from "@solana/kit";
|
|
10
24
|
//#region src/controller.ts
|
|
11
25
|
/**
|
|
12
26
|
* Controller base: a zustand vanilla store plus lifecycle. `update` applies a recipe that must
|
|
@@ -119,6 +133,185 @@ function createMemoryEngineStore(backing) {
|
|
|
119
133
|
};
|
|
120
134
|
}
|
|
121
135
|
//#endregion
|
|
136
|
+
//#region src/chains/bitcoin-scan.ts
|
|
137
|
+
/**
|
|
138
|
+
* Gap-20 BIP-44/86 account scanning over Esplora — derives /0/* and /1/* from the account xpub,
|
|
139
|
+
* walks each chain in windows until 20 consecutive unused addresses, persists the discovered
|
|
140
|
+
* addresses/txs/UTXOs in EngineStore (rebuildable from a rescan), and exposes the next unused
|
|
141
|
+
* receive/change address.
|
|
142
|
+
*/
|
|
143
|
+
const GAP = 20;
|
|
144
|
+
const isObject$10 = (v) => typeof v === "object" && v !== null;
|
|
145
|
+
const isUnknownArray = (v) => Array.isArray(v);
|
|
146
|
+
const asArray = (v) => isUnknownArray(v) ? v : [];
|
|
147
|
+
/** The scriptPubKey bytes of a bech32/base58 address on `network`. */
|
|
148
|
+
const scriptPubKey = (address, network) => OutScript.encode(Address(network).decode(address));
|
|
149
|
+
/** Esplora scripthash: sha256 of the scriptPubKey, digest hex as-is (verified vs electrs REST). */
|
|
150
|
+
const scripthash = (address, network) => [...sha256(OutScript.encode(Address(network).decode(address)))].map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
151
|
+
const windowUsed = async (es, params, addrs) => Promise.all(addrs.map(async (a) => {
|
|
152
|
+
const txs = await es.get(`/scripthash/${scripthash(a.address, params)}/txs`);
|
|
153
|
+
return isUnknownArray(txs) && txs.length > 0;
|
|
154
|
+
}));
|
|
155
|
+
const scanChain = async (es, xpub, template, network, params, chain, start = 0) => {
|
|
156
|
+
const batch = Array.from({ length: GAP }, (_, i) => {
|
|
157
|
+
const index = start + i;
|
|
158
|
+
return {
|
|
159
|
+
chain,
|
|
160
|
+
index,
|
|
161
|
+
address: bitcoinAddressFromXpub(xpub, template, network, chain, index),
|
|
162
|
+
used: false
|
|
163
|
+
};
|
|
164
|
+
});
|
|
165
|
+
const used = await windowUsed(es, params, batch);
|
|
166
|
+
const marked = batch.map((a, i) => ({
|
|
167
|
+
chain: a.chain,
|
|
168
|
+
index: a.index,
|
|
169
|
+
address: a.address,
|
|
170
|
+
used: used[i] === true
|
|
171
|
+
}));
|
|
172
|
+
if (!marked.some((a) => a.used)) return marked;
|
|
173
|
+
const rest = await scanChain(es, xpub, template, network, params, chain, start + GAP);
|
|
174
|
+
return [...marked, ...rest];
|
|
175
|
+
};
|
|
176
|
+
const scanAddressUtxos = async (es, a, params) => {
|
|
177
|
+
const utxos = await es.get(`/scripthash/${scripthash(a.address, params)}/utxo`);
|
|
178
|
+
return asArray(utxos).flatMap((u) => {
|
|
179
|
+
const o = isObject$10(u) ? u : {};
|
|
180
|
+
const status = isObject$10(o["status"]) ? o["status"] : {};
|
|
181
|
+
const txid = typeof o["txid"] === "string" ? o["txid"] : "";
|
|
182
|
+
if (txid === "") return [];
|
|
183
|
+
return [{
|
|
184
|
+
txid,
|
|
185
|
+
vout: typeof o["vout"] === "number" ? o["vout"] : 0,
|
|
186
|
+
value: typeof o["value"] === "number" ? o["value"] : 0,
|
|
187
|
+
address: a.address,
|
|
188
|
+
height: typeof status["block_height"] === "number" ? status["block_height"] : 0
|
|
189
|
+
}];
|
|
190
|
+
});
|
|
191
|
+
};
|
|
192
|
+
const scanUtxos = async (es, addresses, params) => {
|
|
193
|
+
const candidates = addresses.filter((a) => a.used || a.chain === 1 || a.index < GAP);
|
|
194
|
+
return Promise.all(candidates.map(async (a) => scanAddressUtxos(es, a, params))).then((r) => r.flat());
|
|
195
|
+
};
|
|
196
|
+
const str$1 = (v) => typeof v === "string" ? v : void 0;
|
|
197
|
+
const num = (v) => typeof v === "number" ? v : void 0;
|
|
198
|
+
const toScanUtxo = (v) => {
|
|
199
|
+
if (!isObject$10(v)) return [];
|
|
200
|
+
const txid = str$1(v["txid"]);
|
|
201
|
+
const vout = num(v["vout"]);
|
|
202
|
+
const value = num(v["value"]);
|
|
203
|
+
const address = str$1(v["address"]);
|
|
204
|
+
const height = num(v["height"]);
|
|
205
|
+
return txid !== void 0 && vout !== void 0 && value !== void 0 && address !== void 0 ? [{
|
|
206
|
+
txid,
|
|
207
|
+
vout,
|
|
208
|
+
value,
|
|
209
|
+
address,
|
|
210
|
+
height: height ?? 0
|
|
211
|
+
}] : [];
|
|
212
|
+
};
|
|
213
|
+
const toLockedUtxo = (v) => {
|
|
214
|
+
const [base] = toScanUtxo(v);
|
|
215
|
+
if (base === void 0 || !isObject$10(v)) return;
|
|
216
|
+
const status = str$1(v["status"]);
|
|
217
|
+
const txId = str$1(v["txId"]);
|
|
218
|
+
if (status !== "confirmed" && status !== "locked" && status !== "frozen") return;
|
|
219
|
+
return txId === void 0 ? {
|
|
220
|
+
txid: base.txid,
|
|
221
|
+
vout: base.vout,
|
|
222
|
+
value: base.value,
|
|
223
|
+
address: base.address,
|
|
224
|
+
height: base.height,
|
|
225
|
+
status
|
|
226
|
+
} : {
|
|
227
|
+
txid: base.txid,
|
|
228
|
+
vout: base.vout,
|
|
229
|
+
value: base.value,
|
|
230
|
+
address: base.address,
|
|
231
|
+
height: base.height,
|
|
232
|
+
status,
|
|
233
|
+
txId
|
|
234
|
+
};
|
|
235
|
+
};
|
|
236
|
+
var BtcUtxoStore = class BtcUtxoStore {
|
|
237
|
+
engineStore;
|
|
238
|
+
utxos = /* @__PURE__ */ new Map();
|
|
239
|
+
async unlock(store) {
|
|
240
|
+
this.engineStore = store;
|
|
241
|
+
const raw = await store.collection("btc-utxos").get("state");
|
|
242
|
+
this.utxos = raw === void 0 ? /* @__PURE__ */ new Map() : new Map(Object.entries((() => {
|
|
243
|
+
const rawParsed = JSON.parse(new TextDecoder().decode(raw));
|
|
244
|
+
const parsed = isObject$10(rawParsed) ? rawParsed : {};
|
|
245
|
+
const pairs = Object.entries(parsed).flatMap(([k, v]) => {
|
|
246
|
+
const u = toLockedUtxo(v);
|
|
247
|
+
return u === void 0 ? [] : [[k, u]];
|
|
248
|
+
});
|
|
249
|
+
return Object.fromEntries(pairs);
|
|
250
|
+
})()));
|
|
251
|
+
}
|
|
252
|
+
async persist() {
|
|
253
|
+
const store = this.engineStore;
|
|
254
|
+
if (store !== void 0) await store.collection("btc-utxos").put("state", new TextEncoder().encode(JSON.stringify(Object.fromEntries(this.utxos))));
|
|
255
|
+
}
|
|
256
|
+
static key(u) {
|
|
257
|
+
return `${u.txid}:${u.vout}`;
|
|
258
|
+
}
|
|
259
|
+
replace(utxos) {
|
|
260
|
+
const locked = [...this.utxos.entries()].filter(([, u]) => u.status === "locked" || u.status === "frozen");
|
|
261
|
+
this.utxos = new Map(utxos.map((u) => [BtcUtxoStore.key(u), {
|
|
262
|
+
txid: u.txid,
|
|
263
|
+
vout: u.vout,
|
|
264
|
+
value: u.value,
|
|
265
|
+
address: u.address,
|
|
266
|
+
height: u.height,
|
|
267
|
+
status: "confirmed"
|
|
268
|
+
}]));
|
|
269
|
+
for (const [k, u] of locked) this.utxos.set(k, u);
|
|
270
|
+
}
|
|
271
|
+
lock(utxos, txId) {
|
|
272
|
+
for (const u of utxos) this.utxos.set(BtcUtxoStore.key(u), {
|
|
273
|
+
txid: u.txid,
|
|
274
|
+
vout: u.vout,
|
|
275
|
+
value: u.value,
|
|
276
|
+
address: u.address,
|
|
277
|
+
height: u.height,
|
|
278
|
+
status: "locked",
|
|
279
|
+
txId
|
|
280
|
+
});
|
|
281
|
+
this.persist();
|
|
282
|
+
}
|
|
283
|
+
/** Released on failed/rejected/dropped; spent-removed on confirmed. */
|
|
284
|
+
endTx(txId, final) {
|
|
285
|
+
if (final === "confirmed") {
|
|
286
|
+
for (const [k, u] of this.utxos) if (u.txId === txId) this.utxos.delete(k);
|
|
287
|
+
} else for (const [k, u] of this.utxos) if (u.txId === txId && u.status === "locked") this.utxos.set(k, {
|
|
288
|
+
txid: u.txid,
|
|
289
|
+
vout: u.vout,
|
|
290
|
+
value: u.value,
|
|
291
|
+
address: u.address,
|
|
292
|
+
height: u.height,
|
|
293
|
+
status: "confirmed"
|
|
294
|
+
});
|
|
295
|
+
this.persist();
|
|
296
|
+
}
|
|
297
|
+
spendable() {
|
|
298
|
+
return [...this.utxos.values()].filter((u) => u.status === "confirmed" && !(u.value === 546 || u.value === 330));
|
|
299
|
+
}
|
|
300
|
+
frozen() {
|
|
301
|
+
return [...this.utxos.values()].filter((u) => u.status === "frozen" || u.value === 546 || u.value === 330);
|
|
302
|
+
}
|
|
303
|
+
all() {
|
|
304
|
+
return [...this.utxos.values()];
|
|
305
|
+
}
|
|
306
|
+
lockStore() {
|
|
307
|
+
this.engineStore = void 0;
|
|
308
|
+
}
|
|
309
|
+
};
|
|
310
|
+
//#endregion
|
|
311
|
+
//#region src/transactions/route.ts
|
|
312
|
+
const newId = (random) => `tx-${[...random(8)].map((b) => b.toString(16).padStart(2, "0")).join("")}`;
|
|
313
|
+
const errorMessage = (error) => error instanceof Error ? error.message : String(error);
|
|
314
|
+
//#endregion
|
|
122
315
|
//#region src/accounts/errors.ts
|
|
123
316
|
var AccountTreeError = class extends Error {
|
|
124
317
|
category;
|
|
@@ -262,6 +455,18 @@ function encodeTree(tree) {
|
|
|
262
455
|
* selected again). The active account's `balanceSats` feeds `totals`, debounced 1 s before
|
|
263
456
|
* persist.
|
|
264
457
|
*/
|
|
458
|
+
const check = async (family, run) => {
|
|
459
|
+
try {
|
|
460
|
+
return [family, { used: await run() }];
|
|
461
|
+
} catch (error) {
|
|
462
|
+
return [family, {
|
|
463
|
+
used: false,
|
|
464
|
+
error: errorMessage(error)
|
|
465
|
+
}];
|
|
466
|
+
}
|
|
467
|
+
};
|
|
468
|
+
const evmAddress = (v) => typeof v === "string" && isAddress(v) ? v : void 0;
|
|
469
|
+
const addressOf = (v) => typeof v === "string" ? v : void 0;
|
|
265
470
|
var AccountTreeController = class extends Controller {
|
|
266
471
|
options;
|
|
267
472
|
col;
|
|
@@ -646,6 +851,125 @@ var AccountTreeController = class extends Controller {
|
|
|
646
851
|
}
|
|
647
852
|
return found;
|
|
648
853
|
}
|
|
854
|
+
/**
|
|
855
|
+
* Per-group chain-family discovery — same stop rule as `discoverSpark`: group 0 always, stop
|
|
856
|
+
* after two consecutive fully-unused groups. A family error is reported on the group and does NOT
|
|
857
|
+
* count as unused (the group stays in `found` — it can't be proven empty).
|
|
858
|
+
*/
|
|
859
|
+
async discoverGroups(walletId) {
|
|
860
|
+
this.mustWallet(walletId);
|
|
861
|
+
const apis = this.options.chains ?? chains(this.options.wallet);
|
|
862
|
+
const nets = this.options.networks;
|
|
863
|
+
const found = [0];
|
|
864
|
+
let index = 0;
|
|
865
|
+
let unused = 0;
|
|
866
|
+
while (index < 65) {
|
|
867
|
+
const groupIndex = index;
|
|
868
|
+
const evm = async () => {
|
|
869
|
+
if (nets === void 0) return false;
|
|
870
|
+
return (await Promise.all(nets.enabled("evm").map(async (network) => {
|
|
871
|
+
const acct = await apis.account({
|
|
872
|
+
walletId,
|
|
873
|
+
groupIndex,
|
|
874
|
+
template: "evm.bip44",
|
|
875
|
+
network
|
|
876
|
+
});
|
|
877
|
+
const address = "address" in acct ? evmAddress(acct.address) : void 0;
|
|
878
|
+
if (address === void 0) return false;
|
|
879
|
+
const client = nets.client(network);
|
|
880
|
+
const [nonce, balance] = await Promise.all([client.getTransactionCount({
|
|
881
|
+
address,
|
|
882
|
+
blockTag: "latest"
|
|
883
|
+
}), client.getBalance({ address })]);
|
|
884
|
+
return nonce > 0 || balance > 0n;
|
|
885
|
+
}))).some(Boolean);
|
|
886
|
+
};
|
|
887
|
+
const solana = async () => {
|
|
888
|
+
if (nets === void 0) return false;
|
|
889
|
+
return (await Promise.all(nets.enabled("solana").map(async (network) => {
|
|
890
|
+
const acct = await apis.account({
|
|
891
|
+
walletId,
|
|
892
|
+
groupIndex,
|
|
893
|
+
template: "solana.bip44-change",
|
|
894
|
+
network
|
|
895
|
+
});
|
|
896
|
+
const address = "address" in acct ? addressOf(acct.address) : void 0;
|
|
897
|
+
if (address === void 0) return false;
|
|
898
|
+
const sigs = await nets.solanaRpc(network).call("getSignaturesForAddress", [address, { limit: 1 }]);
|
|
899
|
+
return Array.isArray(sigs) && sigs.length > 0;
|
|
900
|
+
}))).some(Boolean);
|
|
901
|
+
};
|
|
902
|
+
const bitcoin = async () => {
|
|
903
|
+
if (nets === void 0) return false;
|
|
904
|
+
const combos = nets.enabled("bitcoin").flatMap((network) => [
|
|
905
|
+
"btc.bip44",
|
|
906
|
+
"btc.bip49",
|
|
907
|
+
"btc.bip84",
|
|
908
|
+
"btc.bip86"
|
|
909
|
+
].map((template) => [network, template]));
|
|
910
|
+
return (await Promise.all(combos.map(async ([network, template]) => {
|
|
911
|
+
const net = bitcoinNetwork(network);
|
|
912
|
+
const es = nets.esplora(network);
|
|
913
|
+
const acct = await apis.account({
|
|
914
|
+
walletId,
|
|
915
|
+
groupIndex,
|
|
916
|
+
template,
|
|
917
|
+
network
|
|
918
|
+
});
|
|
919
|
+
const xpub = "xpub" in acct ? acct.xpub : void 0;
|
|
920
|
+
if (xpub === void 0) return false;
|
|
921
|
+
const [receive, change] = await Promise.all([scanChain(es, xpub, template, network, net.params, 0), scanChain(es, xpub, template, network, net.params, 1)]);
|
|
922
|
+
return receive.some((a) => a.used) || change.some((a) => a.used);
|
|
923
|
+
}))).some(Boolean);
|
|
924
|
+
};
|
|
925
|
+
const spark = async () => {
|
|
926
|
+
const ref = {
|
|
927
|
+
walletId,
|
|
928
|
+
groupIndex,
|
|
929
|
+
network: this.options.sparkNetwork
|
|
930
|
+
};
|
|
931
|
+
const { used } = await this.options.spark.inspect(ref);
|
|
932
|
+
return used;
|
|
933
|
+
};
|
|
934
|
+
const results = await Promise.all([
|
|
935
|
+
check("spark", spark),
|
|
936
|
+
check("evm", evm),
|
|
937
|
+
check("solana", solana),
|
|
938
|
+
check("bitcoin", bitcoin)
|
|
939
|
+
]);
|
|
940
|
+
const usedMap = Object.fromEntries(results.map(([k, v]) => [k, v.used]));
|
|
941
|
+
const errorMap = Object.fromEntries(results.flatMap(([k, v]) => v.error === void 0 ? [] : [[k, v.error]]));
|
|
942
|
+
const anyUsed = results.some(([, v]) => v.used);
|
|
943
|
+
const anyError = results.some(([, v]) => v.error !== void 0);
|
|
944
|
+
this.mutateGroups(walletId, (list) => {
|
|
945
|
+
const have = list.some((g) => g.index === groupIndex);
|
|
946
|
+
const updatedAt = this.options.now();
|
|
947
|
+
if (!have) return [...list, {
|
|
948
|
+
walletId,
|
|
949
|
+
index: groupIndex,
|
|
950
|
+
hidden: false,
|
|
951
|
+
used: usedMap,
|
|
952
|
+
errors: errorMap,
|
|
953
|
+
updatedAt
|
|
954
|
+
}].sort((a, b) => a.index - b.index);
|
|
955
|
+
return list.map((g) => g.index === groupIndex ? {
|
|
956
|
+
...g,
|
|
957
|
+
used: usedMap,
|
|
958
|
+
errors: errorMap,
|
|
959
|
+
updatedAt
|
|
960
|
+
} : g);
|
|
961
|
+
});
|
|
962
|
+
if (anyUsed || anyError) {
|
|
963
|
+
if (index !== 0 && anyUsed) found.push(index);
|
|
964
|
+
unused = 0;
|
|
965
|
+
} else {
|
|
966
|
+
unused += 1;
|
|
967
|
+
if (index !== 0 && unused >= 2) break;
|
|
968
|
+
}
|
|
969
|
+
index += 1;
|
|
970
|
+
}
|
|
971
|
+
return found;
|
|
972
|
+
}
|
|
649
973
|
/** Refresh `totals` for every other visible group; per-group failures are logged and skipped. */
|
|
650
974
|
async refreshTotals() {
|
|
651
975
|
const state = this.getState();
|
|
@@ -815,635 +1139,589 @@ function persistedToState(tree) {
|
|
|
815
1139
|
};
|
|
816
1140
|
}
|
|
817
1141
|
//#endregion
|
|
818
|
-
//#region
|
|
819
|
-
const fromNumH = (n) => n / 2 ** 32 | 0;
|
|
820
|
-
const fromNumL = (n) => n >>> 0;
|
|
821
|
-
function setU64FromNum(view, byteOffset, n, isLE) {
|
|
822
|
-
const h = fromNumH(n);
|
|
823
|
-
const l = fromNumL(n);
|
|
824
|
-
view.setUint32(byteOffset, isLE ? l : h, isLE);
|
|
825
|
-
view.setUint32(byteOffset + 4, isLE ? h : l, isLE);
|
|
826
|
-
}
|
|
827
|
-
//#endregion
|
|
828
|
-
//#region ../../node_modules/.bun/@noble+hashes@2.4.0/node_modules/@noble/hashes/utils.js
|
|
829
|
-
/**
|
|
830
|
-
* Checks if something is Uint8Array. Be careful: nodejs Buffer will return true.
|
|
831
|
-
* @param a - value to test
|
|
832
|
-
* @returns `true` when the value is a Uint8Array-compatible view.
|
|
833
|
-
* @example
|
|
834
|
-
* Check whether a value is a Uint8Array-compatible view.
|
|
835
|
-
* ```ts
|
|
836
|
-
* isBytes(new Uint8Array([1, 2, 3]));
|
|
837
|
-
* ```
|
|
838
|
-
*/
|
|
839
|
-
function isBytes(a) {
|
|
840
|
-
return a instanceof Uint8Array || ArrayBuffer.isView(a) && a.constructor.name === "Uint8Array" && "BYTES_PER_ELEMENT" in a && a.BYTES_PER_ELEMENT === 1;
|
|
841
|
-
}
|
|
842
|
-
const atitle = (title) => title ? `"${title}" ` : "";
|
|
843
|
-
/**
|
|
844
|
-
* Asserts something is a non-negative integer.
|
|
845
|
-
* @param n - number to validate
|
|
846
|
-
* @param title - label included in thrown errors
|
|
847
|
-
* @returns The validated number.
|
|
848
|
-
* @throws On wrong argument types. {@link TypeError}
|
|
849
|
-
* @throws On wrong argument ranges or values. {@link RangeError}
|
|
850
|
-
* @example
|
|
851
|
-
* Validate a non-negative integer option.
|
|
852
|
-
* ```ts
|
|
853
|
-
* anumber(32, 'length');
|
|
854
|
-
* ```
|
|
855
|
-
*/
|
|
856
|
-
function anumber(n, title = "") {
|
|
857
|
-
if (typeof n !== "number") throw new TypeError(atitle(title) + "expected number, got " + typeof n);
|
|
858
|
-
if (!Number.isSafeInteger(n) || n < 0) throw new RangeError(atitle(title) + "expected integer >= 0, got " + n);
|
|
859
|
-
return n;
|
|
860
|
-
}
|
|
1142
|
+
//#region src/chains/evm.ts
|
|
861
1143
|
/**
|
|
862
|
-
*
|
|
863
|
-
*
|
|
864
|
-
*
|
|
865
|
-
*
|
|
866
|
-
*
|
|
867
|
-
* @throws On wrong argument types. {@link TypeError}
|
|
868
|
-
* @throws On wrong argument ranges or values. {@link RangeError}
|
|
869
|
-
* @example
|
|
870
|
-
* Validate that a value is a byte array.
|
|
871
|
-
* ```ts
|
|
872
|
-
* abytes(new Uint8Array([1, 2, 3]));
|
|
873
|
-
* ```
|
|
1144
|
+
* EVM chain adapter: native transfers through a viem PublicClient injected by NetworkController.
|
|
1145
|
+
* Fee tiers come from eth_feeHistory (20 blocks, p10/p50/p90 → slow/normal/fast, maxFee = 2×baseFee +
|
|
1146
|
+
* priority); networks that cannot answer feeHistory fall back to eth_gasPrice. gas = estimateGas ×
|
|
1147
|
+
* 1.2 (ceil). Nonces are allocated on a per-(network, from) serial queue as max(pending count, last
|
|
1148
|
+
* submitted + 1). Finalization uses the `finalized` block tag; receipts with status 0 fail.
|
|
874
1149
|
*/
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
1150
|
+
const ERC20_ABI = [
|
|
1151
|
+
{
|
|
1152
|
+
type: "function",
|
|
1153
|
+
name: "transfer",
|
|
1154
|
+
inputs: [{
|
|
1155
|
+
name: "to",
|
|
1156
|
+
type: "address"
|
|
1157
|
+
}, {
|
|
1158
|
+
name: "value",
|
|
1159
|
+
type: "uint256"
|
|
1160
|
+
}],
|
|
1161
|
+
outputs: [{
|
|
1162
|
+
name: "",
|
|
1163
|
+
type: "bool"
|
|
1164
|
+
}],
|
|
1165
|
+
stateMutability: "nonpayable"
|
|
1166
|
+
},
|
|
1167
|
+
{
|
|
1168
|
+
type: "function",
|
|
1169
|
+
name: "balanceOf",
|
|
1170
|
+
inputs: [{
|
|
1171
|
+
name: "account",
|
|
1172
|
+
type: "address"
|
|
1173
|
+
}],
|
|
1174
|
+
outputs: [{
|
|
1175
|
+
name: "",
|
|
1176
|
+
type: "uint256"
|
|
1177
|
+
}],
|
|
1178
|
+
stateMutability: "view"
|
|
1179
|
+
},
|
|
1180
|
+
{
|
|
1181
|
+
type: "function",
|
|
1182
|
+
name: "symbol",
|
|
1183
|
+
inputs: [],
|
|
1184
|
+
outputs: [{
|
|
1185
|
+
name: "",
|
|
1186
|
+
type: "string"
|
|
1187
|
+
}],
|
|
1188
|
+
stateMutability: "view"
|
|
1189
|
+
},
|
|
1190
|
+
{
|
|
1191
|
+
type: "function",
|
|
1192
|
+
name: "decimals",
|
|
1193
|
+
inputs: [],
|
|
1194
|
+
outputs: [{
|
|
1195
|
+
name: "",
|
|
1196
|
+
type: "uint8"
|
|
1197
|
+
}],
|
|
1198
|
+
stateMutability: "view"
|
|
1199
|
+
}
|
|
1200
|
+
];
|
|
1201
|
+
const checkedAddress = (v) => {
|
|
1202
|
+
if (!isAddress(v)) throw new TypeError(`malformed evm address: ${v}`);
|
|
1203
|
+
return v;
|
|
887
1204
|
};
|
|
888
|
-
const
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
* import { sha256 } from '@noble/hashes/sha2.js';
|
|
904
|
-
* const hash = sha256.create();
|
|
905
|
-
* aexists(hash);
|
|
906
|
-
* ```
|
|
907
|
-
*/
|
|
908
|
-
function aexists(instance, checkFinished = true) {
|
|
909
|
-
if (instance.destroyed) throw new Error("hash was destroyed");
|
|
910
|
-
if (checkFinished && instance.finished) throw new Error("digest() was already called");
|
|
911
|
-
}
|
|
912
|
-
/**
|
|
913
|
-
* Asserts output is a sufficiently-sized byte array.
|
|
914
|
-
* @param out - destination buffer
|
|
915
|
-
* @param instance - hash instance providing output length
|
|
916
|
-
* Oversized buffers are allowed; downstream code only promises to fill the first `outputLen` bytes.
|
|
917
|
-
* @throws On wrong argument types. {@link TypeError}
|
|
918
|
-
* @throws On wrong argument ranges or values. {@link RangeError}
|
|
919
|
-
* @example
|
|
920
|
-
* Validate a caller-provided digest buffer.
|
|
921
|
-
* ```ts
|
|
922
|
-
* import { aoutput } from '@noble/hashes/utils.js';
|
|
923
|
-
* import { sha256 } from '@noble/hashes/sha2.js';
|
|
924
|
-
* const hash = sha256.create();
|
|
925
|
-
* aoutput(new Uint8Array(hash.outputLen), hash);
|
|
926
|
-
* ```
|
|
927
|
-
*/
|
|
928
|
-
function aoutput(out, instance) {
|
|
929
|
-
abytes(out, void 0, "output");
|
|
930
|
-
const min = instance.outputLen;
|
|
931
|
-
if (!(out.length >= min)) throw new RangeError("\"output\" expected length >= " + min);
|
|
932
|
-
}
|
|
933
|
-
/**
|
|
934
|
-
* Zeroizes typed arrays in place. Warning: JS provides no guarantees.
|
|
935
|
-
* @param arrays - arrays to overwrite with zeros
|
|
936
|
-
* @example
|
|
937
|
-
* Zeroize sensitive buffers in place.
|
|
938
|
-
* ```ts
|
|
939
|
-
* clean(new Uint8Array([1, 2, 3]));
|
|
940
|
-
* ```
|
|
941
|
-
*/
|
|
942
|
-
function clean(...arrays) {
|
|
943
|
-
for (let i = 0; i < arrays.length; i++) arrays[i].fill(0);
|
|
944
|
-
}
|
|
945
|
-
/**
|
|
946
|
-
* Creates a DataView for byte-level manipulation.
|
|
947
|
-
* @param arr - source typed array
|
|
948
|
-
* @returns DataView over the same buffer region.
|
|
949
|
-
* @example
|
|
950
|
-
* Create a DataView over an existing buffer.
|
|
951
|
-
* ```ts
|
|
952
|
-
* createView(new Uint8Array(4));
|
|
953
|
-
* ```
|
|
954
|
-
*/
|
|
955
|
-
function createView(arr) {
|
|
956
|
-
return new DataView(arr.buffer, arr.byteOffset, arr.byteLength);
|
|
957
|
-
}
|
|
958
|
-
/**
|
|
959
|
-
* Rotate-right operation for uint32 values.
|
|
960
|
-
* @param word - source word
|
|
961
|
-
* @param shift - shift amount in bits
|
|
962
|
-
* @returns Rotated word.
|
|
963
|
-
* @example
|
|
964
|
-
* Rotate a 32-bit word to the right.
|
|
965
|
-
* ```ts
|
|
966
|
-
* rotr(0x12345678, 8);
|
|
967
|
-
* ```
|
|
968
|
-
*/
|
|
969
|
-
function rotr(word, shift) {
|
|
970
|
-
return word << 32 - shift | word >>> shift;
|
|
971
|
-
}
|
|
972
|
-
const hasHexBuiltin = /* @__PURE__ */ (() => typeof Uint8Array.from([]).toHex === "function" && typeof Uint8Array.fromHex === "function")();
|
|
973
|
-
const hexes = /* @__PURE__ */ Array.from({ length: 256 }, (_, i) => i.toString(16).padStart(2, "0"));
|
|
974
|
-
/**
|
|
975
|
-
* Convert byte array to hex string.
|
|
976
|
-
* Uses the built-in function when available and assumes it matches the tested
|
|
977
|
-
* fallback semantics.
|
|
978
|
-
* @param bytes - bytes to encode
|
|
979
|
-
* @returns Lowercase hexadecimal string.
|
|
980
|
-
* @throws On wrong argument types. {@link TypeError}
|
|
981
|
-
* @example
|
|
982
|
-
* Convert bytes to lowercase hexadecimal.
|
|
983
|
-
* ```ts
|
|
984
|
-
* bytesToHex(Uint8Array.from([0xca, 0xfe, 0x01, 0x23])); // 'cafe0123'
|
|
985
|
-
* ```
|
|
986
|
-
*/
|
|
987
|
-
function bytesToHex(bytes) {
|
|
988
|
-
abytes(bytes);
|
|
989
|
-
if (hasHexBuiltin) return bytes.toHex();
|
|
990
|
-
let hex = "";
|
|
991
|
-
for (let i = 0; i < bytes.length; i++) hex += hexes[bytes[i]];
|
|
992
|
-
return hex;
|
|
993
|
-
}
|
|
994
|
-
/**
|
|
995
|
-
* Converts string to bytes using UTF8 encoding.
|
|
996
|
-
* Built-in doesn't validate input to be string: we do the check.
|
|
997
|
-
* Non-ASCII details are delegated to the platform `TextEncoder`.
|
|
998
|
-
* @param str - string to encode
|
|
999
|
-
* @returns UTF-8 encoded bytes.
|
|
1000
|
-
* @throws On wrong argument types. {@link TypeError}
|
|
1001
|
-
* @example
|
|
1002
|
-
* Encode a string as UTF-8 bytes.
|
|
1003
|
-
* ```ts
|
|
1004
|
-
* utf8ToBytes('abc'); // Uint8Array.from([97, 98, 99])
|
|
1005
|
-
* ```
|
|
1006
|
-
*/
|
|
1007
|
-
function utf8ToBytes(str) {
|
|
1008
|
-
if (typeof str !== "string") throw new TypeError("string expected");
|
|
1009
|
-
const encoded = new TextEncoder().encode(str);
|
|
1010
|
-
try {
|
|
1011
|
-
return new Uint8Array(encoded);
|
|
1012
|
-
} finally {
|
|
1013
|
-
clean(encoded);
|
|
1205
|
+
const hex$1 = (v) => numberToHex(v);
|
|
1206
|
+
var EvmChainAdapter = class {
|
|
1207
|
+
family = "evm";
|
|
1208
|
+
nonces = /* @__PURE__ */ new Map();
|
|
1209
|
+
lastNonce = /* @__PURE__ */ new Map();
|
|
1210
|
+
async serial(key, run) {
|
|
1211
|
+
const prior = this.nonces.get(key) ?? Promise.resolve();
|
|
1212
|
+
const next = (async () => {
|
|
1213
|
+
try {
|
|
1214
|
+
await prior;
|
|
1215
|
+
} catch {}
|
|
1216
|
+
return run();
|
|
1217
|
+
})();
|
|
1218
|
+
this.nonces.set(key, next);
|
|
1219
|
+
return next;
|
|
1014
1220
|
}
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1221
|
+
async rpc(client, method, params) {
|
|
1222
|
+
return client.transport.request({
|
|
1223
|
+
method,
|
|
1224
|
+
params
|
|
1225
|
+
});
|
|
1226
|
+
}
|
|
1227
|
+
async rpcHex(client, method, params) {
|
|
1228
|
+
const result = await this.rpc(client, method, params);
|
|
1229
|
+
if (typeof result !== "string") throw new TypeError(`${method} returned a malformed result`);
|
|
1230
|
+
return result;
|
|
1231
|
+
}
|
|
1232
|
+
/** Allocate the next nonce for (network, from) — serial per key. */
|
|
1233
|
+
async nonce(client, network, from) {
|
|
1234
|
+
const key = `${network}:${from}`;
|
|
1235
|
+
return this.serial(key, async () => {
|
|
1236
|
+
const pending = Number(await this.rpcHex(client, "eth_getTransactionCount", [from, "pending"]));
|
|
1237
|
+
const local = this.lastNonce.get(key);
|
|
1238
|
+
const nonce = Math.max(pending, local === void 0 ? 0 : local + 1);
|
|
1239
|
+
this.lastNonce.set(key, nonce);
|
|
1240
|
+
return nonce;
|
|
1241
|
+
});
|
|
1242
|
+
}
|
|
1243
|
+
/** ERC-20 gas is buffered ×1.1 instead of the native ×1.2 (chains.mdx "EVM"). */
|
|
1244
|
+
async build(client, network, from, intent) {
|
|
1245
|
+
const nonce = await this.nonce(client, network, from);
|
|
1246
|
+
const erc20 = intent.kind === "erc20";
|
|
1247
|
+
const to = checksumEvmAddress(erc20 ? intent.token : intent.to);
|
|
1248
|
+
const value = erc20 ? 0n : intent.value;
|
|
1249
|
+
const data = erc20 ? encodeFunctionData({
|
|
1250
|
+
abi: ERC20_ABI,
|
|
1251
|
+
functionName: "transfer",
|
|
1252
|
+
args: [checkedAddress(intent.to), intent.value]
|
|
1253
|
+
}) : "0x";
|
|
1254
|
+
const estimate = BigInt(await this.rpcHex(client, "eth_estimateGas", [{
|
|
1255
|
+
from,
|
|
1256
|
+
to,
|
|
1257
|
+
value: hex$1(value),
|
|
1258
|
+
data
|
|
1259
|
+
}]));
|
|
1260
|
+
const scale = erc20 ? 11n : 12n;
|
|
1261
|
+
return {
|
|
1262
|
+
family: "evm",
|
|
1263
|
+
network,
|
|
1264
|
+
from,
|
|
1265
|
+
to,
|
|
1266
|
+
value,
|
|
1267
|
+
data,
|
|
1268
|
+
nonce,
|
|
1269
|
+
gas: estimate * scale / 10n + (estimate * scale % 10n === 0n ? 0n : 1n)
|
|
1270
|
+
};
|
|
1271
|
+
}
|
|
1272
|
+
async estimateFee(client) {
|
|
1273
|
+
try {
|
|
1274
|
+
const history = await this.rpc(client, "eth_feeHistory", [
|
|
1275
|
+
"0x14",
|
|
1276
|
+
"latest",
|
|
1277
|
+
[
|
|
1278
|
+
10,
|
|
1279
|
+
50,
|
|
1280
|
+
90
|
|
1281
|
+
]
|
|
1282
|
+
]);
|
|
1283
|
+
if (typeof history !== "object" || history === null || !("baseFeePerGas" in history) || !Array.isArray(history.baseFeePerGas) || !("reward" in history) || !Array.isArray(history.reward)) throw new TypeError("eth_feeHistory returned a malformed result");
|
|
1284
|
+
const { baseFeePerGas, reward } = history;
|
|
1285
|
+
if (!Array.isArray(baseFeePerGas) || !baseFeePerGas.every((v) => typeof v === "string") || !Array.isArray(reward) || !reward.every((r) => Array.isArray(r) && r.every((v) => typeof v === "string"))) throw new TypeError("eth_feeHistory returned a malformed result");
|
|
1286
|
+
const baseFee = BigInt(baseFeePerGas.at(-1) ?? "0x0");
|
|
1287
|
+
const tier = (i, name) => {
|
|
1288
|
+
const last = reward.at(-1);
|
|
1289
|
+
const priority = last === void 0 ? 0n : BigInt(last[i] ?? "0x0");
|
|
1290
|
+
const maxFeePerGas = baseFee * 2n + priority;
|
|
1291
|
+
return {
|
|
1292
|
+
tier: name,
|
|
1293
|
+
wei: maxFeePerGas,
|
|
1294
|
+
maxFeePerGas,
|
|
1295
|
+
maxPriorityFeePerGas: priority
|
|
1296
|
+
};
|
|
1297
|
+
};
|
|
1298
|
+
return [
|
|
1299
|
+
tier(0, "slow"),
|
|
1300
|
+
tier(1, "normal"),
|
|
1301
|
+
tier(2, "fast")
|
|
1302
|
+
];
|
|
1303
|
+
} catch {
|
|
1304
|
+
const price = BigInt(await this.rpcHex(client, "eth_gasPrice", []));
|
|
1305
|
+
return [
|
|
1306
|
+
"slow",
|
|
1307
|
+
"normal",
|
|
1308
|
+
"fast"
|
|
1309
|
+
].map((name) => ({
|
|
1310
|
+
tier: name,
|
|
1311
|
+
wei: price,
|
|
1312
|
+
maxFeePerGas: price,
|
|
1313
|
+
maxPriorityFeePerGas: void 0
|
|
1314
|
+
}));
|
|
1315
|
+
}
|
|
1316
|
+
}
|
|
1317
|
+
applyFee(tx, fee) {
|
|
1318
|
+
if (fee.maxPriorityFeePerGas === void 0) return {
|
|
1319
|
+
...tx,
|
|
1320
|
+
gasPrice: fee.maxFeePerGas
|
|
1321
|
+
};
|
|
1322
|
+
return {
|
|
1323
|
+
...tx,
|
|
1324
|
+
maxFeePerGas: fee.maxFeePerGas,
|
|
1325
|
+
maxPriorityFeePerGas: fee.maxPriorityFeePerGas
|
|
1326
|
+
};
|
|
1327
|
+
}
|
|
1328
|
+
toSignRequest(tx, account, chainId) {
|
|
1329
|
+
const base = {
|
|
1330
|
+
chainId,
|
|
1331
|
+
nonce: tx.nonce,
|
|
1332
|
+
gas: hex$1(tx.gas),
|
|
1333
|
+
to: tx.to,
|
|
1334
|
+
value: hex$1(tx.value),
|
|
1335
|
+
data: tx.data
|
|
1336
|
+
};
|
|
1337
|
+
const transaction = tx.gasPrice === void 0 ? {
|
|
1338
|
+
...base,
|
|
1339
|
+
type: "eip1559",
|
|
1340
|
+
accessList: [],
|
|
1341
|
+
maxFeePerGas: hex$1(tx.maxFeePerGas ?? 0n),
|
|
1342
|
+
maxPriorityFeePerGas: hex$1(tx.maxPriorityFeePerGas ?? 0n)
|
|
1343
|
+
} : {
|
|
1344
|
+
...base,
|
|
1345
|
+
type: "legacy",
|
|
1346
|
+
gasPrice: hex$1(tx.gasPrice)
|
|
1347
|
+
};
|
|
1348
|
+
return {
|
|
1349
|
+
kind: "evm.transaction",
|
|
1350
|
+
network: tx.network,
|
|
1351
|
+
account,
|
|
1352
|
+
tx: transaction
|
|
1353
|
+
};
|
|
1354
|
+
}
|
|
1355
|
+
finalize(_tx, signed) {
|
|
1356
|
+
const bytes = signed.signedTransaction;
|
|
1357
|
+
if (signed.kind !== "evm.transaction" || bytes === void 0) throw new Error(`unexpected sign result ${signed.kind}`);
|
|
1358
|
+
return `0x${[...bytes].map((b) => b.toString(16).padStart(2, "0")).join("")}`;
|
|
1359
|
+
}
|
|
1360
|
+
async broadcast(client, raw) {
|
|
1361
|
+
return this.rpcHex(client, "eth_sendRawTransaction", [raw]);
|
|
1362
|
+
}
|
|
1363
|
+
async status(client, hash) {
|
|
1364
|
+
const receipt = await this.rpc(client, "eth_getTransactionReceipt", [hash]);
|
|
1365
|
+
if (receipt === null || typeof receipt !== "object") return "pending";
|
|
1366
|
+
if ("status" in receipt && typeof receipt.status === "string" && BigInt(receipt.status) === 0n) return "failed";
|
|
1367
|
+
try {
|
|
1368
|
+
const finalized = await this.rpc(client, "eth_getBlockByNumber", ["finalized", false]);
|
|
1369
|
+
if (finalized !== null && typeof finalized === "object" && "number" in finalized && typeof finalized.number === "string" && "blockNumber" in receipt && typeof receipt.blockNumber === "string" && BigInt(receipt.blockNumber) <= BigInt(finalized.number)) return "finalized";
|
|
1370
|
+
} catch {}
|
|
1371
|
+
return "confirmed";
|
|
1372
|
+
}
|
|
1373
|
+
};
|
|
1089
1374
|
//#endregion
|
|
1090
|
-
//#region
|
|
1091
|
-
/**
|
|
1092
|
-
* Internal Merkle-Damgard hash utils.
|
|
1093
|
-
* @module
|
|
1094
|
-
*/
|
|
1095
|
-
/**
|
|
1096
|
-
* Shared 32-bit conditional boolean primitive reused by SHA-256, SHA-1, and MD5 `F`.
|
|
1097
|
-
* Returns bits from `b` when `a` is set, otherwise from `c`.
|
|
1098
|
-
* The XOR form is equivalent to MD5's `F(X,Y,Z) = XY v not(X)Z` because the masked terms never
|
|
1099
|
-
* set the same bit.
|
|
1100
|
-
* @param a - selector word
|
|
1101
|
-
* @param b - word chosen when selector bit is set
|
|
1102
|
-
* @param c - word chosen when selector bit is clear
|
|
1103
|
-
* @returns Mixed 32-bit word.
|
|
1104
|
-
* @example
|
|
1105
|
-
* Combine three words with the shared 32-bit choice primitive.
|
|
1106
|
-
* ```ts
|
|
1107
|
-
* Chi(0xffffffff, 0x12345678, 0x87654321);
|
|
1108
|
-
* ```
|
|
1109
|
-
*/
|
|
1110
|
-
function Chi(a, b, c) {
|
|
1111
|
-
return a & b ^ ~a & c;
|
|
1112
|
-
}
|
|
1113
|
-
/**
|
|
1114
|
-
* Shared 32-bit majority primitive reused by SHA-256 and SHA-1.
|
|
1115
|
-
* Returns bits shared by at least two inputs.
|
|
1116
|
-
* @param a - first input word
|
|
1117
|
-
* @param b - second input word
|
|
1118
|
-
* @param c - third input word
|
|
1119
|
-
* @returns Mixed 32-bit word.
|
|
1120
|
-
* @example
|
|
1121
|
-
* Combine three words with the shared 32-bit majority primitive.
|
|
1122
|
-
* ```ts
|
|
1123
|
-
* Maj(0xffffffff, 0x12345678, 0x87654321);
|
|
1124
|
-
* ```
|
|
1125
|
-
*/
|
|
1126
|
-
function Maj(a, b, c) {
|
|
1127
|
-
return a & b ^ a & c ^ b & c;
|
|
1128
|
-
}
|
|
1375
|
+
//#region src/assets/controller.ts
|
|
1129
1376
|
/**
|
|
1130
|
-
*
|
|
1131
|
-
*
|
|
1132
|
-
*
|
|
1133
|
-
*
|
|
1134
|
-
*
|
|
1135
|
-
* @param outputLen - digest size in bytes
|
|
1136
|
-
* @param padOffset - trailing length field size in bytes
|
|
1137
|
-
* @param isLE - whether length and state words are encoded in little-endian
|
|
1138
|
-
* @example
|
|
1139
|
-
* Use a concrete subclass to get the shared Merkle-Damgard update/digest flow.
|
|
1140
|
-
* ```ts
|
|
1141
|
-
* import { _SHA1 } from '@noble/hashes/legacy.js';
|
|
1142
|
-
* const hash = new _SHA1();
|
|
1143
|
-
* hash.update(new Uint8Array([97, 98, 99]));
|
|
1144
|
-
* hash.digest();
|
|
1145
|
-
* ```
|
|
1377
|
+
* EVM balances per (account address, network). Native via eth_getBalance; token balanceOf calls go
|
|
1378
|
+
* through the network's Multicall3 (one aggregate eth_call) when the registry declares it,
|
|
1379
|
+
* otherwise one eth_call per token. Custom tokens persist in the EngineStore as unverified until a
|
|
1380
|
+
* built-in-table merge marks them otherwise. State values are display-only: transaction build
|
|
1381
|
+
* always re-reads the chain.
|
|
1146
1382
|
*/
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
if (take === blockLen) {
|
|
1176
|
-
const dataView = createView(data);
|
|
1177
|
-
for (; blockLen <= len - pos; pos += blockLen) this.process(dataView, pos);
|
|
1178
|
-
processed = true;
|
|
1179
|
-
continue;
|
|
1383
|
+
const obj$2 = (v) => isObject$9(v) ? v : void 0;
|
|
1384
|
+
const arr$1 = (v) => Array.isArray(v);
|
|
1385
|
+
const COLLECTION$1 = "assets-custom-tokens";
|
|
1386
|
+
const POLL_MS = 6e4;
|
|
1387
|
+
const isObject$9 = (v) => typeof v === "object" && v !== null;
|
|
1388
|
+
const checked = (v) => {
|
|
1389
|
+
if (!isAddress(v)) throw new TypeError(`malformed evm address: ${v}`);
|
|
1390
|
+
return v;
|
|
1391
|
+
};
|
|
1392
|
+
const key$1 = (network, address) => `${network}:${address}`;
|
|
1393
|
+
const MULTICALL3_ABI = [{
|
|
1394
|
+
type: "function",
|
|
1395
|
+
name: "aggregate3",
|
|
1396
|
+
inputs: [{
|
|
1397
|
+
name: "calls",
|
|
1398
|
+
type: "tuple[]",
|
|
1399
|
+
components: [
|
|
1400
|
+
{
|
|
1401
|
+
name: "target",
|
|
1402
|
+
type: "address"
|
|
1403
|
+
},
|
|
1404
|
+
{
|
|
1405
|
+
name: "allowFailure",
|
|
1406
|
+
type: "bool"
|
|
1407
|
+
},
|
|
1408
|
+
{
|
|
1409
|
+
name: "callData",
|
|
1410
|
+
type: "bytes"
|
|
1180
1411
|
}
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1412
|
+
]
|
|
1413
|
+
}],
|
|
1414
|
+
outputs: [{
|
|
1415
|
+
name: "returnData",
|
|
1416
|
+
type: "tuple[]",
|
|
1417
|
+
components: [{
|
|
1418
|
+
name: "success",
|
|
1419
|
+
type: "bool"
|
|
1420
|
+
}, {
|
|
1421
|
+
name: "returnData",
|
|
1422
|
+
type: "bytes"
|
|
1423
|
+
}]
|
|
1424
|
+
}],
|
|
1425
|
+
stateMutability: "payable"
|
|
1426
|
+
}];
|
|
1427
|
+
var AssetsController = class extends Controller {
|
|
1428
|
+
options;
|
|
1429
|
+
engineStore;
|
|
1430
|
+
bitcoinUtxos = new BtcUtxoStore();
|
|
1431
|
+
visible = false;
|
|
1432
|
+
cancelPoll;
|
|
1433
|
+
customTokens = /* @__PURE__ */ new Map();
|
|
1434
|
+
constructor(options) {
|
|
1435
|
+
super({
|
|
1436
|
+
name: "assets",
|
|
1437
|
+
defaultState: () => ({ balances: {} })
|
|
1438
|
+
});
|
|
1439
|
+
this.options = options;
|
|
1440
|
+
}
|
|
1441
|
+
async init() {
|
|
1442
|
+
this.engineStore = await this.options.openStore();
|
|
1443
|
+
const listed = await this.engineStore.collection(COLLECTION$1).get("customTokens");
|
|
1444
|
+
if (listed !== void 0) {
|
|
1445
|
+
const parsed = JSON.parse(new TextDecoder().decode(listed));
|
|
1446
|
+
if (isObject$9(parsed)) {
|
|
1447
|
+
for (const [network, arr] of Object.entries(parsed)) if (Array.isArray(arr)) {
|
|
1448
|
+
const tokens = arr.flatMap((t) => isObject$9(t) && typeof t["address"] === "string" && typeof t["verified"] === "boolean" ? [{
|
|
1449
|
+
address: t["address"],
|
|
1450
|
+
verified: t["verified"]
|
|
1451
|
+
}] : []);
|
|
1452
|
+
this.customTokens.set(network, tokens);
|
|
1453
|
+
}
|
|
1188
1454
|
}
|
|
1189
1455
|
}
|
|
1190
|
-
this.
|
|
1191
|
-
if (processed) this.roundClean();
|
|
1192
|
-
return this;
|
|
1193
|
-
}
|
|
1194
|
-
digestInto(out) {
|
|
1195
|
-
aexists(this);
|
|
1196
|
-
aoutput(out, this);
|
|
1197
|
-
this.finished = true;
|
|
1198
|
-
const { buffer, view, blockLen, isLE } = this;
|
|
1199
|
-
let { pos } = this;
|
|
1200
|
-
buffer[pos++] = 128;
|
|
1201
|
-
buffer.fill(0, pos);
|
|
1202
|
-
if (this.padOffset > blockLen - pos) {
|
|
1203
|
-
this.process(view, 0);
|
|
1204
|
-
buffer.fill(0);
|
|
1205
|
-
}
|
|
1206
|
-
setU64FromNum(view, blockLen - 8, this.length * 8, isLE);
|
|
1207
|
-
this.process(view, 0);
|
|
1208
|
-
this.roundClean();
|
|
1209
|
-
const oview = out === buffer ? view : createView(out);
|
|
1210
|
-
const len = this.outputLen;
|
|
1211
|
-
const outLen = len / 4;
|
|
1212
|
-
const state = this.get();
|
|
1213
|
-
if (len % 4 || outLen > state.length) throw new Error("invalid outputLen");
|
|
1214
|
-
for (let i = 0; i < outLen; i++) oview.setUint32(4 * i, state[i], isLE);
|
|
1215
|
-
}
|
|
1216
|
-
digest() {
|
|
1217
|
-
const { buffer, outputLen } = this;
|
|
1218
|
-
this.digestInto(buffer);
|
|
1219
|
-
const res = buffer.slice(0, outputLen);
|
|
1220
|
-
this.destroy();
|
|
1221
|
-
return res;
|
|
1222
|
-
}
|
|
1223
|
-
_cloneIntoMeta(to) {
|
|
1224
|
-
const { buffer, length, finished, destroyed, pos } = this;
|
|
1225
|
-
to.destroyed = destroyed;
|
|
1226
|
-
to.finished = finished;
|
|
1227
|
-
to.length = length;
|
|
1228
|
-
to.pos = pos;
|
|
1229
|
-
if (pos) to.buffer.set(buffer);
|
|
1230
|
-
return to;
|
|
1231
|
-
}
|
|
1232
|
-
clone() {
|
|
1233
|
-
return this._cloneInto();
|
|
1456
|
+
if (this.visible) this.armPoll();
|
|
1234
1457
|
}
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
* square roots of the first eight prime numbers. Exported as a shared table; callers must treat
|
|
1242
|
-
* it as read-only because constructors copy words from it by index. */
|
|
1243
|
-
const SHA256_IV = /* @__PURE__ */ Uint32Array.from([
|
|
1244
|
-
1779033703,
|
|
1245
|
-
3144134277,
|
|
1246
|
-
1013904242,
|
|
1247
|
-
2773480762,
|
|
1248
|
-
1359893119,
|
|
1249
|
-
2600822924,
|
|
1250
|
-
528734635,
|
|
1251
|
-
1541459225
|
|
1252
|
-
]);
|
|
1253
|
-
//#endregion
|
|
1254
|
-
//#region ../../node_modules/.bun/@noble+hashes@2.4.0/node_modules/@noble/hashes/sha2.js
|
|
1255
|
-
/**
|
|
1256
|
-
* SHA2 hash function. A.k.a. sha256, sha384, sha512, sha512_224, sha512_256.
|
|
1257
|
-
* SHA256 is the fastest hash implementable in JS, even faster than Blake3.
|
|
1258
|
-
* Check out {@link https://www.rfc-editor.org/rfc/rfc4634 | RFC 4634} and
|
|
1259
|
-
* {@link https://nvlpubs.nist.gov/nistpubs/FIPS/NIST.FIPS.180-4.pdf | FIPS 180-4}.
|
|
1260
|
-
* @module
|
|
1261
|
-
*/
|
|
1262
|
-
/**
|
|
1263
|
-
* SHA-224 / SHA-256 round constants from RFC 6234 §5.1: the first 32 bits
|
|
1264
|
-
* of the cube roots of the first 64 primes (2..311).
|
|
1265
|
-
*/
|
|
1266
|
-
const SHA256_K = /* @__PURE__ */ Uint32Array.from([
|
|
1267
|
-
1116352408,
|
|
1268
|
-
1899447441,
|
|
1269
|
-
3049323471,
|
|
1270
|
-
3921009573,
|
|
1271
|
-
961987163,
|
|
1272
|
-
1508970993,
|
|
1273
|
-
2453635748,
|
|
1274
|
-
2870763221,
|
|
1275
|
-
3624381080,
|
|
1276
|
-
310598401,
|
|
1277
|
-
607225278,
|
|
1278
|
-
1426881987,
|
|
1279
|
-
1925078388,
|
|
1280
|
-
2162078206,
|
|
1281
|
-
2614888103,
|
|
1282
|
-
3248222580,
|
|
1283
|
-
3835390401,
|
|
1284
|
-
4022224774,
|
|
1285
|
-
264347078,
|
|
1286
|
-
604807628,
|
|
1287
|
-
770255983,
|
|
1288
|
-
1249150122,
|
|
1289
|
-
1555081692,
|
|
1290
|
-
1996064986,
|
|
1291
|
-
2554220882,
|
|
1292
|
-
2821834349,
|
|
1293
|
-
2952996808,
|
|
1294
|
-
3210313671,
|
|
1295
|
-
3336571891,
|
|
1296
|
-
3584528711,
|
|
1297
|
-
113926993,
|
|
1298
|
-
338241895,
|
|
1299
|
-
666307205,
|
|
1300
|
-
773529912,
|
|
1301
|
-
1294757372,
|
|
1302
|
-
1396182291,
|
|
1303
|
-
1695183700,
|
|
1304
|
-
1986661051,
|
|
1305
|
-
2177026350,
|
|
1306
|
-
2456956037,
|
|
1307
|
-
2730485921,
|
|
1308
|
-
2820302411,
|
|
1309
|
-
3259730800,
|
|
1310
|
-
3345764771,
|
|
1311
|
-
3516065817,
|
|
1312
|
-
3600352804,
|
|
1313
|
-
4094571909,
|
|
1314
|
-
275423344,
|
|
1315
|
-
430227734,
|
|
1316
|
-
506948616,
|
|
1317
|
-
659060556,
|
|
1318
|
-
883997877,
|
|
1319
|
-
958139571,
|
|
1320
|
-
1322822218,
|
|
1321
|
-
1537002063,
|
|
1322
|
-
1747873779,
|
|
1323
|
-
1955562222,
|
|
1324
|
-
2024104815,
|
|
1325
|
-
2227730452,
|
|
1326
|
-
2361852424,
|
|
1327
|
-
2428436474,
|
|
1328
|
-
2756734187,
|
|
1329
|
-
3204031479,
|
|
1330
|
-
3329325298
|
|
1331
|
-
]);
|
|
1332
|
-
/** Reusable SHA-224 / SHA-256 message schedule buffer `W_t` from RFC 6234 §6.2 step 1. */
|
|
1333
|
-
const SHA256_W = /* @__PURE__ */ new Uint32Array(64);
|
|
1334
|
-
/** Internal SHA-224 / SHA-256 compression engine from RFC 6234 §6.2. */
|
|
1335
|
-
var SHA2_32B = class extends HashMD {
|
|
1336
|
-
A = 0;
|
|
1337
|
-
B = 0;
|
|
1338
|
-
C = 0;
|
|
1339
|
-
D = 0;
|
|
1340
|
-
E = 0;
|
|
1341
|
-
F = 0;
|
|
1342
|
-
G = 0;
|
|
1343
|
-
H = 0;
|
|
1344
|
-
constructor(outputLen, IV) {
|
|
1345
|
-
super(64, outputLen, 8, false);
|
|
1346
|
-
this.A = IV[0] | 0;
|
|
1347
|
-
this.B = IV[1] | 0;
|
|
1348
|
-
this.C = IV[2] | 0;
|
|
1349
|
-
this.D = IV[3] | 0;
|
|
1350
|
-
this.E = IV[4] | 0;
|
|
1351
|
-
this.F = IV[5] | 0;
|
|
1352
|
-
this.G = IV[6] | 0;
|
|
1353
|
-
this.H = IV[7] | 0;
|
|
1354
|
-
}
|
|
1355
|
-
get() {
|
|
1356
|
-
const { A, B, C, D, E, F, G, H } = this;
|
|
1357
|
-
return [
|
|
1358
|
-
A,
|
|
1359
|
-
B,
|
|
1360
|
-
C,
|
|
1361
|
-
D,
|
|
1362
|
-
E,
|
|
1363
|
-
F,
|
|
1364
|
-
G,
|
|
1365
|
-
H
|
|
1366
|
-
];
|
|
1458
|
+
lock() {
|
|
1459
|
+
this.cancelPoll?.();
|
|
1460
|
+
this.cancelPoll = void 0;
|
|
1461
|
+
this.customTokens.clear();
|
|
1462
|
+
this.engineStore = void 0;
|
|
1463
|
+
this.update(() => ({ balances: {} }));
|
|
1367
1464
|
}
|
|
1368
|
-
|
|
1369
|
-
this.
|
|
1370
|
-
|
|
1371
|
-
this.C = C | 0;
|
|
1372
|
-
this.D = D | 0;
|
|
1373
|
-
this.E = E | 0;
|
|
1374
|
-
this.F = F | 0;
|
|
1375
|
-
this.G = G | 0;
|
|
1376
|
-
this.H = H | 0;
|
|
1377
|
-
}
|
|
1378
|
-
_cloneInto(to) {
|
|
1379
|
-
(to ||= new this.constructor()).set(...this.get());
|
|
1380
|
-
return this._cloneIntoMeta(to);
|
|
1381
|
-
}
|
|
1382
|
-
process(view, offset) {
|
|
1383
|
-
for (let i = 0; i < 16; i++, offset += 4) SHA256_W[i] = view.getUint32(offset, false);
|
|
1384
|
-
for (let i = 16; i < 64; i++) {
|
|
1385
|
-
const W15 = SHA256_W[i - 15];
|
|
1386
|
-
const W2 = SHA256_W[i - 2];
|
|
1387
|
-
const s0 = rotr(W15, 7) ^ rotr(W15, 18) ^ W15 >>> 3;
|
|
1388
|
-
const s1 = rotr(W2, 17) ^ rotr(W2, 19) ^ W2 >>> 10;
|
|
1389
|
-
SHA256_W[i] = s1 + SHA256_W[i - 7] + s0 + SHA256_W[i - 16] | 0;
|
|
1390
|
-
}
|
|
1391
|
-
let { A, B, C, D, E, F, G, H } = this;
|
|
1392
|
-
for (let i = 0; i < 64; i++) {
|
|
1393
|
-
const sigma1 = rotr(E, 6) ^ rotr(E, 11) ^ rotr(E, 25);
|
|
1394
|
-
const T1 = H + sigma1 + Chi(E, F, G) + SHA256_K[i] + SHA256_W[i] | 0;
|
|
1395
|
-
const T2 = (rotr(A, 2) ^ rotr(A, 13) ^ rotr(A, 22)) + Maj(A, B, C) | 0;
|
|
1396
|
-
H = G;
|
|
1397
|
-
G = F;
|
|
1398
|
-
F = E;
|
|
1399
|
-
E = D + T1 | 0;
|
|
1400
|
-
D = C;
|
|
1401
|
-
C = B;
|
|
1402
|
-
B = A;
|
|
1403
|
-
A = T1 + T2 | 0;
|
|
1404
|
-
}
|
|
1405
|
-
A = A + this.A | 0;
|
|
1406
|
-
B = B + this.B | 0;
|
|
1407
|
-
C = C + this.C | 0;
|
|
1408
|
-
D = D + this.D | 0;
|
|
1409
|
-
E = E + this.E | 0;
|
|
1410
|
-
F = F + this.F | 0;
|
|
1411
|
-
G = G + this.G | 0;
|
|
1412
|
-
H = H + this.H | 0;
|
|
1413
|
-
this.set(A, B, C, D, E, F, G, H);
|
|
1414
|
-
}
|
|
1415
|
-
roundClean() {
|
|
1416
|
-
clean(SHA256_W);
|
|
1417
|
-
}
|
|
1418
|
-
destroy() {
|
|
1419
|
-
this.destroyed = true;
|
|
1420
|
-
this.set(0, 0, 0, 0, 0, 0, 0, 0);
|
|
1421
|
-
clean(this.buffer);
|
|
1465
|
+
async destroy() {
|
|
1466
|
+
this.lock();
|
|
1467
|
+
await Promise.resolve();
|
|
1422
1468
|
}
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1469
|
+
setVisible(visible) {
|
|
1470
|
+
this.visible = visible;
|
|
1471
|
+
if (!visible) {
|
|
1472
|
+
this.cancelPoll?.();
|
|
1473
|
+
this.cancelPoll = void 0;
|
|
1474
|
+
return;
|
|
1475
|
+
}
|
|
1476
|
+
this.armPoll();
|
|
1477
|
+
}
|
|
1478
|
+
armPoll() {
|
|
1479
|
+
if (this.cancelPoll !== void 0) return;
|
|
1480
|
+
this.cancelPoll = this.options.setTimer(POLL_MS, () => {
|
|
1481
|
+
(async () => {
|
|
1482
|
+
try {
|
|
1483
|
+
await this.refreshAll();
|
|
1484
|
+
} catch (error) {
|
|
1485
|
+
this.options.logger.warn("assets refresh failed", error);
|
|
1486
|
+
}
|
|
1487
|
+
})();
|
|
1488
|
+
});
|
|
1489
|
+
}
|
|
1490
|
+
async refreshAll() {
|
|
1491
|
+
await Promise.all(Object.keys(this.getState().balances).flatMap((entry) => {
|
|
1492
|
+
const [network, address] = entry.split(":");
|
|
1493
|
+
return network === void 0 || address === void 0 ? [] : [this.refresh(address, network)];
|
|
1494
|
+
}));
|
|
1495
|
+
}
|
|
1496
|
+
async refresh(address, network) {
|
|
1497
|
+
const info = this.options.networks.info(network);
|
|
1498
|
+
if (info.family === "solana") {
|
|
1499
|
+
await this.refreshSolana(address, network);
|
|
1500
|
+
return;
|
|
1501
|
+
}
|
|
1502
|
+
if (info.family === "bitcoin") {
|
|
1503
|
+
await this.refreshBitcoin(address, network);
|
|
1504
|
+
return;
|
|
1505
|
+
}
|
|
1506
|
+
const client = this.options.networks.client(network);
|
|
1507
|
+
const native = BigInt(await this.rpcHex(client, "eth_getBalance", [address, "latest"])).toString();
|
|
1508
|
+
const custom = this.customTokens.get(network) ?? [];
|
|
1509
|
+
const tokens = await this.readTokens(client, info.multicall3, address, custom.map((t) => ({
|
|
1510
|
+
address: t.address,
|
|
1511
|
+
verified: t.verified
|
|
1512
|
+
})));
|
|
1513
|
+
this.update((s) => ({ balances: {
|
|
1514
|
+
...s.balances,
|
|
1515
|
+
[key$1(network, address)]: {
|
|
1516
|
+
native,
|
|
1517
|
+
tokens
|
|
1518
|
+
}
|
|
1519
|
+
} }));
|
|
1520
|
+
}
|
|
1521
|
+
/** SOL via getBalance; SPL via getTokenAccountsByOwner (jsonParsed) for known mints. */
|
|
1522
|
+
async refreshSolana(owner, network) {
|
|
1523
|
+
const rpc = this.options.networks.solanaRpc(network);
|
|
1524
|
+
const balValue = obj$2(await rpc.call("getBalance", [owner, { commitment: "confirmed" }]))?.["value"];
|
|
1525
|
+
const native = typeof balValue === "string" ? balValue : typeof balValue === "number" ? String(balValue) : "0";
|
|
1526
|
+
const custom = this.customTokens.get(network) ?? [];
|
|
1527
|
+
const tokens = await Promise.all(custom.map(async (t) => {
|
|
1528
|
+
const accounts = obj$2(await rpc.call("getTokenAccountsByOwner", [
|
|
1529
|
+
owner,
|
|
1530
|
+
{ mint: t.address },
|
|
1531
|
+
{ encoding: "jsonParsed" }
|
|
1532
|
+
]));
|
|
1533
|
+
const first = arr$1(accounts?.["value"]) ? accounts?.["value"][0] : void 0;
|
|
1534
|
+
const parsed = obj$2(obj$2(obj$2(obj$2(obj$2(first)?.["account"])?.["data"])?.["parsed"])?.["info"]);
|
|
1535
|
+
const amount = obj$2(parsed?.["tokenAmount"]);
|
|
1536
|
+
const mintInfo = obj$2(await rpc.call("getAccountInfo", [t.address, { encoding: "jsonParsed" }]));
|
|
1537
|
+
const mintParsed = obj$2(obj$2(obj$2(obj$2(mintInfo?.["value"])?.["data"])?.["parsed"])?.["info"]);
|
|
1538
|
+
return {
|
|
1539
|
+
address: t.address,
|
|
1540
|
+
symbol: "?",
|
|
1541
|
+
decimals: typeof mintParsed?.["decimals"] === "number" ? mintParsed["decimals"] : 9,
|
|
1542
|
+
balance: typeof amount?.["amount"] === "string" ? amount["amount"] : "0",
|
|
1543
|
+
verified: t.verified
|
|
1544
|
+
};
|
|
1545
|
+
}));
|
|
1546
|
+
this.update((s) => ({ balances: {
|
|
1547
|
+
...s.balances,
|
|
1548
|
+
[key$1(network, owner)]: {
|
|
1549
|
+
native,
|
|
1550
|
+
tokens
|
|
1551
|
+
}
|
|
1552
|
+
} }));
|
|
1553
|
+
}
|
|
1554
|
+
async readTokens(client, multicall3, owner, tokens) {
|
|
1555
|
+
if (tokens.length === 0) return [];
|
|
1556
|
+
const balances = multicall3 === void 0 ? await Promise.all(tokens.map(async (t) => this.balanceOf(client, t.address, owner))) : await this.balanceOfBatch(client, multicall3, owner, tokens.map((t) => t.address));
|
|
1557
|
+
return Promise.all(tokens.map(async (t, i) => ({
|
|
1558
|
+
address: t.address,
|
|
1559
|
+
symbol: await this.callString(client, t.address, "symbol") ?? "?",
|
|
1560
|
+
decimals: await this.callNumber(client, t.address, "decimals") ?? 18,
|
|
1561
|
+
balance: balances[i]?.toString() ?? "0",
|
|
1562
|
+
verified: t.verified
|
|
1563
|
+
})));
|
|
1564
|
+
}
|
|
1565
|
+
async balanceOfBatch(client, multicall3, owner, tokens) {
|
|
1566
|
+
const data = encodeFunctionData({
|
|
1567
|
+
abi: MULTICALL3_ABI,
|
|
1568
|
+
functionName: "aggregate3",
|
|
1569
|
+
args: [tokens.map((token) => ({
|
|
1570
|
+
target: checked(token),
|
|
1571
|
+
allowFailure: true,
|
|
1572
|
+
callData: encodeFunctionData({
|
|
1573
|
+
abi: ERC20_ABI,
|
|
1574
|
+
functionName: "balanceOf",
|
|
1575
|
+
args: [checked(owner)]
|
|
1576
|
+
})
|
|
1577
|
+
}))]
|
|
1578
|
+
});
|
|
1579
|
+
const result = await this.rpcHex(client, "eth_call", [{
|
|
1580
|
+
to: multicall3,
|
|
1581
|
+
data
|
|
1582
|
+
}, "latest"]);
|
|
1583
|
+
if (!isHex(result)) throw new TypeError("eth_call returned malformed data");
|
|
1584
|
+
return decodeFunctionResult({
|
|
1585
|
+
abi: MULTICALL3_ABI,
|
|
1586
|
+
functionName: "aggregate3",
|
|
1587
|
+
data: result
|
|
1588
|
+
}).map((r) => r.success ? decodeFunctionResult({
|
|
1589
|
+
abi: ERC20_ABI,
|
|
1590
|
+
functionName: "balanceOf",
|
|
1591
|
+
data: r.returnData
|
|
1592
|
+
}) : void 0);
|
|
1593
|
+
}
|
|
1594
|
+
async balanceOf(client, token, owner) {
|
|
1595
|
+
const data = encodeFunctionData({
|
|
1596
|
+
abi: ERC20_ABI,
|
|
1597
|
+
functionName: "balanceOf",
|
|
1598
|
+
args: [checked(owner)]
|
|
1599
|
+
});
|
|
1600
|
+
const result = await this.rpcHex(client, "eth_call", [{
|
|
1601
|
+
to: token,
|
|
1602
|
+
data
|
|
1603
|
+
}, "latest"]);
|
|
1604
|
+
if (!isHex(result)) throw new TypeError("eth_call returned malformed data");
|
|
1605
|
+
return decodeFunctionResult({
|
|
1606
|
+
abi: ERC20_ABI,
|
|
1607
|
+
functionName: "balanceOf",
|
|
1608
|
+
data: result
|
|
1609
|
+
});
|
|
1610
|
+
}
|
|
1611
|
+
async callString(client, token, fn) {
|
|
1612
|
+
const data = encodeFunctionData({
|
|
1613
|
+
abi: ERC20_ABI,
|
|
1614
|
+
functionName: fn
|
|
1615
|
+
});
|
|
1616
|
+
const result = await this.rpcHex(client, "eth_call", [{
|
|
1617
|
+
to: token,
|
|
1618
|
+
data
|
|
1619
|
+
}, "latest"]);
|
|
1620
|
+
if (!isHex(result)) return;
|
|
1621
|
+
try {
|
|
1622
|
+
return decodeFunctionResult({
|
|
1623
|
+
abi: ERC20_ABI,
|
|
1624
|
+
functionName: fn,
|
|
1625
|
+
data: result
|
|
1626
|
+
});
|
|
1627
|
+
} catch {
|
|
1628
|
+
return;
|
|
1629
|
+
}
|
|
1630
|
+
}
|
|
1631
|
+
async callNumber(client, token, fn) {
|
|
1632
|
+
const data = encodeFunctionData({
|
|
1633
|
+
abi: ERC20_ABI,
|
|
1634
|
+
functionName: fn
|
|
1635
|
+
});
|
|
1636
|
+
const result = await this.rpcHex(client, "eth_call", [{
|
|
1637
|
+
to: token,
|
|
1638
|
+
data
|
|
1639
|
+
}, "latest"]);
|
|
1640
|
+
if (!isHex(result)) return;
|
|
1641
|
+
try {
|
|
1642
|
+
return decodeFunctionResult({
|
|
1643
|
+
abi: ERC20_ABI,
|
|
1644
|
+
functionName: fn,
|
|
1645
|
+
data: result
|
|
1646
|
+
});
|
|
1647
|
+
} catch {
|
|
1648
|
+
return;
|
|
1649
|
+
}
|
|
1650
|
+
}
|
|
1651
|
+
/** Reads symbol/decimals on-chain, marks unverified, persists; throws when the calls fail. */
|
|
1652
|
+
async addToken(network, address) {
|
|
1653
|
+
if (this.options.networks.info(network).family === "solana") {
|
|
1654
|
+
const rpc = this.options.networks.solanaRpc(network);
|
|
1655
|
+
const mint = obj$2(await rpc.call("getAccountInfo", [address, { encoding: "jsonParsed" }]));
|
|
1656
|
+
const decimals = obj$2(obj$2(obj$2(mint?.["value"])?.["data"])?.["parsed"])?.["info"];
|
|
1657
|
+
if (typeof obj$2(decimals)?.["decimals"] !== "number") throw new TypeError(`${address} is not a readable SPL mint on ${network}`);
|
|
1658
|
+
await this.persistToken(network, address);
|
|
1659
|
+
return;
|
|
1660
|
+
}
|
|
1661
|
+
const client = this.options.networks.client(network);
|
|
1662
|
+
await this.callString(client, address, "symbol");
|
|
1663
|
+
await this.callNumber(client, address, "decimals");
|
|
1664
|
+
await this.persistToken(network, address);
|
|
1665
|
+
}
|
|
1666
|
+
async persistToken(network, address) {
|
|
1667
|
+
const existing = this.customTokens.get(network) ?? [];
|
|
1668
|
+
if (existing.some((t) => t.address === address)) return;
|
|
1669
|
+
const next = [...existing, {
|
|
1670
|
+
address,
|
|
1671
|
+
verified: false
|
|
1672
|
+
}];
|
|
1673
|
+
this.customTokens.set(network, next);
|
|
1674
|
+
if (this.engineStore !== void 0) {
|
|
1675
|
+
const merged = { [network]: next };
|
|
1676
|
+
for (const [n, list] of this.customTokens) if (n !== network) merged[n] = list;
|
|
1677
|
+
await this.engineStore.collection(COLLECTION$1).put("customTokens", new TextEncoder().encode(JSON.stringify(merged)));
|
|
1678
|
+
}
|
|
1679
|
+
}
|
|
1680
|
+
async rpc(client, method, params) {
|
|
1681
|
+
return client.transport.request({
|
|
1682
|
+
method,
|
|
1683
|
+
params
|
|
1684
|
+
});
|
|
1685
|
+
}
|
|
1686
|
+
async rpcHex(client, method, params) {
|
|
1687
|
+
const result = await this.rpc(client, method, params);
|
|
1688
|
+
if (typeof result !== "string") throw new TypeError(`${method} returned a malformed result`);
|
|
1689
|
+
return result;
|
|
1690
|
+
}
|
|
1691
|
+
/** BTC balance = confirmed spendable + own unconfirmed change; frozen 546/330 shown separately. */
|
|
1692
|
+
async refreshBitcoin(xpub, network) {
|
|
1693
|
+
const es = this.options.networks.esplora(network);
|
|
1694
|
+
const net = bitcoinNetwork(network);
|
|
1695
|
+
const template = "btc.bip84";
|
|
1696
|
+
const receive = await scanChain(es, xpub, template, network, net.params, 0);
|
|
1697
|
+
const change = await scanChain(es, xpub, template, network, net.params, 1);
|
|
1698
|
+
const addresses = [...receive, ...change];
|
|
1699
|
+
const scanned = await scanUtxos(es, addresses, net.params);
|
|
1700
|
+
this.bitcoinUtxos.replace(scanned);
|
|
1701
|
+
const changeSet = new Set(change.map((a) => a.address));
|
|
1702
|
+
let confirmed = 0n;
|
|
1703
|
+
let ownUnconfirmed = 0n;
|
|
1704
|
+
let frozen = 0n;
|
|
1705
|
+
for (const u of this.bitcoinUtxos.all()) {
|
|
1706
|
+
const v = BigInt(u.value);
|
|
1707
|
+
if (u.status === "frozen" || u.value === 546 || u.value === 330) frozen += v;
|
|
1708
|
+
else if (u.height > 0) confirmed += v;
|
|
1709
|
+
else if (changeSet.has(u.address)) ownUnconfirmed += v;
|
|
1710
|
+
}
|
|
1711
|
+
this.update((s) => ({
|
|
1712
|
+
...s,
|
|
1713
|
+
balances: {
|
|
1714
|
+
...s.balances,
|
|
1715
|
+
[`${xpub}:${network}:BTC`]: {
|
|
1716
|
+
native: confirmed.toString(),
|
|
1717
|
+
tokens: [],
|
|
1718
|
+
unconfirmed: ownUnconfirmed.toString(),
|
|
1719
|
+
frozen: frozen.toString()
|
|
1720
|
+
}
|
|
1721
|
+
}
|
|
1722
|
+
}));
|
|
1428
1723
|
}
|
|
1429
1724
|
};
|
|
1430
|
-
/**
|
|
1431
|
-
* SHA2-256 hash function from RFC 4634. In JS it's the fastest: even faster than Blake3. Some info:
|
|
1432
|
-
*
|
|
1433
|
-
* - Trying 2^128 hashes would get 50% chance of collision, using birthday attack.
|
|
1434
|
-
* - BTC network is doing 2^70 hashes/sec (2^95 hashes/year) as per 2025.
|
|
1435
|
-
* - Each sha256 hash is executing 2^18 bit operations.
|
|
1436
|
-
* - Good 2024 ASICs can do 200Th/sec with 3500 watts of power, corresponding to 2^36 hashes/joule.
|
|
1437
|
-
* @param msg - message bytes to hash
|
|
1438
|
-
* @param opts - Reserved hash options.
|
|
1439
|
-
* @returns Digest bytes.
|
|
1440
|
-
* @example
|
|
1441
|
-
* Hash a message with SHA2-256.
|
|
1442
|
-
* ```ts
|
|
1443
|
-
* sha256(new Uint8Array([97, 98, 99]));
|
|
1444
|
-
* ```
|
|
1445
|
-
*/
|
|
1446
|
-
const sha256 = /* @__PURE__ */ createHasher(() => new _SHA256(), /* @__PURE__ */ oidNist(1));
|
|
1447
1725
|
//#endregion
|
|
1448
1726
|
//#region src/backup/errors.ts
|
|
1449
1727
|
var BackupControllerError = class extends Error {
|
|
@@ -1481,14 +1759,14 @@ function sorted(values) {
|
|
|
1481
1759
|
function sameSet(a, b) {
|
|
1482
1760
|
return a.length === b.length && a.every((v, i) => v === b[i]);
|
|
1483
1761
|
}
|
|
1484
|
-
const isObject$
|
|
1762
|
+
const isObject$8 = (v) => typeof v === "object" && v !== null;
|
|
1485
1763
|
const isStringArray = (v) => Array.isArray(v) && v.every((e) => typeof e === "string");
|
|
1486
1764
|
/** Strict record validation — anything malformed reads as absent (and is logged). */
|
|
1487
1765
|
function parseRecord(bytes) {
|
|
1488
1766
|
if (bytes === void 0) return;
|
|
1489
1767
|
try {
|
|
1490
1768
|
const value = JSON.parse(new TextDecoder().decode(bytes));
|
|
1491
|
-
if (!isObject$
|
|
1769
|
+
if (!isObject$8(value)) return;
|
|
1492
1770
|
const o = value;
|
|
1493
1771
|
const { id } = o;
|
|
1494
1772
|
const { digest } = o;
|
|
@@ -1817,17 +2095,17 @@ const KNOWN_GROUP_KEYS = /* @__PURE__ */ new Set([
|
|
|
1817
2095
|
"boundPubkey",
|
|
1818
2096
|
"updatedAt"
|
|
1819
2097
|
]);
|
|
1820
|
-
const isObject$
|
|
2098
|
+
const isObject$7 = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
|
|
1821
2099
|
const isNonNegativeInt = (v) => typeof v === "number" && Number.isSafeInteger(v) && v >= 0;
|
|
1822
2100
|
const validName = (v) => v === void 0 || typeof v === "string" && v.trim().length > 0 && v.trim().length <= NAME_MAX;
|
|
1823
2101
|
const trimmedName = (v) => typeof v === "string" ? v.trim() : void 0;
|
|
1824
2102
|
/** Strict decode — any malformed shape, unknown key or duplicate index returns `undefined`. */
|
|
1825
2103
|
function decodeMetaDoc(value) {
|
|
1826
|
-
if (!isObject$
|
|
2104
|
+
if (!isObject$7(value)) return;
|
|
1827
2105
|
for (const k of Object.keys(value)) if (!KNOWN_DOC_KEYS.has(k)) return;
|
|
1828
2106
|
if (value["v"] !== 1) return;
|
|
1829
2107
|
const { wallet } = value;
|
|
1830
|
-
if (!isObject$
|
|
2108
|
+
if (!isObject$7(wallet)) return;
|
|
1831
2109
|
for (const k of Object.keys(wallet)) if (!KNOWN_WALLET_KEYS.has(k)) return;
|
|
1832
2110
|
if (!validName(wallet["name"]) || !isNonNegativeInt(wallet["updatedAt"])) return;
|
|
1833
2111
|
const { groups } = value;
|
|
@@ -1835,7 +2113,7 @@ function decodeMetaDoc(value) {
|
|
|
1835
2113
|
const seen = /* @__PURE__ */ new Set();
|
|
1836
2114
|
const out = [];
|
|
1837
2115
|
for (const item of groups) {
|
|
1838
|
-
if (!isObject$
|
|
2116
|
+
if (!isObject$7(item)) return;
|
|
1839
2117
|
for (const k of Object.keys(item)) if (!KNOWN_GROUP_KEYS.has(k)) return;
|
|
1840
2118
|
const { index } = item;
|
|
1841
2119
|
if (!isNonNegativeInt(index) || seen.has(index)) return;
|
|
@@ -2137,32 +2415,512 @@ function isUnsupportedDoc(plaintext) {
|
|
|
2137
2415
|
}
|
|
2138
2416
|
}
|
|
2139
2417
|
//#endregion
|
|
2140
|
-
//#region src/
|
|
2141
|
-
|
|
2142
|
-
|
|
2143
|
-
|
|
2144
|
-
|
|
2145
|
-
|
|
2146
|
-
|
|
2147
|
-
|
|
2148
|
-
|
|
2149
|
-
|
|
2150
|
-
|
|
2151
|
-
|
|
2152
|
-
|
|
2153
|
-
|
|
2154
|
-
|
|
2155
|
-
}
|
|
2156
|
-
|
|
2157
|
-
|
|
2158
|
-
|
|
2159
|
-
|
|
2160
|
-
|
|
2161
|
-
|
|
2162
|
-
|
|
2163
|
-
|
|
2164
|
-
|
|
2165
|
-
|
|
2418
|
+
//#region src/chains/bitcoin.ts
|
|
2419
|
+
const DUST = {
|
|
2420
|
+
p2wpkh: 294,
|
|
2421
|
+
p2tr: 330,
|
|
2422
|
+
p2pkh: 546
|
|
2423
|
+
};
|
|
2424
|
+
const FROZEN_AMOUNTS = /* @__PURE__ */ new Set([546, 330]);
|
|
2425
|
+
const isObject$6 = (v) => typeof v === "object" && v !== null;
|
|
2426
|
+
var EsploraError = class extends Error {
|
|
2427
|
+
/** True for request timeout/abort, network failure, 429-after-retries and 5xx — safe to retry. */
|
|
2428
|
+
transient;
|
|
2429
|
+
constructor(message, transient) {
|
|
2430
|
+
super(message);
|
|
2431
|
+
this.name = "EsploraError";
|
|
2432
|
+
this.transient = transient;
|
|
2433
|
+
}
|
|
2434
|
+
};
|
|
2435
|
+
const MAX_IN_FLIGHT = 4;
|
|
2436
|
+
const MAX_RETRIES = 3;
|
|
2437
|
+
const MAX_BACKOFF_MS = 2e3;
|
|
2438
|
+
const REQUEST_TIMEOUT_MS = 1e4;
|
|
2439
|
+
/** Bounded-concurrency + 429-aware ordered failover over the endpoint list. */
|
|
2440
|
+
const esplora = (endpoints, fetchFn, sleep, timeoutMs = REQUEST_TIMEOUT_MS) => {
|
|
2441
|
+
let inFlight = 0;
|
|
2442
|
+
const waiters = [];
|
|
2443
|
+
const acquire = async () => {
|
|
2444
|
+
if (inFlight >= MAX_IN_FLIGHT) await new Promise((resolve) => {
|
|
2445
|
+
waiters.push(resolve);
|
|
2446
|
+
});
|
|
2447
|
+
inFlight += 1;
|
|
2448
|
+
};
|
|
2449
|
+
const release = () => {
|
|
2450
|
+
inFlight -= 1;
|
|
2451
|
+
waiters.shift()?.();
|
|
2452
|
+
};
|
|
2453
|
+
const backoff = async (headers, retry) => {
|
|
2454
|
+
const ra = headers.get("retry-after");
|
|
2455
|
+
const retryAfterMs = ra !== null && ra !== "" && !Number.isNaN(Number(ra)) ? Number(ra) * 1e3 : void 0;
|
|
2456
|
+
const jitter = Math.floor(Math.random() * 100);
|
|
2457
|
+
await sleep(retryAfterMs ?? Math.min(100 * 2 ** retry + jitter, MAX_BACKOFF_MS));
|
|
2458
|
+
};
|
|
2459
|
+
const request = async (base, path, body) => {
|
|
2460
|
+
await acquire();
|
|
2461
|
+
try {
|
|
2462
|
+
let res;
|
|
2463
|
+
try {
|
|
2464
|
+
res = await fetchFn(`${base.replace(/\/$/, "")}${path}`, {
|
|
2465
|
+
...body === void 0 ? { method: "GET" } : {
|
|
2466
|
+
method: "POST",
|
|
2467
|
+
headers: { "content-type": "text/plain" },
|
|
2468
|
+
body
|
|
2469
|
+
},
|
|
2470
|
+
signal: AbortSignal.timeout(timeoutMs)
|
|
2471
|
+
});
|
|
2472
|
+
} catch (error) {
|
|
2473
|
+
throw new EsploraError(`esplora ${base}${path}: request failed (${error instanceof Error ? error.name : "unknown"})`, true);
|
|
2474
|
+
}
|
|
2475
|
+
if (res.status === 429) return { throttled: res.headers };
|
|
2476
|
+
if (!res.ok) throw new EsploraError(`esplora ${base}${path}: ${res.status}`, res.status >= 500);
|
|
2477
|
+
const text = await res.text();
|
|
2478
|
+
try {
|
|
2479
|
+
return { value: JSON.parse(text) };
|
|
2480
|
+
} catch {
|
|
2481
|
+
return { value: text };
|
|
2482
|
+
}
|
|
2483
|
+
} finally {
|
|
2484
|
+
release();
|
|
2485
|
+
}
|
|
2486
|
+
};
|
|
2487
|
+
const call = async (base, path, body, retry) => {
|
|
2488
|
+
const outcome = await request(base, path, body);
|
|
2489
|
+
if ("value" in outcome) return outcome.value;
|
|
2490
|
+
if (retry >= MAX_RETRIES) throw new EsploraError(`esplora ${base}${path}: 429 after ${MAX_RETRIES} retries`, true);
|
|
2491
|
+
await backoff(outcome.throttled, retry);
|
|
2492
|
+
return call(base, path, body, retry + 1);
|
|
2493
|
+
};
|
|
2494
|
+
const attempt = async (rest, path, body, last) => {
|
|
2495
|
+
const [base, ...next] = rest;
|
|
2496
|
+
if (base === void 0) throw last instanceof Error ? last : /* @__PURE__ */ new Error("all esplora endpoints failed");
|
|
2497
|
+
try {
|
|
2498
|
+
return await call(base, path, body, 0);
|
|
2499
|
+
} catch (error) {
|
|
2500
|
+
return attempt(next, path, body, error);
|
|
2501
|
+
}
|
|
2502
|
+
};
|
|
2503
|
+
return {
|
|
2504
|
+
get: async (path) => attempt(endpoints, path, void 0, void 0),
|
|
2505
|
+
post: async (path, body) => attempt(endpoints, path, body, void 0)
|
|
2506
|
+
};
|
|
2507
|
+
};
|
|
2508
|
+
const feeTiers = async (es) => {
|
|
2509
|
+
const res = await es.get("/fee-estimates");
|
|
2510
|
+
const estimates = isObject$6(res) ? res : {};
|
|
2511
|
+
const pick = (target) => Math.max(1, Math.ceil(Number(estimates[target] ?? 1)));
|
|
2512
|
+
return [
|
|
2513
|
+
{
|
|
2514
|
+
tier: "fast",
|
|
2515
|
+
satPerByte: pick("1")
|
|
2516
|
+
},
|
|
2517
|
+
{
|
|
2518
|
+
tier: "normal",
|
|
2519
|
+
satPerByte: pick("3")
|
|
2520
|
+
},
|
|
2521
|
+
{
|
|
2522
|
+
tier: "slow",
|
|
2523
|
+
satPerByte: pick("6")
|
|
2524
|
+
},
|
|
2525
|
+
{
|
|
2526
|
+
tier: "economy",
|
|
2527
|
+
satPerByte: pick("144")
|
|
2528
|
+
}
|
|
2529
|
+
];
|
|
2530
|
+
};
|
|
2531
|
+
const dustFor = (scriptType) => DUST[scriptType] ?? 546;
|
|
2532
|
+
const isFrozen = (value) => FROZEN_AMOUNTS.has(value);
|
|
2533
|
+
var BitcoinChainAdapter = class {
|
|
2534
|
+
/** Inputs for selectUTXO — the same TransactionInputUpdate objects added to the PSBT. */
|
|
2535
|
+
toInputs(utxos, fingerprint, pathOf, pubkeyOf, scriptOf, template, rawTxOf) {
|
|
2536
|
+
return utxos.filter((u) => u.status === "confirmed" && !isFrozen(u.value)).map((u) => {
|
|
2537
|
+
const pub = pubkeyOf(u);
|
|
2538
|
+
const entry = {
|
|
2539
|
+
txid: u.txid,
|
|
2540
|
+
index: u.vout,
|
|
2541
|
+
sequence: 4294967293
|
|
2542
|
+
};
|
|
2543
|
+
if (template !== "btc.bip44") entry.witnessUtxo = {
|
|
2544
|
+
amount: BigInt(u.value),
|
|
2545
|
+
script: scriptOf(u)
|
|
2546
|
+
};
|
|
2547
|
+
if (template === "btc.bip44") {
|
|
2548
|
+
const raw = rawTxOf === void 0 ? void 0 : rawTxOf(u);
|
|
2549
|
+
if (raw === void 0) throw new Error(`no raw transaction for bip44 input ${u.txid}`);
|
|
2550
|
+
entry.nonWitnessUtxo = raw;
|
|
2551
|
+
}
|
|
2552
|
+
if (template === "btc.bip86") {
|
|
2553
|
+
entry.tapInternalKey = pub.subarray(1);
|
|
2554
|
+
entry.tapBip32Derivation = [[pub.subarray(1), {
|
|
2555
|
+
hashes: [],
|
|
2556
|
+
der: {
|
|
2557
|
+
fingerprint,
|
|
2558
|
+
path: pathOf(u)
|
|
2559
|
+
}
|
|
2560
|
+
}]];
|
|
2561
|
+
} else {
|
|
2562
|
+
entry.bip32Derivation = [[pub, {
|
|
2563
|
+
fingerprint,
|
|
2564
|
+
path: pathOf(u)
|
|
2565
|
+
}]];
|
|
2566
|
+
if (template === "btc.bip49") entry.redeemScript = p2wpkh(pub).script;
|
|
2567
|
+
}
|
|
2568
|
+
return entry;
|
|
2569
|
+
});
|
|
2570
|
+
}
|
|
2571
|
+
buildPsbt(inputs, to, amount, feePerByte, changeAddress, network) {
|
|
2572
|
+
const selection = selectUTXO([...inputs], [{
|
|
2573
|
+
address: to,
|
|
2574
|
+
amount
|
|
2575
|
+
}], "default", {
|
|
2576
|
+
feePerByte,
|
|
2577
|
+
changeAddress,
|
|
2578
|
+
bip69: true,
|
|
2579
|
+
dust: BigInt(dustFor("p2wpkh")),
|
|
2580
|
+
network,
|
|
2581
|
+
createTx: true
|
|
2582
|
+
});
|
|
2583
|
+
if (selection === void 0 || selection.tx === void 0) throw new Error("insufficient confirmed utxo for the amount + fee");
|
|
2584
|
+
const wanted = new Set((selection.inputs ?? []).flatMap((sel) => {
|
|
2585
|
+
const raw = sel.txid;
|
|
2586
|
+
if (!(raw instanceof Uint8Array)) return [];
|
|
2587
|
+
const forward = hex.encode(raw);
|
|
2588
|
+
const reverse = hex.encode(Uint8Array.from(raw).reverse());
|
|
2589
|
+
return [`${forward}:${String(sel.index ?? -1)}`, `${reverse}:${String(sel.index ?? -1)}`];
|
|
2590
|
+
}));
|
|
2591
|
+
const chosen = inputs.filter((i) => wanted.has(`${String(i.txid)}:${String(i.index)}`));
|
|
2592
|
+
return {
|
|
2593
|
+
psbt: selection.tx.toPSBT(),
|
|
2594
|
+
inputs: chosen
|
|
2595
|
+
};
|
|
2596
|
+
}
|
|
2597
|
+
/** Fixed-input/output PSBT — the RBF/CPFP path, which chooses nothing itself. */
|
|
2598
|
+
buildExactPsbt(inputs, outputs, network) {
|
|
2599
|
+
const tx = new Transaction();
|
|
2600
|
+
for (const input of inputs) tx.addInput(input);
|
|
2601
|
+
for (const out of outputs) tx.addOutputAddress(out.address, out.amount, network);
|
|
2602
|
+
const tap = inputs.every((i) => i.tapInternalKey !== void 0);
|
|
2603
|
+
const vsize = Math.ceil(10.5 + inputs.length * (tap ? 57.5 : 68) + outputs.length * (tap ? 43 : 31));
|
|
2604
|
+
return {
|
|
2605
|
+
psbt: tx.toPSBT(),
|
|
2606
|
+
vsize
|
|
2607
|
+
};
|
|
2608
|
+
}
|
|
2609
|
+
};
|
|
2610
|
+
//#endregion
|
|
2611
|
+
//#region src/networks/registry.ts
|
|
2612
|
+
const entry = (chainId, name, symbol, testnet, _source) => ({
|
|
2613
|
+
id: `eip155:${chainId}`,
|
|
2614
|
+
chainId,
|
|
2615
|
+
name,
|
|
2616
|
+
symbol,
|
|
2617
|
+
decimals: 18,
|
|
2618
|
+
testnet,
|
|
2619
|
+
family: "evm",
|
|
2620
|
+
multicall3: "0xcA11bde05977b3631167028862bE2a173976CA11"
|
|
2621
|
+
});
|
|
2622
|
+
const NETWORKS = [
|
|
2623
|
+
entry(1, "Ethereum", "ETH", false, "https://ethereum.org/en/developers/docs/networks/"),
|
|
2624
|
+
entry(8453, "Base", "ETH", false, "https://docs.base.org/docs/network-information"),
|
|
2625
|
+
entry(42161, "Arbitrum One", "ETH", false, "https://docs.arbitrum.io/for-devs/dev-tools-and-resources/chain-info"),
|
|
2626
|
+
entry(10, "OP Mainnet", "ETH", false, "https://docs.optimism.io/chain/networks"),
|
|
2627
|
+
entry(137, "Polygon", "POL", false, "https://docs.polygon.technology/pos/reference/rpc-endpoints/"),
|
|
2628
|
+
entry(56, "BNB Smart Chain", "BNB", false, "https://docs.bnbchain.org/bnb-smart-chain/developers/json_rpc/"),
|
|
2629
|
+
entry(999, "HyperEVM", "HYPE", false, "https://hyperliquid.gitbook.io/hyperliquid-docs/for-developers/hyperevm"),
|
|
2630
|
+
entry(43114, "Avalanche C-Chain", "AVAX", false, "https://build.avax.network/docs/tooling/rpc-providers"),
|
|
2631
|
+
entry(11155111, "Sepolia", "ETH", true, "https://ethereum.org/en/developers/docs/networks/"),
|
|
2632
|
+
entry(84532, "Base Sepolia", "ETH", true, "https://docs.base.org/docs/network-information"),
|
|
2633
|
+
entry(421614, "Arbitrum Sepolia", "ETH", true, "https://docs.arbitrum.io/for-devs/dev-tools-and-resources/chain-info"),
|
|
2634
|
+
entry(11155420, "OP Sepolia", "ETH", true, "https://docs.optimism.io/chain/networks"),
|
|
2635
|
+
entry(80002, "Polygon Amoy", "POL", true, "https://docs.polygon.technology/pos/reference/rpc-endpoints/"),
|
|
2636
|
+
entry(97, "BNB Smart Chain Testnet", "tBNB", true, "https://docs.bnbchain.org/bnb-smart-chain/developers/json_rpc/"),
|
|
2637
|
+
entry(998, "HyperEVM Testnet", "HYPE", true, "https://hyperliquid.gitbook.io/hyperliquid-docs/for-developers/hyperevm"),
|
|
2638
|
+
entry(43113, "Avalanche Fuji", "AVAX", true, "https://build.avax.network/docs/tooling/rpc-providers")
|
|
2639
|
+
];
|
|
2640
|
+
const networkById = (id) => [
|
|
2641
|
+
...NETWORKS,
|
|
2642
|
+
...SOLANA_NETWORKS,
|
|
2643
|
+
...BITCOIN_NETWORKS
|
|
2644
|
+
].find((n) => n.id === id);
|
|
2645
|
+
const BITCOIN_NETWORKS = [
|
|
2646
|
+
{
|
|
2647
|
+
id: "bip122:000000000019d6689c085ae165831e93",
|
|
2648
|
+
chainId: 0,
|
|
2649
|
+
name: "Bitcoin",
|
|
2650
|
+
symbol: "BTC",
|
|
2651
|
+
decimals: 8,
|
|
2652
|
+
testnet: false,
|
|
2653
|
+
family: "bitcoin",
|
|
2654
|
+
genesisHash: "000000000019d6689c085ae165831e934ff763ae46a2a6c172b3f1b60a8ce26f"
|
|
2655
|
+
},
|
|
2656
|
+
{
|
|
2657
|
+
id: "bip122:00000000da84f2bafbbc53dee25a72ae",
|
|
2658
|
+
chainId: 0,
|
|
2659
|
+
name: "Bitcoin Testnet4",
|
|
2660
|
+
symbol: "tBTC",
|
|
2661
|
+
decimals: 8,
|
|
2662
|
+
testnet: true,
|
|
2663
|
+
family: "bitcoin",
|
|
2664
|
+
genesisHash: "00000000da84f2bafbbc53dee25a72ae507ff4914b867c565be350b0da8bf043"
|
|
2665
|
+
},
|
|
2666
|
+
{
|
|
2667
|
+
id: "bip122:00000008819873e925422c1ff0f99f7c",
|
|
2668
|
+
chainId: 0,
|
|
2669
|
+
name: "Bitcoin Signet",
|
|
2670
|
+
symbol: "sBTC",
|
|
2671
|
+
decimals: 8,
|
|
2672
|
+
testnet: true,
|
|
2673
|
+
family: "bitcoin",
|
|
2674
|
+
genesisHash: "00000008819873e925422c1ff0f99f7cc9bbb232af63a077a480a3633bee1ef6"
|
|
2675
|
+
},
|
|
2676
|
+
{
|
|
2677
|
+
id: "bip122:0f9188f13cb7b2c71f2a335e3a4fc328",
|
|
2678
|
+
chainId: 0,
|
|
2679
|
+
name: "Bitcoin Regtest",
|
|
2680
|
+
symbol: "rBTC",
|
|
2681
|
+
decimals: 8,
|
|
2682
|
+
testnet: true,
|
|
2683
|
+
family: "bitcoin",
|
|
2684
|
+
genesisHash: "0f9188f13cb7b2c71f2a335e3a4fc328bf5beb436012afca590b1a11466e2206"
|
|
2685
|
+
}
|
|
2686
|
+
];
|
|
2687
|
+
const SOLANA_NETWORKS = [{
|
|
2688
|
+
id: "solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp",
|
|
2689
|
+
chainId: 0,
|
|
2690
|
+
name: "Solana",
|
|
2691
|
+
symbol: "SOL",
|
|
2692
|
+
decimals: 9,
|
|
2693
|
+
testnet: false,
|
|
2694
|
+
family: "solana",
|
|
2695
|
+
genesisHash: "5eykt4UsFv8P8NJdTREpY1vzqKqZKvdpWvDVGok8cUQPCbq9AzdTQvpNbNvyq4qDvMNTrDDezsFapSihepu2eQEdje"
|
|
2696
|
+
}, {
|
|
2697
|
+
id: "solana:EtWTRABZaYq6iMfeYKouRu166VU2xqa1",
|
|
2698
|
+
chainId: 0,
|
|
2699
|
+
name: "Solana Devnet",
|
|
2700
|
+
symbol: "SOL",
|
|
2701
|
+
decimals: 9,
|
|
2702
|
+
testnet: true,
|
|
2703
|
+
family: "solana",
|
|
2704
|
+
genesisHash: "EtWTRABZaYq6iMfeYKouRu166VU2xqa1wcaWoxPkrZBGGSWgX4GwCJRmgdcvRaRmFbXfw3nDZBTqsr"
|
|
2705
|
+
}];
|
|
2706
|
+
//#endregion
|
|
2707
|
+
//#region src/networks/controller.ts
|
|
2708
|
+
/**
|
|
2709
|
+
* EVM network controller: the built-in registry plus host-provided and user-added RPC endpoints.
|
|
2710
|
+
* Each network gets a lazily-created viem PublicClient on a `fallback` transport — endpoints are
|
|
2711
|
+
* tried in order (custom → host), each with the injected fetch, so a dead endpoint fails over
|
|
2712
|
+
* without code here. Custom RPC URLs are validated against the registry chain id with `eth_chainId`
|
|
2713
|
+
* and persisted in the EngineStore.
|
|
2714
|
+
*/
|
|
2715
|
+
const esploraSleep = async (ms) => new Promise((resolve) => {
|
|
2716
|
+
setTimeout(resolve, ms);
|
|
2717
|
+
});
|
|
2718
|
+
const isObject$5 = (v) => typeof v === "object" && v !== null;
|
|
2719
|
+
var NetworkController = class extends Controller {
|
|
2720
|
+
options;
|
|
2721
|
+
engineStore;
|
|
2722
|
+
clients = /* @__PURE__ */ new Map();
|
|
2723
|
+
constructor(options) {
|
|
2724
|
+
super({
|
|
2725
|
+
name: "networks",
|
|
2726
|
+
defaultState: () => ({ customRpc: {} })
|
|
2727
|
+
});
|
|
2728
|
+
this.options = options;
|
|
2729
|
+
}
|
|
2730
|
+
async init() {
|
|
2731
|
+
this.engineStore = await this.options.openStore();
|
|
2732
|
+
const raw = await this.engineStore.collection("networks").get("customRpc");
|
|
2733
|
+
if (raw === void 0) return;
|
|
2734
|
+
const parsed = JSON.parse(new TextDecoder().decode(raw));
|
|
2735
|
+
if (typeof parsed === "object" && parsed !== null) {
|
|
2736
|
+
const map = {};
|
|
2737
|
+
for (const [k, v] of Object.entries(parsed)) if (Array.isArray(v) && v.every((u) => typeof u === "string")) map[k] = v;
|
|
2738
|
+
this.update((s) => ({
|
|
2739
|
+
...s,
|
|
2740
|
+
customRpc: map
|
|
2741
|
+
}));
|
|
2742
|
+
}
|
|
2743
|
+
}
|
|
2744
|
+
async destroy() {
|
|
2745
|
+
this.lock();
|
|
2746
|
+
await Promise.resolve();
|
|
2747
|
+
}
|
|
2748
|
+
lock() {
|
|
2749
|
+
this.clients.clear();
|
|
2750
|
+
this.engineStore = void 0;
|
|
2751
|
+
}
|
|
2752
|
+
endpoints(network) {
|
|
2753
|
+
return [...this.getState().customRpc[network] ?? [], ...this.options.rpc[network] ?? []];
|
|
2754
|
+
}
|
|
2755
|
+
info(network) {
|
|
2756
|
+
const info = networkById(network);
|
|
2757
|
+
if (info === void 0) throw new Error(`unknown network ${network}`);
|
|
2758
|
+
return info;
|
|
2759
|
+
}
|
|
2760
|
+
client(network) {
|
|
2761
|
+
const info = this.info(network);
|
|
2762
|
+
const cached = this.clients.get(network);
|
|
2763
|
+
if (cached !== void 0) return cached;
|
|
2764
|
+
const urls = this.endpoints(network);
|
|
2765
|
+
if (urls.length === 0) throw new Error(`no rpc endpoint for ${network}`);
|
|
2766
|
+
const client = createPublicClient({
|
|
2767
|
+
chain: {
|
|
2768
|
+
id: info.chainId,
|
|
2769
|
+
name: info.name,
|
|
2770
|
+
nativeCurrency: {
|
|
2771
|
+
name: info.symbol,
|
|
2772
|
+
symbol: info.symbol,
|
|
2773
|
+
decimals: info.decimals
|
|
2774
|
+
},
|
|
2775
|
+
rpcUrls: { default: { http: urls } }
|
|
2776
|
+
},
|
|
2777
|
+
transport: fallback(urls.map((url) => http(url, { fetchFn: this.options.fetch }))),
|
|
2778
|
+
batch: { multicall: true }
|
|
2779
|
+
});
|
|
2780
|
+
this.clients.set(network, client);
|
|
2781
|
+
return client;
|
|
2782
|
+
}
|
|
2783
|
+
/** Validate a custom endpoint's reported chain id, then persist it first in the list. */
|
|
2784
|
+
/** Solana JSON-RPC over the ordered endpoint list — kit's transport is single-endpoint. */
|
|
2785
|
+
/** Networks with at least one configured endpoint, filtered by family. */
|
|
2786
|
+
enabled(family) {
|
|
2787
|
+
return [.../* @__PURE__ */ new Set([...Object.keys(this.options.rpc), ...Object.keys(this.getState().customRpc)])].filter((id) => {
|
|
2788
|
+
const info = networkById(id);
|
|
2789
|
+
return info !== void 0 && info.family === family;
|
|
2790
|
+
});
|
|
2791
|
+
}
|
|
2792
|
+
solanaRpc(network) {
|
|
2793
|
+
const urls = this.endpoints(network);
|
|
2794
|
+
if (urls.length === 0) throw new Error(`no rpc endpoint for ${network}`);
|
|
2795
|
+
return { call: async (method, params) => {
|
|
2796
|
+
const attempt = async (rest, last) => {
|
|
2797
|
+
const [url, ...next] = rest;
|
|
2798
|
+
if (url === void 0) throw last instanceof Error ? last : new Error(typeof last === "string" ? last : method);
|
|
2799
|
+
try {
|
|
2800
|
+
const body = await (await this.options.fetch(url, {
|
|
2801
|
+
method: "POST",
|
|
2802
|
+
headers: { "content-type": "application/json" },
|
|
2803
|
+
body: JSON.stringify({
|
|
2804
|
+
jsonrpc: "2.0",
|
|
2805
|
+
id: 1,
|
|
2806
|
+
method,
|
|
2807
|
+
params
|
|
2808
|
+
})
|
|
2809
|
+
})).json();
|
|
2810
|
+
if (!isObject$5(body)) return await attempt(next, /* @__PURE__ */ new Error(`malformed response from ${url}`));
|
|
2811
|
+
if (body["error"] !== void 0) {
|
|
2812
|
+
const msg = isObject$5(body["error"]) ? body["error"]["message"] : void 0;
|
|
2813
|
+
return await attempt(next, new Error(typeof msg === "string" ? msg : method));
|
|
2814
|
+
}
|
|
2815
|
+
return body["result"];
|
|
2816
|
+
} catch (error) {
|
|
2817
|
+
return attempt(next, error);
|
|
2818
|
+
}
|
|
2819
|
+
};
|
|
2820
|
+
return attempt(urls, void 0);
|
|
2821
|
+
} };
|
|
2822
|
+
}
|
|
2823
|
+
/** Bounded-concurrency + 429-aware ordered-failover Esplora client. */
|
|
2824
|
+
esplora(network) {
|
|
2825
|
+
const urls = this.endpoints(network);
|
|
2826
|
+
if (urls.length === 0) throw new Error(`no esplora endpoint for ${network}`);
|
|
2827
|
+
return esplora(urls, async (url, init) => {
|
|
2828
|
+
const r = await this.options.fetch(url, init);
|
|
2829
|
+
return {
|
|
2830
|
+
ok: r.ok,
|
|
2831
|
+
status: r.status,
|
|
2832
|
+
headers: { get: (h) => r.headers.get(h) ?? void 0 },
|
|
2833
|
+
json: async () => r.json(),
|
|
2834
|
+
text: async () => r.text()
|
|
2835
|
+
};
|
|
2836
|
+
}, esploraSleep);
|
|
2837
|
+
}
|
|
2838
|
+
async addRpc(network, url) {
|
|
2839
|
+
const info = this.info(network);
|
|
2840
|
+
if (info.family === "bitcoin") {
|
|
2841
|
+
const genesis = (await (await this.options.fetch(`${url.replace(/\/$/, "")}/block-height/0`)).text()).trim();
|
|
2842
|
+
if (genesis !== info.genesisHash) throw new Error(`${url} reports genesis ${genesis}, expected ${info.genesisHash ?? ""}`);
|
|
2843
|
+
} else if (info.family === "solana") {
|
|
2844
|
+
const body = await (await this.options.fetch(url, {
|
|
2845
|
+
method: "POST",
|
|
2846
|
+
headers: { "content-type": "application/json" },
|
|
2847
|
+
body: JSON.stringify({
|
|
2848
|
+
jsonrpc: "2.0",
|
|
2849
|
+
id: 1,
|
|
2850
|
+
method: "getGenesisHash",
|
|
2851
|
+
params: []
|
|
2852
|
+
})
|
|
2853
|
+
})).json();
|
|
2854
|
+
const genesis = isObject$5(body) ? body["result"] : void 0;
|
|
2855
|
+
if (genesis !== info.genesisHash) throw new Error(`${url} reports genesis hash ${String(genesis)}, expected ${info.genesisHash ?? ""}`);
|
|
2856
|
+
const current = this.getState().customRpc[network] ?? [];
|
|
2857
|
+
const customRpc = {
|
|
2858
|
+
...this.getState().customRpc,
|
|
2859
|
+
[network]: [url, ...current]
|
|
2860
|
+
};
|
|
2861
|
+
this.update((s) => ({
|
|
2862
|
+
...s,
|
|
2863
|
+
customRpc
|
|
2864
|
+
}));
|
|
2865
|
+
const store = this.engineStore;
|
|
2866
|
+
if (store !== void 0) await store.collection("networks").put("customRpc", new TextEncoder().encode(JSON.stringify(customRpc)));
|
|
2867
|
+
return;
|
|
2868
|
+
}
|
|
2869
|
+
const body = await (await this.options.fetch(url, {
|
|
2870
|
+
method: "POST",
|
|
2871
|
+
headers: { "content-type": "application/json" },
|
|
2872
|
+
body: JSON.stringify({
|
|
2873
|
+
jsonrpc: "2.0",
|
|
2874
|
+
id: 1,
|
|
2875
|
+
method: "eth_chainId",
|
|
2876
|
+
params: []
|
|
2877
|
+
})
|
|
2878
|
+
})).json();
|
|
2879
|
+
const result = typeof body === "object" && body !== null && "result" in body ? body.result : void 0;
|
|
2880
|
+
if (typeof result !== "string") throw new TypeError(`eth_chainId returned a malformed response from ${url}`);
|
|
2881
|
+
const chainId = Number.parseInt(result, 16);
|
|
2882
|
+
if (chainId !== info.chainId) throw new Error(`${url} reports chain id ${chainId}, expected ${info.chainId}`);
|
|
2883
|
+
const current = this.getState().customRpc[network] ?? [];
|
|
2884
|
+
const customRpc = {
|
|
2885
|
+
...this.getState().customRpc,
|
|
2886
|
+
[network]: [url, ...current]
|
|
2887
|
+
};
|
|
2888
|
+
this.update((s) => ({
|
|
2889
|
+
...s,
|
|
2890
|
+
customRpc
|
|
2891
|
+
}));
|
|
2892
|
+
this.clients.delete(network);
|
|
2893
|
+
const store = this.engineStore;
|
|
2894
|
+
if (store !== void 0) await store.collection("networks").put("customRpc", new TextEncoder().encode(JSON.stringify(customRpc)));
|
|
2895
|
+
}
|
|
2896
|
+
};
|
|
2897
|
+
//#endregion
|
|
2898
|
+
//#region src/payments/parse.ts
|
|
2899
|
+
/** Invoices with less than this much life left are treated as already expired. */
|
|
2900
|
+
const BOLT11_EXPIRY_GRACE_MS = 6e4;
|
|
2901
|
+
const invalid = (reason) => ({
|
|
2902
|
+
kind: "invalid",
|
|
2903
|
+
reason
|
|
2904
|
+
});
|
|
2905
|
+
/** BTC decimal `amount` → sats. At most 8 decimal places; must fit a safe integer. */
|
|
2906
|
+
function btcAmountSats(text) {
|
|
2907
|
+
const match = /^(\d+)(?:\.(\d{1,8}))?$/.exec(text);
|
|
2908
|
+
if (match === null) return;
|
|
2909
|
+
const [, whole] = match;
|
|
2910
|
+
if (whole === void 0) return;
|
|
2911
|
+
const sats = BigInt(whole) * 100000000n + BigInt((match[2] ?? "").padEnd(8, "0") || "0");
|
|
2912
|
+
return sats <= BigInt(Number.MAX_SAFE_INTEGER) ? Number(sats) : void 0;
|
|
2913
|
+
}
|
|
2914
|
+
/**
|
|
2915
|
+
* BOLT11 → fields, network- and expiry-checked. `undefined` = not a bolt11 string; `"network"` /
|
|
2916
|
+
* `"expired"` / `"malformed"` are the failure modes.
|
|
2917
|
+
*/
|
|
2918
|
+
function bolt11Fields(text, options) {
|
|
2919
|
+
let decoded;
|
|
2920
|
+
try {
|
|
2921
|
+
decoded = decodeBolt11(text);
|
|
2922
|
+
} catch {
|
|
2923
|
+
return;
|
|
2166
2924
|
}
|
|
2167
2925
|
const network = decoded.network === "mainnet" ? "mainnet" : decoded.network === "regtest" ? "regtest" : void 0;
|
|
2168
2926
|
if (network === void 0) return "network";
|
|
@@ -2681,7 +3439,7 @@ function categorizePayment(error) {
|
|
|
2681
3439
|
//#endregion
|
|
2682
3440
|
//#region src/spark/notes.ts
|
|
2683
3441
|
const HEX64 = /^[0-9a-f]{64}$/;
|
|
2684
|
-
const isObject = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
|
|
3442
|
+
const isObject$4 = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
|
|
2685
3443
|
const NOTE_FIELDS = /* @__PURE__ */ new Set([
|
|
2686
3444
|
"kind",
|
|
2687
3445
|
"recipientPubkey",
|
|
@@ -2691,7 +3449,7 @@ const NOTE_FIELDS = /* @__PURE__ */ new Set([
|
|
|
2691
3449
|
]);
|
|
2692
3450
|
/** Strict read-side validation: anything off-shape is `undefined`, never a throw. */
|
|
2693
3451
|
function parsePaymentNote(value) {
|
|
2694
|
-
if (!isObject(value)) return;
|
|
3452
|
+
if (!isObject$4(value)) return;
|
|
2695
3453
|
for (const field of Object.keys(value)) if (!NOTE_FIELDS.has(field)) return;
|
|
2696
3454
|
const { kind, recipientPubkey, eventId, comment, lightning } = value;
|
|
2697
3455
|
if (kind !== "zap") return;
|
|
@@ -3249,6 +4007,1161 @@ var SparkController = class extends Controller {
|
|
|
3249
4007
|
}
|
|
3250
4008
|
};
|
|
3251
4009
|
//#endregion
|
|
4010
|
+
//#region src/transactions/bitcoin.ts
|
|
4011
|
+
/** Bitcoin route: scan → coin-select → PSBT → sign → broadcast, plus esplora status poll. */
|
|
4012
|
+
const isObject$3 = (v) => typeof v === "object" && v !== null;
|
|
4013
|
+
const fetchRawTxes = async (es, utxos) => {
|
|
4014
|
+
const out = /* @__PURE__ */ new Map();
|
|
4015
|
+
await Promise.all(utxos.map(async (u) => {
|
|
4016
|
+
const raw = await es.get(`/tx/${u.txid}/hex`);
|
|
4017
|
+
if (typeof raw !== "string") throw new TypeError(`esplora /tx/${u.txid}/hex: not a string`);
|
|
4018
|
+
const bytes = hex.decode(raw);
|
|
4019
|
+
if (Transaction.fromRaw(bytes, { allowUnknownInputs: true }).id !== u.txid) throw new Error(`esplora /tx/${u.txid}/hex: txid mismatch`);
|
|
4020
|
+
out.set(u.txid, bytes);
|
|
4021
|
+
}));
|
|
4022
|
+
return out;
|
|
4023
|
+
};
|
|
4024
|
+
/** The account-relative full path `purpose'/coin'/group'/change/index` for a PSBT entry. */
|
|
4025
|
+
const accountPath = (accountPath) => accountPath.replace(/^m\//, "").split("/").map((p) => {
|
|
4026
|
+
const hardened = p.endsWith("'");
|
|
4027
|
+
const n = Number(hardened ? p.slice(0, -1) : p);
|
|
4028
|
+
return hardened ? n + 2147483648 : n;
|
|
4029
|
+
});
|
|
4030
|
+
const poll = async (ctx, tx) => {
|
|
4031
|
+
if (tx.hash === void 0) return "failed";
|
|
4032
|
+
const status = await ctx.options.networks.esplora(tx.network).get(`/tx/${tx.hash}/status`);
|
|
4033
|
+
if (!(isObject$3(status) && status["confirmed"] === true)) return;
|
|
4034
|
+
return "confirmed";
|
|
4035
|
+
};
|
|
4036
|
+
/** Per-controller route — the UTXO store and the build stash are engine state. */
|
|
4037
|
+
const createBitcoinRoute = () => {
|
|
4038
|
+
const bitcoin = new BitcoinChainAdapter();
|
|
4039
|
+
const bitcoinUtxos = new BtcUtxoStore();
|
|
4040
|
+
const pending = /* @__PURE__ */ new Map();
|
|
4041
|
+
const send = async (ctx, params) => {
|
|
4042
|
+
const { account, network, to, amount, fee, verify } = params;
|
|
4043
|
+
if (account.kind !== "hd") throw new Error("imported accounts are not yet supported");
|
|
4044
|
+
const es = ctx.options.networks.esplora(network);
|
|
4045
|
+
const apis = ctx.options.chains ?? chains(ctx.options.wallet);
|
|
4046
|
+
const template = [
|
|
4047
|
+
"btc.bip44",
|
|
4048
|
+
"btc.bip49",
|
|
4049
|
+
"btc.bip84",
|
|
4050
|
+
"btc.bip86"
|
|
4051
|
+
].find((x) => x === account.template) ?? "btc.bip84";
|
|
4052
|
+
const btcAccount = await apis.account({
|
|
4053
|
+
walletId: account.walletId,
|
|
4054
|
+
groupIndex: account.groupIndex,
|
|
4055
|
+
template,
|
|
4056
|
+
network
|
|
4057
|
+
});
|
|
4058
|
+
if (!("xpub" in btcAccount) || btcAccount.xpub === void 0) throw new Error("bitcoin account carries no xpub");
|
|
4059
|
+
const { xpub, fingerprint, path } = btcAccount;
|
|
4060
|
+
const fp = Number.parseInt(fingerprint, 16);
|
|
4061
|
+
const net = bitcoinNetwork(network);
|
|
4062
|
+
const tap = template === "btc.bip86";
|
|
4063
|
+
const rawTxOf = (u) => rawTxes.get(u.txid);
|
|
4064
|
+
const receive = await scanChain(es, xpub, template, network, net.params, 0);
|
|
4065
|
+
const change = await scanChain(es, xpub, template, network, net.params, 1);
|
|
4066
|
+
const addresses = [...receive, ...change];
|
|
4067
|
+
const utxos = (await scanUtxos(es, addresses, net.params)).map((u) => ({
|
|
4068
|
+
txid: u.txid,
|
|
4069
|
+
vout: u.vout,
|
|
4070
|
+
value: u.value,
|
|
4071
|
+
address: u.address,
|
|
4072
|
+
status: "confirmed",
|
|
4073
|
+
height: u.height
|
|
4074
|
+
}));
|
|
4075
|
+
const byAddr = new Map(addresses.map((a) => [a.address, a]));
|
|
4076
|
+
const base = accountPath(path);
|
|
4077
|
+
const rawTxes = template === "btc.bip44" ? await fetchRawTxes(es, utxos) : /* @__PURE__ */ new Map();
|
|
4078
|
+
const inputs = bitcoin.toInputs(utxos, fp, (u) => {
|
|
4079
|
+
const a = byAddr.get(u.address);
|
|
4080
|
+
return a === void 0 ? [] : [
|
|
4081
|
+
...base,
|
|
4082
|
+
a.chain,
|
|
4083
|
+
a.index
|
|
4084
|
+
];
|
|
4085
|
+
}, (u) => {
|
|
4086
|
+
const a = byAddr.get(u.address);
|
|
4087
|
+
if (a === void 0) throw new Error(`utxo ${u.txid} on an unknown address`);
|
|
4088
|
+
return bitcoinPubkeyFromXpub(xpub, template, network, a.chain, a.index);
|
|
4089
|
+
}, (u) => scriptPubKey(u.address, net.params), template, template === "btc.bip44" ? rawTxOf : void 0);
|
|
4090
|
+
let changeAddr = change.find((a) => !a.used)?.address;
|
|
4091
|
+
if (template === "btc.bip49" || template === "btc.bip44") {
|
|
4092
|
+
const acct84 = await apis.account({
|
|
4093
|
+
walletId: account.walletId,
|
|
4094
|
+
groupIndex: account.groupIndex,
|
|
4095
|
+
template: "btc.bip84",
|
|
4096
|
+
network
|
|
4097
|
+
});
|
|
4098
|
+
const xpub84 = "xpub" in acct84 ? acct84.xpub : void 0;
|
|
4099
|
+
if (xpub84 === void 0) throw new Error("no bip84 account for the legacy-template change chain");
|
|
4100
|
+
changeAddr = (await scanChain(es, xpub84, "btc.bip84", network, net.params, 1)).find((a) => !a.used)?.address;
|
|
4101
|
+
}
|
|
4102
|
+
if (changeAddr === void 0) throw new Error("no unused change address");
|
|
4103
|
+
const quotes = await feeTiers(es);
|
|
4104
|
+
const quote = quotes.find((q) => q.tier === (fee === "fast" ? "fast" : fee === "slow" ? "slow" : "normal")) ?? quotes[1];
|
|
4105
|
+
if (quote === void 0) throw new Error("no fee quote");
|
|
4106
|
+
const { psbt, inputs: selected } = bitcoin.buildPsbt(inputs, to, amount, BigInt(quote.satPerByte), changeAddr, net.params);
|
|
4107
|
+
const selectedKeys = new Set(selected.map((i) => `${String(i.txid ?? "")}:${String(i.index ?? 0)}`));
|
|
4108
|
+
const selectedUtxos = utxos.filter((u) => selectedKeys.has(`${u.txid}:${u.vout}`));
|
|
4109
|
+
const txP = Transaction.fromPSBT(psbt);
|
|
4110
|
+
const ownScripts = new Set(change.filter((a) => !a.used).map((a) => hex.encode(scriptPubKey(a.address, net.params))));
|
|
4111
|
+
const request = {
|
|
4112
|
+
kind: "bitcoin.psbt",
|
|
4113
|
+
network,
|
|
4114
|
+
account,
|
|
4115
|
+
psbt,
|
|
4116
|
+
sign: Array.from({ length: txP.inputsLength }, (_, i) => i)
|
|
4117
|
+
};
|
|
4118
|
+
const intent = decodeBitcoinPsbt(request, ownScripts);
|
|
4119
|
+
const vsize = Math.ceil(10.5 + txP.inputsLength * (tap ? 57.5 : 68) + txP.outputsLength * (tap ? 43 : 31));
|
|
4120
|
+
const outValues = Array.from({ length: txP.outputsLength }, (_, i) => {
|
|
4121
|
+
const o = txP.getOutput(i);
|
|
4122
|
+
const scriptHex = "script" in o && o.script instanceof Uint8Array ? o.script : /* @__PURE__ */ new Uint8Array();
|
|
4123
|
+
return {
|
|
4124
|
+
address: scriptHex.length > 0 ? Address(net.params).encode(OutScript.decode(scriptHex)) : "",
|
|
4125
|
+
value: Number(o.amount ?? 0n),
|
|
4126
|
+
own: false
|
|
4127
|
+
};
|
|
4128
|
+
});
|
|
4129
|
+
for (const o of outValues) if (o.address !== "" && ownScripts.has(hex.encode(scriptPubKey(o.address, net.params)))) o.own = true;
|
|
4130
|
+
const inTotal = selectedUtxos.reduce((a, u) => a + u.value, 0);
|
|
4131
|
+
const outTotal = outValues.reduce((a, o) => a + o.value, 0);
|
|
4132
|
+
const id0 = newId(ctx.options.random);
|
|
4133
|
+
pending.set(id0, {
|
|
4134
|
+
inputs,
|
|
4135
|
+
allUtxos: utxos,
|
|
4136
|
+
pathOf: (u) => {
|
|
4137
|
+
const a = byAddr.get(u.address);
|
|
4138
|
+
return a === void 0 ? [] : [
|
|
4139
|
+
...base,
|
|
4140
|
+
a.chain,
|
|
4141
|
+
a.index
|
|
4142
|
+
];
|
|
4143
|
+
},
|
|
4144
|
+
pubkeyOf: (u) => {
|
|
4145
|
+
const a = byAddr.get(u.address);
|
|
4146
|
+
if (a === void 0) throw new Error(`utxo ${String(u.txid)} on an unknown address`);
|
|
4147
|
+
return bitcoinPubkeyFromXpub(xpub, template, network, a.chain, a.index);
|
|
4148
|
+
},
|
|
4149
|
+
scriptOf: (u) => scriptPubKey(u.address, net.params),
|
|
4150
|
+
changeAddresses: change.filter((a) => !a.used).map((a) => a.address),
|
|
4151
|
+
fp,
|
|
4152
|
+
tier: fee === "fast" ? "fast" : fee === "slow" ? "slow" : "normal",
|
|
4153
|
+
template,
|
|
4154
|
+
network
|
|
4155
|
+
});
|
|
4156
|
+
let tx = {
|
|
4157
|
+
id: id0,
|
|
4158
|
+
family: "bitcoin",
|
|
4159
|
+
network,
|
|
4160
|
+
account: account.walletId,
|
|
4161
|
+
hash: void 0,
|
|
4162
|
+
state: "draft",
|
|
4163
|
+
intent,
|
|
4164
|
+
nonce: 0,
|
|
4165
|
+
btc: {
|
|
4166
|
+
inputs: selectedUtxos.map((u) => ({
|
|
4167
|
+
txid: u.txid,
|
|
4168
|
+
vout: u.vout,
|
|
4169
|
+
value: u.value,
|
|
4170
|
+
address: u.address
|
|
4171
|
+
})),
|
|
4172
|
+
outputs: outValues,
|
|
4173
|
+
vsize,
|
|
4174
|
+
fee: inTotal - outTotal,
|
|
4175
|
+
feeRate: Math.ceil((inTotal - outTotal) / vsize)
|
|
4176
|
+
},
|
|
4177
|
+
submittedAt: ctx.options.now(),
|
|
4178
|
+
error: void 0
|
|
4179
|
+
};
|
|
4180
|
+
tx = await ctx.transition(tx, {});
|
|
4181
|
+
try {
|
|
4182
|
+
const result = await apis.sign({
|
|
4183
|
+
request,
|
|
4184
|
+
verify
|
|
4185
|
+
});
|
|
4186
|
+
tx = await ctx.transition(tx, { state: "signed" });
|
|
4187
|
+
if (result.kind !== "bitcoin.psbt" || !(result.signedPsbt instanceof Uint8Array)) throw new Error("unexpected bitcoin sign result");
|
|
4188
|
+
bitcoinUtxos.lock(selectedUtxos, tx.id);
|
|
4189
|
+
const signed = Transaction.fromPSBT(result.signedPsbt);
|
|
4190
|
+
signed.finalize();
|
|
4191
|
+
const raw = signed.extract();
|
|
4192
|
+
const hash = String(await es.post("/tx", hex.encode(raw)));
|
|
4193
|
+
tx = await ctx.transition(tx, {
|
|
4194
|
+
state: "submitted",
|
|
4195
|
+
hash,
|
|
4196
|
+
submittedAt: ctx.options.now()
|
|
4197
|
+
});
|
|
4198
|
+
ctx.track(tx.id, hash);
|
|
4199
|
+
return tx;
|
|
4200
|
+
} catch (error) {
|
|
4201
|
+
const failed = tx.state === "signed" ? "failed" : "rejected";
|
|
4202
|
+
await ctx.transition(tx, {
|
|
4203
|
+
state: failed,
|
|
4204
|
+
error: errorMessage(error)
|
|
4205
|
+
});
|
|
4206
|
+
throw error;
|
|
4207
|
+
}
|
|
4208
|
+
};
|
|
4209
|
+
/** Esplora `/tx/:txid/status` → terminal state or `undefined` to keep polling. */
|
|
4210
|
+
const finish = async (ctx, stash, es, account, old, psbt, record, link, verify) => {
|
|
4211
|
+
const apis = ctx.options.chains ?? chains(ctx.options.wallet);
|
|
4212
|
+
const txP = Transaction.fromPSBT(psbt);
|
|
4213
|
+
const request = {
|
|
4214
|
+
kind: "bitcoin.psbt",
|
|
4215
|
+
network: old.network,
|
|
4216
|
+
account,
|
|
4217
|
+
psbt,
|
|
4218
|
+
sign: Array.from({ length: txP.inputsLength }, (_, i) => i)
|
|
4219
|
+
};
|
|
4220
|
+
const ownScripts = new Set(stash.changeAddresses.map((a) => hex.encode(scriptPubKey(a, bitcoinNetwork(stash.network).params))));
|
|
4221
|
+
const intent = decodeBitcoinPsbt(request, ownScripts);
|
|
4222
|
+
let tx = {
|
|
4223
|
+
id: newId(ctx.options.random),
|
|
4224
|
+
family: "bitcoin",
|
|
4225
|
+
network: old.network,
|
|
4226
|
+
account: old.account,
|
|
4227
|
+
hash: void 0,
|
|
4228
|
+
state: "draft",
|
|
4229
|
+
intent,
|
|
4230
|
+
nonce: 0,
|
|
4231
|
+
btc: record,
|
|
4232
|
+
submittedAt: ctx.options.now(),
|
|
4233
|
+
error: void 0,
|
|
4234
|
+
...link
|
|
4235
|
+
};
|
|
4236
|
+
tx = await ctx.transition(tx, {});
|
|
4237
|
+
try {
|
|
4238
|
+
const result = await apis.sign({
|
|
4239
|
+
request,
|
|
4240
|
+
verify
|
|
4241
|
+
});
|
|
4242
|
+
tx = await ctx.transition(tx, { state: "signed" });
|
|
4243
|
+
if (result.kind !== "bitcoin.psbt" || !(result.signedPsbt instanceof Uint8Array)) throw new Error("unexpected bitcoin sign result");
|
|
4244
|
+
bitcoinUtxos.endTx(old.id, "dropped");
|
|
4245
|
+
const signed = Transaction.fromPSBT(result.signedPsbt);
|
|
4246
|
+
signed.finalize();
|
|
4247
|
+
const raw = signed.extract();
|
|
4248
|
+
const hash = String(await es.post("/tx", hex.encode(raw)));
|
|
4249
|
+
bitcoinUtxos.lock(record.inputs.map((i) => ({
|
|
4250
|
+
...i,
|
|
4251
|
+
status: "confirmed",
|
|
4252
|
+
height: 0
|
|
4253
|
+
})), tx.id);
|
|
4254
|
+
tx = await ctx.transition(tx, {
|
|
4255
|
+
state: "submitted",
|
|
4256
|
+
hash,
|
|
4257
|
+
submittedAt: ctx.options.now()
|
|
4258
|
+
});
|
|
4259
|
+
ctx.track(tx.id, hash);
|
|
4260
|
+
return tx;
|
|
4261
|
+
} catch (error) {
|
|
4262
|
+
const failed = tx.state === "signed" ? "failed" : "rejected";
|
|
4263
|
+
await ctx.transition(tx, {
|
|
4264
|
+
state: failed,
|
|
4265
|
+
error: errorMessage(error)
|
|
4266
|
+
});
|
|
4267
|
+
throw error;
|
|
4268
|
+
}
|
|
4269
|
+
};
|
|
4270
|
+
const replace = async (ctx, old, mode, verify) => {
|
|
4271
|
+
const stash = pending.get(old.id);
|
|
4272
|
+
const { btc } = old;
|
|
4273
|
+
if (stash === void 0 || btc === void 0) throw new Error("the original build context is gone — cannot replace after restart");
|
|
4274
|
+
const es = ctx.options.networks.esplora(old.network);
|
|
4275
|
+
const apis = ctx.options.chains ?? chains(ctx.options.wallet);
|
|
4276
|
+
const { template } = stash;
|
|
4277
|
+
if (!("xpub" in await apis.account({
|
|
4278
|
+
walletId: old.account,
|
|
4279
|
+
groupIndex: 0,
|
|
4280
|
+
template,
|
|
4281
|
+
network: old.network
|
|
4282
|
+
}))) throw new Error("bitcoin account carries no xpub");
|
|
4283
|
+
const account = {
|
|
4284
|
+
kind: "hd",
|
|
4285
|
+
walletId: old.account,
|
|
4286
|
+
groupIndex: 0,
|
|
4287
|
+
template
|
|
4288
|
+
};
|
|
4289
|
+
const tiers = await feeTiers(es);
|
|
4290
|
+
const tierQuote = tiers.find((t) => t.tier === stash.tier) ?? tiers[1];
|
|
4291
|
+
const rate = tierQuote === void 0 ? 1 : tierQuote.satPerByte;
|
|
4292
|
+
const newFee = Math.max(btc.fee + btc.vsize, Math.ceil(btc.vsize * rate));
|
|
4293
|
+
const [changeAddr] = stash.changeAddresses;
|
|
4294
|
+
if (changeAddr === void 0) throw new Error("no unused change address");
|
|
4295
|
+
const net = bitcoinNetwork(old.network);
|
|
4296
|
+
const inTotal = btc.inputs.reduce((a, i) => a + i.value, 0);
|
|
4297
|
+
const dust = 546;
|
|
4298
|
+
const payee = btc.outputs.filter((o) => !o.own).map((o) => ({
|
|
4299
|
+
address: o.address,
|
|
4300
|
+
amount: BigInt(o.value)
|
|
4301
|
+
}));
|
|
4302
|
+
const usedKeys = new Set(btc.inputs.map((i) => `${i.txid}:${i.vout}`));
|
|
4303
|
+
const spare = stash.allUtxos.filter((u) => !usedKeys.has(`${u.txid}:${u.vout}`));
|
|
4304
|
+
const extraInputs = [];
|
|
4305
|
+
const extraUtxos = [];
|
|
4306
|
+
const feeNeeded = (o) => BigInt(newFee) - (BigInt(inTotal) + extraUtxos.reduce((a, u) => a + BigInt(u.value), 0n) - o.reduce((a, x) => a + x.amount, 0n));
|
|
4307
|
+
let outputs;
|
|
4308
|
+
if (mode === "cancel") outputs = [{
|
|
4309
|
+
address: changeAddr,
|
|
4310
|
+
amount: BigInt(inTotal - newFee)
|
|
4311
|
+
}];
|
|
4312
|
+
else {
|
|
4313
|
+
const shrunk = btc.outputs.find((o) => o.own);
|
|
4314
|
+
const changeAfter = shrunk === void 0 ? 0 : shrunk.value - (newFee - btc.fee);
|
|
4315
|
+
outputs = changeAfter >= dust ? [...payee, {
|
|
4316
|
+
address: changeAddr,
|
|
4317
|
+
amount: BigInt(changeAfter)
|
|
4318
|
+
}] : payee;
|
|
4319
|
+
while (feeNeeded(outputs) > 0n && spare.length > extraUtxos.length) {
|
|
4320
|
+
const next = spare[extraUtxos.length];
|
|
4321
|
+
if (next === void 0) break;
|
|
4322
|
+
extraUtxos.push(next);
|
|
4323
|
+
extraInputs.push(...bitcoin.toInputs([{
|
|
4324
|
+
...next,
|
|
4325
|
+
status: "confirmed"
|
|
4326
|
+
}], stash.fp, stash.pathOf, stash.pubkeyOf, stash.scriptOf, stash.template));
|
|
4327
|
+
}
|
|
4328
|
+
if (feeNeeded(outputs) > 0n) {
|
|
4329
|
+
const keepChange = [...outputs, {
|
|
4330
|
+
address: changeAddr,
|
|
4331
|
+
amount: BigInt(dust)
|
|
4332
|
+
}];
|
|
4333
|
+
while (feeNeeded(keepChange) > 0n && spare.length > extraUtxos.length) {
|
|
4334
|
+
const next = spare[extraUtxos.length];
|
|
4335
|
+
if (next === void 0) break;
|
|
4336
|
+
extraUtxos.push(next);
|
|
4337
|
+
extraInputs.push(...bitcoin.toInputs([{
|
|
4338
|
+
...next,
|
|
4339
|
+
status: "confirmed"
|
|
4340
|
+
}], 0, stash.pathOf, stash.pubkeyOf, stash.scriptOf, stash.template));
|
|
4341
|
+
}
|
|
4342
|
+
if (feeNeeded(keepChange) > 0n) throw new Error("insufficient confirmed utxo to fund the replacement fee");
|
|
4343
|
+
const changeValue = inTotal + extraUtxos.reduce((a, u) => a + u.value, 0) - payee.reduce((a, o) => a + Number(o.amount), 0) - newFee;
|
|
4344
|
+
outputs = [...payee, {
|
|
4345
|
+
address: changeAddr,
|
|
4346
|
+
amount: BigInt(changeValue)
|
|
4347
|
+
}];
|
|
4348
|
+
} else {
|
|
4349
|
+
const changeValue = inTotal + extraUtxos.reduce((a, u) => a + u.value, 0) - payee.reduce((a, o) => a + Number(o.amount), 0) - newFee;
|
|
4350
|
+
if (changeValue >= dust) outputs = [...payee, {
|
|
4351
|
+
address: changeAddr,
|
|
4352
|
+
amount: BigInt(changeValue)
|
|
4353
|
+
}];
|
|
4354
|
+
}
|
|
4355
|
+
}
|
|
4356
|
+
const allInputs = [...stash.inputs, ...extraInputs];
|
|
4357
|
+
const { psbt, vsize } = bitcoin.buildExactPsbt(allInputs, outputs, net.params);
|
|
4358
|
+
const record = {
|
|
4359
|
+
inputs: [...btc.inputs, ...extraUtxos.map((u) => ({
|
|
4360
|
+
txid: u.txid,
|
|
4361
|
+
vout: u.vout,
|
|
4362
|
+
value: u.value,
|
|
4363
|
+
address: u.address
|
|
4364
|
+
}))],
|
|
4365
|
+
outputs: outputs.map((o) => ({
|
|
4366
|
+
address: o.address,
|
|
4367
|
+
value: Number(o.amount),
|
|
4368
|
+
own: o.address === changeAddr
|
|
4369
|
+
})),
|
|
4370
|
+
vsize,
|
|
4371
|
+
fee: newFee,
|
|
4372
|
+
feeRate: Math.ceil(newFee / vsize)
|
|
4373
|
+
};
|
|
4374
|
+
return finish(ctx, stash, es, account, old, psbt, record, { replaces: old.id }, verify);
|
|
4375
|
+
};
|
|
4376
|
+
const bumpChild = async (ctx, old, targetRate, verify) => {
|
|
4377
|
+
const stash = pending.get(old.id);
|
|
4378
|
+
const { btc } = old;
|
|
4379
|
+
if (stash === void 0 || btc === void 0) throw new Error("the parent build context is gone — cannot bump after restart");
|
|
4380
|
+
const es = ctx.options.networks.esplora(old.network);
|
|
4381
|
+
const { template } = stash;
|
|
4382
|
+
const account = {
|
|
4383
|
+
kind: "hd",
|
|
4384
|
+
walletId: old.account,
|
|
4385
|
+
groupIndex: 0,
|
|
4386
|
+
template
|
|
4387
|
+
};
|
|
4388
|
+
const own = btc.outputs.flatMap((o, vout) => o.own ? [{
|
|
4389
|
+
...o,
|
|
4390
|
+
vout
|
|
4391
|
+
}] : []);
|
|
4392
|
+
if (own.length === 0) throw new Error("parent has no own outputs to bump");
|
|
4393
|
+
const childInputs = bitcoin.toInputs(own.map((o) => ({
|
|
4394
|
+
txid: old.hash ?? "",
|
|
4395
|
+
vout: o.vout,
|
|
4396
|
+
value: o.value,
|
|
4397
|
+
address: o.address,
|
|
4398
|
+
status: "confirmed",
|
|
4399
|
+
height: 0
|
|
4400
|
+
})), stash.fp, stash.pathOf, stash.pubkeyOf, stash.scriptOf, stash.template);
|
|
4401
|
+
const [changeAddr] = stash.changeAddresses;
|
|
4402
|
+
if (changeAddr === void 0) throw new Error("no unused change address");
|
|
4403
|
+
const net = bitcoinNetwork(old.network);
|
|
4404
|
+
const inTotal = own.reduce((a, o) => a + o.value, 0);
|
|
4405
|
+
const probe = bitcoin.buildExactPsbt(childInputs, [{
|
|
4406
|
+
address: changeAddr,
|
|
4407
|
+
amount: BigInt(inTotal)
|
|
4408
|
+
}], net.params);
|
|
4409
|
+
const childFee = Math.max(Math.ceil(targetRate * (btc.vsize + probe.vsize)) - btc.fee, probe.vsize);
|
|
4410
|
+
const { psbt, vsize } = bitcoin.buildExactPsbt(childInputs, [{
|
|
4411
|
+
address: changeAddr,
|
|
4412
|
+
amount: BigInt(inTotal - childFee)
|
|
4413
|
+
}], net.params);
|
|
4414
|
+
const record = {
|
|
4415
|
+
inputs: own.map((o, i) => ({
|
|
4416
|
+
txid: old.hash ?? "",
|
|
4417
|
+
vout: i,
|
|
4418
|
+
value: o.value,
|
|
4419
|
+
address: o.address
|
|
4420
|
+
})),
|
|
4421
|
+
outputs: [{
|
|
4422
|
+
address: changeAddr,
|
|
4423
|
+
value: inTotal - childFee,
|
|
4424
|
+
own: true
|
|
4425
|
+
}],
|
|
4426
|
+
vsize,
|
|
4427
|
+
fee: childFee,
|
|
4428
|
+
feeRate: Math.ceil(childFee / vsize)
|
|
4429
|
+
};
|
|
4430
|
+
return finish(ctx, stash, es, account, old, psbt, record, { parentId: old.id }, verify);
|
|
4431
|
+
};
|
|
4432
|
+
return {
|
|
4433
|
+
send,
|
|
4434
|
+
poll,
|
|
4435
|
+
replace,
|
|
4436
|
+
bumpChild
|
|
4437
|
+
};
|
|
4438
|
+
};
|
|
4439
|
+
//#endregion
|
|
4440
|
+
//#region src/transactions/evm.ts
|
|
4441
|
+
/** EVM route: native/ERC-20 send build → fee → decode → sign → broadcast, plus status poll. */
|
|
4442
|
+
const bump = (v) => (v * 11n + 9n) / 10n;
|
|
4443
|
+
/** Per-controller route — the adapter's nonce queue and the replace stash are engine state. */
|
|
4444
|
+
const createEvmRoute = () => {
|
|
4445
|
+
const evm = new EvmChainAdapter();
|
|
4446
|
+
/** In-memory build context keyed by tx id — the inputs a replace rebuilds from. */
|
|
4447
|
+
const pending = /* @__PURE__ */ new Map();
|
|
4448
|
+
const send = async (ctx, params) => {
|
|
4449
|
+
const { account, network, to, amount, fee, verify } = params;
|
|
4450
|
+
const { networks } = ctx.options;
|
|
4451
|
+
if (account.kind !== "hd") throw new Error("imported accounts are not yet supported");
|
|
4452
|
+
const info = networks.info(network);
|
|
4453
|
+
const client = networks.client(network);
|
|
4454
|
+
const apis = ctx.options.chains ?? chains(ctx.options.wallet);
|
|
4455
|
+
const evmAccount = await apis.account({
|
|
4456
|
+
walletId: account.walletId,
|
|
4457
|
+
groupIndex: account.groupIndex,
|
|
4458
|
+
template: "evm.bip44",
|
|
4459
|
+
network
|
|
4460
|
+
});
|
|
4461
|
+
const from = "address" in evmAccount ? evmAccount.address : "";
|
|
4462
|
+
const unsigned = await evm.build(client, network, from, params.token === void 0 ? {
|
|
4463
|
+
kind: "native",
|
|
4464
|
+
to,
|
|
4465
|
+
value: amount
|
|
4466
|
+
} : {
|
|
4467
|
+
kind: "erc20",
|
|
4468
|
+
token: params.token,
|
|
4469
|
+
to,
|
|
4470
|
+
value: amount
|
|
4471
|
+
});
|
|
4472
|
+
const quotes = await evm.estimateFee(client);
|
|
4473
|
+
const quote = quotes.find((q) => q.tier === fee) ?? quotes[1];
|
|
4474
|
+
if (quote === void 0) throw new Error("no fee quote");
|
|
4475
|
+
const feed = evm.applyFee(unsigned, quote);
|
|
4476
|
+
const request = evm.toSignRequest(feed, account, info.chainId);
|
|
4477
|
+
const intent = decodeEvm(request);
|
|
4478
|
+
const id = newId(ctx.options.random);
|
|
4479
|
+
pending.set(id, {
|
|
4480
|
+
unsigned: feed,
|
|
4481
|
+
request,
|
|
4482
|
+
quote,
|
|
4483
|
+
chainId: info.chainId
|
|
4484
|
+
});
|
|
4485
|
+
let tx = {
|
|
4486
|
+
id,
|
|
4487
|
+
family: "evm",
|
|
4488
|
+
network,
|
|
4489
|
+
account: account.walletId,
|
|
4490
|
+
hash: void 0,
|
|
4491
|
+
state: "draft",
|
|
4492
|
+
intent,
|
|
4493
|
+
nonce: unsigned.nonce,
|
|
4494
|
+
submittedAt: ctx.options.now(),
|
|
4495
|
+
error: void 0
|
|
4496
|
+
};
|
|
4497
|
+
tx = await ctx.transition(tx, {});
|
|
4498
|
+
let signed;
|
|
4499
|
+
try {
|
|
4500
|
+
const result = await apis.sign({
|
|
4501
|
+
request,
|
|
4502
|
+
verify
|
|
4503
|
+
});
|
|
4504
|
+
tx = await ctx.transition(tx, { state: "signed" });
|
|
4505
|
+
signed = result;
|
|
4506
|
+
} catch (error) {
|
|
4507
|
+
await ctx.transition(tx, {
|
|
4508
|
+
state: "rejected",
|
|
4509
|
+
error: errorMessage(error)
|
|
4510
|
+
});
|
|
4511
|
+
throw error;
|
|
4512
|
+
}
|
|
4513
|
+
try {
|
|
4514
|
+
const raw = evm.finalize(feed, signed);
|
|
4515
|
+
const hash = await evm.broadcast(client, raw);
|
|
4516
|
+
tx = await ctx.transition(tx, {
|
|
4517
|
+
state: "submitted",
|
|
4518
|
+
hash,
|
|
4519
|
+
submittedAt: ctx.options.now()
|
|
4520
|
+
});
|
|
4521
|
+
ctx.track(tx.id, hash);
|
|
4522
|
+
return tx;
|
|
4523
|
+
} catch (error) {
|
|
4524
|
+
await ctx.transition(tx, {
|
|
4525
|
+
state: "failed",
|
|
4526
|
+
error: errorMessage(error)
|
|
4527
|
+
});
|
|
4528
|
+
throw error;
|
|
4529
|
+
}
|
|
4530
|
+
};
|
|
4531
|
+
/**
|
|
4532
|
+
* EVM speed-up/cancel — replaces a pending tx at the same nonce with a bumped fee (cancel = a
|
|
4533
|
+
* 0-value transfer to self). The new tx carries `replaces` and needs a fresh signature; a
|
|
4534
|
+
* rejected one never reaches the network and leaves the original.
|
|
4535
|
+
*/
|
|
4536
|
+
const replace = async (ctx, old, mode, verify) => {
|
|
4537
|
+
const stash = pending.get(old.id);
|
|
4538
|
+
if (stash === void 0) throw new Error("the original build context is gone — cannot replace after restart");
|
|
4539
|
+
const { networks } = ctx.options;
|
|
4540
|
+
const client = networks.client(old.network);
|
|
4541
|
+
const apis = ctx.options.chains ?? chains(ctx.options.wallet);
|
|
4542
|
+
const { tier } = stash.quote;
|
|
4543
|
+
const estimates = await evm.estimateFee(client);
|
|
4544
|
+
const estimate = estimates.find((q) => q.tier === tier) ?? estimates[1];
|
|
4545
|
+
if (estimate === void 0) throw new Error("no fee quote");
|
|
4546
|
+
const oldFee = stash.unsigned;
|
|
4547
|
+
const bumpedFee = bump(oldFee.maxFeePerGas ?? oldFee.gasPrice ?? 0n);
|
|
4548
|
+
const maxFeePerGas = bumpedFee > estimate.maxFeePerGas ? bumpedFee : estimate.maxFeePerGas;
|
|
4549
|
+
const priorityBump = oldFee.maxPriorityFeePerGas === void 0 && estimate.maxPriorityFeePerGas === void 0 ? void 0 : bump(oldFee.maxPriorityFeePerGas ?? 0n) > (estimate.maxPriorityFeePerGas ?? 0n) ? bump(oldFee.maxPriorityFeePerGas ?? 0n) : estimate.maxPriorityFeePerGas ?? 0n;
|
|
4550
|
+
const feeFields = priorityBump === void 0 ? { maxFeePerGas } : {
|
|
4551
|
+
maxFeePerGas,
|
|
4552
|
+
maxPriorityFeePerGas: priorityBump
|
|
4553
|
+
};
|
|
4554
|
+
const unsigned = mode === "cancel" ? {
|
|
4555
|
+
...stash.unsigned,
|
|
4556
|
+
to: stash.unsigned.from,
|
|
4557
|
+
value: 0n,
|
|
4558
|
+
data: "0x",
|
|
4559
|
+
gas: 21000n,
|
|
4560
|
+
...feeFields
|
|
4561
|
+
} : {
|
|
4562
|
+
...stash.unsigned,
|
|
4563
|
+
...feeFields
|
|
4564
|
+
};
|
|
4565
|
+
const request = evm.toSignRequest(unsigned, stash.request.account, stash.chainId);
|
|
4566
|
+
const intent = decodeEvm(request);
|
|
4567
|
+
const id = newId(ctx.options.random);
|
|
4568
|
+
pending.set(id, {
|
|
4569
|
+
unsigned,
|
|
4570
|
+
request,
|
|
4571
|
+
quote: stash.quote,
|
|
4572
|
+
chainId: stash.chainId
|
|
4573
|
+
});
|
|
4574
|
+
let tx = {
|
|
4575
|
+
id,
|
|
4576
|
+
family: "evm",
|
|
4577
|
+
network: old.network,
|
|
4578
|
+
account: old.account,
|
|
4579
|
+
hash: void 0,
|
|
4580
|
+
state: "draft",
|
|
4581
|
+
intent,
|
|
4582
|
+
nonce: old.nonce,
|
|
4583
|
+
replaces: old.id,
|
|
4584
|
+
submittedAt: ctx.options.now(),
|
|
4585
|
+
error: void 0
|
|
4586
|
+
};
|
|
4587
|
+
tx = await ctx.transition(tx, {});
|
|
4588
|
+
let signed;
|
|
4589
|
+
try {
|
|
4590
|
+
const result = await apis.sign({
|
|
4591
|
+
request,
|
|
4592
|
+
verify
|
|
4593
|
+
});
|
|
4594
|
+
tx = await ctx.transition(tx, { state: "signed" });
|
|
4595
|
+
signed = result;
|
|
4596
|
+
} catch (error) {
|
|
4597
|
+
await ctx.transition(tx, {
|
|
4598
|
+
state: "rejected",
|
|
4599
|
+
error: errorMessage(error)
|
|
4600
|
+
});
|
|
4601
|
+
throw error;
|
|
4602
|
+
}
|
|
4603
|
+
try {
|
|
4604
|
+
const raw = evm.finalize(unsigned, signed);
|
|
4605
|
+
const hash = await evm.broadcast(client, raw);
|
|
4606
|
+
tx = await ctx.transition(tx, {
|
|
4607
|
+
state: "submitted",
|
|
4608
|
+
hash,
|
|
4609
|
+
submittedAt: ctx.options.now()
|
|
4610
|
+
});
|
|
4611
|
+
ctx.track(tx.id, hash);
|
|
4612
|
+
return tx;
|
|
4613
|
+
} catch (error) {
|
|
4614
|
+
await ctx.transition(tx, {
|
|
4615
|
+
state: "failed",
|
|
4616
|
+
error: errorMessage(error)
|
|
4617
|
+
});
|
|
4618
|
+
throw error;
|
|
4619
|
+
}
|
|
4620
|
+
};
|
|
4621
|
+
/** Returns the terminal state once the receipt lands, or `undefined` to keep polling. */
|
|
4622
|
+
const poll = async (ctx, tx) => {
|
|
4623
|
+
if (tx.hash === void 0) return "failed";
|
|
4624
|
+
const state = await evm.status(ctx.options.networks.client(tx.network), tx.hash);
|
|
4625
|
+
if (state === "pending") return;
|
|
4626
|
+
pending.delete(tx.id);
|
|
4627
|
+
return state === "finalized" ? "confirmed" : state;
|
|
4628
|
+
};
|
|
4629
|
+
return {
|
|
4630
|
+
send,
|
|
4631
|
+
poll,
|
|
4632
|
+
replace
|
|
4633
|
+
};
|
|
4634
|
+
};
|
|
4635
|
+
PROGRAM_IDS.token2022;
|
|
4636
|
+
const MAX_MESSAGE_BYTES = 1232;
|
|
4637
|
+
const CU_SIMULATE_LIMIT = 14e5;
|
|
4638
|
+
const b64 = (bytes) => base64.encode(bytes);
|
|
4639
|
+
const isObject$2 = (v) => typeof v === "object" && v !== null;
|
|
4640
|
+
const obj$1 = (v) => isObject$2(v) ? v : void 0;
|
|
4641
|
+
const str = (v, fallback = "0") => {
|
|
4642
|
+
if (typeof v === "string") return v;
|
|
4643
|
+
return typeof v === "number" || typeof v === "bigint" ? String(v) : fallback;
|
|
4644
|
+
};
|
|
4645
|
+
var SolanaChainAdapter = class {
|
|
4646
|
+
async rpc(rpc, method, params) {
|
|
4647
|
+
return rpc.call(method, params);
|
|
4648
|
+
}
|
|
4649
|
+
async instructions(rpc, from, intent) {
|
|
4650
|
+
if (intent.kind === "sol") return [getTransferSolInstruction({
|
|
4651
|
+
source: createNoopSigner(address(from)),
|
|
4652
|
+
destination: address(intent.to),
|
|
4653
|
+
amount: intent.lamports
|
|
4654
|
+
})];
|
|
4655
|
+
const tokenProgram = intent.tokenProgram === void 0 ? TOKEN_PROGRAM_ADDRESS : address(intent.tokenProgram);
|
|
4656
|
+
const [sourceAta] = await findAssociatedTokenPda({
|
|
4657
|
+
mint: address(intent.mint),
|
|
4658
|
+
owner: address(from),
|
|
4659
|
+
tokenProgram
|
|
4660
|
+
});
|
|
4661
|
+
const [destAta] = await findAssociatedTokenPda({
|
|
4662
|
+
mint: address(intent.mint),
|
|
4663
|
+
owner: address(intent.to),
|
|
4664
|
+
tokenProgram
|
|
4665
|
+
});
|
|
4666
|
+
const ixs = [];
|
|
4667
|
+
const destInfo = await this.rpc(rpc, "getAccountInfo", [destAta, {
|
|
4668
|
+
encoding: "base64",
|
|
4669
|
+
commitment: "confirmed"
|
|
4670
|
+
}]);
|
|
4671
|
+
if (obj$1(obj$1(destInfo)?.["value"]) === void 0) ixs.push(getCreateAssociatedTokenIdempotentInstruction({
|
|
4672
|
+
ata: destAta,
|
|
4673
|
+
mint: address(intent.mint),
|
|
4674
|
+
owner: address(intent.to),
|
|
4675
|
+
payer: createNoopSigner(address(from)),
|
|
4676
|
+
tokenProgram
|
|
4677
|
+
}));
|
|
4678
|
+
ixs.push(getTransferCheckedInstruction({
|
|
4679
|
+
source: sourceAta,
|
|
4680
|
+
mint: address(intent.mint),
|
|
4681
|
+
destination: destAta,
|
|
4682
|
+
authority: address(from),
|
|
4683
|
+
amount: intent.amount,
|
|
4684
|
+
decimals: intent.decimals
|
|
4685
|
+
}, { programAddress: tokenProgram }));
|
|
4686
|
+
return ixs;
|
|
4687
|
+
}
|
|
4688
|
+
/** Instruction set, simulated compute units, and a fresh blockhash — fee fields come later. */
|
|
4689
|
+
async build(rpc, network, from, intent) {
|
|
4690
|
+
const instructions = await this.instructions(rpc, from, intent);
|
|
4691
|
+
const hashResult = obj$1(await this.rpc(rpc, "getLatestBlockhash", [{ commitment: "confirmed" }]));
|
|
4692
|
+
const value = obj$1(hashResult?.["value"]);
|
|
4693
|
+
const blockhashValue = typeof value?.["blockhash"] === "string" ? value["blockhash"] : "";
|
|
4694
|
+
const lastValidBlockHeight = BigInt(str(value?.["lastValidBlockHeight"], "0"));
|
|
4695
|
+
const compiled = this.compile(from, instructions, blockhashValue, lastValidBlockHeight);
|
|
4696
|
+
const units = await this.simulate(rpc, compiled.messageBytes);
|
|
4697
|
+
return {
|
|
4698
|
+
family: "solana",
|
|
4699
|
+
network,
|
|
4700
|
+
from,
|
|
4701
|
+
instructions,
|
|
4702
|
+
writable: this.writableAccounts(instructions),
|
|
4703
|
+
unitsConsumed: units,
|
|
4704
|
+
blockhash: blockhashValue,
|
|
4705
|
+
lastValidBlockHeight
|
|
4706
|
+
};
|
|
4707
|
+
}
|
|
4708
|
+
writableAccounts(instructions) {
|
|
4709
|
+
const out = /* @__PURE__ */ new Set();
|
|
4710
|
+
for (const ix of instructions) {
|
|
4711
|
+
const accounts = obj$1(ix)?.["accounts"];
|
|
4712
|
+
if (!Array.isArray(accounts)) continue;
|
|
4713
|
+
for (const a of accounts) {
|
|
4714
|
+
const record = obj$1(a);
|
|
4715
|
+
const role = Number(record?.["role"] ?? 0);
|
|
4716
|
+
if (typeof record?.["address"] === "string" && role >= 2) out.add(record["address"]);
|
|
4717
|
+
}
|
|
4718
|
+
}
|
|
4719
|
+
return [...out];
|
|
4720
|
+
}
|
|
4721
|
+
/** Unsigned wire bytes: shortvec(1) sig count + 64-byte zero sig + message (for simulate). */
|
|
4722
|
+
wireBytes(messageBytes) {
|
|
4723
|
+
const out = new Uint8Array(65 + messageBytes.length);
|
|
4724
|
+
out[0] = 1;
|
|
4725
|
+
out.set(messageBytes, 65);
|
|
4726
|
+
return out;
|
|
4727
|
+
}
|
|
4728
|
+
async simulate(rpc, messageBytes) {
|
|
4729
|
+
const result = obj$1(await this.rpc(rpc, "simulateTransaction", [b64(this.wireBytes(messageBytes)), {
|
|
4730
|
+
encoding: "base64",
|
|
4731
|
+
sigVerify: false,
|
|
4732
|
+
replaceRecentBlockhash: false
|
|
4733
|
+
}]));
|
|
4734
|
+
const value = obj$1(result?.["value"]);
|
|
4735
|
+
const units = Number(value?.["unitsConsumed"] ?? 0);
|
|
4736
|
+
return units > 0 ? units : CU_SIMULATE_LIMIT;
|
|
4737
|
+
}
|
|
4738
|
+
compile(from, instructions, blockhashValue, lastValidBlockHeight) {
|
|
4739
|
+
const message = createTransactionMessage({ version: 0 });
|
|
4740
|
+
const withPayer = setTransactionMessageFeePayer(address(from), message);
|
|
4741
|
+
const withLifetime = setTransactionMessageLifetimeUsingBlockhash({
|
|
4742
|
+
blockhash: blockhash(blockhashValue),
|
|
4743
|
+
lastValidBlockHeight
|
|
4744
|
+
}, withPayer);
|
|
4745
|
+
const withIxs = appendTransactionMessageInstructions([...instructions], withLifetime);
|
|
4746
|
+
const compiled = compileTransactionMessage(withIxs);
|
|
4747
|
+
const messageBytes = new Uint8Array(getCompiledTransactionMessageEncoder().encode(compiled));
|
|
4748
|
+
if (messageBytes.length > MAX_MESSAGE_BYTES) throw new Error(`compiled message is ${messageBytes.length} bytes (max ${MAX_MESSAGE_BYTES})`);
|
|
4749
|
+
return {
|
|
4750
|
+
messageBytes,
|
|
4751
|
+
instructions
|
|
4752
|
+
};
|
|
4753
|
+
}
|
|
4754
|
+
/** Percentile-tiered prioritization fees over the writable accounts of this transaction. */
|
|
4755
|
+
async estimateFee(rpc, writable) {
|
|
4756
|
+
const result = await this.rpc(rpc, "getRecentPrioritizationFees", [[...writable]]);
|
|
4757
|
+
const fees = (Array.isArray(result) ? result : []).flatMap((r) => {
|
|
4758
|
+
const fee = obj$1(r)?.["prioritizationFee"];
|
|
4759
|
+
return fee === void 0 ? [] : [Number(fee)];
|
|
4760
|
+
}).sort((a, b) => a - b);
|
|
4761
|
+
const pick = (p) => BigInt(fees[Math.min(fees.length - 1, Math.floor(fees.length * p / 100))] ?? 0);
|
|
4762
|
+
return [
|
|
4763
|
+
{
|
|
4764
|
+
tier: "slow",
|
|
4765
|
+
microLamportsPerCu: pick(50)
|
|
4766
|
+
},
|
|
4767
|
+
{
|
|
4768
|
+
tier: "normal",
|
|
4769
|
+
microLamportsPerCu: pick(75)
|
|
4770
|
+
},
|
|
4771
|
+
{
|
|
4772
|
+
tier: "fast",
|
|
4773
|
+
microLamportsPerCu: pick(90)
|
|
4774
|
+
}
|
|
4775
|
+
];
|
|
4776
|
+
}
|
|
4777
|
+
/** Prepend CU-limit + CU-price budget instructions; returns message bytes for the sign request. */
|
|
4778
|
+
applyFee(tx, fee) {
|
|
4779
|
+
const units = Math.ceil((tx.unitsConsumed === 0 ? CU_SIMULATE_LIMIT : tx.unitsConsumed) * 1.1);
|
|
4780
|
+
return this.compile(tx.from, [
|
|
4781
|
+
getSetComputeUnitLimitInstruction({ units }),
|
|
4782
|
+
getSetComputeUnitPriceInstruction({ microLamports: fee.microLamportsPerCu }),
|
|
4783
|
+
...tx.instructions
|
|
4784
|
+
], tx.blockhash, tx.lastValidBlockHeight).messageBytes;
|
|
4785
|
+
}
|
|
4786
|
+
/** Wire transaction bytes — sigCount shortvec + each 64-byte sig + message. */
|
|
4787
|
+
finalize(messageBytes, signatures) {
|
|
4788
|
+
for (const sig of signatures) if (sig.length !== 64) throw new Error(`solana signature must be 64 bytes, got ${sig.length}`);
|
|
4789
|
+
const out = new Uint8Array(1 + 64 * signatures.length + messageBytes.length);
|
|
4790
|
+
out[0] = signatures.length;
|
|
4791
|
+
for (const [i, sig] of signatures.entries()) out.set(sig, 1 + 64 * i);
|
|
4792
|
+
out.set(messageBytes, 1 + 64 * signatures.length);
|
|
4793
|
+
return out;
|
|
4794
|
+
}
|
|
4795
|
+
async broadcast(rpc, wireBytes) {
|
|
4796
|
+
const result = await this.rpc(rpc, "sendTransaction", [b64(wireBytes), {
|
|
4797
|
+
encoding: "base64",
|
|
4798
|
+
skipPreflight: false,
|
|
4799
|
+
maxRetries: 0,
|
|
4800
|
+
preflightCommitment: "confirmed"
|
|
4801
|
+
}]);
|
|
4802
|
+
if (typeof result !== "string") throw new TypeError("sendTransaction returned a malformed signature");
|
|
4803
|
+
return result;
|
|
4804
|
+
}
|
|
4805
|
+
/** Processed → confirmed → finalized; an error on the status means the tx failed on-chain. */
|
|
4806
|
+
async status(rpc, signature) {
|
|
4807
|
+
const result = obj$1(await this.rpc(rpc, "getSignatureStatuses", [[signature], { searchTransactionHistory: true }]));
|
|
4808
|
+
const entry = Array.isArray(result?.["value"]) ? obj$1(result["value"][0]) : void 0;
|
|
4809
|
+
if (entry === void 0) return "pending";
|
|
4810
|
+
if (entry["err"] !== null && entry["err"] !== void 0) return "failed";
|
|
4811
|
+
const c = entry["confirmationStatus"];
|
|
4812
|
+
return c === "finalized" ? "finalized" : c === "confirmed" ? "confirmed" : "pending";
|
|
4813
|
+
}
|
|
4814
|
+
async blockHeight(rpc) {
|
|
4815
|
+
const result = await this.rpc(rpc, "getBlockHeight", [{ commitment: "confirmed" }]);
|
|
4816
|
+
return BigInt(str(result, "0"));
|
|
4817
|
+
}
|
|
4818
|
+
};
|
|
4819
|
+
//#endregion
|
|
4820
|
+
//#region src/transactions/solana.ts
|
|
4821
|
+
/** Solana route: SOL/SPL send → sign → broadcast, plus status poll with rebroadcast/drop. */
|
|
4822
|
+
const isObject$1 = (v) => typeof v === "object" && v !== null;
|
|
4823
|
+
const obj = (v) => isObject$1(v) ? v : void 0;
|
|
4824
|
+
const arr = (v) => Array.isArray(v) ? v : void 0;
|
|
4825
|
+
const solana = new SolanaChainAdapter();
|
|
4826
|
+
/** Signed wire transactions kept for re-broadcast until the tx confirms or drops. */
|
|
4827
|
+
const wires = /* @__PURE__ */ new Map();
|
|
4828
|
+
const sendSolana = async (ctx, params) => {
|
|
4829
|
+
const { account, network, to, amount, fee, verify } = params;
|
|
4830
|
+
if (account.kind !== "hd") throw new Error("imported accounts are not yet supported");
|
|
4831
|
+
const rpc = ctx.options.networks.solanaRpc(network);
|
|
4832
|
+
const apis = ctx.options.chains ?? chains(ctx.options.wallet);
|
|
4833
|
+
const solAccount = await apis.account({
|
|
4834
|
+
walletId: account.walletId,
|
|
4835
|
+
groupIndex: account.groupIndex,
|
|
4836
|
+
template: "solana.bip44-change",
|
|
4837
|
+
network
|
|
4838
|
+
});
|
|
4839
|
+
const from = "address" in solAccount ? solAccount.address : "";
|
|
4840
|
+
let tokenProgram;
|
|
4841
|
+
let ext;
|
|
4842
|
+
if (params.token !== void 0) {
|
|
4843
|
+
const mintInfo = await rpc.call("getAccountInfo", [params.token, {
|
|
4844
|
+
encoding: "base64",
|
|
4845
|
+
commitment: "confirmed"
|
|
4846
|
+
}]);
|
|
4847
|
+
const owner = obj(mintInfo)?.["value"];
|
|
4848
|
+
if (obj(owner)?.["owner"] === PROGRAM_IDS.token2022) {
|
|
4849
|
+
tokenProgram = PROGRAM_IDS.token2022;
|
|
4850
|
+
const mintBytes = arr(obj(owner)?.["data"])?.[0];
|
|
4851
|
+
const raw = typeof mintBytes === "string" ? base64.decode(mintBytes) : void 0;
|
|
4852
|
+
if (raw !== void 0 && raw.length > 166) {
|
|
4853
|
+
const epoch = obj(await rpc.call("getEpochInfo", []))?.["epoch"];
|
|
4854
|
+
ext = parseMintExtensions(raw, BigInt(typeof epoch === "number" ? epoch : 0));
|
|
4855
|
+
if (ext.nonTransferable === true) throw new Error("token is non-transferable");
|
|
4856
|
+
if (ext.confidential === true) throw new Error("confidential transfer extensions are not supported");
|
|
4857
|
+
}
|
|
4858
|
+
}
|
|
4859
|
+
}
|
|
4860
|
+
const unsigned = await solana.build(rpc, network, from, params.token === void 0 ? {
|
|
4861
|
+
kind: "sol",
|
|
4862
|
+
to,
|
|
4863
|
+
lamports: amount
|
|
4864
|
+
} : {
|
|
4865
|
+
kind: "spl",
|
|
4866
|
+
mint: params.token,
|
|
4867
|
+
to,
|
|
4868
|
+
amount,
|
|
4869
|
+
decimals: params.decimals ?? 0,
|
|
4870
|
+
...tokenProgram === void 0 ? {} : { tokenProgram }
|
|
4871
|
+
});
|
|
4872
|
+
const quotes = await solana.estimateFee(rpc, unsigned.writable);
|
|
4873
|
+
const quote = quotes.find((q) => q.tier === fee) ?? quotes[1];
|
|
4874
|
+
if (quote === void 0) throw new Error("no fee quote");
|
|
4875
|
+
const messageBytes = solana.applyFee(unsigned, quote);
|
|
4876
|
+
const request = {
|
|
4877
|
+
kind: "solana.transaction",
|
|
4878
|
+
network,
|
|
4879
|
+
account,
|
|
4880
|
+
message: messageBytes
|
|
4881
|
+
};
|
|
4882
|
+
const intent = ext === void 0 || params.token === void 0 ? decodeSolana(request, from) : token2022Intent(decodeSolana(request, from), ext, params.token, amount);
|
|
4883
|
+
const id = newId(ctx.options.random);
|
|
4884
|
+
let tx = {
|
|
4885
|
+
id,
|
|
4886
|
+
family: "solana",
|
|
4887
|
+
network,
|
|
4888
|
+
account: account.walletId,
|
|
4889
|
+
hash: void 0,
|
|
4890
|
+
state: "draft",
|
|
4891
|
+
intent,
|
|
4892
|
+
nonce: 0,
|
|
4893
|
+
lastValidBlockHeight: Number(unsigned.lastValidBlockHeight),
|
|
4894
|
+
submittedAt: ctx.options.now(),
|
|
4895
|
+
error: void 0
|
|
4896
|
+
};
|
|
4897
|
+
tx = await ctx.transition(tx, {});
|
|
4898
|
+
try {
|
|
4899
|
+
const result = await apis.sign({
|
|
4900
|
+
request,
|
|
4901
|
+
verify
|
|
4902
|
+
});
|
|
4903
|
+
tx = await ctx.transition(tx, { state: "signed" });
|
|
4904
|
+
if (result.kind !== "solana.transaction" || !(result.signature instanceof Uint8Array)) throw new Error("unexpected solana sign result");
|
|
4905
|
+
const wire = solana.finalize(messageBytes, [result.signature]);
|
|
4906
|
+
const hash = await solana.broadcast(rpc, wire);
|
|
4907
|
+
wires.set(id, wire);
|
|
4908
|
+
tx = await ctx.transition(tx, {
|
|
4909
|
+
state: "submitted",
|
|
4910
|
+
hash,
|
|
4911
|
+
submittedAt: ctx.options.now()
|
|
4912
|
+
});
|
|
4913
|
+
ctx.track(tx.id, hash);
|
|
4914
|
+
return tx;
|
|
4915
|
+
} catch (error) {
|
|
4916
|
+
const failed = tx.state === "signed" ? "failed" : "rejected";
|
|
4917
|
+
await ctx.transition(tx, {
|
|
4918
|
+
state: failed,
|
|
4919
|
+
error: errorMessage(error)
|
|
4920
|
+
});
|
|
4921
|
+
throw error;
|
|
4922
|
+
}
|
|
4923
|
+
};
|
|
4924
|
+
/** Returns the terminal state, `dropped` on blockhash expiry, or `undefined` to keep polling. */
|
|
4925
|
+
const pollSolana = async (ctx, tx) => {
|
|
4926
|
+
if (tx.hash === void 0) return "failed";
|
|
4927
|
+
const rpc = ctx.options.networks.solanaRpc(tx.network);
|
|
4928
|
+
const state = await solana.status(rpc, tx.hash);
|
|
4929
|
+
if (state !== "pending") {
|
|
4930
|
+
wires.delete(tx.id);
|
|
4931
|
+
return state === "finalized" ? "confirmed" : state;
|
|
4932
|
+
}
|
|
4933
|
+
if (tx.lastValidBlockHeight !== void 0 && await solana.blockHeight(rpc) > BigInt(tx.lastValidBlockHeight)) {
|
|
4934
|
+
wires.delete(tx.id);
|
|
4935
|
+
return "dropped";
|
|
4936
|
+
}
|
|
4937
|
+
const wire = wires.get(tx.id);
|
|
4938
|
+
if (wire !== void 0) await solana.broadcast(rpc, wire).catch(() => "");
|
|
4939
|
+
};
|
|
4940
|
+
/**
|
|
4941
|
+
* Folds Token-2022 mint extensions into the decoded intent: a TransferFee mint shows the
|
|
4942
|
+
* recipient's net amount (amount − min(amount×bps/10000, maximum)); a TransferHook or
|
|
4943
|
+
* PermanentDelegate marks the intent `partial` with a warning. The mint's extension data is not
|
|
4944
|
+
* part of the message, so the route attaches what it fetched — decode alone cannot see it.
|
|
4945
|
+
*/
|
|
4946
|
+
const token2022Intent = (intent, ext, mint, amount) => {
|
|
4947
|
+
const warnings = [...intent.warnings];
|
|
4948
|
+
let { outflows } = intent;
|
|
4949
|
+
if (ext.transferFee !== void 0) {
|
|
4950
|
+
const net = netAfterTransferFee(amount, ext.transferFee.basisPoints, ext.transferFee.maximumFee);
|
|
4951
|
+
const withheld = amount - net;
|
|
4952
|
+
warnings.push({
|
|
4953
|
+
kind: "token-2022-transfer-fee",
|
|
4954
|
+
message: `transfer fee withheld ${withheld.toString(10)} base units`
|
|
4955
|
+
});
|
|
4956
|
+
outflows = outflows.map((o) => o.amount.asset === mint ? {
|
|
4957
|
+
...o,
|
|
4958
|
+
amount: {
|
|
4959
|
+
...o.amount,
|
|
4960
|
+
amount: net.toString(10)
|
|
4961
|
+
}
|
|
4962
|
+
} : o);
|
|
4963
|
+
}
|
|
4964
|
+
if (ext.transferHook !== void 0 || ext.permanentDelegate !== void 0) {
|
|
4965
|
+
warnings.push({
|
|
4966
|
+
kind: "token-2022-extension",
|
|
4967
|
+
message: "mint carries a transfer-hook or permanent-delegate extension"
|
|
4968
|
+
});
|
|
4969
|
+
return {
|
|
4970
|
+
...intent,
|
|
4971
|
+
outflows,
|
|
4972
|
+
warnings,
|
|
4973
|
+
verifiability: "partial"
|
|
4974
|
+
};
|
|
4975
|
+
}
|
|
4976
|
+
return {
|
|
4977
|
+
...intent,
|
|
4978
|
+
outflows,
|
|
4979
|
+
warnings
|
|
4980
|
+
};
|
|
4981
|
+
};
|
|
4982
|
+
//#endregion
|
|
4983
|
+
//#region src/transactions/controller.ts
|
|
4984
|
+
/**
|
|
4985
|
+
* Transaction controller: orchestrates lifecycle, persistence and polling; each chain family routes
|
|
4986
|
+
* through its own module (`evm.ts`, `solana.ts`, `bitcoin.ts`). A rejected signature (wrong
|
|
4987
|
+
* password, cancelled presence) never broadcasts.
|
|
4988
|
+
*/
|
|
4989
|
+
const encode = (tx) => new TextEncoder().encode(JSON.stringify(tx));
|
|
4990
|
+
const isObject = (v) => typeof v === "object" && v !== null;
|
|
4991
|
+
const TX_STATES = /* @__PURE__ */ new Set([
|
|
4992
|
+
"draft",
|
|
4993
|
+
"signed",
|
|
4994
|
+
"submitted",
|
|
4995
|
+
"pending",
|
|
4996
|
+
"confirmed",
|
|
4997
|
+
"failed",
|
|
4998
|
+
"rejected",
|
|
4999
|
+
"dropped",
|
|
5000
|
+
"replaced"
|
|
5001
|
+
]);
|
|
5002
|
+
const isTxState = (v) => typeof v === "string" && TX_STATES.has(v);
|
|
5003
|
+
const isBtcRecord = (v) => {
|
|
5004
|
+
if (!isObject(v)) return false;
|
|
5005
|
+
const { inputs, outputs, vsize, fee, feeRate } = v;
|
|
5006
|
+
return Array.isArray(inputs) && Array.isArray(outputs) && typeof vsize === "number" && typeof fee === "number" && typeof feeRate === "number" && inputs.every((i) => isObject(i) && typeof i["txid"] === "string" && typeof i["vout"] === "number" && typeof i["value"] === "number" && typeof i["address"] === "string") && outputs.every((o) => isObject(o) && typeof o["address"] === "string" && typeof o["value"] === "number" && typeof o["own"] === "boolean");
|
|
5007
|
+
};
|
|
5008
|
+
const isIntent = (v) => isObject(v) && typeof v["summary"] === "string" && Array.isArray(v["outflows"]) && typeof v["verifiability"] === "string";
|
|
5009
|
+
const decode = (raw) => {
|
|
5010
|
+
const parsed = JSON.parse(new TextDecoder().decode(raw));
|
|
5011
|
+
if (!isObject(parsed)) return;
|
|
5012
|
+
const { id, family, network, account, hash, state, intent, nonce, replaces, parentId, btc, submittedAt, error } = parsed;
|
|
5013
|
+
if (typeof id !== "string" || family !== "evm" && family !== "solana" && family !== "bitcoin" || typeof network !== "string" || typeof account !== "string" || !isTxState(state) || typeof nonce !== "number" || typeof submittedAt !== "number") return;
|
|
5014
|
+
return {
|
|
5015
|
+
id,
|
|
5016
|
+
family,
|
|
5017
|
+
network,
|
|
5018
|
+
account,
|
|
5019
|
+
hash: typeof hash === "string" ? hash : void 0,
|
|
5020
|
+
state,
|
|
5021
|
+
intent: isIntent(intent) ? intent : void 0,
|
|
5022
|
+
nonce,
|
|
5023
|
+
...typeof replaces === "string" ? { replaces } : {},
|
|
5024
|
+
...typeof parentId === "string" ? { parentId } : {},
|
|
5025
|
+
...isBtcRecord(btc) ? { btc } : {},
|
|
5026
|
+
submittedAt,
|
|
5027
|
+
error: typeof error === "string" ? error : void 0
|
|
5028
|
+
};
|
|
5029
|
+
};
|
|
5030
|
+
var TransactionController = class extends Controller {
|
|
5031
|
+
options;
|
|
5032
|
+
engineStore;
|
|
5033
|
+
timers = /* @__PURE__ */ new Map();
|
|
5034
|
+
evmRoute = createEvmRoute();
|
|
5035
|
+
bitcoinRoute = createBitcoinRoute();
|
|
5036
|
+
locked = true;
|
|
5037
|
+
constructor(options) {
|
|
5038
|
+
super({
|
|
5039
|
+
name: "transactions",
|
|
5040
|
+
defaultState: () => ({ txs: {} })
|
|
5041
|
+
});
|
|
5042
|
+
this.options = options;
|
|
5043
|
+
}
|
|
5044
|
+
async init() {
|
|
5045
|
+
this.locked = false;
|
|
5046
|
+
this.engineStore = await this.options.openStore();
|
|
5047
|
+
const collection = this.engineStore.collection("transactions");
|
|
5048
|
+
const keys = await collection.list();
|
|
5049
|
+
const rows = await Promise.all(keys.map(async (key) => collection.get(key)));
|
|
5050
|
+
for (const raw of rows) {
|
|
5051
|
+
const tx = raw === void 0 ? void 0 : decode(raw);
|
|
5052
|
+
if (tx === void 0) continue;
|
|
5053
|
+
this.update((s) => ({
|
|
5054
|
+
...s,
|
|
5055
|
+
txs: {
|
|
5056
|
+
...s.txs,
|
|
5057
|
+
[tx.id]: tx
|
|
5058
|
+
}
|
|
5059
|
+
}));
|
|
5060
|
+
if ((tx.state === "submitted" || tx.state === "pending") && tx.hash !== void 0) this.track(tx.id, tx.hash);
|
|
5061
|
+
}
|
|
5062
|
+
}
|
|
5063
|
+
async destroy() {
|
|
5064
|
+
this.lock();
|
|
5065
|
+
await Promise.resolve();
|
|
5066
|
+
}
|
|
5067
|
+
lock() {
|
|
5068
|
+
this.locked = true;
|
|
5069
|
+
for (const stop of this.timers.values()) stop();
|
|
5070
|
+
this.timers.clear();
|
|
5071
|
+
this.engineStore = void 0;
|
|
5072
|
+
this.evmRoute = createEvmRoute();
|
|
5073
|
+
}
|
|
5074
|
+
async persist(tx) {
|
|
5075
|
+
const store = this.engineStore;
|
|
5076
|
+
if (store !== void 0) await store.collection("transactions").put(tx.id, encode(tx));
|
|
5077
|
+
}
|
|
5078
|
+
async transition(tx, next) {
|
|
5079
|
+
const updated = {
|
|
5080
|
+
...tx,
|
|
5081
|
+
...next
|
|
5082
|
+
};
|
|
5083
|
+
this.update((s) => ({
|
|
5084
|
+
...s,
|
|
5085
|
+
txs: {
|
|
5086
|
+
...s.txs,
|
|
5087
|
+
[tx.id]: updated
|
|
5088
|
+
}
|
|
5089
|
+
}));
|
|
5090
|
+
await this.persist(updated);
|
|
5091
|
+
return updated;
|
|
5092
|
+
}
|
|
5093
|
+
routeCtx() {
|
|
5094
|
+
return {
|
|
5095
|
+
options: this.options,
|
|
5096
|
+
transition: async (tx, next) => this.transition(tx, next),
|
|
5097
|
+
track: (id, hash) => this.track(id, hash),
|
|
5098
|
+
newId: () => newId(this.options.random)
|
|
5099
|
+
};
|
|
5100
|
+
}
|
|
5101
|
+
async send(params) {
|
|
5102
|
+
if (this.locked) throw new Error("transactions require an unlocked vault");
|
|
5103
|
+
const { account, network } = params;
|
|
5104
|
+
const info = this.options.networks.info(network);
|
|
5105
|
+
if (account.kind !== "hd") throw new Error("imported accounts are not yet supported");
|
|
5106
|
+
const ctx = this.routeCtx();
|
|
5107
|
+
if (info.family === "solana") return sendSolana(ctx, params);
|
|
5108
|
+
if (info.family === "bitcoin") return this.bitcoinRoute.send(ctx, params);
|
|
5109
|
+
return this.evmRoute.send(ctx, params);
|
|
5110
|
+
}
|
|
5111
|
+
/**
|
|
5112
|
+
* Speed up or cancel a pending/submitted EVM transaction at the same nonce — a fresh signature is
|
|
5113
|
+
* required and a rejection leaves the original untouched.
|
|
5114
|
+
*/
|
|
5115
|
+
async replace(id, mode, verify) {
|
|
5116
|
+
if (this.locked) throw new Error("transactions require an unlocked vault");
|
|
5117
|
+
const old = this.getState().txs[id];
|
|
5118
|
+
if (old === void 0) throw new Error(`unknown transaction ${id}`);
|
|
5119
|
+
if (old.family === "bitcoin") return this.bitcoinRoute.replace(this.routeCtx(), old, mode, verify);
|
|
5120
|
+
if (old.family !== "evm") throw new Error("replace is EVM-only in this layer");
|
|
5121
|
+
if (old.state !== "pending" && old.state !== "submitted") throw new Error(`cannot replace a ${old.state} transaction`);
|
|
5122
|
+
return this.evmRoute.replace(this.routeCtx(), old, mode, verify);
|
|
5123
|
+
}
|
|
5124
|
+
/**
|
|
5125
|
+
* CPFP — spend an unconfirmed tx's own outputs into a higher-fee child so the package mines.
|
|
5126
|
+
* Bitcoin only; the child is linked via `parentId`.
|
|
5127
|
+
*/
|
|
5128
|
+
async bumpChild(id, targetRate, verify) {
|
|
5129
|
+
if (this.locked) throw new Error("transactions require an unlocked vault");
|
|
5130
|
+
const old = this.getState().txs[id];
|
|
5131
|
+
if (old === void 0) throw new Error(`unknown transaction ${id}`);
|
|
5132
|
+
if (old.family !== "bitcoin") throw new Error("bumpChild is Bitcoin-only in this layer");
|
|
5133
|
+
if (old.state !== "pending" && old.state !== "submitted") throw new Error(`cannot bump a ${old.state} transaction`);
|
|
5134
|
+
return this.bitcoinRoute.bumpChild(this.routeCtx(), old, targetRate, verify);
|
|
5135
|
+
}
|
|
5136
|
+
track(id, hash) {
|
|
5137
|
+
if (this.timers.has(id) || hash === "") return;
|
|
5138
|
+
const poll = this.options.pollMs ?? 4e3;
|
|
5139
|
+
const stop = this.options.setTimer(poll, async () => {
|
|
5140
|
+
if (this.locked) return;
|
|
5141
|
+
try {
|
|
5142
|
+
const tx = this.getState().txs[id];
|
|
5143
|
+
if (tx === void 0) return;
|
|
5144
|
+
const ctx = this.routeCtx();
|
|
5145
|
+
const next = tx.family === "solana" ? await pollSolana(ctx, tx) : tx.family === "bitcoin" ? await this.bitcoinRoute.poll(ctx, tx) : await this.evmRoute.poll(ctx, tx);
|
|
5146
|
+
if (next === void 0) return;
|
|
5147
|
+
const updated = await this.transition(tx, { state: next });
|
|
5148
|
+
this.timers.delete(id);
|
|
5149
|
+
stop();
|
|
5150
|
+
const siblings = Object.values(this.getState().txs).filter((t) => t.id !== updated.id && t.hash !== void 0 && t.state !== "replaced" && (updated.replaces !== void 0 && t.id === updated.replaces || t.replaces !== void 0 && t.replaces === updated.id) && (t.state === "submitted" || t.state === "pending"));
|
|
5151
|
+
await Promise.all(siblings.map(async (sibling) => {
|
|
5152
|
+
await this.transition(sibling, { state: "replaced" });
|
|
5153
|
+
const siblingStop = this.timers.get(sibling.id);
|
|
5154
|
+
this.timers.delete(sibling.id);
|
|
5155
|
+
siblingStop?.();
|
|
5156
|
+
}));
|
|
5157
|
+
} catch (error) {
|
|
5158
|
+
this.options.logger.warn("tx status poll failed", error);
|
|
5159
|
+
}
|
|
5160
|
+
});
|
|
5161
|
+
this.timers.set(id, stop);
|
|
5162
|
+
}
|
|
5163
|
+
};
|
|
5164
|
+
//#endregion
|
|
3252
5165
|
//#region src/engine.ts
|
|
3253
5166
|
/**
|
|
3254
5167
|
* Composition root: wires the Wallet's lock state to the SparkController's lifecycle. Unlocked →
|
|
@@ -3276,10 +5189,42 @@ function createWalletEngine(options) {
|
|
|
3276
5189
|
fetch
|
|
3277
5190
|
})
|
|
3278
5191
|
});
|
|
5192
|
+
const networks = new NetworkController({
|
|
5193
|
+
fetch: options.fetch,
|
|
5194
|
+
rpc: options.rpc ?? {},
|
|
5195
|
+
openStore: options.openStore,
|
|
5196
|
+
logger: options.logger
|
|
5197
|
+
});
|
|
5198
|
+
const assets = new AssetsController({
|
|
5199
|
+
networks,
|
|
5200
|
+
openStore: options.openStore,
|
|
5201
|
+
setTimer: options.setTimer ?? ((ms, fn) => {
|
|
5202
|
+
const id = setTimeout(() => {
|
|
5203
|
+
fn();
|
|
5204
|
+
}, ms);
|
|
5205
|
+
return () => clearTimeout(id);
|
|
5206
|
+
}),
|
|
5207
|
+
logger: options.logger
|
|
5208
|
+
});
|
|
5209
|
+
const transactions = new TransactionController({
|
|
5210
|
+
wallet: options.wallet,
|
|
5211
|
+
networks,
|
|
5212
|
+
openStore: options.openStore,
|
|
5213
|
+
random: options.random,
|
|
5214
|
+
setTimer: options.setTimer ?? ((ms, fn) => {
|
|
5215
|
+
const id = setTimeout(() => {
|
|
5216
|
+
fn();
|
|
5217
|
+
}, ms);
|
|
5218
|
+
return () => clearTimeout(id);
|
|
5219
|
+
}),
|
|
5220
|
+
now: options.now,
|
|
5221
|
+
logger: options.logger
|
|
5222
|
+
});
|
|
3279
5223
|
const accounts = new AccountTreeController({
|
|
3280
5224
|
wallet: options.wallet,
|
|
3281
5225
|
spark: controller,
|
|
3282
5226
|
sparkNetwork: options.sparkNetwork,
|
|
5227
|
+
networks,
|
|
3283
5228
|
logger: options.logger,
|
|
3284
5229
|
now: options.now
|
|
3285
5230
|
});
|
|
@@ -3313,8 +5258,14 @@ function createWalletEngine(options) {
|
|
|
3313
5258
|
await accounts.init();
|
|
3314
5259
|
await controller.init();
|
|
3315
5260
|
await metaSync.init();
|
|
5261
|
+
await networks.init();
|
|
5262
|
+
await assets.init();
|
|
5263
|
+
await transactions.init();
|
|
3316
5264
|
} else if (!unlocked && started) {
|
|
3317
5265
|
started = false;
|
|
5266
|
+
transactions.lock();
|
|
5267
|
+
assets.lock();
|
|
5268
|
+
networks.lock();
|
|
3318
5269
|
await metaSync.lock();
|
|
3319
5270
|
await accounts.lock();
|
|
3320
5271
|
await controller.lockAll();
|
|
@@ -3348,6 +5299,9 @@ function createWalletEngine(options) {
|
|
|
3348
5299
|
});
|
|
3349
5300
|
schedule();
|
|
3350
5301
|
return {
|
|
5302
|
+
networks,
|
|
5303
|
+
assets,
|
|
5304
|
+
transactions,
|
|
3351
5305
|
spark: controller,
|
|
3352
5306
|
accounts,
|
|
3353
5307
|
metaSync,
|