@anywalletkit/extension-kit 0.0.0-stage → 0.1.1
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/LICENSE +202 -0
- package/README.md +114 -2
- package/dist/index.cjs +2305 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +1479 -0
- package/dist/index.d.ts +1479 -0
- package/dist/index.js +2282 -0
- package/dist/index.js.map +1 -0
- package/package.json +60 -3
package/dist/index.js
ADDED
|
@@ -0,0 +1,2282 @@
|
|
|
1
|
+
// src/errors.ts
|
|
2
|
+
var VaultError = class extends Error {
|
|
3
|
+
code;
|
|
4
|
+
constructor(message, code) {
|
|
5
|
+
super(message);
|
|
6
|
+
this.name = "VaultError";
|
|
7
|
+
this.code = code;
|
|
8
|
+
}
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
// src/vault.ts
|
|
12
|
+
import { zeroize } from "@anywalletkit/core";
|
|
13
|
+
import { scryptAsync } from "@noble/hashes/scrypt.js";
|
|
14
|
+
|
|
15
|
+
// src/codec.ts
|
|
16
|
+
function toBase64(bytes) {
|
|
17
|
+
let s = "";
|
|
18
|
+
for (let i = 0; i < bytes.length; i += 32768) s += String.fromCharCode(...bytes.subarray(i, i + 32768));
|
|
19
|
+
return btoa(s);
|
|
20
|
+
}
|
|
21
|
+
function fromBase64(text) {
|
|
22
|
+
const s = atob(text);
|
|
23
|
+
const out = new Uint8Array(s.length);
|
|
24
|
+
for (let i = 0; i < s.length; i++) out[i] = s.charCodeAt(i);
|
|
25
|
+
return out;
|
|
26
|
+
}
|
|
27
|
+
var utf8 = {
|
|
28
|
+
encode: (text) => new TextEncoder().encode(text),
|
|
29
|
+
decode: (bytes) => new TextDecoder().decode(bytes)
|
|
30
|
+
};
|
|
31
|
+
function toArrayBuffer(bytes) {
|
|
32
|
+
const copy = new Uint8Array(bytes.byteLength);
|
|
33
|
+
copy.set(bytes);
|
|
34
|
+
return copy.buffer;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// src/vault.ts
|
|
38
|
+
var VAULT_MIN_ITERATIONS = 6e5;
|
|
39
|
+
var VAULT_DEFAULT_ITERATIONS = 6e5;
|
|
40
|
+
var VAULT_SCRYPT = { N: 1 << 17, r: 8, p: 1 };
|
|
41
|
+
var VAULT_MIN_SCRYPT = { N: 1 << 14, r: 8, p: 1 };
|
|
42
|
+
var VAULT_MIN_PASSWORD_LENGTH = 8;
|
|
43
|
+
var SALT_BYTES = 32;
|
|
44
|
+
var IV_BYTES = 12;
|
|
45
|
+
var DEK_BYTES = 32;
|
|
46
|
+
var PRF_BYTES = 32;
|
|
47
|
+
var aadDekV1 = (kdf) => `forgewallet-vault/1/dek/${kdf.name}/${kdf.iterations}/${kdf.salt}`;
|
|
48
|
+
var aadDekV2 = (kdf) => `forgewallet-vault/2/dek/scrypt/${kdf.N}/${kdf.r}/${kdf.p}/${kdf.salt}`;
|
|
49
|
+
var aadPasskey = (id) => `forgewallet-vault/2/passkey/${id}`;
|
|
50
|
+
var aadSecret = (id) => `forgewallet-vault/secret/${id}`;
|
|
51
|
+
var HKDF_INFO = "forgewallet-vault/2/passkey";
|
|
52
|
+
var Vault = class {
|
|
53
|
+
_storage;
|
|
54
|
+
_kdf;
|
|
55
|
+
_iterations;
|
|
56
|
+
_scrypt;
|
|
57
|
+
_onProgress;
|
|
58
|
+
_crypto;
|
|
59
|
+
constructor(options) {
|
|
60
|
+
this._storage = options.storage;
|
|
61
|
+
this._kdf = options.kdf ?? "scrypt";
|
|
62
|
+
this._iterations = options.iterations ?? VAULT_DEFAULT_ITERATIONS;
|
|
63
|
+
this._scrypt = options.scrypt ?? VAULT_SCRYPT;
|
|
64
|
+
this._onProgress = options.onProgress;
|
|
65
|
+
this._crypto = options.crypto ?? globalThis.crypto;
|
|
66
|
+
if (!Number.isInteger(this._iterations) || this._iterations < VAULT_MIN_ITERATIONS) {
|
|
67
|
+
throw new VaultError(`PBKDF2 iterations must be an integer \u2265 ${VAULT_MIN_ITERATIONS}`, "invalid-input");
|
|
68
|
+
}
|
|
69
|
+
if (!isStrongScrypt(this._scrypt)) throw new VaultError(`scrypt cost must be at least N=${VAULT_MIN_SCRYPT.N} r=${VAULT_MIN_SCRYPT.r} p=${VAULT_MIN_SCRYPT.p} (N a power of two)`, "invalid-input");
|
|
70
|
+
}
|
|
71
|
+
async exists() {
|
|
72
|
+
return await this._storage.get() !== null;
|
|
73
|
+
}
|
|
74
|
+
/** What protects the DEK right now (UI). `null` without a vault. */
|
|
75
|
+
async kdfInfo() {
|
|
76
|
+
const e = await this._storage.get();
|
|
77
|
+
if (!e) return null;
|
|
78
|
+
if (isEnvelopeV1(e)) return { version: 1, name: "PBKDF2-SHA256", summary: `PBKDF2-SHA256 ${e.kdf.iterations.toLocaleString("en-US")} iterations` };
|
|
79
|
+
if (isEnvelopeV2(e)) return { version: 2, name: "scrypt", summary: `scrypt N=2^${Math.log2(e.kdf.N)} r=${e.kdf.r} p=${e.kdf.p}` };
|
|
80
|
+
return null;
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Create an empty vault protected by `password`. Returns the DEK — hand it to the
|
|
84
|
+
* session, then `setSecret` the first seed. Refuses when a vault exists.
|
|
85
|
+
*/
|
|
86
|
+
async create(password) {
|
|
87
|
+
requirePassword(password, VAULT_MIN_PASSWORD_LENGTH);
|
|
88
|
+
if (await this.exists()) throw new VaultError("A vault already exists; unlock it or reset explicitly.", "exists");
|
|
89
|
+
const dek = this._random(DEK_BYTES);
|
|
90
|
+
await this._storage.set(await this._sealEnvelope(password, dek, {}, [], Date.now()));
|
|
91
|
+
return dek;
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Derive the KEK from `password`, unseal and return the DEK. `wrong-password` on
|
|
95
|
+
* failure. A v1 (PBKDF2) envelope that opens is re-sealed as v2 (scrypt) on the
|
|
96
|
+
* way out, secrets untouched.
|
|
97
|
+
*/
|
|
98
|
+
async unlock(password) {
|
|
99
|
+
requirePassword(password);
|
|
100
|
+
const envelope = await this._load();
|
|
101
|
+
if (isEnvelopeV1(envelope)) {
|
|
102
|
+
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");
|
|
103
|
+
const kek2 = await this._derivePbkdf2(password, fromBase64(envelope.kdf.salt), envelope.kdf.iterations);
|
|
104
|
+
const dek = await this._open(kek2, envelope.dek, aadDekV1(envelope.kdf), "wrong-password");
|
|
105
|
+
if (this._kdf === "scrypt") {
|
|
106
|
+
const next = await this._sealEnvelope(password, dek, envelope.secrets, [], envelope.createdAt);
|
|
107
|
+
await this._storage.set(isEnvelopeV2(next) ? { ...next, upgradedAt: Date.now() } : next);
|
|
108
|
+
}
|
|
109
|
+
return dek;
|
|
110
|
+
}
|
|
111
|
+
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");
|
|
112
|
+
const kek = await this._deriveScrypt(password, fromBase64(envelope.kdf.salt), envelope.kdf);
|
|
113
|
+
return this._open(kek, envelope.dek, aadDekV2(envelope.kdf), "wrong-password");
|
|
114
|
+
}
|
|
115
|
+
/** Re-wrap the DEK under a new password (fresh salt, current KDF). Secrets and passkeys are untouched. */
|
|
116
|
+
async changePassword(oldPassword, newPassword) {
|
|
117
|
+
requirePassword(newPassword, VAULT_MIN_PASSWORD_LENGTH);
|
|
118
|
+
const dek = await this.unlock(oldPassword);
|
|
119
|
+
try {
|
|
120
|
+
const envelope = await this._load();
|
|
121
|
+
const passkeys = isEnvelopeV2(envelope) ? envelope.passkeys : [];
|
|
122
|
+
const next = await this._sealEnvelope(newPassword, dek, envelope.secrets, passkeys, envelope.createdAt);
|
|
123
|
+
await this._storage.set(isEnvelopeV2(envelope) && envelope.upgradedAt !== void 0 && isEnvelopeV2(next) ? { ...next, upgradedAt: envelope.upgradedAt } : next);
|
|
124
|
+
} finally {
|
|
125
|
+
zeroize(dek);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
// ---- passkeys (v2) ----
|
|
129
|
+
/** Public view of the registered passkeys (ids, labels; whether each can verify assertions). */
|
|
130
|
+
async listPasskeys() {
|
|
131
|
+
const e = await this._storage.get();
|
|
132
|
+
if (!e || !isEnvelopeV2(e)) return [];
|
|
133
|
+
return e.passkeys.map((p) => ({ id: p.id, label: p.label, createdAt: p.createdAt, canVerify: typeof p.publicKey === "string" && p.alg === -7 }));
|
|
134
|
+
}
|
|
135
|
+
/** The SPKI public key (base64) of one passkey, for assertion verification; `null` when unknown. */
|
|
136
|
+
async passkeyPublicKey(id) {
|
|
137
|
+
const e = await this._storage.get();
|
|
138
|
+
if (!e || !isEnvelopeV2(e)) return null;
|
|
139
|
+
const p = e.passkeys.find((k) => k.id === id);
|
|
140
|
+
return p?.publicKey && p.alg !== void 0 ? { publicKey: p.publicKey, alg: p.alg } : null;
|
|
141
|
+
}
|
|
142
|
+
/**
|
|
143
|
+
* Register a passkey as a second unlock path: the DEK (from the unlocked session)
|
|
144
|
+
* is sealed under HKDF-SHA256(`prf`, salt = credential id). The password stays
|
|
145
|
+
* the recovery path; `prf` is zeroized here. A v1 envelope must be unlocked (and
|
|
146
|
+
* thereby upgraded) first.
|
|
147
|
+
*/
|
|
148
|
+
async addPasskey(dek, passkey) {
|
|
149
|
+
try {
|
|
150
|
+
requireId(passkey.id);
|
|
151
|
+
if (passkey.prf.length !== PRF_BYTES) throw new VaultError(`A PRF output is ${PRF_BYTES} bytes.`, "invalid-input");
|
|
152
|
+
const envelope = await this._load();
|
|
153
|
+
if (!isEnvelopeV2(envelope)) throw new VaultError("Unlock with the password once so the vault is upgraded before adding a passkey.", "unsupported-envelope");
|
|
154
|
+
if (envelope.passkeys.some((p) => p.id === passkey.id)) throw new VaultError("This passkey is already registered.", "exists");
|
|
155
|
+
await this._requireDek(dek, envelope);
|
|
156
|
+
const key = await this._derivePasskeyKek(passkey.prf, passkey.id);
|
|
157
|
+
const entry = { id: passkey.id, label: passkey.label.slice(0, 64), createdAt: Date.now(), dek: await this._seal(key, dek, aadPasskey(passkey.id)) };
|
|
158
|
+
if (passkey.publicKey && passkey.alg !== void 0) {
|
|
159
|
+
entry.publicKey = passkey.publicKey;
|
|
160
|
+
entry.alg = passkey.alg;
|
|
161
|
+
}
|
|
162
|
+
await this._storage.set({ ...envelope, passkeys: [...envelope.passkeys, entry] });
|
|
163
|
+
} finally {
|
|
164
|
+
zeroize(passkey.prf);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
/** Unseal the DEK with a passkey's PRF output. `wrong-password` when the box does not open, `missing` when the id is unknown. */
|
|
168
|
+
async unlockWithPasskey(id, prf) {
|
|
169
|
+
try {
|
|
170
|
+
requireId(id);
|
|
171
|
+
if (prf.length !== PRF_BYTES) throw new VaultError(`A PRF output is ${PRF_BYTES} bytes.`, "invalid-input");
|
|
172
|
+
const envelope = await this._load();
|
|
173
|
+
if (!isEnvelopeV2(envelope)) throw new VaultError("This vault has no passkeys; unlock with the password.", "missing");
|
|
174
|
+
const entry = envelope.passkeys.find((p) => p.id === id);
|
|
175
|
+
if (!entry) throw new VaultError("No such passkey on this vault.", "missing");
|
|
176
|
+
const key = await this._derivePasskeyKek(prf, id);
|
|
177
|
+
return await this._open(key, entry.dek, aadPasskey(id), "wrong-password");
|
|
178
|
+
} finally {
|
|
179
|
+
zeroize(prf);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
async removePasskey(id) {
|
|
183
|
+
requireId(id);
|
|
184
|
+
const envelope = await this._load();
|
|
185
|
+
if (!isEnvelopeV2(envelope)) return;
|
|
186
|
+
await this._storage.set({ ...envelope, passkeys: envelope.passkeys.filter((p) => p.id !== id) });
|
|
187
|
+
}
|
|
188
|
+
// ---- secrets ----
|
|
189
|
+
async listSecretIds() {
|
|
190
|
+
const envelope = await this._storage.get();
|
|
191
|
+
return envelope ? Object.keys(envelope.secrets) : [];
|
|
192
|
+
}
|
|
193
|
+
/** Seal `secret` under the DEK as `sourceId` (overwrites an entry with the same id). */
|
|
194
|
+
async setSecret(dek, sourceId, secret) {
|
|
195
|
+
requireId(sourceId);
|
|
196
|
+
const envelope = await this._load();
|
|
197
|
+
const key = await this._importDek(dek);
|
|
198
|
+
const box = await this._seal(key, secret, aadSecret(sourceId));
|
|
199
|
+
await this._storage.set({ ...envelope, secrets: { ...envelope.secrets, [sourceId]: box } });
|
|
200
|
+
}
|
|
201
|
+
/** Unseal one secret. `null` when the id is unknown; the caller zeroizes the result after use. */
|
|
202
|
+
async getSecret(dek, sourceId) {
|
|
203
|
+
requireId(sourceId);
|
|
204
|
+
const envelope = await this._load();
|
|
205
|
+
const box = envelope.secrets[sourceId];
|
|
206
|
+
if (!box) return null;
|
|
207
|
+
const key = await this._importDek(dek);
|
|
208
|
+
return this._open(key, box, aadSecret(sourceId), "locked");
|
|
209
|
+
}
|
|
210
|
+
async deleteSecret(sourceId) {
|
|
211
|
+
requireId(sourceId);
|
|
212
|
+
const envelope = await this._load();
|
|
213
|
+
if (!(sourceId in envelope.secrets)) return;
|
|
214
|
+
const secrets = { ...envelope.secrets };
|
|
215
|
+
delete secrets[sourceId];
|
|
216
|
+
await this._storage.set({ ...envelope, secrets });
|
|
217
|
+
}
|
|
218
|
+
/** Remove the whole vault. The explicit destructive flow only (back up → wipe → create). */
|
|
219
|
+
async destroy() {
|
|
220
|
+
await this._storage.clear();
|
|
221
|
+
}
|
|
222
|
+
// ---- crypto ----
|
|
223
|
+
_random(n) {
|
|
224
|
+
return this._crypto.getRandomValues(new Uint8Array(n));
|
|
225
|
+
}
|
|
226
|
+
/** A fresh envelope (v2 by default, v1 with `kdf: 'pbkdf2'`) wrapping `dek` under `password`. */
|
|
227
|
+
async _sealEnvelope(password, dek, secrets, passkeys, createdAt) {
|
|
228
|
+
const salt = this._random(SALT_BYTES);
|
|
229
|
+
if (this._kdf === "pbkdf2") {
|
|
230
|
+
const kdf2 = { name: "PBKDF2-SHA256", iterations: this._iterations, salt: toBase64(salt) };
|
|
231
|
+
const kek2 = await this._derivePbkdf2(password, salt, this._iterations);
|
|
232
|
+
return { v: 1, kdf: kdf2, dek: await this._seal(kek2, dek, aadDekV1(kdf2)), secrets, createdAt };
|
|
233
|
+
}
|
|
234
|
+
const kdf = { name: "scrypt", ...this._scrypt, salt: toBase64(salt) };
|
|
235
|
+
const kek = await this._deriveScrypt(password, salt, kdf);
|
|
236
|
+
return { v: 2, kdf, dek: await this._seal(kek, dek, aadDekV2(kdf)), secrets, passkeys, createdAt };
|
|
237
|
+
}
|
|
238
|
+
/** The caller's DEK must open this envelope's secrets — it is the key the password protects, not any 32 bytes. */
|
|
239
|
+
async _requireDek(dek, envelope) {
|
|
240
|
+
const ids = Object.keys(envelope.secrets);
|
|
241
|
+
const key = await this._importDek(dek);
|
|
242
|
+
if (ids.length === 0) return;
|
|
243
|
+
const probe = await this._open(key, envelope.secrets[ids[0]], aadSecret(ids[0]), "locked");
|
|
244
|
+
zeroize(probe);
|
|
245
|
+
}
|
|
246
|
+
async _derivePbkdf2(password, salt, iterations) {
|
|
247
|
+
const subtle = this._crypto.subtle;
|
|
248
|
+
const material = await subtle.importKey("raw", toArrayBuffer(new TextEncoder().encode(password.normalize("NFKC"))), "PBKDF2", false, ["deriveKey"]);
|
|
249
|
+
return subtle.deriveKey({ name: "PBKDF2", hash: "SHA-256", salt: toArrayBuffer(salt), iterations }, material, { name: "AES-GCM", length: 256 }, false, ["encrypt", "decrypt"]);
|
|
250
|
+
}
|
|
251
|
+
async _deriveScrypt(password, salt, cost) {
|
|
252
|
+
const pw = new TextEncoder().encode(password.normalize("NFKC"));
|
|
253
|
+
let raw;
|
|
254
|
+
try {
|
|
255
|
+
raw = await scryptAsync(pw, salt, { N: cost.N, r: cost.r, p: cost.p, dkLen: 32, maxmem: 2 * 128 * cost.N * cost.r + 1024 * 1024, ...this._onProgress ? { onProgress: this._onProgress } : {} });
|
|
256
|
+
} finally {
|
|
257
|
+
zeroize(pw);
|
|
258
|
+
}
|
|
259
|
+
try {
|
|
260
|
+
return await this._crypto.subtle.importKey("raw", toArrayBuffer(raw), { name: "AES-GCM", length: 256 }, false, ["encrypt", "decrypt"]);
|
|
261
|
+
} finally {
|
|
262
|
+
zeroize(raw);
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
/** HKDF-SHA256(ikm = PRF output, salt = credential id bytes, info) → AES-256-GCM key. */
|
|
266
|
+
async _derivePasskeyKek(prf, credentialId) {
|
|
267
|
+
const subtle = this._crypto.subtle;
|
|
268
|
+
const ikm = await subtle.importKey("raw", toArrayBuffer(prf), "HKDF", false, ["deriveKey"]);
|
|
269
|
+
return subtle.deriveKey({ name: "HKDF", hash: "SHA-256", salt: toArrayBuffer(fromBase64Url(credentialId)), info: toArrayBuffer(new TextEncoder().encode(HKDF_INFO)) }, ikm, { name: "AES-GCM", length: 256 }, false, ["encrypt", "decrypt"]);
|
|
270
|
+
}
|
|
271
|
+
async _importDek(dek) {
|
|
272
|
+
if (dek.length !== DEK_BYTES) throw new VaultError("Vault key must be 32 bytes.", "invalid-input");
|
|
273
|
+
return this._crypto.subtle.importKey("raw", toArrayBuffer(dek), { name: "AES-GCM", length: 256 }, false, ["encrypt", "decrypt"]);
|
|
274
|
+
}
|
|
275
|
+
async _seal(key, plaintext, aad) {
|
|
276
|
+
const iv = this._random(IV_BYTES);
|
|
277
|
+
const ct = await this._crypto.subtle.encrypt({ name: "AES-GCM", iv: toArrayBuffer(iv), additionalData: toArrayBuffer(new TextEncoder().encode(aad)) }, key, toArrayBuffer(plaintext));
|
|
278
|
+
return { iv: toBase64(iv), data: toBase64(new Uint8Array(ct)) };
|
|
279
|
+
}
|
|
280
|
+
async _open(key, box, aad, onFail) {
|
|
281
|
+
try {
|
|
282
|
+
const pt = await this._crypto.subtle.decrypt(
|
|
283
|
+
{ name: "AES-GCM", iv: toArrayBuffer(fromBase64(box.iv)), additionalData: toArrayBuffer(new TextEncoder().encode(aad)) },
|
|
284
|
+
key,
|
|
285
|
+
toArrayBuffer(fromBase64(box.data))
|
|
286
|
+
);
|
|
287
|
+
return new Uint8Array(pt);
|
|
288
|
+
} catch {
|
|
289
|
+
throw new VaultError(onFail === "wrong-password" ? "Wrong password." : "The vault key does not open this entry; lock and unlock again.", onFail);
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
async _load() {
|
|
293
|
+
const envelope = await this._storage.get();
|
|
294
|
+
if (!envelope) throw new VaultError("No vault in this storage area.", "missing");
|
|
295
|
+
if (!isEnvelopeV1(envelope) && !isEnvelopeV2(envelope)) throw new VaultError("Unsupported vault envelope.", "unsupported-envelope");
|
|
296
|
+
return envelope;
|
|
297
|
+
}
|
|
298
|
+
};
|
|
299
|
+
function isEnvelopeV1(x) {
|
|
300
|
+
if (typeof x !== "object" || x === null) return false;
|
|
301
|
+
const e = x;
|
|
302
|
+
const kdf = e.kdf;
|
|
303
|
+
return e.v === 1 && typeof kdf === "object" && kdf !== null && kdf.name === "PBKDF2-SHA256" && Number.isInteger(kdf.iterations) && typeof kdf.salt === "string" && isBox(e.dek) && typeof e.secrets === "object" && e.secrets !== null && Object.values(e.secrets).every(isBox);
|
|
304
|
+
}
|
|
305
|
+
function isEnvelopeV2(x) {
|
|
306
|
+
if (typeof x !== "object" || x === null) return false;
|
|
307
|
+
const e = x;
|
|
308
|
+
const kdf = e.kdf;
|
|
309
|
+
return e.v === 2 && typeof kdf === "object" && kdf !== null && kdf.name === "scrypt" && Number.isInteger(kdf.N) && Number.isInteger(kdf.r) && Number.isInteger(kdf.p) && typeof kdf.salt === "string" && isBox(e.dek) && typeof e.secrets === "object" && e.secrets !== null && Object.values(e.secrets).every(isBox) && Array.isArray(e.passkeys) && e.passkeys.every((p) => typeof p === "object" && p !== null && typeof p.id === "string" && typeof p.label === "string" && isBox(p.dek));
|
|
310
|
+
}
|
|
311
|
+
function isBox(x) {
|
|
312
|
+
return typeof x === "object" && x !== null && typeof x.iv === "string" && typeof x.data === "string";
|
|
313
|
+
}
|
|
314
|
+
function isStrongScrypt(c) {
|
|
315
|
+
return Number.isInteger(c.N) && c.N >= VAULT_MIN_SCRYPT.N && (c.N & c.N - 1) === 0 && Number.isInteger(c.r) && c.r >= VAULT_MIN_SCRYPT.r && Number.isInteger(c.p) && c.p >= VAULT_MIN_SCRYPT.p;
|
|
316
|
+
}
|
|
317
|
+
function requirePassword(password, minLength = 1) {
|
|
318
|
+
if (typeof password !== "string" || password.length === 0) throw new VaultError("Password must not be empty.", "invalid-input");
|
|
319
|
+
if ([...password].length < minLength) throw new VaultError(`Password must be at least ${minLength} characters.`, "invalid-input");
|
|
320
|
+
}
|
|
321
|
+
function requireId(id) {
|
|
322
|
+
if (typeof id !== "string" || id.length === 0 || id.length > 128) throw new VaultError("Bad source id.", "invalid-input");
|
|
323
|
+
}
|
|
324
|
+
function fromBase64Url(text) {
|
|
325
|
+
const b64 = text.replace(/-/g, "+").replace(/_/g, "/");
|
|
326
|
+
return fromBase64(b64 + "=".repeat((4 - b64.length % 4) % 4));
|
|
327
|
+
}
|
|
328
|
+
function toBase64Url(bytes) {
|
|
329
|
+
return toBase64(bytes).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
330
|
+
}
|
|
331
|
+
function memoryVaultStorage(initial = null) {
|
|
332
|
+
const s = {
|
|
333
|
+
envelope: initial,
|
|
334
|
+
async get() {
|
|
335
|
+
return s.envelope ? structuredClone(s.envelope) : null;
|
|
336
|
+
},
|
|
337
|
+
async set(envelope) {
|
|
338
|
+
s.envelope = structuredClone(envelope);
|
|
339
|
+
},
|
|
340
|
+
async clear() {
|
|
341
|
+
s.envelope = null;
|
|
342
|
+
}
|
|
343
|
+
};
|
|
344
|
+
return s;
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
// src/session.ts
|
|
348
|
+
import { zeroize as zeroize2 } from "@anywalletkit/core";
|
|
349
|
+
var SESSION_DEFAULT_AUTOLOCK_MS = 15 * 6e4;
|
|
350
|
+
var Session = class {
|
|
351
|
+
_storage;
|
|
352
|
+
_alarms;
|
|
353
|
+
_autoLockMs;
|
|
354
|
+
_key;
|
|
355
|
+
_alarm;
|
|
356
|
+
_now;
|
|
357
|
+
_onLock = /* @__PURE__ */ new Set();
|
|
358
|
+
constructor(options) {
|
|
359
|
+
this._storage = options.storage;
|
|
360
|
+
this._alarms = options.alarms;
|
|
361
|
+
this._autoLockMs = options.autoLockMs ?? SESSION_DEFAULT_AUTOLOCK_MS;
|
|
362
|
+
const name = options.name ?? "anywalletkit-session";
|
|
363
|
+
this._key = name;
|
|
364
|
+
this._alarm = `${name}/autolock`;
|
|
365
|
+
this._now = options.now ?? Date.now;
|
|
366
|
+
this._alarms.onFire((fired) => {
|
|
367
|
+
if (fired === this._alarm) void this.lock();
|
|
368
|
+
});
|
|
369
|
+
}
|
|
370
|
+
/** Store the DEK and arm the auto-lock. The caller's copy is zeroized. */
|
|
371
|
+
async unlock(dek) {
|
|
372
|
+
if (dek.length !== 32) throw new VaultError("Vault key must be 32 bytes.", "invalid-input");
|
|
373
|
+
const expiresAt = this._expiry();
|
|
374
|
+
await this._storage.set(this._key, { key: toBase64(dek), expiresAt });
|
|
375
|
+
zeroize2(dek);
|
|
376
|
+
await this._arm(expiresAt);
|
|
377
|
+
}
|
|
378
|
+
/** Forget the DEK and cancel the alarm. Safe to call when already locked. */
|
|
379
|
+
async lock() {
|
|
380
|
+
const was = await this._storage.get(this._key);
|
|
381
|
+
await this._storage.remove(this._key);
|
|
382
|
+
await this._alarms.clear(this._alarm);
|
|
383
|
+
if (was !== void 0 && was !== null) for (const cb of this._onLock) cb();
|
|
384
|
+
}
|
|
385
|
+
async isUnlocked() {
|
|
386
|
+
return await this._read() !== null;
|
|
387
|
+
}
|
|
388
|
+
/**
|
|
389
|
+
* The DEK for one operation (zeroize it after). `locked` when there is none or the
|
|
390
|
+
* timer ran out while the worker was asleep — the record is then removed as well, so
|
|
391
|
+
* a missed alarm cannot keep the key alive.
|
|
392
|
+
*/
|
|
393
|
+
async getKey() {
|
|
394
|
+
const record = await this._read();
|
|
395
|
+
if (!record) throw new VaultError("Wallet is locked.", "locked");
|
|
396
|
+
return fromBase64(record.key);
|
|
397
|
+
}
|
|
398
|
+
/** User activity: push the auto-lock out again. No-op when locked. */
|
|
399
|
+
async touch() {
|
|
400
|
+
const record = await this._read();
|
|
401
|
+
if (!record) return;
|
|
402
|
+
const expiresAt = this._expiry();
|
|
403
|
+
await this._storage.set(this._key, { ...record, expiresAt });
|
|
404
|
+
await this._arm(expiresAt);
|
|
405
|
+
}
|
|
406
|
+
get autoLockMs() {
|
|
407
|
+
return this._autoLockMs;
|
|
408
|
+
}
|
|
409
|
+
/** Change the timer (options page); re-arms from now when unlocked. 0 disables. */
|
|
410
|
+
async setAutoLockMs(ms) {
|
|
411
|
+
if (!Number.isFinite(ms) || ms < 0) throw new VaultError("Auto-lock must be a non-negative number of milliseconds.", "invalid-input");
|
|
412
|
+
this._autoLockMs = ms;
|
|
413
|
+
await this.touch();
|
|
414
|
+
}
|
|
415
|
+
onLock(callback) {
|
|
416
|
+
this._onLock.add(callback);
|
|
417
|
+
return () => this._onLock.delete(callback);
|
|
418
|
+
}
|
|
419
|
+
_expiry() {
|
|
420
|
+
return this._autoLockMs > 0 ? this._now() + this._autoLockMs : Number.POSITIVE_INFINITY;
|
|
421
|
+
}
|
|
422
|
+
async _arm(expiresAt) {
|
|
423
|
+
if (Number.isFinite(expiresAt)) await this._alarms.schedule(this._alarm, expiresAt);
|
|
424
|
+
else await this._alarms.clear(this._alarm);
|
|
425
|
+
}
|
|
426
|
+
async _read() {
|
|
427
|
+
const raw = await this._storage.get(this._key);
|
|
428
|
+
if (!raw || typeof raw.key !== "string" || typeof raw.expiresAt !== "number") return null;
|
|
429
|
+
if (raw.expiresAt <= this._now()) {
|
|
430
|
+
await this.lock();
|
|
431
|
+
return null;
|
|
432
|
+
}
|
|
433
|
+
return raw;
|
|
434
|
+
}
|
|
435
|
+
};
|
|
436
|
+
function memorySessionStorage() {
|
|
437
|
+
const data = /* @__PURE__ */ new Map();
|
|
438
|
+
return {
|
|
439
|
+
data,
|
|
440
|
+
async get(key) {
|
|
441
|
+
return data.get(key);
|
|
442
|
+
},
|
|
443
|
+
async set(key, value) {
|
|
444
|
+
data.set(key, structuredClone(value));
|
|
445
|
+
},
|
|
446
|
+
async remove(key) {
|
|
447
|
+
data.delete(key);
|
|
448
|
+
}
|
|
449
|
+
};
|
|
450
|
+
}
|
|
451
|
+
function memoryAlarms() {
|
|
452
|
+
const pending = /* @__PURE__ */ new Map();
|
|
453
|
+
const handlers = /* @__PURE__ */ new Set();
|
|
454
|
+
return {
|
|
455
|
+
pending,
|
|
456
|
+
schedule(name, when) {
|
|
457
|
+
pending.set(name, when);
|
|
458
|
+
},
|
|
459
|
+
clear(name) {
|
|
460
|
+
pending.delete(name);
|
|
461
|
+
},
|
|
462
|
+
onFire(handler) {
|
|
463
|
+
handlers.add(handler);
|
|
464
|
+
},
|
|
465
|
+
fire(name) {
|
|
466
|
+
pending.delete(name);
|
|
467
|
+
for (const h of handlers) h(name);
|
|
468
|
+
}
|
|
469
|
+
};
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
// src/relay.ts
|
|
473
|
+
var PORT_NAME = "anywalletkit/relay/1";
|
|
474
|
+
var PAGE_TARGET = { toContent: "anywalletkit/content", toPage: "anywalletkit/page" };
|
|
475
|
+
var RELAY_ERRORS = {
|
|
476
|
+
userRejected: 4001,
|
|
477
|
+
unauthorized: 4100,
|
|
478
|
+
unsupportedMethod: 4200,
|
|
479
|
+
disconnected: 4900,
|
|
480
|
+
chainDisconnected: 4901,
|
|
481
|
+
/** EIP-3326/3085: the chain is not in the wallet (`wallet_switchEthereumChain` → suggest `wallet_addEthereumChain`). */
|
|
482
|
+
chainNotAdded: 4902,
|
|
483
|
+
internal: -32603,
|
|
484
|
+
invalidRequest: -32600,
|
|
485
|
+
invalidParams: -32602,
|
|
486
|
+
/** EIP-1474: a request of the same kind from this origin is already waiting (MetaMask's "already pending"). */
|
|
487
|
+
resourceUnavailable: -32002,
|
|
488
|
+
/** EIP-1474: this origin has too many requests waiting. */
|
|
489
|
+
limitExceeded: -32005
|
|
490
|
+
};
|
|
491
|
+
var CHAINS = /* @__PURE__ */ new Set(["eip155", "solana", "ton", "tron", "bip122", "near"]);
|
|
492
|
+
var isRecord = (x) => typeof x === "object" && x !== null;
|
|
493
|
+
var isId = (x) => typeof x === "string" && x.length > 0 && x.length <= 64;
|
|
494
|
+
function isPageRequest(x) {
|
|
495
|
+
return isRecord(x) && x.target === PAGE_TARGET.toContent && isId(x.id) && CHAINS.has(x.chain) && typeof x.method === "string";
|
|
496
|
+
}
|
|
497
|
+
function isPageResponse(x) {
|
|
498
|
+
return isRecord(x) && x.target === PAGE_TARGET.toPage && isId(x.id);
|
|
499
|
+
}
|
|
500
|
+
function isPageEvent(x) {
|
|
501
|
+
return isRecord(x) && x.target === PAGE_TARGET.toPage && !("id" in x) && CHAINS.has(x.chain) && typeof x.event === "string";
|
|
502
|
+
}
|
|
503
|
+
function isRelayRequest(x) {
|
|
504
|
+
return isRecord(x) && x.kind === "request" && isId(x.id) && typeof x.origin === "string" && CHAINS.has(x.chain) && typeof x.method === "string";
|
|
505
|
+
}
|
|
506
|
+
function isRelayToContent(x) {
|
|
507
|
+
return isRecord(x) && (x.kind === "response" && isId(x.id) || x.kind === "event" && typeof x.event === "string");
|
|
508
|
+
}
|
|
509
|
+
function newRequestId() {
|
|
510
|
+
return crypto.randomUUID();
|
|
511
|
+
}
|
|
512
|
+
function stampOrigin(request, origin) {
|
|
513
|
+
const { id, chain, method } = request;
|
|
514
|
+
return "params" in request ? { kind: "request", id, origin, chain, method, params: request.params } : { kind: "request", id, origin, chain, method };
|
|
515
|
+
}
|
|
516
|
+
function checkSenderOrigin(sender, stamped) {
|
|
517
|
+
const fromSender = sender?.origin ?? (sender?.url ? safeOrigin(sender.url) : void 0);
|
|
518
|
+
if (!fromSender || fromSender === "null") return null;
|
|
519
|
+
return fromSender === stamped ? fromSender : null;
|
|
520
|
+
}
|
|
521
|
+
function safeOrigin(url) {
|
|
522
|
+
try {
|
|
523
|
+
return new URL(url).origin;
|
|
524
|
+
} catch {
|
|
525
|
+
return void 0;
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
var relayError = (code, message, data) => data === void 0 ? { code, message } : { code, message, data };
|
|
529
|
+
|
|
530
|
+
// src/chrome.ts
|
|
531
|
+
var VAULT_STORAGE_KEY = "forgewallet.vault.v1";
|
|
532
|
+
function chromeLocalVaultStorage(area, key = VAULT_STORAGE_KEY) {
|
|
533
|
+
return {
|
|
534
|
+
async get() {
|
|
535
|
+
const r = await area.get(key);
|
|
536
|
+
return r[key] ?? null;
|
|
537
|
+
},
|
|
538
|
+
async set(envelope) {
|
|
539
|
+
await area.set({ [key]: envelope });
|
|
540
|
+
},
|
|
541
|
+
async clear() {
|
|
542
|
+
await area.remove(key);
|
|
543
|
+
}
|
|
544
|
+
};
|
|
545
|
+
}
|
|
546
|
+
function chromeSessionStorage(area) {
|
|
547
|
+
const ready = area.setAccessLevel ? area.setAccessLevel({ accessLevel: "TRUSTED_CONTEXTS" }) : Promise.resolve();
|
|
548
|
+
return {
|
|
549
|
+
async get(key) {
|
|
550
|
+
await ready;
|
|
551
|
+
return (await area.get(key))[key];
|
|
552
|
+
},
|
|
553
|
+
async set(key, value) {
|
|
554
|
+
await ready;
|
|
555
|
+
await area.set({ [key]: value });
|
|
556
|
+
},
|
|
557
|
+
async remove(key) {
|
|
558
|
+
await ready;
|
|
559
|
+
await area.remove(key);
|
|
560
|
+
}
|
|
561
|
+
};
|
|
562
|
+
}
|
|
563
|
+
function chromeAlarms(alarms) {
|
|
564
|
+
return {
|
|
565
|
+
async schedule(name, when) {
|
|
566
|
+
await alarms.create(name, { when });
|
|
567
|
+
},
|
|
568
|
+
async clear(name) {
|
|
569
|
+
await alarms.clear(name);
|
|
570
|
+
},
|
|
571
|
+
onFire(handler) {
|
|
572
|
+
alarms.onAlarm.addListener((alarm) => handler(alarm.name));
|
|
573
|
+
}
|
|
574
|
+
};
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
// src/keyring.ts
|
|
578
|
+
import { zeroize as zeroize3 } from "@anywalletkit/core";
|
|
579
|
+
var KEYRING_STORAGE_KEY = "forgewallet.keyring.v1";
|
|
580
|
+
function chromeKeyringStorage(area, key = KEYRING_STORAGE_KEY) {
|
|
581
|
+
return {
|
|
582
|
+
async load() {
|
|
583
|
+
const r = await area.get(key);
|
|
584
|
+
return r[key] ?? null;
|
|
585
|
+
},
|
|
586
|
+
async save(state) {
|
|
587
|
+
await area.set({ [key]: state });
|
|
588
|
+
}
|
|
589
|
+
};
|
|
590
|
+
}
|
|
591
|
+
function vaultSecretStorage(vault, session) {
|
|
592
|
+
const withKey = async (fn) => {
|
|
593
|
+
const dek = await session.getKey();
|
|
594
|
+
try {
|
|
595
|
+
return await fn(dek);
|
|
596
|
+
} finally {
|
|
597
|
+
zeroize3(dek);
|
|
598
|
+
}
|
|
599
|
+
};
|
|
600
|
+
return {
|
|
601
|
+
async get(sourceId) {
|
|
602
|
+
return withKey(async (dek) => {
|
|
603
|
+
const bytes = await vault.getSecret(dek, sourceId);
|
|
604
|
+
if (!bytes) return null;
|
|
605
|
+
try {
|
|
606
|
+
return utf8.decode(bytes);
|
|
607
|
+
} finally {
|
|
608
|
+
zeroize3(bytes);
|
|
609
|
+
}
|
|
610
|
+
});
|
|
611
|
+
},
|
|
612
|
+
async set(sourceId, secret) {
|
|
613
|
+
const bytes = utf8.encode(secret);
|
|
614
|
+
try {
|
|
615
|
+
await withKey((dek) => vault.setSecret(dek, sourceId, bytes));
|
|
616
|
+
} finally {
|
|
617
|
+
zeroize3(bytes);
|
|
618
|
+
}
|
|
619
|
+
},
|
|
620
|
+
async delete(sourceId) {
|
|
621
|
+
await vault.deleteSecret(sourceId);
|
|
622
|
+
}
|
|
623
|
+
};
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
// src/networks.ts
|
|
627
|
+
import { ccChain } from "@anywalletkit/swap2-assets";
|
|
628
|
+
var DEFAULT_CHAIN = "eip155:1";
|
|
629
|
+
var net = (chainId, name, symbol, rpcs, explorer) => explorer === void 0 ? { caip2: `eip155:${chainId}`, chainId, name, symbol, rpcs } : { caip2: `eip155:${chainId}`, chainId, name, symbol, rpcs, explorer };
|
|
630
|
+
var DEFAULT_NETWORKS = Object.freeze([
|
|
631
|
+
net(1, "Ethereum", "ETH", ["https://ethereum-rpc.publicnode.com", "https://eth.drpc.org"], "https://etherscan.io"),
|
|
632
|
+
net(8453, "Base", "ETH", ["https://base-rpc.publicnode.com", "https://mainnet.base.org", "https://base.drpc.org"], "https://basescan.org"),
|
|
633
|
+
net(56, "BNB Chain", "BNB", ["https://bsc-rpc.publicnode.com", "https://bsc-dataseed.bnbchain.org", "https://bsc.drpc.org"], "https://bscscan.com"),
|
|
634
|
+
net(42161, "Arbitrum", "ETH", ["https://arbitrum-one-rpc.publicnode.com", "https://arb1.arbitrum.io/rpc", "https://arbitrum.drpc.org"], "https://arbiscan.io"),
|
|
635
|
+
net(10, "Optimism", "ETH", ["https://optimism-rpc.publicnode.com", "https://mainnet.optimism.io", "https://optimism.drpc.org"], "https://optimistic.etherscan.io"),
|
|
636
|
+
net(137, "Polygon", "POL", ["https://polygon-bor-rpc.publicnode.com", "https://polygon.drpc.org"], "https://polygonscan.com"),
|
|
637
|
+
net(43114, "Avalanche", "AVAX", ["https://avalanche-c-chain-rpc.publicnode.com", "https://api.avax.network/ext/bc/C/rpc", "https://avalanche.drpc.org"], "https://snowtrace.io"),
|
|
638
|
+
net(999, "HyperEVM", "HYPE", ["https://rpc.hyperliquid.xyz/evm", "https://hyperliquid.drpc.org", "https://hyperliquid-json-rpc.stakely.io"]),
|
|
639
|
+
net(4663, "Robinhood Chain", "ETH", ["https://rpc.mainnet.chain.robinhood.com", "https://robinhood.drpc.org"], "https://robinhoodchain.blockscout.com"),
|
|
640
|
+
net(324, "zkSync Era", "ETH", ["https://mainnet.era.zksync.io", "https://zksync.drpc.org"], "https://era.zksync.network"),
|
|
641
|
+
net(42220, "Celo", "CELO", ["https://forno.celo.org", "https://celo.drpc.org"], "https://celoscan.io"),
|
|
642
|
+
net(4326, "MegaETH", "ETH", ["https://mainnet.megaeth.com/rpc"]),
|
|
643
|
+
net(130, "Unichain", "ETH", ["https://mainnet.unichain.org", "https://unichain-rpc.publicnode.com"], "https://uniscan.xyz"),
|
|
644
|
+
net(143, "Monad", "MON", ["https://rpc.monad.xyz"]),
|
|
645
|
+
net(9745, "Plasma", "XPL", ["https://rpc.plasma.to"]),
|
|
646
|
+
net(59144, "Linea", "ETH", ["https://rpc.linea.build", "https://linea-rpc.publicnode.com"], "https://lineascan.build"),
|
|
647
|
+
net(100, "Gnosis", "xDAI", ["https://rpc.gnosischain.com", "https://gnosis-rpc.publicnode.com"], "https://gnosisscan.io"),
|
|
648
|
+
net(146, "Sonic", "S", ["https://rpc.soniclabs.com", "https://sonic-rpc.publicnode.com"], "https://sonicscan.org"),
|
|
649
|
+
net(480, "World Chain", "ETH", ["https://worldchain-mainnet.g.alchemy.com/public", "https://worldchain-mainnet.gateway.tenderly.co"], "https://worldscan.org"),
|
|
650
|
+
net(1329, "Sei", "SEI", ["https://evm-rpc.sei-apis.com"], "https://seitrace.com"),
|
|
651
|
+
net(57073, "Ink", "ETH", ["https://rpc-gel.inkonchain.com"], "https://explorer.inkonchain.com"),
|
|
652
|
+
net(98866, "Plume", "PLUME", ["https://rpc.plume.org"], "https://explorer.plume.org"),
|
|
653
|
+
net(196, "X Layer", "OKB", ["https://rpc.xlayer.tech", "https://xlayerrpc.okx.com"], "https://www.okx.com/web3/explorer/xlayer"),
|
|
654
|
+
net(534352, "Scroll", "ETH", ["https://rpc.scroll.io", "https://scroll-rpc.publicnode.com"], "https://scrollscan.com"),
|
|
655
|
+
net(80094, "Berachain", "BERA", ["https://rpc.berachain.com", "https://berachain-rpc.publicnode.com"], "https://berascan.com"),
|
|
656
|
+
// Arc (Circle's L1): the gas token is USDC. eth_getBalance / msg.value / gas are accounted in 18-decimal units (the
|
|
657
|
+
// `symbol` here names that native view); the ERC-20 interface at 0x3600…0000 reads the SAME balance in 6 decimals
|
|
658
|
+
// and is what the catalog, LI.FI and CCTP address (see swap2-assets). Balances must show one view, not both.
|
|
659
|
+
net(5042, "Arc", "USDC", ["https://rpc.mainnet.arc.io"], "https://explorer.arc.io"),
|
|
660
|
+
net(5e3, "Mantle", "MNT", ["https://rpc.mantle.xyz", "https://mantle-rpc.publicnode.com"], "https://mantlescan.xyz"),
|
|
661
|
+
net(747474, "Katana", "ETH", ["https://rpc.katana.network"], "https://explorer.katanarpc.com"),
|
|
662
|
+
net(2818, "Morph", "ETH", ["https://rpc.morphl2.io", "https://rpc-quicknode.morphl2.io"], "https://explorer.morphl2.io"),
|
|
663
|
+
net(14, "Flare", "FLR", ["https://flare-api.flare.network/ext/C/rpc", "https://flare.rpc.thirdweb.com"], "https://flare-explorer.flare.network")
|
|
664
|
+
]);
|
|
665
|
+
var chainHex = (caip2) => `0x${Number(caip2.split(":")[1]).toString(16)}`;
|
|
666
|
+
function caip2FromChainId(value) {
|
|
667
|
+
let n;
|
|
668
|
+
if (typeof value === "string") {
|
|
669
|
+
const s = value.trim();
|
|
670
|
+
if (/^0x[0-9a-f]+$/i.test(s)) n = Number.parseInt(s, 16);
|
|
671
|
+
else if (/^[0-9]+$/.test(s)) n = Number.parseInt(s, 10);
|
|
672
|
+
else return null;
|
|
673
|
+
} else if (typeof value === "number") n = value;
|
|
674
|
+
else return null;
|
|
675
|
+
return Number.isSafeInteger(n) && n > 0 ? `eip155:${n}` : null;
|
|
676
|
+
}
|
|
677
|
+
function isCatalogChain(caip2) {
|
|
678
|
+
return ccChain(caip2) !== void 0;
|
|
679
|
+
}
|
|
680
|
+
var NetworksTable = class {
|
|
681
|
+
constructor(storage, defaults = DEFAULT_NETWORKS) {
|
|
682
|
+
this.storage = storage;
|
|
683
|
+
this.defaults = defaults;
|
|
684
|
+
}
|
|
685
|
+
storage;
|
|
686
|
+
defaults;
|
|
687
|
+
_custom = null;
|
|
688
|
+
_loading = null;
|
|
689
|
+
/** add/remove run one after another (scan F22, as PermissionStore). */
|
|
690
|
+
_queue = Promise.resolve();
|
|
691
|
+
async list() {
|
|
692
|
+
return [...this.defaults, ...await this.custom()];
|
|
693
|
+
}
|
|
694
|
+
async get(caip2) {
|
|
695
|
+
return this.defaults.find((n) => n.caip2 === caip2) ?? (await this.custom()).find((n) => n.caip2 === caip2) ?? null;
|
|
696
|
+
}
|
|
697
|
+
add(network) {
|
|
698
|
+
return this.serialize(async () => {
|
|
699
|
+
if (this.defaults.some((n) => n.caip2 === network.caip2)) return;
|
|
700
|
+
const custom = (await this.custom()).filter((n) => n.caip2 !== network.caip2);
|
|
701
|
+
custom.push(network);
|
|
702
|
+
await this.write(custom);
|
|
703
|
+
});
|
|
704
|
+
}
|
|
705
|
+
remove(caip2) {
|
|
706
|
+
return this.serialize(async () => {
|
|
707
|
+
const current = await this.custom();
|
|
708
|
+
if (!current.some((n) => n.caip2 === caip2)) return;
|
|
709
|
+
await this.write(current.filter((n) => n.caip2 !== caip2));
|
|
710
|
+
});
|
|
711
|
+
}
|
|
712
|
+
serialize(fn) {
|
|
713
|
+
const run = this._queue.then(fn, fn);
|
|
714
|
+
this._queue = run.then(() => void 0, () => void 0);
|
|
715
|
+
return run;
|
|
716
|
+
}
|
|
717
|
+
/** Cache first (readers see it at once), rolled back when storage refuses. */
|
|
718
|
+
async write(custom) {
|
|
719
|
+
const prev = await this.custom();
|
|
720
|
+
this._custom = custom;
|
|
721
|
+
try {
|
|
722
|
+
await this.storage.set(custom);
|
|
723
|
+
} catch (e) {
|
|
724
|
+
if (this._custom === custom) this._custom = prev;
|
|
725
|
+
throw e;
|
|
726
|
+
}
|
|
727
|
+
}
|
|
728
|
+
async custom() {
|
|
729
|
+
if (this._custom) return this._custom;
|
|
730
|
+
this._loading ??= this.storage.get().then(
|
|
731
|
+
(s) => this._custom ??= s ?? [],
|
|
732
|
+
(e) => {
|
|
733
|
+
this._loading = null;
|
|
734
|
+
throw e;
|
|
735
|
+
}
|
|
736
|
+
);
|
|
737
|
+
return this._loading;
|
|
738
|
+
}
|
|
739
|
+
};
|
|
740
|
+
function parseAddChainParams(params, known = isCatalogChain) {
|
|
741
|
+
const p = Array.isArray(params) ? params[0] : params;
|
|
742
|
+
if (!p || typeof p !== "object") return relayError(RELAY_ERRORS.invalidParams, "wallet_addEthereumChain expects one object parameter.");
|
|
743
|
+
const caip2 = caip2FromChainId(p.chainId);
|
|
744
|
+
if (!caip2) return relayError(RELAY_ERRORS.invalidParams, "chainId must be a 0x-prefixed hex string.");
|
|
745
|
+
if (!known(caip2)) return relayError(RELAY_ERRORS.chainNotAdded, `Chain ${String(p.chainId)} is not in this wallet's catalog.`);
|
|
746
|
+
const rpcs = Array.isArray(p.rpcUrls) ? p.rpcUrls.filter((u) => typeof u === "string" && /^https:\/\/[^\s/?#]+/i.test(u)) : [];
|
|
747
|
+
if (rpcs.length === 0) return relayError(RELAY_ERRORS.invalidParams, "rpcUrls must contain at least one https URL.");
|
|
748
|
+
const name = typeof p.chainName === "string" && p.chainName.trim() ? p.chainName.trim().slice(0, 64) : caip2;
|
|
749
|
+
const currency = p.nativeCurrency;
|
|
750
|
+
if (currency && currency.decimals !== void 0 && currency.decimals !== 18) return relayError(RELAY_ERRORS.invalidParams, "nativeCurrency.decimals must be 18.");
|
|
751
|
+
const symbol = typeof currency?.symbol === "string" && /^[A-Za-z0-9]{1,12}$/.test(currency.symbol) ? currency.symbol : "ETH";
|
|
752
|
+
const explorers = Array.isArray(p.blockExplorerUrls) ? p.blockExplorerUrls.filter((u) => typeof u === "string" && /^https:\/\//i.test(u)) : [];
|
|
753
|
+
const network = { caip2, chainId: Number(caip2.split(":")[1]), name, symbol, rpcs };
|
|
754
|
+
return explorers[0] ? { ...network, explorer: explorers[0] } : network;
|
|
755
|
+
}
|
|
756
|
+
var SOLANA_CHAIN = "solana:mainnet";
|
|
757
|
+
var SOLANA_RPCS = Object.freeze(["https://solana-rpc.publicnode.com", "https://api.mainnet-beta.solana.com"]);
|
|
758
|
+
var TON_CHAIN = "ton:-239";
|
|
759
|
+
var TON_ENDPOINTS = Object.freeze(["https://ton.access.orbs.network/1/mainnet/toncenter-api-v2/jsonRPC", "https://toncenter.com/api/v2/jsonRPC"]);
|
|
760
|
+
var TRON_CHAIN = "tron:728126428";
|
|
761
|
+
var TRON_PROVIDERS = Object.freeze(["https://api.trongrid.io"]);
|
|
762
|
+
var BTC_CHAIN = "bitcoin:mainnet";
|
|
763
|
+
var BTC_ESPLORA = Object.freeze(["https://mempool.space/api", "https://blockstream.info/api"]);
|
|
764
|
+
var NEAR_CHAIN = "near:mainnet";
|
|
765
|
+
var NEAR_PROVIDERS = Object.freeze(["https://free.rpc.fastnear.com", "https://rpc.mainnet.near.org"]);
|
|
766
|
+
var ZCASH_CHAIN = "zcash:mainnet";
|
|
767
|
+
var ZCASH_BLOCKCHAIR = "https://api.blockchair.com/zcash";
|
|
768
|
+
var NON_EVM_CHAINS = Object.freeze([
|
|
769
|
+
{ caip2: SOLANA_CHAIN, key: "solana", name: "Solana", symbol: "SOL", decimals: 9, explorerTx: "https://solscan.io/tx/", explorerAddress: "https://solscan.io/account/" },
|
|
770
|
+
{ caip2: TON_CHAIN, key: "ton", name: "TON", symbol: "GRAM", decimals: 9, explorerTx: "https://tonviewer.com/transaction/", explorerAddress: "https://tonviewer.com/" },
|
|
771
|
+
{ caip2: TRON_CHAIN, key: "tron", name: "Tron", symbol: "TRX", decimals: 6, explorerTx: "https://tronscan.org/#/transaction/", explorerAddress: "https://tronscan.org/#/address/" },
|
|
772
|
+
{ caip2: BTC_CHAIN, key: "btc", name: "Bitcoin", symbol: "BTC", decimals: 8, explorerTx: "https://mempool.space/tx/", explorerAddress: "https://mempool.space/address/" },
|
|
773
|
+
{ caip2: NEAR_CHAIN, key: "near", name: "NEAR", symbol: "NEAR", decimals: 24, explorerTx: "https://nearblocks.io/txns/", explorerAddress: "https://nearblocks.io/address/" },
|
|
774
|
+
{ caip2: ZCASH_CHAIN, key: "zcash", name: "Zcash", symbol: "ZEC", decimals: 8, explorerTx: "https://blockchair.com/zcash/transaction/", explorerAddress: "https://blockchair.com/zcash/address/" }
|
|
775
|
+
]);
|
|
776
|
+
var nonEvmChain = (caip2) => NON_EVM_CHAINS.find((c) => c.caip2 === caip2);
|
|
777
|
+
|
|
778
|
+
// src/storage.ts
|
|
779
|
+
function chromeJsonStorage(area, key) {
|
|
780
|
+
return {
|
|
781
|
+
async get() {
|
|
782
|
+
return (await area.get(key))[key] ?? null;
|
|
783
|
+
},
|
|
784
|
+
async set(value) {
|
|
785
|
+
await area.set({ [key]: value });
|
|
786
|
+
}
|
|
787
|
+
};
|
|
788
|
+
}
|
|
789
|
+
function memoryJsonStorage(initial = null) {
|
|
790
|
+
const s = {
|
|
791
|
+
value: initial,
|
|
792
|
+
async get() {
|
|
793
|
+
return s.value === null ? null : structuredClone(s.value);
|
|
794
|
+
},
|
|
795
|
+
async set(value) {
|
|
796
|
+
s.value = structuredClone(value);
|
|
797
|
+
}
|
|
798
|
+
};
|
|
799
|
+
return s;
|
|
800
|
+
}
|
|
801
|
+
|
|
802
|
+
// src/permissions.ts
|
|
803
|
+
var CHAIN_ACCOUNT_KEYS = ["solanaAccounts", "tonAccounts", "tronAccounts", "btcAccounts", "nearAccounts"];
|
|
804
|
+
var PermissionStore = class {
|
|
805
|
+
constructor(storage) {
|
|
806
|
+
this.storage = storage;
|
|
807
|
+
}
|
|
808
|
+
storage;
|
|
809
|
+
_state = null;
|
|
810
|
+
_loading = null;
|
|
811
|
+
/** Every mutation runs here, one after another (scan F22): a read-modify-write never starts from a snapshot another one is about to replace. */
|
|
812
|
+
_queue = Promise.resolve();
|
|
813
|
+
_listeners = /* @__PURE__ */ new Set();
|
|
814
|
+
async state() {
|
|
815
|
+
if (this._state) return this._state;
|
|
816
|
+
this._loading ??= this.storage.get().then(
|
|
817
|
+
(s) => this._state ??= s ?? {},
|
|
818
|
+
(e) => {
|
|
819
|
+
this._loading = null;
|
|
820
|
+
throw e;
|
|
821
|
+
}
|
|
822
|
+
);
|
|
823
|
+
return this._loading;
|
|
824
|
+
}
|
|
825
|
+
/**
|
|
826
|
+
* Run `fn` after every earlier mutation settled. Public mutators wrap their WHOLE read-modify-write
|
|
827
|
+
* in this; `write` itself is unlocked (setAccountsEverywhere calls it in a loop — a lock there too
|
|
828
|
+
* would deadlock). Nothing awaited inside may wait on the user: routers await the approval sheet
|
|
829
|
+
* first and only then call the store.
|
|
830
|
+
*/
|
|
831
|
+
serialize(fn) {
|
|
832
|
+
const run = this._queue.then(fn, fn);
|
|
833
|
+
this._queue = run.then(() => void 0, () => void 0);
|
|
834
|
+
return run;
|
|
835
|
+
}
|
|
836
|
+
async get(origin) {
|
|
837
|
+
return (await this.state())[origin] ?? null;
|
|
838
|
+
}
|
|
839
|
+
async list() {
|
|
840
|
+
return Object.values(await this.state());
|
|
841
|
+
}
|
|
842
|
+
connect(origin, accounts, chain) {
|
|
843
|
+
return this.serialize(async () => {
|
|
844
|
+
if (accounts.length === 0) throw new Error("Connect at least one account.");
|
|
845
|
+
const current = await this.get(origin);
|
|
846
|
+
const permission = { ...current ?? {}, origin, accounts: [...accounts], chain, connectedAt: current?.connectedAt ?? Date.now() };
|
|
847
|
+
await this.write(origin, permission);
|
|
848
|
+
return permission;
|
|
849
|
+
});
|
|
850
|
+
}
|
|
851
|
+
/** Connect another chain's accounts to the same origin (one "connected sites" list). */
|
|
852
|
+
connectChain(origin, key, accounts, defaultChain) {
|
|
853
|
+
return this.serialize(async () => {
|
|
854
|
+
if (accounts.length === 0) throw new Error("Connect at least one account.");
|
|
855
|
+
const current = await this.get(origin);
|
|
856
|
+
const permission = { origin, accounts: current?.accounts ?? [], chain: current?.chain ?? defaultChain, ...current ?? {}, [key]: [...accounts], connectedAt: current?.connectedAt ?? Date.now() };
|
|
857
|
+
await this.write(origin, permission);
|
|
858
|
+
return permission;
|
|
859
|
+
});
|
|
860
|
+
}
|
|
861
|
+
/** Drop one chain's accounts; the origin stays connected while any chain remains. A no-op (no write) when there is nothing to drop. */
|
|
862
|
+
disconnectChain(origin, key) {
|
|
863
|
+
return this.serialize(async () => {
|
|
864
|
+
const current = await this.get(origin);
|
|
865
|
+
if (!current) return;
|
|
866
|
+
if (key === "accounts" ? current.accounts.length === 0 : current[key] === void 0) return;
|
|
867
|
+
const next = { ...current, [key]: key === "accounts" ? [] : void 0 };
|
|
868
|
+
if (key !== "accounts") delete next[key];
|
|
869
|
+
const anything = next.accounts.length > 0 || CHAIN_ACCOUNT_KEYS.some((k) => (next[k]?.length ?? 0) > 0);
|
|
870
|
+
await this.write(origin, anything ? next : null);
|
|
871
|
+
});
|
|
872
|
+
}
|
|
873
|
+
setChain(origin, chain) {
|
|
874
|
+
return this.serialize(async () => {
|
|
875
|
+
const current = await this.get(origin);
|
|
876
|
+
if (!current) return null;
|
|
877
|
+
const next = { ...current, chain };
|
|
878
|
+
await this.write(origin, next);
|
|
879
|
+
return next;
|
|
880
|
+
});
|
|
881
|
+
}
|
|
882
|
+
/**
|
|
883
|
+
* The active EVM account changed: every origin holding an EVM grant now sees `accounts` (MetaMask behaviour).
|
|
884
|
+
* An origin connected only on another chain (`accounts: []` — Solana/TON/Tron/BTC/NEAR providers, a WalletConnect
|
|
885
|
+
* session without eip155) is skipped: it never passed an EVM connect and must not learn the EVM address (scan F6/F8).
|
|
886
|
+
* Lock never writes `[]` here, so the skip cannot strip a real EVM grant.
|
|
887
|
+
*/
|
|
888
|
+
setAccountsEverywhere(accounts) {
|
|
889
|
+
return this.serialize(async () => {
|
|
890
|
+
const changed = [];
|
|
891
|
+
for (const p of await this.list()) {
|
|
892
|
+
if (p.accounts.length === 0 || sameList(p.accounts, accounts)) continue;
|
|
893
|
+
const next = { ...p, accounts: [...accounts] };
|
|
894
|
+
await this.write(p.origin, next);
|
|
895
|
+
changed.push(next);
|
|
896
|
+
}
|
|
897
|
+
return changed;
|
|
898
|
+
});
|
|
899
|
+
}
|
|
900
|
+
disconnect(origin) {
|
|
901
|
+
return this.serialize(async () => {
|
|
902
|
+
if (!await this.get(origin)) return;
|
|
903
|
+
await this.write(origin, null);
|
|
904
|
+
});
|
|
905
|
+
}
|
|
906
|
+
onChange(listener) {
|
|
907
|
+
this._listeners.add(listener);
|
|
908
|
+
return () => this._listeners.delete(listener);
|
|
909
|
+
}
|
|
910
|
+
/** Unlocked: callers hold the queue. The cache changes before the await (readers see it at once) and is rolled back when storage refuses. */
|
|
911
|
+
async write(origin, permission) {
|
|
912
|
+
const prev = await this.state();
|
|
913
|
+
const state = { ...prev };
|
|
914
|
+
if (permission) state[origin] = permission;
|
|
915
|
+
else delete state[origin];
|
|
916
|
+
this._state = state;
|
|
917
|
+
try {
|
|
918
|
+
await this.storage.set(state);
|
|
919
|
+
} catch (e) {
|
|
920
|
+
if (this._state === state) this._state = prev;
|
|
921
|
+
throw e;
|
|
922
|
+
}
|
|
923
|
+
for (const l of this._listeners) l(origin, permission);
|
|
924
|
+
}
|
|
925
|
+
};
|
|
926
|
+
var sameList = (a, b) => a.length === b.length && a.every((x, i) => x === b[i]);
|
|
927
|
+
|
|
928
|
+
// src/approvals.ts
|
|
929
|
+
var ReviewDeclined = class extends Error {
|
|
930
|
+
code = RELAY_ERRORS.userRejected;
|
|
931
|
+
constructor(message = "You declined \u2014 nothing was signed.") {
|
|
932
|
+
super(message);
|
|
933
|
+
this.name = "ReviewDeclined";
|
|
934
|
+
}
|
|
935
|
+
};
|
|
936
|
+
var QueueLimitError = class extends Error {
|
|
937
|
+
constructor(code, message) {
|
|
938
|
+
super(message);
|
|
939
|
+
this.code = code;
|
|
940
|
+
this.name = "QueueLimitError";
|
|
941
|
+
}
|
|
942
|
+
code;
|
|
943
|
+
};
|
|
944
|
+
var APPROVALS_MAX_PENDING_PER_ORIGIN = 3;
|
|
945
|
+
var ApprovalQueue = class {
|
|
946
|
+
constructor(window, newId = () => crypto.randomUUID(), options = {}) {
|
|
947
|
+
this.window = window;
|
|
948
|
+
this.newId = newId;
|
|
949
|
+
this.options = options;
|
|
950
|
+
window.onClosed((id) => {
|
|
951
|
+
if (this._current?.request.id === id) this.decline(id);
|
|
952
|
+
});
|
|
953
|
+
}
|
|
954
|
+
window;
|
|
955
|
+
newId;
|
|
956
|
+
options;
|
|
957
|
+
_current = null;
|
|
958
|
+
_waiting = [];
|
|
959
|
+
_listeners = /* @__PURE__ */ new Set();
|
|
960
|
+
_via = /* @__PURE__ */ new Map();
|
|
961
|
+
/** Enqueue and wait for the user's answer. Refuses (never queues) past the per-origin limits. */
|
|
962
|
+
request(input) {
|
|
963
|
+
const sameOrigin = [...this._current ? [this._current] : [], ...this._waiting].filter((p) => p.request.origin === input.origin);
|
|
964
|
+
if (input.kind === "connect" && sameOrigin.some((p) => p.request.kind === "connect")) {
|
|
965
|
+
return Promise.reject(new QueueLimitError(RELAY_ERRORS.resourceUnavailable, "A connect request from this site is already waiting for your answer."));
|
|
966
|
+
}
|
|
967
|
+
const max = this.options.maxPendingPerOrigin ?? APPROVALS_MAX_PENDING_PER_ORIGIN;
|
|
968
|
+
if (sameOrigin.length >= max) {
|
|
969
|
+
return Promise.reject(new QueueLimitError(RELAY_ERRORS.limitExceeded, `This site already has ${max} requests waiting for you; answer or close them first.`));
|
|
970
|
+
}
|
|
971
|
+
return new Promise((resolve, reject) => {
|
|
972
|
+
const warnings = this.options.annotate?.(input.origin) ?? [];
|
|
973
|
+
const via = this._via.get(input.origin);
|
|
974
|
+
const pending = { request: { ...input, id: this.newId(), createdAt: Date.now(), ...warnings.length ? { originWarnings: warnings } : {}, ...via ? { via } : {} }, resolve, reject };
|
|
975
|
+
this._waiting.push(pending);
|
|
976
|
+
void this.next();
|
|
977
|
+
});
|
|
978
|
+
}
|
|
979
|
+
/** Attach (or clear) the transport note every later request from `origin` carries. */
|
|
980
|
+
setVia(origin, via) {
|
|
981
|
+
if (via) this._via.set(origin, via);
|
|
982
|
+
else this._via.delete(origin);
|
|
983
|
+
}
|
|
984
|
+
/** The transport note later requests from `origin` carry (WalletConnect Verify's verdict for `wc:<topic>`), if any. */
|
|
985
|
+
viaFor(origin) {
|
|
986
|
+
return this._via.get(origin);
|
|
987
|
+
}
|
|
988
|
+
/** Requests waiting for `origin` (current + queued). */
|
|
989
|
+
pendingFor(origin) {
|
|
990
|
+
return [...this._current ? [this._current] : [], ...this._waiting].filter((p) => p.request.origin === origin).length;
|
|
991
|
+
}
|
|
992
|
+
/** The request currently shown (the window asks for it by id on load). */
|
|
993
|
+
get current() {
|
|
994
|
+
return this._current?.request ?? null;
|
|
995
|
+
}
|
|
996
|
+
get queued() {
|
|
997
|
+
return this._waiting.length;
|
|
998
|
+
}
|
|
999
|
+
resolve(requestId, value) {
|
|
1000
|
+
return this.settle(requestId, (p) => p.resolve(value));
|
|
1001
|
+
}
|
|
1002
|
+
decline(requestId, error = new ReviewDeclined()) {
|
|
1003
|
+
return this.settle(requestId, (p) => p.reject(error));
|
|
1004
|
+
}
|
|
1005
|
+
/** Lock, disconnect, shutdown: everything pending is declined. */
|
|
1006
|
+
declineAll(error = new ReviewDeclined("The wallet was locked.")) {
|
|
1007
|
+
const all = [...this._current ? [this._current] : [], ...this._waiting.splice(0)];
|
|
1008
|
+
for (const p of all) p.reject(error);
|
|
1009
|
+
if (this._current) void this.window.close(this._current.request.id);
|
|
1010
|
+
this._current = null;
|
|
1011
|
+
this.emit();
|
|
1012
|
+
}
|
|
1013
|
+
onChange(listener) {
|
|
1014
|
+
this._listeners.add(listener);
|
|
1015
|
+
return () => this._listeners.delete(listener);
|
|
1016
|
+
}
|
|
1017
|
+
settle(requestId, fn) {
|
|
1018
|
+
if (this._current?.request.id === requestId) {
|
|
1019
|
+
const p = this._current;
|
|
1020
|
+
this._current = null;
|
|
1021
|
+
fn(p);
|
|
1022
|
+
void this.window.close(requestId).catch(() => void 0);
|
|
1023
|
+
this.emit();
|
|
1024
|
+
void this.next();
|
|
1025
|
+
return true;
|
|
1026
|
+
}
|
|
1027
|
+
const i = this._waiting.findIndex((p) => p.request.id === requestId);
|
|
1028
|
+
if (i < 0) return false;
|
|
1029
|
+
fn(this._waiting.splice(i, 1)[0]);
|
|
1030
|
+
this.emit();
|
|
1031
|
+
return true;
|
|
1032
|
+
}
|
|
1033
|
+
async next() {
|
|
1034
|
+
if (this._current || this._waiting.length === 0) return;
|
|
1035
|
+
const p = this._waiting.shift();
|
|
1036
|
+
this._current = p;
|
|
1037
|
+
this.emit();
|
|
1038
|
+
try {
|
|
1039
|
+
await this.window.open(p.request);
|
|
1040
|
+
} catch (e) {
|
|
1041
|
+
if (this._current === p) {
|
|
1042
|
+
this._current = null;
|
|
1043
|
+
p.reject(e instanceof Error ? e : new Error(String(e)));
|
|
1044
|
+
this.emit();
|
|
1045
|
+
void this.next();
|
|
1046
|
+
}
|
|
1047
|
+
}
|
|
1048
|
+
}
|
|
1049
|
+
emit() {
|
|
1050
|
+
for (const l of this._listeners) l();
|
|
1051
|
+
}
|
|
1052
|
+
};
|
|
1053
|
+
function memoryApprovalWindow() {
|
|
1054
|
+
let handler = () => void 0;
|
|
1055
|
+
const w = {
|
|
1056
|
+
opened: [],
|
|
1057
|
+
openIds: /* @__PURE__ */ new Set(),
|
|
1058
|
+
async open(request) {
|
|
1059
|
+
w.opened.push(request);
|
|
1060
|
+
w.openIds.add(request.id);
|
|
1061
|
+
},
|
|
1062
|
+
async close(id) {
|
|
1063
|
+
w.openIds.delete(id);
|
|
1064
|
+
},
|
|
1065
|
+
onClosed(h) {
|
|
1066
|
+
handler = h;
|
|
1067
|
+
},
|
|
1068
|
+
userClose(id) {
|
|
1069
|
+
w.openIds.delete(id);
|
|
1070
|
+
handler(id);
|
|
1071
|
+
}
|
|
1072
|
+
};
|
|
1073
|
+
return w;
|
|
1074
|
+
}
|
|
1075
|
+
|
|
1076
|
+
// src/router.ts
|
|
1077
|
+
var RouterError = class _RouterError extends Error {
|
|
1078
|
+
code;
|
|
1079
|
+
data;
|
|
1080
|
+
constructor(code, message, data) {
|
|
1081
|
+
super(message);
|
|
1082
|
+
this.name = "RouterError";
|
|
1083
|
+
this.code = code;
|
|
1084
|
+
this.data = data;
|
|
1085
|
+
}
|
|
1086
|
+
static from(e) {
|
|
1087
|
+
return new _RouterError(e.code, e.message, e.data);
|
|
1088
|
+
}
|
|
1089
|
+
};
|
|
1090
|
+
function toRelayError(e) {
|
|
1091
|
+
if (e instanceof RouterError) return e.data === void 0 ? { code: e.code, message: e.message } : { code: e.code, message: e.message, data: e.data };
|
|
1092
|
+
if (e instanceof ReviewDeclined) return { code: e.code, message: e.message };
|
|
1093
|
+
const code = typeof e?.code === "number" ? e.code : RELAY_ERRORS.internal;
|
|
1094
|
+
return { code, message: e instanceof Error ? e.message : String(e) };
|
|
1095
|
+
}
|
|
1096
|
+
var READ_METHODS = /* @__PURE__ */ new Set([
|
|
1097
|
+
"eth_blockNumber",
|
|
1098
|
+
"eth_call",
|
|
1099
|
+
"eth_estimateGas",
|
|
1100
|
+
"eth_feeHistory",
|
|
1101
|
+
"eth_gasPrice",
|
|
1102
|
+
"eth_getBalance",
|
|
1103
|
+
"eth_getBlockByHash",
|
|
1104
|
+
"eth_getBlockByNumber",
|
|
1105
|
+
"eth_getCode",
|
|
1106
|
+
"eth_getLogs",
|
|
1107
|
+
"eth_getStorageAt",
|
|
1108
|
+
"eth_getTransactionByHash",
|
|
1109
|
+
"eth_getTransactionCount",
|
|
1110
|
+
"eth_getTransactionReceipt",
|
|
1111
|
+
"eth_maxPriorityFeePerGas"
|
|
1112
|
+
]);
|
|
1113
|
+
var SIGNING_METHODS = /* @__PURE__ */ new Set(["personal_sign", "eth_signTypedData_v4", "eth_signTypedData_v3", "eth_signTypedData", "eth_sendTransaction"]);
|
|
1114
|
+
var RequestRouter = class {
|
|
1115
|
+
constructor(deps) {
|
|
1116
|
+
this.deps = deps;
|
|
1117
|
+
}
|
|
1118
|
+
deps;
|
|
1119
|
+
/** The chain an origin currently sees (its permission's, else the default). */
|
|
1120
|
+
async chainFor(origin) {
|
|
1121
|
+
const permission = await this.deps.permissions.get(origin);
|
|
1122
|
+
const wanted = permission?.chain ?? this.deps.defaultChain ?? DEFAULT_CHAIN;
|
|
1123
|
+
return await this.deps.networks.get(wanted) ?? await this.deps.networks.get(DEFAULT_CHAIN);
|
|
1124
|
+
}
|
|
1125
|
+
async handle(origin, chain, method, rawParams) {
|
|
1126
|
+
if (chain !== "eip155") throw new RouterError(RELAY_ERRORS.unsupportedMethod, `${chain} providers are not available yet.`);
|
|
1127
|
+
const params = Array.isArray(rawParams) ? rawParams : rawParams === void 0 ? [] : [rawParams];
|
|
1128
|
+
const network = await this.chainFor(origin);
|
|
1129
|
+
const permission = await this.deps.permissions.get(origin);
|
|
1130
|
+
switch (method) {
|
|
1131
|
+
case "eth_chainId":
|
|
1132
|
+
return chainHex(network.caip2);
|
|
1133
|
+
case "net_version":
|
|
1134
|
+
return String(network.chainId);
|
|
1135
|
+
case "eth_accounts":
|
|
1136
|
+
return this.visibleAccounts(permission);
|
|
1137
|
+
case "eth_requestAccounts":
|
|
1138
|
+
return this.connect(origin, network);
|
|
1139
|
+
case "wallet_getPermissions":
|
|
1140
|
+
return permission && permission.accounts.length > 0 ? [permissionDescriptor(origin, permission.accounts)] : [];
|
|
1141
|
+
case "wallet_requestPermissions": {
|
|
1142
|
+
const accounts = await this.connect(origin, network);
|
|
1143
|
+
return [permissionDescriptor(origin, accounts)];
|
|
1144
|
+
}
|
|
1145
|
+
case "wallet_revokePermissions":
|
|
1146
|
+
await this.disconnect(origin);
|
|
1147
|
+
return null;
|
|
1148
|
+
case "wallet_switchEthereumChain": {
|
|
1149
|
+
const target = caip2FromChainId(params[0]?.chainId);
|
|
1150
|
+
if (!target) throw new RouterError(RELAY_ERRORS.invalidParams, "wallet_switchEthereumChain expects [{ chainId }].");
|
|
1151
|
+
const to = await this.deps.networks.get(target);
|
|
1152
|
+
if (!to) throw new RouterError(RELAY_ERRORS.chainNotAdded, `Chain ${chainHex(target)} is not in this wallet; add it with wallet_addEthereumChain.`);
|
|
1153
|
+
await this.switchChain(origin, network, to);
|
|
1154
|
+
return null;
|
|
1155
|
+
}
|
|
1156
|
+
case "wallet_addEthereumChain": {
|
|
1157
|
+
const parsed = parseAddChainParams(params);
|
|
1158
|
+
if ("code" in parsed) throw RouterError.from(parsed);
|
|
1159
|
+
const existing = await this.deps.networks.get(parsed.caip2);
|
|
1160
|
+
if (existing) {
|
|
1161
|
+
await this.switchChain(origin, network, existing);
|
|
1162
|
+
return null;
|
|
1163
|
+
}
|
|
1164
|
+
await this.deps.approvals.request({ origin, chain: network.caip2, kind: "add-chain", payload: { network: parsed } });
|
|
1165
|
+
let answered;
|
|
1166
|
+
try {
|
|
1167
|
+
answered = await this.deps.rpc(parsed, "eth_chainId", []);
|
|
1168
|
+
} catch (e) {
|
|
1169
|
+
throw new RouterError(RELAY_ERRORS.invalidParams, `The RPC for ${parsed.name} did not answer eth_chainId (${e instanceof Error ? e.message : String(e)}); the network was not added.`);
|
|
1170
|
+
}
|
|
1171
|
+
const got = caip2FromChainId(answered);
|
|
1172
|
+
if (got !== parsed.caip2) throw new RouterError(RELAY_ERRORS.invalidParams, `The RPC for ${parsed.name} answers chain ${String(answered)}, not ${chainHex(parsed.caip2)}; the network was not added.`);
|
|
1173
|
+
await this.deps.networks.add({ ...parsed, custom: { origin, addedAt: Date.now() } });
|
|
1174
|
+
if (permission) await this.switchChain(origin, network, parsed, true);
|
|
1175
|
+
return null;
|
|
1176
|
+
}
|
|
1177
|
+
case "eth_sign":
|
|
1178
|
+
case "eth_signTransaction":
|
|
1179
|
+
throw new RouterError(RELAY_ERRORS.unsupportedMethod, `${method} (blind signing) is never offered by this wallet.`);
|
|
1180
|
+
}
|
|
1181
|
+
if (READ_METHODS.has(method)) return this.deps.rpc(network, method, params);
|
|
1182
|
+
if (SIGNING_METHODS.has(method)) {
|
|
1183
|
+
if (!this.deps.sign) throw new RouterError(RELAY_ERRORS.unsupportedMethod, `${method} is not available yet.`);
|
|
1184
|
+
if (!permission) throw new RouterError(RELAY_ERRORS.unauthorized, "Connect the wallet first (eth_requestAccounts).");
|
|
1185
|
+
return this.deps.sign({ origin, network, method, params, accounts: permission.accounts });
|
|
1186
|
+
}
|
|
1187
|
+
throw new RouterError(RELAY_ERRORS.unsupportedMethod, `This wallet does not support ${method}.`);
|
|
1188
|
+
}
|
|
1189
|
+
/** What a connected origin sees right now: its permitted accounts that the wallet can expose (none while locked). */
|
|
1190
|
+
async visibleAccounts(permission) {
|
|
1191
|
+
if (!permission) return [];
|
|
1192
|
+
const available = await this.deps.accounts();
|
|
1193
|
+
return permission.accounts.filter((a) => available.includes(a));
|
|
1194
|
+
}
|
|
1195
|
+
/**
|
|
1196
|
+
* Connected and unlocked → the accounts; otherwise the approval window (which
|
|
1197
|
+
* unlocks first when needed) picks them. Emits `connect` + `accountsChanged`.
|
|
1198
|
+
*/
|
|
1199
|
+
async connect(origin, network) {
|
|
1200
|
+
const existing = await this.visibleAccounts(await this.deps.permissions.get(origin));
|
|
1201
|
+
if (existing.length) return existing;
|
|
1202
|
+
const chosen = await this.deps.approvals.request({ origin, chain: network.caip2, kind: "connect", payload: { accounts: await this.deps.accounts() } });
|
|
1203
|
+
const available = await this.deps.accounts();
|
|
1204
|
+
const accounts = Array.isArray(chosen) ? chosen.filter((a) => typeof a === "string" && available.includes(a)) : [];
|
|
1205
|
+
if (accounts.length === 0) throw new ReviewDeclined("No account was selected.");
|
|
1206
|
+
await this.deps.permissions.connect(origin, accounts, network.caip2);
|
|
1207
|
+
this.deps.emit(origin, "connect", { chainId: chainHex(network.caip2) });
|
|
1208
|
+
this.deps.emit(origin, "accountsChanged", accounts);
|
|
1209
|
+
return accounts;
|
|
1210
|
+
}
|
|
1211
|
+
async disconnect(origin) {
|
|
1212
|
+
if (!await this.deps.permissions.get(origin)) return;
|
|
1213
|
+
await this.deps.permissions.disconnect(origin);
|
|
1214
|
+
this.deps.emit(origin, "accountsChanged", []);
|
|
1215
|
+
this.deps.emit(origin, "disconnect", { code: RELAY_ERRORS.disconnected, message: "The wallet disconnected this site." });
|
|
1216
|
+
}
|
|
1217
|
+
async switchChain(origin, from, to, approved = false) {
|
|
1218
|
+
if (from.caip2 === to.caip2) return;
|
|
1219
|
+
const permission = await this.deps.permissions.get(origin);
|
|
1220
|
+
if (!permission) throw new RouterError(RELAY_ERRORS.unauthorized, "Connect the wallet first (eth_requestAccounts).");
|
|
1221
|
+
if (!approved) await this.deps.approvals.request({ origin, chain: from.caip2, kind: "switch-chain", payload: { from, to } });
|
|
1222
|
+
await this.deps.permissions.setChain(origin, to.caip2);
|
|
1223
|
+
this.deps.emit(origin, "chainChanged", chainHex(to.caip2));
|
|
1224
|
+
}
|
|
1225
|
+
};
|
|
1226
|
+
var permissionDescriptor = (origin, accounts) => ({
|
|
1227
|
+
id: crypto.randomUUID(),
|
|
1228
|
+
parentCapability: "eth_accounts",
|
|
1229
|
+
invoker: origin,
|
|
1230
|
+
caveats: [{ type: "restrictReturnedAccounts", value: [...accounts] }],
|
|
1231
|
+
date: Date.now()
|
|
1232
|
+
});
|
|
1233
|
+
|
|
1234
|
+
// src/siwe.ts
|
|
1235
|
+
var HEADER_LINE = /^(?<domain>[^\n]+?) wants you to sign in with your Ethereum account:(?:\n|$)/;
|
|
1236
|
+
var ADDRESS_LINE = /^0x[0-9a-fA-F]{40}$/;
|
|
1237
|
+
var FIELD = /^([A-Za-z ]+): (.*)$/;
|
|
1238
|
+
function parseLoose(text) {
|
|
1239
|
+
const head = HEADER_LINE.exec(text);
|
|
1240
|
+
if (!head?.groups) return null;
|
|
1241
|
+
const lines = text.slice(head[0].length).split("\n");
|
|
1242
|
+
const missing = [];
|
|
1243
|
+
const addressLine = lines.shift() ?? "";
|
|
1244
|
+
const address = ADDRESS_LINE.test(addressLine) ? addressLine : "";
|
|
1245
|
+
if (!address) missing.push("address");
|
|
1246
|
+
const uriAt = lines.findIndex((l) => l.startsWith("URI: "));
|
|
1247
|
+
const between = (uriAt < 0 ? lines : lines.slice(0, uriAt)).filter((l) => l !== "");
|
|
1248
|
+
let statement;
|
|
1249
|
+
if (uriAt >= 0) {
|
|
1250
|
+
if (between.length === 1 && !FIELD.test(between[0])) statement = between[0];
|
|
1251
|
+
else if (between.length > 0) missing.push("statement layout");
|
|
1252
|
+
}
|
|
1253
|
+
const fields = {};
|
|
1254
|
+
const resources = [];
|
|
1255
|
+
for (const line of uriAt < 0 ? [] : lines.slice(uriAt)) {
|
|
1256
|
+
if (line.startsWith("- ")) {
|
|
1257
|
+
resources.push(line.slice(2));
|
|
1258
|
+
continue;
|
|
1259
|
+
}
|
|
1260
|
+
const m = FIELD.exec(line);
|
|
1261
|
+
if (m && !(m[1] in fields)) fields[m[1]] = m[2];
|
|
1262
|
+
}
|
|
1263
|
+
const chainId = Number(fields["Chain ID"]);
|
|
1264
|
+
if (!fields.URI) missing.push("URI");
|
|
1265
|
+
if (!fields.Version) missing.push("Version");
|
|
1266
|
+
if (fields["Chain ID"] === void 0 || !Number.isInteger(chainId)) missing.push("Chain ID");
|
|
1267
|
+
if (!fields.Nonce) missing.push("Nonce");
|
|
1268
|
+
if (!fields["Issued At"]) missing.push("Issued At");
|
|
1269
|
+
const message = { domain: head.groups.domain, address, uri: fields.URI ?? "", version: fields.Version ?? "", chainId: Number.isInteger(chainId) ? chainId : NaN, nonce: fields.Nonce ?? "", issuedAt: fields["Issued At"] ?? "", resources };
|
|
1270
|
+
if (statement) message.statement = statement;
|
|
1271
|
+
if (fields["Expiration Time"]) message.expirationTime = fields["Expiration Time"];
|
|
1272
|
+
if (fields["Not Before"]) message.notBefore = fields["Not Before"];
|
|
1273
|
+
if (fields["Request ID"]) message.requestId = fields["Request ID"];
|
|
1274
|
+
return { message, missing };
|
|
1275
|
+
}
|
|
1276
|
+
function parseSiwe(text) {
|
|
1277
|
+
const p = parseLoose(text);
|
|
1278
|
+
return p && p.missing.length === 0 ? p.message : null;
|
|
1279
|
+
}
|
|
1280
|
+
function whereOf(s) {
|
|
1281
|
+
try {
|
|
1282
|
+
if (!s.includes("://")) {
|
|
1283
|
+
const host = new URL(`https://${s}`).host.toLowerCase();
|
|
1284
|
+
return host ? { scheme: "https", host } : null;
|
|
1285
|
+
}
|
|
1286
|
+
const u = new URL(s);
|
|
1287
|
+
return u.host ? { scheme: u.protocol.replace(/:$/, "").toLowerCase(), host: u.host.toLowerCase() } : null;
|
|
1288
|
+
} catch {
|
|
1289
|
+
return null;
|
|
1290
|
+
}
|
|
1291
|
+
}
|
|
1292
|
+
function originWhere(origin) {
|
|
1293
|
+
try {
|
|
1294
|
+
const u = new URL(origin);
|
|
1295
|
+
return (u.protocol === "https:" || u.protocol === "http:") && u.host ? { scheme: u.protocol.slice(0, -1), host: u.host.toLowerCase() } : null;
|
|
1296
|
+
} catch {
|
|
1297
|
+
return null;
|
|
1298
|
+
}
|
|
1299
|
+
}
|
|
1300
|
+
function isLocalHost(host) {
|
|
1301
|
+
const h = host.toLowerCase().replace(/:\d+$/, "");
|
|
1302
|
+
return h === "localhost" || h === "127.0.0.1" || h === "[::1]" || h.endsWith(".localhost");
|
|
1303
|
+
}
|
|
1304
|
+
function siweProblems(m, ctx) {
|
|
1305
|
+
const out = [];
|
|
1306
|
+
const domain = whereOf(m.domain);
|
|
1307
|
+
let site = originWhere(ctx.origin);
|
|
1308
|
+
if (!site) {
|
|
1309
|
+
const claimed = ctx.via?.verify === "VALID" && !ctx.via.scam ? whereOf(ctx.via.url) : null;
|
|
1310
|
+
if (claimed && domain && claimed.host === domain.host) site = claimed;
|
|
1311
|
+
else out.push(`The wallet cannot verify which site is asking; this sign-in for ${m.domain} may be replayed there.`);
|
|
1312
|
+
}
|
|
1313
|
+
if (site) {
|
|
1314
|
+
const local = isLocalHost(site.host);
|
|
1315
|
+
if (site.scheme === "http" && !local) out.push(`This sign-in is requested by an insecure page (${site.scheme}://${site.host}); anyone on the network can read or change it and replay the signature on the https site.`);
|
|
1316
|
+
if (!domain || domain.host !== site.host) out.push(`The sign-in names ${m.domain} but the request comes from ${site.host} \u2014 a sign-in meant for another site would be replayable there.`);
|
|
1317
|
+
else if (!local && domain.scheme !== site.scheme) out.push(`The sign-in is for ${domain.scheme}://${domain.host} but the request comes from ${site.scheme}://${site.host}.`);
|
|
1318
|
+
const uri = m.uri ? whereOf(m.uri) : null;
|
|
1319
|
+
if (uri && uri.host !== site.host) out.push(`The URI ${m.uri} is not on ${site.host}.`);
|
|
1320
|
+
else if (uri && !local && (uri.scheme === "http" || uri.scheme === "https") && uri.scheme !== site.scheme) out.push(`The URI ${m.uri} uses ${uri.scheme}:// but the request comes from ${site.scheme}://${site.host}.`);
|
|
1321
|
+
}
|
|
1322
|
+
if (m.address && m.address.toLowerCase() !== ctx.account.toLowerCase()) out.push(`The sign-in is for ${m.address}, not the account that would sign (${ctx.account}).`);
|
|
1323
|
+
if (Number.isInteger(m.chainId) && m.chainId !== ctx.chainId) out.push(`The sign-in is for chain ${m.chainId}; this site is on chain ${ctx.chainId}.`);
|
|
1324
|
+
if (m.version && m.version !== "1") out.push(`Unknown SIWE version ${m.version}.`);
|
|
1325
|
+
if (m.nonce && (m.nonce.length < 8 || !/^[A-Za-z0-9]+$/.test(m.nonce))) out.push("The nonce is too short to protect against replay.");
|
|
1326
|
+
const now = ctx.now ?? Date.now();
|
|
1327
|
+
const exp = m.expirationTime ? Date.parse(m.expirationTime) : NaN;
|
|
1328
|
+
if (m.expirationTime && (Number.isNaN(exp) || exp <= now)) out.push("The sign-in has already expired.");
|
|
1329
|
+
const nbf = m.notBefore ? Date.parse(m.notBefore) : NaN;
|
|
1330
|
+
if (m.notBefore && (Number.isNaN(nbf) || nbf > now)) out.push("The sign-in is not valid yet (Not Before is in the future).");
|
|
1331
|
+
if (m.issuedAt) {
|
|
1332
|
+
const iat = Date.parse(m.issuedAt);
|
|
1333
|
+
if (Number.isNaN(iat)) out.push("Issued At is not a date.");
|
|
1334
|
+
else if (iat > now + 5 * 6e4) out.push("Issued At is in the future.");
|
|
1335
|
+
}
|
|
1336
|
+
return out;
|
|
1337
|
+
}
|
|
1338
|
+
function describeSiwe(text, ctx) {
|
|
1339
|
+
const p = parseLoose(text);
|
|
1340
|
+
if (!p) return null;
|
|
1341
|
+
const problems = siweProblems(p.message, ctx);
|
|
1342
|
+
if (p.missing.length === 0) return { message: p.message, problems };
|
|
1343
|
+
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 };
|
|
1344
|
+
}
|
|
1345
|
+
|
|
1346
|
+
// src/signing.ts
|
|
1347
|
+
var ADDRESS = /^0x[0-9a-fA-F]{40}$/;
|
|
1348
|
+
var HEX = /^0x([0-9a-fA-F]{2})*$/;
|
|
1349
|
+
var lc = (s) => s.toLowerCase();
|
|
1350
|
+
function messageText(value) {
|
|
1351
|
+
if (typeof value !== "string") return "";
|
|
1352
|
+
if (!HEX.test(value)) return value;
|
|
1353
|
+
try {
|
|
1354
|
+
const bytes = Uint8Array.from(value.slice(2).match(/../g) ?? [], (b) => Number.parseInt(b, 16));
|
|
1355
|
+
const s = new TextDecoder("utf-8", { fatal: true }).decode(bytes);
|
|
1356
|
+
return /^[\x09\x0a\x0d\x20-\x7e -]*$/.test(s) ? s : value;
|
|
1357
|
+
} catch {
|
|
1358
|
+
return value;
|
|
1359
|
+
}
|
|
1360
|
+
}
|
|
1361
|
+
function decodedMessage(value) {
|
|
1362
|
+
if (typeof value !== "string") return null;
|
|
1363
|
+
if (!HEX.test(value)) return value;
|
|
1364
|
+
try {
|
|
1365
|
+
return new TextDecoder("utf-8", { fatal: true }).decode(Uint8Array.from(value.slice(2).match(/../g) ?? [], (b) => Number.parseInt(b, 16)));
|
|
1366
|
+
} catch {
|
|
1367
|
+
return null;
|
|
1368
|
+
}
|
|
1369
|
+
}
|
|
1370
|
+
var HIDDEN_CONTROL = /[\x00-\x08\x0b\x0c\x0e-\x1f\x7f-\x9f]/;
|
|
1371
|
+
var err = (code, message) => ({ code, message });
|
|
1372
|
+
function classifySignRequest(input) {
|
|
1373
|
+
const { method, params } = input;
|
|
1374
|
+
const owned = new Set(input.accounts.map(lc));
|
|
1375
|
+
const ours = (a) => typeof a === "string" && ADDRESS.test(a) && owned.has(lc(a));
|
|
1376
|
+
const checksum = (a) => input.accounts.find((x) => lc(x) === lc(a)) ?? a;
|
|
1377
|
+
if (method === "personal_sign") {
|
|
1378
|
+
let [msg, addr] = params;
|
|
1379
|
+
if (ours(msg) && !ours(addr)) [msg, addr] = [addr, msg];
|
|
1380
|
+
if (!ours(addr)) return err(RELAY_ERRORS.unauthorized, "personal_sign for an address this wallet does not hold.");
|
|
1381
|
+
if (typeof msg !== "string") return err(RELAY_ERRORS.invalidParams, "personal_sign expects a message string.");
|
|
1382
|
+
const text = messageText(msg);
|
|
1383
|
+
const signed = decodedMessage(msg) ?? text;
|
|
1384
|
+
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;
|
|
1385
|
+
if (siwe && HIDDEN_CONTROL.test(signed)) return err(RELAY_ERRORS.invalidParams, "This sign-in message contains hidden control characters \u2014 refused.");
|
|
1386
|
+
return { kind: "sign-message", method, account: checksum(addr), text, raw: msg, ...siwe ? { siwe } : {} };
|
|
1387
|
+
}
|
|
1388
|
+
if (method === "eth_signTypedData_v4" || method === "eth_signTypedData_v3" || method === "eth_signTypedData") {
|
|
1389
|
+
const [addr, data] = params;
|
|
1390
|
+
if (!ours(addr)) return err(RELAY_ERRORS.unauthorized, "signTypedData for an address this wallet does not hold.");
|
|
1391
|
+
let parsed;
|
|
1392
|
+
try {
|
|
1393
|
+
parsed = typeof data === "string" ? JSON.parse(data) : data;
|
|
1394
|
+
} catch {
|
|
1395
|
+
return err(RELAY_ERRORS.invalidParams, "Typed data is not valid JSON.");
|
|
1396
|
+
}
|
|
1397
|
+
if (!parsed || typeof parsed !== "object" || typeof parsed.domain !== "object" || typeof parsed.types !== "object" || typeof parsed.message !== "object") {
|
|
1398
|
+
return err(RELAY_ERRORS.invalidParams, "Typed data needs domain, types and message.");
|
|
1399
|
+
}
|
|
1400
|
+
const cid = parsed.domain.chainId;
|
|
1401
|
+
if (cid !== void 0 && cid !== null && Number(cid) !== input.network.chainId) {
|
|
1402
|
+
return err(RELAY_ERRORS.chainDisconnected, `Typed data is for chain ${String(cid)}; this site is on ${input.network.name} (${input.network.chainId}).`);
|
|
1403
|
+
}
|
|
1404
|
+
return { kind: "sign-typed-data", method, account: checksum(addr), text: JSON.stringify(parsed, bigintSafe, 2), typedData: parsed };
|
|
1405
|
+
}
|
|
1406
|
+
if (method === "eth_sendTransaction") {
|
|
1407
|
+
const tx = params[0] ?? {};
|
|
1408
|
+
if (!ours(tx.from)) return err(RELAY_ERRORS.unauthorized, "Transaction from an address this wallet does not hold.");
|
|
1409
|
+
if (Array.isArray(tx.authorizationList) && tx.authorizationList.length > 0 || tx.type === "0x4" || tx.type === 4) return err(RELAY_ERRORS.unsupportedMethod, "EIP-7702 delegations (authorizationList / type 4) are never signed for a site by this wallet.");
|
|
1410
|
+
if (typeof tx.to !== "string" || !ADDRESS.test(tx.to)) return err(RELAY_ERRORS.invalidParams, "Contract deployment is not supported (a transaction needs a valid `to`).");
|
|
1411
|
+
if (tx.chainId !== void 0 && tx.chainId !== null && Number(tx.chainId) !== input.network.chainId) return err(RELAY_ERRORS.chainDisconnected, "Transaction chainId differs from the selected chain.");
|
|
1412
|
+
let value;
|
|
1413
|
+
try {
|
|
1414
|
+
value = BigInt(tx.value ?? 0).toString();
|
|
1415
|
+
} catch {
|
|
1416
|
+
return err(RELAY_ERRORS.invalidParams, "Bad transaction value.");
|
|
1417
|
+
}
|
|
1418
|
+
const data = typeof tx.data === "string" && tx.data !== "0x" ? tx.data : null;
|
|
1419
|
+
if (data !== null && !HEX.test(data)) return err(RELAY_ERRORS.invalidParams, "Transaction data is not hex.");
|
|
1420
|
+
const gasRaw = tx.gas ?? tx.gasLimit;
|
|
1421
|
+
const gas = typeof gasRaw === "string" && /^0x[0-9a-fA-F]+$/.test(gasRaw) ? gasRaw : null;
|
|
1422
|
+
return { kind: "send-transaction", method, account: checksum(tx.from), tx: { to: tx.to, value, data, gas } };
|
|
1423
|
+
}
|
|
1424
|
+
return err(RELAY_ERRORS.unsupportedMethod, `This wallet does not support ${method}.`);
|
|
1425
|
+
}
|
|
1426
|
+
var bigintSafe = (_k, v) => typeof v === "bigint" ? v.toString() : v;
|
|
1427
|
+
var hex = (n) => `0x${n.toString(16)}`;
|
|
1428
|
+
var big = (v) => BigInt(v);
|
|
1429
|
+
var MIN_TIP_WEI = 100000000n;
|
|
1430
|
+
async function prepareTransaction(deps, network, from, tx) {
|
|
1431
|
+
const call = { from, to: tx.to, value: hex(BigInt(tx.value)), ...tx.data ? { data: tx.data } : {} };
|
|
1432
|
+
const [nonceHex, block, gasHex] = await Promise.all([
|
|
1433
|
+
deps.rpc(network, "eth_getTransactionCount", [from, "pending"]),
|
|
1434
|
+
deps.rpc(network, "eth_getBlockByNumber", ["latest", false]),
|
|
1435
|
+
tx.gas ? Promise.resolve(tx.gas) : deps.rpc(network, "eth_estimateGas", [call])
|
|
1436
|
+
]);
|
|
1437
|
+
const estimated = big(gasHex);
|
|
1438
|
+
const gasLimit = tx.gas ? estimated : (estimated * 120n + 99n) / 100n;
|
|
1439
|
+
const base = block?.baseFeePerGas != null ? big(block.baseFeePerGas) : null;
|
|
1440
|
+
if (base === null) {
|
|
1441
|
+
const gasPrice = big(await deps.rpc(network, "eth_gasPrice", []));
|
|
1442
|
+
return { chainId: network.chainId, nonce: Number(big(nonceHex)), gasLimit: hex(gasLimit), gasPrice: hex(gasPrice), maxFee: (gasLimit * gasPrice).toString() };
|
|
1443
|
+
}
|
|
1444
|
+
const floor = deps.tipFloor?.(network.chainId) ?? MIN_TIP_WEI;
|
|
1445
|
+
const nodeTip = await deps.rpc(network, "eth_maxPriorityFeePerGas", []).then(big).catch(() => 0n);
|
|
1446
|
+
const tip = nodeTip > floor ? nodeTip : floor;
|
|
1447
|
+
const maxFeePerGas = base * 2n + tip;
|
|
1448
|
+
return { chainId: network.chainId, nonce: Number(big(nonceHex)), gasLimit: hex(gasLimit), maxFeePerGas: hex(maxFeePerGas), maxPriorityFeePerGas: hex(tip), maxFee: (gasLimit * maxFeePerGas).toString() };
|
|
1449
|
+
}
|
|
1450
|
+
function createSignHandler(deps) {
|
|
1451
|
+
return async ({ origin, network, method, params, accounts }) => {
|
|
1452
|
+
const via = deps.approvals.viaFor(origin);
|
|
1453
|
+
const info = classifySignRequest({ network, method, params, accounts, origin, ...via ? { via } : {} });
|
|
1454
|
+
if ("code" in info) throw RouterError.from(info);
|
|
1455
|
+
const prepared = info.kind === "send-transaction" ? await prepareTransaction(deps, network, info.account, info.tx) : void 0;
|
|
1456
|
+
const preview = deps.preview ? await deps.preview({ network, request: info, accounts }).catch(() => null) : null;
|
|
1457
|
+
const payload = { request: info, network, ...prepared ? { prepared } : {}, preview };
|
|
1458
|
+
await deps.approvals.request({ origin, chain: network.caip2, kind: info.kind, payload });
|
|
1459
|
+
const handle = await deps.signerFor(info.account, network);
|
|
1460
|
+
try {
|
|
1461
|
+
switch (info.kind) {
|
|
1462
|
+
case "sign-message":
|
|
1463
|
+
return await handle.signer.signMessage(HEX.test(info.raw) ? Uint8Array.from(info.raw.slice(2).match(/../g) ?? [], (b) => Number.parseInt(b, 16)) : info.raw);
|
|
1464
|
+
case "sign-typed-data":
|
|
1465
|
+
return await handle.signer.signTypedData(info.typedData);
|
|
1466
|
+
case "send-transaction": {
|
|
1467
|
+
const p = prepared;
|
|
1468
|
+
const raw = await handle.signer.signTransaction({
|
|
1469
|
+
chainId: p.chainId,
|
|
1470
|
+
nonce: p.nonce,
|
|
1471
|
+
to: info.tx.to,
|
|
1472
|
+
value: BigInt(info.tx.value),
|
|
1473
|
+
data: info.tx.data ?? "0x",
|
|
1474
|
+
gasLimit: BigInt(p.gasLimit),
|
|
1475
|
+
...p.gasPrice !== void 0 ? { gasPrice: BigInt(p.gasPrice) } : { maxFeePerGas: BigInt(p.maxFeePerGas), maxPriorityFeePerGas: BigInt(p.maxPriorityFeePerGas) }
|
|
1476
|
+
});
|
|
1477
|
+
return await deps.rpc(network, "eth_sendRawTransaction", [raw]);
|
|
1478
|
+
}
|
|
1479
|
+
}
|
|
1480
|
+
} finally {
|
|
1481
|
+
handle.dispose();
|
|
1482
|
+
}
|
|
1483
|
+
};
|
|
1484
|
+
}
|
|
1485
|
+
|
|
1486
|
+
// src/gate.ts
|
|
1487
|
+
var ReviewGate = class {
|
|
1488
|
+
constructor(now = Date.now) {
|
|
1489
|
+
this.now = now;
|
|
1490
|
+
}
|
|
1491
|
+
now;
|
|
1492
|
+
approved = [];
|
|
1493
|
+
/** Each matcher passes ONE operation, within `ttlMs` (default 10 min). */
|
|
1494
|
+
preApprove(matchers, ttlMs = 10 * 6e4) {
|
|
1495
|
+
const until = this.now() + ttlMs;
|
|
1496
|
+
this.approved.push(...matchers.map((match) => ({ match, until })));
|
|
1497
|
+
}
|
|
1498
|
+
/** True (and the matcher is spent) when a pre-approval covers `op`. */
|
|
1499
|
+
consume(op) {
|
|
1500
|
+
const now = this.now();
|
|
1501
|
+
this.approved = this.approved.filter((a) => a.until > now);
|
|
1502
|
+
const i = this.approved.findIndex((a) => a.match(op));
|
|
1503
|
+
if (i < 0) return false;
|
|
1504
|
+
this.approved.splice(i, 1);
|
|
1505
|
+
return true;
|
|
1506
|
+
}
|
|
1507
|
+
get pending() {
|
|
1508
|
+
return this.approved.length;
|
|
1509
|
+
}
|
|
1510
|
+
clear() {
|
|
1511
|
+
this.approved = [];
|
|
1512
|
+
}
|
|
1513
|
+
};
|
|
1514
|
+
var lc2 = (s) => s.toLowerCase();
|
|
1515
|
+
function evmCallMatcher(expected) {
|
|
1516
|
+
const data = expected.data && expected.data !== "0x" ? lc2(expected.data) : void 0;
|
|
1517
|
+
return (op) => op.kind === "evm-tx" && op.caip2 === expected.caip2 && lc2(op.to) === lc2(expected.to) && (op.data && op.data !== "0x" ? lc2(op.data) : void 0) === data && op.value === expected.value;
|
|
1518
|
+
}
|
|
1519
|
+
function chainTxMatcher(expected) {
|
|
1520
|
+
return (op) => op.kind === "chain-tx" && op.chain === expected.chain && op.to === expected.to && op.amount === expected.amount && (op.token ?? "") === (expected.token ?? "") && (op.memo ?? "") === (expected.memo ?? "");
|
|
1521
|
+
}
|
|
1522
|
+
|
|
1523
|
+
// src/swap.ts
|
|
1524
|
+
import { validQuote } from "@anywalletkit/protocol-swap2-core";
|
|
1525
|
+
async function quoteRoutes(providers, params, options = {}) {
|
|
1526
|
+
const timeoutMs = options.timeoutMs ?? 2e4;
|
|
1527
|
+
const from = { chain: params.sourceChain, token: params.sourceToken };
|
|
1528
|
+
const to = { chain: params.destChain, token: params.destToken };
|
|
1529
|
+
const refusals = {};
|
|
1530
|
+
const quotes = {};
|
|
1531
|
+
const candidates = [];
|
|
1532
|
+
for (const p of providers) {
|
|
1533
|
+
let enabled = false;
|
|
1534
|
+
try {
|
|
1535
|
+
enabled = p.enabled();
|
|
1536
|
+
} catch {
|
|
1537
|
+
}
|
|
1538
|
+
if (!enabled) continue;
|
|
1539
|
+
if (params.sourceChain === params.destChain && !p.supportsSameChain) {
|
|
1540
|
+
refusals[p.name] = "does not do same-chain swaps";
|
|
1541
|
+
continue;
|
|
1542
|
+
}
|
|
1543
|
+
let serves = false;
|
|
1544
|
+
try {
|
|
1545
|
+
serves = p.supportsRoute(from, to);
|
|
1546
|
+
} catch (e) {
|
|
1547
|
+
refusals[p.name] = e instanceof Error ? e.message : String(e);
|
|
1548
|
+
continue;
|
|
1549
|
+
}
|
|
1550
|
+
if (!serves) {
|
|
1551
|
+
refusals[p.name] = "does not serve this pair";
|
|
1552
|
+
continue;
|
|
1553
|
+
}
|
|
1554
|
+
candidates.push(p);
|
|
1555
|
+
}
|
|
1556
|
+
const pending = new Set(candidates.map((p) => p.name));
|
|
1557
|
+
const view = () => ({ routes: rank(Object.values(quotes)), pending: [...pending], refusals: { ...refusals }, quotes: { ...quotes } });
|
|
1558
|
+
const emit = () => options.onUpdate?.(view());
|
|
1559
|
+
emit();
|
|
1560
|
+
await Promise.all(
|
|
1561
|
+
candidates.map(async (p) => {
|
|
1562
|
+
const ctrl = new AbortController();
|
|
1563
|
+
const timer = setTimeout(() => ctrl.abort(new Error(`no answer within ${Math.round(timeoutMs / 1e3)} s`)), timeoutMs);
|
|
1564
|
+
const onAbort = () => ctrl.abort(options.signal?.reason);
|
|
1565
|
+
options.signal?.addEventListener("abort", onAbort, { once: true });
|
|
1566
|
+
let refusal;
|
|
1567
|
+
try {
|
|
1568
|
+
const aborted = new Promise((_, reject) => ctrl.signal.addEventListener("abort", () => reject(ctrl.signal.reason instanceof Error ? ctrl.signal.reason : new Error(String(ctrl.signal.reason ?? "cancelled"))), { once: true }));
|
|
1569
|
+
const q = await Promise.race([p.getQuote({ ...params, dry: true, onRefusal: (r) => {
|
|
1570
|
+
refusal = r;
|
|
1571
|
+
} }, ctrl.signal), aborted]);
|
|
1572
|
+
if (q && validQuote(q, { ...params, dry: true }, p.name)) quotes[p.name] = q;
|
|
1573
|
+
else refusals[p.name] = refusal ?? (q ? "returned an invalid quote" : "no route for this pair right now");
|
|
1574
|
+
} catch (e) {
|
|
1575
|
+
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);
|
|
1576
|
+
} finally {
|
|
1577
|
+
clearTimeout(timer);
|
|
1578
|
+
options.signal?.removeEventListener("abort", onAbort);
|
|
1579
|
+
pending.delete(p.name);
|
|
1580
|
+
emit();
|
|
1581
|
+
}
|
|
1582
|
+
})
|
|
1583
|
+
);
|
|
1584
|
+
return view();
|
|
1585
|
+
}
|
|
1586
|
+
function rank(quotes) {
|
|
1587
|
+
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);
|
|
1588
|
+
const fastest = sorted.length ? sorted.reduce((f, q) => q.etaSeconds < f.etaSeconds ? q : f) : null;
|
|
1589
|
+
return sorted.map((q, i) => {
|
|
1590
|
+
const poolFee = q.providerData?.poolFee;
|
|
1591
|
+
const feeNote = typeof poolFee === "string" && poolFee && q.feeUsd === 0 ? poolFee === "pools" ? "pool fees in price" : `pool ${poolFee} in price` : void 0;
|
|
1592
|
+
const tags = [];
|
|
1593
|
+
if (i === 0) tags.push("best");
|
|
1594
|
+
if (fastest && q.provider === fastest.provider) tags.push("fastest");
|
|
1595
|
+
return { provider: q.provider, destAmount: q.destAmount, minDestAmount: q.minDestAmount, feeUsd: q.feeUsd, etaSeconds: q.etaSeconds, validUntilMs: q.validUntilMs, tags, ...feeNote ? { feeNote } : {} };
|
|
1596
|
+
});
|
|
1597
|
+
}
|
|
1598
|
+
var FLOOR_TOLERANCE_BPS = 50n;
|
|
1599
|
+
function executionFloor(minDestAmount) {
|
|
1600
|
+
const floor = minDestAmount * (10000n - FLOOR_TOLERANCE_BPS) / 10000n;
|
|
1601
|
+
return floor > 0n ? floor : minDestAmount;
|
|
1602
|
+
}
|
|
1603
|
+
var APPROVE_SELECTOR = "0x095ea7b3";
|
|
1604
|
+
var TRANSFER_SELECTOR = "0xa9059cbb";
|
|
1605
|
+
var pad = (hexNoPrefix) => hexNoPrefix.toLowerCase().padStart(64, "0");
|
|
1606
|
+
var encodeApproveCalldata = (spender, amount) => `${APPROVE_SELECTOR}${pad(spender.slice(2))}${pad(amount.toString(16))}`;
|
|
1607
|
+
var lc3 = (s) => s.toLowerCase();
|
|
1608
|
+
function depositOf(s, approvals) {
|
|
1609
|
+
if (approvals > 0) return void 0;
|
|
1610
|
+
if (!s.data || s.data === "0x") return s.to;
|
|
1611
|
+
if (!s.data.startsWith(TRANSFER_SELECTOR) || s.data.length < 10 + 64) return void 0;
|
|
1612
|
+
return `0x${s.data.slice(10 + 24, 10 + 64)}`;
|
|
1613
|
+
}
|
|
1614
|
+
function swapApprovalPayload(plan, meta) {
|
|
1615
|
+
const isOwn = meta.own.some((a) => lc3(a) === lc3(plan.recipient));
|
|
1616
|
+
const checks = [{ status: "ok", label: `Built by the ${meta.providerLabel} module` }];
|
|
1617
|
+
const s = plan.source;
|
|
1618
|
+
let source;
|
|
1619
|
+
if (s.kind === "call") {
|
|
1620
|
+
const deposit = depositOf(s, plan.approvals.length);
|
|
1621
|
+
source = { kind: "call", to: s.to, ...s.data ? { data: s.data } : {}, value: s.value.toString(), ...deposit ? { deposit } : {} };
|
|
1622
|
+
checks.push(deposit ? { status: "ok", label: `Deposit address from the ${meta.providerLabel} quote` } : { status: "ok", label: `Calls the ${meta.providerLabel} contract` });
|
|
1623
|
+
} else if (s.kind === "token-transfer") {
|
|
1624
|
+
source = { kind: "token-transfer", token: s.token, recipient: s.recipient, amount: s.amount.toString(), ...s.memo ? { memo: s.memo } : {} };
|
|
1625
|
+
checks.push({ status: "ok", label: `Deposit address from the ${meta.providerLabel} quote` });
|
|
1626
|
+
} else if (s.kind === "serialized") {
|
|
1627
|
+
source = { kind: "serialized" };
|
|
1628
|
+
checks.push({ status: "ok", label: "Balance changes are proven before signing (SDK guard)" });
|
|
1629
|
+
} else {
|
|
1630
|
+
source = { kind: "typed-order" };
|
|
1631
|
+
checks.push({ status: "ok", label: "Signed order \u2014 no transaction is sent from your wallet" });
|
|
1632
|
+
}
|
|
1633
|
+
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" });
|
|
1634
|
+
if (plan.approvals.length > 1) checks.push({ status: "ok", label: `${plan.approvals.length} approvals (reset to 0 first)` });
|
|
1635
|
+
checks.push(isOwn ? { status: "ok", label: "Funds arrive at your address" } : { status: "warn", label: `Funds arrive at ${plan.recipient.slice(0, 6)}\u2026${plan.recipient.slice(-4)}, not your address` });
|
|
1636
|
+
return {
|
|
1637
|
+
provider: plan.provider,
|
|
1638
|
+
providerLabel: meta.providerLabel,
|
|
1639
|
+
fromChain: plan.fromChain,
|
|
1640
|
+
toChain: plan.destChain,
|
|
1641
|
+
fromChainName: meta.fromChainName,
|
|
1642
|
+
toChainName: meta.toChainName,
|
|
1643
|
+
send: { symbol: meta.fromSymbol, decimals: meta.fromDecimals, amount: plan.sourceAmount.toString() },
|
|
1644
|
+
receive: { symbol: meta.toSymbol, decimals: meta.toDecimals, amount: plan.destAmount.toString(), min: plan.minDestAmount.toString() },
|
|
1645
|
+
recipient: { address: plan.recipient, isOwn },
|
|
1646
|
+
refundAddress: plan.refundAddress,
|
|
1647
|
+
feeUsd: plan.feeUsd,
|
|
1648
|
+
etaSeconds: plan.etaSeconds,
|
|
1649
|
+
validUntilMs: plan.validUntilMs,
|
|
1650
|
+
approvals: plan.approvals.map((a) => ({ token: a.token, spender: a.spender, amount: a.amount.toString() })),
|
|
1651
|
+
source,
|
|
1652
|
+
checks
|
|
1653
|
+
};
|
|
1654
|
+
}
|
|
1655
|
+
function planMatchers(plan) {
|
|
1656
|
+
const m = plan.approvals.map((a) => evmCallMatcher({ caip2: plan.fromChain, to: a.token, data: encodeApproveCalldata(a.spender, a.amount), value: 0n }));
|
|
1657
|
+
const s = plan.source;
|
|
1658
|
+
if (s.kind === "call") {
|
|
1659
|
+
if (plan.fromChain.startsWith("eip155:")) m.push(evmCallMatcher({ caip2: plan.fromChain, to: s.to, data: s.data, value: s.value }));
|
|
1660
|
+
else if (plan.fromChain.startsWith("solana:")) m.push((op) => op.kind === "solana-tx" && op.to === s.to && op.value === s.value);
|
|
1661
|
+
else m.push(chainTxMatcher({ chain: plan.fromChain, to: s.to, amount: s.value, memo: s.memo }));
|
|
1662
|
+
} else if (s.kind === "token-transfer") {
|
|
1663
|
+
if (plan.fromChain.startsWith("eip155:")) m.push(evmCallMatcher({ caip2: plan.fromChain, to: s.token, data: `${TRANSFER_SELECTOR}${pad(s.recipient.slice(2))}${pad(s.amount.toString(16))}`, value: 0n }));
|
|
1664
|
+
else if (plan.fromChain.startsWith("solana:")) m.push((op) => op.kind === "solana-transfer" && op.to === s.recipient && op.amount === s.amount && (op.token ?? "") === s.token);
|
|
1665
|
+
else m.push(chainTxMatcher({ chain: plan.fromChain, to: s.recipient, amount: s.amount, token: s.token, memo: s.memo }));
|
|
1666
|
+
} else if (s.kind === "serialized") {
|
|
1667
|
+
m.push((op) => op.kind === "solana-serialized" && op.serializedTx === s.serializedTx);
|
|
1668
|
+
} else {
|
|
1669
|
+
m.push((op) => op.kind === "evm-typed-data" && op.caip2 === plan.fromChain);
|
|
1670
|
+
}
|
|
1671
|
+
return m;
|
|
1672
|
+
}
|
|
1673
|
+
function reviewSwapPlan(approvals, gate, meta) {
|
|
1674
|
+
return async (plan) => {
|
|
1675
|
+
const payload = swapApprovalPayload(plan, meta);
|
|
1676
|
+
await approvals.request({ origin: meta.origin, chain: plan.fromChain, kind: "swap", payload });
|
|
1677
|
+
gate.clear();
|
|
1678
|
+
gate.preApprove(planMatchers(plan), Math.max(6e4, plan.validUntilMs - Date.now() + 5 * 6e4));
|
|
1679
|
+
};
|
|
1680
|
+
}
|
|
1681
|
+
|
|
1682
|
+
// src/swaps.ts
|
|
1683
|
+
import { SWAP2_ID_PREFIX } from "@anywalletkit/protocol-swap2-core";
|
|
1684
|
+
var ACTIVE_STATUSES = /* @__PURE__ */ new Set(["pending", "action-required", "refund-pending", "partial", "claiming"]);
|
|
1685
|
+
function parseSwapId(id) {
|
|
1686
|
+
if (!id.startsWith(`${SWAP2_ID_PREFIX}.`)) return null;
|
|
1687
|
+
const parts = id.slice(SWAP2_ID_PREFIX.length + 1).split(".");
|
|
1688
|
+
if (parts.length < 2 || parts.length > 3 || !parts[0] || !parts[1]) return null;
|
|
1689
|
+
const provider = decodeURIComponent(parts[0]);
|
|
1690
|
+
const intentId = decodeURIComponent(parts[1]);
|
|
1691
|
+
const sourceTxHash = parts[2] ? decodeURIComponent(parts[2]) : void 0;
|
|
1692
|
+
return sourceTxHash ? { provider, intentId, sourceTxHash } : { provider, intentId };
|
|
1693
|
+
}
|
|
1694
|
+
var SwapsStore = class {
|
|
1695
|
+
constructor(storage) {
|
|
1696
|
+
this.storage = storage;
|
|
1697
|
+
}
|
|
1698
|
+
storage;
|
|
1699
|
+
_records = null;
|
|
1700
|
+
_listeners = /* @__PURE__ */ new Set();
|
|
1701
|
+
async list() {
|
|
1702
|
+
this._records ??= await this.storage.get() ?? [];
|
|
1703
|
+
return [...this._records].sort((a, b) => b.createdAt - a.createdAt);
|
|
1704
|
+
}
|
|
1705
|
+
async get(id) {
|
|
1706
|
+
return (await this.list()).find((r) => r.id === id) ?? null;
|
|
1707
|
+
}
|
|
1708
|
+
async active() {
|
|
1709
|
+
return (await this.list()).filter((r) => ACTIVE_STATUSES.has(r.status));
|
|
1710
|
+
}
|
|
1711
|
+
async add(record) {
|
|
1712
|
+
const all = (await this.list()).filter((r) => r.id !== record.id);
|
|
1713
|
+
await this.write([...all, record]);
|
|
1714
|
+
}
|
|
1715
|
+
async update(id, patch) {
|
|
1716
|
+
const all = await this.list();
|
|
1717
|
+
const i = all.findIndex((r) => r.id === id);
|
|
1718
|
+
if (i < 0) return null;
|
|
1719
|
+
const next = { ...all[i], ...patch, updatedAt: patch.updatedAt ?? Date.now() };
|
|
1720
|
+
all[i] = next;
|
|
1721
|
+
await this.write(all);
|
|
1722
|
+
return next;
|
|
1723
|
+
}
|
|
1724
|
+
async remove(id) {
|
|
1725
|
+
await this.write((await this.list()).filter((r) => r.id !== id));
|
|
1726
|
+
}
|
|
1727
|
+
onChange(listener) {
|
|
1728
|
+
this._listeners.add(listener);
|
|
1729
|
+
return () => this._listeners.delete(listener);
|
|
1730
|
+
}
|
|
1731
|
+
async write(records) {
|
|
1732
|
+
await this.storage.set(records);
|
|
1733
|
+
this._records = records;
|
|
1734
|
+
for (const l of this._listeners) l();
|
|
1735
|
+
}
|
|
1736
|
+
};
|
|
1737
|
+
var SwapTracker = class {
|
|
1738
|
+
constructor(deps) {
|
|
1739
|
+
this.deps = deps;
|
|
1740
|
+
this.now = deps.now ?? Date.now;
|
|
1741
|
+
}
|
|
1742
|
+
deps;
|
|
1743
|
+
now;
|
|
1744
|
+
ticking = null;
|
|
1745
|
+
/** Record a just-executed swap and make sure the alarm runs. */
|
|
1746
|
+
async track(input) {
|
|
1747
|
+
const t = this.now();
|
|
1748
|
+
const record = { ...input, status: input.status ?? "pending", destTxHash: null, createdAt: t, updatedAt: t, checks: 0 };
|
|
1749
|
+
await this.deps.store.add(record);
|
|
1750
|
+
await this.deps.alarm.start();
|
|
1751
|
+
return record;
|
|
1752
|
+
}
|
|
1753
|
+
/** After the user's claim transaction went out: keep polling until the provider sees the mint. */
|
|
1754
|
+
async claimed(id, destTxHash) {
|
|
1755
|
+
await this.deps.store.update(id, { status: "claiming", destTxHash, claim: void 0 });
|
|
1756
|
+
await this.deps.alarm.start();
|
|
1757
|
+
}
|
|
1758
|
+
/** One alarm tick: check every active record once; stop the alarm when nothing is active. Concurrent ticks coalesce. */
|
|
1759
|
+
tick() {
|
|
1760
|
+
this.ticking ??= this.run().finally(() => {
|
|
1761
|
+
this.ticking = null;
|
|
1762
|
+
});
|
|
1763
|
+
return this.ticking;
|
|
1764
|
+
}
|
|
1765
|
+
async run() {
|
|
1766
|
+
const active = await this.deps.store.active();
|
|
1767
|
+
if (active.length === 0) {
|
|
1768
|
+
await this.deps.alarm.stop();
|
|
1769
|
+
return;
|
|
1770
|
+
}
|
|
1771
|
+
const giveUp = this.deps.giveUpAfterMs ?? 48 * 60 * 6e4;
|
|
1772
|
+
for (const record of active) {
|
|
1773
|
+
const patch = { checks: record.checks + 1, updatedAt: this.now() };
|
|
1774
|
+
try {
|
|
1775
|
+
const s = await this.deps.status(record);
|
|
1776
|
+
patch.lastError = void 0;
|
|
1777
|
+
if (s.depositReceived !== void 0) patch.depositReceived = s.depositReceived;
|
|
1778
|
+
if (s.destTxHash) patch.destTxHash = s.destTxHash;
|
|
1779
|
+
if (s.claim) patch.claim = s.claim;
|
|
1780
|
+
patch.status = record.status === "claiming" && (s.state === "action-required" || s.state === "pending") ? "claiming" : s.state;
|
|
1781
|
+
} catch (e) {
|
|
1782
|
+
patch.lastError = e instanceof Error ? e.message : String(e);
|
|
1783
|
+
if (this.now() - record.createdAt > giveUp) patch.status = "expired";
|
|
1784
|
+
}
|
|
1785
|
+
await this.deps.store.update(record.id, patch);
|
|
1786
|
+
}
|
|
1787
|
+
if ((await this.deps.store.active()).length === 0) await this.deps.alarm.stop();
|
|
1788
|
+
}
|
|
1789
|
+
};
|
|
1790
|
+
|
|
1791
|
+
// src/earn.ts
|
|
1792
|
+
var APPROVE_SELECTOR2 = "0x095ea7b3";
|
|
1793
|
+
function earnPlanMatchers(network, plan) {
|
|
1794
|
+
return [...plan.approvals, plan.tx].map((t) => evmCallMatcher({ caip2: network.caip2, to: t.to, data: t.data, value: BigInt(t.value) }));
|
|
1795
|
+
}
|
|
1796
|
+
function earnApprovalPayload(plan, meta) {
|
|
1797
|
+
const checks = [{ status: "ok", label: `Built by the ${meta.module} module` }, ...meta.checks ?? []];
|
|
1798
|
+
for (const a of plan.approvals) {
|
|
1799
|
+
if (!a.data?.startsWith(APPROVE_SELECTOR2)) checks.push({ status: "warn", label: `Unexpected pre-call to ${a.to.slice(0, 8)}\u2026` });
|
|
1800
|
+
}
|
|
1801
|
+
if (plan.approvals.length > 1) checks.push({ status: "ok", label: `${plan.approvals.length} approvals (reset to 0 first)` });
|
|
1802
|
+
return { module: meta.module, action: meta.action, network: meta.network, facts: meta.facts, approvals: plan.approvals, tx: plan.tx, checks, preview: meta.preview ?? null };
|
|
1803
|
+
}
|
|
1804
|
+
async function reviewEarnPlan(approvals, gate, meta, plan) {
|
|
1805
|
+
await approvals.request({ origin: meta.origin, chain: meta.network.caip2, kind: "earn", payload: earnApprovalPayload(plan, meta) });
|
|
1806
|
+
gate.preApprove(earnPlanMatchers(meta.network, plan), 10 * 6e4);
|
|
1807
|
+
}
|
|
1808
|
+
|
|
1809
|
+
// src/tonconnect.ts
|
|
1810
|
+
import { sha256 } from "@noble/hashes/sha2.js";
|
|
1811
|
+
var TONCONNECT_ERRORS = { unknown: 0, badRequest: 1, manifestNotFound: 2, manifestContent: 3, unknownApp: 100, userDeclined: 300, methodNotSupported: 400 };
|
|
1812
|
+
var TonConnectError = class extends Error {
|
|
1813
|
+
constructor(code, message) {
|
|
1814
|
+
super(message);
|
|
1815
|
+
this.code = code;
|
|
1816
|
+
this.name = "TonConnectError";
|
|
1817
|
+
}
|
|
1818
|
+
code;
|
|
1819
|
+
};
|
|
1820
|
+
function parseConnectRequest(message) {
|
|
1821
|
+
const m = message;
|
|
1822
|
+
if (!m || typeof m.manifestUrl !== "string" || !/^https:\/\//.test(m.manifestUrl)) throw new TonConnectError(TONCONNECT_ERRORS.badRequest, "ConnectRequest needs an https manifestUrl.");
|
|
1823
|
+
const items = Array.isArray(m.items) ? m.items : [];
|
|
1824
|
+
if (!items.some((i) => i?.name === "ton_addr")) throw new TonConnectError(TONCONNECT_ERRORS.badRequest, "ConnectRequest must ask for ton_addr.");
|
|
1825
|
+
const proof = items.find((i) => i?.name === "ton_proof");
|
|
1826
|
+
if (proof && typeof proof.payload !== "string") throw new TonConnectError(TONCONNECT_ERRORS.badRequest, "ton_proof needs a string payload.");
|
|
1827
|
+
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)}.`);
|
|
1828
|
+
return { manifestUrl: m.manifestUrl, ...proof ? { proofPayload: proof.payload } : {} };
|
|
1829
|
+
}
|
|
1830
|
+
var TONCONNECT_MAX_MESSAGES = 4;
|
|
1831
|
+
function parseSendTransaction(param, options = {}) {
|
|
1832
|
+
let o;
|
|
1833
|
+
try {
|
|
1834
|
+
o = typeof param === "string" ? JSON.parse(param) : param;
|
|
1835
|
+
} catch {
|
|
1836
|
+
throw new TonConnectError(TONCONNECT_ERRORS.badRequest, "sendTransaction params[0] is not valid JSON.");
|
|
1837
|
+
}
|
|
1838
|
+
if (!o || typeof o !== "object") throw new TonConnectError(TONCONNECT_ERRORS.badRequest, "sendTransaction needs a request object.");
|
|
1839
|
+
const network = options.network ?? "-239";
|
|
1840
|
+
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)}.`);
|
|
1841
|
+
const raw = Array.isArray(o.messages) ? o.messages : [];
|
|
1842
|
+
if (raw.length === 0) throw new TonConnectError(TONCONNECT_ERRORS.badRequest, "sendTransaction has no messages.");
|
|
1843
|
+
if (raw.length > (options.maxMessages ?? TONCONNECT_MAX_MESSAGES)) throw new TonConnectError(TONCONNECT_ERRORS.badRequest, `At most ${options.maxMessages ?? TONCONNECT_MAX_MESSAGES} messages per transaction.`);
|
|
1844
|
+
const messages = raw.map((m, i) => {
|
|
1845
|
+
if (typeof m.address !== "string" || !m.address) throw new TonConnectError(TONCONNECT_ERRORS.badRequest, `Message ${i + 1} has no address.`);
|
|
1846
|
+
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.`);
|
|
1847
|
+
if (m.payload !== void 0 && typeof m.payload !== "string") throw new TonConnectError(TONCONNECT_ERRORS.badRequest, `Message ${i + 1} payload must be a base64 BoC.`);
|
|
1848
|
+
if (m.stateInit !== void 0 && typeof m.stateInit !== "string") throw new TonConnectError(TONCONNECT_ERRORS.badRequest, `Message ${i + 1} stateInit must be a base64 BoC.`);
|
|
1849
|
+
return { address: m.address, amount: m.amount, ...m.payload ? { payload: m.payload } : {}, ...m.stateInit ? { stateInit: m.stateInit } : {} };
|
|
1850
|
+
});
|
|
1851
|
+
const validUntil = typeof o.valid_until === "number" ? o.valid_until : typeof o.validUntil === "number" ? o.validUntil : void 0;
|
|
1852
|
+
if (validUntil !== void 0 && validUntil * 1e3 < Date.now()) throw new TonConnectError(TONCONNECT_ERRORS.badRequest, "The request has expired (valid_until is in the past).");
|
|
1853
|
+
return { messages, ...validUntil !== void 0 ? { validUntil } : {}, ...o.network !== void 0 ? { network: String(o.network) } : {}, ...typeof o.from === "string" ? { from: o.from } : {} };
|
|
1854
|
+
}
|
|
1855
|
+
function parseSignData(param) {
|
|
1856
|
+
let o;
|
|
1857
|
+
try {
|
|
1858
|
+
o = typeof param === "string" ? JSON.parse(param) : param;
|
|
1859
|
+
} catch {
|
|
1860
|
+
throw new TonConnectError(TONCONNECT_ERRORS.badRequest, "signData params[0] is not valid JSON.");
|
|
1861
|
+
}
|
|
1862
|
+
if (o?.type === "text" && typeof o.text === "string") return { type: "text", text: o.text };
|
|
1863
|
+
if (o?.type === "binary" && typeof o.bytes === "string") return { type: "binary", bytes: o.bytes };
|
|
1864
|
+
if (o?.type === "cell") throw new TonConnectError(TONCONNECT_ERRORS.methodNotSupported, "signData with a cell payload is not supported by this wallet.");
|
|
1865
|
+
throw new TonConnectError(TONCONNECT_ERRORS.badRequest, 'signData payload must be { type: "text", text } or { type: "binary", bytes }.');
|
|
1866
|
+
}
|
|
1867
|
+
var utf82 = (s) => new TextEncoder().encode(s);
|
|
1868
|
+
var int32BE = (n) => {
|
|
1869
|
+
const b = new Uint8Array(4);
|
|
1870
|
+
new DataView(b.buffer).setInt32(0, n, false);
|
|
1871
|
+
return b;
|
|
1872
|
+
};
|
|
1873
|
+
var uint32LE = (n) => {
|
|
1874
|
+
const b = new Uint8Array(4);
|
|
1875
|
+
new DataView(b.buffer).setUint32(0, n, true);
|
|
1876
|
+
return b;
|
|
1877
|
+
};
|
|
1878
|
+
var uint32BE = (n) => {
|
|
1879
|
+
const b = new Uint8Array(4);
|
|
1880
|
+
new DataView(b.buffer).setUint32(0, n, false);
|
|
1881
|
+
return b;
|
|
1882
|
+
};
|
|
1883
|
+
var uint64LE = (n) => {
|
|
1884
|
+
const b = new Uint8Array(8);
|
|
1885
|
+
new DataView(b.buffer).setBigUint64(0, BigInt(n), true);
|
|
1886
|
+
return b;
|
|
1887
|
+
};
|
|
1888
|
+
var uint64BE = (n) => {
|
|
1889
|
+
const b = new Uint8Array(8);
|
|
1890
|
+
new DataView(b.buffer).setBigUint64(0, BigInt(n), false);
|
|
1891
|
+
return b;
|
|
1892
|
+
};
|
|
1893
|
+
var concat = (...parts) => {
|
|
1894
|
+
const out = new Uint8Array(parts.reduce((n, p) => n + p.length, 0));
|
|
1895
|
+
let o = 0;
|
|
1896
|
+
for (const p of parts) {
|
|
1897
|
+
out.set(p, o);
|
|
1898
|
+
o += p.length;
|
|
1899
|
+
}
|
|
1900
|
+
return out;
|
|
1901
|
+
};
|
|
1902
|
+
function tonProofMessage(input) {
|
|
1903
|
+
if (input.address.hash.length !== 32) throw new TonConnectError(TONCONNECT_ERRORS.unknown, "address hash must be 32 bytes");
|
|
1904
|
+
const domain = utf82(input.domain);
|
|
1905
|
+
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));
|
|
1906
|
+
return sha256(concat(new Uint8Array([255, 255]), utf82("ton-connect"), sha256(inner)));
|
|
1907
|
+
}
|
|
1908
|
+
function tonSignDataMessage(input) {
|
|
1909
|
+
if (input.address.hash.length !== 32) throw new TonConnectError(TONCONNECT_ERRORS.unknown, "address hash must be 32 bytes");
|
|
1910
|
+
const domain = utf82(input.domain);
|
|
1911
|
+
const payload = input.payload.type === "text" ? utf82(input.payload.text) : Uint8Array.from(atob(input.payload.bytes), (c) => c.charCodeAt(0));
|
|
1912
|
+
const prefix = utf82(input.payload.type === "text" ? "txt" : "bin");
|
|
1913
|
+
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));
|
|
1914
|
+
}
|
|
1915
|
+
function parseRawAddress(raw) {
|
|
1916
|
+
const m = /^(-?\d+):([0-9a-fA-F]{64})$/.exec(raw);
|
|
1917
|
+
if (!m) throw new TonConnectError(TONCONNECT_ERRORS.unknown, `not a raw TON address: ${raw}`);
|
|
1918
|
+
return { workchain: Number(m[1]), hash: Uint8Array.from(m[2].match(/../g), (h) => Number.parseInt(h, 16)) };
|
|
1919
|
+
}
|
|
1920
|
+
function proofDomain(origin) {
|
|
1921
|
+
try {
|
|
1922
|
+
return new URL(origin).host;
|
|
1923
|
+
} catch {
|
|
1924
|
+
throw new TonConnectError(TONCONNECT_ERRORS.badRequest, `bad origin ${origin}`);
|
|
1925
|
+
}
|
|
1926
|
+
}
|
|
1927
|
+
function tonConnectErrorCode(e) {
|
|
1928
|
+
if (e instanceof TonConnectError) return e.code;
|
|
1929
|
+
const code = e?.code;
|
|
1930
|
+
if (code === 4001) return TONCONNECT_ERRORS.userDeclined;
|
|
1931
|
+
if (code === 4100) return TONCONNECT_ERRORS.unknownApp;
|
|
1932
|
+
if (code === 4200) return TONCONNECT_ERRORS.methodNotSupported;
|
|
1933
|
+
if (code === -32602) return TONCONNECT_ERRORS.badRequest;
|
|
1934
|
+
return TONCONNECT_ERRORS.unknown;
|
|
1935
|
+
}
|
|
1936
|
+
|
|
1937
|
+
// src/phishing.ts
|
|
1938
|
+
var PhishingDetector = class {
|
|
1939
|
+
black;
|
|
1940
|
+
white;
|
|
1941
|
+
fuzzy;
|
|
1942
|
+
tolerance;
|
|
1943
|
+
constructor(list) {
|
|
1944
|
+
this.black = new Set(list.blacklist.map(normalizeHost));
|
|
1945
|
+
this.white = new Set((list.whitelist ?? []).map(normalizeHost));
|
|
1946
|
+
this.fuzzy = [...new Set([...list.fuzzylist ?? [], ...list.extraFuzzylist ?? []].map(normalizeHost))];
|
|
1947
|
+
this.tolerance = list.tolerance ?? 1;
|
|
1948
|
+
}
|
|
1949
|
+
get size() {
|
|
1950
|
+
return this.black.size;
|
|
1951
|
+
}
|
|
1952
|
+
check(hostOrOrigin) {
|
|
1953
|
+
const host = hostOf(hostOrOrigin);
|
|
1954
|
+
const warnings = [];
|
|
1955
|
+
if (!host) return { host, blocked: false, warnings };
|
|
1956
|
+
if (matchesList(host, this.white)) return { host, blocked: false, warnings };
|
|
1957
|
+
if (matchesList(host, this.black)) return { host, blocked: true, warnings: ["This site is on the shipped phishing list."] };
|
|
1958
|
+
if (host.split(".").some((l) => l.startsWith("xn--"))) warnings.push("The address uses punycode (xn--): it may imitate another site with look-alike characters.");
|
|
1959
|
+
const look = this.lookalike(host);
|
|
1960
|
+
if (look) warnings.push(`The address looks like ${look} but is not that site.`);
|
|
1961
|
+
else {
|
|
1962
|
+
const brand = this.brandInLabel(host);
|
|
1963
|
+
if (brand) warnings.push(`The address contains the name of ${brand.known} (\u201C${brand.label}\u201D) but is not that site.`);
|
|
1964
|
+
}
|
|
1965
|
+
return { host, blocked: false, warnings };
|
|
1966
|
+
}
|
|
1967
|
+
/**
|
|
1968
|
+
* `metamask.io-login.com`, `app-uniswap.com`, `uniswap.org.evil.net`: a known site's name used as (part of) a label
|
|
1969
|
+
* of a host that is not that site. Names shorter than 5 characters are skipped (too many honest matches).
|
|
1970
|
+
*/
|
|
1971
|
+
brandInLabel(host) {
|
|
1972
|
+
const labels = host.split(".");
|
|
1973
|
+
for (const known of this.fuzzy) {
|
|
1974
|
+
if (host === known || host.endsWith(`.${known}`)) return null;
|
|
1975
|
+
const name = stripForFuzzy(known).split(".").pop() ?? "";
|
|
1976
|
+
if (name.length < 5) continue;
|
|
1977
|
+
const hit = labels.find((l) => l.includes(name));
|
|
1978
|
+
if (hit) return { known, label: hit };
|
|
1979
|
+
}
|
|
1980
|
+
return null;
|
|
1981
|
+
}
|
|
1982
|
+
/** eth-phishing-detect's fuzzy check: the host (minus the TLD and a www.) within `tolerance` edits of a known one. */
|
|
1983
|
+
lookalike(host) {
|
|
1984
|
+
const probe = stripForFuzzy(host);
|
|
1985
|
+
const probeName = probe.split(".").pop() ?? probe;
|
|
1986
|
+
for (const known of this.fuzzy) {
|
|
1987
|
+
if (host === known || host.endsWith(`.${known}`)) return null;
|
|
1988
|
+
const stripped = stripForFuzzy(known);
|
|
1989
|
+
if (levenshtein(probe, stripped) <= this.tolerance) return known;
|
|
1990
|
+
const name = stripped.split(".").pop() ?? stripped;
|
|
1991
|
+
if (name.length >= 5 && name !== probeName && levenshtein(probeName, name) <= this.tolerance) return known;
|
|
1992
|
+
}
|
|
1993
|
+
return null;
|
|
1994
|
+
}
|
|
1995
|
+
};
|
|
1996
|
+
var normalizeHost = (h) => h.trim().toLowerCase().replace(/\.$/, "");
|
|
1997
|
+
function hostOf(hostOrOrigin) {
|
|
1998
|
+
try {
|
|
1999
|
+
return normalizeHost(new URL(hostOrOrigin.includes("://") ? hostOrOrigin : `https://${hostOrOrigin}`).hostname);
|
|
2000
|
+
} catch {
|
|
2001
|
+
return normalizeHost(hostOrOrigin);
|
|
2002
|
+
}
|
|
2003
|
+
}
|
|
2004
|
+
function matchesList(host, list) {
|
|
2005
|
+
const parts = host.split(".");
|
|
2006
|
+
for (let i = 0; i < parts.length - 1; i++) if (list.has(parts.slice(i).join("."))) return true;
|
|
2007
|
+
return list.has(host);
|
|
2008
|
+
}
|
|
2009
|
+
function stripForFuzzy(host) {
|
|
2010
|
+
const parts = host.replace(/^www\./, "").split(".");
|
|
2011
|
+
if (parts.length > 1) parts.pop();
|
|
2012
|
+
return parts.join(".");
|
|
2013
|
+
}
|
|
2014
|
+
function levenshtein(a, b) {
|
|
2015
|
+
if (a === b) return 0;
|
|
2016
|
+
const m = a.length, n = b.length;
|
|
2017
|
+
if (m === 0) return n;
|
|
2018
|
+
if (n === 0) return m;
|
|
2019
|
+
let prev = Array.from({ length: n + 1 }, (_, j) => j);
|
|
2020
|
+
for (let i = 1; i <= m; i++) {
|
|
2021
|
+
const cur = [i];
|
|
2022
|
+
for (let j = 1; j <= n; j++) cur[j] = Math.min(prev[j] + 1, cur[j - 1] + 1, prev[j - 1] + (a[i - 1] === b[j - 1] ? 0 : 1));
|
|
2023
|
+
prev = cur;
|
|
2024
|
+
}
|
|
2025
|
+
return prev[n];
|
|
2026
|
+
}
|
|
2027
|
+
|
|
2028
|
+
// src/webauthn.ts
|
|
2029
|
+
var FLAG_UP = 1;
|
|
2030
|
+
var FLAG_UV = 4;
|
|
2031
|
+
async function verifyAssertion(input, expect) {
|
|
2032
|
+
const c = expect.crypto ?? globalThis.crypto;
|
|
2033
|
+
try {
|
|
2034
|
+
if (expect.alg !== -7) return { ok: false, reason: `unsupported algorithm ${expect.alg} (ES256 only)` };
|
|
2035
|
+
const authData = fromBase64Url(input.authenticatorData);
|
|
2036
|
+
const clientData = fromBase64Url(input.clientDataJSON);
|
|
2037
|
+
const signature = fromBase64Url(input.signature);
|
|
2038
|
+
if (authData.length < 37) return { ok: false, reason: "authenticatorData too short" };
|
|
2039
|
+
let client;
|
|
2040
|
+
try {
|
|
2041
|
+
client = JSON.parse(new TextDecoder().decode(clientData));
|
|
2042
|
+
} catch {
|
|
2043
|
+
return { ok: false, reason: "clientDataJSON is not JSON" };
|
|
2044
|
+
}
|
|
2045
|
+
if (client.type !== "webauthn.get") return { ok: false, reason: `clientData.type is ${String(client.type)}` };
|
|
2046
|
+
if (client.origin !== expect.origin) return { ok: false, reason: `assertion origin ${String(client.origin)} is not ${expect.origin}` };
|
|
2047
|
+
if (client.crossOrigin === true) return { ok: false, reason: "cross-origin assertion" };
|
|
2048
|
+
if (typeof client.challenge !== "string" || !equal(fromBase64Url(client.challenge), expect.challenge)) return { ok: false, reason: "challenge does not match this request" };
|
|
2049
|
+
const rpIdHash = new Uint8Array(await c.subtle.digest("SHA-256", toArrayBuffer(new TextEncoder().encode(expect.rpId))));
|
|
2050
|
+
if (!equal(authData.subarray(0, 32), rpIdHash)) return { ok: false, reason: "rpIdHash does not match this extension" };
|
|
2051
|
+
const flags = authData[32];
|
|
2052
|
+
if (!(flags & FLAG_UP)) return { ok: false, reason: "user presence flag not set" };
|
|
2053
|
+
if (expect.requireUserVerification !== false && !(flags & FLAG_UV)) return { ok: false, reason: "user verification flag not set" };
|
|
2054
|
+
const counter = (authData[33] << 24 | authData[34] << 16 | authData[35] << 8 | authData[36]) >>> 0;
|
|
2055
|
+
const clientHash = new Uint8Array(await c.subtle.digest("SHA-256", toArrayBuffer(clientData)));
|
|
2056
|
+
const signed = new Uint8Array(authData.length + clientHash.length);
|
|
2057
|
+
signed.set(authData, 0);
|
|
2058
|
+
signed.set(clientHash, authData.length);
|
|
2059
|
+
const raw = derToRaw(signature);
|
|
2060
|
+
if (!raw) return { ok: false, reason: "signature is not a DER ECDSA signature" };
|
|
2061
|
+
const key = await c.subtle.importKey("spki", toArrayBuffer(fromBase64(expect.publicKey)), { name: "ECDSA", namedCurve: "P-256" }, false, ["verify"]);
|
|
2062
|
+
const ok = await c.subtle.verify({ name: "ECDSA", hash: "SHA-256" }, key, toArrayBuffer(raw), toArrayBuffer(signed));
|
|
2063
|
+
return ok ? { ok: true, counter } : { ok: false, reason: "signature does not verify against the registered passkey" };
|
|
2064
|
+
} catch (e) {
|
|
2065
|
+
return { ok: false, reason: e instanceof Error ? e.message : String(e) };
|
|
2066
|
+
}
|
|
2067
|
+
}
|
|
2068
|
+
function derToRaw(der) {
|
|
2069
|
+
if (der[0] !== 48) return null;
|
|
2070
|
+
let i = 2;
|
|
2071
|
+
if (der[1] & 128) i = 2 + (der[1] & 127);
|
|
2072
|
+
const int = () => {
|
|
2073
|
+
if (der[i] !== 2) return null;
|
|
2074
|
+
const len = der[i + 1];
|
|
2075
|
+
const start = i + 2;
|
|
2076
|
+
i = start + len;
|
|
2077
|
+
let v = der.subarray(start, start + len);
|
|
2078
|
+
while (v.length > 32 && v[0] === 0) v = v.subarray(1);
|
|
2079
|
+
if (v.length > 32) return null;
|
|
2080
|
+
const out = new Uint8Array(32);
|
|
2081
|
+
out.set(v, 32 - v.length);
|
|
2082
|
+
return out;
|
|
2083
|
+
};
|
|
2084
|
+
const r = int();
|
|
2085
|
+
if (!r) return null;
|
|
2086
|
+
const s = int();
|
|
2087
|
+
if (!s) return null;
|
|
2088
|
+
const raw = new Uint8Array(64);
|
|
2089
|
+
raw.set(r, 0);
|
|
2090
|
+
raw.set(s, 32);
|
|
2091
|
+
return raw;
|
|
2092
|
+
}
|
|
2093
|
+
function rawToDer(raw) {
|
|
2094
|
+
const enc = (v) => {
|
|
2095
|
+
let i = 0;
|
|
2096
|
+
while (i < v.length - 1 && v[i] === 0) i++;
|
|
2097
|
+
let body = Array.from(v.subarray(i));
|
|
2098
|
+
if (body[0] & 128) body = [0, ...body];
|
|
2099
|
+
return [2, body.length, ...body];
|
|
2100
|
+
};
|
|
2101
|
+
const r = enc(raw.subarray(0, 32));
|
|
2102
|
+
const s = enc(raw.subarray(32, 64));
|
|
2103
|
+
return Uint8Array.from([48, r.length + s.length, ...r, ...s]);
|
|
2104
|
+
}
|
|
2105
|
+
async function approvalChallenge(request, c = globalThis.crypto) {
|
|
2106
|
+
const text = JSON.stringify([request.id, request.kind, request.origin, request.payload]);
|
|
2107
|
+
return new Uint8Array(await c.subtle.digest("SHA-256", toArrayBuffer(new TextEncoder().encode(text))));
|
|
2108
|
+
}
|
|
2109
|
+
function equal(a, b) {
|
|
2110
|
+
if (a.length !== b.length) return false;
|
|
2111
|
+
let d = 0;
|
|
2112
|
+
for (let i = 0; i < a.length; i++) d |= a[i] ^ b[i];
|
|
2113
|
+
return d === 0;
|
|
2114
|
+
}
|
|
2115
|
+
|
|
2116
|
+
// src/poison.ts
|
|
2117
|
+
var LOOKALIKE_PREFIX = 4;
|
|
2118
|
+
var LOOKALIKE_SUFFIX = 4;
|
|
2119
|
+
var norm = (a) => a.trim().toLowerCase().replace(/^0x/, "");
|
|
2120
|
+
function lookalikeOf(address, known) {
|
|
2121
|
+
const a = norm(address);
|
|
2122
|
+
if (a.length < LOOKALIKE_PREFIX + LOOKALIKE_SUFFIX + 1) return null;
|
|
2123
|
+
for (const k of known) {
|
|
2124
|
+
const b = norm(k);
|
|
2125
|
+
if (b === a || b.length !== a.length) continue;
|
|
2126
|
+
if (a.startsWith(b.slice(0, LOOKALIKE_PREFIX)) && a.endsWith(b.slice(-LOOKALIKE_SUFFIX))) return k;
|
|
2127
|
+
}
|
|
2128
|
+
return null;
|
|
2129
|
+
}
|
|
2130
|
+
var RecipientBook = class {
|
|
2131
|
+
constructor(storage, cap = 500) {
|
|
2132
|
+
this.storage = storage;
|
|
2133
|
+
this.cap = cap;
|
|
2134
|
+
}
|
|
2135
|
+
storage;
|
|
2136
|
+
cap;
|
|
2137
|
+
_state = null;
|
|
2138
|
+
async state() {
|
|
2139
|
+
this._state ??= await this.storage.get() ?? {};
|
|
2140
|
+
return this._state;
|
|
2141
|
+
}
|
|
2142
|
+
async list(namespace) {
|
|
2143
|
+
return [...(await this.state())[namespace] ?? []];
|
|
2144
|
+
}
|
|
2145
|
+
async remember(namespace, address) {
|
|
2146
|
+
const s = { ...await this.state() };
|
|
2147
|
+
const list = (s[namespace] ?? []).filter((a) => norm(a) !== norm(address));
|
|
2148
|
+
list.push(address);
|
|
2149
|
+
s[namespace] = list.slice(-this.cap);
|
|
2150
|
+
await this.storage.set(s);
|
|
2151
|
+
this._state = s;
|
|
2152
|
+
}
|
|
2153
|
+
/** `known` → the address is one the user paid before; `lookalike` → it imitates one. */
|
|
2154
|
+
async check(namespace, address) {
|
|
2155
|
+
const list = await this.list(namespace);
|
|
2156
|
+
const known = list.some((a) => norm(a) === norm(address));
|
|
2157
|
+
return { known, lookalike: known ? null : lookalikeOf(address, list) };
|
|
2158
|
+
}
|
|
2159
|
+
};
|
|
2160
|
+
export {
|
|
2161
|
+
ACTIVE_STATUSES,
|
|
2162
|
+
APPROVALS_MAX_PENDING_PER_ORIGIN,
|
|
2163
|
+
ApprovalQueue,
|
|
2164
|
+
BTC_CHAIN,
|
|
2165
|
+
BTC_ESPLORA,
|
|
2166
|
+
CHAIN_ACCOUNT_KEYS,
|
|
2167
|
+
DEFAULT_CHAIN,
|
|
2168
|
+
DEFAULT_NETWORKS,
|
|
2169
|
+
FLOOR_TOLERANCE_BPS,
|
|
2170
|
+
KEYRING_STORAGE_KEY,
|
|
2171
|
+
LOOKALIKE_PREFIX,
|
|
2172
|
+
LOOKALIKE_SUFFIX,
|
|
2173
|
+
NEAR_CHAIN,
|
|
2174
|
+
NEAR_PROVIDERS,
|
|
2175
|
+
NON_EVM_CHAINS,
|
|
2176
|
+
NetworksTable,
|
|
2177
|
+
PAGE_TARGET,
|
|
2178
|
+
PORT_NAME,
|
|
2179
|
+
PermissionStore,
|
|
2180
|
+
PhishingDetector,
|
|
2181
|
+
QueueLimitError,
|
|
2182
|
+
READ_METHODS,
|
|
2183
|
+
RELAY_ERRORS,
|
|
2184
|
+
RecipientBook,
|
|
2185
|
+
RequestRouter,
|
|
2186
|
+
ReviewDeclined,
|
|
2187
|
+
ReviewGate,
|
|
2188
|
+
RouterError,
|
|
2189
|
+
SESSION_DEFAULT_AUTOLOCK_MS,
|
|
2190
|
+
SIGNING_METHODS,
|
|
2191
|
+
SOLANA_CHAIN,
|
|
2192
|
+
SOLANA_RPCS,
|
|
2193
|
+
Session,
|
|
2194
|
+
SwapTracker,
|
|
2195
|
+
SwapsStore,
|
|
2196
|
+
TONCONNECT_ERRORS,
|
|
2197
|
+
TONCONNECT_MAX_MESSAGES,
|
|
2198
|
+
TON_CHAIN,
|
|
2199
|
+
TON_ENDPOINTS,
|
|
2200
|
+
TRON_CHAIN,
|
|
2201
|
+
TRON_PROVIDERS,
|
|
2202
|
+
TonConnectError,
|
|
2203
|
+
VAULT_DEFAULT_ITERATIONS,
|
|
2204
|
+
VAULT_MIN_ITERATIONS,
|
|
2205
|
+
VAULT_MIN_PASSWORD_LENGTH,
|
|
2206
|
+
VAULT_MIN_SCRYPT,
|
|
2207
|
+
VAULT_SCRYPT,
|
|
2208
|
+
VAULT_STORAGE_KEY,
|
|
2209
|
+
Vault,
|
|
2210
|
+
VaultError,
|
|
2211
|
+
ZCASH_BLOCKCHAIR,
|
|
2212
|
+
ZCASH_CHAIN,
|
|
2213
|
+
approvalChallenge,
|
|
2214
|
+
caip2FromChainId,
|
|
2215
|
+
chainHex,
|
|
2216
|
+
chainTxMatcher,
|
|
2217
|
+
checkSenderOrigin,
|
|
2218
|
+
chromeAlarms,
|
|
2219
|
+
chromeJsonStorage,
|
|
2220
|
+
chromeKeyringStorage,
|
|
2221
|
+
chromeLocalVaultStorage,
|
|
2222
|
+
chromeSessionStorage,
|
|
2223
|
+
classifySignRequest,
|
|
2224
|
+
createSignHandler,
|
|
2225
|
+
decodedMessage,
|
|
2226
|
+
derToRaw,
|
|
2227
|
+
describeSiwe,
|
|
2228
|
+
earnApprovalPayload,
|
|
2229
|
+
earnPlanMatchers,
|
|
2230
|
+
encodeApproveCalldata,
|
|
2231
|
+
evmCallMatcher,
|
|
2232
|
+
executionFloor,
|
|
2233
|
+
fromBase64Url,
|
|
2234
|
+
hostOf,
|
|
2235
|
+
isCatalogChain,
|
|
2236
|
+
isEnvelopeV1,
|
|
2237
|
+
isEnvelopeV2,
|
|
2238
|
+
isLocalHost,
|
|
2239
|
+
isPageEvent,
|
|
2240
|
+
isPageRequest,
|
|
2241
|
+
isPageResponse,
|
|
2242
|
+
isRelayRequest,
|
|
2243
|
+
isRelayToContent,
|
|
2244
|
+
levenshtein,
|
|
2245
|
+
lookalikeOf,
|
|
2246
|
+
memoryAlarms,
|
|
2247
|
+
memoryApprovalWindow,
|
|
2248
|
+
memoryJsonStorage,
|
|
2249
|
+
memorySessionStorage,
|
|
2250
|
+
memoryVaultStorage,
|
|
2251
|
+
messageText,
|
|
2252
|
+
newRequestId,
|
|
2253
|
+
nonEvmChain,
|
|
2254
|
+
normalizeHost,
|
|
2255
|
+
parseAddChainParams,
|
|
2256
|
+
parseConnectRequest,
|
|
2257
|
+
parseRawAddress,
|
|
2258
|
+
parseSendTransaction,
|
|
2259
|
+
parseSignData,
|
|
2260
|
+
parseSiwe,
|
|
2261
|
+
parseSwapId,
|
|
2262
|
+
planMatchers,
|
|
2263
|
+
prepareTransaction,
|
|
2264
|
+
proofDomain,
|
|
2265
|
+
quoteRoutes,
|
|
2266
|
+
rank,
|
|
2267
|
+
rawToDer,
|
|
2268
|
+
relayError,
|
|
2269
|
+
reviewEarnPlan,
|
|
2270
|
+
reviewSwapPlan,
|
|
2271
|
+
siweProblems,
|
|
2272
|
+
stampOrigin,
|
|
2273
|
+
swapApprovalPayload,
|
|
2274
|
+
toBase64Url,
|
|
2275
|
+
toRelayError,
|
|
2276
|
+
tonConnectErrorCode,
|
|
2277
|
+
tonProofMessage,
|
|
2278
|
+
tonSignDataMessage,
|
|
2279
|
+
vaultSecretStorage,
|
|
2280
|
+
verifyAssertion
|
|
2281
|
+
};
|
|
2282
|
+
//# sourceMappingURL=index.js.map
|