@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.js
CHANGED
|
@@ -135,7 +135,7 @@ var Vault = class {
|
|
|
135
135
|
if (isEnvelopeV1(envelope)) {
|
|
136
136
|
if (envelope.kdf.iterations < VAULT_MIN_ITERATIONS) throw new VaultError(`The vault's KDF (${envelope.kdf.iterations} iterations) is below the floor this build accepts.`, "weak-kdf");
|
|
137
137
|
const kek2 = await this._derivePbkdf2(password, fromBase64(envelope.kdf.salt), envelope.kdf.iterations);
|
|
138
|
-
const dek = await this._open(kek2, envelope.dek, aadDekV1(envelope.kdf), "wrong-password");
|
|
138
|
+
const dek = await this._proveOwn(await this._open(kek2, envelope.dek, aadDekV1(envelope.kdf), "wrong-password"), envelope);
|
|
139
139
|
if (this._kdf === "scrypt") {
|
|
140
140
|
const next = await this._sealEnvelope(password, dek, envelope.secrets, [], envelope.createdAt);
|
|
141
141
|
await this._write(isEnvelopeV2(next) ? { ...next, upgradedAt: Date.now() } : next);
|
|
@@ -144,7 +144,24 @@ var Vault = class {
|
|
|
144
144
|
}
|
|
145
145
|
if (!isStrongScrypt(envelope.kdf)) throw new VaultError(`The vault's scrypt cost (N=${envelope.kdf.N} r=${envelope.kdf.r} p=${envelope.kdf.p}) is below the floor this build accepts.`, "weak-kdf");
|
|
146
146
|
const kek = await this._deriveScrypt(password, fromBase64(envelope.kdf.salt), envelope.kdf);
|
|
147
|
-
return this._open(kek, envelope.dek, aadDekV2(envelope.kdf), "wrong-password");
|
|
147
|
+
return this._proveOwn(await this._open(kek, envelope.dek, aadDekV2(envelope.kdf), "wrong-password"), envelope);
|
|
148
|
+
}
|
|
149
|
+
/**
|
|
150
|
+
* A DEK unwrapped from a dek / passkey box must be the key this vault's secrets are sealed under
|
|
151
|
+
* (audit 2026-11-09 E21): a box spliced in from another vault, under another password or passkey,
|
|
152
|
+
* opens with that password but holds a foreign key. Proven with the DEK check, or — in an envelope
|
|
153
|
+
* written before it — the first secret; an old empty envelope has nothing to prove it with. Returns
|
|
154
|
+
* `dek`; on a mismatch zeroizes it and fails as `wrong-password` (the same failure as tampering).
|
|
155
|
+
*/
|
|
156
|
+
async _proveOwn(dek, envelope) {
|
|
157
|
+
if (envelope.dekCheck === void 0 && Object.keys(envelope.secrets).length === 0) return dek;
|
|
158
|
+
try {
|
|
159
|
+
await this._requireDek(dek, envelope);
|
|
160
|
+
return dek;
|
|
161
|
+
} catch {
|
|
162
|
+
zeroize(dek);
|
|
163
|
+
throw new VaultError("Wrong password.", "wrong-password");
|
|
164
|
+
}
|
|
148
165
|
}
|
|
149
166
|
/**
|
|
150
167
|
* Re-wrap the DEK under a new password (fresh salt, current KDF). Secrets and passkeys are untouched —
|
|
@@ -176,9 +193,13 @@ var Vault = class {
|
|
|
176
193
|
*
|
|
177
194
|
* Passkeys are dropped — their boxes hold the old DEK and cannot be re-wrapped without their PRF
|
|
178
195
|
* output — and must be registered again ({@link Vault.addPasskey} with the new DEK).
|
|
196
|
+
*
|
|
197
|
+
* With `newPassword` this is also the password change: the new DEK is wrapped under `newPassword`
|
|
198
|
+
* (at least {@link VAULT_MIN_PASSWORD_LENGTH} characters), and `password` must be the current one.
|
|
179
199
|
*/
|
|
180
|
-
async rotateVaultKey(password) {
|
|
200
|
+
async rotateVaultKey(password, newPassword) {
|
|
181
201
|
requirePassword(password);
|
|
202
|
+
if (newPassword !== void 0) requirePassword(newPassword, VAULT_MIN_PASSWORD_LENGTH);
|
|
182
203
|
return this._exclusive(async () => {
|
|
183
204
|
const oldDek = await this._unlock(password);
|
|
184
205
|
const dek = this._random(DEK_BYTES);
|
|
@@ -194,7 +215,7 @@ var Vault = class {
|
|
|
194
215
|
zeroize(plaintext);
|
|
195
216
|
}
|
|
196
217
|
}
|
|
197
|
-
const next = await this._sealEnvelope(password, dek, secrets, [], envelope.createdAt);
|
|
218
|
+
const next = await this._sealEnvelope(newPassword ?? password, dek, secrets, [], envelope.createdAt);
|
|
198
219
|
const upgradedAt = isEnvelopeV2(envelope) ? envelope.upgradedAt : void 0;
|
|
199
220
|
await this._write(isEnvelopeV2(next) ? { ...next, ...upgradedAt !== void 0 ? { upgradedAt } : {}, rotatedAt: Date.now() } : next);
|
|
200
221
|
return dek;
|
|
@@ -258,7 +279,7 @@ var Vault = class {
|
|
|
258
279
|
const entry = envelope.passkeys.find((p) => p.id === id);
|
|
259
280
|
if (!entry) throw new VaultError("No such passkey on this vault.", "missing");
|
|
260
281
|
const key = await this._derivePasskeyKek(prf, id);
|
|
261
|
-
return await this._open(key, entry.dek, aadPasskey(id), "wrong-password");
|
|
282
|
+
return await this._proveOwn(await this._open(key, entry.dek, aadPasskey(id), "wrong-password"), envelope);
|
|
262
283
|
} finally {
|
|
263
284
|
zeroize(prf);
|
|
264
285
|
}
|
|
@@ -553,7 +574,11 @@ var Session = class {
|
|
|
553
574
|
return fromBase64(record.key);
|
|
554
575
|
});
|
|
555
576
|
}
|
|
556
|
-
/**
|
|
577
|
+
/**
|
|
578
|
+
* User activity: push the auto-lock out again. No-op when locked. Call it for explicit user
|
|
579
|
+
* gestures only (unlock, send, approve / decline, an action in the wallet's own UI) — not for
|
|
580
|
+
* passive polls, page requests or worker restarts, or the timer never runs out.
|
|
581
|
+
*/
|
|
557
582
|
async touch() {
|
|
558
583
|
return this._serial(async () => {
|
|
559
584
|
const epoch = this._epoch;
|
|
@@ -567,11 +592,25 @@ var Session = class {
|
|
|
567
592
|
get autoLockMs() {
|
|
568
593
|
return this._autoLockMs;
|
|
569
594
|
}
|
|
570
|
-
/**
|
|
595
|
+
/**
|
|
596
|
+
* Set the timer period (0 disables). Never pushes the deadline out — call this at every worker
|
|
597
|
+
* start with the stored setting: it re-arms the alarm at the stored deadline (a restart must not
|
|
598
|
+
* count as activity, audit 2026-11-09 E11), and only brings the deadline forward when the new
|
|
599
|
+
* period ends sooner. A longer period (or 0) applies from the next {@link Session.touch} /
|
|
600
|
+
* {@link Session.unlock}; call `touch()` right after when the user changed it explicitly.
|
|
601
|
+
*/
|
|
571
602
|
async setAutoLockMs(ms) {
|
|
572
603
|
if (!Number.isFinite(ms) || ms < 0) throw new VaultError("Auto-lock must be a non-negative number of milliseconds.", "invalid-input");
|
|
573
604
|
this._autoLockMs = ms;
|
|
574
|
-
|
|
605
|
+
return this._serial(async () => {
|
|
606
|
+
const epoch = this._epoch;
|
|
607
|
+
const record = await this._read();
|
|
608
|
+
if (!record || epoch !== this._epoch) return;
|
|
609
|
+
const cap = this._expiry();
|
|
610
|
+
const expiresAt = cap !== null && (record.expiresAt === null || cap < record.expiresAt) ? cap : record.expiresAt;
|
|
611
|
+
if (expiresAt !== record.expiresAt) await this._storage.set(this._key, { ...record, expiresAt });
|
|
612
|
+
await this._arm(expiresAt);
|
|
613
|
+
});
|
|
575
614
|
}
|
|
576
615
|
onLock(callback) {
|
|
577
616
|
this._onLock.add(callback);
|
|
@@ -841,7 +880,8 @@ var DEFAULT_NETWORKS = Object.freeze([
|
|
|
841
880
|
net(4326, "MegaETH", "ETH", ["https://mainnet.megaeth.com/rpc"]),
|
|
842
881
|
net(130, "Unichain", "ETH", ["https://mainnet.unichain.org", "https://unichain-rpc.publicnode.com"], "https://uniscan.xyz"),
|
|
843
882
|
net(143, "Monad", "MON", ["https://rpc.monad.xyz"]),
|
|
844
|
-
|
|
883
|
+
// Second operator (Tenderly's public gateway, keyless): rpc.plasma.to alone cannot satisfy a two-operator check.
|
|
884
|
+
net(9745, "Plasma", "XPL", ["https://rpc.plasma.to", "https://plasma.gateway.tenderly.co"]),
|
|
845
885
|
net(59144, "Linea", "ETH", ["https://rpc.linea.build", "https://linea-rpc.publicnode.com"], "https://lineascan.build"),
|
|
846
886
|
net(100, "Gnosis", "xDAI", ["https://rpc.gnosischain.com", "https://gnosis-rpc.publicnode.com"], "https://gnosisscan.io"),
|
|
847
887
|
net(146, "Sonic", "S", ["https://rpc.soniclabs.com", "https://sonic-rpc.publicnode.com"], "https://sonicscan.org"),
|
|
@@ -859,7 +899,11 @@ var DEFAULT_NETWORKS = Object.freeze([
|
|
|
859
899
|
net(5e3, "Mantle", "MNT", ["https://rpc.mantle.xyz", "https://mantle-rpc.publicnode.com"], "https://mantlescan.xyz"),
|
|
860
900
|
net(747474, "Katana", "ETH", ["https://rpc.katana.network"], "https://explorer.katanarpc.com"),
|
|
861
901
|
net(2818, "Morph", "ETH", ["https://rpc.morphl2.io", "https://rpc-quicknode.morphl2.io"], "https://explorer.morphl2.io"),
|
|
862
|
-
net(14, "Flare", "FLR", ["https://flare-api.flare.network/ext/C/rpc", "https://flare.rpc.thirdweb.com"], "https://flare-explorer.flare.network")
|
|
902
|
+
net(14, "Flare", "FLR", ["https://flare-api.flare.network/ext/C/rpc", "https://flare.rpc.thirdweb.com"], "https://flare-explorer.flare.network"),
|
|
903
|
+
// Elysium by Kinetiq (chain 99901, HYPE gas; Arbitrum Orbit on HyperEVM) — public RPC and explorer checked 2026-11-10.
|
|
904
|
+
// The explorer value is the link base (<root>/tx/<hash>, <root>/address/<a>), not LI.FI's blockExplorerUrls
|
|
905
|
+
// https://elysium.kinetiq.xyz/explorer/ (the overview page; the bare root is the Elysium site home).
|
|
906
|
+
net(99901, "Elysium", "HYPE", ["https://rpc.elysium.kinetiq.xyz"], "https://elysium.kinetiq.xyz")
|
|
863
907
|
]);
|
|
864
908
|
var chainHex = (caip2) => `0x${Number(caip2.split(":")[1]).toString(16)}`;
|
|
865
909
|
function caip2FromChainId(value) {
|
|
@@ -935,6 +979,17 @@ var NetworksTable = class {
|
|
|
935
979
|
);
|
|
936
980
|
return this._loading;
|
|
937
981
|
}
|
|
982
|
+
/**
|
|
983
|
+
* Forget the cached table: the next read asks storage again. For a storage whose answer changes under
|
|
984
|
+
* the table — e.g. one that only hands out the user's networks once the wallet is unlocked.
|
|
985
|
+
*/
|
|
986
|
+
reload() {
|
|
987
|
+
return this.serialize(async () => {
|
|
988
|
+
await this._loading?.catch(() => void 0);
|
|
989
|
+
this._custom = null;
|
|
990
|
+
this._loading = null;
|
|
991
|
+
});
|
|
992
|
+
}
|
|
938
993
|
};
|
|
939
994
|
var isSiteRpcUrl = (u) => typeof u === "string" && u.length <= 2048 && isPublicHttpsUrl(u);
|
|
940
995
|
function parseAddChainParams(params, known = isCatalogChain) {
|
|
@@ -1048,6 +1103,21 @@ var PermissionStore = class {
|
|
|
1048
1103
|
return permission;
|
|
1049
1104
|
});
|
|
1050
1105
|
}
|
|
1106
|
+
/**
|
|
1107
|
+
* Add EVM accounts to an origin's grant (per-site grants): the new ones first, the ones it already
|
|
1108
|
+
* holds stay. The chain stays the origin's own when it has one. Used by the router's connect; `connect`
|
|
1109
|
+
* replaces the list instead.
|
|
1110
|
+
*/
|
|
1111
|
+
grant(origin, accounts, chain) {
|
|
1112
|
+
return this.serialize(async () => {
|
|
1113
|
+
if (accounts.length === 0) throw new Error("Connect at least one account.");
|
|
1114
|
+
const current = await this.get(origin);
|
|
1115
|
+
const merged = [...accounts, ...(current?.accounts ?? []).filter((a) => !accounts.some((x) => x.toLowerCase() === a.toLowerCase()))];
|
|
1116
|
+
const permission = { ...current ?? {}, origin, accounts: merged, chain: current?.chain ?? chain, connectedAt: current?.connectedAt ?? Date.now() };
|
|
1117
|
+
await this.write(origin, permission);
|
|
1118
|
+
return permission;
|
|
1119
|
+
});
|
|
1120
|
+
}
|
|
1051
1121
|
/** Connect another chain's accounts to the same origin (one "connected sites" list). */
|
|
1052
1122
|
connectChain(origin, key, accounts, defaultChain) {
|
|
1053
1123
|
return this.serialize(async () => {
|
|
@@ -1080,6 +1150,11 @@ var PermissionStore = class {
|
|
|
1080
1150
|
});
|
|
1081
1151
|
}
|
|
1082
1152
|
/**
|
|
1153
|
+
* @deprecated Rewrites every origin's grant to the active account, which shows that account to sites
|
|
1154
|
+
* the user never connected it to (and links accounts across sites). Grants are per site now: the
|
|
1155
|
+
* router shows an origin its granted accounts that are also active, and connecting another account
|
|
1156
|
+
* adds it to that one origin ({@link grant}). Kept for compatibility; nothing in this package calls it.
|
|
1157
|
+
*
|
|
1083
1158
|
* The active EVM account changed: every origin holding an EVM grant now sees `accounts` (MetaMask behaviour).
|
|
1084
1159
|
* An origin connected only on another chain (`accounts: []` — Solana/TON/Tron/BTC/NEAR providers, a WalletConnect
|
|
1085
1160
|
* session without eip155) is skipped: it never passed an EVM connect and must not learn the EVM address (scan F6/F8).
|
|
@@ -1125,6 +1200,179 @@ var PermissionStore = class {
|
|
|
1125
1200
|
};
|
|
1126
1201
|
var sameList = (a, b) => a.length === b.length && a.every((x, i) => x === b[i]);
|
|
1127
1202
|
|
|
1203
|
+
// src/site.ts
|
|
1204
|
+
var MULTI_LABEL_SUFFIXES = /* @__PURE__ */ new Set([
|
|
1205
|
+
// country second-level registries
|
|
1206
|
+
"co.uk",
|
|
1207
|
+
"org.uk",
|
|
1208
|
+
"me.uk",
|
|
1209
|
+
"ltd.uk",
|
|
1210
|
+
"plc.uk",
|
|
1211
|
+
"net.uk",
|
|
1212
|
+
"ac.uk",
|
|
1213
|
+
"gov.uk",
|
|
1214
|
+
"com.au",
|
|
1215
|
+
"net.au",
|
|
1216
|
+
"org.au",
|
|
1217
|
+
"edu.au",
|
|
1218
|
+
"gov.au",
|
|
1219
|
+
"id.au",
|
|
1220
|
+
"co.nz",
|
|
1221
|
+
"org.nz",
|
|
1222
|
+
"net.nz",
|
|
1223
|
+
"co.jp",
|
|
1224
|
+
"ne.jp",
|
|
1225
|
+
"or.jp",
|
|
1226
|
+
"ac.jp",
|
|
1227
|
+
"go.jp",
|
|
1228
|
+
"co.kr",
|
|
1229
|
+
"or.kr",
|
|
1230
|
+
"ne.kr",
|
|
1231
|
+
"com.br",
|
|
1232
|
+
"net.br",
|
|
1233
|
+
"org.br",
|
|
1234
|
+
"com.cn",
|
|
1235
|
+
"net.cn",
|
|
1236
|
+
"org.cn",
|
|
1237
|
+
"com.hk",
|
|
1238
|
+
"com.tw",
|
|
1239
|
+
"com.sg",
|
|
1240
|
+
"com.my",
|
|
1241
|
+
"co.in",
|
|
1242
|
+
"net.in",
|
|
1243
|
+
"org.in",
|
|
1244
|
+
"co.id",
|
|
1245
|
+
"co.il",
|
|
1246
|
+
"co.th",
|
|
1247
|
+
"co.za",
|
|
1248
|
+
"com.mx",
|
|
1249
|
+
"com.ar",
|
|
1250
|
+
"com.co",
|
|
1251
|
+
"com.tr",
|
|
1252
|
+
"com.ua",
|
|
1253
|
+
"com.pl",
|
|
1254
|
+
"com.ru",
|
|
1255
|
+
"com.vn",
|
|
1256
|
+
"com.ph",
|
|
1257
|
+
"com.pk",
|
|
1258
|
+
"com.ng",
|
|
1259
|
+
"com.eg",
|
|
1260
|
+
"com.sa",
|
|
1261
|
+
"co.ke",
|
|
1262
|
+
// hosting platforms: every customer gets its own subdomain
|
|
1263
|
+
"github.io",
|
|
1264
|
+
"gitlab.io",
|
|
1265
|
+
"vercel.app",
|
|
1266
|
+
"now.sh",
|
|
1267
|
+
"pages.dev",
|
|
1268
|
+
"workers.dev",
|
|
1269
|
+
"r2.dev",
|
|
1270
|
+
"netlify.app",
|
|
1271
|
+
"netlify.com",
|
|
1272
|
+
"herokuapp.com",
|
|
1273
|
+
"web.app",
|
|
1274
|
+
"firebaseapp.com",
|
|
1275
|
+
"appspot.com",
|
|
1276
|
+
"azurewebsites.net",
|
|
1277
|
+
"azurestaticapps.net",
|
|
1278
|
+
"cloudfront.net",
|
|
1279
|
+
"amplifyapp.com",
|
|
1280
|
+
"onrender.com",
|
|
1281
|
+
"fly.dev",
|
|
1282
|
+
"railway.app",
|
|
1283
|
+
"up.railway.app",
|
|
1284
|
+
"deno.dev",
|
|
1285
|
+
"glitch.me",
|
|
1286
|
+
"repl.co",
|
|
1287
|
+
"replit.app",
|
|
1288
|
+
"replit.dev",
|
|
1289
|
+
"surge.sh",
|
|
1290
|
+
"webflow.io",
|
|
1291
|
+
"framer.app",
|
|
1292
|
+
"framer.website",
|
|
1293
|
+
"wixsite.com",
|
|
1294
|
+
"blogspot.com",
|
|
1295
|
+
"myshopify.com",
|
|
1296
|
+
"notion.site",
|
|
1297
|
+
"carrd.co",
|
|
1298
|
+
"gitbook.io",
|
|
1299
|
+
"readthedocs.io",
|
|
1300
|
+
"ngrok.io",
|
|
1301
|
+
"ngrok-free.app",
|
|
1302
|
+
"loca.lt",
|
|
1303
|
+
"trycloudflare.com",
|
|
1304
|
+
// ENS / IPFS gateways: every name or CID is its own site
|
|
1305
|
+
"eth.limo",
|
|
1306
|
+
"eth.link",
|
|
1307
|
+
"ipfs.dweb.link",
|
|
1308
|
+
"ipns.dweb.link",
|
|
1309
|
+
"ipfs.w3s.link",
|
|
1310
|
+
"ipfs.cf-ipfs.com"
|
|
1311
|
+
]);
|
|
1312
|
+
var IPV4 = /^\d{1,3}(\.\d{1,3}){3}$/;
|
|
1313
|
+
function siteOf(origin) {
|
|
1314
|
+
let host;
|
|
1315
|
+
try {
|
|
1316
|
+
const url = new URL(origin);
|
|
1317
|
+
if (url.protocol !== "http:" && url.protocol !== "https:") return origin;
|
|
1318
|
+
host = url.hostname.toLowerCase().replace(/\.$/, "");
|
|
1319
|
+
} catch {
|
|
1320
|
+
return origin;
|
|
1321
|
+
}
|
|
1322
|
+
if (!host || host.startsWith("[") || host.includes(":") || IPV4.test(host)) return host || origin;
|
|
1323
|
+
const labels = host.split(".");
|
|
1324
|
+
if (labels.length <= 2) return host;
|
|
1325
|
+
for (const size of [3, 2]) {
|
|
1326
|
+
if (labels.length <= size) continue;
|
|
1327
|
+
if (MULTI_LABEL_SUFFIXES.has(labels.slice(-size).join("."))) return labels.slice(-(size + 1)).join(".");
|
|
1328
|
+
}
|
|
1329
|
+
return labels.slice(-2).join(".");
|
|
1330
|
+
}
|
|
1331
|
+
var TokenBucket = class {
|
|
1332
|
+
constructor(options) {
|
|
1333
|
+
this.options = options;
|
|
1334
|
+
}
|
|
1335
|
+
options;
|
|
1336
|
+
buckets = /* @__PURE__ */ new Map();
|
|
1337
|
+
take(key, cost = 1) {
|
|
1338
|
+
const now = (this.options.now ?? Date.now)();
|
|
1339
|
+
const b = this.refill(key, now);
|
|
1340
|
+
if (b.tokens < cost) return false;
|
|
1341
|
+
b.tokens -= cost;
|
|
1342
|
+
return true;
|
|
1343
|
+
}
|
|
1344
|
+
/** Tokens `key` holds right now (full for an unknown key). */
|
|
1345
|
+
available(key) {
|
|
1346
|
+
return this.refill(key, (this.options.now ?? Date.now)()).tokens;
|
|
1347
|
+
}
|
|
1348
|
+
refill(key, now) {
|
|
1349
|
+
let b = this.buckets.get(key);
|
|
1350
|
+
if (!b) {
|
|
1351
|
+
if (this.buckets.size >= (this.options.maxKeys ?? 2e3)) this.prune(now);
|
|
1352
|
+
b = { tokens: this.options.capacity, at: now };
|
|
1353
|
+
this.buckets.set(key, b);
|
|
1354
|
+
return b;
|
|
1355
|
+
}
|
|
1356
|
+
const elapsed = Math.max(0, now - b.at) / 1e3;
|
|
1357
|
+
b.tokens = Math.min(this.options.capacity, b.tokens + elapsed * this.options.refillPerSecond);
|
|
1358
|
+
b.at = now;
|
|
1359
|
+
return b;
|
|
1360
|
+
}
|
|
1361
|
+
/** Drop buckets that are full again (they carry no state); the oldest half when every one is in use. */
|
|
1362
|
+
prune(now) {
|
|
1363
|
+
for (const [k, b] of this.buckets) {
|
|
1364
|
+
if (b.tokens + Math.max(0, now - b.at) / 1e3 * this.options.refillPerSecond >= this.options.capacity) this.buckets.delete(k);
|
|
1365
|
+
}
|
|
1366
|
+
if (this.buckets.size >= (this.options.maxKeys ?? 2e3)) {
|
|
1367
|
+
let drop = Math.ceil(this.buckets.size / 2);
|
|
1368
|
+
for (const k of this.buckets.keys()) {
|
|
1369
|
+
if (drop-- <= 0) break;
|
|
1370
|
+
this.buckets.delete(k);
|
|
1371
|
+
}
|
|
1372
|
+
}
|
|
1373
|
+
}
|
|
1374
|
+
};
|
|
1375
|
+
|
|
1128
1376
|
// src/approvals.ts
|
|
1129
1377
|
var ReviewDeclined = class extends Error {
|
|
1130
1378
|
code = RELAY_ERRORS.userRejected;
|
|
@@ -1149,7 +1397,11 @@ var OriginRefused = class extends Error {
|
|
|
1149
1397
|
}
|
|
1150
1398
|
};
|
|
1151
1399
|
var APPROVALS_MAX_PENDING_PER_ORIGIN = 3;
|
|
1152
|
-
var
|
|
1400
|
+
var APPROVALS_MAX_PENDING_PER_SITE = 5;
|
|
1401
|
+
var APPROVALS_MAX_PENDING_TOTAL = 20;
|
|
1402
|
+
var FOLLOW_UP_CREDITS_MAX = 8;
|
|
1403
|
+
var FOLLOW_UP_KEYS_MAX = 256;
|
|
1404
|
+
var ApprovalQueue = class _ApprovalQueue {
|
|
1153
1405
|
constructor(window, newId = () => crypto.randomUUID(), options = {}) {
|
|
1154
1406
|
this.window = window;
|
|
1155
1407
|
this.newId = newId;
|
|
@@ -1157,6 +1409,8 @@ var ApprovalQueue = class {
|
|
|
1157
1409
|
window.onClosed((id) => {
|
|
1158
1410
|
if (this._current?.request.id === id) this.decline(id);
|
|
1159
1411
|
});
|
|
1412
|
+
const rate = options.openRate === false ? null : options.openRate ?? {};
|
|
1413
|
+
this._rate = rate ? new TokenBucket({ capacity: rate.burst ?? 10, refillPerSecond: rate.refillPerSecond ?? 1 / 6, ...options.now ? { now: options.now } : {} }) : null;
|
|
1160
1414
|
}
|
|
1161
1415
|
window;
|
|
1162
1416
|
newId;
|
|
@@ -1165,8 +1419,62 @@ var ApprovalQueue = class {
|
|
|
1165
1419
|
_waiting = [];
|
|
1166
1420
|
_listeners = /* @__PURE__ */ new Set();
|
|
1167
1421
|
_via = /* @__PURE__ */ new Map();
|
|
1422
|
+
_rate;
|
|
1423
|
+
/** Per site: cooldown end and strikes (how many times the bucket ran dry recently). */
|
|
1424
|
+
_cooldown = /* @__PURE__ */ new Map();
|
|
1425
|
+
/** Per origin + kind + chain: approvals that have not been followed up (see {@link ApprovalRequestOptions.followUp}). */
|
|
1426
|
+
_approved = /* @__PURE__ */ new Map();
|
|
1427
|
+
site(origin) {
|
|
1428
|
+
try {
|
|
1429
|
+
return (this.options.siteOf ?? siteOf)(origin);
|
|
1430
|
+
} catch {
|
|
1431
|
+
return origin;
|
|
1432
|
+
}
|
|
1433
|
+
}
|
|
1434
|
+
get all() {
|
|
1435
|
+
return [...this._current ? [this._current] : [], ...this._waiting];
|
|
1436
|
+
}
|
|
1437
|
+
/** The per-site rate: a reason to refuse, or null (and one token spent). */
|
|
1438
|
+
rateRefusal(site) {
|
|
1439
|
+
if (!this._rate) return null;
|
|
1440
|
+
const rate = this.options.openRate || {};
|
|
1441
|
+
const now = (this.options.now ?? Date.now)();
|
|
1442
|
+
const max = rate.maxBackoffMs ?? 6e5;
|
|
1443
|
+
const c = this._cooldown.get(site);
|
|
1444
|
+
if (c && now < c.until) return `This site opened too many requests; try again in ${Math.ceil((c.until - now) / 1e3)} s.`;
|
|
1445
|
+
if (this._rate.take(site)) return null;
|
|
1446
|
+
const strikes = c && now - c.last < max ? c.strikes + 1 : 1;
|
|
1447
|
+
const wait = Math.min(max, (rate.backoffMs ?? 3e4) * 2 ** (strikes - 1));
|
|
1448
|
+
this._cooldown.set(site, { until: now + wait, strikes, last: now });
|
|
1449
|
+
if (this._cooldown.size > 1e3) {
|
|
1450
|
+
for (const [k, v] of this._cooldown) if (now - v.last >= max) this._cooldown.delete(k);
|
|
1451
|
+
}
|
|
1452
|
+
return `This site opened too many requests; try again in ${Math.ceil(wait / 1e3)} s.`;
|
|
1453
|
+
}
|
|
1454
|
+
static followUpKey(r) {
|
|
1455
|
+
return `${r.origin}
|
|
1456
|
+
${r.kind}
|
|
1457
|
+
${r.chain}`;
|
|
1458
|
+
}
|
|
1459
|
+
/** The user approved `r`: it may be followed up once. */
|
|
1460
|
+
approved(r) {
|
|
1461
|
+
const key = _ApprovalQueue.followUpKey(r);
|
|
1462
|
+
const n = Math.min((this._approved.get(key) ?? 0) + 1, FOLLOW_UP_CREDITS_MAX);
|
|
1463
|
+
this._approved.delete(key);
|
|
1464
|
+
this._approved.set(key, n);
|
|
1465
|
+
if (this._approved.size > FOLLOW_UP_KEYS_MAX) this._approved.delete(this._approved.keys().next().value);
|
|
1466
|
+
}
|
|
1467
|
+
/** Spends one approval of the same origin, kind and chain; false when there is none. */
|
|
1468
|
+
spendApproval(r) {
|
|
1469
|
+
const key = _ApprovalQueue.followUpKey(r);
|
|
1470
|
+
const n = this._approved.get(key) ?? 0;
|
|
1471
|
+
if (n <= 0) return false;
|
|
1472
|
+
if (n === 1) this._approved.delete(key);
|
|
1473
|
+
else this._approved.set(key, n - 1);
|
|
1474
|
+
return true;
|
|
1475
|
+
}
|
|
1168
1476
|
/** Enqueue and wait for the user's answer. Refuses (never queues) past the per-origin limits. */
|
|
1169
|
-
request(input) {
|
|
1477
|
+
request(input, opts = {}) {
|
|
1170
1478
|
let refused;
|
|
1171
1479
|
try {
|
|
1172
1480
|
refused = this.options.refuse?.(input.origin);
|
|
@@ -1174,19 +1482,41 @@ var ApprovalQueue = class {
|
|
|
1174
1482
|
refused = `The wallet could not check this site (${e instanceof Error ? e.message : String(e)}).`;
|
|
1175
1483
|
}
|
|
1176
1484
|
if (refused) return Promise.reject(new OriginRefused(refused));
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1485
|
+
let exempt = false;
|
|
1486
|
+
try {
|
|
1487
|
+
exempt = this.options.exempt?.(input.origin) === true;
|
|
1488
|
+
} catch {
|
|
1489
|
+
exempt = false;
|
|
1180
1490
|
}
|
|
1181
|
-
const
|
|
1182
|
-
if (
|
|
1183
|
-
|
|
1491
|
+
const followUp = opts.followUp === true && this.spendApproval(input);
|
|
1492
|
+
if (!exempt && !followUp) {
|
|
1493
|
+
const all = this.all;
|
|
1494
|
+
const sameOrigin = all.filter((p) => p.request.origin === input.origin);
|
|
1495
|
+
if (input.kind === "connect" && sameOrigin.some((p) => p.request.kind === "connect")) {
|
|
1496
|
+
return Promise.reject(new QueueLimitError(RELAY_ERRORS.resourceUnavailable, "A connect request from this site is already waiting for your answer."));
|
|
1497
|
+
}
|
|
1498
|
+
const max = this.options.maxPendingPerOrigin ?? APPROVALS_MAX_PENDING_PER_ORIGIN;
|
|
1499
|
+
if (sameOrigin.length >= max) {
|
|
1500
|
+
return Promise.reject(new QueueLimitError(RELAY_ERRORS.limitExceeded, `This site already has ${max} requests waiting for you; answer or close them first.`));
|
|
1501
|
+
}
|
|
1502
|
+
const site = this.site(input.origin);
|
|
1503
|
+
const maxSite = this.options.maxPendingPerSite ?? APPROVALS_MAX_PENDING_PER_SITE;
|
|
1504
|
+
if (all.filter((p) => this.site(p.request.origin) === site).length >= maxSite) {
|
|
1505
|
+
return Promise.reject(new QueueLimitError(RELAY_ERRORS.limitExceeded, `This site already has ${maxSite} requests waiting for you across its subdomains; answer or close them first.`));
|
|
1506
|
+
}
|
|
1507
|
+
const maxTotal = this.options.maxPendingTotal ?? APPROVALS_MAX_PENDING_TOTAL;
|
|
1508
|
+
if (all.filter((p) => !this.isExempt(p.request.origin)).length >= maxTotal) {
|
|
1509
|
+
return Promise.reject(new QueueLimitError(RELAY_ERRORS.limitExceeded, `The wallet already has ${maxTotal} requests waiting for you; answer or close them first.`));
|
|
1510
|
+
}
|
|
1511
|
+
const slow = this.rateRefusal(site);
|
|
1512
|
+
if (slow) return Promise.reject(new QueueLimitError(RELAY_ERRORS.limitExceeded, slow));
|
|
1184
1513
|
}
|
|
1185
1514
|
return new Promise((resolve, reject) => {
|
|
1186
1515
|
const warnings = this.options.annotate?.(input.origin) ?? [];
|
|
1187
1516
|
const via = this._via.get(input.origin);
|
|
1188
1517
|
const pending = { request: { ...input, id: this.newId(), createdAt: Date.now(), ...warnings.length ? { originWarnings: warnings } : {}, ...via ? { via } : {} }, resolve, reject };
|
|
1189
|
-
this._waiting.
|
|
1518
|
+
if (followUp) this._waiting.unshift(pending);
|
|
1519
|
+
else this._waiting.push(pending);
|
|
1190
1520
|
void this.next();
|
|
1191
1521
|
});
|
|
1192
1522
|
}
|
|
@@ -1199,9 +1529,37 @@ var ApprovalQueue = class {
|
|
|
1199
1529
|
viaFor(origin) {
|
|
1200
1530
|
return this._via.get(origin);
|
|
1201
1531
|
}
|
|
1532
|
+
isExempt(origin) {
|
|
1533
|
+
try {
|
|
1534
|
+
return this.options.exempt?.(origin) === true;
|
|
1535
|
+
} catch {
|
|
1536
|
+
return false;
|
|
1537
|
+
}
|
|
1538
|
+
}
|
|
1202
1539
|
/** Requests waiting for `origin` (current + queued). */
|
|
1203
1540
|
pendingFor(origin) {
|
|
1204
|
-
return
|
|
1541
|
+
return this.all.filter((p) => p.request.origin === origin).length;
|
|
1542
|
+
}
|
|
1543
|
+
/** Requests waiting for any origin of `site` (current + queued). */
|
|
1544
|
+
pendingForSite(site) {
|
|
1545
|
+
return this.all.filter((p) => this.site(p.request.origin) === site).length;
|
|
1546
|
+
}
|
|
1547
|
+
/** The site (registrable domain) the queue's limits put `origin` under. */
|
|
1548
|
+
siteFor(origin) {
|
|
1549
|
+
return this.site(origin);
|
|
1550
|
+
}
|
|
1551
|
+
/** Decline everything waiting for `origin` (its last page closed). Returns how many requests were declined. */
|
|
1552
|
+
declineOrigin(origin, error = new ReviewDeclined("The page was closed \u2014 nothing was signed.")) {
|
|
1553
|
+
return this.declineWhere((p) => p.request.origin === origin, error);
|
|
1554
|
+
}
|
|
1555
|
+
/** "Decline all from this site": everything waiting for any origin of `site` (see {@link siteFor}). */
|
|
1556
|
+
declineSite(site, error = new ReviewDeclined()) {
|
|
1557
|
+
return this.declineWhere((p) => this.site(p.request.origin) === site, error);
|
|
1558
|
+
}
|
|
1559
|
+
declineWhere(match, error) {
|
|
1560
|
+
const ids = [...this._waiting.filter(match), ...this._current && match(this._current) ? [this._current] : []].map((p) => p.request.id);
|
|
1561
|
+
for (const id of ids) this.decline(id, error);
|
|
1562
|
+
return ids.length;
|
|
1205
1563
|
}
|
|
1206
1564
|
/** The request currently shown (the window asks for it by id on load). */
|
|
1207
1565
|
get current() {
|
|
@@ -1211,7 +1569,10 @@ var ApprovalQueue = class {
|
|
|
1211
1569
|
return this._waiting.length;
|
|
1212
1570
|
}
|
|
1213
1571
|
resolve(requestId, value) {
|
|
1214
|
-
return this.settle(requestId, (p) =>
|
|
1572
|
+
return this.settle(requestId, (p) => {
|
|
1573
|
+
this.approved(p.request);
|
|
1574
|
+
p.resolve(value);
|
|
1575
|
+
});
|
|
1215
1576
|
}
|
|
1216
1577
|
decline(requestId, error = new ReviewDeclined()) {
|
|
1217
1578
|
return this.settle(requestId, (p) => p.reject(error));
|
|
@@ -1219,6 +1580,7 @@ var ApprovalQueue = class {
|
|
|
1219
1580
|
/** Lock, disconnect, shutdown: everything pending is declined. */
|
|
1220
1581
|
declineAll(error = new ReviewDeclined("The wallet was locked.")) {
|
|
1221
1582
|
const all = [...this._current ? [this._current] : [], ...this._waiting.splice(0)];
|
|
1583
|
+
this._approved.clear();
|
|
1222
1584
|
for (const p of all) p.reject(error);
|
|
1223
1585
|
if (this._current) void this.window.close(this._current.request.id);
|
|
1224
1586
|
this._current = null;
|
|
@@ -1350,6 +1712,7 @@ var READ_METHODS = /* @__PURE__ */ new Set([
|
|
|
1350
1712
|
"eth_getTransactionReceipt",
|
|
1351
1713
|
"eth_maxPriorityFeePerGas"
|
|
1352
1714
|
]);
|
|
1715
|
+
var GETLOGS_MAX_SPAN_UNCONNECTED = 1e3;
|
|
1353
1716
|
var SIGNING_METHODS = /* @__PURE__ */ new Set(["personal_sign", "eth_signTypedData_v4", "eth_signTypedData_v3", "eth_signTypedData", "eth_sendTransaction"]);
|
|
1354
1717
|
var RequestRouter = class {
|
|
1355
1718
|
constructor(deps) {
|
|
@@ -1379,8 +1742,10 @@ var RequestRouter = class {
|
|
|
1379
1742
|
return this.visibleAccounts(permission);
|
|
1380
1743
|
case "eth_requestAccounts":
|
|
1381
1744
|
return this.connect(origin, network);
|
|
1382
|
-
case "wallet_getPermissions":
|
|
1383
|
-
|
|
1745
|
+
case "wallet_getPermissions": {
|
|
1746
|
+
const visible = await this.visibleAccounts(permission);
|
|
1747
|
+
return visible.length > 0 ? [permissionDescriptor(origin, visible)] : [];
|
|
1748
|
+
}
|
|
1384
1749
|
case "wallet_requestPermissions": {
|
|
1385
1750
|
const accounts = await this.connect(origin, network);
|
|
1386
1751
|
return [permissionDescriptor(origin, accounts)];
|
|
@@ -1421,11 +1786,20 @@ var RequestRouter = class {
|
|
|
1421
1786
|
case "eth_signTransaction":
|
|
1422
1787
|
throw new RouterError(RELAY_ERRORS.unsupportedMethod, `${method} (blind signing) is never offered by this wallet.`);
|
|
1423
1788
|
}
|
|
1424
|
-
if (READ_METHODS.has(method))
|
|
1789
|
+
if (READ_METHODS.has(method)) {
|
|
1790
|
+
if (method === "eth_getLogs" && !(permission && permission.accounts.length > 0)) await this.boundLogs(network, params);
|
|
1791
|
+
return this.deps.rpc(network, method, params);
|
|
1792
|
+
}
|
|
1425
1793
|
if (SIGNING_METHODS.has(method)) {
|
|
1426
1794
|
if (!this.deps.sign) throw new RouterError(RELAY_ERRORS.unsupportedMethod, `${method} is not available yet.`);
|
|
1427
|
-
if (!permission) throw new RouterError(RELAY_ERRORS.unauthorized, "Connect the wallet first (eth_requestAccounts).");
|
|
1428
|
-
|
|
1795
|
+
if (!permission || permission.accounts.length === 0) throw new RouterError(RELAY_ERRORS.unauthorized, "Connect the wallet first (eth_requestAccounts).");
|
|
1796
|
+
const visible = await this.visibleAccounts(permission);
|
|
1797
|
+
const named = namedAccount(method, params);
|
|
1798
|
+
const inactive = named !== void 0 && permission.accounts.some((a) => a.toLowerCase() === named.toLowerCase()) && !visible.some((a) => a.toLowerCase() === named.toLowerCase());
|
|
1799
|
+
if (visible.length === 0 || inactive) {
|
|
1800
|
+
throw new RouterError(RELAY_ERRORS.unauthorized, "The account this site asks for is not the wallet's active account (or the wallet is locked). Switch to it in the wallet, or connect the active account (eth_requestAccounts).");
|
|
1801
|
+
}
|
|
1802
|
+
return this.deps.sign({ origin, network, method, params, accounts: visible });
|
|
1429
1803
|
}
|
|
1430
1804
|
throw new RouterError(RELAY_ERRORS.unsupportedMethod, `This wallet does not support ${method}.`);
|
|
1431
1805
|
}
|
|
@@ -1435,9 +1809,31 @@ var RequestRouter = class {
|
|
|
1435
1809
|
const available = await this.deps.accounts();
|
|
1436
1810
|
return permission.accounts.filter((a) => available.includes(a));
|
|
1437
1811
|
}
|
|
1812
|
+
/** An unconnected origin's `eth_getLogs`: a block hash, or a range of at most {@link GETLOGS_MAX_SPAN_UNCONNECTED} blocks. */
|
|
1813
|
+
async boundLogs(network, params) {
|
|
1814
|
+
const filter = params[0] ?? {};
|
|
1815
|
+
if (typeof filter !== "object" || filter === null) throw new RouterError(RELAY_ERRORS.invalidParams, "eth_getLogs expects a filter object.");
|
|
1816
|
+
if (filter.blockHash !== void 0 && filter.blockHash !== null) return;
|
|
1817
|
+
let head;
|
|
1818
|
+
const block = async (tag) => {
|
|
1819
|
+
if (tag === void 0 || tag === null || tag === "latest" || tag === "pending" || tag === "safe" || tag === "finalized") {
|
|
1820
|
+
head ??= BigInt(String(await this.deps.rpc(network, "eth_blockNumber", [])));
|
|
1821
|
+
return head;
|
|
1822
|
+
}
|
|
1823
|
+
if (tag === "earliest") return 0n;
|
|
1824
|
+
if (typeof tag === "string" && /^0x[0-9a-fA-F]{1,16}$/.test(tag)) return BigInt(tag);
|
|
1825
|
+
throw new RouterError(RELAY_ERRORS.invalidParams, "eth_getLogs: fromBlock / toBlock must be a block tag or a hex block number.");
|
|
1826
|
+
};
|
|
1827
|
+
const from = await block(filter.fromBlock);
|
|
1828
|
+
const to = await block(filter.toBlock);
|
|
1829
|
+
if (to - from > BigInt(GETLOGS_MAX_SPAN_UNCONNECTED)) {
|
|
1830
|
+
throw new RouterError(RELAY_ERRORS.limitExceeded, `eth_getLogs over more than ${GETLOGS_MAX_SPAN_UNCONNECTED} blocks is answered for connected sites only; narrow the range or connect the wallet.`);
|
|
1831
|
+
}
|
|
1832
|
+
}
|
|
1438
1833
|
/**
|
|
1439
1834
|
* Connected and unlocked → the accounts; otherwise the approval window (which
|
|
1440
|
-
* unlocks first when needed) picks them
|
|
1835
|
+
* unlocks first when needed) picks them, and they are ADDED to this origin's
|
|
1836
|
+
* grant (other origins are untouched). Emits `connect` + `accountsChanged`.
|
|
1441
1837
|
*/
|
|
1442
1838
|
async connect(origin, network) {
|
|
1443
1839
|
const existing = await this.visibleAccounts(await this.deps.permissions.get(origin));
|
|
@@ -1446,7 +1842,7 @@ var RequestRouter = class {
|
|
|
1446
1842
|
const available = await this.deps.accounts();
|
|
1447
1843
|
const accounts = Array.isArray(chosen) ? chosen.filter((a) => typeof a === "string" && available.includes(a)) : [];
|
|
1448
1844
|
if (accounts.length === 0) throw new ReviewDeclined("No account was selected.");
|
|
1449
|
-
await this.deps.permissions.
|
|
1845
|
+
await this.deps.permissions.grant(origin, accounts, network.caip2);
|
|
1450
1846
|
this.deps.emit(origin, "connect", { chainId: chainHex(network.caip2) });
|
|
1451
1847
|
this.deps.emit(origin, "accountsChanged", accounts);
|
|
1452
1848
|
return accounts;
|
|
@@ -1466,6 +1862,12 @@ var RequestRouter = class {
|
|
|
1466
1862
|
this.deps.emit(origin, "chainChanged", chainHex(to.caip2));
|
|
1467
1863
|
}
|
|
1468
1864
|
};
|
|
1865
|
+
function namedAccount(method, params) {
|
|
1866
|
+
const addr = (x) => typeof x === "string" && /^0x[0-9a-fA-F]{40}$/.test(x) ? x : void 0;
|
|
1867
|
+
if (method === "personal_sign") return addr(params[1]) ?? addr(params[0]);
|
|
1868
|
+
if (method === "eth_sendTransaction") return addr(params[0]?.from);
|
|
1869
|
+
return addr(params[0]);
|
|
1870
|
+
}
|
|
1469
1871
|
var permissionDescriptor = (origin, accounts) => ({
|
|
1470
1872
|
id: crypto.randomUUID(),
|
|
1471
1873
|
parentCapability: "eth_accounts",
|
|
@@ -1475,6 +1877,7 @@ var permissionDescriptor = (origin, accounts) => ({
|
|
|
1475
1877
|
});
|
|
1476
1878
|
|
|
1477
1879
|
// src/siwe.ts
|
|
1880
|
+
var SIWE_FORBIDDEN_CHARS = /[\u061c\u200b-\u200f\u202a-\u202e\u2060-\u2064\u2066-\u2069\ufeff]/;
|
|
1478
1881
|
var HEADER_LINE = /^(?<domain>[^\n]+?) wants you to sign in with your Ethereum account:(?:\n|$)/;
|
|
1479
1882
|
var ADDRESS_LINE = /^0x[0-9a-fA-F]{40}$/;
|
|
1480
1883
|
var FIELD = /^([A-Za-z ]+): (.*)$/;
|
|
@@ -1578,12 +1981,20 @@ function siweProblems(m, ctx) {
|
|
|
1578
1981
|
}
|
|
1579
1982
|
return out;
|
|
1580
1983
|
}
|
|
1984
|
+
function siweCrossSite(m, ctx) {
|
|
1985
|
+
const site = originWhere(ctx.origin) ?? (ctx.via?.verify === "VALID" && !ctx.via.scam ? whereOf(ctx.via.url) : null);
|
|
1986
|
+
if (!site) return null;
|
|
1987
|
+
const domain = whereOf(m.domain);
|
|
1988
|
+
return !domain || domain.host !== site.host ? { domain: m.domain, origin: `${site.scheme}://${site.host}` } : null;
|
|
1989
|
+
}
|
|
1581
1990
|
function describeSiwe(text, ctx) {
|
|
1582
1991
|
const p = parseLoose(text);
|
|
1583
1992
|
if (!p) return null;
|
|
1584
1993
|
const problems = siweProblems(p.message, ctx);
|
|
1585
|
-
|
|
1586
|
-
|
|
1994
|
+
const cross = siweCrossSite(p.message, ctx);
|
|
1995
|
+
const crossSite = cross ? { crossSite: cross } : {};
|
|
1996
|
+
if (p.missing.length === 0) return { message: p.message, problems, ...crossSite };
|
|
1997
|
+
return { message: p.message, problems: [`This is a malformed sign-in message (EIP-4361): ${p.missing.join(", ")} missing or unreadable \u2014 check the site before signing.`, ...problems], malformed: true, ...crossSite };
|
|
1587
1998
|
}
|
|
1588
1999
|
|
|
1589
2000
|
// src/signing.ts
|
|
@@ -1591,18 +2002,31 @@ var safeNonce = (n) => {
|
|
|
1591
2002
|
if (n < 0n || n > BigInt(Number.MAX_SAFE_INTEGER)) throw new Error(`The RPC returned an impossible nonce (${n}).`);
|
|
1592
2003
|
return Number(n.toString());
|
|
1593
2004
|
};
|
|
2005
|
+
var MAX_FEE_REASKS = 3;
|
|
1594
2006
|
var ADDRESS = /^0x[0-9a-fA-F]{40}$/;
|
|
1595
2007
|
var HEX = /^0x([0-9a-fA-F]{2})*$/;
|
|
1596
2008
|
var lc = (s) => s.toLowerCase();
|
|
2009
|
+
var INVISIBLE = /[\p{Cf}\u2028\u2029\u115f\u1160\u3164\uffa0\x00-\x08\x0b\x0c\x0e-\x1f\x7f-\x9f]/gu;
|
|
2010
|
+
function revealHidden(text) {
|
|
2011
|
+
let hidden = false;
|
|
2012
|
+
const out = text.replace(INVISIBLE, (c) => {
|
|
2013
|
+
hidden = true;
|
|
2014
|
+
return `\u27E8U+${c.codePointAt(0).toString(16).toUpperCase().padStart(4, "0")}\u27E9`;
|
|
2015
|
+
});
|
|
2016
|
+
return { text: out, hidden };
|
|
2017
|
+
}
|
|
1597
2018
|
function messageText(value) {
|
|
1598
|
-
|
|
1599
|
-
|
|
2019
|
+
return shownMessage(value).text;
|
|
2020
|
+
}
|
|
2021
|
+
function shownMessage(value) {
|
|
2022
|
+
if (typeof value !== "string") return { text: "", hidden: false };
|
|
2023
|
+
if (!HEX.test(value)) return revealHidden(value);
|
|
1600
2024
|
try {
|
|
1601
2025
|
const bytes = Uint8Array.from(value.slice(2).match(/../g) ?? [], (b) => Number.parseInt(b, 16));
|
|
1602
2026
|
const s = new TextDecoder("utf-8", { fatal: true }).decode(bytes);
|
|
1603
|
-
return /^[\x09\x0a\x0d\x20-\x7e
|
|
2027
|
+
return /^[\x09\x0a\x0d\x20-\x7e\u00a0-\uffff]*$/.test(s) ? revealHidden(s) : { text: value, hidden: false };
|
|
1604
2028
|
} catch {
|
|
1605
|
-
return value;
|
|
2029
|
+
return { text: value, hidden: false };
|
|
1606
2030
|
}
|
|
1607
2031
|
}
|
|
1608
2032
|
function decodedMessage(value) {
|
|
@@ -1616,6 +2040,7 @@ function decodedMessage(value) {
|
|
|
1616
2040
|
}
|
|
1617
2041
|
var HIDDEN_CONTROL = /[\x00-\x08\x0b\x0c\x0e-\x1f\x7f-\x9f]/;
|
|
1618
2042
|
var err = (code, message) => ({ code, message });
|
|
2043
|
+
var BLIND_HASH = /^0x[0-9a-fA-F]{64}$/;
|
|
1619
2044
|
function classifySignRequest(input) {
|
|
1620
2045
|
const { method, params } = input;
|
|
1621
2046
|
const owned = new Set(input.accounts.map(lc));
|
|
@@ -1626,11 +2051,15 @@ function classifySignRequest(input) {
|
|
|
1626
2051
|
if (ours(msg) && !ours(addr)) [msg, addr] = [addr, msg];
|
|
1627
2052
|
if (!ours(addr)) return err(RELAY_ERRORS.unauthorized, "personal_sign for an address this wallet does not hold.");
|
|
1628
2053
|
if (typeof msg !== "string") return err(RELAY_ERRORS.invalidParams, "personal_sign expects a message string.");
|
|
1629
|
-
const
|
|
2054
|
+
const blindHash = BLIND_HASH.test(msg);
|
|
2055
|
+
if (blindHash && !input.allowBlindHash) return err(RELAY_ERRORS.unsupportedMethod, 'This site asks you to sign a raw 32-byte hash (blind signing). It can be a swap order or a transfer (e.g. CoW Protocol) paying someone else, so this wallet refuses it. "Allow blind hash signatures" in Options turns on a review for such requests.');
|
|
2056
|
+
const shown = shownMessage(msg);
|
|
2057
|
+
const text = shown.text;
|
|
1630
2058
|
const signed = decodedMessage(msg) ?? text;
|
|
1631
2059
|
const siwe = input.origin ? describeSiwe(signed, { origin: input.origin, account: checksum(addr), chainId: input.network.chainId, ...input.via ? { via: input.via } : {}, ...input.now !== void 0 ? { now: input.now } : {} }) : null;
|
|
1632
2060
|
if (siwe && HIDDEN_CONTROL.test(signed)) return err(RELAY_ERRORS.invalidParams, "This sign-in message contains hidden control characters \u2014 refused.");
|
|
1633
|
-
|
|
2061
|
+
if (siwe && SIWE_FORBIDDEN_CHARS.test(signed)) return err(RELAY_ERRORS.invalidParams, "This sign-in message contains invisible or text-direction characters \u2014 refused.");
|
|
2062
|
+
return { kind: "sign-message", method, account: checksum(addr), text, raw: msg, ...siwe ? { siwe } : {}, ...shown.hidden ? { hiddenChars: true } : {}, ...blindHash ? { blindHash: true } : {} };
|
|
1634
2063
|
}
|
|
1635
2064
|
if (method === "eth_signTypedData_v4" || method === "eth_signTypedData_v3" || method === "eth_signTypedData") {
|
|
1636
2065
|
const [addr, data] = params;
|
|
@@ -1648,7 +2077,8 @@ function classifySignRequest(input) {
|
|
|
1648
2077
|
if (cid !== void 0 && cid !== null && Number(cid) !== input.network.chainId) {
|
|
1649
2078
|
return err(RELAY_ERRORS.chainDisconnected, `Typed data is for chain ${String(cid)}; this site is on ${input.network.name} (${input.network.chainId}).`);
|
|
1650
2079
|
}
|
|
1651
|
-
|
|
2080
|
+
const shown = revealHidden(JSON.stringify(parsed, bigintSafe, 2));
|
|
2081
|
+
return { kind: "sign-typed-data", method, account: checksum(addr), text: shown.text, typedData: parsed, ...shown.hidden ? { hiddenChars: true } : {} };
|
|
1652
2082
|
}
|
|
1653
2083
|
if (method === "eth_sendTransaction") {
|
|
1654
2084
|
const tx = params[0] ?? {};
|
|
@@ -1697,16 +2127,33 @@ function overpriced(shown, fresh) {
|
|
|
1697
2127
|
if (shown.maxFeePerGas !== void 0 && fresh.maxFeePerGas !== void 0) return big(shown.maxFeePerGas) > STALE_FEE_FACTOR * big(fresh.maxFeePerGas);
|
|
1698
2128
|
return false;
|
|
1699
2129
|
}
|
|
2130
|
+
function gasLimitExcessive(limit, estimate) {
|
|
2131
|
+
const a = estimate * 3n / 2n;
|
|
2132
|
+
const b = estimate + 50000n;
|
|
2133
|
+
return limit > (a > b ? a : b);
|
|
2134
|
+
}
|
|
1700
2135
|
async function prepareTransaction(deps, network, from, tx) {
|
|
1701
2136
|
const call = { from, to: tx.to, value: hex(BigInt(tx.value)), ...tx.data ? { data: tx.data } : {} };
|
|
2137
|
+
const estimate = () => deps.rpc(network, "eth_estimateGas", [call]);
|
|
1702
2138
|
const [nonceHex, block, gasHex] = await Promise.all([
|
|
1703
2139
|
deps.rpc(network, "eth_getTransactionCount", [from, "pending"]),
|
|
1704
2140
|
deps.rpc(network, "eth_getBlockByNumber", ["latest", false]),
|
|
1705
|
-
tx.gas ?
|
|
2141
|
+
tx.gas ? estimate().then((v) => v, () => null) : estimate()
|
|
1706
2142
|
]);
|
|
1707
|
-
const
|
|
1708
|
-
|
|
1709
|
-
|
|
2143
|
+
const nonce = safeNonce(big(nonceHex));
|
|
2144
|
+
if (!tx.gas) {
|
|
2145
|
+
const estimated = big(gasHex);
|
|
2146
|
+
return withFees(network.chainId, nonce, (estimated * 120n + 99n) / 100n, await readFees(deps, network, block));
|
|
2147
|
+
}
|
|
2148
|
+
const limit = big(tx.gas);
|
|
2149
|
+
const prepared = withFees(network.chainId, nonce, limit, await readFees(deps, network, block));
|
|
2150
|
+
let est = null;
|
|
2151
|
+
try {
|
|
2152
|
+
est = typeof gasHex === "string" ? big(gasHex) : null;
|
|
2153
|
+
} catch {
|
|
2154
|
+
est = null;
|
|
2155
|
+
}
|
|
2156
|
+
return est !== null && est > 0n ? { ...prepared, gasCheck: { estimate: hex(est), excessive: gasLimitExcessive(limit, est) } } : prepared;
|
|
1710
2157
|
}
|
|
1711
2158
|
function createSignHandler(deps) {
|
|
1712
2159
|
const tails = /* @__PURE__ */ new Map();
|
|
@@ -1722,7 +2169,8 @@ function createSignHandler(deps) {
|
|
|
1722
2169
|
};
|
|
1723
2170
|
return async ({ origin, network, method, params, accounts }) => {
|
|
1724
2171
|
const via = deps.approvals.viaFor(origin);
|
|
1725
|
-
const
|
|
2172
|
+
const allowBlindHash = method === "personal_sign" && deps.allowBlindHash ? await deps.allowBlindHash() : false;
|
|
2173
|
+
const info = classifySignRequest({ network, method, params, accounts, origin, ...via ? { via } : {}, ...allowBlindHash ? { allowBlindHash: true } : {} });
|
|
1726
2174
|
if ("code" in info) throw RouterError.from(info);
|
|
1727
2175
|
const prepared = info.kind === "send-transaction" ? await prepareTransaction(deps, network, info.account, info.tx) : void 0;
|
|
1728
2176
|
const preview = deps.preview ? await deps.preview({ network, request: info, accounts }).catch(() => null) : null;
|
|
@@ -1732,14 +2180,20 @@ function createSignHandler(deps) {
|
|
|
1732
2180
|
const key = `${network.caip2}|${lc(info.account)}`;
|
|
1733
2181
|
return serial(key, async () => {
|
|
1734
2182
|
let shown = prepared;
|
|
2183
|
+
let reasks = 0;
|
|
1735
2184
|
for (; ; ) {
|
|
1736
2185
|
const [nonceHex, fees] = await Promise.all([deps.rpc(network, "eth_getTransactionCount", [info.account, "pending"]), readFees(deps, network)]);
|
|
1737
2186
|
const local = nextNonce.get(key) ?? 0;
|
|
1738
2187
|
const nodeNonce = safeNonce(big(nonceHex));
|
|
1739
2188
|
const nonce = nodeNonce > local ? nodeNonce : local;
|
|
1740
2189
|
if (outgrown(shown, fees)) {
|
|
1741
|
-
|
|
1742
|
-
|
|
2190
|
+
const next = withFees(network.chainId, nonce, big(shown.gasLimit), fees);
|
|
2191
|
+
const ceiling = deps.feeCeiling?.(network);
|
|
2192
|
+
if (ceiling !== void 0 && BigInt(next.maxFee) > ceiling) throw new RouterError(RELAY_ERRORS.internal, `The network fee rose above the wallet's limit for ${network.name}; nothing was signed.`);
|
|
2193
|
+
if (reasks >= MAX_FEE_REASKS) throw new RouterError(RELAY_ERRORS.internal, "The network fee kept rising while you were reviewing; nothing was signed. Try again later.");
|
|
2194
|
+
reasks++;
|
|
2195
|
+
await deps.approvals.request({ origin, chain: network.caip2, kind: info.kind, payload: { ...payload, prepared: next, repriced: true, repricedFrom: shown.maxFee } }, { followUp: true });
|
|
2196
|
+
shown = next;
|
|
1743
2197
|
continue;
|
|
1744
2198
|
}
|
|
1745
2199
|
const fresh = withFees(network.chainId, nonce, big(shown.gasLimit), fees);
|
|
@@ -1827,7 +2281,7 @@ function chainTxMatcher(expected) {
|
|
|
1827
2281
|
}
|
|
1828
2282
|
|
|
1829
2283
|
// src/swap.ts
|
|
1830
|
-
import { validQuote } from "@anywalletkit/protocol-swap2-core";
|
|
2284
|
+
import { slippageFloor, validQuote } from "@anywalletkit/protocol-swap2-core";
|
|
1831
2285
|
import { typedDataHash } from "@anywalletkit/wallet-evm";
|
|
1832
2286
|
async function quoteRoutes(providers, params, options = {}) {
|
|
1833
2287
|
const timeoutMs = options.timeoutMs ?? 2e4;
|
|
@@ -1861,7 +2315,14 @@ async function quoteRoutes(providers, params, options = {}) {
|
|
|
1861
2315
|
candidates.push(p);
|
|
1862
2316
|
}
|
|
1863
2317
|
const pending = new Set(candidates.map((p) => p.name));
|
|
1864
|
-
const
|
|
2318
|
+
const depositAddressRoute = options.depositAddressRoute;
|
|
2319
|
+
const rankOptions = {
|
|
2320
|
+
...params.slippageBps !== void 0 ? { slippageBps: params.slippageBps } : {},
|
|
2321
|
+
...options.depositAddressProviders ? { depositAddressProviders: options.depositAddressProviders } : {},
|
|
2322
|
+
...depositAddressRoute ? { depositAddressRoute: (q) => depositAddressRoute(q, params) } : {}
|
|
2323
|
+
};
|
|
2324
|
+
const { slippageBps: _slippage, ...shapeParams } = params;
|
|
2325
|
+
const view = () => ({ routes: rank(Object.values(quotes), rankOptions), pending: [...pending], refusals: { ...refusals }, quotes: { ...quotes } });
|
|
1865
2326
|
const emit = () => options.onUpdate?.(view());
|
|
1866
2327
|
emit();
|
|
1867
2328
|
await Promise.all(
|
|
@@ -1876,7 +2337,7 @@ async function quoteRoutes(providers, params, options = {}) {
|
|
|
1876
2337
|
const q = await Promise.race([p.getQuote({ ...params, dry: true, onRefusal: (r) => {
|
|
1877
2338
|
refusal = r;
|
|
1878
2339
|
} }, ctrl.signal), aborted]);
|
|
1879
|
-
if (q && validQuote(q, { ...
|
|
2340
|
+
if (q && validQuote(q, { ...shapeParams, dry: true }, p.name)) quotes[p.name] = q;
|
|
1880
2341
|
else refusals[p.name] = refusal ?? (q ? "returned an invalid quote" : "no route for this pair right now");
|
|
1881
2342
|
} catch (e) {
|
|
1882
2343
|
refusals[p.name] = ctrl.signal.aborted ? String(ctrl.signal.reason instanceof Error ? ctrl.signal.reason.message : ctrl.signal.reason ?? "cancelled") : e instanceof Error ? e.message : String(e);
|
|
@@ -1890,16 +2351,50 @@ async function quoteRoutes(providers, params, options = {}) {
|
|
|
1890
2351
|
);
|
|
1891
2352
|
return view();
|
|
1892
2353
|
}
|
|
1893
|
-
function
|
|
2354
|
+
function gapBps(dest, min) {
|
|
2355
|
+
if (dest <= 0n || min >= dest) return 0;
|
|
2356
|
+
return Number((((dest - min) * 10000n + dest - 1n) / dest).toString());
|
|
2357
|
+
}
|
|
2358
|
+
function rank(quotes, options = {}) {
|
|
1894
2359
|
const sorted = [...quotes].sort((a, b) => BigInt(b.destAmount) > BigInt(a.destAmount) ? 1 : BigInt(b.destAmount) < BigInt(a.destAmount) ? -1 : a.feeUsd - b.feeUsd || a.etaSeconds - b.etaSeconds);
|
|
1895
2360
|
const fastest = sorted.length ? sorted.reduce((f, q) => q.etaSeconds < f.etaSeconds ? q : f) : null;
|
|
1896
|
-
|
|
2361
|
+
const depositProviders = new Set(options.depositAddressProviders ?? []);
|
|
2362
|
+
const paysAssignedAddress = (q) => {
|
|
2363
|
+
if (depositProviders.has(q.provider)) return true;
|
|
2364
|
+
if (!options.depositAddressRoute) return false;
|
|
2365
|
+
try {
|
|
2366
|
+
return options.depositAddressRoute(q) === true;
|
|
2367
|
+
} catch {
|
|
2368
|
+
return true;
|
|
2369
|
+
}
|
|
2370
|
+
};
|
|
2371
|
+
const assigned = new Map(sorted.map((q) => [q, paysAssignedAddress(q)]));
|
|
2372
|
+
const s = options.slippageBps;
|
|
2373
|
+
const warningsOf = (q) => {
|
|
2374
|
+
const w = [];
|
|
2375
|
+
if (s !== void 0 && Number.isInteger(s) && s >= 0 && s < 1e4) {
|
|
2376
|
+
const dest = BigInt(q.destAmount);
|
|
2377
|
+
const min = BigInt(q.minDestAmount);
|
|
2378
|
+
if (min < slippageFloor(dest, s)) w.push({ kind: "min-below-slippage", slippageBps: s, minGapBps: gapBps(dest, min) });
|
|
2379
|
+
}
|
|
2380
|
+
if (assigned.get(q)) w.push({ kind: "deposit-address" });
|
|
2381
|
+
return w;
|
|
2382
|
+
};
|
|
2383
|
+
const warned = new Map(sorted.map((q) => [q, warningsOf(q)]));
|
|
2384
|
+
const verifiableExists = sorted.some((q) => !assigned.get(q));
|
|
2385
|
+
const best = sorted.find((q) => {
|
|
2386
|
+
const w = warned.get(q);
|
|
2387
|
+
return !w.some((x) => x.kind === "min-below-slippage") && !(w.some((x) => x.kind === "deposit-address") && verifiableExists);
|
|
2388
|
+
});
|
|
2389
|
+
const ordered = best ? [best, ...sorted.filter((q) => q !== best)] : sorted;
|
|
2390
|
+
return ordered.map((q) => {
|
|
1897
2391
|
const poolFee = q.providerData?.poolFee;
|
|
1898
2392
|
const feeNote = typeof poolFee === "string" && poolFee && q.feeUsd === 0 ? poolFee === "pools" ? "pool fees in price" : `pool ${poolFee} in price` : void 0;
|
|
1899
2393
|
const tags = [];
|
|
1900
|
-
if (
|
|
2394
|
+
if (q === best) tags.push("best");
|
|
1901
2395
|
if (fastest && q.provider === fastest.provider) tags.push("fastest");
|
|
1902
|
-
|
|
2396
|
+
const warnings = warned.get(q);
|
|
2397
|
+
return { provider: q.provider, destAmount: q.destAmount, minDestAmount: q.minDestAmount, feeUsd: q.feeUsd, etaSeconds: q.etaSeconds, validUntilMs: q.validUntilMs, tags, ...feeNote ? { feeNote } : {}, ...warnings.length ? { warnings } : {} };
|
|
1903
2398
|
});
|
|
1904
2399
|
}
|
|
1905
2400
|
var FLOOR_TOLERANCE_BPS = 50n;
|
|
@@ -1918,18 +2413,21 @@ function depositOf(s, approvals) {
|
|
|
1918
2413
|
if (!s.data.startsWith(TRANSFER_SELECTOR) || s.data.length < 10 + 64) return void 0;
|
|
1919
2414
|
return `0x${s.data.slice(10 + 24, 10 + 64)}`;
|
|
1920
2415
|
}
|
|
2416
|
+
var pct = (bps) => `${Number((bps / 100).toFixed(2))}%`;
|
|
1921
2417
|
function swapApprovalPayload(plan, meta) {
|
|
1922
2418
|
const isOwn = meta.own.some((a) => lc3(a) === lc3(plan.recipient));
|
|
2419
|
+
const assigned = meta.depositAddressAssignedByProvider === true;
|
|
1923
2420
|
const checks = [{ status: "ok", label: `Built by the ${meta.providerLabel} module` }];
|
|
2421
|
+
const depositCheck = assigned ? { status: "warn", label: `Deposit address assigned by the ${meta.providerLabel} server \u2014 funds go to an address the wallet cannot verify` } : { status: "ok", label: `Deposit address from the ${meta.providerLabel} quote` };
|
|
1924
2422
|
const s = plan.source;
|
|
1925
2423
|
let source;
|
|
1926
2424
|
if (s.kind === "call") {
|
|
1927
2425
|
const deposit = depositOf(s, plan.approvals.length);
|
|
1928
2426
|
source = { kind: "call", to: s.to, ...s.data ? { data: s.data } : {}, value: s.value.toString(), ...deposit ? { deposit } : {} };
|
|
1929
|
-
checks.push(deposit
|
|
2427
|
+
checks.push(deposit || assigned ? depositCheck : { status: "ok", label: `Calls the ${meta.providerLabel} contract` });
|
|
1930
2428
|
} else if (s.kind === "token-transfer") {
|
|
1931
2429
|
source = { kind: "token-transfer", token: s.token, recipient: s.recipient, amount: s.amount.toString(), ...s.memo ? { memo: s.memo } : {} };
|
|
1932
|
-
checks.push(
|
|
2430
|
+
checks.push(depositCheck);
|
|
1933
2431
|
} else if (s.kind === "serialized") {
|
|
1934
2432
|
source = { kind: "serialized" };
|
|
1935
2433
|
checks.push({ status: "ok", label: "Balance changes are proven before signing (SDK guard)" });
|
|
@@ -1940,7 +2438,14 @@ function swapApprovalPayload(plan, meta) {
|
|
|
1940
2438
|
}
|
|
1941
2439
|
for (const a of plan.approvals) checks.push(a.amount > plan.sourceAmount ? { status: "warn", label: "Approval is larger than this swap needs" } : { status: "ok", label: "Approval is exactly the amount needed" });
|
|
1942
2440
|
if (plan.approvals.length > 1) checks.push({ status: "ok", label: `${plan.approvals.length} approvals (reset to 0 first)` });
|
|
1943
|
-
checks.push(isOwn ? { status: "
|
|
2441
|
+
checks.push(!isOwn ? { status: "warn", label: `Funds arrive at ${plan.recipient.slice(0, 6)}\u2026${plan.recipient.slice(-4)}, not your address` } : assigned ? { status: "unknown", label: `Delivery to your address is the ${meta.providerLabel} server\u2019s promise \u2014 this transaction does not enforce it` } : { status: "ok", label: "Funds arrive at your address" });
|
|
2442
|
+
let slippage;
|
|
2443
|
+
const sb = meta.slippageBps;
|
|
2444
|
+
if (sb !== void 0 && Number.isInteger(sb) && sb >= 0 && sb < 1e4) {
|
|
2445
|
+
const below = plan.minDestAmount < slippageFloor(plan.destAmount, sb);
|
|
2446
|
+
slippage = { bps: sb, minGapBps: gapBps(plan.destAmount, plan.minDestAmount), below };
|
|
2447
|
+
checks.push(below ? { status: "warn", label: `Minimum is ${pct(slippage.minGapBps)} under the quote \u2014 more than your ${pct(sb)} slippage` } : { status: "ok", label: `Minimum within your ${pct(sb)} slippage` });
|
|
2448
|
+
}
|
|
1944
2449
|
return {
|
|
1945
2450
|
provider: plan.provider,
|
|
1946
2451
|
providerLabel: meta.providerLabel,
|
|
@@ -1957,7 +2462,9 @@ function swapApprovalPayload(plan, meta) {
|
|
|
1957
2462
|
validUntilMs: plan.validUntilMs,
|
|
1958
2463
|
approvals: plan.approvals.map((a) => ({ token: a.token, spender: a.spender, amount: a.amount.toString() })),
|
|
1959
2464
|
source,
|
|
1960
|
-
checks
|
|
2465
|
+
checks,
|
|
2466
|
+
...slippage ? { slippage } : {},
|
|
2467
|
+
...assigned ? { depositAssignedByProvider: true } : {}
|
|
1961
2468
|
};
|
|
1962
2469
|
}
|
|
1963
2470
|
function parseTypedOrder(json) {
|
|
@@ -1999,15 +2506,15 @@ import { SWAP2_ID_PREFIX } from "@anywalletkit/protocol-swap2-core";
|
|
|
1999
2506
|
var ACTIVE_STATUSES = /* @__PURE__ */ new Set(["pending", "action-required", "refund-pending", "partial", "claiming"]);
|
|
2000
2507
|
function parseSwapId(id) {
|
|
2001
2508
|
if (!id.startsWith(`${SWAP2_ID_PREFIX}.`)) return null;
|
|
2002
|
-
const
|
|
2003
|
-
if (
|
|
2004
|
-
if (
|
|
2005
|
-
const provider = decodeURIComponent(
|
|
2006
|
-
const intentId = decodeURIComponent(
|
|
2007
|
-
const sourceTxHash =
|
|
2008
|
-
const destTxHash =
|
|
2509
|
+
const parts2 = id.slice(SWAP2_ID_PREFIX.length + 1).split(".");
|
|
2510
|
+
if (parts2.length < 2 || parts2.length > 5 || !parts2[0] || !parts2[1]) return null;
|
|
2511
|
+
if (parts2[4] !== void 0 && (parts2[4] !== "ok" || !parts2[3])) return null;
|
|
2512
|
+
const provider = decodeURIComponent(parts2[0]);
|
|
2513
|
+
const intentId = decodeURIComponent(parts2[1]);
|
|
2514
|
+
const sourceTxHash = parts2[2] ? decodeURIComponent(parts2[2]) : void 0;
|
|
2515
|
+
const destTxHash = parts2[3] ? decodeURIComponent(parts2[3]) : void 0;
|
|
2009
2516
|
if (destTxHash && !sourceTxHash) return null;
|
|
2010
|
-
return { provider, intentId, ...sourceTxHash ? { sourceTxHash } : {}, ...destTxHash ? { destTxHash } : {}, ...
|
|
2517
|
+
return { provider, intentId, ...sourceTxHash ? { sourceTxHash } : {}, ...destTxHash ? { destTxHash } : {}, ...parts2[4] !== void 0 ? { destTxConfirmed: true } : {} };
|
|
2011
2518
|
}
|
|
2012
2519
|
var SwapsStore = class {
|
|
2013
2520
|
constructor(storage) {
|
|
@@ -2146,115 +2653,38 @@ async function reviewEarnPlan(approvals, gate, meta, plan) {
|
|
|
2146
2653
|
}
|
|
2147
2654
|
|
|
2148
2655
|
// src/tonconnect.ts
|
|
2149
|
-
import {
|
|
2150
|
-
|
|
2151
|
-
|
|
2152
|
-
|
|
2153
|
-
|
|
2154
|
-
|
|
2155
|
-
|
|
2156
|
-
|
|
2157
|
-
|
|
2158
|
-
|
|
2159
|
-
|
|
2160
|
-
|
|
2161
|
-
|
|
2162
|
-
|
|
2163
|
-
|
|
2164
|
-
|
|
2165
|
-
|
|
2166
|
-
for (const i of items) if (i?.name !== "ton_addr" && i?.name !== "ton_proof") throw new TonConnectError(TONCONNECT_ERRORS.badRequest, `Unknown connect item ${String(i?.name)}.`);
|
|
2167
|
-
return { manifestUrl: m.manifestUrl, ...proof ? { proofPayload: proof.payload } : {} };
|
|
2168
|
-
}
|
|
2169
|
-
var TONCONNECT_MAX_MESSAGES = 4;
|
|
2170
|
-
function parseSendTransaction(param, options = {}) {
|
|
2171
|
-
let o;
|
|
2172
|
-
try {
|
|
2173
|
-
o = typeof param === "string" ? JSON.parse(param) : param;
|
|
2174
|
-
} catch {
|
|
2175
|
-
throw new TonConnectError(TONCONNECT_ERRORS.badRequest, "sendTransaction params[0] is not valid JSON.");
|
|
2176
|
-
}
|
|
2177
|
-
if (!o || typeof o !== "object") throw new TonConnectError(TONCONNECT_ERRORS.badRequest, "sendTransaction needs a request object.");
|
|
2178
|
-
const network = options.network ?? "-239";
|
|
2179
|
-
if (o.network !== void 0 && String(o.network) !== network) throw new TonConnectError(TONCONNECT_ERRORS.badRequest, `This wallet is on network ${network}; the request names ${String(o.network)}.`);
|
|
2180
|
-
const raw = Array.isArray(o.messages) ? o.messages : [];
|
|
2181
|
-
if (raw.length === 0) throw new TonConnectError(TONCONNECT_ERRORS.badRequest, "sendTransaction has no messages.");
|
|
2182
|
-
if (raw.length > (options.maxMessages ?? TONCONNECT_MAX_MESSAGES)) throw new TonConnectError(TONCONNECT_ERRORS.badRequest, `At most ${options.maxMessages ?? TONCONNECT_MAX_MESSAGES} messages per transaction.`);
|
|
2183
|
-
const messages = raw.map((m, i) => {
|
|
2184
|
-
if (typeof m.address !== "string" || !m.address) throw new TonConnectError(TONCONNECT_ERRORS.badRequest, `Message ${i + 1} has no address.`);
|
|
2185
|
-
if (typeof m.amount !== "string" || !/^[0-9]+$/.test(m.amount)) throw new TonConnectError(TONCONNECT_ERRORS.badRequest, `Message ${i + 1} amount must be a decimal string of nanotons.`);
|
|
2186
|
-
if (m.payload !== void 0 && typeof m.payload !== "string") throw new TonConnectError(TONCONNECT_ERRORS.badRequest, `Message ${i + 1} payload must be a base64 BoC.`);
|
|
2187
|
-
if (m.stateInit !== void 0 && typeof m.stateInit !== "string") throw new TonConnectError(TONCONNECT_ERRORS.badRequest, `Message ${i + 1} stateInit must be a base64 BoC.`);
|
|
2188
|
-
return { address: m.address, amount: m.amount, ...m.payload ? { payload: m.payload } : {}, ...m.stateInit ? { stateInit: m.stateInit } : {} };
|
|
2189
|
-
});
|
|
2190
|
-
const validUntil = typeof o.valid_until === "number" ? o.valid_until : typeof o.validUntil === "number" ? o.validUntil : void 0;
|
|
2191
|
-
if (validUntil !== void 0 && validUntil * 1e3 < Date.now()) throw new TonConnectError(TONCONNECT_ERRORS.badRequest, "The request has expired (valid_until is in the past).");
|
|
2192
|
-
return { messages, ...validUntil !== void 0 ? { validUntil } : {}, ...o.network !== void 0 ? { network: String(o.network) } : {}, ...typeof o.from === "string" ? { from: o.from } : {} };
|
|
2193
|
-
}
|
|
2194
|
-
function parseSignData(param) {
|
|
2195
|
-
let o;
|
|
2196
|
-
try {
|
|
2197
|
-
o = typeof param === "string" ? JSON.parse(param) : param;
|
|
2198
|
-
} catch {
|
|
2199
|
-
throw new TonConnectError(TONCONNECT_ERRORS.badRequest, "signData params[0] is not valid JSON.");
|
|
2200
|
-
}
|
|
2201
|
-
if (o?.type === "text" && typeof o.text === "string") return { type: "text", text: o.text };
|
|
2202
|
-
if (o?.type === "binary" && typeof o.bytes === "string") return { type: "binary", bytes: o.bytes };
|
|
2203
|
-
if (o?.type === "cell") throw new TonConnectError(TONCONNECT_ERRORS.methodNotSupported, "signData with a cell payload is not supported by this wallet.");
|
|
2204
|
-
throw new TonConnectError(TONCONNECT_ERRORS.badRequest, 'signData payload must be { type: "text", text } or { type: "binary", bytes }.');
|
|
2205
|
-
}
|
|
2206
|
-
var utf82 = (s) => new TextEncoder().encode(s);
|
|
2207
|
-
var int32BE = (n) => {
|
|
2208
|
-
const b = new Uint8Array(4);
|
|
2209
|
-
new DataView(b.buffer).setInt32(0, n, false);
|
|
2210
|
-
return b;
|
|
2211
|
-
};
|
|
2212
|
-
var uint32LE = (n) => {
|
|
2213
|
-
const b = new Uint8Array(4);
|
|
2214
|
-
new DataView(b.buffer).setUint32(0, n, true);
|
|
2215
|
-
return b;
|
|
2216
|
-
};
|
|
2217
|
-
var uint32BE = (n) => {
|
|
2218
|
-
const b = new Uint8Array(4);
|
|
2219
|
-
new DataView(b.buffer).setUint32(0, n, false);
|
|
2220
|
-
return b;
|
|
2221
|
-
};
|
|
2222
|
-
var uint64LE = (n) => {
|
|
2223
|
-
const b = new Uint8Array(8);
|
|
2224
|
-
new DataView(b.buffer).setBigUint64(0, BigInt(n), true);
|
|
2225
|
-
return b;
|
|
2226
|
-
};
|
|
2227
|
-
var uint64BE = (n) => {
|
|
2228
|
-
const b = new Uint8Array(8);
|
|
2229
|
-
new DataView(b.buffer).setBigUint64(0, BigInt(n), false);
|
|
2230
|
-
return b;
|
|
2231
|
-
};
|
|
2232
|
-
var concat = (...parts) => {
|
|
2233
|
-
const out = new Uint8Array(parts.reduce((n, p) => n + p.length, 0));
|
|
2234
|
-
let o = 0;
|
|
2235
|
-
for (const p of parts) {
|
|
2236
|
-
out.set(p, o);
|
|
2237
|
-
o += p.length;
|
|
2238
|
-
}
|
|
2239
|
-
return out;
|
|
2656
|
+
import { TONCONNECT_ERRORS, TonConnectError, tonAddressParts, tonProofDigest, tonSignDataDigest } from "@anywalletkit/wallet-ton/tonconnect";
|
|
2657
|
+
import {
|
|
2658
|
+
TONCONNECT_ERRORS as TONCONNECT_ERRORS2,
|
|
2659
|
+
TONCONNECT_MAX_MESSAGES,
|
|
2660
|
+
TONCONNECT_NETWORKS,
|
|
2661
|
+
TONCONNECT_SIGN_DATA_MAX,
|
|
2662
|
+
TonConnectError as TonConnectError2,
|
|
2663
|
+
parseConnectRequest,
|
|
2664
|
+
parseSendTransaction,
|
|
2665
|
+
parseSignData,
|
|
2666
|
+
tonConnectRawAddress,
|
|
2667
|
+
tonConnectFriendlyAddress,
|
|
2668
|
+
sameTonConnectAddress
|
|
2669
|
+
} from "@anywalletkit/wallet-ton/tonconnect";
|
|
2670
|
+
var parts = (a) => {
|
|
2671
|
+
if (a.hash.length !== 32) throw new TonConnectError(TONCONNECT_ERRORS.unknown, "address hash must be 32 bytes");
|
|
2672
|
+
return { workChain: a.workchain, hash: a.hash };
|
|
2240
2673
|
};
|
|
2241
2674
|
function tonProofMessage(input) {
|
|
2242
|
-
|
|
2243
|
-
const domain = utf82(input.domain);
|
|
2244
|
-
const inner = concat(utf82("ton-proof-item-v2/"), int32BE(input.address.workchain), input.address.hash, uint32LE(domain.length), domain, uint64LE(input.timestamp), utf82(input.payload));
|
|
2245
|
-
return sha256(concat(new Uint8Array([255, 255]), utf82("ton-connect"), sha256(inner)));
|
|
2675
|
+
return tonProofDigest(parts(input.address), { domain: input.domain, timestamp: input.timestamp, payload: input.payload });
|
|
2246
2676
|
}
|
|
2247
2677
|
function tonSignDataMessage(input) {
|
|
2248
|
-
|
|
2249
|
-
const domain = utf82(input.domain);
|
|
2250
|
-
const payload = input.payload.type === "text" ? utf82(input.payload.text) : Uint8Array.from(atob(input.payload.bytes), (c) => c.charCodeAt(0));
|
|
2251
|
-
const prefix = utf82(input.payload.type === "text" ? "txt" : "bin");
|
|
2252
|
-
return sha256(concat(new Uint8Array([255, 255]), utf82("ton-connect/sign-data/"), int32BE(input.address.workchain), input.address.hash, uint32BE(domain.length), domain, uint64BE(input.timestamp), prefix, uint32BE(payload.length), payload));
|
|
2678
|
+
return tonSignDataDigest(parts(input.address), { domain: input.domain, timestamp: input.timestamp, payload: input.payload });
|
|
2253
2679
|
}
|
|
2254
2680
|
function parseRawAddress(raw) {
|
|
2255
|
-
|
|
2256
|
-
|
|
2257
|
-
|
|
2681
|
+
if (!/^-?\d+:[0-9a-fA-F]{64}$/.test(raw)) throw new TonConnectError(TONCONNECT_ERRORS.unknown, `not a raw TON address: ${raw}`);
|
|
2682
|
+
try {
|
|
2683
|
+
const p = tonAddressParts(raw);
|
|
2684
|
+
return { workchain: p.workChain, hash: p.hash };
|
|
2685
|
+
} catch {
|
|
2686
|
+
throw new TonConnectError(TONCONNECT_ERRORS.unknown, `not a raw TON address: ${raw}`);
|
|
2687
|
+
}
|
|
2258
2688
|
}
|
|
2259
2689
|
function proofDomain(origin) {
|
|
2260
2690
|
try {
|
|
@@ -2264,7 +2694,7 @@ function proofDomain(origin) {
|
|
|
2264
2694
|
}
|
|
2265
2695
|
}
|
|
2266
2696
|
function tonConnectErrorCode(e) {
|
|
2267
|
-
if (e instanceof TonConnectError) return e.code;
|
|
2697
|
+
if (e instanceof TonConnectError || e?.name === "TonConnectError") return e.code;
|
|
2268
2698
|
const code = e?.code;
|
|
2269
2699
|
if (code === 4001) return TONCONNECT_ERRORS.userDeclined;
|
|
2270
2700
|
if (code === 4100) return TONCONNECT_ERRORS.unknownApp;
|
|
@@ -2349,14 +2779,14 @@ function hostOf(hostOrOrigin) {
|
|
|
2349
2779
|
}
|
|
2350
2780
|
}
|
|
2351
2781
|
function matchesList(host, list) {
|
|
2352
|
-
const
|
|
2353
|
-
for (let i = 0; i <
|
|
2782
|
+
const parts2 = host.split(".");
|
|
2783
|
+
for (let i = 0; i < parts2.length - 1; i++) if (list.has(parts2.slice(i).join("."))) return true;
|
|
2354
2784
|
return list.has(host);
|
|
2355
2785
|
}
|
|
2356
2786
|
function stripForFuzzy(host) {
|
|
2357
|
-
const
|
|
2358
|
-
if (
|
|
2359
|
-
return
|
|
2787
|
+
const parts2 = host.replace(/^www\./, "").split(".");
|
|
2788
|
+
if (parts2.length > 1) parts2.pop();
|
|
2789
|
+
return parts2.join(".");
|
|
2360
2790
|
}
|
|
2361
2791
|
function levenshtein(a, b) {
|
|
2362
2792
|
if (a === b) return 0;
|
|
@@ -2497,6 +2927,10 @@ var RecipientBook = class {
|
|
|
2497
2927
|
await this.storage.set(s);
|
|
2498
2928
|
this._state = s;
|
|
2499
2929
|
}
|
|
2930
|
+
/** Forget the cached book: the next read asks storage again (e.g. a storage that only answers once the wallet is unlocked). */
|
|
2931
|
+
reload() {
|
|
2932
|
+
this._state = null;
|
|
2933
|
+
}
|
|
2500
2934
|
/** `known` → the address is one the user paid before; `lookalike` → it imitates one. */
|
|
2501
2935
|
async check(namespace, address) {
|
|
2502
2936
|
const list = await this.list(namespace);
|
|
@@ -2504,9 +2938,243 @@ var RecipientBook = class {
|
|
|
2504
2938
|
return { known, lookalike: known ? null : lookalikeOf(address, list) };
|
|
2505
2939
|
}
|
|
2506
2940
|
};
|
|
2941
|
+
|
|
2942
|
+
// src/mac-storage.ts
|
|
2943
|
+
import { zeroize as zeroize4 } from "@anywalletkit/core";
|
|
2944
|
+
var STORAGE_MAC_FIELD = "anywalletkit.mac/1";
|
|
2945
|
+
var STORAGE_VERSION_FIELD = "anywalletkit.ver/1";
|
|
2946
|
+
var HKDF_INFO2 = "anywalletkit/storage-mac/1";
|
|
2947
|
+
var KEY_BYTES = 32;
|
|
2948
|
+
async function deriveStorageMacKey(dek, crypto2 = globalThis.crypto) {
|
|
2949
|
+
if (dek.length !== 32) throw new VaultError("Vault key must be 32 bytes.", "invalid-input");
|
|
2950
|
+
const ikm = await withSecretBuffer(dek, (b) => crypto2.subtle.importKey("raw", b, "HKDF", false, ["deriveBits"]));
|
|
2951
|
+
const bits = await crypto2.subtle.deriveBits({ name: "HKDF", hash: "SHA-256", salt: new ArrayBuffer(0), info: toArrayBuffer(utf8.encode(HKDF_INFO2)) }, ikm, KEY_BYTES * 8);
|
|
2952
|
+
return new Uint8Array(bits);
|
|
2953
|
+
}
|
|
2954
|
+
function macJsonStorage(inner, options) {
|
|
2955
|
+
const { label, versions } = options;
|
|
2956
|
+
if (versions && label.includes("\n")) throw new VaultError("A versioned store label has no line break.", "invalid-input");
|
|
2957
|
+
const readWithoutKey = options.readWithoutKey ?? "unverified";
|
|
2958
|
+
const writeWithoutKey = options.writeWithoutKey ?? "unsigned";
|
|
2959
|
+
const withKey = async (given, fn) => {
|
|
2960
|
+
const key = given ? Uint8Array.from(given) : await options.key();
|
|
2961
|
+
try {
|
|
2962
|
+
return await fn(key);
|
|
2963
|
+
} finally {
|
|
2964
|
+
if (key) zeroize4(key);
|
|
2965
|
+
}
|
|
2966
|
+
};
|
|
2967
|
+
const stored = async (key) => {
|
|
2968
|
+
const raw = await inner.get();
|
|
2969
|
+
if (raw === null || raw === void 0) return { state: "empty", value: null };
|
|
2970
|
+
const envelope = isStored(raw);
|
|
2971
|
+
const value = envelope ? raw.value : raw;
|
|
2972
|
+
if (!envelope || raw[STORAGE_MAC_FIELD] === null) return { state: "unsigned", value };
|
|
2973
|
+
if (!key) return { state: "no-key", value };
|
|
2974
|
+
if (!versions) return { state: await verify(key, label, value, raw[STORAGE_MAC_FIELD]) ? "ok" : "invalid", value };
|
|
2975
|
+
const version = STORAGE_VERSION_FIELD in raw ? raw[STORAGE_VERSION_FIELD] : 0;
|
|
2976
|
+
if (version !== 0 && !isVersion(version)) return { state: "invalid", value };
|
|
2977
|
+
if (!await verify(key, label, value, raw[STORAGE_MAC_FIELD], version || void 0)) return { state: "invalid", value };
|
|
2978
|
+
return { state: "ok", value, version };
|
|
2979
|
+
};
|
|
2980
|
+
const put = async (key, value, version) => {
|
|
2981
|
+
const mac = await sign(key, label, value, version);
|
|
2982
|
+
await inner.set(version === void 0 ? { [STORAGE_MAC_FIELD]: mac, value } : { [STORAGE_MAC_FIELD]: mac, [STORAGE_VERSION_FIELD]: version, value });
|
|
2983
|
+
};
|
|
2984
|
+
const seen = async (key) => {
|
|
2985
|
+
const r = await stored(key);
|
|
2986
|
+
return r.state === "ok" ? r.version ?? 0 : 0;
|
|
2987
|
+
};
|
|
2988
|
+
const read = (given) => withKey(given, async (key) => {
|
|
2989
|
+
if (!versions || !key) return stored(key);
|
|
2990
|
+
return versions.exclusive(async (ledger) => {
|
|
2991
|
+
const r = await stored(key);
|
|
2992
|
+
if (r.state === "empty") return await ledger.check(label, null, key) === "ok" ? r : { state: "stale", value: null };
|
|
2993
|
+
if (r.state !== "ok") return r;
|
|
2994
|
+
return await ledger.check(label, r.version ?? 0, key) === "ok" ? r : { ...r, state: "stale" };
|
|
2995
|
+
});
|
|
2996
|
+
});
|
|
2997
|
+
const write = (value, given) => withKey(given, async (key) => {
|
|
2998
|
+
if (!key) throw new VaultError("The wallet is locked: unlock it to change this.", "locked");
|
|
2999
|
+
if (!versions) return put(key, value);
|
|
3000
|
+
await versions.exclusive((ledger) => ledger.advance(label, key, () => seen(key), (version) => put(key, value, version)));
|
|
3001
|
+
});
|
|
3002
|
+
return {
|
|
3003
|
+
read,
|
|
3004
|
+
write,
|
|
3005
|
+
async get() {
|
|
3006
|
+
const r = await read();
|
|
3007
|
+
if (r.state === "ok") return r.value;
|
|
3008
|
+
if (r.state === "no-key" && readWithoutKey === "unverified") return r.value;
|
|
3009
|
+
return null;
|
|
3010
|
+
},
|
|
3011
|
+
async set(value) {
|
|
3012
|
+
await withKey(void 0, async (key) => {
|
|
3013
|
+
if (key) return write(value, key);
|
|
3014
|
+
if (writeWithoutKey === "refuse") throw new VaultError("The wallet is locked: unlock it to change this.", "locked");
|
|
3015
|
+
await inner.set({ [STORAGE_MAC_FIELD]: null, value });
|
|
3016
|
+
});
|
|
3017
|
+
},
|
|
3018
|
+
async rekey(oldKey, newKey) {
|
|
3019
|
+
if (!versions) {
|
|
3020
|
+
const r = await read(oldKey);
|
|
3021
|
+
if (r.state === "ok") await write(r.value, newKey);
|
|
3022
|
+
return r.state;
|
|
3023
|
+
}
|
|
3024
|
+
return versions.exclusive(async (ledger) => {
|
|
3025
|
+
const r = await stored(oldKey);
|
|
3026
|
+
if (r.state === "empty") return await ledger.check(label, null, newKey) === "ok" ? "empty" : "stale";
|
|
3027
|
+
if (r.state !== "ok") return r.state;
|
|
3028
|
+
const version = r.version ?? 0;
|
|
3029
|
+
if (await ledger.check(label, version, newKey) !== "ok") return "stale";
|
|
3030
|
+
if (version > 0) await put(newKey, r.value, version);
|
|
3031
|
+
else await ledger.advance(label, newKey, async () => 0, (v) => put(newKey, r.value, v));
|
|
3032
|
+
return "ok";
|
|
3033
|
+
});
|
|
3034
|
+
}
|
|
3035
|
+
};
|
|
3036
|
+
}
|
|
3037
|
+
function isStored(raw) {
|
|
3038
|
+
if (typeof raw !== "object" || raw === null || !(STORAGE_MAC_FIELD in raw) || !("value" in raw)) return false;
|
|
3039
|
+
const mac = raw[STORAGE_MAC_FIELD];
|
|
3040
|
+
return mac === null || typeof mac === "string";
|
|
3041
|
+
}
|
|
3042
|
+
async function hmacKey(key, usage) {
|
|
3043
|
+
if (key.length !== KEY_BYTES) throw new VaultError("A storage MAC key is 32 bytes.", "invalid-input");
|
|
3044
|
+
return withSecretBuffer(key, (b) => globalThis.crypto.subtle.importKey("raw", b, { name: "HMAC", hash: "SHA-256" }, false, [usage]));
|
|
3045
|
+
}
|
|
3046
|
+
function macInput(label, value, version) {
|
|
3047
|
+
return toArrayBuffer(utf8.encode(version === void 0 ? `${label}
|
|
3048
|
+
${canonicalJson(value)}` : `${label}
|
|
3049
|
+
${version}
|
|
3050
|
+
${canonicalJson(value)}`));
|
|
3051
|
+
}
|
|
3052
|
+
async function sign(key, label, value, version) {
|
|
3053
|
+
return toBase64(new Uint8Array(await globalThis.crypto.subtle.sign("HMAC", await hmacKey(key, "sign"), macInput(label, value, version))));
|
|
3054
|
+
}
|
|
3055
|
+
async function verify(key, label, value, mac, version) {
|
|
3056
|
+
let bytes;
|
|
3057
|
+
try {
|
|
3058
|
+
bytes = fromBase64(mac);
|
|
3059
|
+
} catch {
|
|
3060
|
+
return false;
|
|
3061
|
+
}
|
|
3062
|
+
return globalThis.crypto.subtle.verify("HMAC", await hmacKey(key, "verify"), toArrayBuffer(bytes), macInput(label, value, version));
|
|
3063
|
+
}
|
|
3064
|
+
var isVersion = (x) => typeof x === "number" && Number.isSafeInteger(x) && x > 0;
|
|
3065
|
+
function canonicalJson(value) {
|
|
3066
|
+
return JSON.stringify(sortKeys(JSON.parse(JSON.stringify(value ?? null))));
|
|
3067
|
+
}
|
|
3068
|
+
function sortKeys(x) {
|
|
3069
|
+
if (Array.isArray(x)) return x.map(sortKeys);
|
|
3070
|
+
if (x === null || typeof x !== "object") return x;
|
|
3071
|
+
const out = {};
|
|
3072
|
+
for (const k of Object.keys(x).sort()) out[k] = sortKeys(x[k]);
|
|
3073
|
+
return out;
|
|
3074
|
+
}
|
|
3075
|
+
var LEDGER_LABEL = "anywalletkit/storage-versions/1";
|
|
3076
|
+
function storageVersionLedger(options) {
|
|
3077
|
+
const label = options.label ?? LEDGER_LABEL;
|
|
3078
|
+
const acceptUnversioned = options.acceptUnversioned ?? false;
|
|
3079
|
+
const now = options.now ?? Date.now;
|
|
3080
|
+
let tail = Promise.resolve();
|
|
3081
|
+
const queue = (fn) => {
|
|
3082
|
+
const run = tail.then(fn, fn);
|
|
3083
|
+
tail = run.then(() => void 0, () => void 0);
|
|
3084
|
+
return run;
|
|
3085
|
+
};
|
|
3086
|
+
const high = async () => {
|
|
3087
|
+
const v = await options.mark.get();
|
|
3088
|
+
return isVersion(v) ? v : 0;
|
|
3089
|
+
};
|
|
3090
|
+
const parse = async (raw, key) => {
|
|
3091
|
+
if (!isStored(raw) || raw[STORAGE_MAC_FIELD] === null) return null;
|
|
3092
|
+
const epoch = raw[STORAGE_VERSION_FIELD];
|
|
3093
|
+
const v = raw.value;
|
|
3094
|
+
if (!isVersion(epoch) || typeof v !== "object" || v === null || !isVersion(v.base) || typeof v.sealed !== "boolean") return null;
|
|
3095
|
+
if (typeof v.versions !== "object" || v.versions === null || !Object.values(v.versions).every(isVersion)) return null;
|
|
3096
|
+
if (!await verify(key, label, v, raw[STORAGE_MAC_FIELD], epoch)) return null;
|
|
3097
|
+
return { epoch, base: v.base, sealed: v.sealed, versions: { ...v.versions } };
|
|
3098
|
+
};
|
|
3099
|
+
const load = async (key) => {
|
|
3100
|
+
const mark = await high();
|
|
3101
|
+
const raw = await options.manifest.get();
|
|
3102
|
+
if (raw === null || raw === void 0) return { state: mark > 0 ? "stale" : "empty" };
|
|
3103
|
+
const manifest = await parse(raw, key);
|
|
3104
|
+
if (!manifest) return { state: "invalid" };
|
|
3105
|
+
if (manifest.epoch < mark) return { state: "stale", manifest };
|
|
3106
|
+
if (manifest.epoch > mark) await options.mark.set(manifest.epoch);
|
|
3107
|
+
return { state: "ok", manifest };
|
|
3108
|
+
};
|
|
3109
|
+
const store = async (m, key) => {
|
|
3110
|
+
const value = { base: m.base, sealed: m.sealed, versions: m.versions };
|
|
3111
|
+
await options.manifest.set({ [STORAGE_MAC_FIELD]: await sign(key, label, value, m.epoch), [STORAGE_VERSION_FIELD]: m.epoch, value });
|
|
3112
|
+
if (m.epoch > await high()) await options.mark.set(m.epoch);
|
|
3113
|
+
};
|
|
3114
|
+
const fresh = async (sealed, prior, key) => {
|
|
3115
|
+
const floor = Math.max(await high(), Math.floor(now()), prior ? Math.max(prior.epoch, prior.base, ...Object.values(prior.versions)) : 0);
|
|
3116
|
+
if (!Number.isSafeInteger(floor + 2)) throw new VaultError("Storage version out of range.", "invalid-input");
|
|
3117
|
+
const m = { epoch: floor + 1, base: floor + 1, sealed, versions: {} };
|
|
3118
|
+
await store(m, key);
|
|
3119
|
+
return m;
|
|
3120
|
+
};
|
|
3121
|
+
const record = (m, name, version, key) => store({ ...m, epoch: m.epoch + 1, versions: { ...m.versions, [name]: version } }, key);
|
|
3122
|
+
const named = (name) => {
|
|
3123
|
+
if (name === label || name.includes("\n")) throw new VaultError("Not a store label.", "invalid-input");
|
|
3124
|
+
};
|
|
3125
|
+
const ops = {
|
|
3126
|
+
async check(name, version, key) {
|
|
3127
|
+
named(name);
|
|
3128
|
+
const l = await load(key);
|
|
3129
|
+
if (l.state === "empty") return version === null || version === 0 && acceptUnversioned ? "ok" : "stale";
|
|
3130
|
+
if (l.state !== "ok") return "stale";
|
|
3131
|
+
const m = l.manifest;
|
|
3132
|
+
const listed = m.versions[name];
|
|
3133
|
+
if (listed === void 0 && version === null) return m.sealed ? "stale" : "ok";
|
|
3134
|
+
if (listed === void 0 && version === 0) return !m.sealed && acceptUnversioned ? "ok" : "stale";
|
|
3135
|
+
if (version === null || version === 0) return "stale";
|
|
3136
|
+
if (version === listed) return "ok";
|
|
3137
|
+
if (version !== (listed ?? m.base) + 1) return "stale";
|
|
3138
|
+
await record(m, name, version, key).catch(() => void 0);
|
|
3139
|
+
return "ok";
|
|
3140
|
+
},
|
|
3141
|
+
async advance(name, key, seen, write) {
|
|
3142
|
+
named(name);
|
|
3143
|
+
const l = await load(key);
|
|
3144
|
+
const before = await seen();
|
|
3145
|
+
const m = l.state === "ok" ? l.manifest : await fresh(l.state !== "empty" || before > 0, l.manifest, key);
|
|
3146
|
+
const version = Math.max(m.versions[name] ?? m.base, before) + 1;
|
|
3147
|
+
if (!Number.isSafeInteger(version)) throw new VaultError("Storage version out of range.", "invalid-input");
|
|
3148
|
+
await write(version);
|
|
3149
|
+
await record(m, name, version, key);
|
|
3150
|
+
return version;
|
|
3151
|
+
}
|
|
3152
|
+
};
|
|
3153
|
+
return {
|
|
3154
|
+
exclusive: (fn) => queue(() => fn(ops)),
|
|
3155
|
+
condemn: (key) => queue(async () => {
|
|
3156
|
+
await fresh(true, (await load(key)).manifest, key);
|
|
3157
|
+
}),
|
|
3158
|
+
rekey: (oldKey, newKey) => queue(async () => {
|
|
3159
|
+
const l = await load(oldKey);
|
|
3160
|
+
if (l.state === "ok") await store({ ...l.manifest, epoch: l.manifest.epoch + 1 }, newKey);
|
|
3161
|
+
return l.state;
|
|
3162
|
+
}),
|
|
3163
|
+
read: (key) => queue(async () => {
|
|
3164
|
+
const l = await load(key);
|
|
3165
|
+
return { state: l.state, ...l.manifest ?? { epoch: 0, base: 0, sealed: false, versions: {} } };
|
|
3166
|
+
}),
|
|
3167
|
+
reset: () => queue(async () => {
|
|
3168
|
+
await options.manifest.set(null);
|
|
3169
|
+
await options.mark.set(null);
|
|
3170
|
+
})
|
|
3171
|
+
};
|
|
3172
|
+
}
|
|
2507
3173
|
export {
|
|
2508
3174
|
ACTIVE_STATUSES,
|
|
2509
3175
|
APPROVALS_MAX_PENDING_PER_ORIGIN,
|
|
3176
|
+
APPROVALS_MAX_PENDING_PER_SITE,
|
|
3177
|
+
APPROVALS_MAX_PENDING_TOTAL,
|
|
2510
3178
|
ApprovalQueue,
|
|
2511
3179
|
BTC_CHAIN,
|
|
2512
3180
|
BTC_ESPLORA,
|
|
@@ -2514,9 +3182,12 @@ export {
|
|
|
2514
3182
|
DEFAULT_CHAIN,
|
|
2515
3183
|
DEFAULT_NETWORKS,
|
|
2516
3184
|
FLOOR_TOLERANCE_BPS,
|
|
3185
|
+
GETLOGS_MAX_SPAN_UNCONNECTED,
|
|
2517
3186
|
KEYRING_STORAGE_KEY,
|
|
2518
3187
|
LOOKALIKE_PREFIX,
|
|
2519
3188
|
LOOKALIKE_SUFFIX,
|
|
3189
|
+
MAX_FEE_REASKS,
|
|
3190
|
+
MULTI_LABEL_SUFFIXES,
|
|
2520
3191
|
NEAR_CHAIN,
|
|
2521
3192
|
NEAR_PROVIDERS,
|
|
2522
3193
|
NON_EVM_CHAINS,
|
|
@@ -2537,18 +3208,24 @@ export {
|
|
|
2537
3208
|
RouterError,
|
|
2538
3209
|
SESSION_DEFAULT_AUTOLOCK_MS,
|
|
2539
3210
|
SIGNING_METHODS,
|
|
3211
|
+
SIWE_FORBIDDEN_CHARS,
|
|
2540
3212
|
SOLANA_CHAIN,
|
|
2541
3213
|
SOLANA_RPCS,
|
|
3214
|
+
STORAGE_MAC_FIELD,
|
|
3215
|
+
STORAGE_VERSION_FIELD,
|
|
2542
3216
|
Session,
|
|
2543
3217
|
SwapTracker,
|
|
2544
3218
|
SwapsStore,
|
|
2545
|
-
TONCONNECT_ERRORS,
|
|
3219
|
+
TONCONNECT_ERRORS2 as TONCONNECT_ERRORS,
|
|
2546
3220
|
TONCONNECT_MAX_MESSAGES,
|
|
3221
|
+
TONCONNECT_NETWORKS,
|
|
3222
|
+
TONCONNECT_SIGN_DATA_MAX,
|
|
2547
3223
|
TON_CHAIN,
|
|
2548
3224
|
TON_ENDPOINTS,
|
|
2549
3225
|
TRON_CHAIN,
|
|
2550
3226
|
TRON_PROVIDERS,
|
|
2551
|
-
|
|
3227
|
+
TokenBucket,
|
|
3228
|
+
TonConnectError2 as TonConnectError,
|
|
2552
3229
|
VAULT_DEFAULT_ITERATIONS,
|
|
2553
3230
|
VAULT_LIMITS,
|
|
2554
3231
|
VAULT_MIN_ITERATIONS,
|
|
@@ -2562,6 +3239,7 @@ export {
|
|
|
2562
3239
|
ZCASH_CHAIN,
|
|
2563
3240
|
approvalChallenge,
|
|
2564
3241
|
caip2FromChainId,
|
|
3242
|
+
canonicalJson,
|
|
2565
3243
|
chainHex,
|
|
2566
3244
|
chainTxMatcher,
|
|
2567
3245
|
checkSenderOrigin,
|
|
@@ -2574,6 +3252,7 @@ export {
|
|
|
2574
3252
|
createSignHandler,
|
|
2575
3253
|
decodedMessage,
|
|
2576
3254
|
derToRaw,
|
|
3255
|
+
deriveStorageMacKey,
|
|
2577
3256
|
describeSiwe,
|
|
2578
3257
|
earnApprovalPayload,
|
|
2579
3258
|
earnPlanMatchers,
|
|
@@ -2581,6 +3260,7 @@ export {
|
|
|
2581
3260
|
evmCallMatcher,
|
|
2582
3261
|
executionFloor,
|
|
2583
3262
|
fromBase64Url,
|
|
3263
|
+
gasLimitExcessive,
|
|
2584
3264
|
hostOf,
|
|
2585
3265
|
isCatalogChain,
|
|
2586
3266
|
isEnvelopeV1,
|
|
@@ -2594,6 +3274,7 @@ export {
|
|
|
2594
3274
|
isRelayToContent,
|
|
2595
3275
|
levenshtein,
|
|
2596
3276
|
lookalikeOf,
|
|
3277
|
+
macJsonStorage,
|
|
2597
3278
|
memoryAlarms,
|
|
2598
3279
|
memoryApprovalWindow,
|
|
2599
3280
|
memoryJsonStorage,
|
|
@@ -2618,14 +3299,21 @@ export {
|
|
|
2618
3299
|
rank,
|
|
2619
3300
|
rawToDer,
|
|
2620
3301
|
relayError,
|
|
3302
|
+
revealHidden,
|
|
2621
3303
|
reviewEarnPlan,
|
|
2622
3304
|
reviewSwapPlan,
|
|
3305
|
+
sameTonConnectAddress,
|
|
3306
|
+
siteOf,
|
|
3307
|
+
siweCrossSite,
|
|
2623
3308
|
siweProblems,
|
|
2624
3309
|
stampOrigin,
|
|
3310
|
+
storageVersionLedger,
|
|
2625
3311
|
swapApprovalPayload,
|
|
2626
3312
|
toBase64Url,
|
|
2627
3313
|
toRelayError,
|
|
2628
3314
|
tonConnectErrorCode,
|
|
3315
|
+
tonConnectFriendlyAddress,
|
|
3316
|
+
tonConnectRawAddress,
|
|
2629
3317
|
tonProofMessage,
|
|
2630
3318
|
tonSignDataMessage,
|
|
2631
3319
|
typedDataMatcher,
|