@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/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 {
@@ -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
- const base = block?.baseFeePerGas != null ? big(block.baseFeePerGas) : null;
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
- /** Each matcher passes ONE operation, within `ttlMs` (default 10 min). */
1620
- 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) {
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
- source = { kind: "typed-order" };
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((op) => op.kind === "evm-typed-data" && op.caip2 === plan.fromChain);
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(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);
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 > 3 || !parts[0] || !parts[1]) return null;
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
- 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 } : {} };
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
- this._records ??= await this.storage.get() ?? [];
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
- async add(record) {
1838
- const all = (await this.list()).filter((r) => r.id !== record.id);
1839
- 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
+ });
1840
1974
  }
1841
- async update(id, patch) {
1842
- const all = await this.list();
1843
- const i = all.findIndex((r) => r.id === id);
1844
- if (i < 0) return null;
1845
- const next = { ...all[i], ...patch, updatedAt: patch.updatedAt ?? Date.now() };
1846
- all[i] = next;
1847
- await this.write(all);
1848
- 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
+ });
1849
1985
  }
1850
- async remove(id) {
1851
- 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
+ });
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
  };