@anywalletkit/extension-kit 0.3.5 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +28 -7
- package/dist/index.cjs +859 -183
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +484 -69
- package/dist/index.d.ts +484 -69
- package/dist/index.js +862 -174
- package/dist/index.js.map +1 -1
- package/package.json +8 -8
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,18 +57,24 @@ __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,
|
|
60
|
-
TONCONNECT_ERRORS: () => TONCONNECT_ERRORS,
|
|
61
|
-
TONCONNECT_MAX_MESSAGES: () => TONCONNECT_MAX_MESSAGES,
|
|
68
|
+
TONCONNECT_ERRORS: () => import_tonconnect2.TONCONNECT_ERRORS,
|
|
69
|
+
TONCONNECT_MAX_MESSAGES: () => import_tonconnect2.TONCONNECT_MAX_MESSAGES,
|
|
70
|
+
TONCONNECT_NETWORKS: () => import_tonconnect2.TONCONNECT_NETWORKS,
|
|
71
|
+
TONCONNECT_SIGN_DATA_MAX: () => import_tonconnect2.TONCONNECT_SIGN_DATA_MAX,
|
|
62
72
|
TON_CHAIN: () => TON_CHAIN,
|
|
63
73
|
TON_ENDPOINTS: () => TON_ENDPOINTS,
|
|
64
74
|
TRON_CHAIN: () => TRON_CHAIN,
|
|
65
75
|
TRON_PROVIDERS: () => TRON_PROVIDERS,
|
|
66
|
-
|
|
76
|
+
TokenBucket: () => TokenBucket,
|
|
77
|
+
TonConnectError: () => import_tonconnect2.TonConnectError,
|
|
67
78
|
VAULT_DEFAULT_ITERATIONS: () => VAULT_DEFAULT_ITERATIONS,
|
|
68
79
|
VAULT_LIMITS: () => VAULT_LIMITS,
|
|
69
80
|
VAULT_MIN_ITERATIONS: () => VAULT_MIN_ITERATIONS,
|
|
@@ -77,6 +88,7 @@ __export(src_exports, {
|
|
|
77
88
|
ZCASH_CHAIN: () => ZCASH_CHAIN,
|
|
78
89
|
approvalChallenge: () => approvalChallenge,
|
|
79
90
|
caip2FromChainId: () => caip2FromChainId,
|
|
91
|
+
canonicalJson: () => canonicalJson,
|
|
80
92
|
chainHex: () => chainHex,
|
|
81
93
|
chainTxMatcher: () => chainTxMatcher,
|
|
82
94
|
checkSenderOrigin: () => checkSenderOrigin,
|
|
@@ -89,6 +101,7 @@ __export(src_exports, {
|
|
|
89
101
|
createSignHandler: () => createSignHandler,
|
|
90
102
|
decodedMessage: () => decodedMessage,
|
|
91
103
|
derToRaw: () => derToRaw,
|
|
104
|
+
deriveStorageMacKey: () => deriveStorageMacKey,
|
|
92
105
|
describeSiwe: () => describeSiwe,
|
|
93
106
|
earnApprovalPayload: () => earnApprovalPayload,
|
|
94
107
|
earnPlanMatchers: () => earnPlanMatchers,
|
|
@@ -96,6 +109,7 @@ __export(src_exports, {
|
|
|
96
109
|
evmCallMatcher: () => evmCallMatcher,
|
|
97
110
|
executionFloor: () => executionFloor,
|
|
98
111
|
fromBase64Url: () => fromBase64Url,
|
|
112
|
+
gasLimitExcessive: () => gasLimitExcessive,
|
|
99
113
|
hostOf: () => hostOf,
|
|
100
114
|
isCatalogChain: () => isCatalogChain,
|
|
101
115
|
isEnvelopeV1: () => isEnvelopeV1,
|
|
@@ -109,6 +123,7 @@ __export(src_exports, {
|
|
|
109
123
|
isRelayToContent: () => isRelayToContent,
|
|
110
124
|
levenshtein: () => levenshtein,
|
|
111
125
|
lookalikeOf: () => lookalikeOf,
|
|
126
|
+
macJsonStorage: () => macJsonStorage,
|
|
112
127
|
memoryAlarms: () => memoryAlarms,
|
|
113
128
|
memoryApprovalWindow: () => memoryApprovalWindow,
|
|
114
129
|
memoryJsonStorage: () => memoryJsonStorage,
|
|
@@ -120,10 +135,10 @@ __export(src_exports, {
|
|
|
120
135
|
normalizeHost: () => normalizeHost,
|
|
121
136
|
pageRequestFrom: () => pageRequestFrom,
|
|
122
137
|
parseAddChainParams: () => parseAddChainParams,
|
|
123
|
-
parseConnectRequest: () => parseConnectRequest,
|
|
138
|
+
parseConnectRequest: () => import_tonconnect2.parseConnectRequest,
|
|
124
139
|
parseRawAddress: () => parseRawAddress,
|
|
125
|
-
parseSendTransaction: () => parseSendTransaction,
|
|
126
|
-
parseSignData: () => parseSignData,
|
|
140
|
+
parseSendTransaction: () => import_tonconnect2.parseSendTransaction,
|
|
141
|
+
parseSignData: () => import_tonconnect2.parseSignData,
|
|
127
142
|
parseSiwe: () => parseSiwe,
|
|
128
143
|
parseSwapId: () => parseSwapId,
|
|
129
144
|
planMatchers: () => planMatchers,
|
|
@@ -133,14 +148,21 @@ __export(src_exports, {
|
|
|
133
148
|
rank: () => rank,
|
|
134
149
|
rawToDer: () => rawToDer,
|
|
135
150
|
relayError: () => relayError,
|
|
151
|
+
revealHidden: () => revealHidden,
|
|
136
152
|
reviewEarnPlan: () => reviewEarnPlan,
|
|
137
153
|
reviewSwapPlan: () => reviewSwapPlan,
|
|
154
|
+
sameTonConnectAddress: () => import_tonconnect2.sameTonConnectAddress,
|
|
155
|
+
siteOf: () => siteOf,
|
|
156
|
+
siweCrossSite: () => siweCrossSite,
|
|
138
157
|
siweProblems: () => siweProblems,
|
|
139
158
|
stampOrigin: () => stampOrigin,
|
|
159
|
+
storageVersionLedger: () => storageVersionLedger,
|
|
140
160
|
swapApprovalPayload: () => swapApprovalPayload,
|
|
141
161
|
toBase64Url: () => toBase64Url,
|
|
142
162
|
toRelayError: () => toRelayError,
|
|
143
163
|
tonConnectErrorCode: () => tonConnectErrorCode,
|
|
164
|
+
tonConnectFriendlyAddress: () => import_tonconnect2.tonConnectFriendlyAddress,
|
|
165
|
+
tonConnectRawAddress: () => import_tonconnect2.tonConnectRawAddress,
|
|
144
166
|
tonProofMessage: () => tonProofMessage,
|
|
145
167
|
tonSignDataMessage: () => tonSignDataMessage,
|
|
146
168
|
typedDataMatcher: () => typedDataMatcher,
|
|
@@ -286,7 +308,7 @@ var Vault = class {
|
|
|
286
308
|
if (isEnvelopeV1(envelope)) {
|
|
287
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");
|
|
288
310
|
const kek2 = await this._derivePbkdf2(password, fromBase64(envelope.kdf.salt), envelope.kdf.iterations);
|
|
289
|
-
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);
|
|
290
312
|
if (this._kdf === "scrypt") {
|
|
291
313
|
const next = await this._sealEnvelope(password, dek, envelope.secrets, [], envelope.createdAt);
|
|
292
314
|
await this._write(isEnvelopeV2(next) ? { ...next, upgradedAt: Date.now() } : next);
|
|
@@ -295,7 +317,24 @@ var Vault = class {
|
|
|
295
317
|
}
|
|
296
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");
|
|
297
319
|
const kek = await this._deriveScrypt(password, fromBase64(envelope.kdf.salt), envelope.kdf);
|
|
298
|
-
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
|
+
}
|
|
299
338
|
}
|
|
300
339
|
/**
|
|
301
340
|
* Re-wrap the DEK under a new password (fresh salt, current KDF). Secrets and passkeys are untouched —
|
|
@@ -327,9 +366,13 @@ var Vault = class {
|
|
|
327
366
|
*
|
|
328
367
|
* Passkeys are dropped — their boxes hold the old DEK and cannot be re-wrapped without their PRF
|
|
329
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.
|
|
330
372
|
*/
|
|
331
|
-
async rotateVaultKey(password) {
|
|
373
|
+
async rotateVaultKey(password, newPassword) {
|
|
332
374
|
requirePassword(password);
|
|
375
|
+
if (newPassword !== void 0) requirePassword(newPassword, VAULT_MIN_PASSWORD_LENGTH);
|
|
333
376
|
return this._exclusive(async () => {
|
|
334
377
|
const oldDek = await this._unlock(password);
|
|
335
378
|
const dek = this._random(DEK_BYTES);
|
|
@@ -345,7 +388,7 @@ var Vault = class {
|
|
|
345
388
|
(0, import_core.zeroize)(plaintext);
|
|
346
389
|
}
|
|
347
390
|
}
|
|
348
|
-
const next = await this._sealEnvelope(password, dek, secrets, [], envelope.createdAt);
|
|
391
|
+
const next = await this._sealEnvelope(newPassword ?? password, dek, secrets, [], envelope.createdAt);
|
|
349
392
|
const upgradedAt = isEnvelopeV2(envelope) ? envelope.upgradedAt : void 0;
|
|
350
393
|
await this._write(isEnvelopeV2(next) ? { ...next, ...upgradedAt !== void 0 ? { upgradedAt } : {}, rotatedAt: Date.now() } : next);
|
|
351
394
|
return dek;
|
|
@@ -409,7 +452,7 @@ var Vault = class {
|
|
|
409
452
|
const entry = envelope.passkeys.find((p) => p.id === id);
|
|
410
453
|
if (!entry) throw new VaultError("No such passkey on this vault.", "missing");
|
|
411
454
|
const key = await this._derivePasskeyKek(prf, id);
|
|
412
|
-
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);
|
|
413
456
|
} finally {
|
|
414
457
|
(0, import_core.zeroize)(prf);
|
|
415
458
|
}
|
|
@@ -704,7 +747,11 @@ var Session = class {
|
|
|
704
747
|
return fromBase64(record.key);
|
|
705
748
|
});
|
|
706
749
|
}
|
|
707
|
-
/**
|
|
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
|
+
*/
|
|
708
755
|
async touch() {
|
|
709
756
|
return this._serial(async () => {
|
|
710
757
|
const epoch = this._epoch;
|
|
@@ -718,11 +765,25 @@ var Session = class {
|
|
|
718
765
|
get autoLockMs() {
|
|
719
766
|
return this._autoLockMs;
|
|
720
767
|
}
|
|
721
|
-
/**
|
|
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
|
+
*/
|
|
722
775
|
async setAutoLockMs(ms) {
|
|
723
776
|
if (!Number.isFinite(ms) || ms < 0) throw new VaultError("Auto-lock must be a non-negative number of milliseconds.", "invalid-input");
|
|
724
777
|
this._autoLockMs = ms;
|
|
725
|
-
|
|
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
|
+
});
|
|
726
787
|
}
|
|
727
788
|
onLock(callback) {
|
|
728
789
|
this._onLock.add(callback);
|
|
@@ -992,7 +1053,8 @@ var DEFAULT_NETWORKS = Object.freeze([
|
|
|
992
1053
|
net(4326, "MegaETH", "ETH", ["https://mainnet.megaeth.com/rpc"]),
|
|
993
1054
|
net(130, "Unichain", "ETH", ["https://mainnet.unichain.org", "https://unichain-rpc.publicnode.com"], "https://uniscan.xyz"),
|
|
994
1055
|
net(143, "Monad", "MON", ["https://rpc.monad.xyz"]),
|
|
995
|
-
|
|
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"]),
|
|
996
1058
|
net(59144, "Linea", "ETH", ["https://rpc.linea.build", "https://linea-rpc.publicnode.com"], "https://lineascan.build"),
|
|
997
1059
|
net(100, "Gnosis", "xDAI", ["https://rpc.gnosischain.com", "https://gnosis-rpc.publicnode.com"], "https://gnosisscan.io"),
|
|
998
1060
|
net(146, "Sonic", "S", ["https://rpc.soniclabs.com", "https://sonic-rpc.publicnode.com"], "https://sonicscan.org"),
|
|
@@ -1010,7 +1072,11 @@ var DEFAULT_NETWORKS = Object.freeze([
|
|
|
1010
1072
|
net(5e3, "Mantle", "MNT", ["https://rpc.mantle.xyz", "https://mantle-rpc.publicnode.com"], "https://mantlescan.xyz"),
|
|
1011
1073
|
net(747474, "Katana", "ETH", ["https://rpc.katana.network"], "https://explorer.katanarpc.com"),
|
|
1012
1074
|
net(2818, "Morph", "ETH", ["https://rpc.morphl2.io", "https://rpc-quicknode.morphl2.io"], "https://explorer.morphl2.io"),
|
|
1013
|
-
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")
|
|
1014
1080
|
]);
|
|
1015
1081
|
var chainHex = (caip2) => `0x${Number(caip2.split(":")[1]).toString(16)}`;
|
|
1016
1082
|
function caip2FromChainId(value) {
|
|
@@ -1086,6 +1152,17 @@ var NetworksTable = class {
|
|
|
1086
1152
|
);
|
|
1087
1153
|
return this._loading;
|
|
1088
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
|
+
}
|
|
1089
1166
|
};
|
|
1090
1167
|
var isSiteRpcUrl = (u) => typeof u === "string" && u.length <= 2048 && (0, import_url.isPublicHttpsUrl)(u);
|
|
1091
1168
|
function parseAddChainParams(params, known = isCatalogChain) {
|
|
@@ -1199,6 +1276,21 @@ var PermissionStore = class {
|
|
|
1199
1276
|
return permission;
|
|
1200
1277
|
});
|
|
1201
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
|
+
}
|
|
1202
1294
|
/** Connect another chain's accounts to the same origin (one "connected sites" list). */
|
|
1203
1295
|
connectChain(origin, key, accounts, defaultChain) {
|
|
1204
1296
|
return this.serialize(async () => {
|
|
@@ -1231,6 +1323,11 @@ var PermissionStore = class {
|
|
|
1231
1323
|
});
|
|
1232
1324
|
}
|
|
1233
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
|
+
*
|
|
1234
1331
|
* The active EVM account changed: every origin holding an EVM grant now sees `accounts` (MetaMask behaviour).
|
|
1235
1332
|
* An origin connected only on another chain (`accounts: []` — Solana/TON/Tron/BTC/NEAR providers, a WalletConnect
|
|
1236
1333
|
* session without eip155) is skipped: it never passed an EVM connect and must not learn the EVM address (scan F6/F8).
|
|
@@ -1276,6 +1373,179 @@ var PermissionStore = class {
|
|
|
1276
1373
|
};
|
|
1277
1374
|
var sameList = (a, b) => a.length === b.length && a.every((x, i) => x === b[i]);
|
|
1278
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
|
+
|
|
1279
1549
|
// src/approvals.ts
|
|
1280
1550
|
var ReviewDeclined = class extends Error {
|
|
1281
1551
|
code = RELAY_ERRORS.userRejected;
|
|
@@ -1300,7 +1570,11 @@ var OriginRefused = class extends Error {
|
|
|
1300
1570
|
}
|
|
1301
1571
|
};
|
|
1302
1572
|
var APPROVALS_MAX_PENDING_PER_ORIGIN = 3;
|
|
1303
|
-
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 {
|
|
1304
1578
|
constructor(window, newId = () => crypto.randomUUID(), options = {}) {
|
|
1305
1579
|
this.window = window;
|
|
1306
1580
|
this.newId = newId;
|
|
@@ -1308,6 +1582,8 @@ var ApprovalQueue = class {
|
|
|
1308
1582
|
window.onClosed((id) => {
|
|
1309
1583
|
if (this._current?.request.id === id) this.decline(id);
|
|
1310
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;
|
|
1311
1587
|
}
|
|
1312
1588
|
window;
|
|
1313
1589
|
newId;
|
|
@@ -1316,8 +1592,62 @@ var ApprovalQueue = class {
|
|
|
1316
1592
|
_waiting = [];
|
|
1317
1593
|
_listeners = /* @__PURE__ */ new Set();
|
|
1318
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
|
+
}
|
|
1319
1649
|
/** Enqueue and wait for the user's answer. Refuses (never queues) past the per-origin limits. */
|
|
1320
|
-
request(input) {
|
|
1650
|
+
request(input, opts = {}) {
|
|
1321
1651
|
let refused;
|
|
1322
1652
|
try {
|
|
1323
1653
|
refused = this.options.refuse?.(input.origin);
|
|
@@ -1325,19 +1655,41 @@ var ApprovalQueue = class {
|
|
|
1325
1655
|
refused = `The wallet could not check this site (${e instanceof Error ? e.message : String(e)}).`;
|
|
1326
1656
|
}
|
|
1327
1657
|
if (refused) return Promise.reject(new OriginRefused(refused));
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1658
|
+
let exempt = false;
|
|
1659
|
+
try {
|
|
1660
|
+
exempt = this.options.exempt?.(input.origin) === true;
|
|
1661
|
+
} catch {
|
|
1662
|
+
exempt = false;
|
|
1331
1663
|
}
|
|
1332
|
-
const
|
|
1333
|
-
if (
|
|
1334
|
-
|
|
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));
|
|
1335
1686
|
}
|
|
1336
1687
|
return new Promise((resolve, reject) => {
|
|
1337
1688
|
const warnings = this.options.annotate?.(input.origin) ?? [];
|
|
1338
1689
|
const via = this._via.get(input.origin);
|
|
1339
1690
|
const pending = { request: { ...input, id: this.newId(), createdAt: Date.now(), ...warnings.length ? { originWarnings: warnings } : {}, ...via ? { via } : {} }, resolve, reject };
|
|
1340
|
-
this._waiting.
|
|
1691
|
+
if (followUp) this._waiting.unshift(pending);
|
|
1692
|
+
else this._waiting.push(pending);
|
|
1341
1693
|
void this.next();
|
|
1342
1694
|
});
|
|
1343
1695
|
}
|
|
@@ -1350,9 +1702,37 @@ var ApprovalQueue = class {
|
|
|
1350
1702
|
viaFor(origin) {
|
|
1351
1703
|
return this._via.get(origin);
|
|
1352
1704
|
}
|
|
1705
|
+
isExempt(origin) {
|
|
1706
|
+
try {
|
|
1707
|
+
return this.options.exempt?.(origin) === true;
|
|
1708
|
+
} catch {
|
|
1709
|
+
return false;
|
|
1710
|
+
}
|
|
1711
|
+
}
|
|
1353
1712
|
/** Requests waiting for `origin` (current + queued). */
|
|
1354
1713
|
pendingFor(origin) {
|
|
1355
|
-
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;
|
|
1356
1736
|
}
|
|
1357
1737
|
/** The request currently shown (the window asks for it by id on load). */
|
|
1358
1738
|
get current() {
|
|
@@ -1362,7 +1742,10 @@ var ApprovalQueue = class {
|
|
|
1362
1742
|
return this._waiting.length;
|
|
1363
1743
|
}
|
|
1364
1744
|
resolve(requestId, value) {
|
|
1365
|
-
return this.settle(requestId, (p) =>
|
|
1745
|
+
return this.settle(requestId, (p) => {
|
|
1746
|
+
this.approved(p.request);
|
|
1747
|
+
p.resolve(value);
|
|
1748
|
+
});
|
|
1366
1749
|
}
|
|
1367
1750
|
decline(requestId, error = new ReviewDeclined()) {
|
|
1368
1751
|
return this.settle(requestId, (p) => p.reject(error));
|
|
@@ -1370,6 +1753,7 @@ var ApprovalQueue = class {
|
|
|
1370
1753
|
/** Lock, disconnect, shutdown: everything pending is declined. */
|
|
1371
1754
|
declineAll(error = new ReviewDeclined("The wallet was locked.")) {
|
|
1372
1755
|
const all = [...this._current ? [this._current] : [], ...this._waiting.splice(0)];
|
|
1756
|
+
this._approved.clear();
|
|
1373
1757
|
for (const p of all) p.reject(error);
|
|
1374
1758
|
if (this._current) void this.window.close(this._current.request.id);
|
|
1375
1759
|
this._current = null;
|
|
@@ -1501,6 +1885,7 @@ var READ_METHODS = /* @__PURE__ */ new Set([
|
|
|
1501
1885
|
"eth_getTransactionReceipt",
|
|
1502
1886
|
"eth_maxPriorityFeePerGas"
|
|
1503
1887
|
]);
|
|
1888
|
+
var GETLOGS_MAX_SPAN_UNCONNECTED = 1e3;
|
|
1504
1889
|
var SIGNING_METHODS = /* @__PURE__ */ new Set(["personal_sign", "eth_signTypedData_v4", "eth_signTypedData_v3", "eth_signTypedData", "eth_sendTransaction"]);
|
|
1505
1890
|
var RequestRouter = class {
|
|
1506
1891
|
constructor(deps) {
|
|
@@ -1530,8 +1915,10 @@ var RequestRouter = class {
|
|
|
1530
1915
|
return this.visibleAccounts(permission);
|
|
1531
1916
|
case "eth_requestAccounts":
|
|
1532
1917
|
return this.connect(origin, network);
|
|
1533
|
-
case "wallet_getPermissions":
|
|
1534
|
-
|
|
1918
|
+
case "wallet_getPermissions": {
|
|
1919
|
+
const visible = await this.visibleAccounts(permission);
|
|
1920
|
+
return visible.length > 0 ? [permissionDescriptor(origin, visible)] : [];
|
|
1921
|
+
}
|
|
1535
1922
|
case "wallet_requestPermissions": {
|
|
1536
1923
|
const accounts = await this.connect(origin, network);
|
|
1537
1924
|
return [permissionDescriptor(origin, accounts)];
|
|
@@ -1572,11 +1959,20 @@ var RequestRouter = class {
|
|
|
1572
1959
|
case "eth_signTransaction":
|
|
1573
1960
|
throw new RouterError(RELAY_ERRORS.unsupportedMethod, `${method} (blind signing) is never offered by this wallet.`);
|
|
1574
1961
|
}
|
|
1575
|
-
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
|
+
}
|
|
1576
1966
|
if (SIGNING_METHODS.has(method)) {
|
|
1577
1967
|
if (!this.deps.sign) throw new RouterError(RELAY_ERRORS.unsupportedMethod, `${method} is not available yet.`);
|
|
1578
|
-
if (!permission) throw new RouterError(RELAY_ERRORS.unauthorized, "Connect the wallet first (eth_requestAccounts).");
|
|
1579
|
-
|
|
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 });
|
|
1580
1976
|
}
|
|
1581
1977
|
throw new RouterError(RELAY_ERRORS.unsupportedMethod, `This wallet does not support ${method}.`);
|
|
1582
1978
|
}
|
|
@@ -1586,9 +1982,31 @@ var RequestRouter = class {
|
|
|
1586
1982
|
const available = await this.deps.accounts();
|
|
1587
1983
|
return permission.accounts.filter((a) => available.includes(a));
|
|
1588
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
|
+
}
|
|
1589
2006
|
/**
|
|
1590
2007
|
* Connected and unlocked → the accounts; otherwise the approval window (which
|
|
1591
|
-
* 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`.
|
|
1592
2010
|
*/
|
|
1593
2011
|
async connect(origin, network) {
|
|
1594
2012
|
const existing = await this.visibleAccounts(await this.deps.permissions.get(origin));
|
|
@@ -1597,7 +2015,7 @@ var RequestRouter = class {
|
|
|
1597
2015
|
const available = await this.deps.accounts();
|
|
1598
2016
|
const accounts = Array.isArray(chosen) ? chosen.filter((a) => typeof a === "string" && available.includes(a)) : [];
|
|
1599
2017
|
if (accounts.length === 0) throw new ReviewDeclined("No account was selected.");
|
|
1600
|
-
await this.deps.permissions.
|
|
2018
|
+
await this.deps.permissions.grant(origin, accounts, network.caip2);
|
|
1601
2019
|
this.deps.emit(origin, "connect", { chainId: chainHex(network.caip2) });
|
|
1602
2020
|
this.deps.emit(origin, "accountsChanged", accounts);
|
|
1603
2021
|
return accounts;
|
|
@@ -1617,6 +2035,12 @@ var RequestRouter = class {
|
|
|
1617
2035
|
this.deps.emit(origin, "chainChanged", chainHex(to.caip2));
|
|
1618
2036
|
}
|
|
1619
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
|
+
}
|
|
1620
2044
|
var permissionDescriptor = (origin, accounts) => ({
|
|
1621
2045
|
id: crypto.randomUUID(),
|
|
1622
2046
|
parentCapability: "eth_accounts",
|
|
@@ -1626,6 +2050,7 @@ var permissionDescriptor = (origin, accounts) => ({
|
|
|
1626
2050
|
});
|
|
1627
2051
|
|
|
1628
2052
|
// src/siwe.ts
|
|
2053
|
+
var SIWE_FORBIDDEN_CHARS = /[\u061c\u200b-\u200f\u202a-\u202e\u2060-\u2064\u2066-\u2069\ufeff]/;
|
|
1629
2054
|
var HEADER_LINE = /^(?<domain>[^\n]+?) wants you to sign in with your Ethereum account:(?:\n|$)/;
|
|
1630
2055
|
var ADDRESS_LINE = /^0x[0-9a-fA-F]{40}$/;
|
|
1631
2056
|
var FIELD = /^([A-Za-z ]+): (.*)$/;
|
|
@@ -1729,12 +2154,20 @@ function siweProblems(m, ctx) {
|
|
|
1729
2154
|
}
|
|
1730
2155
|
return out;
|
|
1731
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
|
+
}
|
|
1732
2163
|
function describeSiwe(text, ctx) {
|
|
1733
2164
|
const p = parseLoose(text);
|
|
1734
2165
|
if (!p) return null;
|
|
1735
2166
|
const problems = siweProblems(p.message, ctx);
|
|
1736
|
-
|
|
1737
|
-
|
|
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 };
|
|
1738
2171
|
}
|
|
1739
2172
|
|
|
1740
2173
|
// src/signing.ts
|
|
@@ -1742,18 +2175,31 @@ var safeNonce = (n) => {
|
|
|
1742
2175
|
if (n < 0n || n > BigInt(Number.MAX_SAFE_INTEGER)) throw new Error(`The RPC returned an impossible nonce (${n}).`);
|
|
1743
2176
|
return Number(n.toString());
|
|
1744
2177
|
};
|
|
2178
|
+
var MAX_FEE_REASKS = 3;
|
|
1745
2179
|
var ADDRESS = /^0x[0-9a-fA-F]{40}$/;
|
|
1746
2180
|
var HEX = /^0x([0-9a-fA-F]{2})*$/;
|
|
1747
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
|
+
}
|
|
1748
2191
|
function messageText(value) {
|
|
1749
|
-
|
|
1750
|
-
|
|
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);
|
|
1751
2197
|
try {
|
|
1752
2198
|
const bytes = Uint8Array.from(value.slice(2).match(/../g) ?? [], (b) => Number.parseInt(b, 16));
|
|
1753
2199
|
const s = new TextDecoder("utf-8", { fatal: true }).decode(bytes);
|
|
1754
|
-
return /^[\x09\x0a\x0d\x20-\x7e
|
|
2200
|
+
return /^[\x09\x0a\x0d\x20-\x7e\u00a0-\uffff]*$/.test(s) ? revealHidden(s) : { text: value, hidden: false };
|
|
1755
2201
|
} catch {
|
|
1756
|
-
return value;
|
|
2202
|
+
return { text: value, hidden: false };
|
|
1757
2203
|
}
|
|
1758
2204
|
}
|
|
1759
2205
|
function decodedMessage(value) {
|
|
@@ -1767,6 +2213,7 @@ function decodedMessage(value) {
|
|
|
1767
2213
|
}
|
|
1768
2214
|
var HIDDEN_CONTROL = /[\x00-\x08\x0b\x0c\x0e-\x1f\x7f-\x9f]/;
|
|
1769
2215
|
var err = (code, message) => ({ code, message });
|
|
2216
|
+
var BLIND_HASH = /^0x[0-9a-fA-F]{64}$/;
|
|
1770
2217
|
function classifySignRequest(input) {
|
|
1771
2218
|
const { method, params } = input;
|
|
1772
2219
|
const owned = new Set(input.accounts.map(lc));
|
|
@@ -1777,11 +2224,15 @@ function classifySignRequest(input) {
|
|
|
1777
2224
|
if (ours(msg) && !ours(addr)) [msg, addr] = [addr, msg];
|
|
1778
2225
|
if (!ours(addr)) return err(RELAY_ERRORS.unauthorized, "personal_sign for an address this wallet does not hold.");
|
|
1779
2226
|
if (typeof msg !== "string") return err(RELAY_ERRORS.invalidParams, "personal_sign expects a message string.");
|
|
1780
|
-
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;
|
|
1781
2231
|
const signed = decodedMessage(msg) ?? text;
|
|
1782
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;
|
|
1783
2233
|
if (siwe && HIDDEN_CONTROL.test(signed)) return err(RELAY_ERRORS.invalidParams, "This sign-in message contains hidden control characters \u2014 refused.");
|
|
1784
|
-
|
|
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 } : {} };
|
|
1785
2236
|
}
|
|
1786
2237
|
if (method === "eth_signTypedData_v4" || method === "eth_signTypedData_v3" || method === "eth_signTypedData") {
|
|
1787
2238
|
const [addr, data] = params;
|
|
@@ -1799,7 +2250,8 @@ function classifySignRequest(input) {
|
|
|
1799
2250
|
if (cid !== void 0 && cid !== null && Number(cid) !== input.network.chainId) {
|
|
1800
2251
|
return err(RELAY_ERRORS.chainDisconnected, `Typed data is for chain ${String(cid)}; this site is on ${input.network.name} (${input.network.chainId}).`);
|
|
1801
2252
|
}
|
|
1802
|
-
|
|
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 } : {} };
|
|
1803
2255
|
}
|
|
1804
2256
|
if (method === "eth_sendTransaction") {
|
|
1805
2257
|
const tx = params[0] ?? {};
|
|
@@ -1848,16 +2300,33 @@ function overpriced(shown, fresh) {
|
|
|
1848
2300
|
if (shown.maxFeePerGas !== void 0 && fresh.maxFeePerGas !== void 0) return big(shown.maxFeePerGas) > STALE_FEE_FACTOR * big(fresh.maxFeePerGas);
|
|
1849
2301
|
return false;
|
|
1850
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
|
+
}
|
|
1851
2308
|
async function prepareTransaction(deps, network, from, tx) {
|
|
1852
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]);
|
|
1853
2311
|
const [nonceHex, block, gasHex] = await Promise.all([
|
|
1854
2312
|
deps.rpc(network, "eth_getTransactionCount", [from, "pending"]),
|
|
1855
2313
|
deps.rpc(network, "eth_getBlockByNumber", ["latest", false]),
|
|
1856
|
-
tx.gas ?
|
|
2314
|
+
tx.gas ? estimate().then((v) => v, () => null) : estimate()
|
|
1857
2315
|
]);
|
|
1858
|
-
const
|
|
1859
|
-
|
|
1860
|
-
|
|
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;
|
|
1861
2330
|
}
|
|
1862
2331
|
function createSignHandler(deps) {
|
|
1863
2332
|
const tails = /* @__PURE__ */ new Map();
|
|
@@ -1873,7 +2342,8 @@ function createSignHandler(deps) {
|
|
|
1873
2342
|
};
|
|
1874
2343
|
return async ({ origin, network, method, params, accounts }) => {
|
|
1875
2344
|
const via = deps.approvals.viaFor(origin);
|
|
1876
|
-
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 } : {} });
|
|
1877
2347
|
if ("code" in info) throw RouterError.from(info);
|
|
1878
2348
|
const prepared = info.kind === "send-transaction" ? await prepareTransaction(deps, network, info.account, info.tx) : void 0;
|
|
1879
2349
|
const preview = deps.preview ? await deps.preview({ network, request: info, accounts }).catch(() => null) : null;
|
|
@@ -1883,14 +2353,20 @@ function createSignHandler(deps) {
|
|
|
1883
2353
|
const key = `${network.caip2}|${lc(info.account)}`;
|
|
1884
2354
|
return serial(key, async () => {
|
|
1885
2355
|
let shown = prepared;
|
|
2356
|
+
let reasks = 0;
|
|
1886
2357
|
for (; ; ) {
|
|
1887
2358
|
const [nonceHex, fees] = await Promise.all([deps.rpc(network, "eth_getTransactionCount", [info.account, "pending"]), readFees(deps, network)]);
|
|
1888
2359
|
const local = nextNonce.get(key) ?? 0;
|
|
1889
2360
|
const nodeNonce = safeNonce(big(nonceHex));
|
|
1890
2361
|
const nonce = nodeNonce > local ? nodeNonce : local;
|
|
1891
2362
|
if (outgrown(shown, fees)) {
|
|
1892
|
-
|
|
1893
|
-
|
|
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;
|
|
1894
2370
|
continue;
|
|
1895
2371
|
}
|
|
1896
2372
|
const fresh = withFees(network.chainId, nonce, big(shown.gasLimit), fees);
|
|
@@ -2012,7 +2488,14 @@ async function quoteRoutes(providers, params, options = {}) {
|
|
|
2012
2488
|
candidates.push(p);
|
|
2013
2489
|
}
|
|
2014
2490
|
const pending = new Set(candidates.map((p) => p.name));
|
|
2015
|
-
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 } });
|
|
2016
2499
|
const emit = () => options.onUpdate?.(view());
|
|
2017
2500
|
emit();
|
|
2018
2501
|
await Promise.all(
|
|
@@ -2027,7 +2510,7 @@ async function quoteRoutes(providers, params, options = {}) {
|
|
|
2027
2510
|
const q = await Promise.race([p.getQuote({ ...params, dry: true, onRefusal: (r) => {
|
|
2028
2511
|
refusal = r;
|
|
2029
2512
|
} }, ctrl.signal), aborted]);
|
|
2030
|
-
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;
|
|
2031
2514
|
else refusals[p.name] = refusal ?? (q ? "returned an invalid quote" : "no route for this pair right now");
|
|
2032
2515
|
} catch (e) {
|
|
2033
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);
|
|
@@ -2041,16 +2524,50 @@ async function quoteRoutes(providers, params, options = {}) {
|
|
|
2041
2524
|
);
|
|
2042
2525
|
return view();
|
|
2043
2526
|
}
|
|
2044
|
-
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 = {}) {
|
|
2045
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);
|
|
2046
2533
|
const fastest = sorted.length ? sorted.reduce((f, q) => q.etaSeconds < f.etaSeconds ? q : f) : null;
|
|
2047
|
-
|
|
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) => {
|
|
2048
2564
|
const poolFee = q.providerData?.poolFee;
|
|
2049
2565
|
const feeNote = typeof poolFee === "string" && poolFee && q.feeUsd === 0 ? poolFee === "pools" ? "pool fees in price" : `pool ${poolFee} in price` : void 0;
|
|
2050
2566
|
const tags = [];
|
|
2051
|
-
if (
|
|
2567
|
+
if (q === best) tags.push("best");
|
|
2052
2568
|
if (fastest && q.provider === fastest.provider) tags.push("fastest");
|
|
2053
|
-
|
|
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 } : {} };
|
|
2054
2571
|
});
|
|
2055
2572
|
}
|
|
2056
2573
|
var FLOOR_TOLERANCE_BPS = 50n;
|
|
@@ -2069,18 +2586,21 @@ function depositOf(s, approvals) {
|
|
|
2069
2586
|
if (!s.data.startsWith(TRANSFER_SELECTOR) || s.data.length < 10 + 64) return void 0;
|
|
2070
2587
|
return `0x${s.data.slice(10 + 24, 10 + 64)}`;
|
|
2071
2588
|
}
|
|
2589
|
+
var pct = (bps) => `${Number((bps / 100).toFixed(2))}%`;
|
|
2072
2590
|
function swapApprovalPayload(plan, meta) {
|
|
2073
2591
|
const isOwn = meta.own.some((a) => lc3(a) === lc3(plan.recipient));
|
|
2592
|
+
const assigned = meta.depositAddressAssignedByProvider === true;
|
|
2074
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` };
|
|
2075
2595
|
const s = plan.source;
|
|
2076
2596
|
let source;
|
|
2077
2597
|
if (s.kind === "call") {
|
|
2078
2598
|
const deposit = depositOf(s, plan.approvals.length);
|
|
2079
2599
|
source = { kind: "call", to: s.to, ...s.data ? { data: s.data } : {}, value: s.value.toString(), ...deposit ? { deposit } : {} };
|
|
2080
|
-
checks.push(deposit
|
|
2600
|
+
checks.push(deposit || assigned ? depositCheck : { status: "ok", label: `Calls the ${meta.providerLabel} contract` });
|
|
2081
2601
|
} else if (s.kind === "token-transfer") {
|
|
2082
2602
|
source = { kind: "token-transfer", token: s.token, recipient: s.recipient, amount: s.amount.toString(), ...s.memo ? { memo: s.memo } : {} };
|
|
2083
|
-
checks.push(
|
|
2603
|
+
checks.push(depositCheck);
|
|
2084
2604
|
} else if (s.kind === "serialized") {
|
|
2085
2605
|
source = { kind: "serialized" };
|
|
2086
2606
|
checks.push({ status: "ok", label: "Balance changes are proven before signing (SDK guard)" });
|
|
@@ -2091,7 +2611,14 @@ function swapApprovalPayload(plan, meta) {
|
|
|
2091
2611
|
}
|
|
2092
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" });
|
|
2093
2613
|
if (plan.approvals.length > 1) checks.push({ status: "ok", label: `${plan.approvals.length} approvals (reset to 0 first)` });
|
|
2094
|
-
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
|
+
}
|
|
2095
2622
|
return {
|
|
2096
2623
|
provider: plan.provider,
|
|
2097
2624
|
providerLabel: meta.providerLabel,
|
|
@@ -2108,7 +2635,9 @@ function swapApprovalPayload(plan, meta) {
|
|
|
2108
2635
|
validUntilMs: plan.validUntilMs,
|
|
2109
2636
|
approvals: plan.approvals.map((a) => ({ token: a.token, spender: a.spender, amount: a.amount.toString() })),
|
|
2110
2637
|
source,
|
|
2111
|
-
checks
|
|
2638
|
+
checks,
|
|
2639
|
+
...slippage ? { slippage } : {},
|
|
2640
|
+
...assigned ? { depositAssignedByProvider: true } : {}
|
|
2112
2641
|
};
|
|
2113
2642
|
}
|
|
2114
2643
|
function parseTypedOrder(json) {
|
|
@@ -2150,15 +2679,15 @@ var import_protocol_swap2_core2 = require("@anywalletkit/protocol-swap2-core");
|
|
|
2150
2679
|
var ACTIVE_STATUSES = /* @__PURE__ */ new Set(["pending", "action-required", "refund-pending", "partial", "claiming"]);
|
|
2151
2680
|
function parseSwapId(id) {
|
|
2152
2681
|
if (!id.startsWith(`${import_protocol_swap2_core2.SWAP2_ID_PREFIX}.`)) return null;
|
|
2153
|
-
const
|
|
2154
|
-
if (
|
|
2155
|
-
if (
|
|
2156
|
-
const provider = decodeURIComponent(
|
|
2157
|
-
const intentId = decodeURIComponent(
|
|
2158
|
-
const sourceTxHash =
|
|
2159
|
-
const destTxHash =
|
|
2682
|
+
const parts2 = id.slice(import_protocol_swap2_core2.SWAP2_ID_PREFIX.length + 1).split(".");
|
|
2683
|
+
if (parts2.length < 2 || parts2.length > 5 || !parts2[0] || !parts2[1]) return null;
|
|
2684
|
+
if (parts2[4] !== void 0 && (parts2[4] !== "ok" || !parts2[3])) return null;
|
|
2685
|
+
const provider = decodeURIComponent(parts2[0]);
|
|
2686
|
+
const intentId = decodeURIComponent(parts2[1]);
|
|
2687
|
+
const sourceTxHash = parts2[2] ? decodeURIComponent(parts2[2]) : void 0;
|
|
2688
|
+
const destTxHash = parts2[3] ? decodeURIComponent(parts2[3]) : void 0;
|
|
2160
2689
|
if (destTxHash && !sourceTxHash) return null;
|
|
2161
|
-
return { provider, intentId, ...sourceTxHash ? { sourceTxHash } : {}, ...destTxHash ? { destTxHash } : {}, ...
|
|
2690
|
+
return { provider, intentId, ...sourceTxHash ? { sourceTxHash } : {}, ...destTxHash ? { destTxHash } : {}, ...parts2[4] !== void 0 ? { destTxConfirmed: true } : {} };
|
|
2162
2691
|
}
|
|
2163
2692
|
var SwapsStore = class {
|
|
2164
2693
|
constructor(storage) {
|
|
@@ -2297,131 +2826,42 @@ async function reviewEarnPlan(approvals, gate, meta, plan) {
|
|
|
2297
2826
|
}
|
|
2298
2827
|
|
|
2299
2828
|
// src/tonconnect.ts
|
|
2300
|
-
var
|
|
2301
|
-
var
|
|
2302
|
-
var
|
|
2303
|
-
|
|
2304
|
-
|
|
2305
|
-
this.code = code;
|
|
2306
|
-
this.name = "TonConnectError";
|
|
2307
|
-
}
|
|
2308
|
-
code;
|
|
2309
|
-
};
|
|
2310
|
-
function parseConnectRequest(message) {
|
|
2311
|
-
const m = message;
|
|
2312
|
-
if (!m || typeof m.manifestUrl !== "string" || !/^https:\/\//.test(m.manifestUrl)) throw new TonConnectError(TONCONNECT_ERRORS.badRequest, "ConnectRequest needs an https manifestUrl.");
|
|
2313
|
-
const items = Array.isArray(m.items) ? m.items : [];
|
|
2314
|
-
if (!items.some((i) => i?.name === "ton_addr")) throw new TonConnectError(TONCONNECT_ERRORS.badRequest, "ConnectRequest must ask for ton_addr.");
|
|
2315
|
-
const proof = items.find((i) => i?.name === "ton_proof");
|
|
2316
|
-
if (proof && typeof proof.payload !== "string") throw new TonConnectError(TONCONNECT_ERRORS.badRequest, "ton_proof needs a string payload.");
|
|
2317
|
-
for (const i of items) if (i?.name !== "ton_addr" && i?.name !== "ton_proof") throw new TonConnectError(TONCONNECT_ERRORS.badRequest, `Unknown connect item ${String(i?.name)}.`);
|
|
2318
|
-
return { manifestUrl: m.manifestUrl, ...proof ? { proofPayload: proof.payload } : {} };
|
|
2319
|
-
}
|
|
2320
|
-
var TONCONNECT_MAX_MESSAGES = 4;
|
|
2321
|
-
function parseSendTransaction(param, options = {}) {
|
|
2322
|
-
let o;
|
|
2323
|
-
try {
|
|
2324
|
-
o = typeof param === "string" ? JSON.parse(param) : param;
|
|
2325
|
-
} catch {
|
|
2326
|
-
throw new TonConnectError(TONCONNECT_ERRORS.badRequest, "sendTransaction params[0] is not valid JSON.");
|
|
2327
|
-
}
|
|
2328
|
-
if (!o || typeof o !== "object") throw new TonConnectError(TONCONNECT_ERRORS.badRequest, "sendTransaction needs a request object.");
|
|
2329
|
-
const network = options.network ?? "-239";
|
|
2330
|
-
if (o.network !== void 0 && String(o.network) !== network) throw new TonConnectError(TONCONNECT_ERRORS.badRequest, `This wallet is on network ${network}; the request names ${String(o.network)}.`);
|
|
2331
|
-
const raw = Array.isArray(o.messages) ? o.messages : [];
|
|
2332
|
-
if (raw.length === 0) throw new TonConnectError(TONCONNECT_ERRORS.badRequest, "sendTransaction has no messages.");
|
|
2333
|
-
if (raw.length > (options.maxMessages ?? TONCONNECT_MAX_MESSAGES)) throw new TonConnectError(TONCONNECT_ERRORS.badRequest, `At most ${options.maxMessages ?? TONCONNECT_MAX_MESSAGES} messages per transaction.`);
|
|
2334
|
-
const messages = raw.map((m, i) => {
|
|
2335
|
-
if (typeof m.address !== "string" || !m.address) throw new TonConnectError(TONCONNECT_ERRORS.badRequest, `Message ${i + 1} has no address.`);
|
|
2336
|
-
if (typeof m.amount !== "string" || !/^[0-9]+$/.test(m.amount)) throw new TonConnectError(TONCONNECT_ERRORS.badRequest, `Message ${i + 1} amount must be a decimal string of nanotons.`);
|
|
2337
|
-
if (m.payload !== void 0 && typeof m.payload !== "string") throw new TonConnectError(TONCONNECT_ERRORS.badRequest, `Message ${i + 1} payload must be a base64 BoC.`);
|
|
2338
|
-
if (m.stateInit !== void 0 && typeof m.stateInit !== "string") throw new TonConnectError(TONCONNECT_ERRORS.badRequest, `Message ${i + 1} stateInit must be a base64 BoC.`);
|
|
2339
|
-
return { address: m.address, amount: m.amount, ...m.payload ? { payload: m.payload } : {}, ...m.stateInit ? { stateInit: m.stateInit } : {} };
|
|
2340
|
-
});
|
|
2341
|
-
const validUntil = typeof o.valid_until === "number" ? o.valid_until : typeof o.validUntil === "number" ? o.validUntil : void 0;
|
|
2342
|
-
if (validUntil !== void 0 && validUntil * 1e3 < Date.now()) throw new TonConnectError(TONCONNECT_ERRORS.badRequest, "The request has expired (valid_until is in the past).");
|
|
2343
|
-
return { messages, ...validUntil !== void 0 ? { validUntil } : {}, ...o.network !== void 0 ? { network: String(o.network) } : {}, ...typeof o.from === "string" ? { from: o.from } : {} };
|
|
2344
|
-
}
|
|
2345
|
-
function parseSignData(param) {
|
|
2346
|
-
let o;
|
|
2347
|
-
try {
|
|
2348
|
-
o = typeof param === "string" ? JSON.parse(param) : param;
|
|
2349
|
-
} catch {
|
|
2350
|
-
throw new TonConnectError(TONCONNECT_ERRORS.badRequest, "signData params[0] is not valid JSON.");
|
|
2351
|
-
}
|
|
2352
|
-
if (o?.type === "text" && typeof o.text === "string") return { type: "text", text: o.text };
|
|
2353
|
-
if (o?.type === "binary" && typeof o.bytes === "string") return { type: "binary", bytes: o.bytes };
|
|
2354
|
-
if (o?.type === "cell") throw new TonConnectError(TONCONNECT_ERRORS.methodNotSupported, "signData with a cell payload is not supported by this wallet.");
|
|
2355
|
-
throw new TonConnectError(TONCONNECT_ERRORS.badRequest, 'signData payload must be { type: "text", text } or { type: "binary", bytes }.');
|
|
2356
|
-
}
|
|
2357
|
-
var utf82 = (s) => new TextEncoder().encode(s);
|
|
2358
|
-
var int32BE = (n) => {
|
|
2359
|
-
const b = new Uint8Array(4);
|
|
2360
|
-
new DataView(b.buffer).setInt32(0, n, false);
|
|
2361
|
-
return b;
|
|
2362
|
-
};
|
|
2363
|
-
var uint32LE = (n) => {
|
|
2364
|
-
const b = new Uint8Array(4);
|
|
2365
|
-
new DataView(b.buffer).setUint32(0, n, true);
|
|
2366
|
-
return b;
|
|
2367
|
-
};
|
|
2368
|
-
var uint32BE = (n) => {
|
|
2369
|
-
const b = new Uint8Array(4);
|
|
2370
|
-
new DataView(b.buffer).setUint32(0, n, false);
|
|
2371
|
-
return b;
|
|
2372
|
-
};
|
|
2373
|
-
var uint64LE = (n) => {
|
|
2374
|
-
const b = new Uint8Array(8);
|
|
2375
|
-
new DataView(b.buffer).setBigUint64(0, BigInt(n), true);
|
|
2376
|
-
return b;
|
|
2377
|
-
};
|
|
2378
|
-
var uint64BE = (n) => {
|
|
2379
|
-
const b = new Uint8Array(8);
|
|
2380
|
-
new DataView(b.buffer).setBigUint64(0, BigInt(n), false);
|
|
2381
|
-
return b;
|
|
2382
|
-
};
|
|
2383
|
-
var concat = (...parts) => {
|
|
2384
|
-
const out = new Uint8Array(parts.reduce((n, p) => n + p.length, 0));
|
|
2385
|
-
let o = 0;
|
|
2386
|
-
for (const p of parts) {
|
|
2387
|
-
out.set(p, o);
|
|
2388
|
-
o += p.length;
|
|
2389
|
-
}
|
|
2390
|
-
return out;
|
|
2829
|
+
var import_tonconnect = require("@anywalletkit/wallet-ton/tonconnect");
|
|
2830
|
+
var import_tonconnect2 = require("@anywalletkit/wallet-ton/tonconnect");
|
|
2831
|
+
var parts = (a) => {
|
|
2832
|
+
if (a.hash.length !== 32) throw new import_tonconnect.TonConnectError(import_tonconnect.TONCONNECT_ERRORS.unknown, "address hash must be 32 bytes");
|
|
2833
|
+
return { workChain: a.workchain, hash: a.hash };
|
|
2391
2834
|
};
|
|
2392
2835
|
function tonProofMessage(input) {
|
|
2393
|
-
|
|
2394
|
-
const domain = utf82(input.domain);
|
|
2395
|
-
const inner = concat(utf82("ton-proof-item-v2/"), int32BE(input.address.workchain), input.address.hash, uint32LE(domain.length), domain, uint64LE(input.timestamp), utf82(input.payload));
|
|
2396
|
-
return (0, import_sha2.sha256)(concat(new Uint8Array([255, 255]), utf82("ton-connect"), (0, import_sha2.sha256)(inner)));
|
|
2836
|
+
return (0, import_tonconnect.tonProofDigest)(parts(input.address), { domain: input.domain, timestamp: input.timestamp, payload: input.payload });
|
|
2397
2837
|
}
|
|
2398
2838
|
function tonSignDataMessage(input) {
|
|
2399
|
-
|
|
2400
|
-
const domain = utf82(input.domain);
|
|
2401
|
-
const payload = input.payload.type === "text" ? utf82(input.payload.text) : Uint8Array.from(atob(input.payload.bytes), (c) => c.charCodeAt(0));
|
|
2402
|
-
const prefix = utf82(input.payload.type === "text" ? "txt" : "bin");
|
|
2403
|
-
return (0, import_sha2.sha256)(concat(new Uint8Array([255, 255]), utf82("ton-connect/sign-data/"), int32BE(input.address.workchain), input.address.hash, uint32BE(domain.length), domain, uint64BE(input.timestamp), prefix, uint32BE(payload.length), payload));
|
|
2839
|
+
return (0, import_tonconnect.tonSignDataDigest)(parts(input.address), { domain: input.domain, timestamp: input.timestamp, payload: input.payload });
|
|
2404
2840
|
}
|
|
2405
2841
|
function parseRawAddress(raw) {
|
|
2406
|
-
|
|
2407
|
-
|
|
2408
|
-
|
|
2842
|
+
if (!/^-?\d+:[0-9a-fA-F]{64}$/.test(raw)) throw new import_tonconnect.TonConnectError(import_tonconnect.TONCONNECT_ERRORS.unknown, `not a raw TON address: ${raw}`);
|
|
2843
|
+
try {
|
|
2844
|
+
const p = (0, import_tonconnect.tonAddressParts)(raw);
|
|
2845
|
+
return { workchain: p.workChain, hash: p.hash };
|
|
2846
|
+
} catch {
|
|
2847
|
+
throw new import_tonconnect.TonConnectError(import_tonconnect.TONCONNECT_ERRORS.unknown, `not a raw TON address: ${raw}`);
|
|
2848
|
+
}
|
|
2409
2849
|
}
|
|
2410
2850
|
function proofDomain(origin) {
|
|
2411
2851
|
try {
|
|
2412
2852
|
return new URL(origin).host;
|
|
2413
2853
|
} catch {
|
|
2414
|
-
throw new TonConnectError(TONCONNECT_ERRORS.badRequest, `bad origin ${origin}`);
|
|
2854
|
+
throw new import_tonconnect.TonConnectError(import_tonconnect.TONCONNECT_ERRORS.badRequest, `bad origin ${origin}`);
|
|
2415
2855
|
}
|
|
2416
2856
|
}
|
|
2417
2857
|
function tonConnectErrorCode(e) {
|
|
2418
|
-
if (e instanceof TonConnectError) return e.code;
|
|
2858
|
+
if (e instanceof import_tonconnect.TonConnectError || e?.name === "TonConnectError") return e.code;
|
|
2419
2859
|
const code = e?.code;
|
|
2420
|
-
if (code === 4001) return TONCONNECT_ERRORS.userDeclined;
|
|
2421
|
-
if (code === 4100) return TONCONNECT_ERRORS.unknownApp;
|
|
2422
|
-
if (code === 4200) return TONCONNECT_ERRORS.methodNotSupported;
|
|
2423
|
-
if (code === -32602) return TONCONNECT_ERRORS.badRequest;
|
|
2424
|
-
return TONCONNECT_ERRORS.unknown;
|
|
2860
|
+
if (code === 4001) return import_tonconnect.TONCONNECT_ERRORS.userDeclined;
|
|
2861
|
+
if (code === 4100) return import_tonconnect.TONCONNECT_ERRORS.unknownApp;
|
|
2862
|
+
if (code === 4200) return import_tonconnect.TONCONNECT_ERRORS.methodNotSupported;
|
|
2863
|
+
if (code === -32602) return import_tonconnect.TONCONNECT_ERRORS.badRequest;
|
|
2864
|
+
return import_tonconnect.TONCONNECT_ERRORS.unknown;
|
|
2425
2865
|
}
|
|
2426
2866
|
|
|
2427
2867
|
// src/phishing.ts
|
|
@@ -2500,14 +2940,14 @@ function hostOf(hostOrOrigin) {
|
|
|
2500
2940
|
}
|
|
2501
2941
|
}
|
|
2502
2942
|
function matchesList(host, list) {
|
|
2503
|
-
const
|
|
2504
|
-
for (let i = 0; i <
|
|
2943
|
+
const parts2 = host.split(".");
|
|
2944
|
+
for (let i = 0; i < parts2.length - 1; i++) if (list.has(parts2.slice(i).join("."))) return true;
|
|
2505
2945
|
return list.has(host);
|
|
2506
2946
|
}
|
|
2507
2947
|
function stripForFuzzy(host) {
|
|
2508
|
-
const
|
|
2509
|
-
if (
|
|
2510
|
-
return
|
|
2948
|
+
const parts2 = host.replace(/^www\./, "").split(".");
|
|
2949
|
+
if (parts2.length > 1) parts2.pop();
|
|
2950
|
+
return parts2.join(".");
|
|
2511
2951
|
}
|
|
2512
2952
|
function levenshtein(a, b) {
|
|
2513
2953
|
if (a === b) return 0;
|
|
@@ -2648,6 +3088,10 @@ var RecipientBook = class {
|
|
|
2648
3088
|
await this.storage.set(s);
|
|
2649
3089
|
this._state = s;
|
|
2650
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
|
+
}
|
|
2651
3095
|
/** `known` → the address is one the user paid before; `lookalike` → it imitates one. */
|
|
2652
3096
|
async check(namespace, address) {
|
|
2653
3097
|
const list = await this.list(namespace);
|
|
@@ -2655,4 +3099,236 @@ var RecipientBook = class {
|
|
|
2655
3099
|
return { known, lookalike: known ? null : lookalikeOf(address, list) };
|
|
2656
3100
|
}
|
|
2657
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
|
+
}
|
|
2658
3334
|
//# sourceMappingURL=index.cjs.map
|