@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 CHANGED
@@ -43,10 +43,10 @@ pnpm add @anywalletkit/extension-kit @anywalletkit/core @anywalletkit/keyring
43
43
  | Signing | `classifySignRequest`, `prepareTransaction`, `createSignHandler`, `messageText` | connected-address and chain checks, EIP-7702 delegations from sites refused, nonce / gas / fees from the wallet's RPC |
44
44
  | Networks | `NetworksTable`, `DEFAULT_NETWORKS`, `parseAddChainParams`, Solana / TON / Tron endpoints | EVM networks with public RPC failover lists; `wallet_addEthereumChain` only for catalog chains, https only, `eth_chainId` verified, site-supplied RPCs stamped `custom` |
45
45
  | Review gate | `ReviewGate`, `evmCallMatcher`, `chainTxMatcher` | after one sheet reviewed a whole plan, exactly those operations pass once within a TTL |
46
- | Swaps | `quoteRoutes`, `reviewSwapPlan`, `executionFloor`, `SwapsStore`, `SwapTracker`, `parseSwapId` | progressive quotes over swap2 providers, one review sheet per plan, pending swaps resumed across worker restarts |
47
- | Earn | `reviewEarnPlan`, `earnPlanMatchers` | one sheet for an Earn module's plan (approvals + call) |
46
+ | Swaps | `quoteRoutes`, `reviewSwapPlan`, `executionFloor`, `SwapsStore`, `SwapTracker`, `parseSwapId` | progressive quotes over swap2 providers, one review sheet per plan (a gasless order is shown and matched by its EIP-712 hash), pre-approvals bound to the reviewing account (`meta.account`), pending swaps resumed across worker restarts |
47
+ | Earn | `reviewEarnPlan`, `earnPlanMatchers` | one sheet for an Earn module's plan (approvals + call), bound to `meta.account` |
48
48
  | TON Connect | `parseConnectRequest`, `parseSendTransaction`, `parseSignData`, `tonProofMessage`, `tonSignDataMessage` | the pure-data half of TON Connect v2 (the digests `ton_proof` and `signData` sign, to verify them); the account signs with `signTonProof` / `signTonConnectData` (wallet-ton) — `sign()` refuses 32-byte digests |
49
- | Phishing | `PhishingDetector` | MetaMask `eth-phishing-detect` list format, shipped with the build (never fetched at runtime), fuzzy lookalikes, brand-in-label, punycode |
49
+ | Phishing | `PhishingDetector` | MetaMask `eth-phishing-detect` list format, shipped with the build (never fetched at runtime); a listed site is refused (`refusal` → `refuse` on `ApprovalQueue` and `RequestRouter`, 4100, no window); fuzzy lookalikes, brand-in-label, punycode warn |
50
50
  | SIWE | `parseSiwe`, `siweProblems`, `describeSiwe` | EIP-4361 sign-in messages: domain vs. the asking site, address, chain, expiry, nonce (warnings) |
51
51
  | Address poisoning | `lookalikeOf`, `RecipientBook` | warns when a new recipient imitates one the user confirmed |
52
52
  | WebAuthn | `verifyAssertion`, `approvalChallenge`, `derToRaw`, `rawToDer` | ES256 assertion check for "passkey on every signature"; the worker never trusts the page's word |
package/dist/index.cjs CHANGED
@@ -36,6 +36,7 @@ __export(src_exports, {
36
36
  NEAR_PROVIDERS: () => NEAR_PROVIDERS,
37
37
  NON_EVM_CHAINS: () => NON_EVM_CHAINS,
38
38
  NetworksTable: () => NetworksTable,
39
+ OriginRefused: () => OriginRefused,
39
40
  PAGE_TARGET: () => PAGE_TARGET,
40
41
  PORT_NAME: () => PORT_NAME,
41
42
  PermissionStore: () => PermissionStore,
@@ -140,6 +141,7 @@ __export(src_exports, {
140
141
  tonConnectErrorCode: () => tonConnectErrorCode,
141
142
  tonProofMessage: () => tonProofMessage,
142
143
  tonSignDataMessage: () => tonSignDataMessage,
144
+ typedDataMatcher: () => typedDataMatcher,
143
145
  vaultSecretStorage: () => vaultSecretStorage,
144
146
  verifyAssertion: () => verifyAssertion
145
147
  });
@@ -180,6 +182,14 @@ function toArrayBuffer(bytes) {
180
182
  copy.set(bytes);
181
183
  return copy.buffer;
182
184
  }
185
+ async function withSecretBuffer(bytes, fn) {
186
+ const buffer = toArrayBuffer(bytes);
187
+ try {
188
+ return await fn(buffer);
189
+ } finally {
190
+ new Uint8Array(buffer).fill(0);
191
+ }
192
+ }
183
193
 
184
194
  // src/vault.ts
185
195
  var VAULT_MIN_ITERATIONS = 6e5;
@@ -447,7 +457,13 @@ var Vault = class {
447
457
  }
448
458
  async _derivePbkdf2(password, salt, iterations) {
449
459
  const subtle = this._crypto.subtle;
450
- const material = await subtle.importKey("raw", toArrayBuffer(new TextEncoder().encode(password.normalize("NFKC"))), "PBKDF2", false, ["deriveKey"]);
460
+ const pw = new TextEncoder().encode(password.normalize("NFKC"));
461
+ let material;
462
+ try {
463
+ material = await withSecretBuffer(pw, (b) => subtle.importKey("raw", b, "PBKDF2", false, ["deriveKey"]));
464
+ } finally {
465
+ (0, import_core.zeroize)(pw);
466
+ }
451
467
  return subtle.deriveKey({ name: "PBKDF2", hash: "SHA-256", salt: toArrayBuffer(salt), iterations }, material, { name: "AES-GCM", length: 256 }, false, ["encrypt", "decrypt"]);
452
468
  }
453
469
  async _deriveScrypt(password, salt, cost) {
@@ -459,7 +475,7 @@ var Vault = class {
459
475
  (0, import_core.zeroize)(pw);
460
476
  }
461
477
  try {
462
- return await this._crypto.subtle.importKey("raw", toArrayBuffer(raw), { name: "AES-GCM", length: 256 }, false, ["encrypt", "decrypt"]);
478
+ return await withSecretBuffer(raw, (b) => this._crypto.subtle.importKey("raw", b, { name: "AES-GCM", length: 256 }, false, ["encrypt", "decrypt"]));
463
479
  } finally {
464
480
  (0, import_core.zeroize)(raw);
465
481
  }
@@ -467,16 +483,16 @@ var Vault = class {
467
483
  /** HKDF-SHA256(ikm = PRF output, salt = credential id bytes, info) → AES-256-GCM key. */
468
484
  async _derivePasskeyKek(prf, credentialId) {
469
485
  const subtle = this._crypto.subtle;
470
- const ikm = await subtle.importKey("raw", toArrayBuffer(prf), "HKDF", false, ["deriveKey"]);
486
+ const ikm = await withSecretBuffer(prf, (b) => subtle.importKey("raw", b, "HKDF", false, ["deriveKey"]));
471
487
  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"]);
472
488
  }
473
489
  async _importDek(dek) {
474
490
  if (dek.length !== DEK_BYTES) throw new VaultError("Vault key must be 32 bytes.", "invalid-input");
475
- return this._crypto.subtle.importKey("raw", toArrayBuffer(dek), { name: "AES-GCM", length: 256 }, false, ["encrypt", "decrypt"]);
491
+ return withSecretBuffer(dek, (b) => this._crypto.subtle.importKey("raw", b, { name: "AES-GCM", length: 256 }, false, ["encrypt", "decrypt"]));
476
492
  }
477
493
  async _seal(key, plaintext, aad) {
478
494
  const iv = this._random(IV_BYTES);
479
- const ct = await this._crypto.subtle.encrypt({ name: "AES-GCM", iv: toArrayBuffer(iv), additionalData: toArrayBuffer(new TextEncoder().encode(aad)) }, key, toArrayBuffer(plaintext));
495
+ const ct = await withSecretBuffer(plaintext, (b) => this._crypto.subtle.encrypt({ name: "AES-GCM", iv: toArrayBuffer(iv), additionalData: toArrayBuffer(new TextEncoder().encode(aad)) }, key, b));
480
496
  return { iv: toBase64(iv), data: toBase64(new Uint8Array(ct)) };
481
497
  }
482
498
  async _open(key, box, aad, onFail) {
@@ -661,22 +677,40 @@ var Session = class {
661
677
  if (was !== void 0 && was !== null) for (const cb of this._onLock) cb();
662
678
  }
663
679
  _expiry() {
664
- return this._autoLockMs > 0 ? this._now() + this._autoLockMs : Number.POSITIVE_INFINITY;
680
+ return this._autoLockMs > 0 ? this._now() + this._autoLockMs : null;
665
681
  }
666
682
  async _arm(expiresAt) {
667
- if (Number.isFinite(expiresAt)) await this._alarms.schedule(this._alarm, expiresAt);
683
+ if (expiresAt !== null) await this._alarms.schedule(this._alarm, expiresAt);
668
684
  else await this._alarms.clear(this._alarm);
669
685
  }
686
+ /** The stored record, or `null` when locked. A record that is there but unreadable locks the wallet (removed, listeners told). */
670
687
  async _read() {
671
688
  const raw = await this._storage.get(this._key);
672
- if (!raw || typeof raw.key !== "string" || typeof raw.expiresAt !== "number") return null;
673
- if (raw.expiresAt <= this._now()) {
689
+ if (raw === void 0 || raw === null) return null;
690
+ const r = raw;
691
+ const expiryOk = r.expiresAt === null || typeof r.expiresAt === "number" && Number.isFinite(r.expiresAt);
692
+ if (typeof raw !== "object" || typeof r.key !== "string" || !expiryOk || !isKey(r.key)) {
674
693
  await this._lockNow();
675
694
  return null;
676
695
  }
677
- return raw;
696
+ if (r.expiresAt !== null && r.expiresAt <= this._now()) {
697
+ await this._lockNow();
698
+ return null;
699
+ }
700
+ return r;
678
701
  }
679
702
  };
703
+ function isKey(text) {
704
+ let bytes;
705
+ try {
706
+ bytes = fromBase64(text);
707
+ } catch {
708
+ return false;
709
+ }
710
+ const ok = bytes.length === 32;
711
+ (0, import_core2.zeroize)(bytes);
712
+ return ok;
713
+ }
680
714
  function memorySessionStorage() {
681
715
  const data = /* @__PURE__ */ new Map();
682
716
  return {
@@ -985,14 +1019,27 @@ var NetworksTable = class {
985
1019
  return this._loading;
986
1020
  }
987
1021
  };
1022
+ function isPrivateRpcHost(url) {
1023
+ const m = /^https:\/\/(?:[^@/?#]*@)?(\[[^\]]+\]|[^:/?#]+)/i.exec(url);
1024
+ if (!m) return true;
1025
+ const h = m[1].toLowerCase().replace(/^\[|\]$/g, "").replace(/\.$/, "");
1026
+ if (h === "localhost" || h.endsWith(".localhost") || h.endsWith(".local") || h === "0.0.0.0") return true;
1027
+ const v4 = /^(\d{1,3})\.(\d{1,3})\.\d{1,3}\.\d{1,3}$/.exec(h);
1028
+ if (v4) {
1029
+ const a = Number(v4[1]), b = Number(v4[2]);
1030
+ return a === 127 || a === 10 || a === 0 || a === 172 && b >= 16 && b <= 31 || a === 192 && b === 168 || a === 169 && b === 254;
1031
+ }
1032
+ if (h.includes(":")) return h === "::1" || h === "::" || /^f[cd]/.test(h) || /^fe[89ab]/.test(h);
1033
+ return false;
1034
+ }
988
1035
  function parseAddChainParams(params, known = isCatalogChain) {
989
1036
  const p = Array.isArray(params) ? params[0] : params;
990
1037
  if (!p || typeof p !== "object") return relayError(RELAY_ERRORS.invalidParams, "wallet_addEthereumChain expects one object parameter.");
991
1038
  const caip2 = caip2FromChainId(p.chainId);
992
1039
  if (!caip2) return relayError(RELAY_ERRORS.invalidParams, "chainId must be a 0x-prefixed hex string.");
993
1040
  if (!known(caip2)) return relayError(RELAY_ERRORS.chainNotAdded, `Chain ${String(p.chainId)} is not in this wallet's catalog.`);
994
- const rpcs = Array.isArray(p.rpcUrls) ? p.rpcUrls.filter((u) => typeof u === "string" && /^https:\/\/[^\s/?#]+/i.test(u)) : [];
995
- if (rpcs.length === 0) return relayError(RELAY_ERRORS.invalidParams, "rpcUrls must contain at least one https URL.");
1041
+ const rpcs = Array.isArray(p.rpcUrls) ? p.rpcUrls.filter((u) => typeof u === "string" && /^https:\/\/[^\s/?#]+/i.test(u) && !isPrivateRpcHost(u)) : [];
1042
+ 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).");
996
1043
  const name = typeof p.chainName === "string" && p.chainName.trim() ? p.chainName.trim().slice(0, 64) : caip2;
997
1044
  const currency = p.nativeCurrency;
998
1045
  if (currency && currency.decimals !== void 0 && currency.decimals !== 18) return relayError(RELAY_ERRORS.invalidParams, "nativeCurrency.decimals must be 18.");
@@ -1189,6 +1236,13 @@ var QueueLimitError = class extends Error {
1189
1236
  }
1190
1237
  code;
1191
1238
  };
1239
+ var OriginRefused = class extends Error {
1240
+ code = RELAY_ERRORS.unauthorized;
1241
+ constructor(message) {
1242
+ super(message);
1243
+ this.name = "OriginRefused";
1244
+ }
1245
+ };
1192
1246
  var APPROVALS_MAX_PENDING_PER_ORIGIN = 3;
1193
1247
  var ApprovalQueue = class {
1194
1248
  constructor(window, newId = () => crypto.randomUUID(), options = {}) {
@@ -1208,6 +1262,13 @@ var ApprovalQueue = class {
1208
1262
  _via = /* @__PURE__ */ new Map();
1209
1263
  /** Enqueue and wait for the user's answer. Refuses (never queues) past the per-origin limits. */
1210
1264
  request(input) {
1265
+ let refused;
1266
+ try {
1267
+ refused = this.options.refuse?.(input.origin);
1268
+ } catch (e) {
1269
+ refused = `The wallet could not check this site (${e instanceof Error ? e.message : String(e)}).`;
1270
+ }
1271
+ if (refused) return Promise.reject(new OriginRefused(refused));
1211
1272
  const sameOrigin = [...this._current ? [this._current] : [], ...this._waiting].filter((p) => p.request.origin === input.origin);
1212
1273
  if (input.kind === "connect" && sameOrigin.some((p) => p.request.kind === "connect")) {
1213
1274
  return Promise.reject(new QueueLimitError(RELAY_ERRORS.resourceUnavailable, "A connect request from this site is already waiting for your answer."));
@@ -1396,6 +1457,8 @@ var RequestRouter = class {
1396
1457
  return await this.deps.networks.get(wanted) ?? await this.deps.networks.get(DEFAULT_CHAIN);
1397
1458
  }
1398
1459
  async handle(origin, chain, method, rawParams) {
1460
+ const refused = this.deps.refuse?.(origin);
1461
+ if (refused) throw new RouterError(RELAY_ERRORS.unauthorized, refused);
1399
1462
  if (chain !== "eip155") throw new RouterError(RELAY_ERRORS.unsupportedMethod, `${chain} providers are not available yet.`);
1400
1463
  const params = Array.isArray(rawParams) ? rawParams : rawParams === void 0 ? [] : [rawParams];
1401
1464
  const network = await this.chainFor(origin);
@@ -1704,6 +1767,23 @@ var bigintSafe = (_k, v) => typeof v === "bigint" ? v.toString() : v;
1704
1767
  var hex = (n) => `0x${n.toString(16)}`;
1705
1768
  var big = (v) => BigInt(v);
1706
1769
  var MIN_TIP_WEI = 100000000n;
1770
+ async function readFees(deps, network, block) {
1771
+ const b = block === void 0 ? await deps.rpc(network, "eth_getBlockByNumber", ["latest", false]) : block;
1772
+ const base = b?.baseFeePerGas != null ? big(b.baseFeePerGas) : null;
1773
+ if (base === null) return { gasPrice: big(await deps.rpc(network, "eth_gasPrice", [])) };
1774
+ const floor = deps.tipFloor?.(network.chainId) ?? MIN_TIP_WEI;
1775
+ const nodeTip = await deps.rpc(network, "eth_maxPriorityFeePerGas", []).then(big).catch(() => 0n);
1776
+ return { base, tip: nodeTip > floor ? nodeTip : floor };
1777
+ }
1778
+ function withFees(chainId, nonce, gasLimit, fees) {
1779
+ if ("gasPrice" in fees) return { chainId, nonce, gasLimit: hex(gasLimit), gasPrice: hex(fees.gasPrice), maxFee: (gasLimit * fees.gasPrice).toString() };
1780
+ const maxFeePerGas = fees.base * 2n + fees.tip;
1781
+ return { chainId, nonce, gasLimit: hex(gasLimit), maxFeePerGas: hex(maxFeePerGas), maxPriorityFeePerGas: hex(fees.tip), maxFee: (gasLimit * maxFeePerGas).toString() };
1782
+ }
1783
+ function outgrown(shown, now) {
1784
+ if ("gasPrice" in now) return shown.gasPrice === void 0 || now.gasPrice > big(shown.gasPrice);
1785
+ return shown.maxFeePerGas === void 0 || now.base + now.tip > big(shown.maxFeePerGas);
1786
+ }
1707
1787
  async function prepareTransaction(deps, network, from, tx) {
1708
1788
  const call = { from, to: tx.to, value: hex(BigInt(tx.value)), ...tx.data ? { data: tx.data } : {} };
1709
1789
  const [nonceHex, block, gasHex] = await Promise.all([
@@ -1713,18 +1793,20 @@ async function prepareTransaction(deps, network, from, tx) {
1713
1793
  ]);
1714
1794
  const estimated = big(gasHex);
1715
1795
  const gasLimit = tx.gas ? estimated : (estimated * 120n + 99n) / 100n;
1716
- const base = block?.baseFeePerGas != null ? big(block.baseFeePerGas) : null;
1717
- if (base === null) {
1718
- const gasPrice = big(await deps.rpc(network, "eth_gasPrice", []));
1719
- return { chainId: network.chainId, nonce: safeNonce(big(nonceHex)), gasLimit: hex(gasLimit), gasPrice: hex(gasPrice), maxFee: (gasLimit * gasPrice).toString() };
1720
- }
1721
- const floor = deps.tipFloor?.(network.chainId) ?? MIN_TIP_WEI;
1722
- const nodeTip = await deps.rpc(network, "eth_maxPriorityFeePerGas", []).then(big).catch(() => 0n);
1723
- const tip = nodeTip > floor ? nodeTip : floor;
1724
- const maxFeePerGas = base * 2n + tip;
1725
- return { chainId: network.chainId, nonce: safeNonce(big(nonceHex)), gasLimit: hex(gasLimit), maxFeePerGas: hex(maxFeePerGas), maxPriorityFeePerGas: hex(tip), maxFee: (gasLimit * maxFeePerGas).toString() };
1796
+ return withFees(network.chainId, safeNonce(big(nonceHex)), gasLimit, await readFees(deps, network, block));
1726
1797
  }
1727
1798
  function createSignHandler(deps) {
1799
+ const tails = /* @__PURE__ */ new Map();
1800
+ const nextNonce = /* @__PURE__ */ new Map();
1801
+ const serial = (key, fn) => {
1802
+ const run = (tails.get(key) ?? Promise.resolve()).then(fn, fn);
1803
+ const tail = run.then(() => void 0, () => void 0);
1804
+ tails.set(key, tail);
1805
+ void tail.then(() => {
1806
+ if (tails.get(key) === tail) tails.delete(key);
1807
+ });
1808
+ return run;
1809
+ };
1728
1810
  return async ({ origin, network, method, params, accounts }) => {
1729
1811
  const via = deps.approvals.viaFor(origin);
1730
1812
  const info = classifySignRequest({ network, method, params, accounts, origin, ...via ? { via } : {} });
@@ -1733,6 +1815,41 @@ function createSignHandler(deps) {
1733
1815
  const preview = deps.preview ? await deps.preview({ network, request: info, accounts }).catch(() => null) : null;
1734
1816
  const payload = { request: info, network, ...prepared ? { prepared } : {}, preview };
1735
1817
  await deps.approvals.request({ origin, chain: network.caip2, kind: info.kind, payload });
1818
+ if (info.kind === "send-transaction") {
1819
+ const key = `${network.caip2}|${lc(info.account)}`;
1820
+ return serial(key, async () => {
1821
+ let shown = prepared;
1822
+ for (; ; ) {
1823
+ const [nonceHex, fees] = await Promise.all([deps.rpc(network, "eth_getTransactionCount", [info.account, "pending"]), readFees(deps, network)]);
1824
+ const local = nextNonce.get(key) ?? 0;
1825
+ const nodeNonce = safeNonce(big(nonceHex));
1826
+ const nonce = nodeNonce > local ? nodeNonce : local;
1827
+ if (outgrown(shown, fees)) {
1828
+ shown = withFees(network.chainId, nonce, big(shown.gasLimit), fees);
1829
+ await deps.approvals.request({ origin, chain: network.caip2, kind: info.kind, payload: { ...payload, prepared: shown, repriced: true } });
1830
+ continue;
1831
+ }
1832
+ const handle2 = await deps.signerFor(info.account, network);
1833
+ let raw;
1834
+ try {
1835
+ raw = await handle2.signer.signTransaction({
1836
+ chainId: shown.chainId,
1837
+ nonce,
1838
+ to: info.tx.to,
1839
+ value: BigInt(info.tx.value),
1840
+ data: info.tx.data ?? "0x",
1841
+ gasLimit: BigInt(shown.gasLimit),
1842
+ ...shown.gasPrice !== void 0 ? { gasPrice: BigInt(shown.gasPrice) } : { maxFeePerGas: BigInt(shown.maxFeePerGas), maxPriorityFeePerGas: BigInt(shown.maxPriorityFeePerGas) }
1843
+ });
1844
+ } finally {
1845
+ handle2.dispose();
1846
+ }
1847
+ const hash = await deps.rpc(network, "eth_sendRawTransaction", [raw]);
1848
+ nextNonce.set(key, nonce + 1);
1849
+ return hash;
1850
+ }
1851
+ });
1852
+ }
1736
1853
  const handle = await deps.signerFor(info.account, network);
1737
1854
  try {
1738
1855
  switch (info.kind) {
@@ -1740,19 +1857,6 @@ function createSignHandler(deps) {
1740
1857
  return await handle.signer.signMessage(HEX.test(info.raw) ? Uint8Array.from(info.raw.slice(2).match(/../g) ?? [], (b) => Number.parseInt(b, 16)) : info.raw);
1741
1858
  case "sign-typed-data":
1742
1859
  return await handle.signer.signTypedData(info.typedData);
1743
- case "send-transaction": {
1744
- const p = prepared;
1745
- const raw = await handle.signer.signTransaction({
1746
- chainId: p.chainId,
1747
- nonce: p.nonce,
1748
- to: info.tx.to,
1749
- value: BigInt(info.tx.value),
1750
- data: info.tx.data ?? "0x",
1751
- gasLimit: BigInt(p.gasLimit),
1752
- ...p.gasPrice !== void 0 ? { gasPrice: BigInt(p.gasPrice) } : { maxFeePerGas: BigInt(p.maxFeePerGas), maxPriorityFeePerGas: BigInt(p.maxPriorityFeePerGas) }
1753
- });
1754
- return await deps.rpc(network, "eth_sendRawTransaction", [raw]);
1755
- }
1756
1860
  }
1757
1861
  } finally {
1758
1862
  handle.dispose();
@@ -1761,22 +1865,28 @@ function createSignHandler(deps) {
1761
1865
  }
1762
1866
 
1763
1867
  // src/gate.ts
1868
+ var HEX_ADDRESS = /^0x[0-9a-fA-F]{40}$/;
1869
+ var sameAccount = (a, b) => a === b || HEX_ADDRESS.test(a) && a.toLowerCase() === b.toLowerCase();
1764
1870
  var ReviewGate = class {
1765
1871
  constructor(now = Date.now) {
1766
1872
  this.now = now;
1767
1873
  }
1768
1874
  now;
1769
1875
  approved = [];
1770
- /** Each matcher passes ONE operation, within `ttlMs` (default 10 min). */
1771
- preApprove(matchers, ttlMs = 10 * 6e4) {
1876
+ /**
1877
+ * Each matcher passes ONE operation, within `ttlMs` (default 10 min). With `account`, only an op
1878
+ * whose `account` is that account (an op without one does not pass). `reviewSwapPlan` and
1879
+ * `reviewEarnPlan` always bind.
1880
+ */
1881
+ preApprove(matchers, ttlMs = 10 * 6e4, account) {
1772
1882
  const until = this.now() + ttlMs;
1773
- this.approved.push(...matchers.map((match) => ({ match, until })));
1883
+ this.approved.push(...matchers.map((match) => ({ match, until, ...account ? { account } : {} })));
1774
1884
  }
1775
1885
  /** True (and the matcher is spent) when a pre-approval covers `op`. */
1776
1886
  consume(op) {
1777
1887
  const now = this.now();
1778
1888
  this.approved = this.approved.filter((a) => a.until > now);
1779
- const i = this.approved.findIndex((a) => a.match(op));
1889
+ const i = this.approved.findIndex((a) => (a.account === void 0 || op.account !== void 0 && sameAccount(a.account, op.account)) && a.match(op));
1780
1890
  if (i < 0) return false;
1781
1891
  this.approved.splice(i, 1);
1782
1892
  return true;
@@ -1793,12 +1903,17 @@ function evmCallMatcher(expected) {
1793
1903
  const data = expected.data && expected.data !== "0x" ? lc2(expected.data) : void 0;
1794
1904
  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;
1795
1905
  }
1906
+ function typedDataMatcher(expected) {
1907
+ const hash = lc2(expected.typedDataHash);
1908
+ return (op) => op.kind === "evm-typed-data" && op.caip2 === expected.caip2 && lc2(op.typedDataHash) === hash;
1909
+ }
1796
1910
  function chainTxMatcher(expected) {
1797
1911
  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 ?? "");
1798
1912
  }
1799
1913
 
1800
1914
  // src/swap.ts
1801
1915
  var import_protocol_swap2_core = require("@anywalletkit/protocol-swap2-core");
1916
+ var import_wallet_evm = require("@anywalletkit/wallet-evm");
1802
1917
  async function quoteRoutes(providers, params, options = {}) {
1803
1918
  const timeoutMs = options.timeoutMs ?? 2e4;
1804
1919
  const from = { chain: params.sourceChain, token: params.sourceToken };
@@ -1904,7 +2019,8 @@ function swapApprovalPayload(plan, meta) {
1904
2019
  source = { kind: "serialized" };
1905
2020
  checks.push({ status: "ok", label: "Balance changes are proven before signing (SDK guard)" });
1906
2021
  } else {
1907
- source = { kind: "typed-order" };
2022
+ const typedData = parseTypedOrder(s.typedData);
2023
+ source = { kind: "typed-order", typedData, text: JSON.stringify(typedData, null, 2) };
1908
2024
  checks.push({ status: "ok", label: "Signed order \u2014 no transaction is sent from your wallet" });
1909
2025
  }
1910
2026
  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" });
@@ -1929,6 +2045,11 @@ function swapApprovalPayload(plan, meta) {
1929
2045
  checks
1930
2046
  };
1931
2047
  }
2048
+ function parseTypedOrder(json) {
2049
+ const t = JSON.parse(json);
2050
+ 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.");
2051
+ return t;
2052
+ }
1932
2053
  function planMatchers(plan) {
1933
2054
  const m = plan.approvals.map((a) => evmCallMatcher({ caip2: plan.fromChain, to: a.token, data: encodeApproveCalldata(a.spender, a.amount), value: 0n }));
1934
2055
  const s = plan.source;
@@ -1943,16 +2064,18 @@ function planMatchers(plan) {
1943
2064
  } else if (s.kind === "serialized") {
1944
2065
  m.push((op) => op.kind === "solana-serialized" && op.serializedTx === s.serializedTx);
1945
2066
  } else {
1946
- m.push((op) => op.kind === "evm-typed-data" && op.caip2 === plan.fromChain);
2067
+ m.push(typedDataMatcher({ caip2: plan.fromChain, typedDataHash: (0, import_wallet_evm.typedDataHash)(parseTypedOrder(s.typedData)) }));
1947
2068
  }
1948
2069
  return m;
1949
2070
  }
1950
2071
  function reviewSwapPlan(approvals, gate, meta) {
2072
+ if (typeof meta.account !== "string" || !meta.account) throw new TypeError("reviewSwapPlan needs meta.account: the gate passes the plan for that account only.");
1951
2073
  return async (plan) => {
2074
+ const matchers = planMatchers(plan);
1952
2075
  const payload = swapApprovalPayload(plan, meta);
1953
2076
  await approvals.request({ origin: meta.origin, chain: plan.fromChain, kind: "swap", payload });
1954
2077
  gate.clear();
1955
- gate.preApprove(planMatchers(plan), Math.max(6e4, plan.validUntilMs - Date.now() + 5 * 6e4));
2078
+ gate.preApprove(matchers, Math.max(6e4, plan.validUntilMs - Date.now() + 5 * 6e4), meta.account);
1956
2079
  };
1957
2080
  }
1958
2081
 
@@ -1962,11 +2085,13 @@ var ACTIVE_STATUSES = /* @__PURE__ */ new Set(["pending", "action-required", "re
1962
2085
  function parseSwapId(id) {
1963
2086
  if (!id.startsWith(`${import_protocol_swap2_core2.SWAP2_ID_PREFIX}.`)) return null;
1964
2087
  const parts = id.slice(import_protocol_swap2_core2.SWAP2_ID_PREFIX.length + 1).split(".");
1965
- if (parts.length < 2 || parts.length > 3 || !parts[0] || !parts[1]) return null;
2088
+ if (parts.length < 2 || parts.length > 4 || !parts[0] || !parts[1]) return null;
1966
2089
  const provider = decodeURIComponent(parts[0]);
1967
2090
  const intentId = decodeURIComponent(parts[1]);
1968
2091
  const sourceTxHash = parts[2] ? decodeURIComponent(parts[2]) : void 0;
1969
- return sourceTxHash ? { provider, intentId, sourceTxHash } : { provider, intentId };
2092
+ const destTxHash = parts[3] ? decodeURIComponent(parts[3]) : void 0;
2093
+ if (destTxHash && !sourceTxHash) return null;
2094
+ return { provider, intentId, ...sourceTxHash ? { sourceTxHash } : {}, ...destTxHash ? { destTxHash } : {} };
1970
2095
  }
1971
2096
  var SwapsStore = class {
1972
2097
  constructor(storage) {
@@ -1975,9 +2100,14 @@ var SwapsStore = class {
1975
2100
  storage;
1976
2101
  _records = null;
1977
2102
  _listeners = /* @__PURE__ */ new Set();
2103
+ /**
2104
+ * add / update / remove run here one after another, each reading the list inside its turn: an
2105
+ * alarm tick's update that read the list before a new swap was added would otherwise write the
2106
+ * old list back and drop the swap.
2107
+ */
2108
+ _queue = Promise.resolve();
1978
2109
  async list() {
1979
- this._records ??= await this.storage.get() ?? [];
1980
- return [...this._records].sort((a, b) => b.createdAt - a.createdAt);
2110
+ return [...await this._all()].sort((a, b) => b.createdAt - a.createdAt);
1981
2111
  }
1982
2112
  async get(id) {
1983
2113
  return (await this.list()).find((r) => r.id === id) ?? null;
@@ -1985,26 +2115,41 @@ var SwapsStore = class {
1985
2115
  async active() {
1986
2116
  return (await this.list()).filter((r) => ACTIVE_STATUSES.has(r.status));
1987
2117
  }
1988
- async add(record) {
1989
- const all = (await this.list()).filter((r) => r.id !== record.id);
1990
- await this.write([...all, record]);
2118
+ add(record) {
2119
+ return this._serial(async () => {
2120
+ const all = (await this._all()).filter((r) => r.id !== record.id);
2121
+ await this.write([...all, record]);
2122
+ });
1991
2123
  }
1992
- async update(id, patch) {
1993
- const all = await this.list();
1994
- const i = all.findIndex((r) => r.id === id);
1995
- if (i < 0) return null;
1996
- const next = { ...all[i], ...patch, updatedAt: patch.updatedAt ?? Date.now() };
1997
- all[i] = next;
1998
- await this.write(all);
1999
- return next;
2124
+ update(id, patch) {
2125
+ return this._serial(async () => {
2126
+ const all = [...await this._all()];
2127
+ const i = all.findIndex((r) => r.id === id);
2128
+ if (i < 0) return null;
2129
+ const next = { ...all[i], ...patch, updatedAt: patch.updatedAt ?? Date.now() };
2130
+ all[i] = next;
2131
+ await this.write(all);
2132
+ return next;
2133
+ });
2000
2134
  }
2001
- async remove(id) {
2002
- await this.write((await this.list()).filter((r) => r.id !== id));
2135
+ remove(id) {
2136
+ return this._serial(async () => {
2137
+ await this.write((await this._all()).filter((r) => r.id !== id));
2138
+ });
2003
2139
  }
2004
2140
  onChange(listener) {
2005
2141
  this._listeners.add(listener);
2006
2142
  return () => this._listeners.delete(listener);
2007
2143
  }
2144
+ async _all() {
2145
+ this._records ??= await this.storage.get() ?? [];
2146
+ return this._records;
2147
+ }
2148
+ _serial(fn) {
2149
+ const run = this._queue.then(fn, fn);
2150
+ this._queue = run.then(() => void 0, () => void 0);
2151
+ return run;
2152
+ }
2008
2153
  async write(records) {
2009
2154
  await this.storage.set(records);
2010
2155
  this._records = records;
@@ -2079,8 +2224,9 @@ function earnApprovalPayload(plan, meta) {
2079
2224
  return { module: meta.module, action: meta.action, network: meta.network, facts: meta.facts, approvals: plan.approvals, tx: plan.tx, checks, preview: meta.preview ?? null };
2080
2225
  }
2081
2226
  async function reviewEarnPlan(approvals, gate, meta, plan) {
2227
+ if (typeof meta.account !== "string" || !meta.account) throw new TypeError("reviewEarnPlan needs meta.account: the gate passes the plan for that account only.");
2082
2228
  await approvals.request({ origin: meta.origin, chain: meta.network.caip2, kind: "earn", payload: earnApprovalPayload(plan, meta) });
2083
- gate.preApprove(earnPlanMatchers(meta.network, plan), 10 * 6e4);
2229
+ gate.preApprove(earnPlanMatchers(meta.network, plan), 10 * 6e4, meta.account);
2084
2230
  }
2085
2231
 
2086
2232
  // src/tonconnect.ts
@@ -2241,6 +2387,14 @@ var PhishingDetector = class {
2241
2387
  }
2242
2388
  return { host, blocked: false, warnings };
2243
2389
  }
2390
+ /**
2391
+ * Why `hostOrOrigin` must be refused outright, or `null`: a host on the blocklist (or under a
2392
+ * listed domain) and not on the allowlist. Lookalikes and punycode only warn ({@link check}).
2393
+ * Wire it as `ApprovalQueueOptions.refuse` and `RouterDeps.refuse`.
2394
+ */
2395
+ refusal(hostOrOrigin) {
2396
+ return this.check(hostOrOrigin).blocked ? "This site is on the wallet's phishing list; the wallet refuses its requests." : null;
2397
+ }
2244
2398
  /**
2245
2399
  * `metamask.io-login.com`, `app-uniswap.com`, `uniswap.org.evil.net`: a known site's name used as (part of) a label
2246
2400
  * of a host that is not that site. Names shorter than 5 characters are skipped (too many honest matches).