@anywalletkit/extension-kit 0.1.4 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +10 -7
- package/dist/index.cjs +219 -60
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +100 -12
- package/dist/index.d.ts +100 -12
- package/dist/index.js +219 -60
- package/dist/index.js.map +1 -1
- package/package.json +8 -7
package/README.md
CHANGED
|
@@ -36,17 +36,17 @@ pnpm add @anywalletkit/extension-kit @anywalletkit/core @anywalletkit/keyring
|
|
|
36
36
|
| Vault | `Vault`, `chromeLocalVaultStorage`, `memoryVaultStorage`, `VAULT_SCRYPT`, `VAULT_LIMITS`, `VAULT_MIN_PASSWORD_LENGTH` | every secret at rest in `chrome.storage.local`, two key layers (a KEK wraps a DEK, the DEK seals each secret); passkey (WebAuthn PRF) unlock as a second wrap of the same DEK |
|
|
37
37
|
| Session | `Session`, `chromeSessionStorage`, `chromeAlarms`, `SESSION_DEFAULT_AUTOLOCK_MS` | the DEK in `chrome.storage.session` with an expiry and an alarm; `getKey()` re-checks the expiry itself |
|
|
38
38
|
| Keyring adapters | `chromeKeyringStorage`, `vaultSecretStorage` | `@anywalletkit/keyring` metadata as plain JSON, secrets through the vault, one DEK use per call |
|
|
39
|
-
| Relay | `PageRequest` / `RelayRequest` shapes and guards, `stampOrigin`, `checkSenderOrigin`, `RELAY_ERRORS` | page ⇄ content script ⇄ service worker messages; the origin is the browser's (`port.sender`), never the page's |
|
|
39
|
+
| Relay | `PageRequest` / `RelayRequest` shapes and guards, `pageRequestFrom`, `stampOrigin`, `checkSenderOrigin`, `RELAY_ERRORS` | page ⇄ content script ⇄ service worker messages; the content script takes only messages the page window itself posted (`pageRequestFrom(event, window)`: an iframe's `parent.postMessage` is not the site), and the origin is the browser's (`port.sender`), never the page's |
|
|
40
40
|
| Router | `RequestRouter`, `READ_METHODS`, `SIGNING_METHODS`, `toRelayError` | what a dApp may call: accounts only for connected origins, chain add / switch through the approval window, reads from the wallet's own RPCs, `eth_sign` never offered, everything else 4200 |
|
|
41
41
|
| Permissions | `PermissionStore`, `CHAIN_ACCOUNT_KEYS` | per-origin accounts (EVM, Solana, TON, Tron, Bitcoin, NEAR) and chain |
|
|
42
42
|
| Approvals | `ApprovalQueue`, `ReviewDeclined`, `QueueLimitError`, `APPROVALS_MAX_PENDING_PER_ORIGIN` | one approval window at a time, at most 3 pending requests per origin and one pending `connect`; transport notes (`setVia`) for WalletConnect peers |
|
|
43
43
|
| Signing | `classifySignRequest`, `prepareTransaction`, `createSignHandler`, `messageText` | connected-address and chain checks, EIP-7702 delegations from sites refused, nonce / gas / fees from the wallet's RPC |
|
|
44
44
|
| Networks | `NetworksTable`, `DEFAULT_NETWORKS`, `parseAddChainParams`, Solana / TON / Tron endpoints | EVM networks with public RPC failover lists; `wallet_addEthereumChain` only for catalog chains, https only, `eth_chainId` verified, site-supplied RPCs stamped `custom` |
|
|
45
45
|
| Review gate | `ReviewGate`, `evmCallMatcher`, `chainTxMatcher` | after one sheet reviewed a whole plan, exactly those operations pass once within a TTL |
|
|
46
|
-
| Swaps | `quoteRoutes`, `reviewSwapPlan`, `executionFloor`, `SwapsStore`, `SwapTracker`, `parseSwapId` | progressive quotes over swap2 providers, one review sheet per plan, pending swaps resumed across worker restarts |
|
|
47
|
-
| Earn | `reviewEarnPlan`, `earnPlanMatchers` | one sheet for an Earn module's plan (approvals + call) |
|
|
48
|
-
| TON Connect | `parseConnectRequest`, `parseSendTransaction`, `parseSignData`, `tonProofMessage`, `tonSignDataMessage` | the pure-data half of TON Connect v2 (the
|
|
49
|
-
| Phishing | `PhishingDetector` | MetaMask `eth-phishing-detect` list format, shipped with the build (never fetched at runtime), fuzzy lookalikes, brand-in-label, punycode |
|
|
46
|
+
| Swaps | `quoteRoutes`, `reviewSwapPlan`, `executionFloor`, `SwapsStore`, `SwapTracker`, `parseSwapId` | progressive quotes over swap2 providers, one review sheet per plan (a gasless order is shown and matched by its EIP-712 hash), pre-approvals bound to the reviewing account (`meta.account`), pending swaps resumed across worker restarts |
|
|
47
|
+
| Earn | `reviewEarnPlan`, `earnPlanMatchers` | one sheet for an Earn module's plan (approvals + call), bound to `meta.account` |
|
|
48
|
+
| TON Connect | `parseConnectRequest`, `parseSendTransaction`, `parseSignData`, `tonProofMessage`, `tonSignDataMessage` | the pure-data half of TON Connect v2 (the digests `ton_proof` and `signData` sign, to verify them); the account signs with `signTonProof` / `signTonConnectData` (wallet-ton) — `sign()` refuses 32-byte digests |
|
|
49
|
+
| Phishing | `PhishingDetector` | MetaMask `eth-phishing-detect` list format, shipped with the build (never fetched at runtime); a listed site is refused (`refusal` → `refuse` on `ApprovalQueue` and `RequestRouter`, 4100, no window); fuzzy lookalikes, brand-in-label, punycode warn |
|
|
50
50
|
| SIWE | `parseSiwe`, `siweProblems`, `describeSiwe` | EIP-4361 sign-in messages: domain vs. the asking site, address, chain, expiry, nonce (warnings) |
|
|
51
51
|
| Address poisoning | `lookalikeOf`, `RecipientBook` | warns when a new recipient imitates one the user confirmed |
|
|
52
52
|
| WebAuthn | `verifyAssertion`, `approvalChallenge`, `derToRaw`, `rawToDer` | ES256 assertion check for "passkey on every signature"; the worker never trusts the page's word |
|
|
@@ -76,8 +76,11 @@ pnpm add @anywalletkit/extension-kit @anywalletkit/core @anywalletkit/keyring
|
|
|
76
76
|
- **Errors to pages.** `toRelayError(e, { log })` passes the wallet's own refusals
|
|
77
77
|
(EIP-1193 codes) and a node's JSON-RPC error (code, revert data) with URLs
|
|
78
78
|
removed; anything else is `-32603 Internal error` on the page and goes to `log`.
|
|
79
|
-
- **Origins.** The
|
|
80
|
-
|
|
79
|
+
- **Origins.** The content script relays only what the page window itself
|
|
80
|
+
posted (`pageRequestFrom`: `event.source` is the page, `event.origin` its
|
|
81
|
+
origin), so an ad or embed in an iframe cannot speak as the site. The
|
|
82
|
+
service worker answers a relay message only when the content script's
|
|
83
|
+
stamped origin equals `port.sender.origin`. Permissions are
|
|
81
84
|
keyed by that origin, never by a URL a page or a WalletConnect peer claims.
|
|
82
85
|
- **Requests.** Everything that reveals accounts or moves funds goes through
|
|
83
86
|
the approval queue; blind signing (`eth_sign`) is not offered; typed data for
|
package/dist/index.cjs
CHANGED
|
@@ -36,6 +36,7 @@ __export(src_exports, {
|
|
|
36
36
|
NEAR_PROVIDERS: () => NEAR_PROVIDERS,
|
|
37
37
|
NON_EVM_CHAINS: () => NON_EVM_CHAINS,
|
|
38
38
|
NetworksTable: () => NetworksTable,
|
|
39
|
+
OriginRefused: () => OriginRefused,
|
|
39
40
|
PAGE_TARGET: () => PAGE_TARGET,
|
|
40
41
|
PORT_NAME: () => PORT_NAME,
|
|
41
42
|
PermissionStore: () => PermissionStore,
|
|
@@ -115,6 +116,7 @@ __export(src_exports, {
|
|
|
115
116
|
newRequestId: () => newRequestId,
|
|
116
117
|
nonEvmChain: () => nonEvmChain,
|
|
117
118
|
normalizeHost: () => normalizeHost,
|
|
119
|
+
pageRequestFrom: () => pageRequestFrom,
|
|
118
120
|
parseAddChainParams: () => parseAddChainParams,
|
|
119
121
|
parseConnectRequest: () => parseConnectRequest,
|
|
120
122
|
parseRawAddress: () => parseRawAddress,
|
|
@@ -139,6 +141,7 @@ __export(src_exports, {
|
|
|
139
141
|
tonConnectErrorCode: () => tonConnectErrorCode,
|
|
140
142
|
tonProofMessage: () => tonProofMessage,
|
|
141
143
|
tonSignDataMessage: () => tonSignDataMessage,
|
|
144
|
+
typedDataMatcher: () => typedDataMatcher,
|
|
142
145
|
vaultSecretStorage: () => vaultSecretStorage,
|
|
143
146
|
verifyAssertion: () => verifyAssertion
|
|
144
147
|
});
|
|
@@ -179,6 +182,14 @@ function toArrayBuffer(bytes) {
|
|
|
179
182
|
copy.set(bytes);
|
|
180
183
|
return copy.buffer;
|
|
181
184
|
}
|
|
185
|
+
async function withSecretBuffer(bytes, fn) {
|
|
186
|
+
const buffer = toArrayBuffer(bytes);
|
|
187
|
+
try {
|
|
188
|
+
return await fn(buffer);
|
|
189
|
+
} finally {
|
|
190
|
+
new Uint8Array(buffer).fill(0);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
182
193
|
|
|
183
194
|
// src/vault.ts
|
|
184
195
|
var VAULT_MIN_ITERATIONS = 6e5;
|
|
@@ -446,7 +457,13 @@ var Vault = class {
|
|
|
446
457
|
}
|
|
447
458
|
async _derivePbkdf2(password, salt, iterations) {
|
|
448
459
|
const subtle = this._crypto.subtle;
|
|
449
|
-
const
|
|
460
|
+
const pw = new TextEncoder().encode(password.normalize("NFKC"));
|
|
461
|
+
let material;
|
|
462
|
+
try {
|
|
463
|
+
material = await withSecretBuffer(pw, (b) => subtle.importKey("raw", b, "PBKDF2", false, ["deriveKey"]));
|
|
464
|
+
} finally {
|
|
465
|
+
(0, import_core.zeroize)(pw);
|
|
466
|
+
}
|
|
450
467
|
return subtle.deriveKey({ name: "PBKDF2", hash: "SHA-256", salt: toArrayBuffer(salt), iterations }, material, { name: "AES-GCM", length: 256 }, false, ["encrypt", "decrypt"]);
|
|
451
468
|
}
|
|
452
469
|
async _deriveScrypt(password, salt, cost) {
|
|
@@ -458,7 +475,7 @@ var Vault = class {
|
|
|
458
475
|
(0, import_core.zeroize)(pw);
|
|
459
476
|
}
|
|
460
477
|
try {
|
|
461
|
-
return await this._crypto.subtle.importKey("raw",
|
|
478
|
+
return await withSecretBuffer(raw, (b) => this._crypto.subtle.importKey("raw", b, { name: "AES-GCM", length: 256 }, false, ["encrypt", "decrypt"]));
|
|
462
479
|
} finally {
|
|
463
480
|
(0, import_core.zeroize)(raw);
|
|
464
481
|
}
|
|
@@ -466,16 +483,16 @@ var Vault = class {
|
|
|
466
483
|
/** HKDF-SHA256(ikm = PRF output, salt = credential id bytes, info) → AES-256-GCM key. */
|
|
467
484
|
async _derivePasskeyKek(prf, credentialId) {
|
|
468
485
|
const subtle = this._crypto.subtle;
|
|
469
|
-
const ikm = await subtle.importKey("raw",
|
|
486
|
+
const ikm = await withSecretBuffer(prf, (b) => subtle.importKey("raw", b, "HKDF", false, ["deriveKey"]));
|
|
470
487
|
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"]);
|
|
471
488
|
}
|
|
472
489
|
async _importDek(dek) {
|
|
473
490
|
if (dek.length !== DEK_BYTES) throw new VaultError("Vault key must be 32 bytes.", "invalid-input");
|
|
474
|
-
return this._crypto.subtle.importKey("raw",
|
|
491
|
+
return withSecretBuffer(dek, (b) => this._crypto.subtle.importKey("raw", b, { name: "AES-GCM", length: 256 }, false, ["encrypt", "decrypt"]));
|
|
475
492
|
}
|
|
476
493
|
async _seal(key, plaintext, aad) {
|
|
477
494
|
const iv = this._random(IV_BYTES);
|
|
478
|
-
const ct = await this._crypto.subtle.encrypt({ name: "AES-GCM", iv: toArrayBuffer(iv), additionalData: toArrayBuffer(new TextEncoder().encode(aad)) }, key,
|
|
495
|
+
const ct = await withSecretBuffer(plaintext, (b) => this._crypto.subtle.encrypt({ name: "AES-GCM", iv: toArrayBuffer(iv), additionalData: toArrayBuffer(new TextEncoder().encode(aad)) }, key, b));
|
|
479
496
|
return { iv: toBase64(iv), data: toBase64(new Uint8Array(ct)) };
|
|
480
497
|
}
|
|
481
498
|
async _open(key, box, aad, onFail) {
|
|
@@ -660,22 +677,40 @@ var Session = class {
|
|
|
660
677
|
if (was !== void 0 && was !== null) for (const cb of this._onLock) cb();
|
|
661
678
|
}
|
|
662
679
|
_expiry() {
|
|
663
|
-
return this._autoLockMs > 0 ? this._now() + this._autoLockMs :
|
|
680
|
+
return this._autoLockMs > 0 ? this._now() + this._autoLockMs : null;
|
|
664
681
|
}
|
|
665
682
|
async _arm(expiresAt) {
|
|
666
|
-
if (
|
|
683
|
+
if (expiresAt !== null) await this._alarms.schedule(this._alarm, expiresAt);
|
|
667
684
|
else await this._alarms.clear(this._alarm);
|
|
668
685
|
}
|
|
686
|
+
/** The stored record, or `null` when locked. A record that is there but unreadable locks the wallet (removed, listeners told). */
|
|
669
687
|
async _read() {
|
|
670
688
|
const raw = await this._storage.get(this._key);
|
|
671
|
-
if (
|
|
672
|
-
|
|
689
|
+
if (raw === void 0 || raw === null) return null;
|
|
690
|
+
const r = raw;
|
|
691
|
+
const expiryOk = r.expiresAt === null || typeof r.expiresAt === "number" && Number.isFinite(r.expiresAt);
|
|
692
|
+
if (typeof raw !== "object" || typeof r.key !== "string" || !expiryOk || !isKey(r.key)) {
|
|
673
693
|
await this._lockNow();
|
|
674
694
|
return null;
|
|
675
695
|
}
|
|
676
|
-
|
|
696
|
+
if (r.expiresAt !== null && r.expiresAt <= this._now()) {
|
|
697
|
+
await this._lockNow();
|
|
698
|
+
return null;
|
|
699
|
+
}
|
|
700
|
+
return r;
|
|
677
701
|
}
|
|
678
702
|
};
|
|
703
|
+
function isKey(text) {
|
|
704
|
+
let bytes;
|
|
705
|
+
try {
|
|
706
|
+
bytes = fromBase64(text);
|
|
707
|
+
} catch {
|
|
708
|
+
return false;
|
|
709
|
+
}
|
|
710
|
+
const ok = bytes.length === 32;
|
|
711
|
+
(0, import_core2.zeroize)(bytes);
|
|
712
|
+
return ok;
|
|
713
|
+
}
|
|
679
714
|
function memorySessionStorage() {
|
|
680
715
|
const data = /* @__PURE__ */ new Map();
|
|
681
716
|
return {
|
|
@@ -752,6 +787,10 @@ function isRelayToContent(x) {
|
|
|
752
787
|
function newRequestId() {
|
|
753
788
|
return crypto.randomUUID();
|
|
754
789
|
}
|
|
790
|
+
function pageRequestFrom(event, page) {
|
|
791
|
+
if (event.source !== page || event.origin !== page.location.origin) return null;
|
|
792
|
+
return isPageRequest(event.data) ? event.data : null;
|
|
793
|
+
}
|
|
755
794
|
function stampOrigin(request, origin) {
|
|
756
795
|
const { id, chain, method } = request;
|
|
757
796
|
return "params" in request ? { kind: "request", id, origin, chain, method, params: request.params } : { kind: "request", id, origin, chain, method };
|
|
@@ -980,14 +1019,27 @@ var NetworksTable = class {
|
|
|
980
1019
|
return this._loading;
|
|
981
1020
|
}
|
|
982
1021
|
};
|
|
1022
|
+
function isPrivateRpcHost(url) {
|
|
1023
|
+
const m = /^https:\/\/(?:[^@/?#]*@)?(\[[^\]]+\]|[^:/?#]+)/i.exec(url);
|
|
1024
|
+
if (!m) return true;
|
|
1025
|
+
const h = m[1].toLowerCase().replace(/^\[|\]$/g, "").replace(/\.$/, "");
|
|
1026
|
+
if (h === "localhost" || h.endsWith(".localhost") || h.endsWith(".local") || h === "0.0.0.0") return true;
|
|
1027
|
+
const v4 = /^(\d{1,3})\.(\d{1,3})\.\d{1,3}\.\d{1,3}$/.exec(h);
|
|
1028
|
+
if (v4) {
|
|
1029
|
+
const a = Number(v4[1]), b = Number(v4[2]);
|
|
1030
|
+
return a === 127 || a === 10 || a === 0 || a === 172 && b >= 16 && b <= 31 || a === 192 && b === 168 || a === 169 && b === 254;
|
|
1031
|
+
}
|
|
1032
|
+
if (h.includes(":")) return h === "::1" || h === "::" || /^f[cd]/.test(h) || /^fe[89ab]/.test(h);
|
|
1033
|
+
return false;
|
|
1034
|
+
}
|
|
983
1035
|
function parseAddChainParams(params, known = isCatalogChain) {
|
|
984
1036
|
const p = Array.isArray(params) ? params[0] : params;
|
|
985
1037
|
if (!p || typeof p !== "object") return relayError(RELAY_ERRORS.invalidParams, "wallet_addEthereumChain expects one object parameter.");
|
|
986
1038
|
const caip2 = caip2FromChainId(p.chainId);
|
|
987
1039
|
if (!caip2) return relayError(RELAY_ERRORS.invalidParams, "chainId must be a 0x-prefixed hex string.");
|
|
988
1040
|
if (!known(caip2)) return relayError(RELAY_ERRORS.chainNotAdded, `Chain ${String(p.chainId)} is not in this wallet's catalog.`);
|
|
989
|
-
const rpcs = Array.isArray(p.rpcUrls) ? p.rpcUrls.filter((u) => typeof u === "string" && /^https:\/\/[^\s/?#]+/i.test(u)) : [];
|
|
990
|
-
if (rpcs.length === 0) return relayError(RELAY_ERRORS.invalidParams, "rpcUrls must contain at least one https URL.");
|
|
1041
|
+
const rpcs = Array.isArray(p.rpcUrls) ? p.rpcUrls.filter((u) => typeof u === "string" && /^https:\/\/[^\s/?#]+/i.test(u) && !isPrivateRpcHost(u)) : [];
|
|
1042
|
+
if (rpcs.length === 0) return relayError(RELAY_ERRORS.invalidParams, "rpcUrls must contain at least one public https URL (a site cannot add a private / local endpoint).");
|
|
991
1043
|
const name = typeof p.chainName === "string" && p.chainName.trim() ? p.chainName.trim().slice(0, 64) : caip2;
|
|
992
1044
|
const currency = p.nativeCurrency;
|
|
993
1045
|
if (currency && currency.decimals !== void 0 && currency.decimals !== 18) return relayError(RELAY_ERRORS.invalidParams, "nativeCurrency.decimals must be 18.");
|
|
@@ -1184,6 +1236,13 @@ var QueueLimitError = class extends Error {
|
|
|
1184
1236
|
}
|
|
1185
1237
|
code;
|
|
1186
1238
|
};
|
|
1239
|
+
var OriginRefused = class extends Error {
|
|
1240
|
+
code = RELAY_ERRORS.unauthorized;
|
|
1241
|
+
constructor(message) {
|
|
1242
|
+
super(message);
|
|
1243
|
+
this.name = "OriginRefused";
|
|
1244
|
+
}
|
|
1245
|
+
};
|
|
1187
1246
|
var APPROVALS_MAX_PENDING_PER_ORIGIN = 3;
|
|
1188
1247
|
var ApprovalQueue = class {
|
|
1189
1248
|
constructor(window, newId = () => crypto.randomUUID(), options = {}) {
|
|
@@ -1203,6 +1262,13 @@ var ApprovalQueue = class {
|
|
|
1203
1262
|
_via = /* @__PURE__ */ new Map();
|
|
1204
1263
|
/** Enqueue and wait for the user's answer. Refuses (never queues) past the per-origin limits. */
|
|
1205
1264
|
request(input) {
|
|
1265
|
+
let refused;
|
|
1266
|
+
try {
|
|
1267
|
+
refused = this.options.refuse?.(input.origin);
|
|
1268
|
+
} catch (e) {
|
|
1269
|
+
refused = `The wallet could not check this site (${e instanceof Error ? e.message : String(e)}).`;
|
|
1270
|
+
}
|
|
1271
|
+
if (refused) return Promise.reject(new OriginRefused(refused));
|
|
1206
1272
|
const sameOrigin = [...this._current ? [this._current] : [], ...this._waiting].filter((p) => p.request.origin === input.origin);
|
|
1207
1273
|
if (input.kind === "connect" && sameOrigin.some((p) => p.request.kind === "connect")) {
|
|
1208
1274
|
return Promise.reject(new QueueLimitError(RELAY_ERRORS.resourceUnavailable, "A connect request from this site is already waiting for your answer."));
|
|
@@ -1391,6 +1457,8 @@ var RequestRouter = class {
|
|
|
1391
1457
|
return await this.deps.networks.get(wanted) ?? await this.deps.networks.get(DEFAULT_CHAIN);
|
|
1392
1458
|
}
|
|
1393
1459
|
async handle(origin, chain, method, rawParams) {
|
|
1460
|
+
const refused = this.deps.refuse?.(origin);
|
|
1461
|
+
if (refused) throw new RouterError(RELAY_ERRORS.unauthorized, refused);
|
|
1394
1462
|
if (chain !== "eip155") throw new RouterError(RELAY_ERRORS.unsupportedMethod, `${chain} providers are not available yet.`);
|
|
1395
1463
|
const params = Array.isArray(rawParams) ? rawParams : rawParams === void 0 ? [] : [rawParams];
|
|
1396
1464
|
const network = await this.chainFor(origin);
|
|
@@ -1699,6 +1767,23 @@ var bigintSafe = (_k, v) => typeof v === "bigint" ? v.toString() : v;
|
|
|
1699
1767
|
var hex = (n) => `0x${n.toString(16)}`;
|
|
1700
1768
|
var big = (v) => BigInt(v);
|
|
1701
1769
|
var MIN_TIP_WEI = 100000000n;
|
|
1770
|
+
async function readFees(deps, network, block) {
|
|
1771
|
+
const b = block === void 0 ? await deps.rpc(network, "eth_getBlockByNumber", ["latest", false]) : block;
|
|
1772
|
+
const base = b?.baseFeePerGas != null ? big(b.baseFeePerGas) : null;
|
|
1773
|
+
if (base === null) return { gasPrice: big(await deps.rpc(network, "eth_gasPrice", [])) };
|
|
1774
|
+
const floor = deps.tipFloor?.(network.chainId) ?? MIN_TIP_WEI;
|
|
1775
|
+
const nodeTip = await deps.rpc(network, "eth_maxPriorityFeePerGas", []).then(big).catch(() => 0n);
|
|
1776
|
+
return { base, tip: nodeTip > floor ? nodeTip : floor };
|
|
1777
|
+
}
|
|
1778
|
+
function withFees(chainId, nonce, gasLimit, fees) {
|
|
1779
|
+
if ("gasPrice" in fees) return { chainId, nonce, gasLimit: hex(gasLimit), gasPrice: hex(fees.gasPrice), maxFee: (gasLimit * fees.gasPrice).toString() };
|
|
1780
|
+
const maxFeePerGas = fees.base * 2n + fees.tip;
|
|
1781
|
+
return { chainId, nonce, gasLimit: hex(gasLimit), maxFeePerGas: hex(maxFeePerGas), maxPriorityFeePerGas: hex(fees.tip), maxFee: (gasLimit * maxFeePerGas).toString() };
|
|
1782
|
+
}
|
|
1783
|
+
function outgrown(shown, now) {
|
|
1784
|
+
if ("gasPrice" in now) return shown.gasPrice === void 0 || now.gasPrice > big(shown.gasPrice);
|
|
1785
|
+
return shown.maxFeePerGas === void 0 || now.base + now.tip > big(shown.maxFeePerGas);
|
|
1786
|
+
}
|
|
1702
1787
|
async function prepareTransaction(deps, network, from, tx) {
|
|
1703
1788
|
const call = { from, to: tx.to, value: hex(BigInt(tx.value)), ...tx.data ? { data: tx.data } : {} };
|
|
1704
1789
|
const [nonceHex, block, gasHex] = await Promise.all([
|
|
@@ -1708,18 +1793,20 @@ async function prepareTransaction(deps, network, from, tx) {
|
|
|
1708
1793
|
]);
|
|
1709
1794
|
const estimated = big(gasHex);
|
|
1710
1795
|
const gasLimit = tx.gas ? estimated : (estimated * 120n + 99n) / 100n;
|
|
1711
|
-
|
|
1712
|
-
if (base === null) {
|
|
1713
|
-
const gasPrice = big(await deps.rpc(network, "eth_gasPrice", []));
|
|
1714
|
-
return { chainId: network.chainId, nonce: safeNonce(big(nonceHex)), gasLimit: hex(gasLimit), gasPrice: hex(gasPrice), maxFee: (gasLimit * gasPrice).toString() };
|
|
1715
|
-
}
|
|
1716
|
-
const floor = deps.tipFloor?.(network.chainId) ?? MIN_TIP_WEI;
|
|
1717
|
-
const nodeTip = await deps.rpc(network, "eth_maxPriorityFeePerGas", []).then(big).catch(() => 0n);
|
|
1718
|
-
const tip = nodeTip > floor ? nodeTip : floor;
|
|
1719
|
-
const maxFeePerGas = base * 2n + tip;
|
|
1720
|
-
return { chainId: network.chainId, nonce: safeNonce(big(nonceHex)), gasLimit: hex(gasLimit), maxFeePerGas: hex(maxFeePerGas), maxPriorityFeePerGas: hex(tip), maxFee: (gasLimit * maxFeePerGas).toString() };
|
|
1796
|
+
return withFees(network.chainId, safeNonce(big(nonceHex)), gasLimit, await readFees(deps, network, block));
|
|
1721
1797
|
}
|
|
1722
1798
|
function createSignHandler(deps) {
|
|
1799
|
+
const tails = /* @__PURE__ */ new Map();
|
|
1800
|
+
const nextNonce = /* @__PURE__ */ new Map();
|
|
1801
|
+
const serial = (key, fn) => {
|
|
1802
|
+
const run = (tails.get(key) ?? Promise.resolve()).then(fn, fn);
|
|
1803
|
+
const tail = run.then(() => void 0, () => void 0);
|
|
1804
|
+
tails.set(key, tail);
|
|
1805
|
+
void tail.then(() => {
|
|
1806
|
+
if (tails.get(key) === tail) tails.delete(key);
|
|
1807
|
+
});
|
|
1808
|
+
return run;
|
|
1809
|
+
};
|
|
1723
1810
|
return async ({ origin, network, method, params, accounts }) => {
|
|
1724
1811
|
const via = deps.approvals.viaFor(origin);
|
|
1725
1812
|
const info = classifySignRequest({ network, method, params, accounts, origin, ...via ? { via } : {} });
|
|
@@ -1728,6 +1815,41 @@ function createSignHandler(deps) {
|
|
|
1728
1815
|
const preview = deps.preview ? await deps.preview({ network, request: info, accounts }).catch(() => null) : null;
|
|
1729
1816
|
const payload = { request: info, network, ...prepared ? { prepared } : {}, preview };
|
|
1730
1817
|
await deps.approvals.request({ origin, chain: network.caip2, kind: info.kind, payload });
|
|
1818
|
+
if (info.kind === "send-transaction") {
|
|
1819
|
+
const key = `${network.caip2}|${lc(info.account)}`;
|
|
1820
|
+
return serial(key, async () => {
|
|
1821
|
+
let shown = prepared;
|
|
1822
|
+
for (; ; ) {
|
|
1823
|
+
const [nonceHex, fees] = await Promise.all([deps.rpc(network, "eth_getTransactionCount", [info.account, "pending"]), readFees(deps, network)]);
|
|
1824
|
+
const local = nextNonce.get(key) ?? 0;
|
|
1825
|
+
const nodeNonce = safeNonce(big(nonceHex));
|
|
1826
|
+
const nonce = nodeNonce > local ? nodeNonce : local;
|
|
1827
|
+
if (outgrown(shown, fees)) {
|
|
1828
|
+
shown = withFees(network.chainId, nonce, big(shown.gasLimit), fees);
|
|
1829
|
+
await deps.approvals.request({ origin, chain: network.caip2, kind: info.kind, payload: { ...payload, prepared: shown, repriced: true } });
|
|
1830
|
+
continue;
|
|
1831
|
+
}
|
|
1832
|
+
const handle2 = await deps.signerFor(info.account, network);
|
|
1833
|
+
let raw;
|
|
1834
|
+
try {
|
|
1835
|
+
raw = await handle2.signer.signTransaction({
|
|
1836
|
+
chainId: shown.chainId,
|
|
1837
|
+
nonce,
|
|
1838
|
+
to: info.tx.to,
|
|
1839
|
+
value: BigInt(info.tx.value),
|
|
1840
|
+
data: info.tx.data ?? "0x",
|
|
1841
|
+
gasLimit: BigInt(shown.gasLimit),
|
|
1842
|
+
...shown.gasPrice !== void 0 ? { gasPrice: BigInt(shown.gasPrice) } : { maxFeePerGas: BigInt(shown.maxFeePerGas), maxPriorityFeePerGas: BigInt(shown.maxPriorityFeePerGas) }
|
|
1843
|
+
});
|
|
1844
|
+
} finally {
|
|
1845
|
+
handle2.dispose();
|
|
1846
|
+
}
|
|
1847
|
+
const hash = await deps.rpc(network, "eth_sendRawTransaction", [raw]);
|
|
1848
|
+
nextNonce.set(key, nonce + 1);
|
|
1849
|
+
return hash;
|
|
1850
|
+
}
|
|
1851
|
+
});
|
|
1852
|
+
}
|
|
1731
1853
|
const handle = await deps.signerFor(info.account, network);
|
|
1732
1854
|
try {
|
|
1733
1855
|
switch (info.kind) {
|
|
@@ -1735,19 +1857,6 @@ function createSignHandler(deps) {
|
|
|
1735
1857
|
return await handle.signer.signMessage(HEX.test(info.raw) ? Uint8Array.from(info.raw.slice(2).match(/../g) ?? [], (b) => Number.parseInt(b, 16)) : info.raw);
|
|
1736
1858
|
case "sign-typed-data":
|
|
1737
1859
|
return await handle.signer.signTypedData(info.typedData);
|
|
1738
|
-
case "send-transaction": {
|
|
1739
|
-
const p = prepared;
|
|
1740
|
-
const raw = await handle.signer.signTransaction({
|
|
1741
|
-
chainId: p.chainId,
|
|
1742
|
-
nonce: p.nonce,
|
|
1743
|
-
to: info.tx.to,
|
|
1744
|
-
value: BigInt(info.tx.value),
|
|
1745
|
-
data: info.tx.data ?? "0x",
|
|
1746
|
-
gasLimit: BigInt(p.gasLimit),
|
|
1747
|
-
...p.gasPrice !== void 0 ? { gasPrice: BigInt(p.gasPrice) } : { maxFeePerGas: BigInt(p.maxFeePerGas), maxPriorityFeePerGas: BigInt(p.maxPriorityFeePerGas) }
|
|
1748
|
-
});
|
|
1749
|
-
return await deps.rpc(network, "eth_sendRawTransaction", [raw]);
|
|
1750
|
-
}
|
|
1751
1860
|
}
|
|
1752
1861
|
} finally {
|
|
1753
1862
|
handle.dispose();
|
|
@@ -1756,22 +1865,28 @@ function createSignHandler(deps) {
|
|
|
1756
1865
|
}
|
|
1757
1866
|
|
|
1758
1867
|
// src/gate.ts
|
|
1868
|
+
var HEX_ADDRESS = /^0x[0-9a-fA-F]{40}$/;
|
|
1869
|
+
var sameAccount = (a, b) => a === b || HEX_ADDRESS.test(a) && a.toLowerCase() === b.toLowerCase();
|
|
1759
1870
|
var ReviewGate = class {
|
|
1760
1871
|
constructor(now = Date.now) {
|
|
1761
1872
|
this.now = now;
|
|
1762
1873
|
}
|
|
1763
1874
|
now;
|
|
1764
1875
|
approved = [];
|
|
1765
|
-
/**
|
|
1766
|
-
|
|
1876
|
+
/**
|
|
1877
|
+
* Each matcher passes ONE operation, within `ttlMs` (default 10 min). With `account`, only an op
|
|
1878
|
+
* whose `account` is that account (an op without one does not pass). `reviewSwapPlan` and
|
|
1879
|
+
* `reviewEarnPlan` always bind.
|
|
1880
|
+
*/
|
|
1881
|
+
preApprove(matchers, ttlMs = 10 * 6e4, account) {
|
|
1767
1882
|
const until = this.now() + ttlMs;
|
|
1768
|
-
this.approved.push(...matchers.map((match) => ({ match, until })));
|
|
1883
|
+
this.approved.push(...matchers.map((match) => ({ match, until, ...account ? { account } : {} })));
|
|
1769
1884
|
}
|
|
1770
1885
|
/** True (and the matcher is spent) when a pre-approval covers `op`. */
|
|
1771
1886
|
consume(op) {
|
|
1772
1887
|
const now = this.now();
|
|
1773
1888
|
this.approved = this.approved.filter((a) => a.until > now);
|
|
1774
|
-
const i = this.approved.findIndex((a) => a.match(op));
|
|
1889
|
+
const i = this.approved.findIndex((a) => (a.account === void 0 || op.account !== void 0 && sameAccount(a.account, op.account)) && a.match(op));
|
|
1775
1890
|
if (i < 0) return false;
|
|
1776
1891
|
this.approved.splice(i, 1);
|
|
1777
1892
|
return true;
|
|
@@ -1788,12 +1903,17 @@ function evmCallMatcher(expected) {
|
|
|
1788
1903
|
const data = expected.data && expected.data !== "0x" ? lc2(expected.data) : void 0;
|
|
1789
1904
|
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;
|
|
1790
1905
|
}
|
|
1906
|
+
function typedDataMatcher(expected) {
|
|
1907
|
+
const hash = lc2(expected.typedDataHash);
|
|
1908
|
+
return (op) => op.kind === "evm-typed-data" && op.caip2 === expected.caip2 && lc2(op.typedDataHash) === hash;
|
|
1909
|
+
}
|
|
1791
1910
|
function chainTxMatcher(expected) {
|
|
1792
1911
|
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 ?? "");
|
|
1793
1912
|
}
|
|
1794
1913
|
|
|
1795
1914
|
// src/swap.ts
|
|
1796
1915
|
var import_protocol_swap2_core = require("@anywalletkit/protocol-swap2-core");
|
|
1916
|
+
var import_wallet_evm = require("@anywalletkit/wallet-evm");
|
|
1797
1917
|
async function quoteRoutes(providers, params, options = {}) {
|
|
1798
1918
|
const timeoutMs = options.timeoutMs ?? 2e4;
|
|
1799
1919
|
const from = { chain: params.sourceChain, token: params.sourceToken };
|
|
@@ -1899,7 +2019,8 @@ function swapApprovalPayload(plan, meta) {
|
|
|
1899
2019
|
source = { kind: "serialized" };
|
|
1900
2020
|
checks.push({ status: "ok", label: "Balance changes are proven before signing (SDK guard)" });
|
|
1901
2021
|
} else {
|
|
1902
|
-
|
|
2022
|
+
const typedData = parseTypedOrder(s.typedData);
|
|
2023
|
+
source = { kind: "typed-order", typedData, text: JSON.stringify(typedData, null, 2) };
|
|
1903
2024
|
checks.push({ status: "ok", label: "Signed order \u2014 no transaction is sent from your wallet" });
|
|
1904
2025
|
}
|
|
1905
2026
|
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" });
|
|
@@ -1924,6 +2045,11 @@ function swapApprovalPayload(plan, meta) {
|
|
|
1924
2045
|
checks
|
|
1925
2046
|
};
|
|
1926
2047
|
}
|
|
2048
|
+
function parseTypedOrder(json) {
|
|
2049
|
+
const t = JSON.parse(json);
|
|
2050
|
+
if (!t || typeof t !== "object" || !t.domain || typeof t.domain !== "object" || !t.types || typeof t.types !== "object" || !t.message || typeof t.message !== "object") throw new Error("The swap order is not EIP-712 typed data.");
|
|
2051
|
+
return t;
|
|
2052
|
+
}
|
|
1927
2053
|
function planMatchers(plan) {
|
|
1928
2054
|
const m = plan.approvals.map((a) => evmCallMatcher({ caip2: plan.fromChain, to: a.token, data: encodeApproveCalldata(a.spender, a.amount), value: 0n }));
|
|
1929
2055
|
const s = plan.source;
|
|
@@ -1938,16 +2064,18 @@ function planMatchers(plan) {
|
|
|
1938
2064
|
} else if (s.kind === "serialized") {
|
|
1939
2065
|
m.push((op) => op.kind === "solana-serialized" && op.serializedTx === s.serializedTx);
|
|
1940
2066
|
} else {
|
|
1941
|
-
m.push((
|
|
2067
|
+
m.push(typedDataMatcher({ caip2: plan.fromChain, typedDataHash: (0, import_wallet_evm.typedDataHash)(parseTypedOrder(s.typedData)) }));
|
|
1942
2068
|
}
|
|
1943
2069
|
return m;
|
|
1944
2070
|
}
|
|
1945
2071
|
function reviewSwapPlan(approvals, gate, meta) {
|
|
2072
|
+
if (typeof meta.account !== "string" || !meta.account) throw new TypeError("reviewSwapPlan needs meta.account: the gate passes the plan for that account only.");
|
|
1946
2073
|
return async (plan) => {
|
|
2074
|
+
const matchers = planMatchers(plan);
|
|
1947
2075
|
const payload = swapApprovalPayload(plan, meta);
|
|
1948
2076
|
await approvals.request({ origin: meta.origin, chain: plan.fromChain, kind: "swap", payload });
|
|
1949
2077
|
gate.clear();
|
|
1950
|
-
gate.preApprove(
|
|
2078
|
+
gate.preApprove(matchers, Math.max(6e4, plan.validUntilMs - Date.now() + 5 * 6e4), meta.account);
|
|
1951
2079
|
};
|
|
1952
2080
|
}
|
|
1953
2081
|
|
|
@@ -1957,11 +2085,13 @@ var ACTIVE_STATUSES = /* @__PURE__ */ new Set(["pending", "action-required", "re
|
|
|
1957
2085
|
function parseSwapId(id) {
|
|
1958
2086
|
if (!id.startsWith(`${import_protocol_swap2_core2.SWAP2_ID_PREFIX}.`)) return null;
|
|
1959
2087
|
const parts = id.slice(import_protocol_swap2_core2.SWAP2_ID_PREFIX.length + 1).split(".");
|
|
1960
|
-
if (parts.length < 2 || parts.length >
|
|
2088
|
+
if (parts.length < 2 || parts.length > 4 || !parts[0] || !parts[1]) return null;
|
|
1961
2089
|
const provider = decodeURIComponent(parts[0]);
|
|
1962
2090
|
const intentId = decodeURIComponent(parts[1]);
|
|
1963
2091
|
const sourceTxHash = parts[2] ? decodeURIComponent(parts[2]) : void 0;
|
|
1964
|
-
|
|
2092
|
+
const destTxHash = parts[3] ? decodeURIComponent(parts[3]) : void 0;
|
|
2093
|
+
if (destTxHash && !sourceTxHash) return null;
|
|
2094
|
+
return { provider, intentId, ...sourceTxHash ? { sourceTxHash } : {}, ...destTxHash ? { destTxHash } : {} };
|
|
1965
2095
|
}
|
|
1966
2096
|
var SwapsStore = class {
|
|
1967
2097
|
constructor(storage) {
|
|
@@ -1970,9 +2100,14 @@ var SwapsStore = class {
|
|
|
1970
2100
|
storage;
|
|
1971
2101
|
_records = null;
|
|
1972
2102
|
_listeners = /* @__PURE__ */ new Set();
|
|
2103
|
+
/**
|
|
2104
|
+
* add / update / remove run here one after another, each reading the list inside its turn: an
|
|
2105
|
+
* alarm tick's update that read the list before a new swap was added would otherwise write the
|
|
2106
|
+
* old list back and drop the swap.
|
|
2107
|
+
*/
|
|
2108
|
+
_queue = Promise.resolve();
|
|
1973
2109
|
async list() {
|
|
1974
|
-
|
|
1975
|
-
return [...this._records].sort((a, b) => b.createdAt - a.createdAt);
|
|
2110
|
+
return [...await this._all()].sort((a, b) => b.createdAt - a.createdAt);
|
|
1976
2111
|
}
|
|
1977
2112
|
async get(id) {
|
|
1978
2113
|
return (await this.list()).find((r) => r.id === id) ?? null;
|
|
@@ -1980,26 +2115,41 @@ var SwapsStore = class {
|
|
|
1980
2115
|
async active() {
|
|
1981
2116
|
return (await this.list()).filter((r) => ACTIVE_STATUSES.has(r.status));
|
|
1982
2117
|
}
|
|
1983
|
-
|
|
1984
|
-
|
|
1985
|
-
|
|
2118
|
+
add(record) {
|
|
2119
|
+
return this._serial(async () => {
|
|
2120
|
+
const all = (await this._all()).filter((r) => r.id !== record.id);
|
|
2121
|
+
await this.write([...all, record]);
|
|
2122
|
+
});
|
|
1986
2123
|
}
|
|
1987
|
-
|
|
1988
|
-
|
|
1989
|
-
|
|
1990
|
-
|
|
1991
|
-
|
|
1992
|
-
|
|
1993
|
-
|
|
1994
|
-
|
|
2124
|
+
update(id, patch) {
|
|
2125
|
+
return this._serial(async () => {
|
|
2126
|
+
const all = [...await this._all()];
|
|
2127
|
+
const i = all.findIndex((r) => r.id === id);
|
|
2128
|
+
if (i < 0) return null;
|
|
2129
|
+
const next = { ...all[i], ...patch, updatedAt: patch.updatedAt ?? Date.now() };
|
|
2130
|
+
all[i] = next;
|
|
2131
|
+
await this.write(all);
|
|
2132
|
+
return next;
|
|
2133
|
+
});
|
|
1995
2134
|
}
|
|
1996
|
-
|
|
1997
|
-
|
|
2135
|
+
remove(id) {
|
|
2136
|
+
return this._serial(async () => {
|
|
2137
|
+
await this.write((await this._all()).filter((r) => r.id !== id));
|
|
2138
|
+
});
|
|
1998
2139
|
}
|
|
1999
2140
|
onChange(listener) {
|
|
2000
2141
|
this._listeners.add(listener);
|
|
2001
2142
|
return () => this._listeners.delete(listener);
|
|
2002
2143
|
}
|
|
2144
|
+
async _all() {
|
|
2145
|
+
this._records ??= await this.storage.get() ?? [];
|
|
2146
|
+
return this._records;
|
|
2147
|
+
}
|
|
2148
|
+
_serial(fn) {
|
|
2149
|
+
const run = this._queue.then(fn, fn);
|
|
2150
|
+
this._queue = run.then(() => void 0, () => void 0);
|
|
2151
|
+
return run;
|
|
2152
|
+
}
|
|
2003
2153
|
async write(records) {
|
|
2004
2154
|
await this.storage.set(records);
|
|
2005
2155
|
this._records = records;
|
|
@@ -2074,8 +2224,9 @@ function earnApprovalPayload(plan, meta) {
|
|
|
2074
2224
|
return { module: meta.module, action: meta.action, network: meta.network, facts: meta.facts, approvals: plan.approvals, tx: plan.tx, checks, preview: meta.preview ?? null };
|
|
2075
2225
|
}
|
|
2076
2226
|
async function reviewEarnPlan(approvals, gate, meta, plan) {
|
|
2227
|
+
if (typeof meta.account !== "string" || !meta.account) throw new TypeError("reviewEarnPlan needs meta.account: the gate passes the plan for that account only.");
|
|
2077
2228
|
await approvals.request({ origin: meta.origin, chain: meta.network.caip2, kind: "earn", payload: earnApprovalPayload(plan, meta) });
|
|
2078
|
-
gate.preApprove(earnPlanMatchers(meta.network, plan), 10 * 6e4);
|
|
2229
|
+
gate.preApprove(earnPlanMatchers(meta.network, plan), 10 * 6e4, meta.account);
|
|
2079
2230
|
}
|
|
2080
2231
|
|
|
2081
2232
|
// src/tonconnect.ts
|
|
@@ -2236,6 +2387,14 @@ var PhishingDetector = class {
|
|
|
2236
2387
|
}
|
|
2237
2388
|
return { host, blocked: false, warnings };
|
|
2238
2389
|
}
|
|
2390
|
+
/**
|
|
2391
|
+
* Why `hostOrOrigin` must be refused outright, or `null`: a host on the blocklist (or under a
|
|
2392
|
+
* listed domain) and not on the allowlist. Lookalikes and punycode only warn ({@link check}).
|
|
2393
|
+
* Wire it as `ApprovalQueueOptions.refuse` and `RouterDeps.refuse`.
|
|
2394
|
+
*/
|
|
2395
|
+
refusal(hostOrOrigin) {
|
|
2396
|
+
return this.check(hostOrOrigin).blocked ? "This site is on the wallet's phishing list; the wallet refuses its requests." : null;
|
|
2397
|
+
}
|
|
2239
2398
|
/**
|
|
2240
2399
|
* `metamask.io-login.com`, `app-uniswap.com`, `uniswap.org.evil.net`: a known site's name used as (part of) a label
|
|
2241
2400
|
* of a host that is not that site. Names shorter than 5 characters are skipped (too many honest matches).
|