@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 CHANGED
@@ -36,17 +36,17 @@ pnpm add @anywalletkit/extension-kit @anywalletkit/core @anywalletkit/keyring
36
36
  | Vault | `Vault`, `chromeLocalVaultStorage`, `memoryVaultStorage`, `VAULT_SCRYPT`, `VAULT_LIMITS`, `VAULT_MIN_PASSWORD_LENGTH` | every secret at rest in `chrome.storage.local`, two key layers (a KEK wraps a DEK, the DEK seals each secret); passkey (WebAuthn PRF) unlock as a second wrap of the same DEK |
37
37
  | Session | `Session`, `chromeSessionStorage`, `chromeAlarms`, `SESSION_DEFAULT_AUTOLOCK_MS` | the DEK in `chrome.storage.session` with an expiry and an alarm; `getKey()` re-checks the expiry itself |
38
38
  | Keyring adapters | `chromeKeyringStorage`, `vaultSecretStorage` | `@anywalletkit/keyring` metadata as plain JSON, secrets through the vault, one DEK use per call |
39
- | Relay | `PageRequest` / `RelayRequest` shapes and guards, `stampOrigin`, `checkSenderOrigin`, `RELAY_ERRORS` | page ⇄ content script ⇄ service worker messages; the origin is the browser's (`port.sender`), never the page's |
39
+ | Relay | `PageRequest` / `RelayRequest` shapes and guards, `pageRequestFrom`, `stampOrigin`, `checkSenderOrigin`, `RELAY_ERRORS` | page ⇄ content script ⇄ service worker messages; the content script takes only messages the page window itself posted (`pageRequestFrom(event, window)`: an iframe's `parent.postMessage` is not the site), and the origin is the browser's (`port.sender`), never the page's |
40
40
  | Router | `RequestRouter`, `READ_METHODS`, `SIGNING_METHODS`, `toRelayError` | what a dApp may call: accounts only for connected origins, chain add / switch through the approval window, reads from the wallet's own RPCs, `eth_sign` never offered, everything else 4200 |
41
41
  | Permissions | `PermissionStore`, `CHAIN_ACCOUNT_KEYS` | per-origin accounts (EVM, Solana, TON, Tron, Bitcoin, NEAR) and chain |
42
42
  | Approvals | `ApprovalQueue`, `ReviewDeclined`, `QueueLimitError`, `APPROVALS_MAX_PENDING_PER_ORIGIN` | one approval window at a time, at most 3 pending requests per origin and one pending `connect`; transport notes (`setVia`) for WalletConnect peers |
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) |
48
- | TON Connect | `parseConnectRequest`, `parseSendTransaction`, `parseSignData`, `tonProofMessage`, `tonSignDataMessage` | the pure-data half of TON Connect v2 (the bytes `ton_proof` and `signData` sign) |
49
- | Phishing | `PhishingDetector` | MetaMask `eth-phishing-detect` list format, shipped with the build (never fetched at runtime), fuzzy lookalikes, brand-in-label, punycode |
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
+ | 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); 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 |
@@ -76,8 +76,11 @@ pnpm add @anywalletkit/extension-kit @anywalletkit/core @anywalletkit/keyring
76
76
  - **Errors to pages.** `toRelayError(e, { log })` passes the wallet's own refusals
77
77
  (EIP-1193 codes) and a node's JSON-RPC error (code, revert data) with URLs
78
78
  removed; anything else is `-32603 Internal error` on the page and goes to `log`.
79
- - **Origins.** The service worker answers a relay message only when the
80
- content script's stamped origin equals `port.sender.origin`. Permissions are
79
+ - **Origins.** The content script relays only what the page window itself
80
+ posted (`pageRequestFrom`: `event.source` is the page, `event.origin` its
81
+ origin), so an ad or embed in an iframe cannot speak as the site. The
82
+ service worker answers a relay message only when the content script's
83
+ stamped origin equals `port.sender.origin`. Permissions are
81
84
  keyed by that origin, never by a URL a page or a WalletConnect peer claims.
82
85
  - **Requests.** Everything that reveals accounts or moves funds goes through
83
86
  the approval queue; blind signing (`eth_sign`) is not offered; typed data for
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,
@@ -115,6 +116,7 @@ __export(src_exports, {
115
116
  newRequestId: () => newRequestId,
116
117
  nonEvmChain: () => nonEvmChain,
117
118
  normalizeHost: () => normalizeHost,
119
+ pageRequestFrom: () => pageRequestFrom,
118
120
  parseAddChainParams: () => parseAddChainParams,
119
121
  parseConnectRequest: () => parseConnectRequest,
120
122
  parseRawAddress: () => parseRawAddress,
@@ -139,6 +141,7 @@ __export(src_exports, {
139
141
  tonConnectErrorCode: () => tonConnectErrorCode,
140
142
  tonProofMessage: () => tonProofMessage,
141
143
  tonSignDataMessage: () => tonSignDataMessage,
144
+ typedDataMatcher: () => typedDataMatcher,
142
145
  vaultSecretStorage: () => vaultSecretStorage,
143
146
  verifyAssertion: () => verifyAssertion
144
147
  });
@@ -179,6 +182,14 @@ function toArrayBuffer(bytes) {
179
182
  copy.set(bytes);
180
183
  return copy.buffer;
181
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
+ }
182
193
 
183
194
  // src/vault.ts
184
195
  var VAULT_MIN_ITERATIONS = 6e5;
@@ -446,7 +457,13 @@ var Vault = class {
446
457
  }
447
458
  async _derivePbkdf2(password, salt, iterations) {
448
459
  const subtle = this._crypto.subtle;
449
- 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
+ }
450
467
  return subtle.deriveKey({ name: "PBKDF2", hash: "SHA-256", salt: toArrayBuffer(salt), iterations }, material, { name: "AES-GCM", length: 256 }, false, ["encrypt", "decrypt"]);
451
468
  }
452
469
  async _deriveScrypt(password, salt, cost) {
@@ -458,7 +475,7 @@ var Vault = class {
458
475
  (0, import_core.zeroize)(pw);
459
476
  }
460
477
  try {
461
- 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"]));
462
479
  } finally {
463
480
  (0, import_core.zeroize)(raw);
464
481
  }
@@ -466,16 +483,16 @@ var Vault = class {
466
483
  /** HKDF-SHA256(ikm = PRF output, salt = credential id bytes, info) → AES-256-GCM key. */
467
484
  async _derivePasskeyKek(prf, credentialId) {
468
485
  const subtle = this._crypto.subtle;
469
- 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"]));
470
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"]);
471
488
  }
472
489
  async _importDek(dek) {
473
490
  if (dek.length !== DEK_BYTES) throw new VaultError("Vault key must be 32 bytes.", "invalid-input");
474
- 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"]));
475
492
  }
476
493
  async _seal(key, plaintext, aad) {
477
494
  const iv = this._random(IV_BYTES);
478
- 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));
479
496
  return { iv: toBase64(iv), data: toBase64(new Uint8Array(ct)) };
480
497
  }
481
498
  async _open(key, box, aad, onFail) {
@@ -660,22 +677,40 @@ var Session = class {
660
677
  if (was !== void 0 && was !== null) for (const cb of this._onLock) cb();
661
678
  }
662
679
  _expiry() {
663
- return this._autoLockMs > 0 ? this._now() + this._autoLockMs : Number.POSITIVE_INFINITY;
680
+ return this._autoLockMs > 0 ? this._now() + this._autoLockMs : null;
664
681
  }
665
682
  async _arm(expiresAt) {
666
- if (Number.isFinite(expiresAt)) await this._alarms.schedule(this._alarm, expiresAt);
683
+ if (expiresAt !== null) await this._alarms.schedule(this._alarm, expiresAt);
667
684
  else await this._alarms.clear(this._alarm);
668
685
  }
686
+ /** The stored record, or `null` when locked. A record that is there but unreadable locks the wallet (removed, listeners told). */
669
687
  async _read() {
670
688
  const raw = await this._storage.get(this._key);
671
- if (!raw || typeof raw.key !== "string" || typeof raw.expiresAt !== "number") return null;
672
- 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)) {
673
693
  await this._lockNow();
674
694
  return null;
675
695
  }
676
- return raw;
696
+ if (r.expiresAt !== null && r.expiresAt <= this._now()) {
697
+ await this._lockNow();
698
+ return null;
699
+ }
700
+ return r;
677
701
  }
678
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
+ }
679
714
  function memorySessionStorage() {
680
715
  const data = /* @__PURE__ */ new Map();
681
716
  return {
@@ -752,6 +787,10 @@ function isRelayToContent(x) {
752
787
  function newRequestId() {
753
788
  return crypto.randomUUID();
754
789
  }
790
+ function pageRequestFrom(event, page) {
791
+ if (event.source !== page || event.origin !== page.location.origin) return null;
792
+ return isPageRequest(event.data) ? event.data : null;
793
+ }
755
794
  function stampOrigin(request, origin) {
756
795
  const { id, chain, method } = request;
757
796
  return "params" in request ? { kind: "request", id, origin, chain, method, params: request.params } : { kind: "request", id, origin, chain, method };
@@ -980,14 +1019,27 @@ var NetworksTable = class {
980
1019
  return this._loading;
981
1020
  }
982
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
+ }
983
1035
  function parseAddChainParams(params, known = isCatalogChain) {
984
1036
  const p = Array.isArray(params) ? params[0] : params;
985
1037
  if (!p || typeof p !== "object") return relayError(RELAY_ERRORS.invalidParams, "wallet_addEthereumChain expects one object parameter.");
986
1038
  const caip2 = caip2FromChainId(p.chainId);
987
1039
  if (!caip2) return relayError(RELAY_ERRORS.invalidParams, "chainId must be a 0x-prefixed hex string.");
988
1040
  if (!known(caip2)) return relayError(RELAY_ERRORS.chainNotAdded, `Chain ${String(p.chainId)} is not in this wallet's catalog.`);
989
- const rpcs = Array.isArray(p.rpcUrls) ? p.rpcUrls.filter((u) => typeof u === "string" && /^https:\/\/[^\s/?#]+/i.test(u)) : [];
990
- 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).");
991
1043
  const name = typeof p.chainName === "string" && p.chainName.trim() ? p.chainName.trim().slice(0, 64) : caip2;
992
1044
  const currency = p.nativeCurrency;
993
1045
  if (currency && currency.decimals !== void 0 && currency.decimals !== 18) return relayError(RELAY_ERRORS.invalidParams, "nativeCurrency.decimals must be 18.");
@@ -1184,6 +1236,13 @@ var QueueLimitError = class extends Error {
1184
1236
  }
1185
1237
  code;
1186
1238
  };
1239
+ var OriginRefused = class extends Error {
1240
+ code = RELAY_ERRORS.unauthorized;
1241
+ constructor(message) {
1242
+ super(message);
1243
+ this.name = "OriginRefused";
1244
+ }
1245
+ };
1187
1246
  var APPROVALS_MAX_PENDING_PER_ORIGIN = 3;
1188
1247
  var ApprovalQueue = class {
1189
1248
  constructor(window, newId = () => crypto.randomUUID(), options = {}) {
@@ -1203,6 +1262,13 @@ var ApprovalQueue = class {
1203
1262
  _via = /* @__PURE__ */ new Map();
1204
1263
  /** Enqueue and wait for the user's answer. Refuses (never queues) past the per-origin limits. */
1205
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));
1206
1272
  const sameOrigin = [...this._current ? [this._current] : [], ...this._waiting].filter((p) => p.request.origin === input.origin);
1207
1273
  if (input.kind === "connect" && sameOrigin.some((p) => p.request.kind === "connect")) {
1208
1274
  return Promise.reject(new QueueLimitError(RELAY_ERRORS.resourceUnavailable, "A connect request from this site is already waiting for your answer."));
@@ -1391,6 +1457,8 @@ var RequestRouter = class {
1391
1457
  return await this.deps.networks.get(wanted) ?? await this.deps.networks.get(DEFAULT_CHAIN);
1392
1458
  }
1393
1459
  async handle(origin, chain, method, rawParams) {
1460
+ const refused = this.deps.refuse?.(origin);
1461
+ if (refused) throw new RouterError(RELAY_ERRORS.unauthorized, refused);
1394
1462
  if (chain !== "eip155") throw new RouterError(RELAY_ERRORS.unsupportedMethod, `${chain} providers are not available yet.`);
1395
1463
  const params = Array.isArray(rawParams) ? rawParams : rawParams === void 0 ? [] : [rawParams];
1396
1464
  const network = await this.chainFor(origin);
@@ -1699,6 +1767,23 @@ var bigintSafe = (_k, v) => typeof v === "bigint" ? v.toString() : v;
1699
1767
  var hex = (n) => `0x${n.toString(16)}`;
1700
1768
  var big = (v) => BigInt(v);
1701
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
+ }
1702
1787
  async function prepareTransaction(deps, network, from, tx) {
1703
1788
  const call = { from, to: tx.to, value: hex(BigInt(tx.value)), ...tx.data ? { data: tx.data } : {} };
1704
1789
  const [nonceHex, block, gasHex] = await Promise.all([
@@ -1708,18 +1793,20 @@ async function prepareTransaction(deps, network, from, tx) {
1708
1793
  ]);
1709
1794
  const estimated = big(gasHex);
1710
1795
  const gasLimit = tx.gas ? estimated : (estimated * 120n + 99n) / 100n;
1711
- const base = block?.baseFeePerGas != null ? big(block.baseFeePerGas) : null;
1712
- if (base === null) {
1713
- const gasPrice = big(await deps.rpc(network, "eth_gasPrice", []));
1714
- return { chainId: network.chainId, nonce: safeNonce(big(nonceHex)), gasLimit: hex(gasLimit), gasPrice: hex(gasPrice), maxFee: (gasLimit * gasPrice).toString() };
1715
- }
1716
- const floor = deps.tipFloor?.(network.chainId) ?? MIN_TIP_WEI;
1717
- const nodeTip = await deps.rpc(network, "eth_maxPriorityFeePerGas", []).then(big).catch(() => 0n);
1718
- const tip = nodeTip > floor ? nodeTip : floor;
1719
- const maxFeePerGas = base * 2n + tip;
1720
- 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));
1721
1797
  }
1722
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
+ };
1723
1810
  return async ({ origin, network, method, params, accounts }) => {
1724
1811
  const via = deps.approvals.viaFor(origin);
1725
1812
  const info = classifySignRequest({ network, method, params, accounts, origin, ...via ? { via } : {} });
@@ -1728,6 +1815,41 @@ function createSignHandler(deps) {
1728
1815
  const preview = deps.preview ? await deps.preview({ network, request: info, accounts }).catch(() => null) : null;
1729
1816
  const payload = { request: info, network, ...prepared ? { prepared } : {}, preview };
1730
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
+ }
1731
1853
  const handle = await deps.signerFor(info.account, network);
1732
1854
  try {
1733
1855
  switch (info.kind) {
@@ -1735,19 +1857,6 @@ function createSignHandler(deps) {
1735
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);
1736
1858
  case "sign-typed-data":
1737
1859
  return await handle.signer.signTypedData(info.typedData);
1738
- case "send-transaction": {
1739
- const p = prepared;
1740
- const raw = await handle.signer.signTransaction({
1741
- chainId: p.chainId,
1742
- nonce: p.nonce,
1743
- to: info.tx.to,
1744
- value: BigInt(info.tx.value),
1745
- data: info.tx.data ?? "0x",
1746
- gasLimit: BigInt(p.gasLimit),
1747
- ...p.gasPrice !== void 0 ? { gasPrice: BigInt(p.gasPrice) } : { maxFeePerGas: BigInt(p.maxFeePerGas), maxPriorityFeePerGas: BigInt(p.maxPriorityFeePerGas) }
1748
- });
1749
- return await deps.rpc(network, "eth_sendRawTransaction", [raw]);
1750
- }
1751
1860
  }
1752
1861
  } finally {
1753
1862
  handle.dispose();
@@ -1756,22 +1865,28 @@ function createSignHandler(deps) {
1756
1865
  }
1757
1866
 
1758
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();
1759
1870
  var ReviewGate = class {
1760
1871
  constructor(now = Date.now) {
1761
1872
  this.now = now;
1762
1873
  }
1763
1874
  now;
1764
1875
  approved = [];
1765
- /** Each matcher passes ONE operation, within `ttlMs` (default 10 min). */
1766
- 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) {
1767
1882
  const until = this.now() + ttlMs;
1768
- this.approved.push(...matchers.map((match) => ({ match, until })));
1883
+ this.approved.push(...matchers.map((match) => ({ match, until, ...account ? { account } : {} })));
1769
1884
  }
1770
1885
  /** True (and the matcher is spent) when a pre-approval covers `op`. */
1771
1886
  consume(op) {
1772
1887
  const now = this.now();
1773
1888
  this.approved = this.approved.filter((a) => a.until > now);
1774
- 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));
1775
1890
  if (i < 0) return false;
1776
1891
  this.approved.splice(i, 1);
1777
1892
  return true;
@@ -1788,12 +1903,17 @@ function evmCallMatcher(expected) {
1788
1903
  const data = expected.data && expected.data !== "0x" ? lc2(expected.data) : void 0;
1789
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;
1790
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
+ }
1791
1910
  function chainTxMatcher(expected) {
1792
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 ?? "");
1793
1912
  }
1794
1913
 
1795
1914
  // src/swap.ts
1796
1915
  var import_protocol_swap2_core = require("@anywalletkit/protocol-swap2-core");
1916
+ var import_wallet_evm = require("@anywalletkit/wallet-evm");
1797
1917
  async function quoteRoutes(providers, params, options = {}) {
1798
1918
  const timeoutMs = options.timeoutMs ?? 2e4;
1799
1919
  const from = { chain: params.sourceChain, token: params.sourceToken };
@@ -1899,7 +2019,8 @@ function swapApprovalPayload(plan, meta) {
1899
2019
  source = { kind: "serialized" };
1900
2020
  checks.push({ status: "ok", label: "Balance changes are proven before signing (SDK guard)" });
1901
2021
  } else {
1902
- source = { kind: "typed-order" };
2022
+ const typedData = parseTypedOrder(s.typedData);
2023
+ source = { kind: "typed-order", typedData, text: JSON.stringify(typedData, null, 2) };
1903
2024
  checks.push({ status: "ok", label: "Signed order \u2014 no transaction is sent from your wallet" });
1904
2025
  }
1905
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" });
@@ -1924,6 +2045,11 @@ function swapApprovalPayload(plan, meta) {
1924
2045
  checks
1925
2046
  };
1926
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
+ }
1927
2053
  function planMatchers(plan) {
1928
2054
  const m = plan.approvals.map((a) => evmCallMatcher({ caip2: plan.fromChain, to: a.token, data: encodeApproveCalldata(a.spender, a.amount), value: 0n }));
1929
2055
  const s = plan.source;
@@ -1938,16 +2064,18 @@ function planMatchers(plan) {
1938
2064
  } else if (s.kind === "serialized") {
1939
2065
  m.push((op) => op.kind === "solana-serialized" && op.serializedTx === s.serializedTx);
1940
2066
  } else {
1941
- 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)) }));
1942
2068
  }
1943
2069
  return m;
1944
2070
  }
1945
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.");
1946
2073
  return async (plan) => {
2074
+ const matchers = planMatchers(plan);
1947
2075
  const payload = swapApprovalPayload(plan, meta);
1948
2076
  await approvals.request({ origin: meta.origin, chain: plan.fromChain, kind: "swap", payload });
1949
2077
  gate.clear();
1950
- 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);
1951
2079
  };
1952
2080
  }
1953
2081
 
@@ -1957,11 +2085,13 @@ var ACTIVE_STATUSES = /* @__PURE__ */ new Set(["pending", "action-required", "re
1957
2085
  function parseSwapId(id) {
1958
2086
  if (!id.startsWith(`${import_protocol_swap2_core2.SWAP2_ID_PREFIX}.`)) return null;
1959
2087
  const parts = id.slice(import_protocol_swap2_core2.SWAP2_ID_PREFIX.length + 1).split(".");
1960
- 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;
1961
2089
  const provider = decodeURIComponent(parts[0]);
1962
2090
  const intentId = decodeURIComponent(parts[1]);
1963
2091
  const sourceTxHash = parts[2] ? decodeURIComponent(parts[2]) : void 0;
1964
- 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 } : {} };
1965
2095
  }
1966
2096
  var SwapsStore = class {
1967
2097
  constructor(storage) {
@@ -1970,9 +2100,14 @@ var SwapsStore = class {
1970
2100
  storage;
1971
2101
  _records = null;
1972
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();
1973
2109
  async list() {
1974
- this._records ??= await this.storage.get() ?? [];
1975
- return [...this._records].sort((a, b) => b.createdAt - a.createdAt);
2110
+ return [...await this._all()].sort((a, b) => b.createdAt - a.createdAt);
1976
2111
  }
1977
2112
  async get(id) {
1978
2113
  return (await this.list()).find((r) => r.id === id) ?? null;
@@ -1980,26 +2115,41 @@ var SwapsStore = class {
1980
2115
  async active() {
1981
2116
  return (await this.list()).filter((r) => ACTIVE_STATUSES.has(r.status));
1982
2117
  }
1983
- async add(record) {
1984
- const all = (await this.list()).filter((r) => r.id !== record.id);
1985
- 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
+ });
1986
2123
  }
1987
- async update(id, patch) {
1988
- const all = await this.list();
1989
- const i = all.findIndex((r) => r.id === id);
1990
- if (i < 0) return null;
1991
- const next = { ...all[i], ...patch, updatedAt: patch.updatedAt ?? Date.now() };
1992
- all[i] = next;
1993
- await this.write(all);
1994
- 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
+ });
1995
2134
  }
1996
- async remove(id) {
1997
- 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
+ });
1998
2139
  }
1999
2140
  onChange(listener) {
2000
2141
  this._listeners.add(listener);
2001
2142
  return () => this._listeners.delete(listener);
2002
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
+ }
2003
2153
  async write(records) {
2004
2154
  await this.storage.set(records);
2005
2155
  this._records = records;
@@ -2074,8 +2224,9 @@ function earnApprovalPayload(plan, meta) {
2074
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 };
2075
2225
  }
2076
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.");
2077
2228
  await approvals.request({ origin: meta.origin, chain: meta.network.caip2, kind: "earn", payload: earnApprovalPayload(plan, meta) });
2078
- gate.preApprove(earnPlanMatchers(meta.network, plan), 10 * 6e4);
2229
+ gate.preApprove(earnPlanMatchers(meta.network, plan), 10 * 6e4, meta.account);
2079
2230
  }
2080
2231
 
2081
2232
  // src/tonconnect.ts
@@ -2236,6 +2387,14 @@ var PhishingDetector = class {
2236
2387
  }
2237
2388
  return { host, blocked: false, warnings };
2238
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
+ }
2239
2398
  /**
2240
2399
  * `metamask.io-login.com`, `app-uniswap.com`, `uniswap.org.evil.net`: a known site's name used as (part of) a label
2241
2400
  * of a host that is not that site. Names shorter than 5 characters are skipped (too many honest matches).