@anywalletkit/extension-kit 0.3.6 → 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/README.md +28 -7
- package/dist/index.cjs +814 -54
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +467 -18
- package/dist/index.d.ts +467 -18
- package/dist/index.js +815 -55
- package/dist/index.js.map +1 -1
- package/package.json +7 -7
package/dist/index.cjs
CHANGED
|
@@ -22,6 +22,8 @@ var src_exports = {};
|
|
|
22
22
|
__export(src_exports, {
|
|
23
23
|
ACTIVE_STATUSES: () => ACTIVE_STATUSES,
|
|
24
24
|
APPROVALS_MAX_PENDING_PER_ORIGIN: () => APPROVALS_MAX_PENDING_PER_ORIGIN,
|
|
25
|
+
APPROVALS_MAX_PENDING_PER_SITE: () => APPROVALS_MAX_PENDING_PER_SITE,
|
|
26
|
+
APPROVALS_MAX_PENDING_TOTAL: () => APPROVALS_MAX_PENDING_TOTAL,
|
|
25
27
|
ApprovalQueue: () => ApprovalQueue,
|
|
26
28
|
BTC_CHAIN: () => BTC_CHAIN,
|
|
27
29
|
BTC_ESPLORA: () => BTC_ESPLORA,
|
|
@@ -29,9 +31,12 @@ __export(src_exports, {
|
|
|
29
31
|
DEFAULT_CHAIN: () => DEFAULT_CHAIN,
|
|
30
32
|
DEFAULT_NETWORKS: () => DEFAULT_NETWORKS,
|
|
31
33
|
FLOOR_TOLERANCE_BPS: () => FLOOR_TOLERANCE_BPS,
|
|
34
|
+
GETLOGS_MAX_SPAN_UNCONNECTED: () => GETLOGS_MAX_SPAN_UNCONNECTED,
|
|
32
35
|
KEYRING_STORAGE_KEY: () => KEYRING_STORAGE_KEY,
|
|
33
36
|
LOOKALIKE_PREFIX: () => LOOKALIKE_PREFIX,
|
|
34
37
|
LOOKALIKE_SUFFIX: () => LOOKALIKE_SUFFIX,
|
|
38
|
+
MAX_FEE_REASKS: () => MAX_FEE_REASKS,
|
|
39
|
+
MULTI_LABEL_SUFFIXES: () => MULTI_LABEL_SUFFIXES,
|
|
35
40
|
NEAR_CHAIN: () => NEAR_CHAIN,
|
|
36
41
|
NEAR_PROVIDERS: () => NEAR_PROVIDERS,
|
|
37
42
|
NON_EVM_CHAINS: () => NON_EVM_CHAINS,
|
|
@@ -52,8 +57,11 @@ __export(src_exports, {
|
|
|
52
57
|
RouterError: () => RouterError,
|
|
53
58
|
SESSION_DEFAULT_AUTOLOCK_MS: () => SESSION_DEFAULT_AUTOLOCK_MS,
|
|
54
59
|
SIGNING_METHODS: () => SIGNING_METHODS,
|
|
60
|
+
SIWE_FORBIDDEN_CHARS: () => SIWE_FORBIDDEN_CHARS,
|
|
55
61
|
SOLANA_CHAIN: () => SOLANA_CHAIN,
|
|
56
62
|
SOLANA_RPCS: () => SOLANA_RPCS,
|
|
63
|
+
STORAGE_MAC_FIELD: () => STORAGE_MAC_FIELD,
|
|
64
|
+
STORAGE_VERSION_FIELD: () => STORAGE_VERSION_FIELD,
|
|
57
65
|
Session: () => Session,
|
|
58
66
|
SwapTracker: () => SwapTracker,
|
|
59
67
|
SwapsStore: () => SwapsStore,
|
|
@@ -65,6 +73,7 @@ __export(src_exports, {
|
|
|
65
73
|
TON_ENDPOINTS: () => TON_ENDPOINTS,
|
|
66
74
|
TRON_CHAIN: () => TRON_CHAIN,
|
|
67
75
|
TRON_PROVIDERS: () => TRON_PROVIDERS,
|
|
76
|
+
TokenBucket: () => TokenBucket,
|
|
68
77
|
TonConnectError: () => import_tonconnect2.TonConnectError,
|
|
69
78
|
VAULT_DEFAULT_ITERATIONS: () => VAULT_DEFAULT_ITERATIONS,
|
|
70
79
|
VAULT_LIMITS: () => VAULT_LIMITS,
|
|
@@ -79,6 +88,7 @@ __export(src_exports, {
|
|
|
79
88
|
ZCASH_CHAIN: () => ZCASH_CHAIN,
|
|
80
89
|
approvalChallenge: () => approvalChallenge,
|
|
81
90
|
caip2FromChainId: () => caip2FromChainId,
|
|
91
|
+
canonicalJson: () => canonicalJson,
|
|
82
92
|
chainHex: () => chainHex,
|
|
83
93
|
chainTxMatcher: () => chainTxMatcher,
|
|
84
94
|
checkSenderOrigin: () => checkSenderOrigin,
|
|
@@ -91,6 +101,7 @@ __export(src_exports, {
|
|
|
91
101
|
createSignHandler: () => createSignHandler,
|
|
92
102
|
decodedMessage: () => decodedMessage,
|
|
93
103
|
derToRaw: () => derToRaw,
|
|
104
|
+
deriveStorageMacKey: () => deriveStorageMacKey,
|
|
94
105
|
describeSiwe: () => describeSiwe,
|
|
95
106
|
earnApprovalPayload: () => earnApprovalPayload,
|
|
96
107
|
earnPlanMatchers: () => earnPlanMatchers,
|
|
@@ -98,6 +109,7 @@ __export(src_exports, {
|
|
|
98
109
|
evmCallMatcher: () => evmCallMatcher,
|
|
99
110
|
executionFloor: () => executionFloor,
|
|
100
111
|
fromBase64Url: () => fromBase64Url,
|
|
112
|
+
gasLimitExcessive: () => gasLimitExcessive,
|
|
101
113
|
hostOf: () => hostOf,
|
|
102
114
|
isCatalogChain: () => isCatalogChain,
|
|
103
115
|
isEnvelopeV1: () => isEnvelopeV1,
|
|
@@ -111,6 +123,7 @@ __export(src_exports, {
|
|
|
111
123
|
isRelayToContent: () => isRelayToContent,
|
|
112
124
|
levenshtein: () => levenshtein,
|
|
113
125
|
lookalikeOf: () => lookalikeOf,
|
|
126
|
+
macJsonStorage: () => macJsonStorage,
|
|
114
127
|
memoryAlarms: () => memoryAlarms,
|
|
115
128
|
memoryApprovalWindow: () => memoryApprovalWindow,
|
|
116
129
|
memoryJsonStorage: () => memoryJsonStorage,
|
|
@@ -135,11 +148,15 @@ __export(src_exports, {
|
|
|
135
148
|
rank: () => rank,
|
|
136
149
|
rawToDer: () => rawToDer,
|
|
137
150
|
relayError: () => relayError,
|
|
151
|
+
revealHidden: () => revealHidden,
|
|
138
152
|
reviewEarnPlan: () => reviewEarnPlan,
|
|
139
153
|
reviewSwapPlan: () => reviewSwapPlan,
|
|
140
154
|
sameTonConnectAddress: () => import_tonconnect2.sameTonConnectAddress,
|
|
155
|
+
siteOf: () => siteOf,
|
|
156
|
+
siweCrossSite: () => siweCrossSite,
|
|
141
157
|
siweProblems: () => siweProblems,
|
|
142
158
|
stampOrigin: () => stampOrigin,
|
|
159
|
+
storageVersionLedger: () => storageVersionLedger,
|
|
143
160
|
swapApprovalPayload: () => swapApprovalPayload,
|
|
144
161
|
toBase64Url: () => toBase64Url,
|
|
145
162
|
toRelayError: () => toRelayError,
|
|
@@ -291,7 +308,7 @@ var Vault = class {
|
|
|
291
308
|
if (isEnvelopeV1(envelope)) {
|
|
292
309
|
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");
|
|
293
310
|
const kek2 = await this._derivePbkdf2(password, fromBase64(envelope.kdf.salt), envelope.kdf.iterations);
|
|
294
|
-
const dek = await this._open(kek2, envelope.dek, aadDekV1(envelope.kdf), "wrong-password");
|
|
311
|
+
const dek = await this._proveOwn(await this._open(kek2, envelope.dek, aadDekV1(envelope.kdf), "wrong-password"), envelope);
|
|
295
312
|
if (this._kdf === "scrypt") {
|
|
296
313
|
const next = await this._sealEnvelope(password, dek, envelope.secrets, [], envelope.createdAt);
|
|
297
314
|
await this._write(isEnvelopeV2(next) ? { ...next, upgradedAt: Date.now() } : next);
|
|
@@ -300,7 +317,24 @@ var Vault = class {
|
|
|
300
317
|
}
|
|
301
318
|
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");
|
|
302
319
|
const kek = await this._deriveScrypt(password, fromBase64(envelope.kdf.salt), envelope.kdf);
|
|
303
|
-
return this._open(kek, envelope.dek, aadDekV2(envelope.kdf), "wrong-password");
|
|
320
|
+
return this._proveOwn(await this._open(kek, envelope.dek, aadDekV2(envelope.kdf), "wrong-password"), envelope);
|
|
321
|
+
}
|
|
322
|
+
/**
|
|
323
|
+
* A DEK unwrapped from a dek / passkey box must be the key this vault's secrets are sealed under
|
|
324
|
+
* (audit 2026-11-09 E21): a box spliced in from another vault, under another password or passkey,
|
|
325
|
+
* opens with that password but holds a foreign key. Proven with the DEK check, or — in an envelope
|
|
326
|
+
* written before it — the first secret; an old empty envelope has nothing to prove it with. Returns
|
|
327
|
+
* `dek`; on a mismatch zeroizes it and fails as `wrong-password` (the same failure as tampering).
|
|
328
|
+
*/
|
|
329
|
+
async _proveOwn(dek, envelope) {
|
|
330
|
+
if (envelope.dekCheck === void 0 && Object.keys(envelope.secrets).length === 0) return dek;
|
|
331
|
+
try {
|
|
332
|
+
await this._requireDek(dek, envelope);
|
|
333
|
+
return dek;
|
|
334
|
+
} catch {
|
|
335
|
+
(0, import_core.zeroize)(dek);
|
|
336
|
+
throw new VaultError("Wrong password.", "wrong-password");
|
|
337
|
+
}
|
|
304
338
|
}
|
|
305
339
|
/**
|
|
306
340
|
* Re-wrap the DEK under a new password (fresh salt, current KDF). Secrets and passkeys are untouched —
|
|
@@ -332,9 +366,13 @@ var Vault = class {
|
|
|
332
366
|
*
|
|
333
367
|
* Passkeys are dropped — their boxes hold the old DEK and cannot be re-wrapped without their PRF
|
|
334
368
|
* output — and must be registered again ({@link Vault.addPasskey} with the new DEK).
|
|
369
|
+
*
|
|
370
|
+
* With `newPassword` this is also the password change: the new DEK is wrapped under `newPassword`
|
|
371
|
+
* (at least {@link VAULT_MIN_PASSWORD_LENGTH} characters), and `password` must be the current one.
|
|
335
372
|
*/
|
|
336
|
-
async rotateVaultKey(password) {
|
|
373
|
+
async rotateVaultKey(password, newPassword) {
|
|
337
374
|
requirePassword(password);
|
|
375
|
+
if (newPassword !== void 0) requirePassword(newPassword, VAULT_MIN_PASSWORD_LENGTH);
|
|
338
376
|
return this._exclusive(async () => {
|
|
339
377
|
const oldDek = await this._unlock(password);
|
|
340
378
|
const dek = this._random(DEK_BYTES);
|
|
@@ -350,7 +388,7 @@ var Vault = class {
|
|
|
350
388
|
(0, import_core.zeroize)(plaintext);
|
|
351
389
|
}
|
|
352
390
|
}
|
|
353
|
-
const next = await this._sealEnvelope(password, dek, secrets, [], envelope.createdAt);
|
|
391
|
+
const next = await this._sealEnvelope(newPassword ?? password, dek, secrets, [], envelope.createdAt);
|
|
354
392
|
const upgradedAt = isEnvelopeV2(envelope) ? envelope.upgradedAt : void 0;
|
|
355
393
|
await this._write(isEnvelopeV2(next) ? { ...next, ...upgradedAt !== void 0 ? { upgradedAt } : {}, rotatedAt: Date.now() } : next);
|
|
356
394
|
return dek;
|
|
@@ -414,7 +452,7 @@ var Vault = class {
|
|
|
414
452
|
const entry = envelope.passkeys.find((p) => p.id === id);
|
|
415
453
|
if (!entry) throw new VaultError("No such passkey on this vault.", "missing");
|
|
416
454
|
const key = await this._derivePasskeyKek(prf, id);
|
|
417
|
-
return await this._open(key, entry.dek, aadPasskey(id), "wrong-password");
|
|
455
|
+
return await this._proveOwn(await this._open(key, entry.dek, aadPasskey(id), "wrong-password"), envelope);
|
|
418
456
|
} finally {
|
|
419
457
|
(0, import_core.zeroize)(prf);
|
|
420
458
|
}
|
|
@@ -709,7 +747,11 @@ var Session = class {
|
|
|
709
747
|
return fromBase64(record.key);
|
|
710
748
|
});
|
|
711
749
|
}
|
|
712
|
-
/**
|
|
750
|
+
/**
|
|
751
|
+
* User activity: push the auto-lock out again. No-op when locked. Call it for explicit user
|
|
752
|
+
* gestures only (unlock, send, approve / decline, an action in the wallet's own UI) — not for
|
|
753
|
+
* passive polls, page requests or worker restarts, or the timer never runs out.
|
|
754
|
+
*/
|
|
713
755
|
async touch() {
|
|
714
756
|
return this._serial(async () => {
|
|
715
757
|
const epoch = this._epoch;
|
|
@@ -723,11 +765,25 @@ var Session = class {
|
|
|
723
765
|
get autoLockMs() {
|
|
724
766
|
return this._autoLockMs;
|
|
725
767
|
}
|
|
726
|
-
/**
|
|
768
|
+
/**
|
|
769
|
+
* Set the timer period (0 disables). Never pushes the deadline out — call this at every worker
|
|
770
|
+
* start with the stored setting: it re-arms the alarm at the stored deadline (a restart must not
|
|
771
|
+
* count as activity, audit 2026-11-09 E11), and only brings the deadline forward when the new
|
|
772
|
+
* period ends sooner. A longer period (or 0) applies from the next {@link Session.touch} /
|
|
773
|
+
* {@link Session.unlock}; call `touch()` right after when the user changed it explicitly.
|
|
774
|
+
*/
|
|
727
775
|
async setAutoLockMs(ms) {
|
|
728
776
|
if (!Number.isFinite(ms) || ms < 0) throw new VaultError("Auto-lock must be a non-negative number of milliseconds.", "invalid-input");
|
|
729
777
|
this._autoLockMs = ms;
|
|
730
|
-
|
|
778
|
+
return this._serial(async () => {
|
|
779
|
+
const epoch = this._epoch;
|
|
780
|
+
const record = await this._read();
|
|
781
|
+
if (!record || epoch !== this._epoch) return;
|
|
782
|
+
const cap = this._expiry();
|
|
783
|
+
const expiresAt = cap !== null && (record.expiresAt === null || cap < record.expiresAt) ? cap : record.expiresAt;
|
|
784
|
+
if (expiresAt !== record.expiresAt) await this._storage.set(this._key, { ...record, expiresAt });
|
|
785
|
+
await this._arm(expiresAt);
|
|
786
|
+
});
|
|
731
787
|
}
|
|
732
788
|
onLock(callback) {
|
|
733
789
|
this._onLock.add(callback);
|
|
@@ -997,7 +1053,8 @@ var DEFAULT_NETWORKS = Object.freeze([
|
|
|
997
1053
|
net(4326, "MegaETH", "ETH", ["https://mainnet.megaeth.com/rpc"]),
|
|
998
1054
|
net(130, "Unichain", "ETH", ["https://mainnet.unichain.org", "https://unichain-rpc.publicnode.com"], "https://uniscan.xyz"),
|
|
999
1055
|
net(143, "Monad", "MON", ["https://rpc.monad.xyz"]),
|
|
1000
|
-
|
|
1056
|
+
// Second operator (Tenderly's public gateway, keyless): rpc.plasma.to alone cannot satisfy a two-operator check.
|
|
1057
|
+
net(9745, "Plasma", "XPL", ["https://rpc.plasma.to", "https://plasma.gateway.tenderly.co"]),
|
|
1001
1058
|
net(59144, "Linea", "ETH", ["https://rpc.linea.build", "https://linea-rpc.publicnode.com"], "https://lineascan.build"),
|
|
1002
1059
|
net(100, "Gnosis", "xDAI", ["https://rpc.gnosischain.com", "https://gnosis-rpc.publicnode.com"], "https://gnosisscan.io"),
|
|
1003
1060
|
net(146, "Sonic", "S", ["https://rpc.soniclabs.com", "https://sonic-rpc.publicnode.com"], "https://sonicscan.org"),
|
|
@@ -1015,7 +1072,11 @@ var DEFAULT_NETWORKS = Object.freeze([
|
|
|
1015
1072
|
net(5e3, "Mantle", "MNT", ["https://rpc.mantle.xyz", "https://mantle-rpc.publicnode.com"], "https://mantlescan.xyz"),
|
|
1016
1073
|
net(747474, "Katana", "ETH", ["https://rpc.katana.network"], "https://explorer.katanarpc.com"),
|
|
1017
1074
|
net(2818, "Morph", "ETH", ["https://rpc.morphl2.io", "https://rpc-quicknode.morphl2.io"], "https://explorer.morphl2.io"),
|
|
1018
|
-
net(14, "Flare", "FLR", ["https://flare-api.flare.network/ext/C/rpc", "https://flare.rpc.thirdweb.com"], "https://flare-explorer.flare.network")
|
|
1075
|
+
net(14, "Flare", "FLR", ["https://flare-api.flare.network/ext/C/rpc", "https://flare.rpc.thirdweb.com"], "https://flare-explorer.flare.network"),
|
|
1076
|
+
// Elysium by Kinetiq (chain 99901, HYPE gas; Arbitrum Orbit on HyperEVM) — public RPC and explorer checked 2026-11-10.
|
|
1077
|
+
// The explorer value is the link base (<root>/tx/<hash>, <root>/address/<a>), not LI.FI's blockExplorerUrls
|
|
1078
|
+
// https://elysium.kinetiq.xyz/explorer/ (the overview page; the bare root is the Elysium site home).
|
|
1079
|
+
net(99901, "Elysium", "HYPE", ["https://rpc.elysium.kinetiq.xyz"], "https://elysium.kinetiq.xyz")
|
|
1019
1080
|
]);
|
|
1020
1081
|
var chainHex = (caip2) => `0x${Number(caip2.split(":")[1]).toString(16)}`;
|
|
1021
1082
|
function caip2FromChainId(value) {
|
|
@@ -1091,6 +1152,17 @@ var NetworksTable = class {
|
|
|
1091
1152
|
);
|
|
1092
1153
|
return this._loading;
|
|
1093
1154
|
}
|
|
1155
|
+
/**
|
|
1156
|
+
* Forget the cached table: the next read asks storage again. For a storage whose answer changes under
|
|
1157
|
+
* the table — e.g. one that only hands out the user's networks once the wallet is unlocked.
|
|
1158
|
+
*/
|
|
1159
|
+
reload() {
|
|
1160
|
+
return this.serialize(async () => {
|
|
1161
|
+
await this._loading?.catch(() => void 0);
|
|
1162
|
+
this._custom = null;
|
|
1163
|
+
this._loading = null;
|
|
1164
|
+
});
|
|
1165
|
+
}
|
|
1094
1166
|
};
|
|
1095
1167
|
var isSiteRpcUrl = (u) => typeof u === "string" && u.length <= 2048 && (0, import_url.isPublicHttpsUrl)(u);
|
|
1096
1168
|
function parseAddChainParams(params, known = isCatalogChain) {
|
|
@@ -1204,6 +1276,21 @@ var PermissionStore = class {
|
|
|
1204
1276
|
return permission;
|
|
1205
1277
|
});
|
|
1206
1278
|
}
|
|
1279
|
+
/**
|
|
1280
|
+
* Add EVM accounts to an origin's grant (per-site grants): the new ones first, the ones it already
|
|
1281
|
+
* holds stay. The chain stays the origin's own when it has one. Used by the router's connect; `connect`
|
|
1282
|
+
* replaces the list instead.
|
|
1283
|
+
*/
|
|
1284
|
+
grant(origin, accounts, chain) {
|
|
1285
|
+
return this.serialize(async () => {
|
|
1286
|
+
if (accounts.length === 0) throw new Error("Connect at least one account.");
|
|
1287
|
+
const current = await this.get(origin);
|
|
1288
|
+
const merged = [...accounts, ...(current?.accounts ?? []).filter((a) => !accounts.some((x) => x.toLowerCase() === a.toLowerCase()))];
|
|
1289
|
+
const permission = { ...current ?? {}, origin, accounts: merged, chain: current?.chain ?? chain, connectedAt: current?.connectedAt ?? Date.now() };
|
|
1290
|
+
await this.write(origin, permission);
|
|
1291
|
+
return permission;
|
|
1292
|
+
});
|
|
1293
|
+
}
|
|
1207
1294
|
/** Connect another chain's accounts to the same origin (one "connected sites" list). */
|
|
1208
1295
|
connectChain(origin, key, accounts, defaultChain) {
|
|
1209
1296
|
return this.serialize(async () => {
|
|
@@ -1236,6 +1323,11 @@ var PermissionStore = class {
|
|
|
1236
1323
|
});
|
|
1237
1324
|
}
|
|
1238
1325
|
/**
|
|
1326
|
+
* @deprecated Rewrites every origin's grant to the active account, which shows that account to sites
|
|
1327
|
+
* the user never connected it to (and links accounts across sites). Grants are per site now: the
|
|
1328
|
+
* router shows an origin its granted accounts that are also active, and connecting another account
|
|
1329
|
+
* adds it to that one origin ({@link grant}). Kept for compatibility; nothing in this package calls it.
|
|
1330
|
+
*
|
|
1239
1331
|
* The active EVM account changed: every origin holding an EVM grant now sees `accounts` (MetaMask behaviour).
|
|
1240
1332
|
* An origin connected only on another chain (`accounts: []` — Solana/TON/Tron/BTC/NEAR providers, a WalletConnect
|
|
1241
1333
|
* session without eip155) is skipped: it never passed an EVM connect and must not learn the EVM address (scan F6/F8).
|
|
@@ -1281,6 +1373,179 @@ var PermissionStore = class {
|
|
|
1281
1373
|
};
|
|
1282
1374
|
var sameList = (a, b) => a.length === b.length && a.every((x, i) => x === b[i]);
|
|
1283
1375
|
|
|
1376
|
+
// src/site.ts
|
|
1377
|
+
var MULTI_LABEL_SUFFIXES = /* @__PURE__ */ new Set([
|
|
1378
|
+
// country second-level registries
|
|
1379
|
+
"co.uk",
|
|
1380
|
+
"org.uk",
|
|
1381
|
+
"me.uk",
|
|
1382
|
+
"ltd.uk",
|
|
1383
|
+
"plc.uk",
|
|
1384
|
+
"net.uk",
|
|
1385
|
+
"ac.uk",
|
|
1386
|
+
"gov.uk",
|
|
1387
|
+
"com.au",
|
|
1388
|
+
"net.au",
|
|
1389
|
+
"org.au",
|
|
1390
|
+
"edu.au",
|
|
1391
|
+
"gov.au",
|
|
1392
|
+
"id.au",
|
|
1393
|
+
"co.nz",
|
|
1394
|
+
"org.nz",
|
|
1395
|
+
"net.nz",
|
|
1396
|
+
"co.jp",
|
|
1397
|
+
"ne.jp",
|
|
1398
|
+
"or.jp",
|
|
1399
|
+
"ac.jp",
|
|
1400
|
+
"go.jp",
|
|
1401
|
+
"co.kr",
|
|
1402
|
+
"or.kr",
|
|
1403
|
+
"ne.kr",
|
|
1404
|
+
"com.br",
|
|
1405
|
+
"net.br",
|
|
1406
|
+
"org.br",
|
|
1407
|
+
"com.cn",
|
|
1408
|
+
"net.cn",
|
|
1409
|
+
"org.cn",
|
|
1410
|
+
"com.hk",
|
|
1411
|
+
"com.tw",
|
|
1412
|
+
"com.sg",
|
|
1413
|
+
"com.my",
|
|
1414
|
+
"co.in",
|
|
1415
|
+
"net.in",
|
|
1416
|
+
"org.in",
|
|
1417
|
+
"co.id",
|
|
1418
|
+
"co.il",
|
|
1419
|
+
"co.th",
|
|
1420
|
+
"co.za",
|
|
1421
|
+
"com.mx",
|
|
1422
|
+
"com.ar",
|
|
1423
|
+
"com.co",
|
|
1424
|
+
"com.tr",
|
|
1425
|
+
"com.ua",
|
|
1426
|
+
"com.pl",
|
|
1427
|
+
"com.ru",
|
|
1428
|
+
"com.vn",
|
|
1429
|
+
"com.ph",
|
|
1430
|
+
"com.pk",
|
|
1431
|
+
"com.ng",
|
|
1432
|
+
"com.eg",
|
|
1433
|
+
"com.sa",
|
|
1434
|
+
"co.ke",
|
|
1435
|
+
// hosting platforms: every customer gets its own subdomain
|
|
1436
|
+
"github.io",
|
|
1437
|
+
"gitlab.io",
|
|
1438
|
+
"vercel.app",
|
|
1439
|
+
"now.sh",
|
|
1440
|
+
"pages.dev",
|
|
1441
|
+
"workers.dev",
|
|
1442
|
+
"r2.dev",
|
|
1443
|
+
"netlify.app",
|
|
1444
|
+
"netlify.com",
|
|
1445
|
+
"herokuapp.com",
|
|
1446
|
+
"web.app",
|
|
1447
|
+
"firebaseapp.com",
|
|
1448
|
+
"appspot.com",
|
|
1449
|
+
"azurewebsites.net",
|
|
1450
|
+
"azurestaticapps.net",
|
|
1451
|
+
"cloudfront.net",
|
|
1452
|
+
"amplifyapp.com",
|
|
1453
|
+
"onrender.com",
|
|
1454
|
+
"fly.dev",
|
|
1455
|
+
"railway.app",
|
|
1456
|
+
"up.railway.app",
|
|
1457
|
+
"deno.dev",
|
|
1458
|
+
"glitch.me",
|
|
1459
|
+
"repl.co",
|
|
1460
|
+
"replit.app",
|
|
1461
|
+
"replit.dev",
|
|
1462
|
+
"surge.sh",
|
|
1463
|
+
"webflow.io",
|
|
1464
|
+
"framer.app",
|
|
1465
|
+
"framer.website",
|
|
1466
|
+
"wixsite.com",
|
|
1467
|
+
"blogspot.com",
|
|
1468
|
+
"myshopify.com",
|
|
1469
|
+
"notion.site",
|
|
1470
|
+
"carrd.co",
|
|
1471
|
+
"gitbook.io",
|
|
1472
|
+
"readthedocs.io",
|
|
1473
|
+
"ngrok.io",
|
|
1474
|
+
"ngrok-free.app",
|
|
1475
|
+
"loca.lt",
|
|
1476
|
+
"trycloudflare.com",
|
|
1477
|
+
// ENS / IPFS gateways: every name or CID is its own site
|
|
1478
|
+
"eth.limo",
|
|
1479
|
+
"eth.link",
|
|
1480
|
+
"ipfs.dweb.link",
|
|
1481
|
+
"ipns.dweb.link",
|
|
1482
|
+
"ipfs.w3s.link",
|
|
1483
|
+
"ipfs.cf-ipfs.com"
|
|
1484
|
+
]);
|
|
1485
|
+
var IPV4 = /^\d{1,3}(\.\d{1,3}){3}$/;
|
|
1486
|
+
function siteOf(origin) {
|
|
1487
|
+
let host;
|
|
1488
|
+
try {
|
|
1489
|
+
const url = new URL(origin);
|
|
1490
|
+
if (url.protocol !== "http:" && url.protocol !== "https:") return origin;
|
|
1491
|
+
host = url.hostname.toLowerCase().replace(/\.$/, "");
|
|
1492
|
+
} catch {
|
|
1493
|
+
return origin;
|
|
1494
|
+
}
|
|
1495
|
+
if (!host || host.startsWith("[") || host.includes(":") || IPV4.test(host)) return host || origin;
|
|
1496
|
+
const labels = host.split(".");
|
|
1497
|
+
if (labels.length <= 2) return host;
|
|
1498
|
+
for (const size of [3, 2]) {
|
|
1499
|
+
if (labels.length <= size) continue;
|
|
1500
|
+
if (MULTI_LABEL_SUFFIXES.has(labels.slice(-size).join("."))) return labels.slice(-(size + 1)).join(".");
|
|
1501
|
+
}
|
|
1502
|
+
return labels.slice(-2).join(".");
|
|
1503
|
+
}
|
|
1504
|
+
var TokenBucket = class {
|
|
1505
|
+
constructor(options) {
|
|
1506
|
+
this.options = options;
|
|
1507
|
+
}
|
|
1508
|
+
options;
|
|
1509
|
+
buckets = /* @__PURE__ */ new Map();
|
|
1510
|
+
take(key, cost = 1) {
|
|
1511
|
+
const now = (this.options.now ?? Date.now)();
|
|
1512
|
+
const b = this.refill(key, now);
|
|
1513
|
+
if (b.tokens < cost) return false;
|
|
1514
|
+
b.tokens -= cost;
|
|
1515
|
+
return true;
|
|
1516
|
+
}
|
|
1517
|
+
/** Tokens `key` holds right now (full for an unknown key). */
|
|
1518
|
+
available(key) {
|
|
1519
|
+
return this.refill(key, (this.options.now ?? Date.now)()).tokens;
|
|
1520
|
+
}
|
|
1521
|
+
refill(key, now) {
|
|
1522
|
+
let b = this.buckets.get(key);
|
|
1523
|
+
if (!b) {
|
|
1524
|
+
if (this.buckets.size >= (this.options.maxKeys ?? 2e3)) this.prune(now);
|
|
1525
|
+
b = { tokens: this.options.capacity, at: now };
|
|
1526
|
+
this.buckets.set(key, b);
|
|
1527
|
+
return b;
|
|
1528
|
+
}
|
|
1529
|
+
const elapsed = Math.max(0, now - b.at) / 1e3;
|
|
1530
|
+
b.tokens = Math.min(this.options.capacity, b.tokens + elapsed * this.options.refillPerSecond);
|
|
1531
|
+
b.at = now;
|
|
1532
|
+
return b;
|
|
1533
|
+
}
|
|
1534
|
+
/** Drop buckets that are full again (they carry no state); the oldest half when every one is in use. */
|
|
1535
|
+
prune(now) {
|
|
1536
|
+
for (const [k, b] of this.buckets) {
|
|
1537
|
+
if (b.tokens + Math.max(0, now - b.at) / 1e3 * this.options.refillPerSecond >= this.options.capacity) this.buckets.delete(k);
|
|
1538
|
+
}
|
|
1539
|
+
if (this.buckets.size >= (this.options.maxKeys ?? 2e3)) {
|
|
1540
|
+
let drop = Math.ceil(this.buckets.size / 2);
|
|
1541
|
+
for (const k of this.buckets.keys()) {
|
|
1542
|
+
if (drop-- <= 0) break;
|
|
1543
|
+
this.buckets.delete(k);
|
|
1544
|
+
}
|
|
1545
|
+
}
|
|
1546
|
+
}
|
|
1547
|
+
};
|
|
1548
|
+
|
|
1284
1549
|
// src/approvals.ts
|
|
1285
1550
|
var ReviewDeclined = class extends Error {
|
|
1286
1551
|
code = RELAY_ERRORS.userRejected;
|
|
@@ -1305,7 +1570,11 @@ var OriginRefused = class extends Error {
|
|
|
1305
1570
|
}
|
|
1306
1571
|
};
|
|
1307
1572
|
var APPROVALS_MAX_PENDING_PER_ORIGIN = 3;
|
|
1308
|
-
var
|
|
1573
|
+
var APPROVALS_MAX_PENDING_PER_SITE = 5;
|
|
1574
|
+
var APPROVALS_MAX_PENDING_TOTAL = 20;
|
|
1575
|
+
var FOLLOW_UP_CREDITS_MAX = 8;
|
|
1576
|
+
var FOLLOW_UP_KEYS_MAX = 256;
|
|
1577
|
+
var ApprovalQueue = class _ApprovalQueue {
|
|
1309
1578
|
constructor(window, newId = () => crypto.randomUUID(), options = {}) {
|
|
1310
1579
|
this.window = window;
|
|
1311
1580
|
this.newId = newId;
|
|
@@ -1313,6 +1582,8 @@ var ApprovalQueue = class {
|
|
|
1313
1582
|
window.onClosed((id) => {
|
|
1314
1583
|
if (this._current?.request.id === id) this.decline(id);
|
|
1315
1584
|
});
|
|
1585
|
+
const rate = options.openRate === false ? null : options.openRate ?? {};
|
|
1586
|
+
this._rate = rate ? new TokenBucket({ capacity: rate.burst ?? 10, refillPerSecond: rate.refillPerSecond ?? 1 / 6, ...options.now ? { now: options.now } : {} }) : null;
|
|
1316
1587
|
}
|
|
1317
1588
|
window;
|
|
1318
1589
|
newId;
|
|
@@ -1321,8 +1592,62 @@ var ApprovalQueue = class {
|
|
|
1321
1592
|
_waiting = [];
|
|
1322
1593
|
_listeners = /* @__PURE__ */ new Set();
|
|
1323
1594
|
_via = /* @__PURE__ */ new Map();
|
|
1595
|
+
_rate;
|
|
1596
|
+
/** Per site: cooldown end and strikes (how many times the bucket ran dry recently). */
|
|
1597
|
+
_cooldown = /* @__PURE__ */ new Map();
|
|
1598
|
+
/** Per origin + kind + chain: approvals that have not been followed up (see {@link ApprovalRequestOptions.followUp}). */
|
|
1599
|
+
_approved = /* @__PURE__ */ new Map();
|
|
1600
|
+
site(origin) {
|
|
1601
|
+
try {
|
|
1602
|
+
return (this.options.siteOf ?? siteOf)(origin);
|
|
1603
|
+
} catch {
|
|
1604
|
+
return origin;
|
|
1605
|
+
}
|
|
1606
|
+
}
|
|
1607
|
+
get all() {
|
|
1608
|
+
return [...this._current ? [this._current] : [], ...this._waiting];
|
|
1609
|
+
}
|
|
1610
|
+
/** The per-site rate: a reason to refuse, or null (and one token spent). */
|
|
1611
|
+
rateRefusal(site) {
|
|
1612
|
+
if (!this._rate) return null;
|
|
1613
|
+
const rate = this.options.openRate || {};
|
|
1614
|
+
const now = (this.options.now ?? Date.now)();
|
|
1615
|
+
const max = rate.maxBackoffMs ?? 6e5;
|
|
1616
|
+
const c = this._cooldown.get(site);
|
|
1617
|
+
if (c && now < c.until) return `This site opened too many requests; try again in ${Math.ceil((c.until - now) / 1e3)} s.`;
|
|
1618
|
+
if (this._rate.take(site)) return null;
|
|
1619
|
+
const strikes = c && now - c.last < max ? c.strikes + 1 : 1;
|
|
1620
|
+
const wait = Math.min(max, (rate.backoffMs ?? 3e4) * 2 ** (strikes - 1));
|
|
1621
|
+
this._cooldown.set(site, { until: now + wait, strikes, last: now });
|
|
1622
|
+
if (this._cooldown.size > 1e3) {
|
|
1623
|
+
for (const [k, v] of this._cooldown) if (now - v.last >= max) this._cooldown.delete(k);
|
|
1624
|
+
}
|
|
1625
|
+
return `This site opened too many requests; try again in ${Math.ceil(wait / 1e3)} s.`;
|
|
1626
|
+
}
|
|
1627
|
+
static followUpKey(r) {
|
|
1628
|
+
return `${r.origin}
|
|
1629
|
+
${r.kind}
|
|
1630
|
+
${r.chain}`;
|
|
1631
|
+
}
|
|
1632
|
+
/** The user approved `r`: it may be followed up once. */
|
|
1633
|
+
approved(r) {
|
|
1634
|
+
const key = _ApprovalQueue.followUpKey(r);
|
|
1635
|
+
const n = Math.min((this._approved.get(key) ?? 0) + 1, FOLLOW_UP_CREDITS_MAX);
|
|
1636
|
+
this._approved.delete(key);
|
|
1637
|
+
this._approved.set(key, n);
|
|
1638
|
+
if (this._approved.size > FOLLOW_UP_KEYS_MAX) this._approved.delete(this._approved.keys().next().value);
|
|
1639
|
+
}
|
|
1640
|
+
/** Spends one approval of the same origin, kind and chain; false when there is none. */
|
|
1641
|
+
spendApproval(r) {
|
|
1642
|
+
const key = _ApprovalQueue.followUpKey(r);
|
|
1643
|
+
const n = this._approved.get(key) ?? 0;
|
|
1644
|
+
if (n <= 0) return false;
|
|
1645
|
+
if (n === 1) this._approved.delete(key);
|
|
1646
|
+
else this._approved.set(key, n - 1);
|
|
1647
|
+
return true;
|
|
1648
|
+
}
|
|
1324
1649
|
/** Enqueue and wait for the user's answer. Refuses (never queues) past the per-origin limits. */
|
|
1325
|
-
request(input) {
|
|
1650
|
+
request(input, opts = {}) {
|
|
1326
1651
|
let refused;
|
|
1327
1652
|
try {
|
|
1328
1653
|
refused = this.options.refuse?.(input.origin);
|
|
@@ -1330,19 +1655,41 @@ var ApprovalQueue = class {
|
|
|
1330
1655
|
refused = `The wallet could not check this site (${e instanceof Error ? e.message : String(e)}).`;
|
|
1331
1656
|
}
|
|
1332
1657
|
if (refused) return Promise.reject(new OriginRefused(refused));
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1658
|
+
let exempt = false;
|
|
1659
|
+
try {
|
|
1660
|
+
exempt = this.options.exempt?.(input.origin) === true;
|
|
1661
|
+
} catch {
|
|
1662
|
+
exempt = false;
|
|
1336
1663
|
}
|
|
1337
|
-
const
|
|
1338
|
-
if (
|
|
1339
|
-
|
|
1664
|
+
const followUp = opts.followUp === true && this.spendApproval(input);
|
|
1665
|
+
if (!exempt && !followUp) {
|
|
1666
|
+
const all = this.all;
|
|
1667
|
+
const sameOrigin = all.filter((p) => p.request.origin === input.origin);
|
|
1668
|
+
if (input.kind === "connect" && sameOrigin.some((p) => p.request.kind === "connect")) {
|
|
1669
|
+
return Promise.reject(new QueueLimitError(RELAY_ERRORS.resourceUnavailable, "A connect request from this site is already waiting for your answer."));
|
|
1670
|
+
}
|
|
1671
|
+
const max = this.options.maxPendingPerOrigin ?? APPROVALS_MAX_PENDING_PER_ORIGIN;
|
|
1672
|
+
if (sameOrigin.length >= max) {
|
|
1673
|
+
return Promise.reject(new QueueLimitError(RELAY_ERRORS.limitExceeded, `This site already has ${max} requests waiting for you; answer or close them first.`));
|
|
1674
|
+
}
|
|
1675
|
+
const site = this.site(input.origin);
|
|
1676
|
+
const maxSite = this.options.maxPendingPerSite ?? APPROVALS_MAX_PENDING_PER_SITE;
|
|
1677
|
+
if (all.filter((p) => this.site(p.request.origin) === site).length >= maxSite) {
|
|
1678
|
+
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.`));
|
|
1679
|
+
}
|
|
1680
|
+
const maxTotal = this.options.maxPendingTotal ?? APPROVALS_MAX_PENDING_TOTAL;
|
|
1681
|
+
if (all.filter((p) => !this.isExempt(p.request.origin)).length >= maxTotal) {
|
|
1682
|
+
return Promise.reject(new QueueLimitError(RELAY_ERRORS.limitExceeded, `The wallet already has ${maxTotal} requests waiting for you; answer or close them first.`));
|
|
1683
|
+
}
|
|
1684
|
+
const slow = this.rateRefusal(site);
|
|
1685
|
+
if (slow) return Promise.reject(new QueueLimitError(RELAY_ERRORS.limitExceeded, slow));
|
|
1340
1686
|
}
|
|
1341
1687
|
return new Promise((resolve, reject) => {
|
|
1342
1688
|
const warnings = this.options.annotate?.(input.origin) ?? [];
|
|
1343
1689
|
const via = this._via.get(input.origin);
|
|
1344
1690
|
const pending = { request: { ...input, id: this.newId(), createdAt: Date.now(), ...warnings.length ? { originWarnings: warnings } : {}, ...via ? { via } : {} }, resolve, reject };
|
|
1345
|
-
this._waiting.
|
|
1691
|
+
if (followUp) this._waiting.unshift(pending);
|
|
1692
|
+
else this._waiting.push(pending);
|
|
1346
1693
|
void this.next();
|
|
1347
1694
|
});
|
|
1348
1695
|
}
|
|
@@ -1355,9 +1702,37 @@ var ApprovalQueue = class {
|
|
|
1355
1702
|
viaFor(origin) {
|
|
1356
1703
|
return this._via.get(origin);
|
|
1357
1704
|
}
|
|
1705
|
+
isExempt(origin) {
|
|
1706
|
+
try {
|
|
1707
|
+
return this.options.exempt?.(origin) === true;
|
|
1708
|
+
} catch {
|
|
1709
|
+
return false;
|
|
1710
|
+
}
|
|
1711
|
+
}
|
|
1358
1712
|
/** Requests waiting for `origin` (current + queued). */
|
|
1359
1713
|
pendingFor(origin) {
|
|
1360
|
-
return
|
|
1714
|
+
return this.all.filter((p) => p.request.origin === origin).length;
|
|
1715
|
+
}
|
|
1716
|
+
/** Requests waiting for any origin of `site` (current + queued). */
|
|
1717
|
+
pendingForSite(site) {
|
|
1718
|
+
return this.all.filter((p) => this.site(p.request.origin) === site).length;
|
|
1719
|
+
}
|
|
1720
|
+
/** The site (registrable domain) the queue's limits put `origin` under. */
|
|
1721
|
+
siteFor(origin) {
|
|
1722
|
+
return this.site(origin);
|
|
1723
|
+
}
|
|
1724
|
+
/** Decline everything waiting for `origin` (its last page closed). Returns how many requests were declined. */
|
|
1725
|
+
declineOrigin(origin, error = new ReviewDeclined("The page was closed \u2014 nothing was signed.")) {
|
|
1726
|
+
return this.declineWhere((p) => p.request.origin === origin, error);
|
|
1727
|
+
}
|
|
1728
|
+
/** "Decline all from this site": everything waiting for any origin of `site` (see {@link siteFor}). */
|
|
1729
|
+
declineSite(site, error = new ReviewDeclined()) {
|
|
1730
|
+
return this.declineWhere((p) => this.site(p.request.origin) === site, error);
|
|
1731
|
+
}
|
|
1732
|
+
declineWhere(match, error) {
|
|
1733
|
+
const ids = [...this._waiting.filter(match), ...this._current && match(this._current) ? [this._current] : []].map((p) => p.request.id);
|
|
1734
|
+
for (const id of ids) this.decline(id, error);
|
|
1735
|
+
return ids.length;
|
|
1361
1736
|
}
|
|
1362
1737
|
/** The request currently shown (the window asks for it by id on load). */
|
|
1363
1738
|
get current() {
|
|
@@ -1367,7 +1742,10 @@ var ApprovalQueue = class {
|
|
|
1367
1742
|
return this._waiting.length;
|
|
1368
1743
|
}
|
|
1369
1744
|
resolve(requestId, value) {
|
|
1370
|
-
return this.settle(requestId, (p) =>
|
|
1745
|
+
return this.settle(requestId, (p) => {
|
|
1746
|
+
this.approved(p.request);
|
|
1747
|
+
p.resolve(value);
|
|
1748
|
+
});
|
|
1371
1749
|
}
|
|
1372
1750
|
decline(requestId, error = new ReviewDeclined()) {
|
|
1373
1751
|
return this.settle(requestId, (p) => p.reject(error));
|
|
@@ -1375,6 +1753,7 @@ var ApprovalQueue = class {
|
|
|
1375
1753
|
/** Lock, disconnect, shutdown: everything pending is declined. */
|
|
1376
1754
|
declineAll(error = new ReviewDeclined("The wallet was locked.")) {
|
|
1377
1755
|
const all = [...this._current ? [this._current] : [], ...this._waiting.splice(0)];
|
|
1756
|
+
this._approved.clear();
|
|
1378
1757
|
for (const p of all) p.reject(error);
|
|
1379
1758
|
if (this._current) void this.window.close(this._current.request.id);
|
|
1380
1759
|
this._current = null;
|
|
@@ -1506,6 +1885,7 @@ var READ_METHODS = /* @__PURE__ */ new Set([
|
|
|
1506
1885
|
"eth_getTransactionReceipt",
|
|
1507
1886
|
"eth_maxPriorityFeePerGas"
|
|
1508
1887
|
]);
|
|
1888
|
+
var GETLOGS_MAX_SPAN_UNCONNECTED = 1e3;
|
|
1509
1889
|
var SIGNING_METHODS = /* @__PURE__ */ new Set(["personal_sign", "eth_signTypedData_v4", "eth_signTypedData_v3", "eth_signTypedData", "eth_sendTransaction"]);
|
|
1510
1890
|
var RequestRouter = class {
|
|
1511
1891
|
constructor(deps) {
|
|
@@ -1535,8 +1915,10 @@ var RequestRouter = class {
|
|
|
1535
1915
|
return this.visibleAccounts(permission);
|
|
1536
1916
|
case "eth_requestAccounts":
|
|
1537
1917
|
return this.connect(origin, network);
|
|
1538
|
-
case "wallet_getPermissions":
|
|
1539
|
-
|
|
1918
|
+
case "wallet_getPermissions": {
|
|
1919
|
+
const visible = await this.visibleAccounts(permission);
|
|
1920
|
+
return visible.length > 0 ? [permissionDescriptor(origin, visible)] : [];
|
|
1921
|
+
}
|
|
1540
1922
|
case "wallet_requestPermissions": {
|
|
1541
1923
|
const accounts = await this.connect(origin, network);
|
|
1542
1924
|
return [permissionDescriptor(origin, accounts)];
|
|
@@ -1577,11 +1959,20 @@ var RequestRouter = class {
|
|
|
1577
1959
|
case "eth_signTransaction":
|
|
1578
1960
|
throw new RouterError(RELAY_ERRORS.unsupportedMethod, `${method} (blind signing) is never offered by this wallet.`);
|
|
1579
1961
|
}
|
|
1580
|
-
if (READ_METHODS.has(method))
|
|
1962
|
+
if (READ_METHODS.has(method)) {
|
|
1963
|
+
if (method === "eth_getLogs" && !(permission && permission.accounts.length > 0)) await this.boundLogs(network, params);
|
|
1964
|
+
return this.deps.rpc(network, method, params);
|
|
1965
|
+
}
|
|
1581
1966
|
if (SIGNING_METHODS.has(method)) {
|
|
1582
1967
|
if (!this.deps.sign) throw new RouterError(RELAY_ERRORS.unsupportedMethod, `${method} is not available yet.`);
|
|
1583
|
-
if (!permission) throw new RouterError(RELAY_ERRORS.unauthorized, "Connect the wallet first (eth_requestAccounts).");
|
|
1584
|
-
|
|
1968
|
+
if (!permission || permission.accounts.length === 0) throw new RouterError(RELAY_ERRORS.unauthorized, "Connect the wallet first (eth_requestAccounts).");
|
|
1969
|
+
const visible = await this.visibleAccounts(permission);
|
|
1970
|
+
const named = namedAccount(method, params);
|
|
1971
|
+
const inactive = named !== void 0 && permission.accounts.some((a) => a.toLowerCase() === named.toLowerCase()) && !visible.some((a) => a.toLowerCase() === named.toLowerCase());
|
|
1972
|
+
if (visible.length === 0 || inactive) {
|
|
1973
|
+
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).");
|
|
1974
|
+
}
|
|
1975
|
+
return this.deps.sign({ origin, network, method, params, accounts: visible });
|
|
1585
1976
|
}
|
|
1586
1977
|
throw new RouterError(RELAY_ERRORS.unsupportedMethod, `This wallet does not support ${method}.`);
|
|
1587
1978
|
}
|
|
@@ -1591,9 +1982,31 @@ var RequestRouter = class {
|
|
|
1591
1982
|
const available = await this.deps.accounts();
|
|
1592
1983
|
return permission.accounts.filter((a) => available.includes(a));
|
|
1593
1984
|
}
|
|
1985
|
+
/** An unconnected origin's `eth_getLogs`: a block hash, or a range of at most {@link GETLOGS_MAX_SPAN_UNCONNECTED} blocks. */
|
|
1986
|
+
async boundLogs(network, params) {
|
|
1987
|
+
const filter = params[0] ?? {};
|
|
1988
|
+
if (typeof filter !== "object" || filter === null) throw new RouterError(RELAY_ERRORS.invalidParams, "eth_getLogs expects a filter object.");
|
|
1989
|
+
if (filter.blockHash !== void 0 && filter.blockHash !== null) return;
|
|
1990
|
+
let head;
|
|
1991
|
+
const block = async (tag) => {
|
|
1992
|
+
if (tag === void 0 || tag === null || tag === "latest" || tag === "pending" || tag === "safe" || tag === "finalized") {
|
|
1993
|
+
head ??= BigInt(String(await this.deps.rpc(network, "eth_blockNumber", [])));
|
|
1994
|
+
return head;
|
|
1995
|
+
}
|
|
1996
|
+
if (tag === "earliest") return 0n;
|
|
1997
|
+
if (typeof tag === "string" && /^0x[0-9a-fA-F]{1,16}$/.test(tag)) return BigInt(tag);
|
|
1998
|
+
throw new RouterError(RELAY_ERRORS.invalidParams, "eth_getLogs: fromBlock / toBlock must be a block tag or a hex block number.");
|
|
1999
|
+
};
|
|
2000
|
+
const from = await block(filter.fromBlock);
|
|
2001
|
+
const to = await block(filter.toBlock);
|
|
2002
|
+
if (to - from > BigInt(GETLOGS_MAX_SPAN_UNCONNECTED)) {
|
|
2003
|
+
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.`);
|
|
2004
|
+
}
|
|
2005
|
+
}
|
|
1594
2006
|
/**
|
|
1595
2007
|
* Connected and unlocked → the accounts; otherwise the approval window (which
|
|
1596
|
-
* unlocks first when needed) picks them
|
|
2008
|
+
* unlocks first when needed) picks them, and they are ADDED to this origin's
|
|
2009
|
+
* grant (other origins are untouched). Emits `connect` + `accountsChanged`.
|
|
1597
2010
|
*/
|
|
1598
2011
|
async connect(origin, network) {
|
|
1599
2012
|
const existing = await this.visibleAccounts(await this.deps.permissions.get(origin));
|
|
@@ -1602,7 +2015,7 @@ var RequestRouter = class {
|
|
|
1602
2015
|
const available = await this.deps.accounts();
|
|
1603
2016
|
const accounts = Array.isArray(chosen) ? chosen.filter((a) => typeof a === "string" && available.includes(a)) : [];
|
|
1604
2017
|
if (accounts.length === 0) throw new ReviewDeclined("No account was selected.");
|
|
1605
|
-
await this.deps.permissions.
|
|
2018
|
+
await this.deps.permissions.grant(origin, accounts, network.caip2);
|
|
1606
2019
|
this.deps.emit(origin, "connect", { chainId: chainHex(network.caip2) });
|
|
1607
2020
|
this.deps.emit(origin, "accountsChanged", accounts);
|
|
1608
2021
|
return accounts;
|
|
@@ -1622,6 +2035,12 @@ var RequestRouter = class {
|
|
|
1622
2035
|
this.deps.emit(origin, "chainChanged", chainHex(to.caip2));
|
|
1623
2036
|
}
|
|
1624
2037
|
};
|
|
2038
|
+
function namedAccount(method, params) {
|
|
2039
|
+
const addr = (x) => typeof x === "string" && /^0x[0-9a-fA-F]{40}$/.test(x) ? x : void 0;
|
|
2040
|
+
if (method === "personal_sign") return addr(params[1]) ?? addr(params[0]);
|
|
2041
|
+
if (method === "eth_sendTransaction") return addr(params[0]?.from);
|
|
2042
|
+
return addr(params[0]);
|
|
2043
|
+
}
|
|
1625
2044
|
var permissionDescriptor = (origin, accounts) => ({
|
|
1626
2045
|
id: crypto.randomUUID(),
|
|
1627
2046
|
parentCapability: "eth_accounts",
|
|
@@ -1631,6 +2050,7 @@ var permissionDescriptor = (origin, accounts) => ({
|
|
|
1631
2050
|
});
|
|
1632
2051
|
|
|
1633
2052
|
// src/siwe.ts
|
|
2053
|
+
var SIWE_FORBIDDEN_CHARS = /[\u061c\u200b-\u200f\u202a-\u202e\u2060-\u2064\u2066-\u2069\ufeff]/;
|
|
1634
2054
|
var HEADER_LINE = /^(?<domain>[^\n]+?) wants you to sign in with your Ethereum account:(?:\n|$)/;
|
|
1635
2055
|
var ADDRESS_LINE = /^0x[0-9a-fA-F]{40}$/;
|
|
1636
2056
|
var FIELD = /^([A-Za-z ]+): (.*)$/;
|
|
@@ -1734,12 +2154,20 @@ function siweProblems(m, ctx) {
|
|
|
1734
2154
|
}
|
|
1735
2155
|
return out;
|
|
1736
2156
|
}
|
|
2157
|
+
function siweCrossSite(m, ctx) {
|
|
2158
|
+
const site = originWhere(ctx.origin) ?? (ctx.via?.verify === "VALID" && !ctx.via.scam ? whereOf(ctx.via.url) : null);
|
|
2159
|
+
if (!site) return null;
|
|
2160
|
+
const domain = whereOf(m.domain);
|
|
2161
|
+
return !domain || domain.host !== site.host ? { domain: m.domain, origin: `${site.scheme}://${site.host}` } : null;
|
|
2162
|
+
}
|
|
1737
2163
|
function describeSiwe(text, ctx) {
|
|
1738
2164
|
const p = parseLoose(text);
|
|
1739
2165
|
if (!p) return null;
|
|
1740
2166
|
const problems = siweProblems(p.message, ctx);
|
|
1741
|
-
|
|
1742
|
-
|
|
2167
|
+
const cross = siweCrossSite(p.message, ctx);
|
|
2168
|
+
const crossSite = cross ? { crossSite: cross } : {};
|
|
2169
|
+
if (p.missing.length === 0) return { message: p.message, problems, ...crossSite };
|
|
2170
|
+
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 };
|
|
1743
2171
|
}
|
|
1744
2172
|
|
|
1745
2173
|
// src/signing.ts
|
|
@@ -1747,18 +2175,31 @@ var safeNonce = (n) => {
|
|
|
1747
2175
|
if (n < 0n || n > BigInt(Number.MAX_SAFE_INTEGER)) throw new Error(`The RPC returned an impossible nonce (${n}).`);
|
|
1748
2176
|
return Number(n.toString());
|
|
1749
2177
|
};
|
|
2178
|
+
var MAX_FEE_REASKS = 3;
|
|
1750
2179
|
var ADDRESS = /^0x[0-9a-fA-F]{40}$/;
|
|
1751
2180
|
var HEX = /^0x([0-9a-fA-F]{2})*$/;
|
|
1752
2181
|
var lc = (s) => s.toLowerCase();
|
|
2182
|
+
var INVISIBLE = /[\p{Cf}\u2028\u2029\u115f\u1160\u3164\uffa0\x00-\x08\x0b\x0c\x0e-\x1f\x7f-\x9f]/gu;
|
|
2183
|
+
function revealHidden(text) {
|
|
2184
|
+
let hidden = false;
|
|
2185
|
+
const out = text.replace(INVISIBLE, (c) => {
|
|
2186
|
+
hidden = true;
|
|
2187
|
+
return `\u27E8U+${c.codePointAt(0).toString(16).toUpperCase().padStart(4, "0")}\u27E9`;
|
|
2188
|
+
});
|
|
2189
|
+
return { text: out, hidden };
|
|
2190
|
+
}
|
|
1753
2191
|
function messageText(value) {
|
|
1754
|
-
|
|
1755
|
-
|
|
2192
|
+
return shownMessage(value).text;
|
|
2193
|
+
}
|
|
2194
|
+
function shownMessage(value) {
|
|
2195
|
+
if (typeof value !== "string") return { text: "", hidden: false };
|
|
2196
|
+
if (!HEX.test(value)) return revealHidden(value);
|
|
1756
2197
|
try {
|
|
1757
2198
|
const bytes = Uint8Array.from(value.slice(2).match(/../g) ?? [], (b) => Number.parseInt(b, 16));
|
|
1758
2199
|
const s = new TextDecoder("utf-8", { fatal: true }).decode(bytes);
|
|
1759
|
-
return /^[\x09\x0a\x0d\x20-\x7e
|
|
2200
|
+
return /^[\x09\x0a\x0d\x20-\x7e\u00a0-\uffff]*$/.test(s) ? revealHidden(s) : { text: value, hidden: false };
|
|
1760
2201
|
} catch {
|
|
1761
|
-
return value;
|
|
2202
|
+
return { text: value, hidden: false };
|
|
1762
2203
|
}
|
|
1763
2204
|
}
|
|
1764
2205
|
function decodedMessage(value) {
|
|
@@ -1772,6 +2213,7 @@ function decodedMessage(value) {
|
|
|
1772
2213
|
}
|
|
1773
2214
|
var HIDDEN_CONTROL = /[\x00-\x08\x0b\x0c\x0e-\x1f\x7f-\x9f]/;
|
|
1774
2215
|
var err = (code, message) => ({ code, message });
|
|
2216
|
+
var BLIND_HASH = /^0x[0-9a-fA-F]{64}$/;
|
|
1775
2217
|
function classifySignRequest(input) {
|
|
1776
2218
|
const { method, params } = input;
|
|
1777
2219
|
const owned = new Set(input.accounts.map(lc));
|
|
@@ -1782,11 +2224,15 @@ function classifySignRequest(input) {
|
|
|
1782
2224
|
if (ours(msg) && !ours(addr)) [msg, addr] = [addr, msg];
|
|
1783
2225
|
if (!ours(addr)) return err(RELAY_ERRORS.unauthorized, "personal_sign for an address this wallet does not hold.");
|
|
1784
2226
|
if (typeof msg !== "string") return err(RELAY_ERRORS.invalidParams, "personal_sign expects a message string.");
|
|
1785
|
-
const
|
|
2227
|
+
const blindHash = BLIND_HASH.test(msg);
|
|
2228
|
+
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.');
|
|
2229
|
+
const shown = shownMessage(msg);
|
|
2230
|
+
const text = shown.text;
|
|
1786
2231
|
const signed = decodedMessage(msg) ?? text;
|
|
1787
2232
|
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;
|
|
1788
2233
|
if (siwe && HIDDEN_CONTROL.test(signed)) return err(RELAY_ERRORS.invalidParams, "This sign-in message contains hidden control characters \u2014 refused.");
|
|
1789
|
-
|
|
2234
|
+
if (siwe && SIWE_FORBIDDEN_CHARS.test(signed)) return err(RELAY_ERRORS.invalidParams, "This sign-in message contains invisible or text-direction characters \u2014 refused.");
|
|
2235
|
+
return { kind: "sign-message", method, account: checksum(addr), text, raw: msg, ...siwe ? { siwe } : {}, ...shown.hidden ? { hiddenChars: true } : {}, ...blindHash ? { blindHash: true } : {} };
|
|
1790
2236
|
}
|
|
1791
2237
|
if (method === "eth_signTypedData_v4" || method === "eth_signTypedData_v3" || method === "eth_signTypedData") {
|
|
1792
2238
|
const [addr, data] = params;
|
|
@@ -1804,7 +2250,8 @@ function classifySignRequest(input) {
|
|
|
1804
2250
|
if (cid !== void 0 && cid !== null && Number(cid) !== input.network.chainId) {
|
|
1805
2251
|
return err(RELAY_ERRORS.chainDisconnected, `Typed data is for chain ${String(cid)}; this site is on ${input.network.name} (${input.network.chainId}).`);
|
|
1806
2252
|
}
|
|
1807
|
-
|
|
2253
|
+
const shown = revealHidden(JSON.stringify(parsed, bigintSafe, 2));
|
|
2254
|
+
return { kind: "sign-typed-data", method, account: checksum(addr), text: shown.text, typedData: parsed, ...shown.hidden ? { hiddenChars: true } : {} };
|
|
1808
2255
|
}
|
|
1809
2256
|
if (method === "eth_sendTransaction") {
|
|
1810
2257
|
const tx = params[0] ?? {};
|
|
@@ -1853,16 +2300,33 @@ function overpriced(shown, fresh) {
|
|
|
1853
2300
|
if (shown.maxFeePerGas !== void 0 && fresh.maxFeePerGas !== void 0) return big(shown.maxFeePerGas) > STALE_FEE_FACTOR * big(fresh.maxFeePerGas);
|
|
1854
2301
|
return false;
|
|
1855
2302
|
}
|
|
2303
|
+
function gasLimitExcessive(limit, estimate) {
|
|
2304
|
+
const a = estimate * 3n / 2n;
|
|
2305
|
+
const b = estimate + 50000n;
|
|
2306
|
+
return limit > (a > b ? a : b);
|
|
2307
|
+
}
|
|
1856
2308
|
async function prepareTransaction(deps, network, from, tx) {
|
|
1857
2309
|
const call = { from, to: tx.to, value: hex(BigInt(tx.value)), ...tx.data ? { data: tx.data } : {} };
|
|
2310
|
+
const estimate = () => deps.rpc(network, "eth_estimateGas", [call]);
|
|
1858
2311
|
const [nonceHex, block, gasHex] = await Promise.all([
|
|
1859
2312
|
deps.rpc(network, "eth_getTransactionCount", [from, "pending"]),
|
|
1860
2313
|
deps.rpc(network, "eth_getBlockByNumber", ["latest", false]),
|
|
1861
|
-
tx.gas ?
|
|
2314
|
+
tx.gas ? estimate().then((v) => v, () => null) : estimate()
|
|
1862
2315
|
]);
|
|
1863
|
-
const
|
|
1864
|
-
|
|
1865
|
-
|
|
2316
|
+
const nonce = safeNonce(big(nonceHex));
|
|
2317
|
+
if (!tx.gas) {
|
|
2318
|
+
const estimated = big(gasHex);
|
|
2319
|
+
return withFees(network.chainId, nonce, (estimated * 120n + 99n) / 100n, await readFees(deps, network, block));
|
|
2320
|
+
}
|
|
2321
|
+
const limit = big(tx.gas);
|
|
2322
|
+
const prepared = withFees(network.chainId, nonce, limit, await readFees(deps, network, block));
|
|
2323
|
+
let est = null;
|
|
2324
|
+
try {
|
|
2325
|
+
est = typeof gasHex === "string" ? big(gasHex) : null;
|
|
2326
|
+
} catch {
|
|
2327
|
+
est = null;
|
|
2328
|
+
}
|
|
2329
|
+
return est !== null && est > 0n ? { ...prepared, gasCheck: { estimate: hex(est), excessive: gasLimitExcessive(limit, est) } } : prepared;
|
|
1866
2330
|
}
|
|
1867
2331
|
function createSignHandler(deps) {
|
|
1868
2332
|
const tails = /* @__PURE__ */ new Map();
|
|
@@ -1878,7 +2342,8 @@ function createSignHandler(deps) {
|
|
|
1878
2342
|
};
|
|
1879
2343
|
return async ({ origin, network, method, params, accounts }) => {
|
|
1880
2344
|
const via = deps.approvals.viaFor(origin);
|
|
1881
|
-
const
|
|
2345
|
+
const allowBlindHash = method === "personal_sign" && deps.allowBlindHash ? await deps.allowBlindHash() : false;
|
|
2346
|
+
const info = classifySignRequest({ network, method, params, accounts, origin, ...via ? { via } : {}, ...allowBlindHash ? { allowBlindHash: true } : {} });
|
|
1882
2347
|
if ("code" in info) throw RouterError.from(info);
|
|
1883
2348
|
const prepared = info.kind === "send-transaction" ? await prepareTransaction(deps, network, info.account, info.tx) : void 0;
|
|
1884
2349
|
const preview = deps.preview ? await deps.preview({ network, request: info, accounts }).catch(() => null) : null;
|
|
@@ -1888,14 +2353,20 @@ function createSignHandler(deps) {
|
|
|
1888
2353
|
const key = `${network.caip2}|${lc(info.account)}`;
|
|
1889
2354
|
return serial(key, async () => {
|
|
1890
2355
|
let shown = prepared;
|
|
2356
|
+
let reasks = 0;
|
|
1891
2357
|
for (; ; ) {
|
|
1892
2358
|
const [nonceHex, fees] = await Promise.all([deps.rpc(network, "eth_getTransactionCount", [info.account, "pending"]), readFees(deps, network)]);
|
|
1893
2359
|
const local = nextNonce.get(key) ?? 0;
|
|
1894
2360
|
const nodeNonce = safeNonce(big(nonceHex));
|
|
1895
2361
|
const nonce = nodeNonce > local ? nodeNonce : local;
|
|
1896
2362
|
if (outgrown(shown, fees)) {
|
|
1897
|
-
|
|
1898
|
-
|
|
2363
|
+
const next = withFees(network.chainId, nonce, big(shown.gasLimit), fees);
|
|
2364
|
+
const ceiling = deps.feeCeiling?.(network);
|
|
2365
|
+
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.`);
|
|
2366
|
+
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.");
|
|
2367
|
+
reasks++;
|
|
2368
|
+
await deps.approvals.request({ origin, chain: network.caip2, kind: info.kind, payload: { ...payload, prepared: next, repriced: true, repricedFrom: shown.maxFee } }, { followUp: true });
|
|
2369
|
+
shown = next;
|
|
1899
2370
|
continue;
|
|
1900
2371
|
}
|
|
1901
2372
|
const fresh = withFees(network.chainId, nonce, big(shown.gasLimit), fees);
|
|
@@ -2017,7 +2488,14 @@ async function quoteRoutes(providers, params, options = {}) {
|
|
|
2017
2488
|
candidates.push(p);
|
|
2018
2489
|
}
|
|
2019
2490
|
const pending = new Set(candidates.map((p) => p.name));
|
|
2020
|
-
const
|
|
2491
|
+
const depositAddressRoute = options.depositAddressRoute;
|
|
2492
|
+
const rankOptions = {
|
|
2493
|
+
...params.slippageBps !== void 0 ? { slippageBps: params.slippageBps } : {},
|
|
2494
|
+
...options.depositAddressProviders ? { depositAddressProviders: options.depositAddressProviders } : {},
|
|
2495
|
+
...depositAddressRoute ? { depositAddressRoute: (q) => depositAddressRoute(q, params) } : {}
|
|
2496
|
+
};
|
|
2497
|
+
const { slippageBps: _slippage, ...shapeParams } = params;
|
|
2498
|
+
const view = () => ({ routes: rank(Object.values(quotes), rankOptions), pending: [...pending], refusals: { ...refusals }, quotes: { ...quotes } });
|
|
2021
2499
|
const emit = () => options.onUpdate?.(view());
|
|
2022
2500
|
emit();
|
|
2023
2501
|
await Promise.all(
|
|
@@ -2032,7 +2510,7 @@ async function quoteRoutes(providers, params, options = {}) {
|
|
|
2032
2510
|
const q = await Promise.race([p.getQuote({ ...params, dry: true, onRefusal: (r) => {
|
|
2033
2511
|
refusal = r;
|
|
2034
2512
|
} }, ctrl.signal), aborted]);
|
|
2035
|
-
if (q && (0, import_protocol_swap2_core.validQuote)(q, { ...
|
|
2513
|
+
if (q && (0, import_protocol_swap2_core.validQuote)(q, { ...shapeParams, dry: true }, p.name)) quotes[p.name] = q;
|
|
2036
2514
|
else refusals[p.name] = refusal ?? (q ? "returned an invalid quote" : "no route for this pair right now");
|
|
2037
2515
|
} catch (e) {
|
|
2038
2516
|
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);
|
|
@@ -2046,16 +2524,50 @@ async function quoteRoutes(providers, params, options = {}) {
|
|
|
2046
2524
|
);
|
|
2047
2525
|
return view();
|
|
2048
2526
|
}
|
|
2049
|
-
function
|
|
2527
|
+
function gapBps(dest, min) {
|
|
2528
|
+
if (dest <= 0n || min >= dest) return 0;
|
|
2529
|
+
return Number((((dest - min) * 10000n + dest - 1n) / dest).toString());
|
|
2530
|
+
}
|
|
2531
|
+
function rank(quotes, options = {}) {
|
|
2050
2532
|
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);
|
|
2051
2533
|
const fastest = sorted.length ? sorted.reduce((f, q) => q.etaSeconds < f.etaSeconds ? q : f) : null;
|
|
2052
|
-
|
|
2534
|
+
const depositProviders = new Set(options.depositAddressProviders ?? []);
|
|
2535
|
+
const paysAssignedAddress = (q) => {
|
|
2536
|
+
if (depositProviders.has(q.provider)) return true;
|
|
2537
|
+
if (!options.depositAddressRoute) return false;
|
|
2538
|
+
try {
|
|
2539
|
+
return options.depositAddressRoute(q) === true;
|
|
2540
|
+
} catch {
|
|
2541
|
+
return true;
|
|
2542
|
+
}
|
|
2543
|
+
};
|
|
2544
|
+
const assigned = new Map(sorted.map((q) => [q, paysAssignedAddress(q)]));
|
|
2545
|
+
const s = options.slippageBps;
|
|
2546
|
+
const warningsOf = (q) => {
|
|
2547
|
+
const w = [];
|
|
2548
|
+
if (s !== void 0 && Number.isInteger(s) && s >= 0 && s < 1e4) {
|
|
2549
|
+
const dest = BigInt(q.destAmount);
|
|
2550
|
+
const min = BigInt(q.minDestAmount);
|
|
2551
|
+
if (min < (0, import_protocol_swap2_core.slippageFloor)(dest, s)) w.push({ kind: "min-below-slippage", slippageBps: s, minGapBps: gapBps(dest, min) });
|
|
2552
|
+
}
|
|
2553
|
+
if (assigned.get(q)) w.push({ kind: "deposit-address" });
|
|
2554
|
+
return w;
|
|
2555
|
+
};
|
|
2556
|
+
const warned = new Map(sorted.map((q) => [q, warningsOf(q)]));
|
|
2557
|
+
const verifiableExists = sorted.some((q) => !assigned.get(q));
|
|
2558
|
+
const best = sorted.find((q) => {
|
|
2559
|
+
const w = warned.get(q);
|
|
2560
|
+
return !w.some((x) => x.kind === "min-below-slippage") && !(w.some((x) => x.kind === "deposit-address") && verifiableExists);
|
|
2561
|
+
});
|
|
2562
|
+
const ordered = best ? [best, ...sorted.filter((q) => q !== best)] : sorted;
|
|
2563
|
+
return ordered.map((q) => {
|
|
2053
2564
|
const poolFee = q.providerData?.poolFee;
|
|
2054
2565
|
const feeNote = typeof poolFee === "string" && poolFee && q.feeUsd === 0 ? poolFee === "pools" ? "pool fees in price" : `pool ${poolFee} in price` : void 0;
|
|
2055
2566
|
const tags = [];
|
|
2056
|
-
if (
|
|
2567
|
+
if (q === best) tags.push("best");
|
|
2057
2568
|
if (fastest && q.provider === fastest.provider) tags.push("fastest");
|
|
2058
|
-
|
|
2569
|
+
const warnings = warned.get(q);
|
|
2570
|
+
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 } : {} };
|
|
2059
2571
|
});
|
|
2060
2572
|
}
|
|
2061
2573
|
var FLOOR_TOLERANCE_BPS = 50n;
|
|
@@ -2074,18 +2586,21 @@ function depositOf(s, approvals) {
|
|
|
2074
2586
|
if (!s.data.startsWith(TRANSFER_SELECTOR) || s.data.length < 10 + 64) return void 0;
|
|
2075
2587
|
return `0x${s.data.slice(10 + 24, 10 + 64)}`;
|
|
2076
2588
|
}
|
|
2589
|
+
var pct = (bps) => `${Number((bps / 100).toFixed(2))}%`;
|
|
2077
2590
|
function swapApprovalPayload(plan, meta) {
|
|
2078
2591
|
const isOwn = meta.own.some((a) => lc3(a) === lc3(plan.recipient));
|
|
2592
|
+
const assigned = meta.depositAddressAssignedByProvider === true;
|
|
2079
2593
|
const checks = [{ status: "ok", label: `Built by the ${meta.providerLabel} module` }];
|
|
2594
|
+
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` };
|
|
2080
2595
|
const s = plan.source;
|
|
2081
2596
|
let source;
|
|
2082
2597
|
if (s.kind === "call") {
|
|
2083
2598
|
const deposit = depositOf(s, plan.approvals.length);
|
|
2084
2599
|
source = { kind: "call", to: s.to, ...s.data ? { data: s.data } : {}, value: s.value.toString(), ...deposit ? { deposit } : {} };
|
|
2085
|
-
checks.push(deposit
|
|
2600
|
+
checks.push(deposit || assigned ? depositCheck : { status: "ok", label: `Calls the ${meta.providerLabel} contract` });
|
|
2086
2601
|
} else if (s.kind === "token-transfer") {
|
|
2087
2602
|
source = { kind: "token-transfer", token: s.token, recipient: s.recipient, amount: s.amount.toString(), ...s.memo ? { memo: s.memo } : {} };
|
|
2088
|
-
checks.push(
|
|
2603
|
+
checks.push(depositCheck);
|
|
2089
2604
|
} else if (s.kind === "serialized") {
|
|
2090
2605
|
source = { kind: "serialized" };
|
|
2091
2606
|
checks.push({ status: "ok", label: "Balance changes are proven before signing (SDK guard)" });
|
|
@@ -2096,7 +2611,14 @@ function swapApprovalPayload(plan, meta) {
|
|
|
2096
2611
|
}
|
|
2097
2612
|
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" });
|
|
2098
2613
|
if (plan.approvals.length > 1) checks.push({ status: "ok", label: `${plan.approvals.length} approvals (reset to 0 first)` });
|
|
2099
|
-
checks.push(isOwn ? { status: "
|
|
2614
|
+
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" });
|
|
2615
|
+
let slippage;
|
|
2616
|
+
const sb = meta.slippageBps;
|
|
2617
|
+
if (sb !== void 0 && Number.isInteger(sb) && sb >= 0 && sb < 1e4) {
|
|
2618
|
+
const below = plan.minDestAmount < (0, import_protocol_swap2_core.slippageFloor)(plan.destAmount, sb);
|
|
2619
|
+
slippage = { bps: sb, minGapBps: gapBps(plan.destAmount, plan.minDestAmount), below };
|
|
2620
|
+
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` });
|
|
2621
|
+
}
|
|
2100
2622
|
return {
|
|
2101
2623
|
provider: plan.provider,
|
|
2102
2624
|
providerLabel: meta.providerLabel,
|
|
@@ -2113,7 +2635,9 @@ function swapApprovalPayload(plan, meta) {
|
|
|
2113
2635
|
validUntilMs: plan.validUntilMs,
|
|
2114
2636
|
approvals: plan.approvals.map((a) => ({ token: a.token, spender: a.spender, amount: a.amount.toString() })),
|
|
2115
2637
|
source,
|
|
2116
|
-
checks
|
|
2638
|
+
checks,
|
|
2639
|
+
...slippage ? { slippage } : {},
|
|
2640
|
+
...assigned ? { depositAssignedByProvider: true } : {}
|
|
2117
2641
|
};
|
|
2118
2642
|
}
|
|
2119
2643
|
function parseTypedOrder(json) {
|
|
@@ -2564,6 +3088,10 @@ var RecipientBook = class {
|
|
|
2564
3088
|
await this.storage.set(s);
|
|
2565
3089
|
this._state = s;
|
|
2566
3090
|
}
|
|
3091
|
+
/** Forget the cached book: the next read asks storage again (e.g. a storage that only answers once the wallet is unlocked). */
|
|
3092
|
+
reload() {
|
|
3093
|
+
this._state = null;
|
|
3094
|
+
}
|
|
2567
3095
|
/** `known` → the address is one the user paid before; `lookalike` → it imitates one. */
|
|
2568
3096
|
async check(namespace, address) {
|
|
2569
3097
|
const list = await this.list(namespace);
|
|
@@ -2571,4 +3099,236 @@ var RecipientBook = class {
|
|
|
2571
3099
|
return { known, lookalike: known ? null : lookalikeOf(address, list) };
|
|
2572
3100
|
}
|
|
2573
3101
|
};
|
|
3102
|
+
|
|
3103
|
+
// src/mac-storage.ts
|
|
3104
|
+
var import_core5 = require("@anywalletkit/core");
|
|
3105
|
+
var STORAGE_MAC_FIELD = "anywalletkit.mac/1";
|
|
3106
|
+
var STORAGE_VERSION_FIELD = "anywalletkit.ver/1";
|
|
3107
|
+
var HKDF_INFO2 = "anywalletkit/storage-mac/1";
|
|
3108
|
+
var KEY_BYTES = 32;
|
|
3109
|
+
async function deriveStorageMacKey(dek, crypto2 = globalThis.crypto) {
|
|
3110
|
+
if (dek.length !== 32) throw new VaultError("Vault key must be 32 bytes.", "invalid-input");
|
|
3111
|
+
const ikm = await withSecretBuffer(dek, (b) => crypto2.subtle.importKey("raw", b, "HKDF", false, ["deriveBits"]));
|
|
3112
|
+
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);
|
|
3113
|
+
return new Uint8Array(bits);
|
|
3114
|
+
}
|
|
3115
|
+
function macJsonStorage(inner, options) {
|
|
3116
|
+
const { label, versions } = options;
|
|
3117
|
+
if (versions && label.includes("\n")) throw new VaultError("A versioned store label has no line break.", "invalid-input");
|
|
3118
|
+
const readWithoutKey = options.readWithoutKey ?? "unverified";
|
|
3119
|
+
const writeWithoutKey = options.writeWithoutKey ?? "unsigned";
|
|
3120
|
+
const withKey = async (given, fn) => {
|
|
3121
|
+
const key = given ? Uint8Array.from(given) : await options.key();
|
|
3122
|
+
try {
|
|
3123
|
+
return await fn(key);
|
|
3124
|
+
} finally {
|
|
3125
|
+
if (key) (0, import_core5.zeroize)(key);
|
|
3126
|
+
}
|
|
3127
|
+
};
|
|
3128
|
+
const stored = async (key) => {
|
|
3129
|
+
const raw = await inner.get();
|
|
3130
|
+
if (raw === null || raw === void 0) return { state: "empty", value: null };
|
|
3131
|
+
const envelope = isStored(raw);
|
|
3132
|
+
const value = envelope ? raw.value : raw;
|
|
3133
|
+
if (!envelope || raw[STORAGE_MAC_FIELD] === null) return { state: "unsigned", value };
|
|
3134
|
+
if (!key) return { state: "no-key", value };
|
|
3135
|
+
if (!versions) return { state: await verify(key, label, value, raw[STORAGE_MAC_FIELD]) ? "ok" : "invalid", value };
|
|
3136
|
+
const version = STORAGE_VERSION_FIELD in raw ? raw[STORAGE_VERSION_FIELD] : 0;
|
|
3137
|
+
if (version !== 0 && !isVersion(version)) return { state: "invalid", value };
|
|
3138
|
+
if (!await verify(key, label, value, raw[STORAGE_MAC_FIELD], version || void 0)) return { state: "invalid", value };
|
|
3139
|
+
return { state: "ok", value, version };
|
|
3140
|
+
};
|
|
3141
|
+
const put = async (key, value, version) => {
|
|
3142
|
+
const mac = await sign(key, label, value, version);
|
|
3143
|
+
await inner.set(version === void 0 ? { [STORAGE_MAC_FIELD]: mac, value } : { [STORAGE_MAC_FIELD]: mac, [STORAGE_VERSION_FIELD]: version, value });
|
|
3144
|
+
};
|
|
3145
|
+
const seen = async (key) => {
|
|
3146
|
+
const r = await stored(key);
|
|
3147
|
+
return r.state === "ok" ? r.version ?? 0 : 0;
|
|
3148
|
+
};
|
|
3149
|
+
const read = (given) => withKey(given, async (key) => {
|
|
3150
|
+
if (!versions || !key) return stored(key);
|
|
3151
|
+
return versions.exclusive(async (ledger) => {
|
|
3152
|
+
const r = await stored(key);
|
|
3153
|
+
if (r.state === "empty") return await ledger.check(label, null, key) === "ok" ? r : { state: "stale", value: null };
|
|
3154
|
+
if (r.state !== "ok") return r;
|
|
3155
|
+
return await ledger.check(label, r.version ?? 0, key) === "ok" ? r : { ...r, state: "stale" };
|
|
3156
|
+
});
|
|
3157
|
+
});
|
|
3158
|
+
const write = (value, given) => withKey(given, async (key) => {
|
|
3159
|
+
if (!key) throw new VaultError("The wallet is locked: unlock it to change this.", "locked");
|
|
3160
|
+
if (!versions) return put(key, value);
|
|
3161
|
+
await versions.exclusive((ledger) => ledger.advance(label, key, () => seen(key), (version) => put(key, value, version)));
|
|
3162
|
+
});
|
|
3163
|
+
return {
|
|
3164
|
+
read,
|
|
3165
|
+
write,
|
|
3166
|
+
async get() {
|
|
3167
|
+
const r = await read();
|
|
3168
|
+
if (r.state === "ok") return r.value;
|
|
3169
|
+
if (r.state === "no-key" && readWithoutKey === "unverified") return r.value;
|
|
3170
|
+
return null;
|
|
3171
|
+
},
|
|
3172
|
+
async set(value) {
|
|
3173
|
+
await withKey(void 0, async (key) => {
|
|
3174
|
+
if (key) return write(value, key);
|
|
3175
|
+
if (writeWithoutKey === "refuse") throw new VaultError("The wallet is locked: unlock it to change this.", "locked");
|
|
3176
|
+
await inner.set({ [STORAGE_MAC_FIELD]: null, value });
|
|
3177
|
+
});
|
|
3178
|
+
},
|
|
3179
|
+
async rekey(oldKey, newKey) {
|
|
3180
|
+
if (!versions) {
|
|
3181
|
+
const r = await read(oldKey);
|
|
3182
|
+
if (r.state === "ok") await write(r.value, newKey);
|
|
3183
|
+
return r.state;
|
|
3184
|
+
}
|
|
3185
|
+
return versions.exclusive(async (ledger) => {
|
|
3186
|
+
const r = await stored(oldKey);
|
|
3187
|
+
if (r.state === "empty") return await ledger.check(label, null, newKey) === "ok" ? "empty" : "stale";
|
|
3188
|
+
if (r.state !== "ok") return r.state;
|
|
3189
|
+
const version = r.version ?? 0;
|
|
3190
|
+
if (await ledger.check(label, version, newKey) !== "ok") return "stale";
|
|
3191
|
+
if (version > 0) await put(newKey, r.value, version);
|
|
3192
|
+
else await ledger.advance(label, newKey, async () => 0, (v) => put(newKey, r.value, v));
|
|
3193
|
+
return "ok";
|
|
3194
|
+
});
|
|
3195
|
+
}
|
|
3196
|
+
};
|
|
3197
|
+
}
|
|
3198
|
+
function isStored(raw) {
|
|
3199
|
+
if (typeof raw !== "object" || raw === null || !(STORAGE_MAC_FIELD in raw) || !("value" in raw)) return false;
|
|
3200
|
+
const mac = raw[STORAGE_MAC_FIELD];
|
|
3201
|
+
return mac === null || typeof mac === "string";
|
|
3202
|
+
}
|
|
3203
|
+
async function hmacKey(key, usage) {
|
|
3204
|
+
if (key.length !== KEY_BYTES) throw new VaultError("A storage MAC key is 32 bytes.", "invalid-input");
|
|
3205
|
+
return withSecretBuffer(key, (b) => globalThis.crypto.subtle.importKey("raw", b, { name: "HMAC", hash: "SHA-256" }, false, [usage]));
|
|
3206
|
+
}
|
|
3207
|
+
function macInput(label, value, version) {
|
|
3208
|
+
return toArrayBuffer(utf8.encode(version === void 0 ? `${label}
|
|
3209
|
+
${canonicalJson(value)}` : `${label}
|
|
3210
|
+
${version}
|
|
3211
|
+
${canonicalJson(value)}`));
|
|
3212
|
+
}
|
|
3213
|
+
async function sign(key, label, value, version) {
|
|
3214
|
+
return toBase64(new Uint8Array(await globalThis.crypto.subtle.sign("HMAC", await hmacKey(key, "sign"), macInput(label, value, version))));
|
|
3215
|
+
}
|
|
3216
|
+
async function verify(key, label, value, mac, version) {
|
|
3217
|
+
let bytes;
|
|
3218
|
+
try {
|
|
3219
|
+
bytes = fromBase64(mac);
|
|
3220
|
+
} catch {
|
|
3221
|
+
return false;
|
|
3222
|
+
}
|
|
3223
|
+
return globalThis.crypto.subtle.verify("HMAC", await hmacKey(key, "verify"), toArrayBuffer(bytes), macInput(label, value, version));
|
|
3224
|
+
}
|
|
3225
|
+
var isVersion = (x) => typeof x === "number" && Number.isSafeInteger(x) && x > 0;
|
|
3226
|
+
function canonicalJson(value) {
|
|
3227
|
+
return JSON.stringify(sortKeys(JSON.parse(JSON.stringify(value ?? null))));
|
|
3228
|
+
}
|
|
3229
|
+
function sortKeys(x) {
|
|
3230
|
+
if (Array.isArray(x)) return x.map(sortKeys);
|
|
3231
|
+
if (x === null || typeof x !== "object") return x;
|
|
3232
|
+
const out = {};
|
|
3233
|
+
for (const k of Object.keys(x).sort()) out[k] = sortKeys(x[k]);
|
|
3234
|
+
return out;
|
|
3235
|
+
}
|
|
3236
|
+
var LEDGER_LABEL = "anywalletkit/storage-versions/1";
|
|
3237
|
+
function storageVersionLedger(options) {
|
|
3238
|
+
const label = options.label ?? LEDGER_LABEL;
|
|
3239
|
+
const acceptUnversioned = options.acceptUnversioned ?? false;
|
|
3240
|
+
const now = options.now ?? Date.now;
|
|
3241
|
+
let tail = Promise.resolve();
|
|
3242
|
+
const queue = (fn) => {
|
|
3243
|
+
const run = tail.then(fn, fn);
|
|
3244
|
+
tail = run.then(() => void 0, () => void 0);
|
|
3245
|
+
return run;
|
|
3246
|
+
};
|
|
3247
|
+
const high = async () => {
|
|
3248
|
+
const v = await options.mark.get();
|
|
3249
|
+
return isVersion(v) ? v : 0;
|
|
3250
|
+
};
|
|
3251
|
+
const parse = async (raw, key) => {
|
|
3252
|
+
if (!isStored(raw) || raw[STORAGE_MAC_FIELD] === null) return null;
|
|
3253
|
+
const epoch = raw[STORAGE_VERSION_FIELD];
|
|
3254
|
+
const v = raw.value;
|
|
3255
|
+
if (!isVersion(epoch) || typeof v !== "object" || v === null || !isVersion(v.base) || typeof v.sealed !== "boolean") return null;
|
|
3256
|
+
if (typeof v.versions !== "object" || v.versions === null || !Object.values(v.versions).every(isVersion)) return null;
|
|
3257
|
+
if (!await verify(key, label, v, raw[STORAGE_MAC_FIELD], epoch)) return null;
|
|
3258
|
+
return { epoch, base: v.base, sealed: v.sealed, versions: { ...v.versions } };
|
|
3259
|
+
};
|
|
3260
|
+
const load = async (key) => {
|
|
3261
|
+
const mark = await high();
|
|
3262
|
+
const raw = await options.manifest.get();
|
|
3263
|
+
if (raw === null || raw === void 0) return { state: mark > 0 ? "stale" : "empty" };
|
|
3264
|
+
const manifest = await parse(raw, key);
|
|
3265
|
+
if (!manifest) return { state: "invalid" };
|
|
3266
|
+
if (manifest.epoch < mark) return { state: "stale", manifest };
|
|
3267
|
+
if (manifest.epoch > mark) await options.mark.set(manifest.epoch);
|
|
3268
|
+
return { state: "ok", manifest };
|
|
3269
|
+
};
|
|
3270
|
+
const store = async (m, key) => {
|
|
3271
|
+
const value = { base: m.base, sealed: m.sealed, versions: m.versions };
|
|
3272
|
+
await options.manifest.set({ [STORAGE_MAC_FIELD]: await sign(key, label, value, m.epoch), [STORAGE_VERSION_FIELD]: m.epoch, value });
|
|
3273
|
+
if (m.epoch > await high()) await options.mark.set(m.epoch);
|
|
3274
|
+
};
|
|
3275
|
+
const fresh = async (sealed, prior, key) => {
|
|
3276
|
+
const floor = Math.max(await high(), Math.floor(now()), prior ? Math.max(prior.epoch, prior.base, ...Object.values(prior.versions)) : 0);
|
|
3277
|
+
if (!Number.isSafeInteger(floor + 2)) throw new VaultError("Storage version out of range.", "invalid-input");
|
|
3278
|
+
const m = { epoch: floor + 1, base: floor + 1, sealed, versions: {} };
|
|
3279
|
+
await store(m, key);
|
|
3280
|
+
return m;
|
|
3281
|
+
};
|
|
3282
|
+
const record = (m, name, version, key) => store({ ...m, epoch: m.epoch + 1, versions: { ...m.versions, [name]: version } }, key);
|
|
3283
|
+
const named = (name) => {
|
|
3284
|
+
if (name === label || name.includes("\n")) throw new VaultError("Not a store label.", "invalid-input");
|
|
3285
|
+
};
|
|
3286
|
+
const ops = {
|
|
3287
|
+
async check(name, version, key) {
|
|
3288
|
+
named(name);
|
|
3289
|
+
const l = await load(key);
|
|
3290
|
+
if (l.state === "empty") return version === null || version === 0 && acceptUnversioned ? "ok" : "stale";
|
|
3291
|
+
if (l.state !== "ok") return "stale";
|
|
3292
|
+
const m = l.manifest;
|
|
3293
|
+
const listed = m.versions[name];
|
|
3294
|
+
if (listed === void 0 && version === null) return m.sealed ? "stale" : "ok";
|
|
3295
|
+
if (listed === void 0 && version === 0) return !m.sealed && acceptUnversioned ? "ok" : "stale";
|
|
3296
|
+
if (version === null || version === 0) return "stale";
|
|
3297
|
+
if (version === listed) return "ok";
|
|
3298
|
+
if (version !== (listed ?? m.base) + 1) return "stale";
|
|
3299
|
+
await record(m, name, version, key).catch(() => void 0);
|
|
3300
|
+
return "ok";
|
|
3301
|
+
},
|
|
3302
|
+
async advance(name, key, seen, write) {
|
|
3303
|
+
named(name);
|
|
3304
|
+
const l = await load(key);
|
|
3305
|
+
const before = await seen();
|
|
3306
|
+
const m = l.state === "ok" ? l.manifest : await fresh(l.state !== "empty" || before > 0, l.manifest, key);
|
|
3307
|
+
const version = Math.max(m.versions[name] ?? m.base, before) + 1;
|
|
3308
|
+
if (!Number.isSafeInteger(version)) throw new VaultError("Storage version out of range.", "invalid-input");
|
|
3309
|
+
await write(version);
|
|
3310
|
+
await record(m, name, version, key);
|
|
3311
|
+
return version;
|
|
3312
|
+
}
|
|
3313
|
+
};
|
|
3314
|
+
return {
|
|
3315
|
+
exclusive: (fn) => queue(() => fn(ops)),
|
|
3316
|
+
condemn: (key) => queue(async () => {
|
|
3317
|
+
await fresh(true, (await load(key)).manifest, key);
|
|
3318
|
+
}),
|
|
3319
|
+
rekey: (oldKey, newKey) => queue(async () => {
|
|
3320
|
+
const l = await load(oldKey);
|
|
3321
|
+
if (l.state === "ok") await store({ ...l.manifest, epoch: l.manifest.epoch + 1 }, newKey);
|
|
3322
|
+
return l.state;
|
|
3323
|
+
}),
|
|
3324
|
+
read: (key) => queue(async () => {
|
|
3325
|
+
const l = await load(key);
|
|
3326
|
+
return { state: l.state, ...l.manifest ?? { epoch: 0, base: 0, sealed: false, versions: {} } };
|
|
3327
|
+
}),
|
|
3328
|
+
reset: () => queue(async () => {
|
|
3329
|
+
await options.manifest.set(null);
|
|
3330
|
+
await options.mark.set(null);
|
|
3331
|
+
})
|
|
3332
|
+
};
|
|
3333
|
+
}
|
|
2574
3334
|
//# sourceMappingURL=index.cjs.map
|