@anywalletkit/extension-kit 0.1.5 → 0.2.1

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/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 material = await subtle.importKey("raw", toArrayBuffer(new TextEncoder().encode(password.normalize("NFKC"))), "PBKDF2", false, ["deriveKey"]);
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", toArrayBuffer(raw), { name: "AES-GCM", length: 256 }, false, ["encrypt", "decrypt"]);
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", toArrayBuffer(prf), "HKDF", false, ["deriveKey"]);
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", toArrayBuffer(dek), { name: "AES-GCM", length: 256 }, false, ["encrypt", "decrypt"]);
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, toArrayBuffer(plaintext));
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 : Number.POSITIVE_INFINITY;
531
+ return this._autoLockMs > 0 ? this._now() + this._autoLockMs : null;
518
532
  }
519
533
  async _arm(expiresAt) {
520
- if (Number.isFinite(expiresAt)) await this._alarms.schedule(this._alarm, expiresAt);
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 (!raw || typeof raw.key !== "string" || typeof raw.expiresAt !== "number") return null;
526
- if (raw.expiresAt <= this._now()) {
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
- return raw;
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
- const base = block?.baseFeePerGas != null ? big(block.baseFeePerGas) : null;
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
- /** Each matcher passes ONE operation, within `ttlMs` (default 10 min). */
1624
- preApprove(matchers, ttlMs = 10 * 6e4) {
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
- source = { kind: "typed-order" };
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((op) => op.kind === "evm-typed-data" && op.caip2 === plan.fromChain);
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(planMatchers(plan), Math.max(6e4, plan.validUntilMs - Date.now() + 5 * 6e4));
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 > 3 || !parts[0] || !parts[1]) return null;
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
- return sourceTxHash ? { provider, intentId, sourceTxHash } : { provider, intentId };
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
- this._records ??= await this.storage.get() ?? [];
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
- async add(record) {
1842
- const all = (await this.list()).filter((r) => r.id !== record.id);
1843
- await this.write([...all, record]);
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
- async update(id, patch) {
1846
- const all = await this.list();
1847
- const i = all.findIndex((r) => r.id === id);
1848
- if (i < 0) return null;
1849
- const next = { ...all[i], ...patch, updatedAt: patch.updatedAt ?? Date.now() };
1850
- all[i] = next;
1851
- await this.write(all);
1852
- return next;
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
- async remove(id) {
1855
- await this.write((await this.list()).filter((r) => r.id !== id));
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
  };