@anywalletkit/extension-kit 0.3.6 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +28 -7
- package/dist/index.cjs +867 -56
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +481 -17
- package/dist/index.d.ts +481 -17
- package/dist/index.js +868 -57
- package/dist/index.js.map +1 -1
- package/package.json +9 -9
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",
|
|
@@ -1630,7 +2049,60 @@ var permissionDescriptor = (origin, accounts) => ({
|
|
|
1630
2049
|
date: Date.now()
|
|
1631
2050
|
});
|
|
1632
2051
|
|
|
2052
|
+
// src/personal-sign.ts
|
|
2053
|
+
var HEX = /^0x([0-9a-fA-F]{2})*$/;
|
|
2054
|
+
var BLIND_HASH_MAX_CHARS = 66;
|
|
2055
|
+
var hexBytes = (value) => Uint8Array.from(value.slice(2).match(/../g) ?? [], (b) => Number.parseInt(b, 16));
|
|
2056
|
+
function codePoint(s, i) {
|
|
2057
|
+
const c = s.charCodeAt(i);
|
|
2058
|
+
if (c < 55296 || c > 57343) return c;
|
|
2059
|
+
const lo = c <= 56319 && i + 1 < s.length ? s.charCodeAt(i + 1) : 0;
|
|
2060
|
+
return lo >= 56320 && lo <= 57343 ? 65536 + (c - 55296 << 10) + (lo - 56320) : 65533;
|
|
2061
|
+
}
|
|
2062
|
+
function utf8Length(s) {
|
|
2063
|
+
let n = 0;
|
|
2064
|
+
for (let i = 0; i < s.length; i++) {
|
|
2065
|
+
const c = codePoint(s, i);
|
|
2066
|
+
if (c > 65535) {
|
|
2067
|
+
n += 4;
|
|
2068
|
+
i++;
|
|
2069
|
+
} else n += c < 128 ? 1 : c < 2048 ? 2 : 3;
|
|
2070
|
+
}
|
|
2071
|
+
return n;
|
|
2072
|
+
}
|
|
2073
|
+
function utf8Bytes(s) {
|
|
2074
|
+
const out = new Uint8Array(utf8Length(s));
|
|
2075
|
+
let o = 0;
|
|
2076
|
+
for (let i = 0; i < s.length; i++) {
|
|
2077
|
+
const c = codePoint(s, i);
|
|
2078
|
+
if (c < 128) out[o++] = c;
|
|
2079
|
+
else if (c < 2048) {
|
|
2080
|
+
out[o++] = 192 | c >> 6;
|
|
2081
|
+
out[o++] = 128 | c & 63;
|
|
2082
|
+
} else if (c < 65536) {
|
|
2083
|
+
out[o++] = 224 | c >> 12;
|
|
2084
|
+
out[o++] = 128 | c >> 6 & 63;
|
|
2085
|
+
out[o++] = 128 | c & 63;
|
|
2086
|
+
} else {
|
|
2087
|
+
out[o++] = 240 | c >> 18;
|
|
2088
|
+
out[o++] = 128 | c >> 12 & 63;
|
|
2089
|
+
out[o++] = 128 | c >> 6 & 63;
|
|
2090
|
+
out[o++] = 128 | c & 63;
|
|
2091
|
+
i++;
|
|
2092
|
+
}
|
|
2093
|
+
}
|
|
2094
|
+
return out;
|
|
2095
|
+
}
|
|
2096
|
+
function personalSignInput(message) {
|
|
2097
|
+
return HEX.test(message) ? hexBytes(message) : message;
|
|
2098
|
+
}
|
|
2099
|
+
function personalSignBytes(message) {
|
|
2100
|
+
const input = personalSignInput(message);
|
|
2101
|
+
return typeof input === "string" ? utf8Bytes(input) : input;
|
|
2102
|
+
}
|
|
2103
|
+
|
|
1633
2104
|
// src/siwe.ts
|
|
2105
|
+
var SIWE_FORBIDDEN_CHARS = /[\u061c\u200b-\u200f\u202a-\u202e\u2060-\u2064\u2066-\u2069\ufeff]/;
|
|
1634
2106
|
var HEADER_LINE = /^(?<domain>[^\n]+?) wants you to sign in with your Ethereum account:(?:\n|$)/;
|
|
1635
2107
|
var ADDRESS_LINE = /^0x[0-9a-fA-F]{40}$/;
|
|
1636
2108
|
var FIELD = /^([A-Za-z ]+): (.*)$/;
|
|
@@ -1734,12 +2206,20 @@ function siweProblems(m, ctx) {
|
|
|
1734
2206
|
}
|
|
1735
2207
|
return out;
|
|
1736
2208
|
}
|
|
2209
|
+
function siweCrossSite(m, ctx) {
|
|
2210
|
+
const site = originWhere(ctx.origin) ?? (ctx.via?.verify === "VALID" && !ctx.via.scam ? whereOf(ctx.via.url) : null);
|
|
2211
|
+
if (!site) return null;
|
|
2212
|
+
const domain = whereOf(m.domain);
|
|
2213
|
+
return !domain || domain.host !== site.host ? { domain: m.domain, origin: `${site.scheme}://${site.host}` } : null;
|
|
2214
|
+
}
|
|
1737
2215
|
function describeSiwe(text, ctx) {
|
|
1738
2216
|
const p = parseLoose(text);
|
|
1739
2217
|
if (!p) return null;
|
|
1740
2218
|
const problems = siweProblems(p.message, ctx);
|
|
1741
|
-
|
|
1742
|
-
|
|
2219
|
+
const cross = siweCrossSite(p.message, ctx);
|
|
2220
|
+
const crossSite = cross ? { crossSite: cross } : {};
|
|
2221
|
+
if (p.missing.length === 0) return { message: p.message, problems, ...crossSite };
|
|
2222
|
+
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
2223
|
}
|
|
1744
2224
|
|
|
1745
2225
|
// src/signing.ts
|
|
@@ -1747,18 +2227,30 @@ var safeNonce = (n) => {
|
|
|
1747
2227
|
if (n < 0n || n > BigInt(Number.MAX_SAFE_INTEGER)) throw new Error(`The RPC returned an impossible nonce (${n}).`);
|
|
1748
2228
|
return Number(n.toString());
|
|
1749
2229
|
};
|
|
2230
|
+
var MAX_FEE_REASKS = 3;
|
|
1750
2231
|
var ADDRESS = /^0x[0-9a-fA-F]{40}$/;
|
|
1751
|
-
var HEX = /^0x([0-9a-fA-F]{2})*$/;
|
|
1752
2232
|
var lc = (s) => s.toLowerCase();
|
|
2233
|
+
var INVISIBLE = /[\p{Cf}\u2028\u2029\u115f\u1160\u3164\uffa0\x00-\x08\x0b\x0c\x0e-\x1f\x7f-\x9f]/gu;
|
|
2234
|
+
function revealHidden(text) {
|
|
2235
|
+
let hidden = false;
|
|
2236
|
+
const out = text.replace(INVISIBLE, (c) => {
|
|
2237
|
+
hidden = true;
|
|
2238
|
+
return `\u27E8U+${c.codePointAt(0).toString(16).toUpperCase().padStart(4, "0")}\u27E9`;
|
|
2239
|
+
});
|
|
2240
|
+
return { text: out, hidden };
|
|
2241
|
+
}
|
|
1753
2242
|
function messageText(value) {
|
|
1754
|
-
|
|
1755
|
-
|
|
2243
|
+
return shownMessage(value).text;
|
|
2244
|
+
}
|
|
2245
|
+
function shownMessage(value) {
|
|
2246
|
+
if (typeof value !== "string") return { text: "", hidden: false };
|
|
2247
|
+
if (!HEX.test(value)) return revealHidden(value);
|
|
1756
2248
|
try {
|
|
1757
2249
|
const bytes = Uint8Array.from(value.slice(2).match(/../g) ?? [], (b) => Number.parseInt(b, 16));
|
|
1758
2250
|
const s = new TextDecoder("utf-8", { fatal: true }).decode(bytes);
|
|
1759
|
-
return /^[\x09\x0a\x0d\x20-\x7e
|
|
2251
|
+
return /^[\x09\x0a\x0d\x20-\x7e\u00a0-\uffff]*$/.test(s) ? revealHidden(s) : { text: value, hidden: false };
|
|
1760
2252
|
} catch {
|
|
1761
|
-
return value;
|
|
2253
|
+
return { text: value, hidden: false };
|
|
1762
2254
|
}
|
|
1763
2255
|
}
|
|
1764
2256
|
function decodedMessage(value) {
|
|
@@ -1782,11 +2274,16 @@ function classifySignRequest(input) {
|
|
|
1782
2274
|
if (ours(msg) && !ours(addr)) [msg, addr] = [addr, msg];
|
|
1783
2275
|
if (!ours(addr)) return err(RELAY_ERRORS.unauthorized, "personal_sign for an address this wallet does not hold.");
|
|
1784
2276
|
if (typeof msg !== "string") return err(RELAY_ERRORS.invalidParams, "personal_sign expects a message string.");
|
|
1785
|
-
const
|
|
2277
|
+
const thirtyTwo = msg.length <= BLIND_HASH_MAX_CHARS && personalSignBytes(msg).length === 32;
|
|
2278
|
+
const blindHashForm = !thirtyTwo ? void 0 : HEX.test(msg) ? "hex" : "text";
|
|
2279
|
+
if (blindHashForm === "hex" && !input.allowBlindHash) return err(RELAY_ERRORS.unsupportedMethod, 'This site asks you to sign a raw 32-byte hash (blind signing). It can be a swap order or a transfer (e.g. CoW Protocol) paying someone else, so this wallet refuses it. "Allow blind hash signatures" in Options turns on a review for such requests.');
|
|
2280
|
+
const shown = shownMessage(msg);
|
|
2281
|
+
const text = shown.text;
|
|
1786
2282
|
const signed = decodedMessage(msg) ?? text;
|
|
1787
2283
|
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
2284
|
if (siwe && HIDDEN_CONTROL.test(signed)) return err(RELAY_ERRORS.invalidParams, "This sign-in message contains hidden control characters \u2014 refused.");
|
|
1789
|
-
|
|
2285
|
+
if (siwe && SIWE_FORBIDDEN_CHARS.test(signed)) return err(RELAY_ERRORS.invalidParams, "This sign-in message contains invisible or text-direction characters \u2014 refused.");
|
|
2286
|
+
return { kind: "sign-message", method, account: checksum(addr), text, raw: msg, ...siwe ? { siwe } : {}, ...shown.hidden ? { hiddenChars: true } : {}, ...blindHashForm ? { blindHash: true, blindHashForm } : {} };
|
|
1790
2287
|
}
|
|
1791
2288
|
if (method === "eth_signTypedData_v4" || method === "eth_signTypedData_v3" || method === "eth_signTypedData") {
|
|
1792
2289
|
const [addr, data] = params;
|
|
@@ -1804,7 +2301,8 @@ function classifySignRequest(input) {
|
|
|
1804
2301
|
if (cid !== void 0 && cid !== null && Number(cid) !== input.network.chainId) {
|
|
1805
2302
|
return err(RELAY_ERRORS.chainDisconnected, `Typed data is for chain ${String(cid)}; this site is on ${input.network.name} (${input.network.chainId}).`);
|
|
1806
2303
|
}
|
|
1807
|
-
|
|
2304
|
+
const shown = revealHidden(JSON.stringify(parsed, bigintSafe, 2));
|
|
2305
|
+
return { kind: "sign-typed-data", method, account: checksum(addr), text: shown.text, typedData: parsed, ...shown.hidden ? { hiddenChars: true } : {} };
|
|
1808
2306
|
}
|
|
1809
2307
|
if (method === "eth_sendTransaction") {
|
|
1810
2308
|
const tx = params[0] ?? {};
|
|
@@ -1853,16 +2351,33 @@ function overpriced(shown, fresh) {
|
|
|
1853
2351
|
if (shown.maxFeePerGas !== void 0 && fresh.maxFeePerGas !== void 0) return big(shown.maxFeePerGas) > STALE_FEE_FACTOR * big(fresh.maxFeePerGas);
|
|
1854
2352
|
return false;
|
|
1855
2353
|
}
|
|
2354
|
+
function gasLimitExcessive(limit, estimate) {
|
|
2355
|
+
const a = estimate * 3n / 2n;
|
|
2356
|
+
const b = estimate + 50000n;
|
|
2357
|
+
return limit > (a > b ? a : b);
|
|
2358
|
+
}
|
|
1856
2359
|
async function prepareTransaction(deps, network, from, tx) {
|
|
1857
2360
|
const call = { from, to: tx.to, value: hex(BigInt(tx.value)), ...tx.data ? { data: tx.data } : {} };
|
|
2361
|
+
const estimate = () => deps.rpc(network, "eth_estimateGas", [call]);
|
|
1858
2362
|
const [nonceHex, block, gasHex] = await Promise.all([
|
|
1859
2363
|
deps.rpc(network, "eth_getTransactionCount", [from, "pending"]),
|
|
1860
2364
|
deps.rpc(network, "eth_getBlockByNumber", ["latest", false]),
|
|
1861
|
-
tx.gas ?
|
|
2365
|
+
tx.gas ? estimate().then((v) => v, () => null) : estimate()
|
|
1862
2366
|
]);
|
|
1863
|
-
const
|
|
1864
|
-
|
|
1865
|
-
|
|
2367
|
+
const nonce = safeNonce(big(nonceHex));
|
|
2368
|
+
if (!tx.gas) {
|
|
2369
|
+
const estimated = big(gasHex);
|
|
2370
|
+
return withFees(network.chainId, nonce, (estimated * 120n + 99n) / 100n, await readFees(deps, network, block));
|
|
2371
|
+
}
|
|
2372
|
+
const limit = big(tx.gas);
|
|
2373
|
+
const prepared = withFees(network.chainId, nonce, limit, await readFees(deps, network, block));
|
|
2374
|
+
let est = null;
|
|
2375
|
+
try {
|
|
2376
|
+
est = typeof gasHex === "string" ? big(gasHex) : null;
|
|
2377
|
+
} catch {
|
|
2378
|
+
est = null;
|
|
2379
|
+
}
|
|
2380
|
+
return est !== null && est > 0n ? { ...prepared, gasCheck: { estimate: hex(est), excessive: gasLimitExcessive(limit, est) } } : prepared;
|
|
1866
2381
|
}
|
|
1867
2382
|
function createSignHandler(deps) {
|
|
1868
2383
|
const tails = /* @__PURE__ */ new Map();
|
|
@@ -1878,7 +2393,8 @@ function createSignHandler(deps) {
|
|
|
1878
2393
|
};
|
|
1879
2394
|
return async ({ origin, network, method, params, accounts }) => {
|
|
1880
2395
|
const via = deps.approvals.viaFor(origin);
|
|
1881
|
-
const
|
|
2396
|
+
const allowBlindHash = method === "personal_sign" && deps.allowBlindHash ? await deps.allowBlindHash() : false;
|
|
2397
|
+
const info = classifySignRequest({ network, method, params, accounts, origin, ...via ? { via } : {}, ...allowBlindHash ? { allowBlindHash: true } : {} });
|
|
1882
2398
|
if ("code" in info) throw RouterError.from(info);
|
|
1883
2399
|
const prepared = info.kind === "send-transaction" ? await prepareTransaction(deps, network, info.account, info.tx) : void 0;
|
|
1884
2400
|
const preview = deps.preview ? await deps.preview({ network, request: info, accounts }).catch(() => null) : null;
|
|
@@ -1888,14 +2404,20 @@ function createSignHandler(deps) {
|
|
|
1888
2404
|
const key = `${network.caip2}|${lc(info.account)}`;
|
|
1889
2405
|
return serial(key, async () => {
|
|
1890
2406
|
let shown = prepared;
|
|
2407
|
+
let reasks = 0;
|
|
1891
2408
|
for (; ; ) {
|
|
1892
2409
|
const [nonceHex, fees] = await Promise.all([deps.rpc(network, "eth_getTransactionCount", [info.account, "pending"]), readFees(deps, network)]);
|
|
1893
2410
|
const local = nextNonce.get(key) ?? 0;
|
|
1894
2411
|
const nodeNonce = safeNonce(big(nonceHex));
|
|
1895
2412
|
const nonce = nodeNonce > local ? nodeNonce : local;
|
|
1896
2413
|
if (outgrown(shown, fees)) {
|
|
1897
|
-
|
|
1898
|
-
|
|
2414
|
+
const next = withFees(network.chainId, nonce, big(shown.gasLimit), fees);
|
|
2415
|
+
const ceiling = deps.feeCeiling?.(network);
|
|
2416
|
+
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.`);
|
|
2417
|
+
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.");
|
|
2418
|
+
reasks++;
|
|
2419
|
+
await deps.approvals.request({ origin, chain: network.caip2, kind: info.kind, payload: { ...payload, prepared: next, repriced: true, repricedFrom: shown.maxFee } }, { followUp: true });
|
|
2420
|
+
shown = next;
|
|
1899
2421
|
continue;
|
|
1900
2422
|
}
|
|
1901
2423
|
const fresh = withFees(network.chainId, nonce, big(shown.gasLimit), fees);
|
|
@@ -1925,7 +2447,7 @@ function createSignHandler(deps) {
|
|
|
1925
2447
|
try {
|
|
1926
2448
|
switch (info.kind) {
|
|
1927
2449
|
case "sign-message":
|
|
1928
|
-
return await handle.signer.signMessage(
|
|
2450
|
+
return await handle.signer.signMessage(personalSignInput(info.raw));
|
|
1929
2451
|
case "sign-typed-data":
|
|
1930
2452
|
return await handle.signer.signTypedData(info.typedData);
|
|
1931
2453
|
}
|
|
@@ -2017,7 +2539,14 @@ async function quoteRoutes(providers, params, options = {}) {
|
|
|
2017
2539
|
candidates.push(p);
|
|
2018
2540
|
}
|
|
2019
2541
|
const pending = new Set(candidates.map((p) => p.name));
|
|
2020
|
-
const
|
|
2542
|
+
const depositAddressRoute = options.depositAddressRoute;
|
|
2543
|
+
const rankOptions = {
|
|
2544
|
+
...params.slippageBps !== void 0 ? { slippageBps: params.slippageBps } : {},
|
|
2545
|
+
...options.depositAddressProviders ? { depositAddressProviders: options.depositAddressProviders } : {},
|
|
2546
|
+
...depositAddressRoute ? { depositAddressRoute: (q) => depositAddressRoute(q, params) } : {}
|
|
2547
|
+
};
|
|
2548
|
+
const { slippageBps: _slippage, ...shapeParams } = params;
|
|
2549
|
+
const view = () => ({ routes: rank(Object.values(quotes), rankOptions), pending: [...pending], refusals: { ...refusals }, quotes: { ...quotes } });
|
|
2021
2550
|
const emit = () => options.onUpdate?.(view());
|
|
2022
2551
|
emit();
|
|
2023
2552
|
await Promise.all(
|
|
@@ -2032,7 +2561,7 @@ async function quoteRoutes(providers, params, options = {}) {
|
|
|
2032
2561
|
const q = await Promise.race([p.getQuote({ ...params, dry: true, onRefusal: (r) => {
|
|
2033
2562
|
refusal = r;
|
|
2034
2563
|
} }, ctrl.signal), aborted]);
|
|
2035
|
-
if (q && (0, import_protocol_swap2_core.validQuote)(q, { ...
|
|
2564
|
+
if (q && (0, import_protocol_swap2_core.validQuote)(q, { ...shapeParams, dry: true }, p.name)) quotes[p.name] = q;
|
|
2036
2565
|
else refusals[p.name] = refusal ?? (q ? "returned an invalid quote" : "no route for this pair right now");
|
|
2037
2566
|
} catch (e) {
|
|
2038
2567
|
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 +2575,50 @@ async function quoteRoutes(providers, params, options = {}) {
|
|
|
2046
2575
|
);
|
|
2047
2576
|
return view();
|
|
2048
2577
|
}
|
|
2049
|
-
function
|
|
2578
|
+
function gapBps(dest, min) {
|
|
2579
|
+
if (dest <= 0n || min >= dest) return 0;
|
|
2580
|
+
return Number((((dest - min) * 10000n + dest - 1n) / dest).toString());
|
|
2581
|
+
}
|
|
2582
|
+
function rank(quotes, options = {}) {
|
|
2050
2583
|
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
2584
|
const fastest = sorted.length ? sorted.reduce((f, q) => q.etaSeconds < f.etaSeconds ? q : f) : null;
|
|
2052
|
-
|
|
2585
|
+
const depositProviders = new Set(options.depositAddressProviders ?? []);
|
|
2586
|
+
const paysAssignedAddress = (q) => {
|
|
2587
|
+
if (depositProviders.has(q.provider)) return true;
|
|
2588
|
+
if (!options.depositAddressRoute) return false;
|
|
2589
|
+
try {
|
|
2590
|
+
return options.depositAddressRoute(q) === true;
|
|
2591
|
+
} catch {
|
|
2592
|
+
return true;
|
|
2593
|
+
}
|
|
2594
|
+
};
|
|
2595
|
+
const assigned = new Map(sorted.map((q) => [q, paysAssignedAddress(q)]));
|
|
2596
|
+
const s = options.slippageBps;
|
|
2597
|
+
const warningsOf = (q) => {
|
|
2598
|
+
const w = [];
|
|
2599
|
+
if (s !== void 0 && Number.isInteger(s) && s >= 0 && s < 1e4) {
|
|
2600
|
+
const dest = BigInt(q.destAmount);
|
|
2601
|
+
const min = BigInt(q.minDestAmount);
|
|
2602
|
+
if (min < (0, import_protocol_swap2_core.slippageFloor)(dest, s)) w.push({ kind: "min-below-slippage", slippageBps: s, minGapBps: gapBps(dest, min) });
|
|
2603
|
+
}
|
|
2604
|
+
if (assigned.get(q)) w.push({ kind: "deposit-address" });
|
|
2605
|
+
return w;
|
|
2606
|
+
};
|
|
2607
|
+
const warned = new Map(sorted.map((q) => [q, warningsOf(q)]));
|
|
2608
|
+
const verifiableExists = sorted.some((q) => !assigned.get(q));
|
|
2609
|
+
const best = sorted.find((q) => {
|
|
2610
|
+
const w = warned.get(q);
|
|
2611
|
+
return !w.some((x) => x.kind === "min-below-slippage") && !(w.some((x) => x.kind === "deposit-address") && verifiableExists);
|
|
2612
|
+
});
|
|
2613
|
+
const ordered = best ? [best, ...sorted.filter((q) => q !== best)] : sorted;
|
|
2614
|
+
return ordered.map((q) => {
|
|
2053
2615
|
const poolFee = q.providerData?.poolFee;
|
|
2054
2616
|
const feeNote = typeof poolFee === "string" && poolFee && q.feeUsd === 0 ? poolFee === "pools" ? "pool fees in price" : `pool ${poolFee} in price` : void 0;
|
|
2055
2617
|
const tags = [];
|
|
2056
|
-
if (
|
|
2618
|
+
if (q === best) tags.push("best");
|
|
2057
2619
|
if (fastest && q.provider === fastest.provider) tags.push("fastest");
|
|
2058
|
-
|
|
2620
|
+
const warnings = warned.get(q);
|
|
2621
|
+
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
2622
|
});
|
|
2060
2623
|
}
|
|
2061
2624
|
var FLOOR_TOLERANCE_BPS = 50n;
|
|
@@ -2074,18 +2637,21 @@ function depositOf(s, approvals) {
|
|
|
2074
2637
|
if (!s.data.startsWith(TRANSFER_SELECTOR) || s.data.length < 10 + 64) return void 0;
|
|
2075
2638
|
return `0x${s.data.slice(10 + 24, 10 + 64)}`;
|
|
2076
2639
|
}
|
|
2640
|
+
var pct = (bps) => `${Number((bps / 100).toFixed(2))}%`;
|
|
2077
2641
|
function swapApprovalPayload(plan, meta) {
|
|
2078
2642
|
const isOwn = meta.own.some((a) => lc3(a) === lc3(plan.recipient));
|
|
2643
|
+
const assigned = meta.depositAddressAssignedByProvider === true;
|
|
2079
2644
|
const checks = [{ status: "ok", label: `Built by the ${meta.providerLabel} module` }];
|
|
2645
|
+
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
2646
|
const s = plan.source;
|
|
2081
2647
|
let source;
|
|
2082
2648
|
if (s.kind === "call") {
|
|
2083
2649
|
const deposit = depositOf(s, plan.approvals.length);
|
|
2084
2650
|
source = { kind: "call", to: s.to, ...s.data ? { data: s.data } : {}, value: s.value.toString(), ...deposit ? { deposit } : {} };
|
|
2085
|
-
checks.push(deposit
|
|
2651
|
+
checks.push(deposit || assigned ? depositCheck : { status: "ok", label: `Calls the ${meta.providerLabel} contract` });
|
|
2086
2652
|
} else if (s.kind === "token-transfer") {
|
|
2087
2653
|
source = { kind: "token-transfer", token: s.token, recipient: s.recipient, amount: s.amount.toString(), ...s.memo ? { memo: s.memo } : {} };
|
|
2088
|
-
checks.push(
|
|
2654
|
+
checks.push(depositCheck);
|
|
2089
2655
|
} else if (s.kind === "serialized") {
|
|
2090
2656
|
source = { kind: "serialized" };
|
|
2091
2657
|
checks.push({ status: "ok", label: "Balance changes are proven before signing (SDK guard)" });
|
|
@@ -2096,7 +2662,14 @@ function swapApprovalPayload(plan, meta) {
|
|
|
2096
2662
|
}
|
|
2097
2663
|
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
2664
|
if (plan.approvals.length > 1) checks.push({ status: "ok", label: `${plan.approvals.length} approvals (reset to 0 first)` });
|
|
2099
|
-
checks.push(isOwn ? { status: "
|
|
2665
|
+
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" });
|
|
2666
|
+
let slippage;
|
|
2667
|
+
const sb = meta.slippageBps;
|
|
2668
|
+
if (sb !== void 0 && Number.isInteger(sb) && sb >= 0 && sb < 1e4) {
|
|
2669
|
+
const below = plan.minDestAmount < (0, import_protocol_swap2_core.slippageFloor)(plan.destAmount, sb);
|
|
2670
|
+
slippage = { bps: sb, minGapBps: gapBps(plan.destAmount, plan.minDestAmount), below };
|
|
2671
|
+
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` });
|
|
2672
|
+
}
|
|
2100
2673
|
return {
|
|
2101
2674
|
provider: plan.provider,
|
|
2102
2675
|
providerLabel: meta.providerLabel,
|
|
@@ -2113,7 +2686,9 @@ function swapApprovalPayload(plan, meta) {
|
|
|
2113
2686
|
validUntilMs: plan.validUntilMs,
|
|
2114
2687
|
approvals: plan.approvals.map((a) => ({ token: a.token, spender: a.spender, amount: a.amount.toString() })),
|
|
2115
2688
|
source,
|
|
2116
|
-
checks
|
|
2689
|
+
checks,
|
|
2690
|
+
...slippage ? { slippage } : {},
|
|
2691
|
+
...assigned ? { depositAssignedByProvider: true } : {}
|
|
2117
2692
|
};
|
|
2118
2693
|
}
|
|
2119
2694
|
function parseTypedOrder(json) {
|
|
@@ -2564,6 +3139,10 @@ var RecipientBook = class {
|
|
|
2564
3139
|
await this.storage.set(s);
|
|
2565
3140
|
this._state = s;
|
|
2566
3141
|
}
|
|
3142
|
+
/** Forget the cached book: the next read asks storage again (e.g. a storage that only answers once the wallet is unlocked). */
|
|
3143
|
+
reload() {
|
|
3144
|
+
this._state = null;
|
|
3145
|
+
}
|
|
2567
3146
|
/** `known` → the address is one the user paid before; `lookalike` → it imitates one. */
|
|
2568
3147
|
async check(namespace, address) {
|
|
2569
3148
|
const list = await this.list(namespace);
|
|
@@ -2571,4 +3150,236 @@ var RecipientBook = class {
|
|
|
2571
3150
|
return { known, lookalike: known ? null : lookalikeOf(address, list) };
|
|
2572
3151
|
}
|
|
2573
3152
|
};
|
|
3153
|
+
|
|
3154
|
+
// src/mac-storage.ts
|
|
3155
|
+
var import_core5 = require("@anywalletkit/core");
|
|
3156
|
+
var STORAGE_MAC_FIELD = "anywalletkit.mac/1";
|
|
3157
|
+
var STORAGE_VERSION_FIELD = "anywalletkit.ver/1";
|
|
3158
|
+
var HKDF_INFO2 = "anywalletkit/storage-mac/1";
|
|
3159
|
+
var KEY_BYTES = 32;
|
|
3160
|
+
async function deriveStorageMacKey(dek, crypto2 = globalThis.crypto) {
|
|
3161
|
+
if (dek.length !== 32) throw new VaultError("Vault key must be 32 bytes.", "invalid-input");
|
|
3162
|
+
const ikm = await withSecretBuffer(dek, (b) => crypto2.subtle.importKey("raw", b, "HKDF", false, ["deriveBits"]));
|
|
3163
|
+
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);
|
|
3164
|
+
return new Uint8Array(bits);
|
|
3165
|
+
}
|
|
3166
|
+
function macJsonStorage(inner, options) {
|
|
3167
|
+
const { label, versions } = options;
|
|
3168
|
+
if (versions && label.includes("\n")) throw new VaultError("A versioned store label has no line break.", "invalid-input");
|
|
3169
|
+
const readWithoutKey = options.readWithoutKey ?? "unverified";
|
|
3170
|
+
const writeWithoutKey = options.writeWithoutKey ?? "unsigned";
|
|
3171
|
+
const withKey = async (given, fn) => {
|
|
3172
|
+
const key = given ? Uint8Array.from(given) : await options.key();
|
|
3173
|
+
try {
|
|
3174
|
+
return await fn(key);
|
|
3175
|
+
} finally {
|
|
3176
|
+
if (key) (0, import_core5.zeroize)(key);
|
|
3177
|
+
}
|
|
3178
|
+
};
|
|
3179
|
+
const stored = async (key) => {
|
|
3180
|
+
const raw = await inner.get();
|
|
3181
|
+
if (raw === null || raw === void 0) return { state: "empty", value: null };
|
|
3182
|
+
const envelope = isStored(raw);
|
|
3183
|
+
const value = envelope ? raw.value : raw;
|
|
3184
|
+
if (!envelope || raw[STORAGE_MAC_FIELD] === null) return { state: "unsigned", value };
|
|
3185
|
+
if (!key) return { state: "no-key", value };
|
|
3186
|
+
if (!versions) return { state: await verify(key, label, value, raw[STORAGE_MAC_FIELD]) ? "ok" : "invalid", value };
|
|
3187
|
+
const version = STORAGE_VERSION_FIELD in raw ? raw[STORAGE_VERSION_FIELD] : 0;
|
|
3188
|
+
if (version !== 0 && !isVersion(version)) return { state: "invalid", value };
|
|
3189
|
+
if (!await verify(key, label, value, raw[STORAGE_MAC_FIELD], version || void 0)) return { state: "invalid", value };
|
|
3190
|
+
return { state: "ok", value, version };
|
|
3191
|
+
};
|
|
3192
|
+
const put = async (key, value, version) => {
|
|
3193
|
+
const mac = await sign(key, label, value, version);
|
|
3194
|
+
await inner.set(version === void 0 ? { [STORAGE_MAC_FIELD]: mac, value } : { [STORAGE_MAC_FIELD]: mac, [STORAGE_VERSION_FIELD]: version, value });
|
|
3195
|
+
};
|
|
3196
|
+
const seen = async (key) => {
|
|
3197
|
+
const r = await stored(key);
|
|
3198
|
+
return r.state === "ok" ? r.version ?? 0 : 0;
|
|
3199
|
+
};
|
|
3200
|
+
const read = (given) => withKey(given, async (key) => {
|
|
3201
|
+
if (!versions || !key) return stored(key);
|
|
3202
|
+
return versions.exclusive(async (ledger) => {
|
|
3203
|
+
const r = await stored(key);
|
|
3204
|
+
if (r.state === "empty") return await ledger.check(label, null, key) === "ok" ? r : { state: "stale", value: null };
|
|
3205
|
+
if (r.state !== "ok") return r;
|
|
3206
|
+
return await ledger.check(label, r.version ?? 0, key) === "ok" ? r : { ...r, state: "stale" };
|
|
3207
|
+
});
|
|
3208
|
+
});
|
|
3209
|
+
const write = (value, given) => withKey(given, async (key) => {
|
|
3210
|
+
if (!key) throw new VaultError("The wallet is locked: unlock it to change this.", "locked");
|
|
3211
|
+
if (!versions) return put(key, value);
|
|
3212
|
+
await versions.exclusive((ledger) => ledger.advance(label, key, () => seen(key), (version) => put(key, value, version)));
|
|
3213
|
+
});
|
|
3214
|
+
return {
|
|
3215
|
+
read,
|
|
3216
|
+
write,
|
|
3217
|
+
async get() {
|
|
3218
|
+
const r = await read();
|
|
3219
|
+
if (r.state === "ok") return r.value;
|
|
3220
|
+
if (r.state === "no-key" && readWithoutKey === "unverified") return r.value;
|
|
3221
|
+
return null;
|
|
3222
|
+
},
|
|
3223
|
+
async set(value) {
|
|
3224
|
+
await withKey(void 0, async (key) => {
|
|
3225
|
+
if (key) return write(value, key);
|
|
3226
|
+
if (writeWithoutKey === "refuse") throw new VaultError("The wallet is locked: unlock it to change this.", "locked");
|
|
3227
|
+
await inner.set({ [STORAGE_MAC_FIELD]: null, value });
|
|
3228
|
+
});
|
|
3229
|
+
},
|
|
3230
|
+
async rekey(oldKey, newKey) {
|
|
3231
|
+
if (!versions) {
|
|
3232
|
+
const r = await read(oldKey);
|
|
3233
|
+
if (r.state === "ok") await write(r.value, newKey);
|
|
3234
|
+
return r.state;
|
|
3235
|
+
}
|
|
3236
|
+
return versions.exclusive(async (ledger) => {
|
|
3237
|
+
const r = await stored(oldKey);
|
|
3238
|
+
if (r.state === "empty") return await ledger.check(label, null, newKey) === "ok" ? "empty" : "stale";
|
|
3239
|
+
if (r.state !== "ok") return r.state;
|
|
3240
|
+
const version = r.version ?? 0;
|
|
3241
|
+
if (await ledger.check(label, version, newKey) !== "ok") return "stale";
|
|
3242
|
+
if (version > 0) await put(newKey, r.value, version);
|
|
3243
|
+
else await ledger.advance(label, newKey, async () => 0, (v) => put(newKey, r.value, v));
|
|
3244
|
+
return "ok";
|
|
3245
|
+
});
|
|
3246
|
+
}
|
|
3247
|
+
};
|
|
3248
|
+
}
|
|
3249
|
+
function isStored(raw) {
|
|
3250
|
+
if (typeof raw !== "object" || raw === null || !(STORAGE_MAC_FIELD in raw) || !("value" in raw)) return false;
|
|
3251
|
+
const mac = raw[STORAGE_MAC_FIELD];
|
|
3252
|
+
return mac === null || typeof mac === "string";
|
|
3253
|
+
}
|
|
3254
|
+
async function hmacKey(key, usage) {
|
|
3255
|
+
if (key.length !== KEY_BYTES) throw new VaultError("A storage MAC key is 32 bytes.", "invalid-input");
|
|
3256
|
+
return withSecretBuffer(key, (b) => globalThis.crypto.subtle.importKey("raw", b, { name: "HMAC", hash: "SHA-256" }, false, [usage]));
|
|
3257
|
+
}
|
|
3258
|
+
function macInput(label, value, version) {
|
|
3259
|
+
return toArrayBuffer(utf8.encode(version === void 0 ? `${label}
|
|
3260
|
+
${canonicalJson(value)}` : `${label}
|
|
3261
|
+
${version}
|
|
3262
|
+
${canonicalJson(value)}`));
|
|
3263
|
+
}
|
|
3264
|
+
async function sign(key, label, value, version) {
|
|
3265
|
+
return toBase64(new Uint8Array(await globalThis.crypto.subtle.sign("HMAC", await hmacKey(key, "sign"), macInput(label, value, version))));
|
|
3266
|
+
}
|
|
3267
|
+
async function verify(key, label, value, mac, version) {
|
|
3268
|
+
let bytes;
|
|
3269
|
+
try {
|
|
3270
|
+
bytes = fromBase64(mac);
|
|
3271
|
+
} catch {
|
|
3272
|
+
return false;
|
|
3273
|
+
}
|
|
3274
|
+
return globalThis.crypto.subtle.verify("HMAC", await hmacKey(key, "verify"), toArrayBuffer(bytes), macInput(label, value, version));
|
|
3275
|
+
}
|
|
3276
|
+
var isVersion = (x) => typeof x === "number" && Number.isSafeInteger(x) && x > 0;
|
|
3277
|
+
function canonicalJson(value) {
|
|
3278
|
+
return JSON.stringify(sortKeys(JSON.parse(JSON.stringify(value ?? null))));
|
|
3279
|
+
}
|
|
3280
|
+
function sortKeys(x) {
|
|
3281
|
+
if (Array.isArray(x)) return x.map(sortKeys);
|
|
3282
|
+
if (x === null || typeof x !== "object") return x;
|
|
3283
|
+
const out = {};
|
|
3284
|
+
for (const k of Object.keys(x).sort()) out[k] = sortKeys(x[k]);
|
|
3285
|
+
return out;
|
|
3286
|
+
}
|
|
3287
|
+
var LEDGER_LABEL = "anywalletkit/storage-versions/1";
|
|
3288
|
+
function storageVersionLedger(options) {
|
|
3289
|
+
const label = options.label ?? LEDGER_LABEL;
|
|
3290
|
+
const acceptUnversioned = options.acceptUnversioned ?? false;
|
|
3291
|
+
const now = options.now ?? Date.now;
|
|
3292
|
+
let tail = Promise.resolve();
|
|
3293
|
+
const queue = (fn) => {
|
|
3294
|
+
const run = tail.then(fn, fn);
|
|
3295
|
+
tail = run.then(() => void 0, () => void 0);
|
|
3296
|
+
return run;
|
|
3297
|
+
};
|
|
3298
|
+
const high = async () => {
|
|
3299
|
+
const v = await options.mark.get();
|
|
3300
|
+
return isVersion(v) ? v : 0;
|
|
3301
|
+
};
|
|
3302
|
+
const parse = async (raw, key) => {
|
|
3303
|
+
if (!isStored(raw) || raw[STORAGE_MAC_FIELD] === null) return null;
|
|
3304
|
+
const epoch = raw[STORAGE_VERSION_FIELD];
|
|
3305
|
+
const v = raw.value;
|
|
3306
|
+
if (!isVersion(epoch) || typeof v !== "object" || v === null || !isVersion(v.base) || typeof v.sealed !== "boolean") return null;
|
|
3307
|
+
if (typeof v.versions !== "object" || v.versions === null || !Object.values(v.versions).every(isVersion)) return null;
|
|
3308
|
+
if (!await verify(key, label, v, raw[STORAGE_MAC_FIELD], epoch)) return null;
|
|
3309
|
+
return { epoch, base: v.base, sealed: v.sealed, versions: { ...v.versions } };
|
|
3310
|
+
};
|
|
3311
|
+
const load = async (key) => {
|
|
3312
|
+
const mark = await high();
|
|
3313
|
+
const raw = await options.manifest.get();
|
|
3314
|
+
if (raw === null || raw === void 0) return { state: mark > 0 ? "stale" : "empty" };
|
|
3315
|
+
const manifest = await parse(raw, key);
|
|
3316
|
+
if (!manifest) return { state: "invalid" };
|
|
3317
|
+
if (manifest.epoch < mark) return { state: "stale", manifest };
|
|
3318
|
+
if (manifest.epoch > mark) await options.mark.set(manifest.epoch);
|
|
3319
|
+
return { state: "ok", manifest };
|
|
3320
|
+
};
|
|
3321
|
+
const store = async (m, key) => {
|
|
3322
|
+
const value = { base: m.base, sealed: m.sealed, versions: m.versions };
|
|
3323
|
+
await options.manifest.set({ [STORAGE_MAC_FIELD]: await sign(key, label, value, m.epoch), [STORAGE_VERSION_FIELD]: m.epoch, value });
|
|
3324
|
+
if (m.epoch > await high()) await options.mark.set(m.epoch);
|
|
3325
|
+
};
|
|
3326
|
+
const fresh = async (sealed, prior, key) => {
|
|
3327
|
+
const floor = Math.max(await high(), Math.floor(now()), prior ? Math.max(prior.epoch, prior.base, ...Object.values(prior.versions)) : 0);
|
|
3328
|
+
if (!Number.isSafeInteger(floor + 2)) throw new VaultError("Storage version out of range.", "invalid-input");
|
|
3329
|
+
const m = { epoch: floor + 1, base: floor + 1, sealed, versions: {} };
|
|
3330
|
+
await store(m, key);
|
|
3331
|
+
return m;
|
|
3332
|
+
};
|
|
3333
|
+
const record = (m, name, version, key) => store({ ...m, epoch: m.epoch + 1, versions: { ...m.versions, [name]: version } }, key);
|
|
3334
|
+
const named = (name) => {
|
|
3335
|
+
if (name === label || name.includes("\n")) throw new VaultError("Not a store label.", "invalid-input");
|
|
3336
|
+
};
|
|
3337
|
+
const ops = {
|
|
3338
|
+
async check(name, version, key) {
|
|
3339
|
+
named(name);
|
|
3340
|
+
const l = await load(key);
|
|
3341
|
+
if (l.state === "empty") return version === null || version === 0 && acceptUnversioned ? "ok" : "stale";
|
|
3342
|
+
if (l.state !== "ok") return "stale";
|
|
3343
|
+
const m = l.manifest;
|
|
3344
|
+
const listed = m.versions[name];
|
|
3345
|
+
if (listed === void 0 && version === null) return m.sealed ? "stale" : "ok";
|
|
3346
|
+
if (listed === void 0 && version === 0) return !m.sealed && acceptUnversioned ? "ok" : "stale";
|
|
3347
|
+
if (version === null || version === 0) return "stale";
|
|
3348
|
+
if (version === listed) return "ok";
|
|
3349
|
+
if (version !== (listed ?? m.base) + 1) return "stale";
|
|
3350
|
+
await record(m, name, version, key).catch(() => void 0);
|
|
3351
|
+
return "ok";
|
|
3352
|
+
},
|
|
3353
|
+
async advance(name, key, seen, write) {
|
|
3354
|
+
named(name);
|
|
3355
|
+
const l = await load(key);
|
|
3356
|
+
const before = await seen();
|
|
3357
|
+
const m = l.state === "ok" ? l.manifest : await fresh(l.state !== "empty" || before > 0, l.manifest, key);
|
|
3358
|
+
const version = Math.max(m.versions[name] ?? m.base, before) + 1;
|
|
3359
|
+
if (!Number.isSafeInteger(version)) throw new VaultError("Storage version out of range.", "invalid-input");
|
|
3360
|
+
await write(version);
|
|
3361
|
+
await record(m, name, version, key);
|
|
3362
|
+
return version;
|
|
3363
|
+
}
|
|
3364
|
+
};
|
|
3365
|
+
return {
|
|
3366
|
+
exclusive: (fn) => queue(() => fn(ops)),
|
|
3367
|
+
condemn: (key) => queue(async () => {
|
|
3368
|
+
await fresh(true, (await load(key)).manifest, key);
|
|
3369
|
+
}),
|
|
3370
|
+
rekey: (oldKey, newKey) => queue(async () => {
|
|
3371
|
+
const l = await load(oldKey);
|
|
3372
|
+
if (l.state === "ok") await store({ ...l.manifest, epoch: l.manifest.epoch + 1 }, newKey);
|
|
3373
|
+
return l.state;
|
|
3374
|
+
}),
|
|
3375
|
+
read: (key) => queue(async () => {
|
|
3376
|
+
const l = await load(key);
|
|
3377
|
+
return { state: l.state, ...l.manifest ?? { epoch: 0, base: 0, sealed: false, versions: {} } };
|
|
3378
|
+
}),
|
|
3379
|
+
reset: () => queue(async () => {
|
|
3380
|
+
await options.manifest.set(null);
|
|
3381
|
+
await options.mark.set(null);
|
|
3382
|
+
})
|
|
3383
|
+
};
|
|
3384
|
+
}
|
|
2574
3385
|
//# sourceMappingURL=index.cjs.map
|