Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 18 additions & 17 deletions src/core/chains.ts
Original file line number Diff line number Diff line change
Expand Up @@ -289,26 +289,25 @@ export const rpcUrlMap: Record<number, string> = {
*/
export function resolveChainId(chainIdentifier: number | string): number {
if (typeof chainIdentifier === 'number') {
if (!chainMap[chainIdentifier]) {
throw new Error(`Unsupported chain ID: ${chainIdentifier}. Use a supported network.`);
}
return chainIdentifier;
}

// Convert to lowercase for case-insensitive matching

const networkName = chainIdentifier.toLowerCase();

// Check if the network name is in our map

const chainId = networkNameMap[networkName];
if (chainId !== undefined) {
return chainId;
}

// Try parsing as a number

const parsedId = parseInt(networkName);
if (!isNaN(parsedId)) {
if (!isNaN(parsedId) && chainMap[parsedId]) {
return parsedId;
}

// Default to mainnet if not found
return DEFAULT_CHAIN_ID;

throw new Error(`Unknown network: "${chainIdentifier}". Supported: ${getSupportedNetworks().slice(0, 10).join(', ')}...`);
}

/**
Expand All @@ -320,17 +319,19 @@ export function resolveChainId(chainIdentifier: number | string): number {
export function getChain(chainIdentifier: number | string = DEFAULT_CHAIN_ID): Chain {
if (typeof chainIdentifier === 'string') {
const networkName = chainIdentifier.toLowerCase();
// Try to get from direct network name mapping first
if (networkNameMap[networkName]) {
return chainMap[networkNameMap[networkName]] || mainnet;
const chain = chainMap[networkNameMap[networkName]];
if (!chain) throw new Error(`Chain config missing for network: ${chainIdentifier}`);
return chain;
}

// If not found, throw an error
throw new Error(`Unsupported network: ${chainIdentifier}`);
}

// If it's a number, return the chain from chainMap
return chainMap[chainIdentifier] || mainnet;

const chain = chainMap[chainIdentifier];
if (!chain) {
throw new Error(`Unsupported chain ID: ${chainIdentifier}`);
}
return chain;
}

/**
Expand Down
29 changes: 25 additions & 4 deletions src/core/services/transfer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,23 @@ import { getPublicClient, getWalletClient } from './clients.js';
import { getChain } from '../chains.js';
import { resolveAddress } from './ens.js';

/**
* Validates that a string represents a positive numeric amount.
* Prevents negative values, non-numeric strings, and excessively large amounts.
*/
function validateAmount(amount: string, label = 'Amount'): void {
if (!/^\d+(\.\d+)?$/.test(amount)) {
throw new Error(`${label} must be a positive number (got: "${amount}")`);
}
const parsed = parseFloat(amount);
if (parsed <= 0) {
throw new Error(`${label} must be greater than zero`);
}
if (parsed > 1e18) {
throw new Error(`${label} exceeds maximum allowed value`);
}
}

// Standard ERC20 ABI for transfers
const erc20TransferAbi = [
{
Expand Down Expand Up @@ -137,6 +154,7 @@ export async function transferETH(
: privateKey as Hex;

const client = getWalletClient(formattedKey, network);
validateAmount(amount, 'ETH amount');
const amountWei = parseEther(amount);

return client.sendTransaction({
Expand Down Expand Up @@ -195,11 +213,12 @@ export async function transferERC20(
const symbol = await contract.read.symbol();

// Parse the amount with the correct number of decimals
validateAmount(amount, 'Token amount');
const rawAmount = parseUnits(amount, decimals);

// Create wallet client for sending the transaction
const walletClient = getWalletClient(formattedKey, network);

// Send the transaction
const hash = await walletClient.writeContract({
address: tokenAddress,
Expand Down Expand Up @@ -271,11 +290,12 @@ export async function approveERC20(
const symbol = await contract.read.symbol();

// Parse the amount with the correct number of decimals
validateAmount(amount, 'Approval amount');
const rawAmount = parseUnits(amount, decimals);

// Create wallet client for sending the transaction
const walletClient = getWalletClient(formattedKey, network);

// Send the transaction
const hash = await walletClient.writeContract({
address: tokenAddress,
Expand Down Expand Up @@ -412,6 +432,7 @@ export async function transferERC1155(
const fromAddress = walletClient.account!.address;

// Parse amount to bigint
validateAmount(amount, 'ERC1155 amount');
const amountBigInt = BigInt(amount);

// Send the transaction
Expand Down
16 changes: 16 additions & 0 deletions src/core/tools.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,17 @@ import * as services from "./services/index.js";
import { type Address, type Hex, type Hash } from 'viem';
import { normalize } from 'viem/ens';

/**
* Validates an Ethereum address format (0x + 40 hex chars).
* Throws if invalid. Allows ENS names (containing '.') to pass through.
*/
function validateAddressOrEns(value: string, label = 'Address'): void {
if (value.includes('.')) return;
if (!/^0x[a-fA-F0-9]{40}$/.test(value)) {
throw new Error(`${label} must be a valid Ethereum address (0x + 40 hex chars) or ENS name`);
}
}

/**
* Register all EVM-related tools with the MCP server
*
Expand Down Expand Up @@ -666,6 +677,7 @@ export function registerEVMTools(server: McpServer) {
},
async ({ contractAddress, functionName, args = [], abiJson, network = "ethereum" }) => {
try {
validateAddressOrEns(contractAddress, 'Contract address');
const client = await services.getPublicClient(network);

let abi: any[] | undefined;
Expand Down Expand Up @@ -772,6 +784,7 @@ export function registerEVMTools(server: McpServer) {
},
async ({ contractAddress, functionName, args = [], value, abiJson, network = "ethereum" }) => {
try {
validateAddressOrEns(contractAddress, 'Contract address');
const privateKey = getConfiguredPrivateKey();
const senderAddress = getWalletAddressFromKey();
const client = await services.getPublicClient(network);
Expand Down Expand Up @@ -1010,6 +1023,7 @@ export function registerEVMTools(server: McpServer) {
},
async ({ to, amount, network = "ethereum" }) => {
try {
validateAddressOrEns(to, 'Recipient address');
const privateKey = getConfiguredPrivateKey();
const senderAddress = getWalletAddressFromKey();
const txHash = await services.transferETH(privateKey, to as Address, amount, network);
Expand Down Expand Up @@ -1055,6 +1069,8 @@ export function registerEVMTools(server: McpServer) {
},
async ({ tokenAddress, to, amount, network = "ethereum" }) => {
try {
validateAddressOrEns(tokenAddress, 'Token address');
validateAddressOrEns(to, 'Recipient address');
const privateKey = getConfiguredPrivateKey();
const senderAddress = getWalletAddressFromKey();
const result = await services.transferERC20(tokenAddress as Address, to as Address, amount, privateKey, network);
Expand Down