@anywalletkit/extension-kit 0.3.6 → 0.5.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
@@ -135,7 +135,7 @@ var Vault = class {
135
135
  if (isEnvelopeV1(envelope)) {
136
136
  if (envelope.kdf.iterations < VAULT_MIN_ITERATIONS) throw new VaultError(`The vault's KDF (${envelope.kdf.iterations} iterations) is below the floor this build accepts.`, "weak-kdf");
137
137
  const kek2 = await this._derivePbkdf2(password, fromBase64(envelope.kdf.salt), envelope.kdf.iterations);
138
- const dek = await this._open(kek2, envelope.dek, aadDekV1(envelope.kdf), "wrong-password");
138
+ const dek = await this._proveOwn(await this._open(kek2, envelope.dek, aadDekV1(envelope.kdf), "wrong-password"), envelope);
139
139
  if (this._kdf === "scrypt") {
140
140
  const next = await this._sealEnvelope(password, dek, envelope.secrets, [], envelope.createdAt);
141
141
  await this._write(isEnvelopeV2(next) ? { ...next, upgradedAt: Date.now() } : next);
@@ -144,7 +144,24 @@ var Vault = class {
144
144
  }
145
145
  if (!isStrongScrypt(envelope.kdf)) throw new VaultError(`The vault's scrypt cost (N=${envelope.kdf.N} r=${envelope.kdf.r} p=${envelope.kdf.p}) is below the floor this build accepts.`, "weak-kdf");
146
146
  const kek = await this._deriveScrypt(password, fromBase64(envelope.kdf.salt), envelope.kdf);
147
- return this._open(kek, envelope.dek, aadDekV2(envelope.kdf), "wrong-password");
147
+ return this._proveOwn(await this._open(kek, envelope.dek, aadDekV2(envelope.kdf), "wrong-password"), envelope);
148
+ }
149
+ /**
150
+ * A DEK unwrapped from a dek / passkey box must be the key this vault's secrets are sealed under
151
+ * (audit 2026-11-09 E21): a box spliced in from another vault, under another password or passkey,
152
+ * opens with that password but holds a foreign key. Proven with the DEK check, or — in an envelope
153
+ * written before it — the first secret; an old empty envelope has nothing to prove it with. Returns
154
+ * `dek`; on a mismatch zeroizes it and fails as `wrong-password` (the same failure as tampering).
155
+ */
156
+ async _proveOwn(dek, envelope) {
157
+ if (envelope.dekCheck === void 0 && Object.keys(envelope.secrets).length === 0) return dek;
158
+ try {
159
+ await this._requireDek(dek, envelope);
160
+ return dek;
161
+ } catch {
162
+ zeroize(dek);
163
+ throw new VaultError("Wrong password.", "wrong-password");
164
+ }
148
165
  }
149
166
  /**
150
167
  * Re-wrap the DEK under a new password (fresh salt, current KDF). Secrets and passkeys are untouched —
@@ -176,9 +193,13 @@ var Vault = class {
176
193
  *
177
194
  * Passkeys are dropped — their boxes hold the old DEK and cannot be re-wrapped without their PRF
178
195
  * output — and must be registered again ({@link Vault.addPasskey} with the new DEK).
196
+ *
197
+ * With `newPassword` this is also the password change: the new DEK is wrapped under `newPassword`
198
+ * (at least {@link VAULT_MIN_PASSWORD_LENGTH} characters), and `password` must be the current one.
179
199
  */
180
- async rotateVaultKey(password) {
200
+ async rotateVaultKey(password, newPassword) {
181
201
  requirePassword(password);
202
+ if (newPassword !== void 0) requirePassword(newPassword, VAULT_MIN_PASSWORD_LENGTH);
182
203
  return this._exclusive(async () => {
183
204
  const oldDek = await this._unlock(password);
184
205
  const dek = this._random(DEK_BYTES);
@@ -194,7 +215,7 @@ var Vault = class {
194
215
  zeroize(plaintext);
195
216
  }
196
217
  }
197
- const next = await this._sealEnvelope(password, dek, secrets, [], envelope.createdAt);
218
+ const next = await this._sealEnvelope(newPassword ?? password, dek, secrets, [], envelope.createdAt);
198
219
  const upgradedAt = isEnvelopeV2(envelope) ? envelope.upgradedAt : void 0;
199
220
  await this._write(isEnvelopeV2(next) ? { ...next, ...upgradedAt !== void 0 ? { upgradedAt } : {}, rotatedAt: Date.now() } : next);
200
221
  return dek;
@@ -258,7 +279,7 @@ var Vault = class {
258
279
  const entry = envelope.passkeys.find((p) => p.id === id);
259
280
  if (!entry) throw new VaultError("No such passkey on this vault.", "missing");
260
281
  const key = await this._derivePasskeyKek(prf, id);
261
- return await this._open(key, entry.dek, aadPasskey(id), "wrong-password");
282
+ return await this._proveOwn(await this._open(key, entry.dek, aadPasskey(id), "wrong-password"), envelope);
262
283
  } finally {
263
284
  zeroize(prf);
264
285
  }
@@ -553,7 +574,11 @@ var Session = class {
553
574
  return fromBase64(record.key);
554
575
  });
555
576
  }
556
- /** User activity: push the auto-lock out again. No-op when locked. */
577
+ /**
578
+ * User activity: push the auto-lock out again. No-op when locked. Call it for explicit user
579
+ * gestures only (unlock, send, approve / decline, an action in the wallet's own UI) — not for
580
+ * passive polls, page requests or worker restarts, or the timer never runs out.
581
+ */
557
582
  async touch() {
558
583
  return this._serial(async () => {
559
584
  const epoch = this._epoch;
@@ -567,11 +592,25 @@ var Session = class {
567
592
  get autoLockMs() {
568
593
  return this._autoLockMs;
569
594
  }
570
- /** Change the timer (options page); re-arms from now when unlocked. 0 disables. */
595
+ /**
596
+ * Set the timer period (0 disables). Never pushes the deadline out — call this at every worker
597
+ * start with the stored setting: it re-arms the alarm at the stored deadline (a restart must not
598
+ * count as activity, audit 2026-11-09 E11), and only brings the deadline forward when the new
599
+ * period ends sooner. A longer period (or 0) applies from the next {@link Session.touch} /
600
+ * {@link Session.unlock}; call `touch()` right after when the user changed it explicitly.
601
+ */
571
602
  async setAutoLockMs(ms) {
572
603
  if (!Number.isFinite(ms) || ms < 0) throw new VaultError("Auto-lock must be a non-negative number of milliseconds.", "invalid-input");
573
604
  this._autoLockMs = ms;
574
- await this.touch();
605
+ return this._serial(async () => {
606
+ const epoch = this._epoch;
607
+ const record = await this._read();
608
+ if (!record || epoch !== this._epoch) return;
609
+ const cap = this._expiry();
610
+ const expiresAt = cap !== null && (record.expiresAt === null || cap < record.expiresAt) ? cap : record.expiresAt;
611
+ if (expiresAt !== record.expiresAt) await this._storage.set(this._key, { ...record, expiresAt });
612
+ await this._arm(expiresAt);
613
+ });
575
614
  }
576
615
  onLock(callback) {
577
616
  this._onLock.add(callback);
@@ -841,7 +880,8 @@ var DEFAULT_NETWORKS = Object.freeze([
841
880
  net(4326, "MegaETH", "ETH", ["https://mainnet.megaeth.com/rpc"]),
842
881
  net(130, "Unichain", "ETH", ["https://mainnet.unichain.org", "https://unichain-rpc.publicnode.com"], "https://uniscan.xyz"),
843
882
  net(143, "Monad", "MON", ["https://rpc.monad.xyz"]),
844
- net(9745, "Plasma", "XPL", ["https://rpc.plasma.to"]),
883
+ // Second operator (Tenderly's public gateway, keyless): rpc.plasma.to alone cannot satisfy a two-operator check.
884
+ net(9745, "Plasma", "XPL", ["https://rpc.plasma.to", "https://plasma.gateway.tenderly.co"]),
845
885
  net(59144, "Linea", "ETH", ["https://rpc.linea.build", "https://linea-rpc.publicnode.com"], "https://lineascan.build"),
846
886
  net(100, "Gnosis", "xDAI", ["https://rpc.gnosischain.com", "https://gnosis-rpc.publicnode.com"], "https://gnosisscan.io"),
847
887
  net(146, "Sonic", "S", ["https://rpc.soniclabs.com", "https://sonic-rpc.publicnode.com"], "https://sonicscan.org"),
@@ -859,7 +899,11 @@ var DEFAULT_NETWORKS = Object.freeze([
859
899
  net(5e3, "Mantle", "MNT", ["https://rpc.mantle.xyz", "https://mantle-rpc.publicnode.com"], "https://mantlescan.xyz"),
860
900
  net(747474, "Katana", "ETH", ["https://rpc.katana.network"], "https://explorer.katanarpc.com"),
861
901
  net(2818, "Morph", "ETH", ["https://rpc.morphl2.io", "https://rpc-quicknode.morphl2.io"], "https://explorer.morphl2.io"),
862
- net(14, "Flare", "FLR", ["https://flare-api.flare.network/ext/C/rpc", "https://flare.rpc.thirdweb.com"], "https://flare-explorer.flare.network")
902
+ net(14, "Flare", "FLR", ["https://flare-api.flare.network/ext/C/rpc", "https://flare.rpc.thirdweb.com"], "https://flare-explorer.flare.network"),
903
+ // Elysium by Kinetiq (chain 99901, HYPE gas; Arbitrum Orbit on HyperEVM) — public RPC and explorer checked 2026-11-10.
904
+ // The explorer value is the link base (<root>/tx/<hash>, <root>/address/<a>), not LI.FI's blockExplorerUrls
905
+ // https://elysium.kinetiq.xyz/explorer/ (the overview page; the bare root is the Elysium site home).
906
+ net(99901, "Elysium", "HYPE", ["https://rpc.elysium.kinetiq.xyz"], "https://elysium.kinetiq.xyz")
863
907
  ]);
864
908
  var chainHex = (caip2) => `0x${Number(caip2.split(":")[1]).toString(16)}`;
865
909
  function caip2FromChainId(value) {
@@ -935,6 +979,17 @@ var NetworksTable = class {
935
979
  );
936
980
  return this._loading;
937
981
  }
982
+ /**
983
+ * Forget the cached table: the next read asks storage again. For a storage whose answer changes under
984
+ * the table — e.g. one that only hands out the user's networks once the wallet is unlocked.
985
+ */
986
+ reload() {
987
+ return this.serialize(async () => {
988
+ await this._loading?.catch(() => void 0);
989
+ this._custom = null;
990
+ this._loading = null;
991
+ });
992
+ }
938
993
  };
939
994
  var isSiteRpcUrl = (u) => typeof u === "string" && u.length <= 2048 && isPublicHttpsUrl(u);
940
995
  function parseAddChainParams(params, known = isCatalogChain) {
@@ -1048,6 +1103,21 @@ var PermissionStore = class {
1048
1103
  return permission;
1049
1104
  });
1050
1105
  }
1106
+ /**
1107
+ * Add EVM accounts to an origin's grant (per-site grants): the new ones first, the ones it already
1108
+ * holds stay. The chain stays the origin's own when it has one. Used by the router's connect; `connect`
1109
+ * replaces the list instead.
1110
+ */
1111
+ grant(origin, accounts, chain) {
1112
+ return this.serialize(async () => {
1113
+ if (accounts.length === 0) throw new Error("Connect at least one account.");
1114
+ const current = await this.get(origin);
1115
+ const merged = [...accounts, ...(current?.accounts ?? []).filter((a) => !accounts.some((x) => x.toLowerCase() === a.toLowerCase()))];
1116
+ const permission = { ...current ?? {}, origin, accounts: merged, chain: current?.chain ?? chain, connectedAt: current?.connectedAt ?? Date.now() };
1117
+ await this.write(origin, permission);
1118
+ return permission;
1119
+ });
1120
+ }
1051
1121
  /** Connect another chain's accounts to the same origin (one "connected sites" list). */
1052
1122
  connectChain(origin, key, accounts, defaultChain) {
1053
1123
  return this.serialize(async () => {
@@ -1080,6 +1150,11 @@ var PermissionStore = class {
1080
1150
  });
1081
1151
  }
1082
1152
  /**
1153
+ * @deprecated Rewrites every origin's grant to the active account, which shows that account to sites
1154
+ * the user never connected it to (and links accounts across sites). Grants are per site now: the
1155
+ * router shows an origin its granted accounts that are also active, and connecting another account
1156
+ * adds it to that one origin ({@link grant}). Kept for compatibility; nothing in this package calls it.
1157
+ *
1083
1158
  * The active EVM account changed: every origin holding an EVM grant now sees `accounts` (MetaMask behaviour).
1084
1159
  * An origin connected only on another chain (`accounts: []` — Solana/TON/Tron/BTC/NEAR providers, a WalletConnect
1085
1160
  * session without eip155) is skipped: it never passed an EVM connect and must not learn the EVM address (scan F6/F8).
@@ -1125,6 +1200,179 @@ var PermissionStore = class {
1125
1200
  };
1126
1201
  var sameList = (a, b) => a.length === b.length && a.every((x, i) => x === b[i]);
1127
1202
 
1203
+ // src/site.ts
1204
+ var MULTI_LABEL_SUFFIXES = /* @__PURE__ */ new Set([
1205
+ // country second-level registries
1206
+ "co.uk",
1207
+ "org.uk",
1208
+ "me.uk",
1209
+ "ltd.uk",
1210
+ "plc.uk",
1211
+ "net.uk",
1212
+ "ac.uk",
1213
+ "gov.uk",
1214
+ "com.au",
1215
+ "net.au",
1216
+ "org.au",
1217
+ "edu.au",
1218
+ "gov.au",
1219
+ "id.au",
1220
+ "co.nz",
1221
+ "org.nz",
1222
+ "net.nz",
1223
+ "co.jp",
1224
+ "ne.jp",
1225
+ "or.jp",
1226
+ "ac.jp",
1227
+ "go.jp",
1228
+ "co.kr",
1229
+ "or.kr",
1230
+ "ne.kr",
1231
+ "com.br",
1232
+ "net.br",
1233
+ "org.br",
1234
+ "com.cn",
1235
+ "net.cn",
1236
+ "org.cn",
1237
+ "com.hk",
1238
+ "com.tw",
1239
+ "com.sg",
1240
+ "com.my",
1241
+ "co.in",
1242
+ "net.in",
1243
+ "org.in",
1244
+ "co.id",
1245
+ "co.il",
1246
+ "co.th",
1247
+ "co.za",
1248
+ "com.mx",
1249
+ "com.ar",
1250
+ "com.co",
1251
+ "com.tr",
1252
+ "com.ua",
1253
+ "com.pl",
1254
+ "com.ru",
1255
+ "com.vn",
1256
+ "com.ph",
1257
+ "com.pk",
1258
+ "com.ng",
1259
+ "com.eg",
1260
+ "com.sa",
1261
+ "co.ke",
1262
+ // hosting platforms: every customer gets its own subdomain
1263
+ "github.io",
1264
+ "gitlab.io",
1265
+ "vercel.app",
1266
+ "now.sh",
1267
+ "pages.dev",
1268
+ "workers.dev",
1269
+ "r2.dev",
1270
+ "netlify.app",
1271
+ "netlify.com",
1272
+ "herokuapp.com",
1273
+ "web.app",
1274
+ "firebaseapp.com",
1275
+ "appspot.com",
1276
+ "azurewebsites.net",
1277
+ "azurestaticapps.net",
1278
+ "cloudfront.net",
1279
+ "amplifyapp.com",
1280
+ "onrender.com",
1281
+ "fly.dev",
1282
+ "railway.app",
1283
+ "up.railway.app",
1284
+ "deno.dev",
1285
+ "glitch.me",
1286
+ "repl.co",
1287
+ "replit.app",
1288
+ "replit.dev",
1289
+ "surge.sh",
1290
+ "webflow.io",
1291
+ "framer.app",
1292
+ "framer.website",
1293
+ "wixsite.com",
1294
+ "blogspot.com",
1295
+ "myshopify.com",
1296
+ "notion.site",
1297
+ "carrd.co",
1298
+ "gitbook.io",
1299
+ "readthedocs.io",
1300
+ "ngrok.io",
1301
+ "ngrok-free.app",
1302
+ "loca.lt",
1303
+ "trycloudflare.com",
1304
+ // ENS / IPFS gateways: every name or CID is its own site
1305
+ "eth.limo",
1306
+ "eth.link",
1307
+ "ipfs.dweb.link",
1308
+ "ipns.dweb.link",
1309
+ "ipfs.w3s.link",
1310
+ "ipfs.cf-ipfs.com"
1311
+ ]);
1312
+ var IPV4 = /^\d{1,3}(\.\d{1,3}){3}$/;
1313
+ function siteOf(origin) {
1314
+ let host;
1315
+ try {
1316
+ const url = new URL(origin);
1317
+ if (url.protocol !== "http:" && url.protocol !== "https:") return origin;
1318
+ host = url.hostname.toLowerCase().replace(/\.$/, "");
1319
+ } catch {
1320
+ return origin;
1321
+ }
1322
+ if (!host || host.startsWith("[") || host.includes(":") || IPV4.test(host)) return host || origin;
1323
+ const labels = host.split(".");
1324
+ if (labels.length <= 2) return host;
1325
+ for (const size of [3, 2]) {
1326
+ if (labels.length <= size) continue;
1327
+ if (MULTI_LABEL_SUFFIXES.has(labels.slice(-size).join("."))) return labels.slice(-(size + 1)).join(".");
1328
+ }
1329
+ return labels.slice(-2).join(".");
1330
+ }
1331
+ var TokenBucket = class {
1332
+ constructor(options) {
1333
+ this.options = options;
1334
+ }
1335
+ options;
1336
+ buckets = /* @__PURE__ */ new Map();
1337
+ take(key, cost = 1) {
1338
+ const now = (this.options.now ?? Date.now)();
1339
+ const b = this.refill(key, now);
1340
+ if (b.tokens < cost) return false;
1341
+ b.tokens -= cost;
1342
+ return true;
1343
+ }
1344
+ /** Tokens `key` holds right now (full for an unknown key). */
1345
+ available(key) {
1346
+ return this.refill(key, (this.options.now ?? Date.now)()).tokens;
1347
+ }
1348
+ refill(key, now) {
1349
+ let b = this.buckets.get(key);
1350
+ if (!b) {
1351
+ if (this.buckets.size >= (this.options.maxKeys ?? 2e3)) this.prune(now);
1352
+ b = { tokens: this.options.capacity, at: now };
1353
+ this.buckets.set(key, b);
1354
+ return b;
1355
+ }
1356
+ const elapsed = Math.max(0, now - b.at) / 1e3;
1357
+ b.tokens = Math.min(this.options.capacity, b.tokens + elapsed * this.options.refillPerSecond);
1358
+ b.at = now;
1359
+ return b;
1360
+ }
1361
+ /** Drop buckets that are full again (they carry no state); the oldest half when every one is in use. */
1362
+ prune(now) {
1363
+ for (const [k, b] of this.buckets) {
1364
+ if (b.tokens + Math.max(0, now - b.at) / 1e3 * this.options.refillPerSecond >= this.options.capacity) this.buckets.delete(k);
1365
+ }
1366
+ if (this.buckets.size >= (this.options.maxKeys ?? 2e3)) {
1367
+ let drop = Math.ceil(this.buckets.size / 2);
1368
+ for (const k of this.buckets.keys()) {
1369
+ if (drop-- <= 0) break;
1370
+ this.buckets.delete(k);
1371
+ }
1372
+ }
1373
+ }
1374
+ };
1375
+
1128
1376
  // src/approvals.ts
1129
1377
  var ReviewDeclined = class extends Error {
1130
1378
  code = RELAY_ERRORS.userRejected;
@@ -1149,7 +1397,11 @@ var OriginRefused = class extends Error {
1149
1397
  }
1150
1398
  };
1151
1399
  var APPROVALS_MAX_PENDING_PER_ORIGIN = 3;
1152
- var ApprovalQueue = class {
1400
+ var APPROVALS_MAX_PENDING_PER_SITE = 5;
1401
+ var APPROVALS_MAX_PENDING_TOTAL = 20;
1402
+ var FOLLOW_UP_CREDITS_MAX = 8;
1403
+ var FOLLOW_UP_KEYS_MAX = 256;
1404
+ var ApprovalQueue = class _ApprovalQueue {
1153
1405
  constructor(window, newId = () => crypto.randomUUID(), options = {}) {
1154
1406
  this.window = window;
1155
1407
  this.newId = newId;
@@ -1157,6 +1409,8 @@ var ApprovalQueue = class {
1157
1409
  window.onClosed((id) => {
1158
1410
  if (this._current?.request.id === id) this.decline(id);
1159
1411
  });
1412
+ const rate = options.openRate === false ? null : options.openRate ?? {};
1413
+ this._rate = rate ? new TokenBucket({ capacity: rate.burst ?? 10, refillPerSecond: rate.refillPerSecond ?? 1 / 6, ...options.now ? { now: options.now } : {} }) : null;
1160
1414
  }
1161
1415
  window;
1162
1416
  newId;
@@ -1165,8 +1419,62 @@ var ApprovalQueue = class {
1165
1419
  _waiting = [];
1166
1420
  _listeners = /* @__PURE__ */ new Set();
1167
1421
  _via = /* @__PURE__ */ new Map();
1422
+ _rate;
1423
+ /** Per site: cooldown end and strikes (how many times the bucket ran dry recently). */
1424
+ _cooldown = /* @__PURE__ */ new Map();
1425
+ /** Per origin + kind + chain: approvals that have not been followed up (see {@link ApprovalRequestOptions.followUp}). */
1426
+ _approved = /* @__PURE__ */ new Map();
1427
+ site(origin) {
1428
+ try {
1429
+ return (this.options.siteOf ?? siteOf)(origin);
1430
+ } catch {
1431
+ return origin;
1432
+ }
1433
+ }
1434
+ get all() {
1435
+ return [...this._current ? [this._current] : [], ...this._waiting];
1436
+ }
1437
+ /** The per-site rate: a reason to refuse, or null (and one token spent). */
1438
+ rateRefusal(site) {
1439
+ if (!this._rate) return null;
1440
+ const rate = this.options.openRate || {};
1441
+ const now = (this.options.now ?? Date.now)();
1442
+ const max = rate.maxBackoffMs ?? 6e5;
1443
+ const c = this._cooldown.get(site);
1444
+ if (c && now < c.until) return `This site opened too many requests; try again in ${Math.ceil((c.until - now) / 1e3)} s.`;
1445
+ if (this._rate.take(site)) return null;
1446
+ const strikes = c && now - c.last < max ? c.strikes + 1 : 1;
1447
+ const wait = Math.min(max, (rate.backoffMs ?? 3e4) * 2 ** (strikes - 1));
1448
+ this._cooldown.set(site, { until: now + wait, strikes, last: now });
1449
+ if (this._cooldown.size > 1e3) {
1450
+ for (const [k, v] of this._cooldown) if (now - v.last >= max) this._cooldown.delete(k);
1451
+ }
1452
+ return `This site opened too many requests; try again in ${Math.ceil(wait / 1e3)} s.`;
1453
+ }
1454
+ static followUpKey(r) {
1455
+ return `${r.origin}
1456
+ ${r.kind}
1457
+ ${r.chain}`;
1458
+ }
1459
+ /** The user approved `r`: it may be followed up once. */
1460
+ approved(r) {
1461
+ const key = _ApprovalQueue.followUpKey(r);
1462
+ const n = Math.min((this._approved.get(key) ?? 0) + 1, FOLLOW_UP_CREDITS_MAX);
1463
+ this._approved.delete(key);
1464
+ this._approved.set(key, n);
1465
+ if (this._approved.size > FOLLOW_UP_KEYS_MAX) this._approved.delete(this._approved.keys().next().value);
1466
+ }
1467
+ /** Spends one approval of the same origin, kind and chain; false when there is none. */
1468
+ spendApproval(r) {
1469
+ const key = _ApprovalQueue.followUpKey(r);
1470
+ const n = this._approved.get(key) ?? 0;
1471
+ if (n <= 0) return false;
1472
+ if (n === 1) this._approved.delete(key);
1473
+ else this._approved.set(key, n - 1);
1474
+ return true;
1475
+ }
1168
1476
  /** Enqueue and wait for the user's answer. Refuses (never queues) past the per-origin limits. */
1169
- request(input) {
1477
+ request(input, opts = {}) {
1170
1478
  let refused;
1171
1479
  try {
1172
1480
  refused = this.options.refuse?.(input.origin);
@@ -1174,19 +1482,41 @@ var ApprovalQueue = class {
1174
1482
  refused = `The wallet could not check this site (${e instanceof Error ? e.message : String(e)}).`;
1175
1483
  }
1176
1484
  if (refused) return Promise.reject(new OriginRefused(refused));
1177
- const sameOrigin = [...this._current ? [this._current] : [], ...this._waiting].filter((p) => p.request.origin === input.origin);
1178
- if (input.kind === "connect" && sameOrigin.some((p) => p.request.kind === "connect")) {
1179
- return Promise.reject(new QueueLimitError(RELAY_ERRORS.resourceUnavailable, "A connect request from this site is already waiting for your answer."));
1485
+ let exempt = false;
1486
+ try {
1487
+ exempt = this.options.exempt?.(input.origin) === true;
1488
+ } catch {
1489
+ exempt = false;
1180
1490
  }
1181
- const max = this.options.maxPendingPerOrigin ?? APPROVALS_MAX_PENDING_PER_ORIGIN;
1182
- if (sameOrigin.length >= max) {
1183
- return Promise.reject(new QueueLimitError(RELAY_ERRORS.limitExceeded, `This site already has ${max} requests waiting for you; answer or close them first.`));
1491
+ const followUp = opts.followUp === true && this.spendApproval(input);
1492
+ if (!exempt && !followUp) {
1493
+ const all = this.all;
1494
+ const sameOrigin = all.filter((p) => p.request.origin === input.origin);
1495
+ if (input.kind === "connect" && sameOrigin.some((p) => p.request.kind === "connect")) {
1496
+ return Promise.reject(new QueueLimitError(RELAY_ERRORS.resourceUnavailable, "A connect request from this site is already waiting for your answer."));
1497
+ }
1498
+ const max = this.options.maxPendingPerOrigin ?? APPROVALS_MAX_PENDING_PER_ORIGIN;
1499
+ if (sameOrigin.length >= max) {
1500
+ return Promise.reject(new QueueLimitError(RELAY_ERRORS.limitExceeded, `This site already has ${max} requests waiting for you; answer or close them first.`));
1501
+ }
1502
+ const site = this.site(input.origin);
1503
+ const maxSite = this.options.maxPendingPerSite ?? APPROVALS_MAX_PENDING_PER_SITE;
1504
+ if (all.filter((p) => this.site(p.request.origin) === site).length >= maxSite) {
1505
+ return Promise.reject(new QueueLimitError(RELAY_ERRORS.limitExceeded, `This site already has ${maxSite} requests waiting for you across its subdomains; answer or close them first.`));
1506
+ }
1507
+ const maxTotal = this.options.maxPendingTotal ?? APPROVALS_MAX_PENDING_TOTAL;
1508
+ if (all.filter((p) => !this.isExempt(p.request.origin)).length >= maxTotal) {
1509
+ return Promise.reject(new QueueLimitError(RELAY_ERRORS.limitExceeded, `The wallet already has ${maxTotal} requests waiting for you; answer or close them first.`));
1510
+ }
1511
+ const slow = this.rateRefusal(site);
1512
+ if (slow) return Promise.reject(new QueueLimitError(RELAY_ERRORS.limitExceeded, slow));
1184
1513
  }
1185
1514
  return new Promise((resolve, reject) => {
1186
1515
  const warnings = this.options.annotate?.(input.origin) ?? [];
1187
1516
  const via = this._via.get(input.origin);
1188
1517
  const pending = { request: { ...input, id: this.newId(), createdAt: Date.now(), ...warnings.length ? { originWarnings: warnings } : {}, ...via ? { via } : {} }, resolve, reject };
1189
- this._waiting.push(pending);
1518
+ if (followUp) this._waiting.unshift(pending);
1519
+ else this._waiting.push(pending);
1190
1520
  void this.next();
1191
1521
  });
1192
1522
  }
@@ -1199,9 +1529,37 @@ var ApprovalQueue = class {
1199
1529
  viaFor(origin) {
1200
1530
  return this._via.get(origin);
1201
1531
  }
1532
+ isExempt(origin) {
1533
+ try {
1534
+ return this.options.exempt?.(origin) === true;
1535
+ } catch {
1536
+ return false;
1537
+ }
1538
+ }
1202
1539
  /** Requests waiting for `origin` (current + queued). */
1203
1540
  pendingFor(origin) {
1204
- return [...this._current ? [this._current] : [], ...this._waiting].filter((p) => p.request.origin === origin).length;
1541
+ return this.all.filter((p) => p.request.origin === origin).length;
1542
+ }
1543
+ /** Requests waiting for any origin of `site` (current + queued). */
1544
+ pendingForSite(site) {
1545
+ return this.all.filter((p) => this.site(p.request.origin) === site).length;
1546
+ }
1547
+ /** The site (registrable domain) the queue's limits put `origin` under. */
1548
+ siteFor(origin) {
1549
+ return this.site(origin);
1550
+ }
1551
+ /** Decline everything waiting for `origin` (its last page closed). Returns how many requests were declined. */
1552
+ declineOrigin(origin, error = new ReviewDeclined("The page was closed \u2014 nothing was signed.")) {
1553
+ return this.declineWhere((p) => p.request.origin === origin, error);
1554
+ }
1555
+ /** "Decline all from this site": everything waiting for any origin of `site` (see {@link siteFor}). */
1556
+ declineSite(site, error = new ReviewDeclined()) {
1557
+ return this.declineWhere((p) => this.site(p.request.origin) === site, error);
1558
+ }
1559
+ declineWhere(match, error) {
1560
+ const ids = [...this._waiting.filter(match), ...this._current && match(this._current) ? [this._current] : []].map((p) => p.request.id);
1561
+ for (const id of ids) this.decline(id, error);
1562
+ return ids.length;
1205
1563
  }
1206
1564
  /** The request currently shown (the window asks for it by id on load). */
1207
1565
  get current() {
@@ -1211,7 +1569,10 @@ var ApprovalQueue = class {
1211
1569
  return this._waiting.length;
1212
1570
  }
1213
1571
  resolve(requestId, value) {
1214
- return this.settle(requestId, (p) => p.resolve(value));
1572
+ return this.settle(requestId, (p) => {
1573
+ this.approved(p.request);
1574
+ p.resolve(value);
1575
+ });
1215
1576
  }
1216
1577
  decline(requestId, error = new ReviewDeclined()) {
1217
1578
  return this.settle(requestId, (p) => p.reject(error));
@@ -1219,6 +1580,7 @@ var ApprovalQueue = class {
1219
1580
  /** Lock, disconnect, shutdown: everything pending is declined. */
1220
1581
  declineAll(error = new ReviewDeclined("The wallet was locked.")) {
1221
1582
  const all = [...this._current ? [this._current] : [], ...this._waiting.splice(0)];
1583
+ this._approved.clear();
1222
1584
  for (const p of all) p.reject(error);
1223
1585
  if (this._current) void this.window.close(this._current.request.id);
1224
1586
  this._current = null;
@@ -1350,6 +1712,7 @@ var READ_METHODS = /* @__PURE__ */ new Set([
1350
1712
  "eth_getTransactionReceipt",
1351
1713
  "eth_maxPriorityFeePerGas"
1352
1714
  ]);
1715
+ var GETLOGS_MAX_SPAN_UNCONNECTED = 1e3;
1353
1716
  var SIGNING_METHODS = /* @__PURE__ */ new Set(["personal_sign", "eth_signTypedData_v4", "eth_signTypedData_v3", "eth_signTypedData", "eth_sendTransaction"]);
1354
1717
  var RequestRouter = class {
1355
1718
  constructor(deps) {
@@ -1379,8 +1742,10 @@ var RequestRouter = class {
1379
1742
  return this.visibleAccounts(permission);
1380
1743
  case "eth_requestAccounts":
1381
1744
  return this.connect(origin, network);
1382
- case "wallet_getPermissions":
1383
- return permission && permission.accounts.length > 0 ? [permissionDescriptor(origin, permission.accounts)] : [];
1745
+ case "wallet_getPermissions": {
1746
+ const visible = await this.visibleAccounts(permission);
1747
+ return visible.length > 0 ? [permissionDescriptor(origin, visible)] : [];
1748
+ }
1384
1749
  case "wallet_requestPermissions": {
1385
1750
  const accounts = await this.connect(origin, network);
1386
1751
  return [permissionDescriptor(origin, accounts)];
@@ -1421,11 +1786,20 @@ var RequestRouter = class {
1421
1786
  case "eth_signTransaction":
1422
1787
  throw new RouterError(RELAY_ERRORS.unsupportedMethod, `${method} (blind signing) is never offered by this wallet.`);
1423
1788
  }
1424
- if (READ_METHODS.has(method)) return this.deps.rpc(network, method, params);
1789
+ if (READ_METHODS.has(method)) {
1790
+ if (method === "eth_getLogs" && !(permission && permission.accounts.length > 0)) await this.boundLogs(network, params);
1791
+ return this.deps.rpc(network, method, params);
1792
+ }
1425
1793
  if (SIGNING_METHODS.has(method)) {
1426
1794
  if (!this.deps.sign) throw new RouterError(RELAY_ERRORS.unsupportedMethod, `${method} is not available yet.`);
1427
- if (!permission) throw new RouterError(RELAY_ERRORS.unauthorized, "Connect the wallet first (eth_requestAccounts).");
1428
- return this.deps.sign({ origin, network, method, params, accounts: permission.accounts });
1795
+ if (!permission || permission.accounts.length === 0) throw new RouterError(RELAY_ERRORS.unauthorized, "Connect the wallet first (eth_requestAccounts).");
1796
+ const visible = await this.visibleAccounts(permission);
1797
+ const named = namedAccount(method, params);
1798
+ const inactive = named !== void 0 && permission.accounts.some((a) => a.toLowerCase() === named.toLowerCase()) && !visible.some((a) => a.toLowerCase() === named.toLowerCase());
1799
+ if (visible.length === 0 || inactive) {
1800
+ throw new RouterError(RELAY_ERRORS.unauthorized, "The account this site asks for is not the wallet's active account (or the wallet is locked). Switch to it in the wallet, or connect the active account (eth_requestAccounts).");
1801
+ }
1802
+ return this.deps.sign({ origin, network, method, params, accounts: visible });
1429
1803
  }
1430
1804
  throw new RouterError(RELAY_ERRORS.unsupportedMethod, `This wallet does not support ${method}.`);
1431
1805
  }
@@ -1435,9 +1809,31 @@ var RequestRouter = class {
1435
1809
  const available = await this.deps.accounts();
1436
1810
  return permission.accounts.filter((a) => available.includes(a));
1437
1811
  }
1812
+ /** An unconnected origin's `eth_getLogs`: a block hash, or a range of at most {@link GETLOGS_MAX_SPAN_UNCONNECTED} blocks. */
1813
+ async boundLogs(network, params) {
1814
+ const filter = params[0] ?? {};
1815
+ if (typeof filter !== "object" || filter === null) throw new RouterError(RELAY_ERRORS.invalidParams, "eth_getLogs expects a filter object.");
1816
+ if (filter.blockHash !== void 0 && filter.blockHash !== null) return;
1817
+ let head;
1818
+ const block = async (tag) => {
1819
+ if (tag === void 0 || tag === null || tag === "latest" || tag === "pending" || tag === "safe" || tag === "finalized") {
1820
+ head ??= BigInt(String(await this.deps.rpc(network, "eth_blockNumber", [])));
1821
+ return head;
1822
+ }
1823
+ if (tag === "earliest") return 0n;
1824
+ if (typeof tag === "string" && /^0x[0-9a-fA-F]{1,16}$/.test(tag)) return BigInt(tag);
1825
+ throw new RouterError(RELAY_ERRORS.invalidParams, "eth_getLogs: fromBlock / toBlock must be a block tag or a hex block number.");
1826
+ };
1827
+ const from = await block(filter.fromBlock);
1828
+ const to = await block(filter.toBlock);
1829
+ if (to - from > BigInt(GETLOGS_MAX_SPAN_UNCONNECTED)) {
1830
+ throw new RouterError(RELAY_ERRORS.limitExceeded, `eth_getLogs over more than ${GETLOGS_MAX_SPAN_UNCONNECTED} blocks is answered for connected sites only; narrow the range or connect the wallet.`);
1831
+ }
1832
+ }
1438
1833
  /**
1439
1834
  * Connected and unlocked → the accounts; otherwise the approval window (which
1440
- * unlocks first when needed) picks them. Emits `connect` + `accountsChanged`.
1835
+ * unlocks first when needed) picks them, and they are ADDED to this origin's
1836
+ * grant (other origins are untouched). Emits `connect` + `accountsChanged`.
1441
1837
  */
1442
1838
  async connect(origin, network) {
1443
1839
  const existing = await this.visibleAccounts(await this.deps.permissions.get(origin));
@@ -1446,7 +1842,7 @@ var RequestRouter = class {
1446
1842
  const available = await this.deps.accounts();
1447
1843
  const accounts = Array.isArray(chosen) ? chosen.filter((a) => typeof a === "string" && available.includes(a)) : [];
1448
1844
  if (accounts.length === 0) throw new ReviewDeclined("No account was selected.");
1449
- await this.deps.permissions.connect(origin, accounts, network.caip2);
1845
+ await this.deps.permissions.grant(origin, accounts, network.caip2);
1450
1846
  this.deps.emit(origin, "connect", { chainId: chainHex(network.caip2) });
1451
1847
  this.deps.emit(origin, "accountsChanged", accounts);
1452
1848
  return accounts;
@@ -1466,6 +1862,12 @@ var RequestRouter = class {
1466
1862
  this.deps.emit(origin, "chainChanged", chainHex(to.caip2));
1467
1863
  }
1468
1864
  };
1865
+ function namedAccount(method, params) {
1866
+ const addr = (x) => typeof x === "string" && /^0x[0-9a-fA-F]{40}$/.test(x) ? x : void 0;
1867
+ if (method === "personal_sign") return addr(params[1]) ?? addr(params[0]);
1868
+ if (method === "eth_sendTransaction") return addr(params[0]?.from);
1869
+ return addr(params[0]);
1870
+ }
1469
1871
  var permissionDescriptor = (origin, accounts) => ({
1470
1872
  id: crypto.randomUUID(),
1471
1873
  parentCapability: "eth_accounts",
@@ -1474,7 +1876,60 @@ var permissionDescriptor = (origin, accounts) => ({
1474
1876
  date: Date.now()
1475
1877
  });
1476
1878
 
1879
+ // src/personal-sign.ts
1880
+ var HEX = /^0x([0-9a-fA-F]{2})*$/;
1881
+ var BLIND_HASH_MAX_CHARS = 66;
1882
+ var hexBytes = (value) => Uint8Array.from(value.slice(2).match(/../g) ?? [], (b) => Number.parseInt(b, 16));
1883
+ function codePoint(s, i) {
1884
+ const c = s.charCodeAt(i);
1885
+ if (c < 55296 || c > 57343) return c;
1886
+ const lo = c <= 56319 && i + 1 < s.length ? s.charCodeAt(i + 1) : 0;
1887
+ return lo >= 56320 && lo <= 57343 ? 65536 + (c - 55296 << 10) + (lo - 56320) : 65533;
1888
+ }
1889
+ function utf8Length(s) {
1890
+ let n = 0;
1891
+ for (let i = 0; i < s.length; i++) {
1892
+ const c = codePoint(s, i);
1893
+ if (c > 65535) {
1894
+ n += 4;
1895
+ i++;
1896
+ } else n += c < 128 ? 1 : c < 2048 ? 2 : 3;
1897
+ }
1898
+ return n;
1899
+ }
1900
+ function utf8Bytes(s) {
1901
+ const out = new Uint8Array(utf8Length(s));
1902
+ let o = 0;
1903
+ for (let i = 0; i < s.length; i++) {
1904
+ const c = codePoint(s, i);
1905
+ if (c < 128) out[o++] = c;
1906
+ else if (c < 2048) {
1907
+ out[o++] = 192 | c >> 6;
1908
+ out[o++] = 128 | c & 63;
1909
+ } else if (c < 65536) {
1910
+ out[o++] = 224 | c >> 12;
1911
+ out[o++] = 128 | c >> 6 & 63;
1912
+ out[o++] = 128 | c & 63;
1913
+ } else {
1914
+ out[o++] = 240 | c >> 18;
1915
+ out[o++] = 128 | c >> 12 & 63;
1916
+ out[o++] = 128 | c >> 6 & 63;
1917
+ out[o++] = 128 | c & 63;
1918
+ i++;
1919
+ }
1920
+ }
1921
+ return out;
1922
+ }
1923
+ function personalSignInput(message) {
1924
+ return HEX.test(message) ? hexBytes(message) : message;
1925
+ }
1926
+ function personalSignBytes(message) {
1927
+ const input = personalSignInput(message);
1928
+ return typeof input === "string" ? utf8Bytes(input) : input;
1929
+ }
1930
+
1477
1931
  // src/siwe.ts
1932
+ var SIWE_FORBIDDEN_CHARS = /[\u061c\u200b-\u200f\u202a-\u202e\u2060-\u2064\u2066-\u2069\ufeff]/;
1478
1933
  var HEADER_LINE = /^(?<domain>[^\n]+?) wants you to sign in with your Ethereum account:(?:\n|$)/;
1479
1934
  var ADDRESS_LINE = /^0x[0-9a-fA-F]{40}$/;
1480
1935
  var FIELD = /^([A-Za-z ]+): (.*)$/;
@@ -1578,12 +2033,20 @@ function siweProblems(m, ctx) {
1578
2033
  }
1579
2034
  return out;
1580
2035
  }
2036
+ function siweCrossSite(m, ctx) {
2037
+ const site = originWhere(ctx.origin) ?? (ctx.via?.verify === "VALID" && !ctx.via.scam ? whereOf(ctx.via.url) : null);
2038
+ if (!site) return null;
2039
+ const domain = whereOf(m.domain);
2040
+ return !domain || domain.host !== site.host ? { domain: m.domain, origin: `${site.scheme}://${site.host}` } : null;
2041
+ }
1581
2042
  function describeSiwe(text, ctx) {
1582
2043
  const p = parseLoose(text);
1583
2044
  if (!p) return null;
1584
2045
  const problems = siweProblems(p.message, ctx);
1585
- if (p.missing.length === 0) return { message: p.message, problems };
1586
- return { message: p.message, problems: [`This is a malformed sign-in message (EIP-4361): ${p.missing.join(", ")} missing or unreadable \u2014 check the site before signing.`, ...problems], malformed: true };
2046
+ const cross = siweCrossSite(p.message, ctx);
2047
+ const crossSite = cross ? { crossSite: cross } : {};
2048
+ if (p.missing.length === 0) return { message: p.message, problems, ...crossSite };
2049
+ return { message: p.message, problems: [`This is a malformed sign-in message (EIP-4361): ${p.missing.join(", ")} missing or unreadable \u2014 check the site before signing.`, ...problems], malformed: true, ...crossSite };
1587
2050
  }
1588
2051
 
1589
2052
  // src/signing.ts
@@ -1591,18 +2054,30 @@ var safeNonce = (n) => {
1591
2054
  if (n < 0n || n > BigInt(Number.MAX_SAFE_INTEGER)) throw new Error(`The RPC returned an impossible nonce (${n}).`);
1592
2055
  return Number(n.toString());
1593
2056
  };
2057
+ var MAX_FEE_REASKS = 3;
1594
2058
  var ADDRESS = /^0x[0-9a-fA-F]{40}$/;
1595
- var HEX = /^0x([0-9a-fA-F]{2})*$/;
1596
2059
  var lc = (s) => s.toLowerCase();
2060
+ var INVISIBLE = /[\p{Cf}\u2028\u2029\u115f\u1160\u3164\uffa0\x00-\x08\x0b\x0c\x0e-\x1f\x7f-\x9f]/gu;
2061
+ function revealHidden(text) {
2062
+ let hidden = false;
2063
+ const out = text.replace(INVISIBLE, (c) => {
2064
+ hidden = true;
2065
+ return `\u27E8U+${c.codePointAt(0).toString(16).toUpperCase().padStart(4, "0")}\u27E9`;
2066
+ });
2067
+ return { text: out, hidden };
2068
+ }
1597
2069
  function messageText(value) {
1598
- if (typeof value !== "string") return "";
1599
- if (!HEX.test(value)) return value;
2070
+ return shownMessage(value).text;
2071
+ }
2072
+ function shownMessage(value) {
2073
+ if (typeof value !== "string") return { text: "", hidden: false };
2074
+ if (!HEX.test(value)) return revealHidden(value);
1600
2075
  try {
1601
2076
  const bytes = Uint8Array.from(value.slice(2).match(/../g) ?? [], (b) => Number.parseInt(b, 16));
1602
2077
  const s = new TextDecoder("utf-8", { fatal: true }).decode(bytes);
1603
- return /^[\x09\x0a\x0d\x20-\x7e -￿]*$/.test(s) ? s : value;
2078
+ return /^[\x09\x0a\x0d\x20-\x7e\u00a0-\uffff]*$/.test(s) ? revealHidden(s) : { text: value, hidden: false };
1604
2079
  } catch {
1605
- return value;
2080
+ return { text: value, hidden: false };
1606
2081
  }
1607
2082
  }
1608
2083
  function decodedMessage(value) {
@@ -1626,11 +2101,16 @@ function classifySignRequest(input) {
1626
2101
  if (ours(msg) && !ours(addr)) [msg, addr] = [addr, msg];
1627
2102
  if (!ours(addr)) return err(RELAY_ERRORS.unauthorized, "personal_sign for an address this wallet does not hold.");
1628
2103
  if (typeof msg !== "string") return err(RELAY_ERRORS.invalidParams, "personal_sign expects a message string.");
1629
- const text = messageText(msg);
2104
+ const thirtyTwo = msg.length <= BLIND_HASH_MAX_CHARS && personalSignBytes(msg).length === 32;
2105
+ const blindHashForm = !thirtyTwo ? void 0 : HEX.test(msg) ? "hex" : "text";
2106
+ if (blindHashForm === "hex" && !input.allowBlindHash) return err(RELAY_ERRORS.unsupportedMethod, 'This site asks you to sign a raw 32-byte hash (blind signing). It can be a swap order or a transfer (e.g. CoW Protocol) paying someone else, so this wallet refuses it. "Allow blind hash signatures" in Options turns on a review for such requests.');
2107
+ const shown = shownMessage(msg);
2108
+ const text = shown.text;
1630
2109
  const signed = decodedMessage(msg) ?? text;
1631
2110
  const siwe = input.origin ? describeSiwe(signed, { origin: input.origin, account: checksum(addr), chainId: input.network.chainId, ...input.via ? { via: input.via } : {}, ...input.now !== void 0 ? { now: input.now } : {} }) : null;
1632
2111
  if (siwe && HIDDEN_CONTROL.test(signed)) return err(RELAY_ERRORS.invalidParams, "This sign-in message contains hidden control characters \u2014 refused.");
1633
- return { kind: "sign-message", method, account: checksum(addr), text, raw: msg, ...siwe ? { siwe } : {} };
2112
+ if (siwe && SIWE_FORBIDDEN_CHARS.test(signed)) return err(RELAY_ERRORS.invalidParams, "This sign-in message contains invisible or text-direction characters \u2014 refused.");
2113
+ return { kind: "sign-message", method, account: checksum(addr), text, raw: msg, ...siwe ? { siwe } : {}, ...shown.hidden ? { hiddenChars: true } : {}, ...blindHashForm ? { blindHash: true, blindHashForm } : {} };
1634
2114
  }
1635
2115
  if (method === "eth_signTypedData_v4" || method === "eth_signTypedData_v3" || method === "eth_signTypedData") {
1636
2116
  const [addr, data] = params;
@@ -1648,7 +2128,8 @@ function classifySignRequest(input) {
1648
2128
  if (cid !== void 0 && cid !== null && Number(cid) !== input.network.chainId) {
1649
2129
  return err(RELAY_ERRORS.chainDisconnected, `Typed data is for chain ${String(cid)}; this site is on ${input.network.name} (${input.network.chainId}).`);
1650
2130
  }
1651
- return { kind: "sign-typed-data", method, account: checksum(addr), text: JSON.stringify(parsed, bigintSafe, 2), typedData: parsed };
2131
+ const shown = revealHidden(JSON.stringify(parsed, bigintSafe, 2));
2132
+ return { kind: "sign-typed-data", method, account: checksum(addr), text: shown.text, typedData: parsed, ...shown.hidden ? { hiddenChars: true } : {} };
1652
2133
  }
1653
2134
  if (method === "eth_sendTransaction") {
1654
2135
  const tx = params[0] ?? {};
@@ -1697,16 +2178,33 @@ function overpriced(shown, fresh) {
1697
2178
  if (shown.maxFeePerGas !== void 0 && fresh.maxFeePerGas !== void 0) return big(shown.maxFeePerGas) > STALE_FEE_FACTOR * big(fresh.maxFeePerGas);
1698
2179
  return false;
1699
2180
  }
2181
+ function gasLimitExcessive(limit, estimate) {
2182
+ const a = estimate * 3n / 2n;
2183
+ const b = estimate + 50000n;
2184
+ return limit > (a > b ? a : b);
2185
+ }
1700
2186
  async function prepareTransaction(deps, network, from, tx) {
1701
2187
  const call = { from, to: tx.to, value: hex(BigInt(tx.value)), ...tx.data ? { data: tx.data } : {} };
2188
+ const estimate = () => deps.rpc(network, "eth_estimateGas", [call]);
1702
2189
  const [nonceHex, block, gasHex] = await Promise.all([
1703
2190
  deps.rpc(network, "eth_getTransactionCount", [from, "pending"]),
1704
2191
  deps.rpc(network, "eth_getBlockByNumber", ["latest", false]),
1705
- tx.gas ? Promise.resolve(tx.gas) : deps.rpc(network, "eth_estimateGas", [call])
2192
+ tx.gas ? estimate().then((v) => v, () => null) : estimate()
1706
2193
  ]);
1707
- const estimated = big(gasHex);
1708
- const gasLimit = tx.gas ? estimated : (estimated * 120n + 99n) / 100n;
1709
- return withFees(network.chainId, safeNonce(big(nonceHex)), gasLimit, await readFees(deps, network, block));
2194
+ const nonce = safeNonce(big(nonceHex));
2195
+ if (!tx.gas) {
2196
+ const estimated = big(gasHex);
2197
+ return withFees(network.chainId, nonce, (estimated * 120n + 99n) / 100n, await readFees(deps, network, block));
2198
+ }
2199
+ const limit = big(tx.gas);
2200
+ const prepared = withFees(network.chainId, nonce, limit, await readFees(deps, network, block));
2201
+ let est = null;
2202
+ try {
2203
+ est = typeof gasHex === "string" ? big(gasHex) : null;
2204
+ } catch {
2205
+ est = null;
2206
+ }
2207
+ return est !== null && est > 0n ? { ...prepared, gasCheck: { estimate: hex(est), excessive: gasLimitExcessive(limit, est) } } : prepared;
1710
2208
  }
1711
2209
  function createSignHandler(deps) {
1712
2210
  const tails = /* @__PURE__ */ new Map();
@@ -1722,7 +2220,8 @@ function createSignHandler(deps) {
1722
2220
  };
1723
2221
  return async ({ origin, network, method, params, accounts }) => {
1724
2222
  const via = deps.approvals.viaFor(origin);
1725
- const info = classifySignRequest({ network, method, params, accounts, origin, ...via ? { via } : {} });
2223
+ const allowBlindHash = method === "personal_sign" && deps.allowBlindHash ? await deps.allowBlindHash() : false;
2224
+ const info = classifySignRequest({ network, method, params, accounts, origin, ...via ? { via } : {}, ...allowBlindHash ? { allowBlindHash: true } : {} });
1726
2225
  if ("code" in info) throw RouterError.from(info);
1727
2226
  const prepared = info.kind === "send-transaction" ? await prepareTransaction(deps, network, info.account, info.tx) : void 0;
1728
2227
  const preview = deps.preview ? await deps.preview({ network, request: info, accounts }).catch(() => null) : null;
@@ -1732,14 +2231,20 @@ function createSignHandler(deps) {
1732
2231
  const key = `${network.caip2}|${lc(info.account)}`;
1733
2232
  return serial(key, async () => {
1734
2233
  let shown = prepared;
2234
+ let reasks = 0;
1735
2235
  for (; ; ) {
1736
2236
  const [nonceHex, fees] = await Promise.all([deps.rpc(network, "eth_getTransactionCount", [info.account, "pending"]), readFees(deps, network)]);
1737
2237
  const local = nextNonce.get(key) ?? 0;
1738
2238
  const nodeNonce = safeNonce(big(nonceHex));
1739
2239
  const nonce = nodeNonce > local ? nodeNonce : local;
1740
2240
  if (outgrown(shown, fees)) {
1741
- shown = withFees(network.chainId, nonce, big(shown.gasLimit), fees);
1742
- await deps.approvals.request({ origin, chain: network.caip2, kind: info.kind, payload: { ...payload, prepared: shown, repriced: true } });
2241
+ const next = withFees(network.chainId, nonce, big(shown.gasLimit), fees);
2242
+ const ceiling = deps.feeCeiling?.(network);
2243
+ if (ceiling !== void 0 && BigInt(next.maxFee) > ceiling) throw new RouterError(RELAY_ERRORS.internal, `The network fee rose above the wallet's limit for ${network.name}; nothing was signed.`);
2244
+ if (reasks >= MAX_FEE_REASKS) throw new RouterError(RELAY_ERRORS.internal, "The network fee kept rising while you were reviewing; nothing was signed. Try again later.");
2245
+ reasks++;
2246
+ await deps.approvals.request({ origin, chain: network.caip2, kind: info.kind, payload: { ...payload, prepared: next, repriced: true, repricedFrom: shown.maxFee } }, { followUp: true });
2247
+ shown = next;
1743
2248
  continue;
1744
2249
  }
1745
2250
  const fresh = withFees(network.chainId, nonce, big(shown.gasLimit), fees);
@@ -1769,7 +2274,7 @@ function createSignHandler(deps) {
1769
2274
  try {
1770
2275
  switch (info.kind) {
1771
2276
  case "sign-message":
1772
- return await handle.signer.signMessage(HEX.test(info.raw) ? Uint8Array.from(info.raw.slice(2).match(/../g) ?? [], (b) => Number.parseInt(b, 16)) : info.raw);
2277
+ return await handle.signer.signMessage(personalSignInput(info.raw));
1773
2278
  case "sign-typed-data":
1774
2279
  return await handle.signer.signTypedData(info.typedData);
1775
2280
  }
@@ -1827,7 +2332,7 @@ function chainTxMatcher(expected) {
1827
2332
  }
1828
2333
 
1829
2334
  // src/swap.ts
1830
- import { validQuote } from "@anywalletkit/protocol-swap2-core";
2335
+ import { slippageFloor, validQuote } from "@anywalletkit/protocol-swap2-core";
1831
2336
  import { typedDataHash } from "@anywalletkit/wallet-evm";
1832
2337
  async function quoteRoutes(providers, params, options = {}) {
1833
2338
  const timeoutMs = options.timeoutMs ?? 2e4;
@@ -1861,7 +2366,14 @@ async function quoteRoutes(providers, params, options = {}) {
1861
2366
  candidates.push(p);
1862
2367
  }
1863
2368
  const pending = new Set(candidates.map((p) => p.name));
1864
- const view = () => ({ routes: rank(Object.values(quotes)), pending: [...pending], refusals: { ...refusals }, quotes: { ...quotes } });
2369
+ const depositAddressRoute = options.depositAddressRoute;
2370
+ const rankOptions = {
2371
+ ...params.slippageBps !== void 0 ? { slippageBps: params.slippageBps } : {},
2372
+ ...options.depositAddressProviders ? { depositAddressProviders: options.depositAddressProviders } : {},
2373
+ ...depositAddressRoute ? { depositAddressRoute: (q) => depositAddressRoute(q, params) } : {}
2374
+ };
2375
+ const { slippageBps: _slippage, ...shapeParams } = params;
2376
+ const view = () => ({ routes: rank(Object.values(quotes), rankOptions), pending: [...pending], refusals: { ...refusals }, quotes: { ...quotes } });
1865
2377
  const emit = () => options.onUpdate?.(view());
1866
2378
  emit();
1867
2379
  await Promise.all(
@@ -1876,7 +2388,7 @@ async function quoteRoutes(providers, params, options = {}) {
1876
2388
  const q = await Promise.race([p.getQuote({ ...params, dry: true, onRefusal: (r) => {
1877
2389
  refusal = r;
1878
2390
  } }, ctrl.signal), aborted]);
1879
- if (q && validQuote(q, { ...params, dry: true }, p.name)) quotes[p.name] = q;
2391
+ if (q && validQuote(q, { ...shapeParams, dry: true }, p.name)) quotes[p.name] = q;
1880
2392
  else refusals[p.name] = refusal ?? (q ? "returned an invalid quote" : "no route for this pair right now");
1881
2393
  } catch (e) {
1882
2394
  refusals[p.name] = ctrl.signal.aborted ? String(ctrl.signal.reason instanceof Error ? ctrl.signal.reason.message : ctrl.signal.reason ?? "cancelled") : e instanceof Error ? e.message : String(e);
@@ -1890,16 +2402,50 @@ async function quoteRoutes(providers, params, options = {}) {
1890
2402
  );
1891
2403
  return view();
1892
2404
  }
1893
- function rank(quotes) {
2405
+ function gapBps(dest, min) {
2406
+ if (dest <= 0n || min >= dest) return 0;
2407
+ return Number((((dest - min) * 10000n + dest - 1n) / dest).toString());
2408
+ }
2409
+ function rank(quotes, options = {}) {
1894
2410
  const sorted = [...quotes].sort((a, b) => BigInt(b.destAmount) > BigInt(a.destAmount) ? 1 : BigInt(b.destAmount) < BigInt(a.destAmount) ? -1 : a.feeUsd - b.feeUsd || a.etaSeconds - b.etaSeconds);
1895
2411
  const fastest = sorted.length ? sorted.reduce((f, q) => q.etaSeconds < f.etaSeconds ? q : f) : null;
1896
- return sorted.map((q, i) => {
2412
+ const depositProviders = new Set(options.depositAddressProviders ?? []);
2413
+ const paysAssignedAddress = (q) => {
2414
+ if (depositProviders.has(q.provider)) return true;
2415
+ if (!options.depositAddressRoute) return false;
2416
+ try {
2417
+ return options.depositAddressRoute(q) === true;
2418
+ } catch {
2419
+ return true;
2420
+ }
2421
+ };
2422
+ const assigned = new Map(sorted.map((q) => [q, paysAssignedAddress(q)]));
2423
+ const s = options.slippageBps;
2424
+ const warningsOf = (q) => {
2425
+ const w = [];
2426
+ if (s !== void 0 && Number.isInteger(s) && s >= 0 && s < 1e4) {
2427
+ const dest = BigInt(q.destAmount);
2428
+ const min = BigInt(q.minDestAmount);
2429
+ if (min < slippageFloor(dest, s)) w.push({ kind: "min-below-slippage", slippageBps: s, minGapBps: gapBps(dest, min) });
2430
+ }
2431
+ if (assigned.get(q)) w.push({ kind: "deposit-address" });
2432
+ return w;
2433
+ };
2434
+ const warned = new Map(sorted.map((q) => [q, warningsOf(q)]));
2435
+ const verifiableExists = sorted.some((q) => !assigned.get(q));
2436
+ const best = sorted.find((q) => {
2437
+ const w = warned.get(q);
2438
+ return !w.some((x) => x.kind === "min-below-slippage") && !(w.some((x) => x.kind === "deposit-address") && verifiableExists);
2439
+ });
2440
+ const ordered = best ? [best, ...sorted.filter((q) => q !== best)] : sorted;
2441
+ return ordered.map((q) => {
1897
2442
  const poolFee = q.providerData?.poolFee;
1898
2443
  const feeNote = typeof poolFee === "string" && poolFee && q.feeUsd === 0 ? poolFee === "pools" ? "pool fees in price" : `pool ${poolFee} in price` : void 0;
1899
2444
  const tags = [];
1900
- if (i === 0) tags.push("best");
2445
+ if (q === best) tags.push("best");
1901
2446
  if (fastest && q.provider === fastest.provider) tags.push("fastest");
1902
- return { provider: q.provider, destAmount: q.destAmount, minDestAmount: q.minDestAmount, feeUsd: q.feeUsd, etaSeconds: q.etaSeconds, validUntilMs: q.validUntilMs, tags, ...feeNote ? { feeNote } : {} };
2447
+ const warnings = warned.get(q);
2448
+ return { provider: q.provider, destAmount: q.destAmount, minDestAmount: q.minDestAmount, feeUsd: q.feeUsd, etaSeconds: q.etaSeconds, validUntilMs: q.validUntilMs, tags, ...feeNote ? { feeNote } : {}, ...warnings.length ? { warnings } : {} };
1903
2449
  });
1904
2450
  }
1905
2451
  var FLOOR_TOLERANCE_BPS = 50n;
@@ -1918,18 +2464,21 @@ function depositOf(s, approvals) {
1918
2464
  if (!s.data.startsWith(TRANSFER_SELECTOR) || s.data.length < 10 + 64) return void 0;
1919
2465
  return `0x${s.data.slice(10 + 24, 10 + 64)}`;
1920
2466
  }
2467
+ var pct = (bps) => `${Number((bps / 100).toFixed(2))}%`;
1921
2468
  function swapApprovalPayload(plan, meta) {
1922
2469
  const isOwn = meta.own.some((a) => lc3(a) === lc3(plan.recipient));
2470
+ const assigned = meta.depositAddressAssignedByProvider === true;
1923
2471
  const checks = [{ status: "ok", label: `Built by the ${meta.providerLabel} module` }];
2472
+ const depositCheck = assigned ? { status: "warn", label: `Deposit address assigned by the ${meta.providerLabel} server \u2014 funds go to an address the wallet cannot verify` } : { status: "ok", label: `Deposit address from the ${meta.providerLabel} quote` };
1924
2473
  const s = plan.source;
1925
2474
  let source;
1926
2475
  if (s.kind === "call") {
1927
2476
  const deposit = depositOf(s, plan.approvals.length);
1928
2477
  source = { kind: "call", to: s.to, ...s.data ? { data: s.data } : {}, value: s.value.toString(), ...deposit ? { deposit } : {} };
1929
- checks.push(deposit ? { status: "ok", label: `Deposit address from the ${meta.providerLabel} quote` } : { status: "ok", label: `Calls the ${meta.providerLabel} contract` });
2478
+ checks.push(deposit || assigned ? depositCheck : { status: "ok", label: `Calls the ${meta.providerLabel} contract` });
1930
2479
  } else if (s.kind === "token-transfer") {
1931
2480
  source = { kind: "token-transfer", token: s.token, recipient: s.recipient, amount: s.amount.toString(), ...s.memo ? { memo: s.memo } : {} };
1932
- checks.push({ status: "ok", label: `Deposit address from the ${meta.providerLabel} quote` });
2481
+ checks.push(depositCheck);
1933
2482
  } else if (s.kind === "serialized") {
1934
2483
  source = { kind: "serialized" };
1935
2484
  checks.push({ status: "ok", label: "Balance changes are proven before signing (SDK guard)" });
@@ -1940,7 +2489,14 @@ function swapApprovalPayload(plan, meta) {
1940
2489
  }
1941
2490
  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" });
1942
2491
  if (plan.approvals.length > 1) checks.push({ status: "ok", label: `${plan.approvals.length} approvals (reset to 0 first)` });
1943
- checks.push(isOwn ? { status: "ok", label: "Funds arrive at your address" } : { status: "warn", label: `Funds arrive at ${plan.recipient.slice(0, 6)}\u2026${plan.recipient.slice(-4)}, not your address` });
2492
+ checks.push(!isOwn ? { status: "warn", label: `Funds arrive at ${plan.recipient.slice(0, 6)}\u2026${plan.recipient.slice(-4)}, not your address` } : assigned ? { status: "unknown", label: `Delivery to your address is the ${meta.providerLabel} server\u2019s promise \u2014 this transaction does not enforce it` } : { status: "ok", label: "Funds arrive at your address" });
2493
+ let slippage;
2494
+ const sb = meta.slippageBps;
2495
+ if (sb !== void 0 && Number.isInteger(sb) && sb >= 0 && sb < 1e4) {
2496
+ const below = plan.minDestAmount < slippageFloor(plan.destAmount, sb);
2497
+ slippage = { bps: sb, minGapBps: gapBps(plan.destAmount, plan.minDestAmount), below };
2498
+ checks.push(below ? { status: "warn", label: `Minimum is ${pct(slippage.minGapBps)} under the quote \u2014 more than your ${pct(sb)} slippage` } : { status: "ok", label: `Minimum within your ${pct(sb)} slippage` });
2499
+ }
1944
2500
  return {
1945
2501
  provider: plan.provider,
1946
2502
  providerLabel: meta.providerLabel,
@@ -1957,7 +2513,9 @@ function swapApprovalPayload(plan, meta) {
1957
2513
  validUntilMs: plan.validUntilMs,
1958
2514
  approvals: plan.approvals.map((a) => ({ token: a.token, spender: a.spender, amount: a.amount.toString() })),
1959
2515
  source,
1960
- checks
2516
+ checks,
2517
+ ...slippage ? { slippage } : {},
2518
+ ...assigned ? { depositAssignedByProvider: true } : {}
1961
2519
  };
1962
2520
  }
1963
2521
  function parseTypedOrder(json) {
@@ -2420,6 +2978,10 @@ var RecipientBook = class {
2420
2978
  await this.storage.set(s);
2421
2979
  this._state = s;
2422
2980
  }
2981
+ /** Forget the cached book: the next read asks storage again (e.g. a storage that only answers once the wallet is unlocked). */
2982
+ reload() {
2983
+ this._state = null;
2984
+ }
2423
2985
  /** `known` → the address is one the user paid before; `lookalike` → it imitates one. */
2424
2986
  async check(namespace, address) {
2425
2987
  const list = await this.list(namespace);
@@ -2427,9 +2989,243 @@ var RecipientBook = class {
2427
2989
  return { known, lookalike: known ? null : lookalikeOf(address, list) };
2428
2990
  }
2429
2991
  };
2992
+
2993
+ // src/mac-storage.ts
2994
+ import { zeroize as zeroize4 } from "@anywalletkit/core";
2995
+ var STORAGE_MAC_FIELD = "anywalletkit.mac/1";
2996
+ var STORAGE_VERSION_FIELD = "anywalletkit.ver/1";
2997
+ var HKDF_INFO2 = "anywalletkit/storage-mac/1";
2998
+ var KEY_BYTES = 32;
2999
+ async function deriveStorageMacKey(dek, crypto2 = globalThis.crypto) {
3000
+ if (dek.length !== 32) throw new VaultError("Vault key must be 32 bytes.", "invalid-input");
3001
+ const ikm = await withSecretBuffer(dek, (b) => crypto2.subtle.importKey("raw", b, "HKDF", false, ["deriveBits"]));
3002
+ const bits = await crypto2.subtle.deriveBits({ name: "HKDF", hash: "SHA-256", salt: new ArrayBuffer(0), info: toArrayBuffer(utf8.encode(HKDF_INFO2)) }, ikm, KEY_BYTES * 8);
3003
+ return new Uint8Array(bits);
3004
+ }
3005
+ function macJsonStorage(inner, options) {
3006
+ const { label, versions } = options;
3007
+ if (versions && label.includes("\n")) throw new VaultError("A versioned store label has no line break.", "invalid-input");
3008
+ const readWithoutKey = options.readWithoutKey ?? "unverified";
3009
+ const writeWithoutKey = options.writeWithoutKey ?? "unsigned";
3010
+ const withKey = async (given, fn) => {
3011
+ const key = given ? Uint8Array.from(given) : await options.key();
3012
+ try {
3013
+ return await fn(key);
3014
+ } finally {
3015
+ if (key) zeroize4(key);
3016
+ }
3017
+ };
3018
+ const stored = async (key) => {
3019
+ const raw = await inner.get();
3020
+ if (raw === null || raw === void 0) return { state: "empty", value: null };
3021
+ const envelope = isStored(raw);
3022
+ const value = envelope ? raw.value : raw;
3023
+ if (!envelope || raw[STORAGE_MAC_FIELD] === null) return { state: "unsigned", value };
3024
+ if (!key) return { state: "no-key", value };
3025
+ if (!versions) return { state: await verify(key, label, value, raw[STORAGE_MAC_FIELD]) ? "ok" : "invalid", value };
3026
+ const version = STORAGE_VERSION_FIELD in raw ? raw[STORAGE_VERSION_FIELD] : 0;
3027
+ if (version !== 0 && !isVersion(version)) return { state: "invalid", value };
3028
+ if (!await verify(key, label, value, raw[STORAGE_MAC_FIELD], version || void 0)) return { state: "invalid", value };
3029
+ return { state: "ok", value, version };
3030
+ };
3031
+ const put = async (key, value, version) => {
3032
+ const mac = await sign(key, label, value, version);
3033
+ await inner.set(version === void 0 ? { [STORAGE_MAC_FIELD]: mac, value } : { [STORAGE_MAC_FIELD]: mac, [STORAGE_VERSION_FIELD]: version, value });
3034
+ };
3035
+ const seen = async (key) => {
3036
+ const r = await stored(key);
3037
+ return r.state === "ok" ? r.version ?? 0 : 0;
3038
+ };
3039
+ const read = (given) => withKey(given, async (key) => {
3040
+ if (!versions || !key) return stored(key);
3041
+ return versions.exclusive(async (ledger) => {
3042
+ const r = await stored(key);
3043
+ if (r.state === "empty") return await ledger.check(label, null, key) === "ok" ? r : { state: "stale", value: null };
3044
+ if (r.state !== "ok") return r;
3045
+ return await ledger.check(label, r.version ?? 0, key) === "ok" ? r : { ...r, state: "stale" };
3046
+ });
3047
+ });
3048
+ const write = (value, given) => withKey(given, async (key) => {
3049
+ if (!key) throw new VaultError("The wallet is locked: unlock it to change this.", "locked");
3050
+ if (!versions) return put(key, value);
3051
+ await versions.exclusive((ledger) => ledger.advance(label, key, () => seen(key), (version) => put(key, value, version)));
3052
+ });
3053
+ return {
3054
+ read,
3055
+ write,
3056
+ async get() {
3057
+ const r = await read();
3058
+ if (r.state === "ok") return r.value;
3059
+ if (r.state === "no-key" && readWithoutKey === "unverified") return r.value;
3060
+ return null;
3061
+ },
3062
+ async set(value) {
3063
+ await withKey(void 0, async (key) => {
3064
+ if (key) return write(value, key);
3065
+ if (writeWithoutKey === "refuse") throw new VaultError("The wallet is locked: unlock it to change this.", "locked");
3066
+ await inner.set({ [STORAGE_MAC_FIELD]: null, value });
3067
+ });
3068
+ },
3069
+ async rekey(oldKey, newKey) {
3070
+ if (!versions) {
3071
+ const r = await read(oldKey);
3072
+ if (r.state === "ok") await write(r.value, newKey);
3073
+ return r.state;
3074
+ }
3075
+ return versions.exclusive(async (ledger) => {
3076
+ const r = await stored(oldKey);
3077
+ if (r.state === "empty") return await ledger.check(label, null, newKey) === "ok" ? "empty" : "stale";
3078
+ if (r.state !== "ok") return r.state;
3079
+ const version = r.version ?? 0;
3080
+ if (await ledger.check(label, version, newKey) !== "ok") return "stale";
3081
+ if (version > 0) await put(newKey, r.value, version);
3082
+ else await ledger.advance(label, newKey, async () => 0, (v) => put(newKey, r.value, v));
3083
+ return "ok";
3084
+ });
3085
+ }
3086
+ };
3087
+ }
3088
+ function isStored(raw) {
3089
+ if (typeof raw !== "object" || raw === null || !(STORAGE_MAC_FIELD in raw) || !("value" in raw)) return false;
3090
+ const mac = raw[STORAGE_MAC_FIELD];
3091
+ return mac === null || typeof mac === "string";
3092
+ }
3093
+ async function hmacKey(key, usage) {
3094
+ if (key.length !== KEY_BYTES) throw new VaultError("A storage MAC key is 32 bytes.", "invalid-input");
3095
+ return withSecretBuffer(key, (b) => globalThis.crypto.subtle.importKey("raw", b, { name: "HMAC", hash: "SHA-256" }, false, [usage]));
3096
+ }
3097
+ function macInput(label, value, version) {
3098
+ return toArrayBuffer(utf8.encode(version === void 0 ? `${label}
3099
+ ${canonicalJson(value)}` : `${label}
3100
+ ${version}
3101
+ ${canonicalJson(value)}`));
3102
+ }
3103
+ async function sign(key, label, value, version) {
3104
+ return toBase64(new Uint8Array(await globalThis.crypto.subtle.sign("HMAC", await hmacKey(key, "sign"), macInput(label, value, version))));
3105
+ }
3106
+ async function verify(key, label, value, mac, version) {
3107
+ let bytes;
3108
+ try {
3109
+ bytes = fromBase64(mac);
3110
+ } catch {
3111
+ return false;
3112
+ }
3113
+ return globalThis.crypto.subtle.verify("HMAC", await hmacKey(key, "verify"), toArrayBuffer(bytes), macInput(label, value, version));
3114
+ }
3115
+ var isVersion = (x) => typeof x === "number" && Number.isSafeInteger(x) && x > 0;
3116
+ function canonicalJson(value) {
3117
+ return JSON.stringify(sortKeys(JSON.parse(JSON.stringify(value ?? null))));
3118
+ }
3119
+ function sortKeys(x) {
3120
+ if (Array.isArray(x)) return x.map(sortKeys);
3121
+ if (x === null || typeof x !== "object") return x;
3122
+ const out = {};
3123
+ for (const k of Object.keys(x).sort()) out[k] = sortKeys(x[k]);
3124
+ return out;
3125
+ }
3126
+ var LEDGER_LABEL = "anywalletkit/storage-versions/1";
3127
+ function storageVersionLedger(options) {
3128
+ const label = options.label ?? LEDGER_LABEL;
3129
+ const acceptUnversioned = options.acceptUnversioned ?? false;
3130
+ const now = options.now ?? Date.now;
3131
+ let tail = Promise.resolve();
3132
+ const queue = (fn) => {
3133
+ const run = tail.then(fn, fn);
3134
+ tail = run.then(() => void 0, () => void 0);
3135
+ return run;
3136
+ };
3137
+ const high = async () => {
3138
+ const v = await options.mark.get();
3139
+ return isVersion(v) ? v : 0;
3140
+ };
3141
+ const parse = async (raw, key) => {
3142
+ if (!isStored(raw) || raw[STORAGE_MAC_FIELD] === null) return null;
3143
+ const epoch = raw[STORAGE_VERSION_FIELD];
3144
+ const v = raw.value;
3145
+ if (!isVersion(epoch) || typeof v !== "object" || v === null || !isVersion(v.base) || typeof v.sealed !== "boolean") return null;
3146
+ if (typeof v.versions !== "object" || v.versions === null || !Object.values(v.versions).every(isVersion)) return null;
3147
+ if (!await verify(key, label, v, raw[STORAGE_MAC_FIELD], epoch)) return null;
3148
+ return { epoch, base: v.base, sealed: v.sealed, versions: { ...v.versions } };
3149
+ };
3150
+ const load = async (key) => {
3151
+ const mark = await high();
3152
+ const raw = await options.manifest.get();
3153
+ if (raw === null || raw === void 0) return { state: mark > 0 ? "stale" : "empty" };
3154
+ const manifest = await parse(raw, key);
3155
+ if (!manifest) return { state: "invalid" };
3156
+ if (manifest.epoch < mark) return { state: "stale", manifest };
3157
+ if (manifest.epoch > mark) await options.mark.set(manifest.epoch);
3158
+ return { state: "ok", manifest };
3159
+ };
3160
+ const store = async (m, key) => {
3161
+ const value = { base: m.base, sealed: m.sealed, versions: m.versions };
3162
+ await options.manifest.set({ [STORAGE_MAC_FIELD]: await sign(key, label, value, m.epoch), [STORAGE_VERSION_FIELD]: m.epoch, value });
3163
+ if (m.epoch > await high()) await options.mark.set(m.epoch);
3164
+ };
3165
+ const fresh = async (sealed, prior, key) => {
3166
+ const floor = Math.max(await high(), Math.floor(now()), prior ? Math.max(prior.epoch, prior.base, ...Object.values(prior.versions)) : 0);
3167
+ if (!Number.isSafeInteger(floor + 2)) throw new VaultError("Storage version out of range.", "invalid-input");
3168
+ const m = { epoch: floor + 1, base: floor + 1, sealed, versions: {} };
3169
+ await store(m, key);
3170
+ return m;
3171
+ };
3172
+ const record = (m, name, version, key) => store({ ...m, epoch: m.epoch + 1, versions: { ...m.versions, [name]: version } }, key);
3173
+ const named = (name) => {
3174
+ if (name === label || name.includes("\n")) throw new VaultError("Not a store label.", "invalid-input");
3175
+ };
3176
+ const ops = {
3177
+ async check(name, version, key) {
3178
+ named(name);
3179
+ const l = await load(key);
3180
+ if (l.state === "empty") return version === null || version === 0 && acceptUnversioned ? "ok" : "stale";
3181
+ if (l.state !== "ok") return "stale";
3182
+ const m = l.manifest;
3183
+ const listed = m.versions[name];
3184
+ if (listed === void 0 && version === null) return m.sealed ? "stale" : "ok";
3185
+ if (listed === void 0 && version === 0) return !m.sealed && acceptUnversioned ? "ok" : "stale";
3186
+ if (version === null || version === 0) return "stale";
3187
+ if (version === listed) return "ok";
3188
+ if (version !== (listed ?? m.base) + 1) return "stale";
3189
+ await record(m, name, version, key).catch(() => void 0);
3190
+ return "ok";
3191
+ },
3192
+ async advance(name, key, seen, write) {
3193
+ named(name);
3194
+ const l = await load(key);
3195
+ const before = await seen();
3196
+ const m = l.state === "ok" ? l.manifest : await fresh(l.state !== "empty" || before > 0, l.manifest, key);
3197
+ const version = Math.max(m.versions[name] ?? m.base, before) + 1;
3198
+ if (!Number.isSafeInteger(version)) throw new VaultError("Storage version out of range.", "invalid-input");
3199
+ await write(version);
3200
+ await record(m, name, version, key);
3201
+ return version;
3202
+ }
3203
+ };
3204
+ return {
3205
+ exclusive: (fn) => queue(() => fn(ops)),
3206
+ condemn: (key) => queue(async () => {
3207
+ await fresh(true, (await load(key)).manifest, key);
3208
+ }),
3209
+ rekey: (oldKey, newKey) => queue(async () => {
3210
+ const l = await load(oldKey);
3211
+ if (l.state === "ok") await store({ ...l.manifest, epoch: l.manifest.epoch + 1 }, newKey);
3212
+ return l.state;
3213
+ }),
3214
+ read: (key) => queue(async () => {
3215
+ const l = await load(key);
3216
+ return { state: l.state, ...l.manifest ?? { epoch: 0, base: 0, sealed: false, versions: {} } };
3217
+ }),
3218
+ reset: () => queue(async () => {
3219
+ await options.manifest.set(null);
3220
+ await options.mark.set(null);
3221
+ })
3222
+ };
3223
+ }
2430
3224
  export {
2431
3225
  ACTIVE_STATUSES,
2432
3226
  APPROVALS_MAX_PENDING_PER_ORIGIN,
3227
+ APPROVALS_MAX_PENDING_PER_SITE,
3228
+ APPROVALS_MAX_PENDING_TOTAL,
2433
3229
  ApprovalQueue,
2434
3230
  BTC_CHAIN,
2435
3231
  BTC_ESPLORA,
@@ -2437,9 +3233,12 @@ export {
2437
3233
  DEFAULT_CHAIN,
2438
3234
  DEFAULT_NETWORKS,
2439
3235
  FLOOR_TOLERANCE_BPS,
3236
+ GETLOGS_MAX_SPAN_UNCONNECTED,
2440
3237
  KEYRING_STORAGE_KEY,
2441
3238
  LOOKALIKE_PREFIX,
2442
3239
  LOOKALIKE_SUFFIX,
3240
+ MAX_FEE_REASKS,
3241
+ MULTI_LABEL_SUFFIXES,
2443
3242
  NEAR_CHAIN,
2444
3243
  NEAR_PROVIDERS,
2445
3244
  NON_EVM_CHAINS,
@@ -2460,8 +3259,11 @@ export {
2460
3259
  RouterError,
2461
3260
  SESSION_DEFAULT_AUTOLOCK_MS,
2462
3261
  SIGNING_METHODS,
3262
+ SIWE_FORBIDDEN_CHARS,
2463
3263
  SOLANA_CHAIN,
2464
3264
  SOLANA_RPCS,
3265
+ STORAGE_MAC_FIELD,
3266
+ STORAGE_VERSION_FIELD,
2465
3267
  Session,
2466
3268
  SwapTracker,
2467
3269
  SwapsStore,
@@ -2473,6 +3275,7 @@ export {
2473
3275
  TON_ENDPOINTS,
2474
3276
  TRON_CHAIN,
2475
3277
  TRON_PROVIDERS,
3278
+ TokenBucket,
2476
3279
  TonConnectError2 as TonConnectError,
2477
3280
  VAULT_DEFAULT_ITERATIONS,
2478
3281
  VAULT_LIMITS,
@@ -2487,6 +3290,7 @@ export {
2487
3290
  ZCASH_CHAIN,
2488
3291
  approvalChallenge,
2489
3292
  caip2FromChainId,
3293
+ canonicalJson,
2490
3294
  chainHex,
2491
3295
  chainTxMatcher,
2492
3296
  checkSenderOrigin,
@@ -2499,6 +3303,7 @@ export {
2499
3303
  createSignHandler,
2500
3304
  decodedMessage,
2501
3305
  derToRaw,
3306
+ deriveStorageMacKey,
2502
3307
  describeSiwe,
2503
3308
  earnApprovalPayload,
2504
3309
  earnPlanMatchers,
@@ -2506,6 +3311,7 @@ export {
2506
3311
  evmCallMatcher,
2507
3312
  executionFloor,
2508
3313
  fromBase64Url,
3314
+ gasLimitExcessive,
2509
3315
  hostOf,
2510
3316
  isCatalogChain,
2511
3317
  isEnvelopeV1,
@@ -2519,6 +3325,7 @@ export {
2519
3325
  isRelayToContent,
2520
3326
  levenshtein,
2521
3327
  lookalikeOf,
3328
+ macJsonStorage,
2522
3329
  memoryAlarms,
2523
3330
  memoryApprovalWindow,
2524
3331
  memoryJsonStorage,
@@ -2543,11 +3350,15 @@ export {
2543
3350
  rank,
2544
3351
  rawToDer,
2545
3352
  relayError,
3353
+ revealHidden,
2546
3354
  reviewEarnPlan,
2547
3355
  reviewSwapPlan,
2548
3356
  sameTonConnectAddress,
3357
+ siteOf,
3358
+ siweCrossSite,
2549
3359
  siweProblems,
2550
3360
  stampOrigin,
3361
+ storageVersionLedger,
2551
3362
  swapApprovalPayload,
2552
3363
  toBase64Url,
2553
3364
  toRelayError,