@anywalletkit/extension-kit 0.1.4 → 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 +10 -7
- package/dist/index.cjs +219 -60
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +100 -12
- package/dist/index.d.ts +100 -12
- package/dist/index.js +219 -60
- package/dist/index.js.map +1 -1
- package/package.json +8 -7
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 {
|
|
@@ -606,6 +638,10 @@ function isRelayToContent(x) {
|
|
|
606
638
|
function newRequestId() {
|
|
607
639
|
return crypto.randomUUID();
|
|
608
640
|
}
|
|
641
|
+
function pageRequestFrom(event, page) {
|
|
642
|
+
if (event.source !== page || event.origin !== page.location.origin) return null;
|
|
643
|
+
return isPageRequest(event.data) ? event.data : null;
|
|
644
|
+
}
|
|
609
645
|
function stampOrigin(request, origin) {
|
|
610
646
|
const { id, chain, method } = request;
|
|
611
647
|
return "params" in request ? { kind: "request", id, origin, chain, method, params: request.params } : { kind: "request", id, origin, chain, method };
|
|
@@ -834,14 +870,27 @@ var NetworksTable = class {
|
|
|
834
870
|
return this._loading;
|
|
835
871
|
}
|
|
836
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
|
+
}
|
|
837
886
|
function parseAddChainParams(params, known = isCatalogChain) {
|
|
838
887
|
const p = Array.isArray(params) ? params[0] : params;
|
|
839
888
|
if (!p || typeof p !== "object") return relayError(RELAY_ERRORS.invalidParams, "wallet_addEthereumChain expects one object parameter.");
|
|
840
889
|
const caip2 = caip2FromChainId(p.chainId);
|
|
841
890
|
if (!caip2) return relayError(RELAY_ERRORS.invalidParams, "chainId must be a 0x-prefixed hex string.");
|
|
842
891
|
if (!known(caip2)) return relayError(RELAY_ERRORS.chainNotAdded, `Chain ${String(p.chainId)} is not in this wallet's catalog.`);
|
|
843
|
-
const rpcs = Array.isArray(p.rpcUrls) ? p.rpcUrls.filter((u) => typeof u === "string" && /^https:\/\/[^\s/?#]+/i.test(u)) : [];
|
|
844
|
-
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).");
|
|
845
894
|
const name = typeof p.chainName === "string" && p.chainName.trim() ? p.chainName.trim().slice(0, 64) : caip2;
|
|
846
895
|
const currency = p.nativeCurrency;
|
|
847
896
|
if (currency && currency.decimals !== void 0 && currency.decimals !== 18) return relayError(RELAY_ERRORS.invalidParams, "nativeCurrency.decimals must be 18.");
|
|
@@ -1038,6 +1087,13 @@ var QueueLimitError = class extends Error {
|
|
|
1038
1087
|
}
|
|
1039
1088
|
code;
|
|
1040
1089
|
};
|
|
1090
|
+
var OriginRefused = class extends Error {
|
|
1091
|
+
code = RELAY_ERRORS.unauthorized;
|
|
1092
|
+
constructor(message) {
|
|
1093
|
+
super(message);
|
|
1094
|
+
this.name = "OriginRefused";
|
|
1095
|
+
}
|
|
1096
|
+
};
|
|
1041
1097
|
var APPROVALS_MAX_PENDING_PER_ORIGIN = 3;
|
|
1042
1098
|
var ApprovalQueue = class {
|
|
1043
1099
|
constructor(window, newId = () => crypto.randomUUID(), options = {}) {
|
|
@@ -1057,6 +1113,13 @@ var ApprovalQueue = class {
|
|
|
1057
1113
|
_via = /* @__PURE__ */ new Map();
|
|
1058
1114
|
/** Enqueue and wait for the user's answer. Refuses (never queues) past the per-origin limits. */
|
|
1059
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));
|
|
1060
1123
|
const sameOrigin = [...this._current ? [this._current] : [], ...this._waiting].filter((p) => p.request.origin === input.origin);
|
|
1061
1124
|
if (input.kind === "connect" && sameOrigin.some((p) => p.request.kind === "connect")) {
|
|
1062
1125
|
return Promise.reject(new QueueLimitError(RELAY_ERRORS.resourceUnavailable, "A connect request from this site is already waiting for your answer."));
|
|
@@ -1245,6 +1308,8 @@ var RequestRouter = class {
|
|
|
1245
1308
|
return await this.deps.networks.get(wanted) ?? await this.deps.networks.get(DEFAULT_CHAIN);
|
|
1246
1309
|
}
|
|
1247
1310
|
async handle(origin, chain, method, rawParams) {
|
|
1311
|
+
const refused = this.deps.refuse?.(origin);
|
|
1312
|
+
if (refused) throw new RouterError(RELAY_ERRORS.unauthorized, refused);
|
|
1248
1313
|
if (chain !== "eip155") throw new RouterError(RELAY_ERRORS.unsupportedMethod, `${chain} providers are not available yet.`);
|
|
1249
1314
|
const params = Array.isArray(rawParams) ? rawParams : rawParams === void 0 ? [] : [rawParams];
|
|
1250
1315
|
const network = await this.chainFor(origin);
|
|
@@ -1553,6 +1618,23 @@ var bigintSafe = (_k, v) => typeof v === "bigint" ? v.toString() : v;
|
|
|
1553
1618
|
var hex = (n) => `0x${n.toString(16)}`;
|
|
1554
1619
|
var big = (v) => BigInt(v);
|
|
1555
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
|
+
}
|
|
1556
1638
|
async function prepareTransaction(deps, network, from, tx) {
|
|
1557
1639
|
const call = { from, to: tx.to, value: hex(BigInt(tx.value)), ...tx.data ? { data: tx.data } : {} };
|
|
1558
1640
|
const [nonceHex, block, gasHex] = await Promise.all([
|
|
@@ -1562,18 +1644,20 @@ async function prepareTransaction(deps, network, from, tx) {
|
|
|
1562
1644
|
]);
|
|
1563
1645
|
const estimated = big(gasHex);
|
|
1564
1646
|
const gasLimit = tx.gas ? estimated : (estimated * 120n + 99n) / 100n;
|
|
1565
|
-
|
|
1566
|
-
if (base === null) {
|
|
1567
|
-
const gasPrice = big(await deps.rpc(network, "eth_gasPrice", []));
|
|
1568
|
-
return { chainId: network.chainId, nonce: safeNonce(big(nonceHex)), gasLimit: hex(gasLimit), gasPrice: hex(gasPrice), maxFee: (gasLimit * gasPrice).toString() };
|
|
1569
|
-
}
|
|
1570
|
-
const floor = deps.tipFloor?.(network.chainId) ?? MIN_TIP_WEI;
|
|
1571
|
-
const nodeTip = await deps.rpc(network, "eth_maxPriorityFeePerGas", []).then(big).catch(() => 0n);
|
|
1572
|
-
const tip = nodeTip > floor ? nodeTip : floor;
|
|
1573
|
-
const maxFeePerGas = base * 2n + tip;
|
|
1574
|
-
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));
|
|
1575
1648
|
}
|
|
1576
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
|
+
};
|
|
1577
1661
|
return async ({ origin, network, method, params, accounts }) => {
|
|
1578
1662
|
const via = deps.approvals.viaFor(origin);
|
|
1579
1663
|
const info = classifySignRequest({ network, method, params, accounts, origin, ...via ? { via } : {} });
|
|
@@ -1582,6 +1666,41 @@ function createSignHandler(deps) {
|
|
|
1582
1666
|
const preview = deps.preview ? await deps.preview({ network, request: info, accounts }).catch(() => null) : null;
|
|
1583
1667
|
const payload = { request: info, network, ...prepared ? { prepared } : {}, preview };
|
|
1584
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
|
+
}
|
|
1585
1704
|
const handle = await deps.signerFor(info.account, network);
|
|
1586
1705
|
try {
|
|
1587
1706
|
switch (info.kind) {
|
|
@@ -1589,19 +1708,6 @@ function createSignHandler(deps) {
|
|
|
1589
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);
|
|
1590
1709
|
case "sign-typed-data":
|
|
1591
1710
|
return await handle.signer.signTypedData(info.typedData);
|
|
1592
|
-
case "send-transaction": {
|
|
1593
|
-
const p = prepared;
|
|
1594
|
-
const raw = await handle.signer.signTransaction({
|
|
1595
|
-
chainId: p.chainId,
|
|
1596
|
-
nonce: p.nonce,
|
|
1597
|
-
to: info.tx.to,
|
|
1598
|
-
value: BigInt(info.tx.value),
|
|
1599
|
-
data: info.tx.data ?? "0x",
|
|
1600
|
-
gasLimit: BigInt(p.gasLimit),
|
|
1601
|
-
...p.gasPrice !== void 0 ? { gasPrice: BigInt(p.gasPrice) } : { maxFeePerGas: BigInt(p.maxFeePerGas), maxPriorityFeePerGas: BigInt(p.maxPriorityFeePerGas) }
|
|
1602
|
-
});
|
|
1603
|
-
return await deps.rpc(network, "eth_sendRawTransaction", [raw]);
|
|
1604
|
-
}
|
|
1605
1711
|
}
|
|
1606
1712
|
} finally {
|
|
1607
1713
|
handle.dispose();
|
|
@@ -1610,22 +1716,28 @@ function createSignHandler(deps) {
|
|
|
1610
1716
|
}
|
|
1611
1717
|
|
|
1612
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();
|
|
1613
1721
|
var ReviewGate = class {
|
|
1614
1722
|
constructor(now = Date.now) {
|
|
1615
1723
|
this.now = now;
|
|
1616
1724
|
}
|
|
1617
1725
|
now;
|
|
1618
1726
|
approved = [];
|
|
1619
|
-
/**
|
|
1620
|
-
|
|
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) {
|
|
1621
1733
|
const until = this.now() + ttlMs;
|
|
1622
|
-
this.approved.push(...matchers.map((match) => ({ match, until })));
|
|
1734
|
+
this.approved.push(...matchers.map((match) => ({ match, until, ...account ? { account } : {} })));
|
|
1623
1735
|
}
|
|
1624
1736
|
/** True (and the matcher is spent) when a pre-approval covers `op`. */
|
|
1625
1737
|
consume(op) {
|
|
1626
1738
|
const now = this.now();
|
|
1627
1739
|
this.approved = this.approved.filter((a) => a.until > now);
|
|
1628
|
-
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));
|
|
1629
1741
|
if (i < 0) return false;
|
|
1630
1742
|
this.approved.splice(i, 1);
|
|
1631
1743
|
return true;
|
|
@@ -1642,12 +1754,17 @@ function evmCallMatcher(expected) {
|
|
|
1642
1754
|
const data = expected.data && expected.data !== "0x" ? lc2(expected.data) : void 0;
|
|
1643
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;
|
|
1644
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
|
+
}
|
|
1645
1761
|
function chainTxMatcher(expected) {
|
|
1646
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 ?? "");
|
|
1647
1763
|
}
|
|
1648
1764
|
|
|
1649
1765
|
// src/swap.ts
|
|
1650
1766
|
import { validQuote } from "@anywalletkit/protocol-swap2-core";
|
|
1767
|
+
import { typedDataHash } from "@anywalletkit/wallet-evm";
|
|
1651
1768
|
async function quoteRoutes(providers, params, options = {}) {
|
|
1652
1769
|
const timeoutMs = options.timeoutMs ?? 2e4;
|
|
1653
1770
|
const from = { chain: params.sourceChain, token: params.sourceToken };
|
|
@@ -1753,7 +1870,8 @@ function swapApprovalPayload(plan, meta) {
|
|
|
1753
1870
|
source = { kind: "serialized" };
|
|
1754
1871
|
checks.push({ status: "ok", label: "Balance changes are proven before signing (SDK guard)" });
|
|
1755
1872
|
} else {
|
|
1756
|
-
|
|
1873
|
+
const typedData = parseTypedOrder(s.typedData);
|
|
1874
|
+
source = { kind: "typed-order", typedData, text: JSON.stringify(typedData, null, 2) };
|
|
1757
1875
|
checks.push({ status: "ok", label: "Signed order \u2014 no transaction is sent from your wallet" });
|
|
1758
1876
|
}
|
|
1759
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" });
|
|
@@ -1778,6 +1896,11 @@ function swapApprovalPayload(plan, meta) {
|
|
|
1778
1896
|
checks
|
|
1779
1897
|
};
|
|
1780
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
|
+
}
|
|
1781
1904
|
function planMatchers(plan) {
|
|
1782
1905
|
const m = plan.approvals.map((a) => evmCallMatcher({ caip2: plan.fromChain, to: a.token, data: encodeApproveCalldata(a.spender, a.amount), value: 0n }));
|
|
1783
1906
|
const s = plan.source;
|
|
@@ -1792,16 +1915,18 @@ function planMatchers(plan) {
|
|
|
1792
1915
|
} else if (s.kind === "serialized") {
|
|
1793
1916
|
m.push((op) => op.kind === "solana-serialized" && op.serializedTx === s.serializedTx);
|
|
1794
1917
|
} else {
|
|
1795
|
-
m.push((
|
|
1918
|
+
m.push(typedDataMatcher({ caip2: plan.fromChain, typedDataHash: typedDataHash(parseTypedOrder(s.typedData)) }));
|
|
1796
1919
|
}
|
|
1797
1920
|
return m;
|
|
1798
1921
|
}
|
|
1799
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.");
|
|
1800
1924
|
return async (plan) => {
|
|
1925
|
+
const matchers = planMatchers(plan);
|
|
1801
1926
|
const payload = swapApprovalPayload(plan, meta);
|
|
1802
1927
|
await approvals.request({ origin: meta.origin, chain: plan.fromChain, kind: "swap", payload });
|
|
1803
1928
|
gate.clear();
|
|
1804
|
-
gate.preApprove(
|
|
1929
|
+
gate.preApprove(matchers, Math.max(6e4, plan.validUntilMs - Date.now() + 5 * 6e4), meta.account);
|
|
1805
1930
|
};
|
|
1806
1931
|
}
|
|
1807
1932
|
|
|
@@ -1811,11 +1936,13 @@ var ACTIVE_STATUSES = /* @__PURE__ */ new Set(["pending", "action-required", "re
|
|
|
1811
1936
|
function parseSwapId(id) {
|
|
1812
1937
|
if (!id.startsWith(`${SWAP2_ID_PREFIX}.`)) return null;
|
|
1813
1938
|
const parts = id.slice(SWAP2_ID_PREFIX.length + 1).split(".");
|
|
1814
|
-
if (parts.length < 2 || parts.length >
|
|
1939
|
+
if (parts.length < 2 || parts.length > 4 || !parts[0] || !parts[1]) return null;
|
|
1815
1940
|
const provider = decodeURIComponent(parts[0]);
|
|
1816
1941
|
const intentId = decodeURIComponent(parts[1]);
|
|
1817
1942
|
const sourceTxHash = parts[2] ? decodeURIComponent(parts[2]) : void 0;
|
|
1818
|
-
|
|
1943
|
+
const destTxHash = parts[3] ? decodeURIComponent(parts[3]) : void 0;
|
|
1944
|
+
if (destTxHash && !sourceTxHash) return null;
|
|
1945
|
+
return { provider, intentId, ...sourceTxHash ? { sourceTxHash } : {}, ...destTxHash ? { destTxHash } : {} };
|
|
1819
1946
|
}
|
|
1820
1947
|
var SwapsStore = class {
|
|
1821
1948
|
constructor(storage) {
|
|
@@ -1824,9 +1951,14 @@ var SwapsStore = class {
|
|
|
1824
1951
|
storage;
|
|
1825
1952
|
_records = null;
|
|
1826
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();
|
|
1827
1960
|
async list() {
|
|
1828
|
-
|
|
1829
|
-
return [...this._records].sort((a, b) => b.createdAt - a.createdAt);
|
|
1961
|
+
return [...await this._all()].sort((a, b) => b.createdAt - a.createdAt);
|
|
1830
1962
|
}
|
|
1831
1963
|
async get(id) {
|
|
1832
1964
|
return (await this.list()).find((r) => r.id === id) ?? null;
|
|
@@ -1834,26 +1966,41 @@ var SwapsStore = class {
|
|
|
1834
1966
|
async active() {
|
|
1835
1967
|
return (await this.list()).filter((r) => ACTIVE_STATUSES.has(r.status));
|
|
1836
1968
|
}
|
|
1837
|
-
|
|
1838
|
-
|
|
1839
|
-
|
|
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
|
+
});
|
|
1840
1974
|
}
|
|
1841
|
-
|
|
1842
|
-
|
|
1843
|
-
|
|
1844
|
-
|
|
1845
|
-
|
|
1846
|
-
|
|
1847
|
-
|
|
1848
|
-
|
|
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
|
+
});
|
|
1849
1985
|
}
|
|
1850
|
-
|
|
1851
|
-
|
|
1986
|
+
remove(id) {
|
|
1987
|
+
return this._serial(async () => {
|
|
1988
|
+
await this.write((await this._all()).filter((r) => r.id !== id));
|
|
1989
|
+
});
|
|
1852
1990
|
}
|
|
1853
1991
|
onChange(listener) {
|
|
1854
1992
|
this._listeners.add(listener);
|
|
1855
1993
|
return () => this._listeners.delete(listener);
|
|
1856
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
|
+
}
|
|
1857
2004
|
async write(records) {
|
|
1858
2005
|
await this.storage.set(records);
|
|
1859
2006
|
this._records = records;
|
|
@@ -1928,8 +2075,9 @@ function earnApprovalPayload(plan, meta) {
|
|
|
1928
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 };
|
|
1929
2076
|
}
|
|
1930
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.");
|
|
1931
2079
|
await approvals.request({ origin: meta.origin, chain: meta.network.caip2, kind: "earn", payload: earnApprovalPayload(plan, meta) });
|
|
1932
|
-
gate.preApprove(earnPlanMatchers(meta.network, plan), 10 * 6e4);
|
|
2080
|
+
gate.preApprove(earnPlanMatchers(meta.network, plan), 10 * 6e4, meta.account);
|
|
1933
2081
|
}
|
|
1934
2082
|
|
|
1935
2083
|
// src/tonconnect.ts
|
|
@@ -2090,6 +2238,14 @@ var PhishingDetector = class {
|
|
|
2090
2238
|
}
|
|
2091
2239
|
return { host, blocked: false, warnings };
|
|
2092
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
|
+
}
|
|
2093
2249
|
/**
|
|
2094
2250
|
* `metamask.io-login.com`, `app-uniswap.com`, `uniswap.org.evil.net`: a known site's name used as (part of) a label
|
|
2095
2251
|
* of a host that is not that site. Names shorter than 5 characters are skipped (too many honest matches).
|
|
@@ -2300,6 +2456,7 @@ export {
|
|
|
2300
2456
|
NEAR_PROVIDERS,
|
|
2301
2457
|
NON_EVM_CHAINS,
|
|
2302
2458
|
NetworksTable,
|
|
2459
|
+
OriginRefused,
|
|
2303
2460
|
PAGE_TARGET,
|
|
2304
2461
|
PORT_NAME,
|
|
2305
2462
|
PermissionStore,
|
|
@@ -2379,6 +2536,7 @@ export {
|
|
|
2379
2536
|
newRequestId,
|
|
2380
2537
|
nonEvmChain,
|
|
2381
2538
|
normalizeHost,
|
|
2539
|
+
pageRequestFrom,
|
|
2382
2540
|
parseAddChainParams,
|
|
2383
2541
|
parseConnectRequest,
|
|
2384
2542
|
parseRawAddress,
|
|
@@ -2403,6 +2561,7 @@ export {
|
|
|
2403
2561
|
tonConnectErrorCode,
|
|
2404
2562
|
tonProofMessage,
|
|
2405
2563
|
tonSignDataMessage,
|
|
2564
|
+
typedDataMatcher,
|
|
2406
2565
|
vaultSecretStorage,
|
|
2407
2566
|
verifyAssertion
|
|
2408
2567
|
};
|