@anywalletkit/extension-kit 0.3.5 → 0.4.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",
@@ -1475,6 +1877,7 @@ var permissionDescriptor = (origin, accounts) => ({
1475
1877
  });
1476
1878
 
1477
1879
  // src/siwe.ts
1880
+ var SIWE_FORBIDDEN_CHARS = /[\u061c\u200b-\u200f\u202a-\u202e\u2060-\u2064\u2066-\u2069\ufeff]/;
1478
1881
  var HEADER_LINE = /^(?<domain>[^\n]+?) wants you to sign in with your Ethereum account:(?:\n|$)/;
1479
1882
  var ADDRESS_LINE = /^0x[0-9a-fA-F]{40}$/;
1480
1883
  var FIELD = /^([A-Za-z ]+): (.*)$/;
@@ -1578,12 +1981,20 @@ function siweProblems(m, ctx) {
1578
1981
  }
1579
1982
  return out;
1580
1983
  }
1984
+ function siweCrossSite(m, ctx) {
1985
+ const site = originWhere(ctx.origin) ?? (ctx.via?.verify === "VALID" && !ctx.via.scam ? whereOf(ctx.via.url) : null);
1986
+ if (!site) return null;
1987
+ const domain = whereOf(m.domain);
1988
+ return !domain || domain.host !== site.host ? { domain: m.domain, origin: `${site.scheme}://${site.host}` } : null;
1989
+ }
1581
1990
  function describeSiwe(text, ctx) {
1582
1991
  const p = parseLoose(text);
1583
1992
  if (!p) return null;
1584
1993
  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 };
1994
+ const cross = siweCrossSite(p.message, ctx);
1995
+ const crossSite = cross ? { crossSite: cross } : {};
1996
+ if (p.missing.length === 0) return { message: p.message, problems, ...crossSite };
1997
+ 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
1998
  }
1588
1999
 
1589
2000
  // src/signing.ts
@@ -1591,18 +2002,31 @@ var safeNonce = (n) => {
1591
2002
  if (n < 0n || n > BigInt(Number.MAX_SAFE_INTEGER)) throw new Error(`The RPC returned an impossible nonce (${n}).`);
1592
2003
  return Number(n.toString());
1593
2004
  };
2005
+ var MAX_FEE_REASKS = 3;
1594
2006
  var ADDRESS = /^0x[0-9a-fA-F]{40}$/;
1595
2007
  var HEX = /^0x([0-9a-fA-F]{2})*$/;
1596
2008
  var lc = (s) => s.toLowerCase();
2009
+ var INVISIBLE = /[\p{Cf}\u2028\u2029\u115f\u1160\u3164\uffa0\x00-\x08\x0b\x0c\x0e-\x1f\x7f-\x9f]/gu;
2010
+ function revealHidden(text) {
2011
+ let hidden = false;
2012
+ const out = text.replace(INVISIBLE, (c) => {
2013
+ hidden = true;
2014
+ return `\u27E8U+${c.codePointAt(0).toString(16).toUpperCase().padStart(4, "0")}\u27E9`;
2015
+ });
2016
+ return { text: out, hidden };
2017
+ }
1597
2018
  function messageText(value) {
1598
- if (typeof value !== "string") return "";
1599
- if (!HEX.test(value)) return value;
2019
+ return shownMessage(value).text;
2020
+ }
2021
+ function shownMessage(value) {
2022
+ if (typeof value !== "string") return { text: "", hidden: false };
2023
+ if (!HEX.test(value)) return revealHidden(value);
1600
2024
  try {
1601
2025
  const bytes = Uint8Array.from(value.slice(2).match(/../g) ?? [], (b) => Number.parseInt(b, 16));
1602
2026
  const s = new TextDecoder("utf-8", { fatal: true }).decode(bytes);
1603
- return /^[\x09\x0a\x0d\x20-\x7e -￿]*$/.test(s) ? s : value;
2027
+ return /^[\x09\x0a\x0d\x20-\x7e\u00a0-\uffff]*$/.test(s) ? revealHidden(s) : { text: value, hidden: false };
1604
2028
  } catch {
1605
- return value;
2029
+ return { text: value, hidden: false };
1606
2030
  }
1607
2031
  }
1608
2032
  function decodedMessage(value) {
@@ -1616,6 +2040,7 @@ function decodedMessage(value) {
1616
2040
  }
1617
2041
  var HIDDEN_CONTROL = /[\x00-\x08\x0b\x0c\x0e-\x1f\x7f-\x9f]/;
1618
2042
  var err = (code, message) => ({ code, message });
2043
+ var BLIND_HASH = /^0x[0-9a-fA-F]{64}$/;
1619
2044
  function classifySignRequest(input) {
1620
2045
  const { method, params } = input;
1621
2046
  const owned = new Set(input.accounts.map(lc));
@@ -1626,11 +2051,15 @@ function classifySignRequest(input) {
1626
2051
  if (ours(msg) && !ours(addr)) [msg, addr] = [addr, msg];
1627
2052
  if (!ours(addr)) return err(RELAY_ERRORS.unauthorized, "personal_sign for an address this wallet does not hold.");
1628
2053
  if (typeof msg !== "string") return err(RELAY_ERRORS.invalidParams, "personal_sign expects a message string.");
1629
- const text = messageText(msg);
2054
+ const blindHash = BLIND_HASH.test(msg);
2055
+ if (blindHash && !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.');
2056
+ const shown = shownMessage(msg);
2057
+ const text = shown.text;
1630
2058
  const signed = decodedMessage(msg) ?? text;
1631
2059
  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
2060
  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 } : {} };
2061
+ if (siwe && SIWE_FORBIDDEN_CHARS.test(signed)) return err(RELAY_ERRORS.invalidParams, "This sign-in message contains invisible or text-direction characters \u2014 refused.");
2062
+ return { kind: "sign-message", method, account: checksum(addr), text, raw: msg, ...siwe ? { siwe } : {}, ...shown.hidden ? { hiddenChars: true } : {}, ...blindHash ? { blindHash: true } : {} };
1634
2063
  }
1635
2064
  if (method === "eth_signTypedData_v4" || method === "eth_signTypedData_v3" || method === "eth_signTypedData") {
1636
2065
  const [addr, data] = params;
@@ -1648,7 +2077,8 @@ function classifySignRequest(input) {
1648
2077
  if (cid !== void 0 && cid !== null && Number(cid) !== input.network.chainId) {
1649
2078
  return err(RELAY_ERRORS.chainDisconnected, `Typed data is for chain ${String(cid)}; this site is on ${input.network.name} (${input.network.chainId}).`);
1650
2079
  }
1651
- return { kind: "sign-typed-data", method, account: checksum(addr), text: JSON.stringify(parsed, bigintSafe, 2), typedData: parsed };
2080
+ const shown = revealHidden(JSON.stringify(parsed, bigintSafe, 2));
2081
+ return { kind: "sign-typed-data", method, account: checksum(addr), text: shown.text, typedData: parsed, ...shown.hidden ? { hiddenChars: true } : {} };
1652
2082
  }
1653
2083
  if (method === "eth_sendTransaction") {
1654
2084
  const tx = params[0] ?? {};
@@ -1697,16 +2127,33 @@ function overpriced(shown, fresh) {
1697
2127
  if (shown.maxFeePerGas !== void 0 && fresh.maxFeePerGas !== void 0) return big(shown.maxFeePerGas) > STALE_FEE_FACTOR * big(fresh.maxFeePerGas);
1698
2128
  return false;
1699
2129
  }
2130
+ function gasLimitExcessive(limit, estimate) {
2131
+ const a = estimate * 3n / 2n;
2132
+ const b = estimate + 50000n;
2133
+ return limit > (a > b ? a : b);
2134
+ }
1700
2135
  async function prepareTransaction(deps, network, from, tx) {
1701
2136
  const call = { from, to: tx.to, value: hex(BigInt(tx.value)), ...tx.data ? { data: tx.data } : {} };
2137
+ const estimate = () => deps.rpc(network, "eth_estimateGas", [call]);
1702
2138
  const [nonceHex, block, gasHex] = await Promise.all([
1703
2139
  deps.rpc(network, "eth_getTransactionCount", [from, "pending"]),
1704
2140
  deps.rpc(network, "eth_getBlockByNumber", ["latest", false]),
1705
- tx.gas ? Promise.resolve(tx.gas) : deps.rpc(network, "eth_estimateGas", [call])
2141
+ tx.gas ? estimate().then((v) => v, () => null) : estimate()
1706
2142
  ]);
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));
2143
+ const nonce = safeNonce(big(nonceHex));
2144
+ if (!tx.gas) {
2145
+ const estimated = big(gasHex);
2146
+ return withFees(network.chainId, nonce, (estimated * 120n + 99n) / 100n, await readFees(deps, network, block));
2147
+ }
2148
+ const limit = big(tx.gas);
2149
+ const prepared = withFees(network.chainId, nonce, limit, await readFees(deps, network, block));
2150
+ let est = null;
2151
+ try {
2152
+ est = typeof gasHex === "string" ? big(gasHex) : null;
2153
+ } catch {
2154
+ est = null;
2155
+ }
2156
+ return est !== null && est > 0n ? { ...prepared, gasCheck: { estimate: hex(est), excessive: gasLimitExcessive(limit, est) } } : prepared;
1710
2157
  }
1711
2158
  function createSignHandler(deps) {
1712
2159
  const tails = /* @__PURE__ */ new Map();
@@ -1722,7 +2169,8 @@ function createSignHandler(deps) {
1722
2169
  };
1723
2170
  return async ({ origin, network, method, params, accounts }) => {
1724
2171
  const via = deps.approvals.viaFor(origin);
1725
- const info = classifySignRequest({ network, method, params, accounts, origin, ...via ? { via } : {} });
2172
+ const allowBlindHash = method === "personal_sign" && deps.allowBlindHash ? await deps.allowBlindHash() : false;
2173
+ const info = classifySignRequest({ network, method, params, accounts, origin, ...via ? { via } : {}, ...allowBlindHash ? { allowBlindHash: true } : {} });
1726
2174
  if ("code" in info) throw RouterError.from(info);
1727
2175
  const prepared = info.kind === "send-transaction" ? await prepareTransaction(deps, network, info.account, info.tx) : void 0;
1728
2176
  const preview = deps.preview ? await deps.preview({ network, request: info, accounts }).catch(() => null) : null;
@@ -1732,14 +2180,20 @@ function createSignHandler(deps) {
1732
2180
  const key = `${network.caip2}|${lc(info.account)}`;
1733
2181
  return serial(key, async () => {
1734
2182
  let shown = prepared;
2183
+ let reasks = 0;
1735
2184
  for (; ; ) {
1736
2185
  const [nonceHex, fees] = await Promise.all([deps.rpc(network, "eth_getTransactionCount", [info.account, "pending"]), readFees(deps, network)]);
1737
2186
  const local = nextNonce.get(key) ?? 0;
1738
2187
  const nodeNonce = safeNonce(big(nonceHex));
1739
2188
  const nonce = nodeNonce > local ? nodeNonce : local;
1740
2189
  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 } });
2190
+ const next = withFees(network.chainId, nonce, big(shown.gasLimit), fees);
2191
+ const ceiling = deps.feeCeiling?.(network);
2192
+ 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.`);
2193
+ 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.");
2194
+ reasks++;
2195
+ await deps.approvals.request({ origin, chain: network.caip2, kind: info.kind, payload: { ...payload, prepared: next, repriced: true, repricedFrom: shown.maxFee } }, { followUp: true });
2196
+ shown = next;
1743
2197
  continue;
1744
2198
  }
1745
2199
  const fresh = withFees(network.chainId, nonce, big(shown.gasLimit), fees);
@@ -1827,7 +2281,7 @@ function chainTxMatcher(expected) {
1827
2281
  }
1828
2282
 
1829
2283
  // src/swap.ts
1830
- import { validQuote } from "@anywalletkit/protocol-swap2-core";
2284
+ import { slippageFloor, validQuote } from "@anywalletkit/protocol-swap2-core";
1831
2285
  import { typedDataHash } from "@anywalletkit/wallet-evm";
1832
2286
  async function quoteRoutes(providers, params, options = {}) {
1833
2287
  const timeoutMs = options.timeoutMs ?? 2e4;
@@ -1861,7 +2315,14 @@ async function quoteRoutes(providers, params, options = {}) {
1861
2315
  candidates.push(p);
1862
2316
  }
1863
2317
  const pending = new Set(candidates.map((p) => p.name));
1864
- const view = () => ({ routes: rank(Object.values(quotes)), pending: [...pending], refusals: { ...refusals }, quotes: { ...quotes } });
2318
+ const depositAddressRoute = options.depositAddressRoute;
2319
+ const rankOptions = {
2320
+ ...params.slippageBps !== void 0 ? { slippageBps: params.slippageBps } : {},
2321
+ ...options.depositAddressProviders ? { depositAddressProviders: options.depositAddressProviders } : {},
2322
+ ...depositAddressRoute ? { depositAddressRoute: (q) => depositAddressRoute(q, params) } : {}
2323
+ };
2324
+ const { slippageBps: _slippage, ...shapeParams } = params;
2325
+ const view = () => ({ routes: rank(Object.values(quotes), rankOptions), pending: [...pending], refusals: { ...refusals }, quotes: { ...quotes } });
1865
2326
  const emit = () => options.onUpdate?.(view());
1866
2327
  emit();
1867
2328
  await Promise.all(
@@ -1876,7 +2337,7 @@ async function quoteRoutes(providers, params, options = {}) {
1876
2337
  const q = await Promise.race([p.getQuote({ ...params, dry: true, onRefusal: (r) => {
1877
2338
  refusal = r;
1878
2339
  } }, ctrl.signal), aborted]);
1879
- if (q && validQuote(q, { ...params, dry: true }, p.name)) quotes[p.name] = q;
2340
+ if (q && validQuote(q, { ...shapeParams, dry: true }, p.name)) quotes[p.name] = q;
1880
2341
  else refusals[p.name] = refusal ?? (q ? "returned an invalid quote" : "no route for this pair right now");
1881
2342
  } catch (e) {
1882
2343
  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 +2351,50 @@ async function quoteRoutes(providers, params, options = {}) {
1890
2351
  );
1891
2352
  return view();
1892
2353
  }
1893
- function rank(quotes) {
2354
+ function gapBps(dest, min) {
2355
+ if (dest <= 0n || min >= dest) return 0;
2356
+ return Number((((dest - min) * 10000n + dest - 1n) / dest).toString());
2357
+ }
2358
+ function rank(quotes, options = {}) {
1894
2359
  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
2360
  const fastest = sorted.length ? sorted.reduce((f, q) => q.etaSeconds < f.etaSeconds ? q : f) : null;
1896
- return sorted.map((q, i) => {
2361
+ const depositProviders = new Set(options.depositAddressProviders ?? []);
2362
+ const paysAssignedAddress = (q) => {
2363
+ if (depositProviders.has(q.provider)) return true;
2364
+ if (!options.depositAddressRoute) return false;
2365
+ try {
2366
+ return options.depositAddressRoute(q) === true;
2367
+ } catch {
2368
+ return true;
2369
+ }
2370
+ };
2371
+ const assigned = new Map(sorted.map((q) => [q, paysAssignedAddress(q)]));
2372
+ const s = options.slippageBps;
2373
+ const warningsOf = (q) => {
2374
+ const w = [];
2375
+ if (s !== void 0 && Number.isInteger(s) && s >= 0 && s < 1e4) {
2376
+ const dest = BigInt(q.destAmount);
2377
+ const min = BigInt(q.minDestAmount);
2378
+ if (min < slippageFloor(dest, s)) w.push({ kind: "min-below-slippage", slippageBps: s, minGapBps: gapBps(dest, min) });
2379
+ }
2380
+ if (assigned.get(q)) w.push({ kind: "deposit-address" });
2381
+ return w;
2382
+ };
2383
+ const warned = new Map(sorted.map((q) => [q, warningsOf(q)]));
2384
+ const verifiableExists = sorted.some((q) => !assigned.get(q));
2385
+ const best = sorted.find((q) => {
2386
+ const w = warned.get(q);
2387
+ return !w.some((x) => x.kind === "min-below-slippage") && !(w.some((x) => x.kind === "deposit-address") && verifiableExists);
2388
+ });
2389
+ const ordered = best ? [best, ...sorted.filter((q) => q !== best)] : sorted;
2390
+ return ordered.map((q) => {
1897
2391
  const poolFee = q.providerData?.poolFee;
1898
2392
  const feeNote = typeof poolFee === "string" && poolFee && q.feeUsd === 0 ? poolFee === "pools" ? "pool fees in price" : `pool ${poolFee} in price` : void 0;
1899
2393
  const tags = [];
1900
- if (i === 0) tags.push("best");
2394
+ if (q === best) tags.push("best");
1901
2395
  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 } : {} };
2396
+ const warnings = warned.get(q);
2397
+ 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
2398
  });
1904
2399
  }
1905
2400
  var FLOOR_TOLERANCE_BPS = 50n;
@@ -1918,18 +2413,21 @@ function depositOf(s, approvals) {
1918
2413
  if (!s.data.startsWith(TRANSFER_SELECTOR) || s.data.length < 10 + 64) return void 0;
1919
2414
  return `0x${s.data.slice(10 + 24, 10 + 64)}`;
1920
2415
  }
2416
+ var pct = (bps) => `${Number((bps / 100).toFixed(2))}%`;
1921
2417
  function swapApprovalPayload(plan, meta) {
1922
2418
  const isOwn = meta.own.some((a) => lc3(a) === lc3(plan.recipient));
2419
+ const assigned = meta.depositAddressAssignedByProvider === true;
1923
2420
  const checks = [{ status: "ok", label: `Built by the ${meta.providerLabel} module` }];
2421
+ 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
2422
  const s = plan.source;
1925
2423
  let source;
1926
2424
  if (s.kind === "call") {
1927
2425
  const deposit = depositOf(s, plan.approvals.length);
1928
2426
  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` });
2427
+ checks.push(deposit || assigned ? depositCheck : { status: "ok", label: `Calls the ${meta.providerLabel} contract` });
1930
2428
  } else if (s.kind === "token-transfer") {
1931
2429
  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` });
2430
+ checks.push(depositCheck);
1933
2431
  } else if (s.kind === "serialized") {
1934
2432
  source = { kind: "serialized" };
1935
2433
  checks.push({ status: "ok", label: "Balance changes are proven before signing (SDK guard)" });
@@ -1940,7 +2438,14 @@ function swapApprovalPayload(plan, meta) {
1940
2438
  }
1941
2439
  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
2440
  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` });
2441
+ 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" });
2442
+ let slippage;
2443
+ const sb = meta.slippageBps;
2444
+ if (sb !== void 0 && Number.isInteger(sb) && sb >= 0 && sb < 1e4) {
2445
+ const below = plan.minDestAmount < slippageFloor(plan.destAmount, sb);
2446
+ slippage = { bps: sb, minGapBps: gapBps(plan.destAmount, plan.minDestAmount), below };
2447
+ 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` });
2448
+ }
1944
2449
  return {
1945
2450
  provider: plan.provider,
1946
2451
  providerLabel: meta.providerLabel,
@@ -1957,7 +2462,9 @@ function swapApprovalPayload(plan, meta) {
1957
2462
  validUntilMs: plan.validUntilMs,
1958
2463
  approvals: plan.approvals.map((a) => ({ token: a.token, spender: a.spender, amount: a.amount.toString() })),
1959
2464
  source,
1960
- checks
2465
+ checks,
2466
+ ...slippage ? { slippage } : {},
2467
+ ...assigned ? { depositAssignedByProvider: true } : {}
1961
2468
  };
1962
2469
  }
1963
2470
  function parseTypedOrder(json) {
@@ -1999,15 +2506,15 @@ import { SWAP2_ID_PREFIX } from "@anywalletkit/protocol-swap2-core";
1999
2506
  var ACTIVE_STATUSES = /* @__PURE__ */ new Set(["pending", "action-required", "refund-pending", "partial", "claiming"]);
2000
2507
  function parseSwapId(id) {
2001
2508
  if (!id.startsWith(`${SWAP2_ID_PREFIX}.`)) return null;
2002
- const parts = id.slice(SWAP2_ID_PREFIX.length + 1).split(".");
2003
- if (parts.length < 2 || parts.length > 5 || !parts[0] || !parts[1]) return null;
2004
- if (parts[4] !== void 0 && (parts[4] !== "ok" || !parts[3])) return null;
2005
- const provider = decodeURIComponent(parts[0]);
2006
- const intentId = decodeURIComponent(parts[1]);
2007
- const sourceTxHash = parts[2] ? decodeURIComponent(parts[2]) : void 0;
2008
- const destTxHash = parts[3] ? decodeURIComponent(parts[3]) : void 0;
2509
+ const parts2 = id.slice(SWAP2_ID_PREFIX.length + 1).split(".");
2510
+ if (parts2.length < 2 || parts2.length > 5 || !parts2[0] || !parts2[1]) return null;
2511
+ if (parts2[4] !== void 0 && (parts2[4] !== "ok" || !parts2[3])) return null;
2512
+ const provider = decodeURIComponent(parts2[0]);
2513
+ const intentId = decodeURIComponent(parts2[1]);
2514
+ const sourceTxHash = parts2[2] ? decodeURIComponent(parts2[2]) : void 0;
2515
+ const destTxHash = parts2[3] ? decodeURIComponent(parts2[3]) : void 0;
2009
2516
  if (destTxHash && !sourceTxHash) return null;
2010
- return { provider, intentId, ...sourceTxHash ? { sourceTxHash } : {}, ...destTxHash ? { destTxHash } : {}, ...parts[4] !== void 0 ? { destTxConfirmed: true } : {} };
2517
+ return { provider, intentId, ...sourceTxHash ? { sourceTxHash } : {}, ...destTxHash ? { destTxHash } : {}, ...parts2[4] !== void 0 ? { destTxConfirmed: true } : {} };
2011
2518
  }
2012
2519
  var SwapsStore = class {
2013
2520
  constructor(storage) {
@@ -2146,115 +2653,38 @@ async function reviewEarnPlan(approvals, gate, meta, plan) {
2146
2653
  }
2147
2654
 
2148
2655
  // src/tonconnect.ts
2149
- import { sha256 } from "@noble/hashes/sha2.js";
2150
- var TONCONNECT_ERRORS = { unknown: 0, badRequest: 1, manifestNotFound: 2, manifestContent: 3, unknownApp: 100, userDeclined: 300, methodNotSupported: 400 };
2151
- var TonConnectError = class extends Error {
2152
- constructor(code, message) {
2153
- super(message);
2154
- this.code = code;
2155
- this.name = "TonConnectError";
2156
- }
2157
- code;
2158
- };
2159
- function parseConnectRequest(message) {
2160
- const m = message;
2161
- if (!m || typeof m.manifestUrl !== "string" || !/^https:\/\//.test(m.manifestUrl)) throw new TonConnectError(TONCONNECT_ERRORS.badRequest, "ConnectRequest needs an https manifestUrl.");
2162
- const items = Array.isArray(m.items) ? m.items : [];
2163
- if (!items.some((i) => i?.name === "ton_addr")) throw new TonConnectError(TONCONNECT_ERRORS.badRequest, "ConnectRequest must ask for ton_addr.");
2164
- const proof = items.find((i) => i?.name === "ton_proof");
2165
- if (proof && typeof proof.payload !== "string") throw new TonConnectError(TONCONNECT_ERRORS.badRequest, "ton_proof needs a string payload.");
2166
- for (const i of items) if (i?.name !== "ton_addr" && i?.name !== "ton_proof") throw new TonConnectError(TONCONNECT_ERRORS.badRequest, `Unknown connect item ${String(i?.name)}.`);
2167
- return { manifestUrl: m.manifestUrl, ...proof ? { proofPayload: proof.payload } : {} };
2168
- }
2169
- var TONCONNECT_MAX_MESSAGES = 4;
2170
- function parseSendTransaction(param, options = {}) {
2171
- let o;
2172
- try {
2173
- o = typeof param === "string" ? JSON.parse(param) : param;
2174
- } catch {
2175
- throw new TonConnectError(TONCONNECT_ERRORS.badRequest, "sendTransaction params[0] is not valid JSON.");
2176
- }
2177
- if (!o || typeof o !== "object") throw new TonConnectError(TONCONNECT_ERRORS.badRequest, "sendTransaction needs a request object.");
2178
- const network = options.network ?? "-239";
2179
- if (o.network !== void 0 && String(o.network) !== network) throw new TonConnectError(TONCONNECT_ERRORS.badRequest, `This wallet is on network ${network}; the request names ${String(o.network)}.`);
2180
- const raw = Array.isArray(o.messages) ? o.messages : [];
2181
- if (raw.length === 0) throw new TonConnectError(TONCONNECT_ERRORS.badRequest, "sendTransaction has no messages.");
2182
- if (raw.length > (options.maxMessages ?? TONCONNECT_MAX_MESSAGES)) throw new TonConnectError(TONCONNECT_ERRORS.badRequest, `At most ${options.maxMessages ?? TONCONNECT_MAX_MESSAGES} messages per transaction.`);
2183
- const messages = raw.map((m, i) => {
2184
- if (typeof m.address !== "string" || !m.address) throw new TonConnectError(TONCONNECT_ERRORS.badRequest, `Message ${i + 1} has no address.`);
2185
- if (typeof m.amount !== "string" || !/^[0-9]+$/.test(m.amount)) throw new TonConnectError(TONCONNECT_ERRORS.badRequest, `Message ${i + 1} amount must be a decimal string of nanotons.`);
2186
- if (m.payload !== void 0 && typeof m.payload !== "string") throw new TonConnectError(TONCONNECT_ERRORS.badRequest, `Message ${i + 1} payload must be a base64 BoC.`);
2187
- if (m.stateInit !== void 0 && typeof m.stateInit !== "string") throw new TonConnectError(TONCONNECT_ERRORS.badRequest, `Message ${i + 1} stateInit must be a base64 BoC.`);
2188
- return { address: m.address, amount: m.amount, ...m.payload ? { payload: m.payload } : {}, ...m.stateInit ? { stateInit: m.stateInit } : {} };
2189
- });
2190
- const validUntil = typeof o.valid_until === "number" ? o.valid_until : typeof o.validUntil === "number" ? o.validUntil : void 0;
2191
- if (validUntil !== void 0 && validUntil * 1e3 < Date.now()) throw new TonConnectError(TONCONNECT_ERRORS.badRequest, "The request has expired (valid_until is in the past).");
2192
- return { messages, ...validUntil !== void 0 ? { validUntil } : {}, ...o.network !== void 0 ? { network: String(o.network) } : {}, ...typeof o.from === "string" ? { from: o.from } : {} };
2193
- }
2194
- function parseSignData(param) {
2195
- let o;
2196
- try {
2197
- o = typeof param === "string" ? JSON.parse(param) : param;
2198
- } catch {
2199
- throw new TonConnectError(TONCONNECT_ERRORS.badRequest, "signData params[0] is not valid JSON.");
2200
- }
2201
- if (o?.type === "text" && typeof o.text === "string") return { type: "text", text: o.text };
2202
- if (o?.type === "binary" && typeof o.bytes === "string") return { type: "binary", bytes: o.bytes };
2203
- if (o?.type === "cell") throw new TonConnectError(TONCONNECT_ERRORS.methodNotSupported, "signData with a cell payload is not supported by this wallet.");
2204
- throw new TonConnectError(TONCONNECT_ERRORS.badRequest, 'signData payload must be { type: "text", text } or { type: "binary", bytes }.');
2205
- }
2206
- var utf82 = (s) => new TextEncoder().encode(s);
2207
- var int32BE = (n) => {
2208
- const b = new Uint8Array(4);
2209
- new DataView(b.buffer).setInt32(0, n, false);
2210
- return b;
2211
- };
2212
- var uint32LE = (n) => {
2213
- const b = new Uint8Array(4);
2214
- new DataView(b.buffer).setUint32(0, n, true);
2215
- return b;
2216
- };
2217
- var uint32BE = (n) => {
2218
- const b = new Uint8Array(4);
2219
- new DataView(b.buffer).setUint32(0, n, false);
2220
- return b;
2221
- };
2222
- var uint64LE = (n) => {
2223
- const b = new Uint8Array(8);
2224
- new DataView(b.buffer).setBigUint64(0, BigInt(n), true);
2225
- return b;
2226
- };
2227
- var uint64BE = (n) => {
2228
- const b = new Uint8Array(8);
2229
- new DataView(b.buffer).setBigUint64(0, BigInt(n), false);
2230
- return b;
2231
- };
2232
- var concat = (...parts) => {
2233
- const out = new Uint8Array(parts.reduce((n, p) => n + p.length, 0));
2234
- let o = 0;
2235
- for (const p of parts) {
2236
- out.set(p, o);
2237
- o += p.length;
2238
- }
2239
- return out;
2656
+ import { TONCONNECT_ERRORS, TonConnectError, tonAddressParts, tonProofDigest, tonSignDataDigest } from "@anywalletkit/wallet-ton/tonconnect";
2657
+ import {
2658
+ TONCONNECT_ERRORS as TONCONNECT_ERRORS2,
2659
+ TONCONNECT_MAX_MESSAGES,
2660
+ TONCONNECT_NETWORKS,
2661
+ TONCONNECT_SIGN_DATA_MAX,
2662
+ TonConnectError as TonConnectError2,
2663
+ parseConnectRequest,
2664
+ parseSendTransaction,
2665
+ parseSignData,
2666
+ tonConnectRawAddress,
2667
+ tonConnectFriendlyAddress,
2668
+ sameTonConnectAddress
2669
+ } from "@anywalletkit/wallet-ton/tonconnect";
2670
+ var parts = (a) => {
2671
+ if (a.hash.length !== 32) throw new TonConnectError(TONCONNECT_ERRORS.unknown, "address hash must be 32 bytes");
2672
+ return { workChain: a.workchain, hash: a.hash };
2240
2673
  };
2241
2674
  function tonProofMessage(input) {
2242
- if (input.address.hash.length !== 32) throw new TonConnectError(TONCONNECT_ERRORS.unknown, "address hash must be 32 bytes");
2243
- const domain = utf82(input.domain);
2244
- const inner = concat(utf82("ton-proof-item-v2/"), int32BE(input.address.workchain), input.address.hash, uint32LE(domain.length), domain, uint64LE(input.timestamp), utf82(input.payload));
2245
- return sha256(concat(new Uint8Array([255, 255]), utf82("ton-connect"), sha256(inner)));
2675
+ return tonProofDigest(parts(input.address), { domain: input.domain, timestamp: input.timestamp, payload: input.payload });
2246
2676
  }
2247
2677
  function tonSignDataMessage(input) {
2248
- if (input.address.hash.length !== 32) throw new TonConnectError(TONCONNECT_ERRORS.unknown, "address hash must be 32 bytes");
2249
- const domain = utf82(input.domain);
2250
- const payload = input.payload.type === "text" ? utf82(input.payload.text) : Uint8Array.from(atob(input.payload.bytes), (c) => c.charCodeAt(0));
2251
- const prefix = utf82(input.payload.type === "text" ? "txt" : "bin");
2252
- return sha256(concat(new Uint8Array([255, 255]), utf82("ton-connect/sign-data/"), int32BE(input.address.workchain), input.address.hash, uint32BE(domain.length), domain, uint64BE(input.timestamp), prefix, uint32BE(payload.length), payload));
2678
+ return tonSignDataDigest(parts(input.address), { domain: input.domain, timestamp: input.timestamp, payload: input.payload });
2253
2679
  }
2254
2680
  function parseRawAddress(raw) {
2255
- const m = /^(-?\d+):([0-9a-fA-F]{64})$/.exec(raw);
2256
- if (!m) throw new TonConnectError(TONCONNECT_ERRORS.unknown, `not a raw TON address: ${raw}`);
2257
- return { workchain: Number(m[1]), hash: Uint8Array.from(m[2].match(/../g), (h) => Number.parseInt(h, 16)) };
2681
+ if (!/^-?\d+:[0-9a-fA-F]{64}$/.test(raw)) throw new TonConnectError(TONCONNECT_ERRORS.unknown, `not a raw TON address: ${raw}`);
2682
+ try {
2683
+ const p = tonAddressParts(raw);
2684
+ return { workchain: p.workChain, hash: p.hash };
2685
+ } catch {
2686
+ throw new TonConnectError(TONCONNECT_ERRORS.unknown, `not a raw TON address: ${raw}`);
2687
+ }
2258
2688
  }
2259
2689
  function proofDomain(origin) {
2260
2690
  try {
@@ -2264,7 +2694,7 @@ function proofDomain(origin) {
2264
2694
  }
2265
2695
  }
2266
2696
  function tonConnectErrorCode(e) {
2267
- if (e instanceof TonConnectError) return e.code;
2697
+ if (e instanceof TonConnectError || e?.name === "TonConnectError") return e.code;
2268
2698
  const code = e?.code;
2269
2699
  if (code === 4001) return TONCONNECT_ERRORS.userDeclined;
2270
2700
  if (code === 4100) return TONCONNECT_ERRORS.unknownApp;
@@ -2349,14 +2779,14 @@ function hostOf(hostOrOrigin) {
2349
2779
  }
2350
2780
  }
2351
2781
  function matchesList(host, list) {
2352
- const parts = host.split(".");
2353
- for (let i = 0; i < parts.length - 1; i++) if (list.has(parts.slice(i).join("."))) return true;
2782
+ const parts2 = host.split(".");
2783
+ for (let i = 0; i < parts2.length - 1; i++) if (list.has(parts2.slice(i).join("."))) return true;
2354
2784
  return list.has(host);
2355
2785
  }
2356
2786
  function stripForFuzzy(host) {
2357
- const parts = host.replace(/^www\./, "").split(".");
2358
- if (parts.length > 1) parts.pop();
2359
- return parts.join(".");
2787
+ const parts2 = host.replace(/^www\./, "").split(".");
2788
+ if (parts2.length > 1) parts2.pop();
2789
+ return parts2.join(".");
2360
2790
  }
2361
2791
  function levenshtein(a, b) {
2362
2792
  if (a === b) return 0;
@@ -2497,6 +2927,10 @@ var RecipientBook = class {
2497
2927
  await this.storage.set(s);
2498
2928
  this._state = s;
2499
2929
  }
2930
+ /** Forget the cached book: the next read asks storage again (e.g. a storage that only answers once the wallet is unlocked). */
2931
+ reload() {
2932
+ this._state = null;
2933
+ }
2500
2934
  /** `known` → the address is one the user paid before; `lookalike` → it imitates one. */
2501
2935
  async check(namespace, address) {
2502
2936
  const list = await this.list(namespace);
@@ -2504,9 +2938,243 @@ var RecipientBook = class {
2504
2938
  return { known, lookalike: known ? null : lookalikeOf(address, list) };
2505
2939
  }
2506
2940
  };
2941
+
2942
+ // src/mac-storage.ts
2943
+ import { zeroize as zeroize4 } from "@anywalletkit/core";
2944
+ var STORAGE_MAC_FIELD = "anywalletkit.mac/1";
2945
+ var STORAGE_VERSION_FIELD = "anywalletkit.ver/1";
2946
+ var HKDF_INFO2 = "anywalletkit/storage-mac/1";
2947
+ var KEY_BYTES = 32;
2948
+ async function deriveStorageMacKey(dek, crypto2 = globalThis.crypto) {
2949
+ if (dek.length !== 32) throw new VaultError("Vault key must be 32 bytes.", "invalid-input");
2950
+ const ikm = await withSecretBuffer(dek, (b) => crypto2.subtle.importKey("raw", b, "HKDF", false, ["deriveBits"]));
2951
+ 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);
2952
+ return new Uint8Array(bits);
2953
+ }
2954
+ function macJsonStorage(inner, options) {
2955
+ const { label, versions } = options;
2956
+ if (versions && label.includes("\n")) throw new VaultError("A versioned store label has no line break.", "invalid-input");
2957
+ const readWithoutKey = options.readWithoutKey ?? "unverified";
2958
+ const writeWithoutKey = options.writeWithoutKey ?? "unsigned";
2959
+ const withKey = async (given, fn) => {
2960
+ const key = given ? Uint8Array.from(given) : await options.key();
2961
+ try {
2962
+ return await fn(key);
2963
+ } finally {
2964
+ if (key) zeroize4(key);
2965
+ }
2966
+ };
2967
+ const stored = async (key) => {
2968
+ const raw = await inner.get();
2969
+ if (raw === null || raw === void 0) return { state: "empty", value: null };
2970
+ const envelope = isStored(raw);
2971
+ const value = envelope ? raw.value : raw;
2972
+ if (!envelope || raw[STORAGE_MAC_FIELD] === null) return { state: "unsigned", value };
2973
+ if (!key) return { state: "no-key", value };
2974
+ if (!versions) return { state: await verify(key, label, value, raw[STORAGE_MAC_FIELD]) ? "ok" : "invalid", value };
2975
+ const version = STORAGE_VERSION_FIELD in raw ? raw[STORAGE_VERSION_FIELD] : 0;
2976
+ if (version !== 0 && !isVersion(version)) return { state: "invalid", value };
2977
+ if (!await verify(key, label, value, raw[STORAGE_MAC_FIELD], version || void 0)) return { state: "invalid", value };
2978
+ return { state: "ok", value, version };
2979
+ };
2980
+ const put = async (key, value, version) => {
2981
+ const mac = await sign(key, label, value, version);
2982
+ await inner.set(version === void 0 ? { [STORAGE_MAC_FIELD]: mac, value } : { [STORAGE_MAC_FIELD]: mac, [STORAGE_VERSION_FIELD]: version, value });
2983
+ };
2984
+ const seen = async (key) => {
2985
+ const r = await stored(key);
2986
+ return r.state === "ok" ? r.version ?? 0 : 0;
2987
+ };
2988
+ const read = (given) => withKey(given, async (key) => {
2989
+ if (!versions || !key) return stored(key);
2990
+ return versions.exclusive(async (ledger) => {
2991
+ const r = await stored(key);
2992
+ if (r.state === "empty") return await ledger.check(label, null, key) === "ok" ? r : { state: "stale", value: null };
2993
+ if (r.state !== "ok") return r;
2994
+ return await ledger.check(label, r.version ?? 0, key) === "ok" ? r : { ...r, state: "stale" };
2995
+ });
2996
+ });
2997
+ const write = (value, given) => withKey(given, async (key) => {
2998
+ if (!key) throw new VaultError("The wallet is locked: unlock it to change this.", "locked");
2999
+ if (!versions) return put(key, value);
3000
+ await versions.exclusive((ledger) => ledger.advance(label, key, () => seen(key), (version) => put(key, value, version)));
3001
+ });
3002
+ return {
3003
+ read,
3004
+ write,
3005
+ async get() {
3006
+ const r = await read();
3007
+ if (r.state === "ok") return r.value;
3008
+ if (r.state === "no-key" && readWithoutKey === "unverified") return r.value;
3009
+ return null;
3010
+ },
3011
+ async set(value) {
3012
+ await withKey(void 0, async (key) => {
3013
+ if (key) return write(value, key);
3014
+ if (writeWithoutKey === "refuse") throw new VaultError("The wallet is locked: unlock it to change this.", "locked");
3015
+ await inner.set({ [STORAGE_MAC_FIELD]: null, value });
3016
+ });
3017
+ },
3018
+ async rekey(oldKey, newKey) {
3019
+ if (!versions) {
3020
+ const r = await read(oldKey);
3021
+ if (r.state === "ok") await write(r.value, newKey);
3022
+ return r.state;
3023
+ }
3024
+ return versions.exclusive(async (ledger) => {
3025
+ const r = await stored(oldKey);
3026
+ if (r.state === "empty") return await ledger.check(label, null, newKey) === "ok" ? "empty" : "stale";
3027
+ if (r.state !== "ok") return r.state;
3028
+ const version = r.version ?? 0;
3029
+ if (await ledger.check(label, version, newKey) !== "ok") return "stale";
3030
+ if (version > 0) await put(newKey, r.value, version);
3031
+ else await ledger.advance(label, newKey, async () => 0, (v) => put(newKey, r.value, v));
3032
+ return "ok";
3033
+ });
3034
+ }
3035
+ };
3036
+ }
3037
+ function isStored(raw) {
3038
+ if (typeof raw !== "object" || raw === null || !(STORAGE_MAC_FIELD in raw) || !("value" in raw)) return false;
3039
+ const mac = raw[STORAGE_MAC_FIELD];
3040
+ return mac === null || typeof mac === "string";
3041
+ }
3042
+ async function hmacKey(key, usage) {
3043
+ if (key.length !== KEY_BYTES) throw new VaultError("A storage MAC key is 32 bytes.", "invalid-input");
3044
+ return withSecretBuffer(key, (b) => globalThis.crypto.subtle.importKey("raw", b, { name: "HMAC", hash: "SHA-256" }, false, [usage]));
3045
+ }
3046
+ function macInput(label, value, version) {
3047
+ return toArrayBuffer(utf8.encode(version === void 0 ? `${label}
3048
+ ${canonicalJson(value)}` : `${label}
3049
+ ${version}
3050
+ ${canonicalJson(value)}`));
3051
+ }
3052
+ async function sign(key, label, value, version) {
3053
+ return toBase64(new Uint8Array(await globalThis.crypto.subtle.sign("HMAC", await hmacKey(key, "sign"), macInput(label, value, version))));
3054
+ }
3055
+ async function verify(key, label, value, mac, version) {
3056
+ let bytes;
3057
+ try {
3058
+ bytes = fromBase64(mac);
3059
+ } catch {
3060
+ return false;
3061
+ }
3062
+ return globalThis.crypto.subtle.verify("HMAC", await hmacKey(key, "verify"), toArrayBuffer(bytes), macInput(label, value, version));
3063
+ }
3064
+ var isVersion = (x) => typeof x === "number" && Number.isSafeInteger(x) && x > 0;
3065
+ function canonicalJson(value) {
3066
+ return JSON.stringify(sortKeys(JSON.parse(JSON.stringify(value ?? null))));
3067
+ }
3068
+ function sortKeys(x) {
3069
+ if (Array.isArray(x)) return x.map(sortKeys);
3070
+ if (x === null || typeof x !== "object") return x;
3071
+ const out = {};
3072
+ for (const k of Object.keys(x).sort()) out[k] = sortKeys(x[k]);
3073
+ return out;
3074
+ }
3075
+ var LEDGER_LABEL = "anywalletkit/storage-versions/1";
3076
+ function storageVersionLedger(options) {
3077
+ const label = options.label ?? LEDGER_LABEL;
3078
+ const acceptUnversioned = options.acceptUnversioned ?? false;
3079
+ const now = options.now ?? Date.now;
3080
+ let tail = Promise.resolve();
3081
+ const queue = (fn) => {
3082
+ const run = tail.then(fn, fn);
3083
+ tail = run.then(() => void 0, () => void 0);
3084
+ return run;
3085
+ };
3086
+ const high = async () => {
3087
+ const v = await options.mark.get();
3088
+ return isVersion(v) ? v : 0;
3089
+ };
3090
+ const parse = async (raw, key) => {
3091
+ if (!isStored(raw) || raw[STORAGE_MAC_FIELD] === null) return null;
3092
+ const epoch = raw[STORAGE_VERSION_FIELD];
3093
+ const v = raw.value;
3094
+ if (!isVersion(epoch) || typeof v !== "object" || v === null || !isVersion(v.base) || typeof v.sealed !== "boolean") return null;
3095
+ if (typeof v.versions !== "object" || v.versions === null || !Object.values(v.versions).every(isVersion)) return null;
3096
+ if (!await verify(key, label, v, raw[STORAGE_MAC_FIELD], epoch)) return null;
3097
+ return { epoch, base: v.base, sealed: v.sealed, versions: { ...v.versions } };
3098
+ };
3099
+ const load = async (key) => {
3100
+ const mark = await high();
3101
+ const raw = await options.manifest.get();
3102
+ if (raw === null || raw === void 0) return { state: mark > 0 ? "stale" : "empty" };
3103
+ const manifest = await parse(raw, key);
3104
+ if (!manifest) return { state: "invalid" };
3105
+ if (manifest.epoch < mark) return { state: "stale", manifest };
3106
+ if (manifest.epoch > mark) await options.mark.set(manifest.epoch);
3107
+ return { state: "ok", manifest };
3108
+ };
3109
+ const store = async (m, key) => {
3110
+ const value = { base: m.base, sealed: m.sealed, versions: m.versions };
3111
+ await options.manifest.set({ [STORAGE_MAC_FIELD]: await sign(key, label, value, m.epoch), [STORAGE_VERSION_FIELD]: m.epoch, value });
3112
+ if (m.epoch > await high()) await options.mark.set(m.epoch);
3113
+ };
3114
+ const fresh = async (sealed, prior, key) => {
3115
+ const floor = Math.max(await high(), Math.floor(now()), prior ? Math.max(prior.epoch, prior.base, ...Object.values(prior.versions)) : 0);
3116
+ if (!Number.isSafeInteger(floor + 2)) throw new VaultError("Storage version out of range.", "invalid-input");
3117
+ const m = { epoch: floor + 1, base: floor + 1, sealed, versions: {} };
3118
+ await store(m, key);
3119
+ return m;
3120
+ };
3121
+ const record = (m, name, version, key) => store({ ...m, epoch: m.epoch + 1, versions: { ...m.versions, [name]: version } }, key);
3122
+ const named = (name) => {
3123
+ if (name === label || name.includes("\n")) throw new VaultError("Not a store label.", "invalid-input");
3124
+ };
3125
+ const ops = {
3126
+ async check(name, version, key) {
3127
+ named(name);
3128
+ const l = await load(key);
3129
+ if (l.state === "empty") return version === null || version === 0 && acceptUnversioned ? "ok" : "stale";
3130
+ if (l.state !== "ok") return "stale";
3131
+ const m = l.manifest;
3132
+ const listed = m.versions[name];
3133
+ if (listed === void 0 && version === null) return m.sealed ? "stale" : "ok";
3134
+ if (listed === void 0 && version === 0) return !m.sealed && acceptUnversioned ? "ok" : "stale";
3135
+ if (version === null || version === 0) return "stale";
3136
+ if (version === listed) return "ok";
3137
+ if (version !== (listed ?? m.base) + 1) return "stale";
3138
+ await record(m, name, version, key).catch(() => void 0);
3139
+ return "ok";
3140
+ },
3141
+ async advance(name, key, seen, write) {
3142
+ named(name);
3143
+ const l = await load(key);
3144
+ const before = await seen();
3145
+ const m = l.state === "ok" ? l.manifest : await fresh(l.state !== "empty" || before > 0, l.manifest, key);
3146
+ const version = Math.max(m.versions[name] ?? m.base, before) + 1;
3147
+ if (!Number.isSafeInteger(version)) throw new VaultError("Storage version out of range.", "invalid-input");
3148
+ await write(version);
3149
+ await record(m, name, version, key);
3150
+ return version;
3151
+ }
3152
+ };
3153
+ return {
3154
+ exclusive: (fn) => queue(() => fn(ops)),
3155
+ condemn: (key) => queue(async () => {
3156
+ await fresh(true, (await load(key)).manifest, key);
3157
+ }),
3158
+ rekey: (oldKey, newKey) => queue(async () => {
3159
+ const l = await load(oldKey);
3160
+ if (l.state === "ok") await store({ ...l.manifest, epoch: l.manifest.epoch + 1 }, newKey);
3161
+ return l.state;
3162
+ }),
3163
+ read: (key) => queue(async () => {
3164
+ const l = await load(key);
3165
+ return { state: l.state, ...l.manifest ?? { epoch: 0, base: 0, sealed: false, versions: {} } };
3166
+ }),
3167
+ reset: () => queue(async () => {
3168
+ await options.manifest.set(null);
3169
+ await options.mark.set(null);
3170
+ })
3171
+ };
3172
+ }
2507
3173
  export {
2508
3174
  ACTIVE_STATUSES,
2509
3175
  APPROVALS_MAX_PENDING_PER_ORIGIN,
3176
+ APPROVALS_MAX_PENDING_PER_SITE,
3177
+ APPROVALS_MAX_PENDING_TOTAL,
2510
3178
  ApprovalQueue,
2511
3179
  BTC_CHAIN,
2512
3180
  BTC_ESPLORA,
@@ -2514,9 +3182,12 @@ export {
2514
3182
  DEFAULT_CHAIN,
2515
3183
  DEFAULT_NETWORKS,
2516
3184
  FLOOR_TOLERANCE_BPS,
3185
+ GETLOGS_MAX_SPAN_UNCONNECTED,
2517
3186
  KEYRING_STORAGE_KEY,
2518
3187
  LOOKALIKE_PREFIX,
2519
3188
  LOOKALIKE_SUFFIX,
3189
+ MAX_FEE_REASKS,
3190
+ MULTI_LABEL_SUFFIXES,
2520
3191
  NEAR_CHAIN,
2521
3192
  NEAR_PROVIDERS,
2522
3193
  NON_EVM_CHAINS,
@@ -2537,18 +3208,24 @@ export {
2537
3208
  RouterError,
2538
3209
  SESSION_DEFAULT_AUTOLOCK_MS,
2539
3210
  SIGNING_METHODS,
3211
+ SIWE_FORBIDDEN_CHARS,
2540
3212
  SOLANA_CHAIN,
2541
3213
  SOLANA_RPCS,
3214
+ STORAGE_MAC_FIELD,
3215
+ STORAGE_VERSION_FIELD,
2542
3216
  Session,
2543
3217
  SwapTracker,
2544
3218
  SwapsStore,
2545
- TONCONNECT_ERRORS,
3219
+ TONCONNECT_ERRORS2 as TONCONNECT_ERRORS,
2546
3220
  TONCONNECT_MAX_MESSAGES,
3221
+ TONCONNECT_NETWORKS,
3222
+ TONCONNECT_SIGN_DATA_MAX,
2547
3223
  TON_CHAIN,
2548
3224
  TON_ENDPOINTS,
2549
3225
  TRON_CHAIN,
2550
3226
  TRON_PROVIDERS,
2551
- TonConnectError,
3227
+ TokenBucket,
3228
+ TonConnectError2 as TonConnectError,
2552
3229
  VAULT_DEFAULT_ITERATIONS,
2553
3230
  VAULT_LIMITS,
2554
3231
  VAULT_MIN_ITERATIONS,
@@ -2562,6 +3239,7 @@ export {
2562
3239
  ZCASH_CHAIN,
2563
3240
  approvalChallenge,
2564
3241
  caip2FromChainId,
3242
+ canonicalJson,
2565
3243
  chainHex,
2566
3244
  chainTxMatcher,
2567
3245
  checkSenderOrigin,
@@ -2574,6 +3252,7 @@ export {
2574
3252
  createSignHandler,
2575
3253
  decodedMessage,
2576
3254
  derToRaw,
3255
+ deriveStorageMacKey,
2577
3256
  describeSiwe,
2578
3257
  earnApprovalPayload,
2579
3258
  earnPlanMatchers,
@@ -2581,6 +3260,7 @@ export {
2581
3260
  evmCallMatcher,
2582
3261
  executionFloor,
2583
3262
  fromBase64Url,
3263
+ gasLimitExcessive,
2584
3264
  hostOf,
2585
3265
  isCatalogChain,
2586
3266
  isEnvelopeV1,
@@ -2594,6 +3274,7 @@ export {
2594
3274
  isRelayToContent,
2595
3275
  levenshtein,
2596
3276
  lookalikeOf,
3277
+ macJsonStorage,
2597
3278
  memoryAlarms,
2598
3279
  memoryApprovalWindow,
2599
3280
  memoryJsonStorage,
@@ -2618,14 +3299,21 @@ export {
2618
3299
  rank,
2619
3300
  rawToDer,
2620
3301
  relayError,
3302
+ revealHidden,
2621
3303
  reviewEarnPlan,
2622
3304
  reviewSwapPlan,
3305
+ sameTonConnectAddress,
3306
+ siteOf,
3307
+ siweCrossSite,
2623
3308
  siweProblems,
2624
3309
  stampOrigin,
3310
+ storageVersionLedger,
2625
3311
  swapApprovalPayload,
2626
3312
  toBase64Url,
2627
3313
  toRelayError,
2628
3314
  tonConnectErrorCode,
3315
+ tonConnectFriendlyAddress,
3316
+ tonConnectRawAddress,
2629
3317
  tonProofMessage,
2630
3318
  tonSignDataMessage,
2631
3319
  typedDataMatcher,