@anywalletkit/extension-kit 0.1.5 → 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 +3 -3
- package/dist/index.cjs +214 -60
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +80 -11
- package/dist/index.d.ts +80 -11
- package/dist/index.js +214 -60
- package/dist/index.js.map +1 -1
- package/package.json +8 -8
package/README.md
CHANGED
|
@@ -43,10 +43,10 @@ pnpm add @anywalletkit/extension-kit @anywalletkit/core @anywalletkit/keyring
|
|
|
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) |
|
|
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
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), fuzzy lookalikes, brand-in-label, punycode |
|
|
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 |
|
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,
|
|
@@ -140,6 +141,7 @@ __export(src_exports, {
|
|
|
140
141
|
tonConnectErrorCode: () => tonConnectErrorCode,
|
|
141
142
|
tonProofMessage: () => tonProofMessage,
|
|
142
143
|
tonSignDataMessage: () => tonSignDataMessage,
|
|
144
|
+
typedDataMatcher: () => typedDataMatcher,
|
|
143
145
|
vaultSecretStorage: () => vaultSecretStorage,
|
|
144
146
|
verifyAssertion: () => verifyAssertion
|
|
145
147
|
});
|
|
@@ -180,6 +182,14 @@ function toArrayBuffer(bytes) {
|
|
|
180
182
|
copy.set(bytes);
|
|
181
183
|
return copy.buffer;
|
|
182
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
|
+
}
|
|
183
193
|
|
|
184
194
|
// src/vault.ts
|
|
185
195
|
var VAULT_MIN_ITERATIONS = 6e5;
|
|
@@ -447,7 +457,13 @@ var Vault = class {
|
|
|
447
457
|
}
|
|
448
458
|
async _derivePbkdf2(password, salt, iterations) {
|
|
449
459
|
const subtle = this._crypto.subtle;
|
|
450
|
-
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
|
+
}
|
|
451
467
|
return subtle.deriveKey({ name: "PBKDF2", hash: "SHA-256", salt: toArrayBuffer(salt), iterations }, material, { name: "AES-GCM", length: 256 }, false, ["encrypt", "decrypt"]);
|
|
452
468
|
}
|
|
453
469
|
async _deriveScrypt(password, salt, cost) {
|
|
@@ -459,7 +475,7 @@ var Vault = class {
|
|
|
459
475
|
(0, import_core.zeroize)(pw);
|
|
460
476
|
}
|
|
461
477
|
try {
|
|
462
|
-
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"]));
|
|
463
479
|
} finally {
|
|
464
480
|
(0, import_core.zeroize)(raw);
|
|
465
481
|
}
|
|
@@ -467,16 +483,16 @@ var Vault = class {
|
|
|
467
483
|
/** HKDF-SHA256(ikm = PRF output, salt = credential id bytes, info) → AES-256-GCM key. */
|
|
468
484
|
async _derivePasskeyKek(prf, credentialId) {
|
|
469
485
|
const subtle = this._crypto.subtle;
|
|
470
|
-
const ikm = await subtle.importKey("raw",
|
|
486
|
+
const ikm = await withSecretBuffer(prf, (b) => subtle.importKey("raw", b, "HKDF", false, ["deriveKey"]));
|
|
471
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"]);
|
|
472
488
|
}
|
|
473
489
|
async _importDek(dek) {
|
|
474
490
|
if (dek.length !== DEK_BYTES) throw new VaultError("Vault key must be 32 bytes.", "invalid-input");
|
|
475
|
-
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"]));
|
|
476
492
|
}
|
|
477
493
|
async _seal(key, plaintext, aad) {
|
|
478
494
|
const iv = this._random(IV_BYTES);
|
|
479
|
-
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));
|
|
480
496
|
return { iv: toBase64(iv), data: toBase64(new Uint8Array(ct)) };
|
|
481
497
|
}
|
|
482
498
|
async _open(key, box, aad, onFail) {
|
|
@@ -661,22 +677,40 @@ var Session = class {
|
|
|
661
677
|
if (was !== void 0 && was !== null) for (const cb of this._onLock) cb();
|
|
662
678
|
}
|
|
663
679
|
_expiry() {
|
|
664
|
-
return this._autoLockMs > 0 ? this._now() + this._autoLockMs :
|
|
680
|
+
return this._autoLockMs > 0 ? this._now() + this._autoLockMs : null;
|
|
665
681
|
}
|
|
666
682
|
async _arm(expiresAt) {
|
|
667
|
-
if (
|
|
683
|
+
if (expiresAt !== null) await this._alarms.schedule(this._alarm, expiresAt);
|
|
668
684
|
else await this._alarms.clear(this._alarm);
|
|
669
685
|
}
|
|
686
|
+
/** The stored record, or `null` when locked. A record that is there but unreadable locks the wallet (removed, listeners told). */
|
|
670
687
|
async _read() {
|
|
671
688
|
const raw = await this._storage.get(this._key);
|
|
672
|
-
if (
|
|
673
|
-
|
|
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)) {
|
|
674
693
|
await this._lockNow();
|
|
675
694
|
return null;
|
|
676
695
|
}
|
|
677
|
-
|
|
696
|
+
if (r.expiresAt !== null && r.expiresAt <= this._now()) {
|
|
697
|
+
await this._lockNow();
|
|
698
|
+
return null;
|
|
699
|
+
}
|
|
700
|
+
return r;
|
|
678
701
|
}
|
|
679
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
|
+
}
|
|
680
714
|
function memorySessionStorage() {
|
|
681
715
|
const data = /* @__PURE__ */ new Map();
|
|
682
716
|
return {
|
|
@@ -985,14 +1019,27 @@ var NetworksTable = class {
|
|
|
985
1019
|
return this._loading;
|
|
986
1020
|
}
|
|
987
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
|
+
}
|
|
988
1035
|
function parseAddChainParams(params, known = isCatalogChain) {
|
|
989
1036
|
const p = Array.isArray(params) ? params[0] : params;
|
|
990
1037
|
if (!p || typeof p !== "object") return relayError(RELAY_ERRORS.invalidParams, "wallet_addEthereumChain expects one object parameter.");
|
|
991
1038
|
const caip2 = caip2FromChainId(p.chainId);
|
|
992
1039
|
if (!caip2) return relayError(RELAY_ERRORS.invalidParams, "chainId must be a 0x-prefixed hex string.");
|
|
993
1040
|
if (!known(caip2)) return relayError(RELAY_ERRORS.chainNotAdded, `Chain ${String(p.chainId)} is not in this wallet's catalog.`);
|
|
994
|
-
const rpcs = Array.isArray(p.rpcUrls) ? p.rpcUrls.filter((u) => typeof u === "string" && /^https:\/\/[^\s/?#]+/i.test(u)) : [];
|
|
995
|
-
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).");
|
|
996
1043
|
const name = typeof p.chainName === "string" && p.chainName.trim() ? p.chainName.trim().slice(0, 64) : caip2;
|
|
997
1044
|
const currency = p.nativeCurrency;
|
|
998
1045
|
if (currency && currency.decimals !== void 0 && currency.decimals !== 18) return relayError(RELAY_ERRORS.invalidParams, "nativeCurrency.decimals must be 18.");
|
|
@@ -1189,6 +1236,13 @@ var QueueLimitError = class extends Error {
|
|
|
1189
1236
|
}
|
|
1190
1237
|
code;
|
|
1191
1238
|
};
|
|
1239
|
+
var OriginRefused = class extends Error {
|
|
1240
|
+
code = RELAY_ERRORS.unauthorized;
|
|
1241
|
+
constructor(message) {
|
|
1242
|
+
super(message);
|
|
1243
|
+
this.name = "OriginRefused";
|
|
1244
|
+
}
|
|
1245
|
+
};
|
|
1192
1246
|
var APPROVALS_MAX_PENDING_PER_ORIGIN = 3;
|
|
1193
1247
|
var ApprovalQueue = class {
|
|
1194
1248
|
constructor(window, newId = () => crypto.randomUUID(), options = {}) {
|
|
@@ -1208,6 +1262,13 @@ var ApprovalQueue = class {
|
|
|
1208
1262
|
_via = /* @__PURE__ */ new Map();
|
|
1209
1263
|
/** Enqueue and wait for the user's answer. Refuses (never queues) past the per-origin limits. */
|
|
1210
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));
|
|
1211
1272
|
const sameOrigin = [...this._current ? [this._current] : [], ...this._waiting].filter((p) => p.request.origin === input.origin);
|
|
1212
1273
|
if (input.kind === "connect" && sameOrigin.some((p) => p.request.kind === "connect")) {
|
|
1213
1274
|
return Promise.reject(new QueueLimitError(RELAY_ERRORS.resourceUnavailable, "A connect request from this site is already waiting for your answer."));
|
|
@@ -1396,6 +1457,8 @@ var RequestRouter = class {
|
|
|
1396
1457
|
return await this.deps.networks.get(wanted) ?? await this.deps.networks.get(DEFAULT_CHAIN);
|
|
1397
1458
|
}
|
|
1398
1459
|
async handle(origin, chain, method, rawParams) {
|
|
1460
|
+
const refused = this.deps.refuse?.(origin);
|
|
1461
|
+
if (refused) throw new RouterError(RELAY_ERRORS.unauthorized, refused);
|
|
1399
1462
|
if (chain !== "eip155") throw new RouterError(RELAY_ERRORS.unsupportedMethod, `${chain} providers are not available yet.`);
|
|
1400
1463
|
const params = Array.isArray(rawParams) ? rawParams : rawParams === void 0 ? [] : [rawParams];
|
|
1401
1464
|
const network = await this.chainFor(origin);
|
|
@@ -1704,6 +1767,23 @@ var bigintSafe = (_k, v) => typeof v === "bigint" ? v.toString() : v;
|
|
|
1704
1767
|
var hex = (n) => `0x${n.toString(16)}`;
|
|
1705
1768
|
var big = (v) => BigInt(v);
|
|
1706
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
|
+
}
|
|
1707
1787
|
async function prepareTransaction(deps, network, from, tx) {
|
|
1708
1788
|
const call = { from, to: tx.to, value: hex(BigInt(tx.value)), ...tx.data ? { data: tx.data } : {} };
|
|
1709
1789
|
const [nonceHex, block, gasHex] = await Promise.all([
|
|
@@ -1713,18 +1793,20 @@ async function prepareTransaction(deps, network, from, tx) {
|
|
|
1713
1793
|
]);
|
|
1714
1794
|
const estimated = big(gasHex);
|
|
1715
1795
|
const gasLimit = tx.gas ? estimated : (estimated * 120n + 99n) / 100n;
|
|
1716
|
-
|
|
1717
|
-
if (base === null) {
|
|
1718
|
-
const gasPrice = big(await deps.rpc(network, "eth_gasPrice", []));
|
|
1719
|
-
return { chainId: network.chainId, nonce: safeNonce(big(nonceHex)), gasLimit: hex(gasLimit), gasPrice: hex(gasPrice), maxFee: (gasLimit * gasPrice).toString() };
|
|
1720
|
-
}
|
|
1721
|
-
const floor = deps.tipFloor?.(network.chainId) ?? MIN_TIP_WEI;
|
|
1722
|
-
const nodeTip = await deps.rpc(network, "eth_maxPriorityFeePerGas", []).then(big).catch(() => 0n);
|
|
1723
|
-
const tip = nodeTip > floor ? nodeTip : floor;
|
|
1724
|
-
const maxFeePerGas = base * 2n + tip;
|
|
1725
|
-
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));
|
|
1726
1797
|
}
|
|
1727
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
|
+
};
|
|
1728
1810
|
return async ({ origin, network, method, params, accounts }) => {
|
|
1729
1811
|
const via = deps.approvals.viaFor(origin);
|
|
1730
1812
|
const info = classifySignRequest({ network, method, params, accounts, origin, ...via ? { via } : {} });
|
|
@@ -1733,6 +1815,41 @@ function createSignHandler(deps) {
|
|
|
1733
1815
|
const preview = deps.preview ? await deps.preview({ network, request: info, accounts }).catch(() => null) : null;
|
|
1734
1816
|
const payload = { request: info, network, ...prepared ? { prepared } : {}, preview };
|
|
1735
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
|
+
}
|
|
1736
1853
|
const handle = await deps.signerFor(info.account, network);
|
|
1737
1854
|
try {
|
|
1738
1855
|
switch (info.kind) {
|
|
@@ -1740,19 +1857,6 @@ function createSignHandler(deps) {
|
|
|
1740
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);
|
|
1741
1858
|
case "sign-typed-data":
|
|
1742
1859
|
return await handle.signer.signTypedData(info.typedData);
|
|
1743
|
-
case "send-transaction": {
|
|
1744
|
-
const p = prepared;
|
|
1745
|
-
const raw = await handle.signer.signTransaction({
|
|
1746
|
-
chainId: p.chainId,
|
|
1747
|
-
nonce: p.nonce,
|
|
1748
|
-
to: info.tx.to,
|
|
1749
|
-
value: BigInt(info.tx.value),
|
|
1750
|
-
data: info.tx.data ?? "0x",
|
|
1751
|
-
gasLimit: BigInt(p.gasLimit),
|
|
1752
|
-
...p.gasPrice !== void 0 ? { gasPrice: BigInt(p.gasPrice) } : { maxFeePerGas: BigInt(p.maxFeePerGas), maxPriorityFeePerGas: BigInt(p.maxPriorityFeePerGas) }
|
|
1753
|
-
});
|
|
1754
|
-
return await deps.rpc(network, "eth_sendRawTransaction", [raw]);
|
|
1755
|
-
}
|
|
1756
1860
|
}
|
|
1757
1861
|
} finally {
|
|
1758
1862
|
handle.dispose();
|
|
@@ -1761,22 +1865,28 @@ function createSignHandler(deps) {
|
|
|
1761
1865
|
}
|
|
1762
1866
|
|
|
1763
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();
|
|
1764
1870
|
var ReviewGate = class {
|
|
1765
1871
|
constructor(now = Date.now) {
|
|
1766
1872
|
this.now = now;
|
|
1767
1873
|
}
|
|
1768
1874
|
now;
|
|
1769
1875
|
approved = [];
|
|
1770
|
-
/**
|
|
1771
|
-
|
|
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) {
|
|
1772
1882
|
const until = this.now() + ttlMs;
|
|
1773
|
-
this.approved.push(...matchers.map((match) => ({ match, until })));
|
|
1883
|
+
this.approved.push(...matchers.map((match) => ({ match, until, ...account ? { account } : {} })));
|
|
1774
1884
|
}
|
|
1775
1885
|
/** True (and the matcher is spent) when a pre-approval covers `op`. */
|
|
1776
1886
|
consume(op) {
|
|
1777
1887
|
const now = this.now();
|
|
1778
1888
|
this.approved = this.approved.filter((a) => a.until > now);
|
|
1779
|
-
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));
|
|
1780
1890
|
if (i < 0) return false;
|
|
1781
1891
|
this.approved.splice(i, 1);
|
|
1782
1892
|
return true;
|
|
@@ -1793,12 +1903,17 @@ function evmCallMatcher(expected) {
|
|
|
1793
1903
|
const data = expected.data && expected.data !== "0x" ? lc2(expected.data) : void 0;
|
|
1794
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;
|
|
1795
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
|
+
}
|
|
1796
1910
|
function chainTxMatcher(expected) {
|
|
1797
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 ?? "");
|
|
1798
1912
|
}
|
|
1799
1913
|
|
|
1800
1914
|
// src/swap.ts
|
|
1801
1915
|
var import_protocol_swap2_core = require("@anywalletkit/protocol-swap2-core");
|
|
1916
|
+
var import_wallet_evm = require("@anywalletkit/wallet-evm");
|
|
1802
1917
|
async function quoteRoutes(providers, params, options = {}) {
|
|
1803
1918
|
const timeoutMs = options.timeoutMs ?? 2e4;
|
|
1804
1919
|
const from = { chain: params.sourceChain, token: params.sourceToken };
|
|
@@ -1904,7 +2019,8 @@ function swapApprovalPayload(plan, meta) {
|
|
|
1904
2019
|
source = { kind: "serialized" };
|
|
1905
2020
|
checks.push({ status: "ok", label: "Balance changes are proven before signing (SDK guard)" });
|
|
1906
2021
|
} else {
|
|
1907
|
-
|
|
2022
|
+
const typedData = parseTypedOrder(s.typedData);
|
|
2023
|
+
source = { kind: "typed-order", typedData, text: JSON.stringify(typedData, null, 2) };
|
|
1908
2024
|
checks.push({ status: "ok", label: "Signed order \u2014 no transaction is sent from your wallet" });
|
|
1909
2025
|
}
|
|
1910
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" });
|
|
@@ -1929,6 +2045,11 @@ function swapApprovalPayload(plan, meta) {
|
|
|
1929
2045
|
checks
|
|
1930
2046
|
};
|
|
1931
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
|
+
}
|
|
1932
2053
|
function planMatchers(plan) {
|
|
1933
2054
|
const m = plan.approvals.map((a) => evmCallMatcher({ caip2: plan.fromChain, to: a.token, data: encodeApproveCalldata(a.spender, a.amount), value: 0n }));
|
|
1934
2055
|
const s = plan.source;
|
|
@@ -1943,16 +2064,18 @@ function planMatchers(plan) {
|
|
|
1943
2064
|
} else if (s.kind === "serialized") {
|
|
1944
2065
|
m.push((op) => op.kind === "solana-serialized" && op.serializedTx === s.serializedTx);
|
|
1945
2066
|
} else {
|
|
1946
|
-
m.push((
|
|
2067
|
+
m.push(typedDataMatcher({ caip2: plan.fromChain, typedDataHash: (0, import_wallet_evm.typedDataHash)(parseTypedOrder(s.typedData)) }));
|
|
1947
2068
|
}
|
|
1948
2069
|
return m;
|
|
1949
2070
|
}
|
|
1950
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.");
|
|
1951
2073
|
return async (plan) => {
|
|
2074
|
+
const matchers = planMatchers(plan);
|
|
1952
2075
|
const payload = swapApprovalPayload(plan, meta);
|
|
1953
2076
|
await approvals.request({ origin: meta.origin, chain: plan.fromChain, kind: "swap", payload });
|
|
1954
2077
|
gate.clear();
|
|
1955
|
-
gate.preApprove(
|
|
2078
|
+
gate.preApprove(matchers, Math.max(6e4, plan.validUntilMs - Date.now() + 5 * 6e4), meta.account);
|
|
1956
2079
|
};
|
|
1957
2080
|
}
|
|
1958
2081
|
|
|
@@ -1962,11 +2085,13 @@ var ACTIVE_STATUSES = /* @__PURE__ */ new Set(["pending", "action-required", "re
|
|
|
1962
2085
|
function parseSwapId(id) {
|
|
1963
2086
|
if (!id.startsWith(`${import_protocol_swap2_core2.SWAP2_ID_PREFIX}.`)) return null;
|
|
1964
2087
|
const parts = id.slice(import_protocol_swap2_core2.SWAP2_ID_PREFIX.length + 1).split(".");
|
|
1965
|
-
if (parts.length < 2 || parts.length >
|
|
2088
|
+
if (parts.length < 2 || parts.length > 4 || !parts[0] || !parts[1]) return null;
|
|
1966
2089
|
const provider = decodeURIComponent(parts[0]);
|
|
1967
2090
|
const intentId = decodeURIComponent(parts[1]);
|
|
1968
2091
|
const sourceTxHash = parts[2] ? decodeURIComponent(parts[2]) : void 0;
|
|
1969
|
-
|
|
2092
|
+
const destTxHash = parts[3] ? decodeURIComponent(parts[3]) : void 0;
|
|
2093
|
+
if (destTxHash && !sourceTxHash) return null;
|
|
2094
|
+
return { provider, intentId, ...sourceTxHash ? { sourceTxHash } : {}, ...destTxHash ? { destTxHash } : {} };
|
|
1970
2095
|
}
|
|
1971
2096
|
var SwapsStore = class {
|
|
1972
2097
|
constructor(storage) {
|
|
@@ -1975,9 +2100,14 @@ var SwapsStore = class {
|
|
|
1975
2100
|
storage;
|
|
1976
2101
|
_records = null;
|
|
1977
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();
|
|
1978
2109
|
async list() {
|
|
1979
|
-
|
|
1980
|
-
return [...this._records].sort((a, b) => b.createdAt - a.createdAt);
|
|
2110
|
+
return [...await this._all()].sort((a, b) => b.createdAt - a.createdAt);
|
|
1981
2111
|
}
|
|
1982
2112
|
async get(id) {
|
|
1983
2113
|
return (await this.list()).find((r) => r.id === id) ?? null;
|
|
@@ -1985,26 +2115,41 @@ var SwapsStore = class {
|
|
|
1985
2115
|
async active() {
|
|
1986
2116
|
return (await this.list()).filter((r) => ACTIVE_STATUSES.has(r.status));
|
|
1987
2117
|
}
|
|
1988
|
-
|
|
1989
|
-
|
|
1990
|
-
|
|
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
|
+
});
|
|
1991
2123
|
}
|
|
1992
|
-
|
|
1993
|
-
|
|
1994
|
-
|
|
1995
|
-
|
|
1996
|
-
|
|
1997
|
-
|
|
1998
|
-
|
|
1999
|
-
|
|
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
|
+
});
|
|
2000
2134
|
}
|
|
2001
|
-
|
|
2002
|
-
|
|
2135
|
+
remove(id) {
|
|
2136
|
+
return this._serial(async () => {
|
|
2137
|
+
await this.write((await this._all()).filter((r) => r.id !== id));
|
|
2138
|
+
});
|
|
2003
2139
|
}
|
|
2004
2140
|
onChange(listener) {
|
|
2005
2141
|
this._listeners.add(listener);
|
|
2006
2142
|
return () => this._listeners.delete(listener);
|
|
2007
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
|
+
}
|
|
2008
2153
|
async write(records) {
|
|
2009
2154
|
await this.storage.set(records);
|
|
2010
2155
|
this._records = records;
|
|
@@ -2079,8 +2224,9 @@ function earnApprovalPayload(plan, meta) {
|
|
|
2079
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 };
|
|
2080
2225
|
}
|
|
2081
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.");
|
|
2082
2228
|
await approvals.request({ origin: meta.origin, chain: meta.network.caip2, kind: "earn", payload: earnApprovalPayload(plan, meta) });
|
|
2083
|
-
gate.preApprove(earnPlanMatchers(meta.network, plan), 10 * 6e4);
|
|
2229
|
+
gate.preApprove(earnPlanMatchers(meta.network, plan), 10 * 6e4, meta.account);
|
|
2084
2230
|
}
|
|
2085
2231
|
|
|
2086
2232
|
// src/tonconnect.ts
|
|
@@ -2241,6 +2387,14 @@ var PhishingDetector = class {
|
|
|
2241
2387
|
}
|
|
2242
2388
|
return { host, blocked: false, warnings };
|
|
2243
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
|
+
}
|
|
2244
2398
|
/**
|
|
2245
2399
|
* `metamask.io-login.com`, `app-uniswap.com`, `uniswap.org.evil.net`: a known site's name used as (part of) a label
|
|
2246
2400
|
* of a host that is not that site. Names shorter than 5 characters are skipped (too many honest matches).
|