@anywalletkit/extension-kit 0.1.5 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -3
- package/dist/index.cjs +214 -60
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +80 -11
- package/dist/index.d.ts +80 -11
- package/dist/index.js +214 -60
- package/dist/index.js.map +1 -1
- package/package.json +8 -8
package/dist/index.js
CHANGED
|
@@ -33,6 +33,14 @@ function toArrayBuffer(bytes) {
|
|
|
33
33
|
copy.set(bytes);
|
|
34
34
|
return copy.buffer;
|
|
35
35
|
}
|
|
36
|
+
async function withSecretBuffer(bytes, fn) {
|
|
37
|
+
const buffer = toArrayBuffer(bytes);
|
|
38
|
+
try {
|
|
39
|
+
return await fn(buffer);
|
|
40
|
+
} finally {
|
|
41
|
+
new Uint8Array(buffer).fill(0);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
36
44
|
|
|
37
45
|
// src/vault.ts
|
|
38
46
|
var VAULT_MIN_ITERATIONS = 6e5;
|
|
@@ -300,7 +308,13 @@ var Vault = class {
|
|
|
300
308
|
}
|
|
301
309
|
async _derivePbkdf2(password, salt, iterations) {
|
|
302
310
|
const subtle = this._crypto.subtle;
|
|
303
|
-
const
|
|
311
|
+
const pw = new TextEncoder().encode(password.normalize("NFKC"));
|
|
312
|
+
let material;
|
|
313
|
+
try {
|
|
314
|
+
material = await withSecretBuffer(pw, (b) => subtle.importKey("raw", b, "PBKDF2", false, ["deriveKey"]));
|
|
315
|
+
} finally {
|
|
316
|
+
zeroize(pw);
|
|
317
|
+
}
|
|
304
318
|
return subtle.deriveKey({ name: "PBKDF2", hash: "SHA-256", salt: toArrayBuffer(salt), iterations }, material, { name: "AES-GCM", length: 256 }, false, ["encrypt", "decrypt"]);
|
|
305
319
|
}
|
|
306
320
|
async _deriveScrypt(password, salt, cost) {
|
|
@@ -312,7 +326,7 @@ var Vault = class {
|
|
|
312
326
|
zeroize(pw);
|
|
313
327
|
}
|
|
314
328
|
try {
|
|
315
|
-
return await this._crypto.subtle.importKey("raw",
|
|
329
|
+
return await withSecretBuffer(raw, (b) => this._crypto.subtle.importKey("raw", b, { name: "AES-GCM", length: 256 }, false, ["encrypt", "decrypt"]));
|
|
316
330
|
} finally {
|
|
317
331
|
zeroize(raw);
|
|
318
332
|
}
|
|
@@ -320,16 +334,16 @@ var Vault = class {
|
|
|
320
334
|
/** HKDF-SHA256(ikm = PRF output, salt = credential id bytes, info) → AES-256-GCM key. */
|
|
321
335
|
async _derivePasskeyKek(prf, credentialId) {
|
|
322
336
|
const subtle = this._crypto.subtle;
|
|
323
|
-
const ikm = await subtle.importKey("raw",
|
|
337
|
+
const ikm = await withSecretBuffer(prf, (b) => subtle.importKey("raw", b, "HKDF", false, ["deriveKey"]));
|
|
324
338
|
return subtle.deriveKey({ name: "HKDF", hash: "SHA-256", salt: toArrayBuffer(fromBase64Url(credentialId)), info: toArrayBuffer(new TextEncoder().encode(HKDF_INFO)) }, ikm, { name: "AES-GCM", length: 256 }, false, ["encrypt", "decrypt"]);
|
|
325
339
|
}
|
|
326
340
|
async _importDek(dek) {
|
|
327
341
|
if (dek.length !== DEK_BYTES) throw new VaultError("Vault key must be 32 bytes.", "invalid-input");
|
|
328
|
-
return this._crypto.subtle.importKey("raw",
|
|
342
|
+
return withSecretBuffer(dek, (b) => this._crypto.subtle.importKey("raw", b, { name: "AES-GCM", length: 256 }, false, ["encrypt", "decrypt"]));
|
|
329
343
|
}
|
|
330
344
|
async _seal(key, plaintext, aad) {
|
|
331
345
|
const iv = this._random(IV_BYTES);
|
|
332
|
-
const ct = await this._crypto.subtle.encrypt({ name: "AES-GCM", iv: toArrayBuffer(iv), additionalData: toArrayBuffer(new TextEncoder().encode(aad)) }, key,
|
|
346
|
+
const ct = await withSecretBuffer(plaintext, (b) => this._crypto.subtle.encrypt({ name: "AES-GCM", iv: toArrayBuffer(iv), additionalData: toArrayBuffer(new TextEncoder().encode(aad)) }, key, b));
|
|
333
347
|
return { iv: toBase64(iv), data: toBase64(new Uint8Array(ct)) };
|
|
334
348
|
}
|
|
335
349
|
async _open(key, box, aad, onFail) {
|
|
@@ -514,22 +528,40 @@ var Session = class {
|
|
|
514
528
|
if (was !== void 0 && was !== null) for (const cb of this._onLock) cb();
|
|
515
529
|
}
|
|
516
530
|
_expiry() {
|
|
517
|
-
return this._autoLockMs > 0 ? this._now() + this._autoLockMs :
|
|
531
|
+
return this._autoLockMs > 0 ? this._now() + this._autoLockMs : null;
|
|
518
532
|
}
|
|
519
533
|
async _arm(expiresAt) {
|
|
520
|
-
if (
|
|
534
|
+
if (expiresAt !== null) await this._alarms.schedule(this._alarm, expiresAt);
|
|
521
535
|
else await this._alarms.clear(this._alarm);
|
|
522
536
|
}
|
|
537
|
+
/** The stored record, or `null` when locked. A record that is there but unreadable locks the wallet (removed, listeners told). */
|
|
523
538
|
async _read() {
|
|
524
539
|
const raw = await this._storage.get(this._key);
|
|
525
|
-
if (
|
|
526
|
-
|
|
540
|
+
if (raw === void 0 || raw === null) return null;
|
|
541
|
+
const r = raw;
|
|
542
|
+
const expiryOk = r.expiresAt === null || typeof r.expiresAt === "number" && Number.isFinite(r.expiresAt);
|
|
543
|
+
if (typeof raw !== "object" || typeof r.key !== "string" || !expiryOk || !isKey(r.key)) {
|
|
527
544
|
await this._lockNow();
|
|
528
545
|
return null;
|
|
529
546
|
}
|
|
530
|
-
|
|
547
|
+
if (r.expiresAt !== null && r.expiresAt <= this._now()) {
|
|
548
|
+
await this._lockNow();
|
|
549
|
+
return null;
|
|
550
|
+
}
|
|
551
|
+
return r;
|
|
531
552
|
}
|
|
532
553
|
};
|
|
554
|
+
function isKey(text) {
|
|
555
|
+
let bytes;
|
|
556
|
+
try {
|
|
557
|
+
bytes = fromBase64(text);
|
|
558
|
+
} catch {
|
|
559
|
+
return false;
|
|
560
|
+
}
|
|
561
|
+
const ok = bytes.length === 32;
|
|
562
|
+
zeroize2(bytes);
|
|
563
|
+
return ok;
|
|
564
|
+
}
|
|
533
565
|
function memorySessionStorage() {
|
|
534
566
|
const data = /* @__PURE__ */ new Map();
|
|
535
567
|
return {
|
|
@@ -838,14 +870,27 @@ var NetworksTable = class {
|
|
|
838
870
|
return this._loading;
|
|
839
871
|
}
|
|
840
872
|
};
|
|
873
|
+
function isPrivateRpcHost(url) {
|
|
874
|
+
const m = /^https:\/\/(?:[^@/?#]*@)?(\[[^\]]+\]|[^:/?#]+)/i.exec(url);
|
|
875
|
+
if (!m) return true;
|
|
876
|
+
const h = m[1].toLowerCase().replace(/^\[|\]$/g, "").replace(/\.$/, "");
|
|
877
|
+
if (h === "localhost" || h.endsWith(".localhost") || h.endsWith(".local") || h === "0.0.0.0") return true;
|
|
878
|
+
const v4 = /^(\d{1,3})\.(\d{1,3})\.\d{1,3}\.\d{1,3}$/.exec(h);
|
|
879
|
+
if (v4) {
|
|
880
|
+
const a = Number(v4[1]), b = Number(v4[2]);
|
|
881
|
+
return a === 127 || a === 10 || a === 0 || a === 172 && b >= 16 && b <= 31 || a === 192 && b === 168 || a === 169 && b === 254;
|
|
882
|
+
}
|
|
883
|
+
if (h.includes(":")) return h === "::1" || h === "::" || /^f[cd]/.test(h) || /^fe[89ab]/.test(h);
|
|
884
|
+
return false;
|
|
885
|
+
}
|
|
841
886
|
function parseAddChainParams(params, known = isCatalogChain) {
|
|
842
887
|
const p = Array.isArray(params) ? params[0] : params;
|
|
843
888
|
if (!p || typeof p !== "object") return relayError(RELAY_ERRORS.invalidParams, "wallet_addEthereumChain expects one object parameter.");
|
|
844
889
|
const caip2 = caip2FromChainId(p.chainId);
|
|
845
890
|
if (!caip2) return relayError(RELAY_ERRORS.invalidParams, "chainId must be a 0x-prefixed hex string.");
|
|
846
891
|
if (!known(caip2)) return relayError(RELAY_ERRORS.chainNotAdded, `Chain ${String(p.chainId)} is not in this wallet's catalog.`);
|
|
847
|
-
const rpcs = Array.isArray(p.rpcUrls) ? p.rpcUrls.filter((u) => typeof u === "string" && /^https:\/\/[^\s/?#]+/i.test(u)) : [];
|
|
848
|
-
if (rpcs.length === 0) return relayError(RELAY_ERRORS.invalidParams, "rpcUrls must contain at least one https URL.");
|
|
892
|
+
const rpcs = Array.isArray(p.rpcUrls) ? p.rpcUrls.filter((u) => typeof u === "string" && /^https:\/\/[^\s/?#]+/i.test(u) && !isPrivateRpcHost(u)) : [];
|
|
893
|
+
if (rpcs.length === 0) return relayError(RELAY_ERRORS.invalidParams, "rpcUrls must contain at least one public https URL (a site cannot add a private / local endpoint).");
|
|
849
894
|
const name = typeof p.chainName === "string" && p.chainName.trim() ? p.chainName.trim().slice(0, 64) : caip2;
|
|
850
895
|
const currency = p.nativeCurrency;
|
|
851
896
|
if (currency && currency.decimals !== void 0 && currency.decimals !== 18) return relayError(RELAY_ERRORS.invalidParams, "nativeCurrency.decimals must be 18.");
|
|
@@ -1042,6 +1087,13 @@ var QueueLimitError = class extends Error {
|
|
|
1042
1087
|
}
|
|
1043
1088
|
code;
|
|
1044
1089
|
};
|
|
1090
|
+
var OriginRefused = class extends Error {
|
|
1091
|
+
code = RELAY_ERRORS.unauthorized;
|
|
1092
|
+
constructor(message) {
|
|
1093
|
+
super(message);
|
|
1094
|
+
this.name = "OriginRefused";
|
|
1095
|
+
}
|
|
1096
|
+
};
|
|
1045
1097
|
var APPROVALS_MAX_PENDING_PER_ORIGIN = 3;
|
|
1046
1098
|
var ApprovalQueue = class {
|
|
1047
1099
|
constructor(window, newId = () => crypto.randomUUID(), options = {}) {
|
|
@@ -1061,6 +1113,13 @@ var ApprovalQueue = class {
|
|
|
1061
1113
|
_via = /* @__PURE__ */ new Map();
|
|
1062
1114
|
/** Enqueue and wait for the user's answer. Refuses (never queues) past the per-origin limits. */
|
|
1063
1115
|
request(input) {
|
|
1116
|
+
let refused;
|
|
1117
|
+
try {
|
|
1118
|
+
refused = this.options.refuse?.(input.origin);
|
|
1119
|
+
} catch (e) {
|
|
1120
|
+
refused = `The wallet could not check this site (${e instanceof Error ? e.message : String(e)}).`;
|
|
1121
|
+
}
|
|
1122
|
+
if (refused) return Promise.reject(new OriginRefused(refused));
|
|
1064
1123
|
const sameOrigin = [...this._current ? [this._current] : [], ...this._waiting].filter((p) => p.request.origin === input.origin);
|
|
1065
1124
|
if (input.kind === "connect" && sameOrigin.some((p) => p.request.kind === "connect")) {
|
|
1066
1125
|
return Promise.reject(new QueueLimitError(RELAY_ERRORS.resourceUnavailable, "A connect request from this site is already waiting for your answer."));
|
|
@@ -1249,6 +1308,8 @@ var RequestRouter = class {
|
|
|
1249
1308
|
return await this.deps.networks.get(wanted) ?? await this.deps.networks.get(DEFAULT_CHAIN);
|
|
1250
1309
|
}
|
|
1251
1310
|
async handle(origin, chain, method, rawParams) {
|
|
1311
|
+
const refused = this.deps.refuse?.(origin);
|
|
1312
|
+
if (refused) throw new RouterError(RELAY_ERRORS.unauthorized, refused);
|
|
1252
1313
|
if (chain !== "eip155") throw new RouterError(RELAY_ERRORS.unsupportedMethod, `${chain} providers are not available yet.`);
|
|
1253
1314
|
const params = Array.isArray(rawParams) ? rawParams : rawParams === void 0 ? [] : [rawParams];
|
|
1254
1315
|
const network = await this.chainFor(origin);
|
|
@@ -1557,6 +1618,23 @@ var bigintSafe = (_k, v) => typeof v === "bigint" ? v.toString() : v;
|
|
|
1557
1618
|
var hex = (n) => `0x${n.toString(16)}`;
|
|
1558
1619
|
var big = (v) => BigInt(v);
|
|
1559
1620
|
var MIN_TIP_WEI = 100000000n;
|
|
1621
|
+
async function readFees(deps, network, block) {
|
|
1622
|
+
const b = block === void 0 ? await deps.rpc(network, "eth_getBlockByNumber", ["latest", false]) : block;
|
|
1623
|
+
const base = b?.baseFeePerGas != null ? big(b.baseFeePerGas) : null;
|
|
1624
|
+
if (base === null) return { gasPrice: big(await deps.rpc(network, "eth_gasPrice", [])) };
|
|
1625
|
+
const floor = deps.tipFloor?.(network.chainId) ?? MIN_TIP_WEI;
|
|
1626
|
+
const nodeTip = await deps.rpc(network, "eth_maxPriorityFeePerGas", []).then(big).catch(() => 0n);
|
|
1627
|
+
return { base, tip: nodeTip > floor ? nodeTip : floor };
|
|
1628
|
+
}
|
|
1629
|
+
function withFees(chainId, nonce, gasLimit, fees) {
|
|
1630
|
+
if ("gasPrice" in fees) return { chainId, nonce, gasLimit: hex(gasLimit), gasPrice: hex(fees.gasPrice), maxFee: (gasLimit * fees.gasPrice).toString() };
|
|
1631
|
+
const maxFeePerGas = fees.base * 2n + fees.tip;
|
|
1632
|
+
return { chainId, nonce, gasLimit: hex(gasLimit), maxFeePerGas: hex(maxFeePerGas), maxPriorityFeePerGas: hex(fees.tip), maxFee: (gasLimit * maxFeePerGas).toString() };
|
|
1633
|
+
}
|
|
1634
|
+
function outgrown(shown, now) {
|
|
1635
|
+
if ("gasPrice" in now) return shown.gasPrice === void 0 || now.gasPrice > big(shown.gasPrice);
|
|
1636
|
+
return shown.maxFeePerGas === void 0 || now.base + now.tip > big(shown.maxFeePerGas);
|
|
1637
|
+
}
|
|
1560
1638
|
async function prepareTransaction(deps, network, from, tx) {
|
|
1561
1639
|
const call = { from, to: tx.to, value: hex(BigInt(tx.value)), ...tx.data ? { data: tx.data } : {} };
|
|
1562
1640
|
const [nonceHex, block, gasHex] = await Promise.all([
|
|
@@ -1566,18 +1644,20 @@ async function prepareTransaction(deps, network, from, tx) {
|
|
|
1566
1644
|
]);
|
|
1567
1645
|
const estimated = big(gasHex);
|
|
1568
1646
|
const gasLimit = tx.gas ? estimated : (estimated * 120n + 99n) / 100n;
|
|
1569
|
-
|
|
1570
|
-
if (base === null) {
|
|
1571
|
-
const gasPrice = big(await deps.rpc(network, "eth_gasPrice", []));
|
|
1572
|
-
return { chainId: network.chainId, nonce: safeNonce(big(nonceHex)), gasLimit: hex(gasLimit), gasPrice: hex(gasPrice), maxFee: (gasLimit * gasPrice).toString() };
|
|
1573
|
-
}
|
|
1574
|
-
const floor = deps.tipFloor?.(network.chainId) ?? MIN_TIP_WEI;
|
|
1575
|
-
const nodeTip = await deps.rpc(network, "eth_maxPriorityFeePerGas", []).then(big).catch(() => 0n);
|
|
1576
|
-
const tip = nodeTip > floor ? nodeTip : floor;
|
|
1577
|
-
const maxFeePerGas = base * 2n + tip;
|
|
1578
|
-
return { chainId: network.chainId, nonce: safeNonce(big(nonceHex)), gasLimit: hex(gasLimit), maxFeePerGas: hex(maxFeePerGas), maxPriorityFeePerGas: hex(tip), maxFee: (gasLimit * maxFeePerGas).toString() };
|
|
1647
|
+
return withFees(network.chainId, safeNonce(big(nonceHex)), gasLimit, await readFees(deps, network, block));
|
|
1579
1648
|
}
|
|
1580
1649
|
function createSignHandler(deps) {
|
|
1650
|
+
const tails = /* @__PURE__ */ new Map();
|
|
1651
|
+
const nextNonce = /* @__PURE__ */ new Map();
|
|
1652
|
+
const serial = (key, fn) => {
|
|
1653
|
+
const run = (tails.get(key) ?? Promise.resolve()).then(fn, fn);
|
|
1654
|
+
const tail = run.then(() => void 0, () => void 0);
|
|
1655
|
+
tails.set(key, tail);
|
|
1656
|
+
void tail.then(() => {
|
|
1657
|
+
if (tails.get(key) === tail) tails.delete(key);
|
|
1658
|
+
});
|
|
1659
|
+
return run;
|
|
1660
|
+
};
|
|
1581
1661
|
return async ({ origin, network, method, params, accounts }) => {
|
|
1582
1662
|
const via = deps.approvals.viaFor(origin);
|
|
1583
1663
|
const info = classifySignRequest({ network, method, params, accounts, origin, ...via ? { via } : {} });
|
|
@@ -1586,6 +1666,41 @@ function createSignHandler(deps) {
|
|
|
1586
1666
|
const preview = deps.preview ? await deps.preview({ network, request: info, accounts }).catch(() => null) : null;
|
|
1587
1667
|
const payload = { request: info, network, ...prepared ? { prepared } : {}, preview };
|
|
1588
1668
|
await deps.approvals.request({ origin, chain: network.caip2, kind: info.kind, payload });
|
|
1669
|
+
if (info.kind === "send-transaction") {
|
|
1670
|
+
const key = `${network.caip2}|${lc(info.account)}`;
|
|
1671
|
+
return serial(key, async () => {
|
|
1672
|
+
let shown = prepared;
|
|
1673
|
+
for (; ; ) {
|
|
1674
|
+
const [nonceHex, fees] = await Promise.all([deps.rpc(network, "eth_getTransactionCount", [info.account, "pending"]), readFees(deps, network)]);
|
|
1675
|
+
const local = nextNonce.get(key) ?? 0;
|
|
1676
|
+
const nodeNonce = safeNonce(big(nonceHex));
|
|
1677
|
+
const nonce = nodeNonce > local ? nodeNonce : local;
|
|
1678
|
+
if (outgrown(shown, fees)) {
|
|
1679
|
+
shown = withFees(network.chainId, nonce, big(shown.gasLimit), fees);
|
|
1680
|
+
await deps.approvals.request({ origin, chain: network.caip2, kind: info.kind, payload: { ...payload, prepared: shown, repriced: true } });
|
|
1681
|
+
continue;
|
|
1682
|
+
}
|
|
1683
|
+
const handle2 = await deps.signerFor(info.account, network);
|
|
1684
|
+
let raw;
|
|
1685
|
+
try {
|
|
1686
|
+
raw = await handle2.signer.signTransaction({
|
|
1687
|
+
chainId: shown.chainId,
|
|
1688
|
+
nonce,
|
|
1689
|
+
to: info.tx.to,
|
|
1690
|
+
value: BigInt(info.tx.value),
|
|
1691
|
+
data: info.tx.data ?? "0x",
|
|
1692
|
+
gasLimit: BigInt(shown.gasLimit),
|
|
1693
|
+
...shown.gasPrice !== void 0 ? { gasPrice: BigInt(shown.gasPrice) } : { maxFeePerGas: BigInt(shown.maxFeePerGas), maxPriorityFeePerGas: BigInt(shown.maxPriorityFeePerGas) }
|
|
1694
|
+
});
|
|
1695
|
+
} finally {
|
|
1696
|
+
handle2.dispose();
|
|
1697
|
+
}
|
|
1698
|
+
const hash = await deps.rpc(network, "eth_sendRawTransaction", [raw]);
|
|
1699
|
+
nextNonce.set(key, nonce + 1);
|
|
1700
|
+
return hash;
|
|
1701
|
+
}
|
|
1702
|
+
});
|
|
1703
|
+
}
|
|
1589
1704
|
const handle = await deps.signerFor(info.account, network);
|
|
1590
1705
|
try {
|
|
1591
1706
|
switch (info.kind) {
|
|
@@ -1593,19 +1708,6 @@ function createSignHandler(deps) {
|
|
|
1593
1708
|
return await handle.signer.signMessage(HEX.test(info.raw) ? Uint8Array.from(info.raw.slice(2).match(/../g) ?? [], (b) => Number.parseInt(b, 16)) : info.raw);
|
|
1594
1709
|
case "sign-typed-data":
|
|
1595
1710
|
return await handle.signer.signTypedData(info.typedData);
|
|
1596
|
-
case "send-transaction": {
|
|
1597
|
-
const p = prepared;
|
|
1598
|
-
const raw = await handle.signer.signTransaction({
|
|
1599
|
-
chainId: p.chainId,
|
|
1600
|
-
nonce: p.nonce,
|
|
1601
|
-
to: info.tx.to,
|
|
1602
|
-
value: BigInt(info.tx.value),
|
|
1603
|
-
data: info.tx.data ?? "0x",
|
|
1604
|
-
gasLimit: BigInt(p.gasLimit),
|
|
1605
|
-
...p.gasPrice !== void 0 ? { gasPrice: BigInt(p.gasPrice) } : { maxFeePerGas: BigInt(p.maxFeePerGas), maxPriorityFeePerGas: BigInt(p.maxPriorityFeePerGas) }
|
|
1606
|
-
});
|
|
1607
|
-
return await deps.rpc(network, "eth_sendRawTransaction", [raw]);
|
|
1608
|
-
}
|
|
1609
1711
|
}
|
|
1610
1712
|
} finally {
|
|
1611
1713
|
handle.dispose();
|
|
@@ -1614,22 +1716,28 @@ function createSignHandler(deps) {
|
|
|
1614
1716
|
}
|
|
1615
1717
|
|
|
1616
1718
|
// src/gate.ts
|
|
1719
|
+
var HEX_ADDRESS = /^0x[0-9a-fA-F]{40}$/;
|
|
1720
|
+
var sameAccount = (a, b) => a === b || HEX_ADDRESS.test(a) && a.toLowerCase() === b.toLowerCase();
|
|
1617
1721
|
var ReviewGate = class {
|
|
1618
1722
|
constructor(now = Date.now) {
|
|
1619
1723
|
this.now = now;
|
|
1620
1724
|
}
|
|
1621
1725
|
now;
|
|
1622
1726
|
approved = [];
|
|
1623
|
-
/**
|
|
1624
|
-
|
|
1727
|
+
/**
|
|
1728
|
+
* Each matcher passes ONE operation, within `ttlMs` (default 10 min). With `account`, only an op
|
|
1729
|
+
* whose `account` is that account (an op without one does not pass). `reviewSwapPlan` and
|
|
1730
|
+
* `reviewEarnPlan` always bind.
|
|
1731
|
+
*/
|
|
1732
|
+
preApprove(matchers, ttlMs = 10 * 6e4, account) {
|
|
1625
1733
|
const until = this.now() + ttlMs;
|
|
1626
|
-
this.approved.push(...matchers.map((match) => ({ match, until })));
|
|
1734
|
+
this.approved.push(...matchers.map((match) => ({ match, until, ...account ? { account } : {} })));
|
|
1627
1735
|
}
|
|
1628
1736
|
/** True (and the matcher is spent) when a pre-approval covers `op`. */
|
|
1629
1737
|
consume(op) {
|
|
1630
1738
|
const now = this.now();
|
|
1631
1739
|
this.approved = this.approved.filter((a) => a.until > now);
|
|
1632
|
-
const i = this.approved.findIndex((a) => a.match(op));
|
|
1740
|
+
const i = this.approved.findIndex((a) => (a.account === void 0 || op.account !== void 0 && sameAccount(a.account, op.account)) && a.match(op));
|
|
1633
1741
|
if (i < 0) return false;
|
|
1634
1742
|
this.approved.splice(i, 1);
|
|
1635
1743
|
return true;
|
|
@@ -1646,12 +1754,17 @@ function evmCallMatcher(expected) {
|
|
|
1646
1754
|
const data = expected.data && expected.data !== "0x" ? lc2(expected.data) : void 0;
|
|
1647
1755
|
return (op) => op.kind === "evm-tx" && op.caip2 === expected.caip2 && lc2(op.to) === lc2(expected.to) && (op.data && op.data !== "0x" ? lc2(op.data) : void 0) === data && op.value === expected.value;
|
|
1648
1756
|
}
|
|
1757
|
+
function typedDataMatcher(expected) {
|
|
1758
|
+
const hash = lc2(expected.typedDataHash);
|
|
1759
|
+
return (op) => op.kind === "evm-typed-data" && op.caip2 === expected.caip2 && lc2(op.typedDataHash) === hash;
|
|
1760
|
+
}
|
|
1649
1761
|
function chainTxMatcher(expected) {
|
|
1650
1762
|
return (op) => op.kind === "chain-tx" && op.chain === expected.chain && op.to === expected.to && op.amount === expected.amount && (op.token ?? "") === (expected.token ?? "") && (op.memo ?? "") === (expected.memo ?? "");
|
|
1651
1763
|
}
|
|
1652
1764
|
|
|
1653
1765
|
// src/swap.ts
|
|
1654
1766
|
import { validQuote } from "@anywalletkit/protocol-swap2-core";
|
|
1767
|
+
import { typedDataHash } from "@anywalletkit/wallet-evm";
|
|
1655
1768
|
async function quoteRoutes(providers, params, options = {}) {
|
|
1656
1769
|
const timeoutMs = options.timeoutMs ?? 2e4;
|
|
1657
1770
|
const from = { chain: params.sourceChain, token: params.sourceToken };
|
|
@@ -1757,7 +1870,8 @@ function swapApprovalPayload(plan, meta) {
|
|
|
1757
1870
|
source = { kind: "serialized" };
|
|
1758
1871
|
checks.push({ status: "ok", label: "Balance changes are proven before signing (SDK guard)" });
|
|
1759
1872
|
} else {
|
|
1760
|
-
|
|
1873
|
+
const typedData = parseTypedOrder(s.typedData);
|
|
1874
|
+
source = { kind: "typed-order", typedData, text: JSON.stringify(typedData, null, 2) };
|
|
1761
1875
|
checks.push({ status: "ok", label: "Signed order \u2014 no transaction is sent from your wallet" });
|
|
1762
1876
|
}
|
|
1763
1877
|
for (const a of plan.approvals) checks.push(a.amount > plan.sourceAmount ? { status: "warn", label: "Approval is larger than this swap needs" } : { status: "ok", label: "Approval is exactly the amount needed" });
|
|
@@ -1782,6 +1896,11 @@ function swapApprovalPayload(plan, meta) {
|
|
|
1782
1896
|
checks
|
|
1783
1897
|
};
|
|
1784
1898
|
}
|
|
1899
|
+
function parseTypedOrder(json) {
|
|
1900
|
+
const t = JSON.parse(json);
|
|
1901
|
+
if (!t || typeof t !== "object" || !t.domain || typeof t.domain !== "object" || !t.types || typeof t.types !== "object" || !t.message || typeof t.message !== "object") throw new Error("The swap order is not EIP-712 typed data.");
|
|
1902
|
+
return t;
|
|
1903
|
+
}
|
|
1785
1904
|
function planMatchers(plan) {
|
|
1786
1905
|
const m = plan.approvals.map((a) => evmCallMatcher({ caip2: plan.fromChain, to: a.token, data: encodeApproveCalldata(a.spender, a.amount), value: 0n }));
|
|
1787
1906
|
const s = plan.source;
|
|
@@ -1796,16 +1915,18 @@ function planMatchers(plan) {
|
|
|
1796
1915
|
} else if (s.kind === "serialized") {
|
|
1797
1916
|
m.push((op) => op.kind === "solana-serialized" && op.serializedTx === s.serializedTx);
|
|
1798
1917
|
} else {
|
|
1799
|
-
m.push((
|
|
1918
|
+
m.push(typedDataMatcher({ caip2: plan.fromChain, typedDataHash: typedDataHash(parseTypedOrder(s.typedData)) }));
|
|
1800
1919
|
}
|
|
1801
1920
|
return m;
|
|
1802
1921
|
}
|
|
1803
1922
|
function reviewSwapPlan(approvals, gate, meta) {
|
|
1923
|
+
if (typeof meta.account !== "string" || !meta.account) throw new TypeError("reviewSwapPlan needs meta.account: the gate passes the plan for that account only.");
|
|
1804
1924
|
return async (plan) => {
|
|
1925
|
+
const matchers = planMatchers(plan);
|
|
1805
1926
|
const payload = swapApprovalPayload(plan, meta);
|
|
1806
1927
|
await approvals.request({ origin: meta.origin, chain: plan.fromChain, kind: "swap", payload });
|
|
1807
1928
|
gate.clear();
|
|
1808
|
-
gate.preApprove(
|
|
1929
|
+
gate.preApprove(matchers, Math.max(6e4, plan.validUntilMs - Date.now() + 5 * 6e4), meta.account);
|
|
1809
1930
|
};
|
|
1810
1931
|
}
|
|
1811
1932
|
|
|
@@ -1815,11 +1936,13 @@ var ACTIVE_STATUSES = /* @__PURE__ */ new Set(["pending", "action-required", "re
|
|
|
1815
1936
|
function parseSwapId(id) {
|
|
1816
1937
|
if (!id.startsWith(`${SWAP2_ID_PREFIX}.`)) return null;
|
|
1817
1938
|
const parts = id.slice(SWAP2_ID_PREFIX.length + 1).split(".");
|
|
1818
|
-
if (parts.length < 2 || parts.length >
|
|
1939
|
+
if (parts.length < 2 || parts.length > 4 || !parts[0] || !parts[1]) return null;
|
|
1819
1940
|
const provider = decodeURIComponent(parts[0]);
|
|
1820
1941
|
const intentId = decodeURIComponent(parts[1]);
|
|
1821
1942
|
const sourceTxHash = parts[2] ? decodeURIComponent(parts[2]) : void 0;
|
|
1822
|
-
|
|
1943
|
+
const destTxHash = parts[3] ? decodeURIComponent(parts[3]) : void 0;
|
|
1944
|
+
if (destTxHash && !sourceTxHash) return null;
|
|
1945
|
+
return { provider, intentId, ...sourceTxHash ? { sourceTxHash } : {}, ...destTxHash ? { destTxHash } : {} };
|
|
1823
1946
|
}
|
|
1824
1947
|
var SwapsStore = class {
|
|
1825
1948
|
constructor(storage) {
|
|
@@ -1828,9 +1951,14 @@ var SwapsStore = class {
|
|
|
1828
1951
|
storage;
|
|
1829
1952
|
_records = null;
|
|
1830
1953
|
_listeners = /* @__PURE__ */ new Set();
|
|
1954
|
+
/**
|
|
1955
|
+
* add / update / remove run here one after another, each reading the list inside its turn: an
|
|
1956
|
+
* alarm tick's update that read the list before a new swap was added would otherwise write the
|
|
1957
|
+
* old list back and drop the swap.
|
|
1958
|
+
*/
|
|
1959
|
+
_queue = Promise.resolve();
|
|
1831
1960
|
async list() {
|
|
1832
|
-
|
|
1833
|
-
return [...this._records].sort((a, b) => b.createdAt - a.createdAt);
|
|
1961
|
+
return [...await this._all()].sort((a, b) => b.createdAt - a.createdAt);
|
|
1834
1962
|
}
|
|
1835
1963
|
async get(id) {
|
|
1836
1964
|
return (await this.list()).find((r) => r.id === id) ?? null;
|
|
@@ -1838,26 +1966,41 @@ var SwapsStore = class {
|
|
|
1838
1966
|
async active() {
|
|
1839
1967
|
return (await this.list()).filter((r) => ACTIVE_STATUSES.has(r.status));
|
|
1840
1968
|
}
|
|
1841
|
-
|
|
1842
|
-
|
|
1843
|
-
|
|
1969
|
+
add(record) {
|
|
1970
|
+
return this._serial(async () => {
|
|
1971
|
+
const all = (await this._all()).filter((r) => r.id !== record.id);
|
|
1972
|
+
await this.write([...all, record]);
|
|
1973
|
+
});
|
|
1844
1974
|
}
|
|
1845
|
-
|
|
1846
|
-
|
|
1847
|
-
|
|
1848
|
-
|
|
1849
|
-
|
|
1850
|
-
|
|
1851
|
-
|
|
1852
|
-
|
|
1975
|
+
update(id, patch) {
|
|
1976
|
+
return this._serial(async () => {
|
|
1977
|
+
const all = [...await this._all()];
|
|
1978
|
+
const i = all.findIndex((r) => r.id === id);
|
|
1979
|
+
if (i < 0) return null;
|
|
1980
|
+
const next = { ...all[i], ...patch, updatedAt: patch.updatedAt ?? Date.now() };
|
|
1981
|
+
all[i] = next;
|
|
1982
|
+
await this.write(all);
|
|
1983
|
+
return next;
|
|
1984
|
+
});
|
|
1853
1985
|
}
|
|
1854
|
-
|
|
1855
|
-
|
|
1986
|
+
remove(id) {
|
|
1987
|
+
return this._serial(async () => {
|
|
1988
|
+
await this.write((await this._all()).filter((r) => r.id !== id));
|
|
1989
|
+
});
|
|
1856
1990
|
}
|
|
1857
1991
|
onChange(listener) {
|
|
1858
1992
|
this._listeners.add(listener);
|
|
1859
1993
|
return () => this._listeners.delete(listener);
|
|
1860
1994
|
}
|
|
1995
|
+
async _all() {
|
|
1996
|
+
this._records ??= await this.storage.get() ?? [];
|
|
1997
|
+
return this._records;
|
|
1998
|
+
}
|
|
1999
|
+
_serial(fn) {
|
|
2000
|
+
const run = this._queue.then(fn, fn);
|
|
2001
|
+
this._queue = run.then(() => void 0, () => void 0);
|
|
2002
|
+
return run;
|
|
2003
|
+
}
|
|
1861
2004
|
async write(records) {
|
|
1862
2005
|
await this.storage.set(records);
|
|
1863
2006
|
this._records = records;
|
|
@@ -1932,8 +2075,9 @@ function earnApprovalPayload(plan, meta) {
|
|
|
1932
2075
|
return { module: meta.module, action: meta.action, network: meta.network, facts: meta.facts, approvals: plan.approvals, tx: plan.tx, checks, preview: meta.preview ?? null };
|
|
1933
2076
|
}
|
|
1934
2077
|
async function reviewEarnPlan(approvals, gate, meta, plan) {
|
|
2078
|
+
if (typeof meta.account !== "string" || !meta.account) throw new TypeError("reviewEarnPlan needs meta.account: the gate passes the plan for that account only.");
|
|
1935
2079
|
await approvals.request({ origin: meta.origin, chain: meta.network.caip2, kind: "earn", payload: earnApprovalPayload(plan, meta) });
|
|
1936
|
-
gate.preApprove(earnPlanMatchers(meta.network, plan), 10 * 6e4);
|
|
2080
|
+
gate.preApprove(earnPlanMatchers(meta.network, plan), 10 * 6e4, meta.account);
|
|
1937
2081
|
}
|
|
1938
2082
|
|
|
1939
2083
|
// src/tonconnect.ts
|
|
@@ -2094,6 +2238,14 @@ var PhishingDetector = class {
|
|
|
2094
2238
|
}
|
|
2095
2239
|
return { host, blocked: false, warnings };
|
|
2096
2240
|
}
|
|
2241
|
+
/**
|
|
2242
|
+
* Why `hostOrOrigin` must be refused outright, or `null`: a host on the blocklist (or under a
|
|
2243
|
+
* listed domain) and not on the allowlist. Lookalikes and punycode only warn ({@link check}).
|
|
2244
|
+
* Wire it as `ApprovalQueueOptions.refuse` and `RouterDeps.refuse`.
|
|
2245
|
+
*/
|
|
2246
|
+
refusal(hostOrOrigin) {
|
|
2247
|
+
return this.check(hostOrOrigin).blocked ? "This site is on the wallet's phishing list; the wallet refuses its requests." : null;
|
|
2248
|
+
}
|
|
2097
2249
|
/**
|
|
2098
2250
|
* `metamask.io-login.com`, `app-uniswap.com`, `uniswap.org.evil.net`: a known site's name used as (part of) a label
|
|
2099
2251
|
* of a host that is not that site. Names shorter than 5 characters are skipped (too many honest matches).
|
|
@@ -2304,6 +2456,7 @@ export {
|
|
|
2304
2456
|
NEAR_PROVIDERS,
|
|
2305
2457
|
NON_EVM_CHAINS,
|
|
2306
2458
|
NetworksTable,
|
|
2459
|
+
OriginRefused,
|
|
2307
2460
|
PAGE_TARGET,
|
|
2308
2461
|
PORT_NAME,
|
|
2309
2462
|
PermissionStore,
|
|
@@ -2408,6 +2561,7 @@ export {
|
|
|
2408
2561
|
tonConnectErrorCode,
|
|
2409
2562
|
tonProofMessage,
|
|
2410
2563
|
tonSignDataMessage,
|
|
2564
|
+
typedDataMatcher,
|
|
2411
2565
|
vaultSecretStorage,
|
|
2412
2566
|
verifyAssertion
|
|
2413
2567
|
};
|