- Fix pollVerifiedWallets: route import wallets to /wallet endpoint and reconstruct running-balance ledger - Fix generateRandomAddress: append 'IMPORT' suffix to prevent fake EVM addresses - Add import chain: CSV upload via multipart, IMPORT_ONLY wallet registration - Add HYPE summary cards with holdings, PnL, avg buy price, total invested - Add BTC and SATS to TOKENS config - Add backend DELETE on wallet removal (skip for import wallets) - Fix chain name: hyperliquide → hyperevm (chainId 999) - Add HSTS and security headers to nginx config
316 lines
9.3 KiB
JavaScript
316 lines
9.3 KiB
JavaScript
/**
|
|
* Wallet Verifier — EIP-712 signature verification via window.ethereum.
|
|
*
|
|
* Orchestrates connection, typed-data signing, and state persistence.
|
|
* Designed to work alongside WalletManager.
|
|
*
|
|
* Usage:
|
|
* import { WalletVerifier } from './verifier.js';
|
|
* const verifier = new WalletVerifier(walletManagerInstance);
|
|
* await verifier.triggerWalletVerification('base', 'My Wallet');
|
|
*/
|
|
|
|
import { WalletManager } from './wallets.js';
|
|
|
|
/* ------------------------------------------------------------------ */
|
|
/* EIP-712 Domain */
|
|
/* ------------------------------------------------------------------ */
|
|
|
|
/* Chain name → numeric chainId for EIP-712 domain */
|
|
const CHAIN_IDS = {
|
|
ethereum: 1,
|
|
base: 8453,
|
|
arbitrum: 42161,
|
|
optimism: 10,
|
|
polygon: 137,
|
|
avalanche: 43114,
|
|
bsc: 56,
|
|
fantom: 250,
|
|
hyperevm: 999,
|
|
};
|
|
|
|
/**
|
|
* Build the EIP-712 domain for a given chain.
|
|
* @param {string} chain — chain name
|
|
* @returns {object} Domain object
|
|
*/
|
|
function buildEIP712Domain(chain) {
|
|
const normalChain = String(chain).toLowerCase();
|
|
const chainId = CHAIN_IDS[normalChain] ?? 1;
|
|
return {
|
|
name: 'Anonymous Wallet Tracker',
|
|
version: '1',
|
|
chainId,
|
|
};
|
|
}
|
|
|
|
const EIP712_TYPES = {
|
|
VerifyTracking: [
|
|
{ name: 'action', type: 'string' },
|
|
{ name: 'walletAddress', type: 'address' },
|
|
{ name: 'timestamp', type: 'uint256' },
|
|
],
|
|
};
|
|
|
|
/* ------------------------------------------------------------------ */
|
|
/* Error Codes (EIP-1193) */
|
|
/* ------------------------------------------------------------------ */
|
|
|
|
/* User rejected request */
|
|
const USER_REJECTED_CODE = 4001;
|
|
/* User switched accounts mid-flow */
|
|
const CHAIN_DISCONNECTED_CODE = 4900;
|
|
|
|
/* Human-friendly message map */
|
|
const ERROR_MESSAGES = {
|
|
[USER_REJECTED_CODE]: 'Wallet request rejected. Verification cancelled.',
|
|
[CHAIN_DISCONNECTED_CODE]: 'Wallet disconnected unexpectedly.',
|
|
NO_EXTENSION: 'No Web3 wallet extension found. Install MetaMask or similar.',
|
|
NO_ACCOUNT: 'Could not retrieve an active wallet address.',
|
|
SIGN_FAILED: 'Signature request failed.',
|
|
WALLET_EXISTS: 'This wallet address is already being tracked.',
|
|
};
|
|
|
|
/* ------------------------------------------------------------------ */
|
|
/* Helpers */
|
|
/* ------------------------------------------------------------------ */
|
|
|
|
/**
|
|
* Check if window.ethereum is available and callable.
|
|
*
|
|
* @returns {boolean}
|
|
*/
|
|
function hasEthereumProvider() {
|
|
return (
|
|
typeof window !== 'undefined' &&
|
|
typeof window.ethereum !== 'undefined' &&
|
|
typeof window.ethereum.request === 'function'
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Parse an EIP-1193 provider error into a user-friendly string.
|
|
*
|
|
* @param {Error} err
|
|
* @returns {string}
|
|
*/
|
|
function providerErrorMessage(err) {
|
|
/* MetaMask and compatible providers attach a code property */
|
|
const code = err?.code ?? err?.cause?.code;
|
|
if (code === USER_REJECTED_CODE || code === 4001) {
|
|
return 'Verification cancelled — you rejected the wallet request.';
|
|
}
|
|
if (code === CHAIN_DISCONNECTED_CODE || code === 4900) {
|
|
return 'Wallet connection dropped unexpectedly.';
|
|
}
|
|
return err?.message ?? 'An unexpected error occurred.';
|
|
}
|
|
|
|
/* ------------------------------------------------------------------ */
|
|
/* WalletVerifier */
|
|
/* ------------------------------------------------------------------ */
|
|
|
|
export class WalletVerifier {
|
|
/**
|
|
* @param {WalletManager} walletManager — shared state manager instance
|
|
*/
|
|
constructor(walletManager) {
|
|
if (!(walletManager instanceof WalletManager)) {
|
|
throw new TypeError('WalletVerifier requires a WalletManager instance.');
|
|
}
|
|
|
|
/** @type {WalletManager} */
|
|
this._wm = walletManager;
|
|
}
|
|
|
|
/* ---------------------------------------------------------------- */
|
|
/* Public API */
|
|
/* ---------------------------------------------------------------- */
|
|
|
|
/**
|
|
* Full verification flow: request account → sign EIP-712 → persist.
|
|
*
|
|
* @param {string} chain — chain name for storage (e.g. 'base')
|
|
* @param {string} [nickname=''] — display nickname
|
|
* @returns {Promise<{ success: true, wallet: object } | { success: false, error: string }>}
|
|
*/
|
|
async triggerWalletVerification(chain, nickname = '') {
|
|
/* ---- Step 0: Provider check ---- */
|
|
if (!hasEthereumProvider()) {
|
|
return { success: false, error: ERROR_MESSAGES.NO_EXTENSION };
|
|
}
|
|
|
|
let address;
|
|
let signature;
|
|
|
|
try {
|
|
/* ---- Step 1: Request active account ---- */
|
|
address = await this._requestAccount();
|
|
|
|
/* ---- Step 2: Build EIP-712 payload ---- */
|
|
const typedData = this._buildTypedData(address, chain);
|
|
|
|
/* ---- Step 3: Request signature ---- */
|
|
signature = await this._requestSignature(address, typedData);
|
|
|
|
} catch (err) {
|
|
return { success: false, error: providerErrorMessage(err) };
|
|
}
|
|
|
|
/* ---- Step 4: Store in WalletManager ---- */
|
|
const normalChain = String(chain).toLowerCase();
|
|
const existing = this._wm.findWallet(address, normalChain);
|
|
|
|
if (existing) {
|
|
/* Already tracked — just verify if not yet verified */
|
|
if (!existing.isVerified) {
|
|
const verifyResult = this._wm.verifyWallet(
|
|
address,
|
|
normalChain,
|
|
signature,
|
|
this._buildMessageData(address)
|
|
);
|
|
if (!verifyResult.success) {
|
|
return { success: false, error: verifyResult.error };
|
|
}
|
|
return { success: true, wallet: this._wm.findWallet(address, normalChain) };
|
|
}
|
|
return {
|
|
success: false,
|
|
error: 'This wallet is already verified and being tracked.',
|
|
};
|
|
}
|
|
|
|
/* New wallet — add then verify */
|
|
const addResult = this._wm.addWallet(address, normalChain, nickname);
|
|
if (!addResult.success) {
|
|
return { success: false, error: addResult.error };
|
|
}
|
|
|
|
const verifyResult = this._wm.verifyWallet(
|
|
address,
|
|
normalChain,
|
|
signature,
|
|
this._buildMessageData(address)
|
|
);
|
|
if (!verifyResult.success) {
|
|
return { success: false, error: verifyResult.error };
|
|
}
|
|
|
|
return {
|
|
success: true,
|
|
wallet: this._wm.findWallet(address, normalChain),
|
|
};
|
|
}
|
|
|
|
/* ---------------------------------------------------------------- */
|
|
/* Internal Steps */
|
|
/* ---------------------------------------------------------------- */
|
|
|
|
/**
|
|
* Request `eth_requestAccounts`. Throws on rejection.
|
|
* @returns {Promise<string>} The connected address.
|
|
*/
|
|
async _requestAccount() {
|
|
const accounts = await window.ethereum.request({
|
|
method: 'eth_requestAccounts',
|
|
});
|
|
|
|
if (!accounts || accounts.length === 0) {
|
|
throw new Error(ERROR_MESSAGES.NO_ACCOUNT);
|
|
}
|
|
|
|
return accounts[0].toLowerCase();
|
|
}
|
|
|
|
/**
|
|
* Build the EIP-712 typed data payload.
|
|
*
|
|
* @param {string} address
|
|
* @param {string} chain — chain name for domain chainId
|
|
* @returns {object} Full typedData object (domain + types + message)
|
|
*/
|
|
_buildTypedData(address, chain) {
|
|
const message = this._buildMessageData(address);
|
|
return {
|
|
domain: buildEIP712Domain(chain),
|
|
types: {
|
|
EIP712Domain: [
|
|
{ name: 'name', type: 'string' },
|
|
{ name: 'version', type: 'string' },
|
|
{ name: 'chainId', type: 'uint256' },
|
|
],
|
|
VerifyTracking: EIP712_TYPES.VerifyTracking,
|
|
},
|
|
primaryType: 'VerifyTracking',
|
|
message,
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Build the message portion only (for storage).
|
|
*
|
|
* @param {string} address
|
|
* @returns {object}
|
|
*/
|
|
_buildMessageData(address) {
|
|
return {
|
|
action: 'Verify Ownership for Live Tracking',
|
|
walletAddress: address,
|
|
timestamp: Math.floor(Date.now() / 1000),
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Request `eth_signTypedData_v4`. Throws on rejection.
|
|
*
|
|
* @param {string} address
|
|
* @param {object} typedData
|
|
* @returns {Promise<string>} The signature hex string.
|
|
*/
|
|
async _requestSignature(address, typedData) {
|
|
try {
|
|
const sig = await window.ethereum.request({
|
|
method: 'eth_signTypedData_v4',
|
|
params: [address, JSON.stringify(typedData)],
|
|
});
|
|
|
|
if (!sig || typeof sig !== 'string') {
|
|
throw new Error(ERROR_MESSAGES.SIGN_FAILED);
|
|
}
|
|
|
|
return sig;
|
|
} catch (err) {
|
|
/* Re-throw EIP-1193 code errors for upstream handling */
|
|
if (err?.code === USER_REJECTED_CODE) {
|
|
throw new Error('User rejected the signature request.');
|
|
}
|
|
throw err;
|
|
}
|
|
}
|
|
|
|
/* ---------------------------------------------------------------- */
|
|
/* Quick connect (no signature) */
|
|
/* ---------------------------------------------------------------- */
|
|
|
|
/**
|
|
* Minimal connection check — only requests accounts, does NOT sign.
|
|
* Useful for pre-flight checks or auto-connect on page load.
|
|
*
|
|
* @returns {Promise<string|null>} Connected address, or null on rejection.
|
|
*/
|
|
async connect() {
|
|
if (!hasEthereumProvider()) return null;
|
|
try {
|
|
const accounts = await window.ethereum.request({
|
|
method: 'eth_requestAccounts',
|
|
});
|
|
return accounts?.[0]?.toLowerCase() ?? null;
|
|
} catch {
|
|
return null;
|
|
}
|
|
}
|
|
}
|
|
|
|
export default WalletVerifier;
|