diff --git a/.cursor/mcp.json b/.cursor/mcp.json deleted file mode 100644 index 42a6a03..0000000 --- a/.cursor/mcp.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "mcpServers": { - "evm-mcp-server": { - "command": "npx", - "args": [ - "-y", - "@mcpdotdirect/evm-mcp-server" - ] - }, - "evm-mcp-http": { - "command": "npx", - "args": [ - "-y", - "@mcpdotdirect/evm-mcp-server", - "--http" - ] - } - } -} \ No newline at end of file diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..be62ae8 --- /dev/null +++ b/.env.example @@ -0,0 +1,6 @@ +# Sei MCP Server Environment Variables + +# Private key for blockchain transactions (without 0x prefix) +# This is used when no private key is provided in the request +# SECURITY: Never commit your actual private key to version control +PRIVATE_KEY=your_private_key_here diff --git a/.gitignore b/.gitignore index 62ce640..b3e260e 100644 --- a/.gitignore +++ b/.gitignore @@ -27,5 +27,8 @@ report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json mcp-context/ build/ +# Environment variables +.env + # Finder (MacOS) folder config .DS_Store diff --git a/README.md b/README.md index 6f4f47a..0d12a68 100644 --- a/README.md +++ b/README.md @@ -1,11 +1,12 @@ -# EVM MCP Server +# SEI MCP Server ![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg) ![EVM Networks](https://img.shields.io/badge/Networks-30+-green) ![TypeScript](https://img.shields.io/badge/TypeScript-5.0+-3178C6) ![Viem](https://img.shields.io/badge/Viem-1.0+-green) -A comprehensive Model Context Protocol (MCP) server that provides blockchain services across multiple EVM-compatible networks. This server enables AI agents to interact with Ethereum, Optimism, Arbitrum, Base, Polygon, and many other EVM chains with a unified interface. +Sei Model Context Protocol (Sei MCP) server provides blockchain services for Sei blockchain. +This server enables AI assistants and agents to interact via unified interface. ## 📋 Contents @@ -26,27 +27,25 @@ A comprehensive Model Context Protocol (MCP) server that provides blockchain ser ## 🔭 Overview -The MCP EVM Server leverages the Model Context Protocol to provide blockchain services to AI agents. It supports a wide range of services including: +The MCP EVM Server leverages the Model Context Protocol to provide blockchain services to AI agents. +It supports a wide range of services including: - Reading blockchain state (balances, transactions, blocks, etc.) - Interacting with smart contracts - Transferring tokens (native, ERC20, ERC721, ERC1155) - Querying token metadata and balances -- Chain-specific services across 30+ EVM networks -- **ENS name resolution** for all address parameters (use human-readable names like 'vitalik.eth' instead of addresses) -All services are exposed through a consistent interface of MCP tools and resources, making it easy for AI agents to discover and use blockchain functionality. **Every tool that accepts Ethereum addresses also supports ENS names**, automatically resolving them to addresses behind the scenes. +All services are exposed through a consistent interface of MCP tools and resources, making it easy for AI agents to +discover and use blockchain functionality. ## ✨ Features ### Blockchain Data Access -- **Multi-chain support** for 30+ EVM-compatible networks - **Chain information** including blockNumber, chainId, and RPCs - **Block data** access by number, hash, or latest - **Transaction details** and receipts with decoded logs - **Address balances** for native tokens and all token standards -- **ENS resolution** for human-readable Ethereum addresses (use 'vitalik.eth' instead of '0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045') ### Token services @@ -83,63 +82,9 @@ All services are exposed through a consistent interface of MCP tools and resourc ## 🌐 Supported Networks -### Mainnets -- Ethereum (ETH) -- Optimism (OP) -- Arbitrum (ARB) -- Arbitrum Nova -- Base -- Polygon (MATIC) -- Polygon zkEVM -- Avalanche (AVAX) -- Binance Smart Chain (BSC) -- zkSync Era -- Linea -- Celo -- Gnosis (xDai) -- Fantom (FTM) -- Filecoin (FIL) -- Moonbeam -- Moonriver -- Cronos -- Scroll -- Mantle -- Manta -- Blast -- Fraxtal -- Mode -- Metis -- Kroma -- Zora -- Aurora -- Canto -- Flow -- Lumia - -### Testnets -- Sepolia -- Optimism Sepolia -- Arbitrum Sepolia -- Base Sepolia -- Polygon Amoy -- Avalanche Fuji -- BSC Testnet -- zkSync Sepolia -- Linea Sepolia -- Scroll Sepolia -- Mantle Sepolia -- Manta Sepolia -- Blast Sepolia -- Fraxtal Testnet -- Mode Testnet -- Metis Sepolia -- Kroma Sepolia -- Zora Sepolia -- Celo Alfajores -- Goerli -- Holesky -- Flow Testnet -- Lumia Testnet +- Sei Mainnet +- Sei Testnet +- Sei Localnet ## 🛠️ Prerequisites @@ -150,8 +95,8 @@ All services are exposed through a consistent interface of MCP tools and resourc ```bash # Clone the repository -git clone https://github.com/mcpdotdirect/mcp-evm-server.git -cd mcp-evm-server +git clone https://github.com/sei-protocol/sei-mcp-server.git +cd sei-mcp-server # Install dependencies with Bun bun install @@ -164,7 +109,7 @@ npm install The server uses the following default configuration: -- **Default Chain ID**: 1 (Ethereum Mainnet) +- **Default Chain ID**: 1329 (Sei Mainnet) - **Server Port**: 3001 - **Server Host**: 0.0.0.0 (accessible from any network interface) @@ -173,18 +118,33 @@ These values are hardcoded in the application. If you need to modify them, you c - For chain configuration: `src/core/chains.ts` - For server configuration: `src/server/http-server.ts` +### Environment Variables + +The server supports loading configuration from environment variables: + +- `PRIVATE_KEY`: **Required** private key for any blockchain operations that involve signing transactions (e.g., transferring tokens, interacting with smart contracts that modify state). This is the **sole method** for providing a private key. If this environment variable is not set when a transaction-signing tool is invoked, the tool will return an error message instructing the AI assistant to ask the user to set the `PRIVATE_KEY` environment variable and restart the MCP server. + +Create a `.env` file in the root directory based on the `.env.example` template: + +```bash +# .env.example +PRIVATE_KEY=your_private_key_here +``` + +> **SECURITY WARNING**: Never commit your actual private key to version control. The `.env` file is included in `.gitignore` by default. + ## 🚀 Usage ### Using npx (No Installation Required) -You can run the MCP EVM Server directly without installation using npx: +You can run the Sei MCP Server directly without installation using npx: ```bash # Run the server in stdio mode (for CLI tools) -npx @mcpdotdirect/evm-mcp-server +npx @sei-js/sei-mcp-server # Run the server in HTTP mode (for web applications) -npx @mcpdotdirect/evm-mcp-server --http +npx @sei-js/sei-mcp-server --http ``` ### Running the Server Locally @@ -222,9 +182,9 @@ To connect to the MCP server from Cursor: 3. Scroll down to "MCP Servers" section 4. Click "Add new MCP server" 5. Enter the following details: - - Server name: `evm-mcp-server` + - Server name: `sei-mcp-server` - Type: `command` - - Command: `npx @mcpdotdirect/evm-mcp-server` + - Command: `npx @sei-js/sei-mcp-server` 6. Click "Save" @@ -241,14 +201,14 @@ For a more portable configuration that you can share with your team or use acros "command": "npx", "args": [ "-y", - "@mcpdotdirect/evm-mcp-server" + "@sei-protocol/sei-mcp-server" ] }, "evm-mcp-http": { "command": "npx", "args": [ "-y", - "@mcpdotdirect/evm-mcp-server", + "@sei-protocol/sei-mcp-server", "--http" ] } @@ -297,12 +257,12 @@ After configuring the MCP server with `mcp.json`, you can easily use it in Curso // blockchain-example.js async function main() { try { - // Get ETH balance for an address using ENS - console.log("Getting ETH balance for vitalik.eth..."); + // Get Sei balance for an address + console.log("Getting Sei balance for 0x1234..."); // When using with Cursor, you can simply ask Cursor to: - // "Check the ETH balance of vitalik.eth on mainnet" - // Or "Transfer 0.1 ETH from my wallet to vitalik.eth" + // "Check the Sei balance of 0x1234 on mainnet" + // Or "Transfer 0.1 Sei from my wallet to 0x1234" // Cursor will use the MCP server to execute these operations // without requiring any additional code from you @@ -319,9 +279,9 @@ main(); 2. With the file open in Cursor, you can ask Cursor to: - - "Check the current ETH balance of vitalik.eth" - - "Look up the price of USDC on Ethereum" - - "Show me the latest block on Optimism" + - "Check the current Sei balance of 0x1234 on mainnet" + - "Look up the price of USDC on Sei" + - "Show me the latest block on Sei" - "Check if 0x1234... is a contract address" 3. Cursor will use the MCP server to execute these operations and return the results directly in your conversation. @@ -334,29 +294,29 @@ If you're using Claude CLI, you can connect to the MCP server with just two comm ```bash # Add the MCP server -claude mcp add evm-mcp-server npx @mcpdotdirect/evm-mcp-server +claude mcp add evm-mcp-server npx @sei-protocol/sei-mcp-server # Start Claude with the MCP server enabled claude ``` -### Example: Getting a Token Balance with ENS +### Example: Getting a Token Balance ```javascript -// Example of using the MCP client to check a token balance using ENS +// Example of using the MCP client to check a token balance const mcp = new McpClient("http://localhost:3000"); const result = await mcp.invokeTool("get-token-balance", { - tokenAddress: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", // USDC on Ethereum - ownerAddress: "vitalik.eth", // ENS name instead of address - network: "ethereum" + tokenAddress: "0x3894085ef7ff0f0aedf52e2a2704928d1ec074f1", // USDC on Sei + ownerAddress: "0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045", + network: "sei" }); console.log(result); // { -// tokenAddress: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", +// tokenAddress: "0x3894085ef7ff0f0aedf52e2a2704928d1ec074f1", // owner: "0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045", -// network: "ethereum", +// network: "sei", // raw: "1000000000", // formatted: "1000", // symbol: "USDC", @@ -364,76 +324,54 @@ console.log(result); // } ``` -### Example: Resolving an ENS Name - -```javascript -// Example of using the MCP client to resolve an ENS name to an address -const mcp = new McpClient("http://localhost:3000"); - -const result = await mcp.invokeTool("resolve-ens", { - ensName: "vitalik.eth", - network: "ethereum" -}); - -console.log(result); -// { -// ensName: "vitalik.eth", -// normalizedName: "vitalik.eth", -// resolvedAddress: "0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045", -// network: "ethereum" -// } -``` - ## 📚 API Reference ### Tools -The server provides the following MCP tools for agents. **All tools that accept address parameters support both Ethereum addresses and ENS names.** +The server provides the following MCP tools for agents. #### Token services | Tool Name | Description | Key Parameters | |-----------|-------------|----------------| -| `get-token-info` | Get ERC20 token metadata | `tokenAddress` (address/ENS), `network` | -| `get-token-balance` | Check ERC20 token balance | `tokenAddress` (address/ENS), `ownerAddress` (address/ENS), `network` | -| `transfer-token` | Transfer ERC20 tokens | `privateKey`, `tokenAddress` (address/ENS), `toAddress` (address/ENS), `amount`, `network` | -| `approve-token-spending` | Approve token allowances | `privateKey`, `tokenAddress` (address/ENS), `spenderAddress` (address/ENS), `amount`, `network` | -| `get-nft-info` | Get NFT metadata | `tokenAddress` (address/ENS), `tokenId`, `network` | -| `check-nft-ownership` | Verify NFT ownership | `tokenAddress` (address/ENS), `tokenId`, `ownerAddress` (address/ENS), `network` | -| `transfer-nft` | Transfer an NFT | `privateKey`, `tokenAddress` (address/ENS), `tokenId`, `toAddress` (address/ENS), `network` | -| `get-nft-balance` | Count NFTs owned | `tokenAddress` (address/ENS), `ownerAddress` (address/ENS), `network` | -| `get-erc1155-token-uri` | Get ERC1155 metadata | `tokenAddress` (address/ENS), `tokenId`, `network` | -| `get-erc1155-balance` | Check ERC1155 balance | `tokenAddress` (address/ENS), `tokenId`, `ownerAddress` (address/ENS), `network` | -| `transfer-erc1155` | Transfer ERC1155 tokens | `privateKey`, `tokenAddress` (address/ENS), `tokenId`, `amount`, `toAddress` (address/ENS), `network` | +| `get-token-info` | Get ERC20 token metadata | `tokenAddress` (address), `network` | +| `get-token-balance` | Check ERC20 token balance | `tokenAddress` (address), `ownerAddress` (address), `network` | +| `transfer-token` | Transfer ERC20 tokens | `tokenAddress` (address), `toAddress` (address), `amount`, `network` | +| `approve-token-spending` | Approve token allowances | `tokenAddress` (address), `spenderAddress` (address), `amount`, `network` | +| `get-nft-info` | Get NFT metadata | `tokenAddress` (address), `tokenId`, `network` | +| `check-nft-ownership` | Verify NFT ownership | `tokenAddress` (address), `tokenId`, `ownerAddress` (address), `network` | +| `get-nft-balance` | Count NFTs owned | `tokenAddress` (address), `ownerAddress` (address), `network` | +| `get-erc1155-token-uri` | Get ERC1155 metadata | `tokenAddress` (address), `tokenId`, `network` | +| `get-erc1155-balance` | Check ERC1155 balance | `tokenAddress` (address), `tokenId`, `ownerAddress` (address), `network` | +| `transfer-erc1155` | Transfer ERC1155 tokens | `tokenAddress` (address), `tokenId`, `amount`, `toAddress` (address), `network` | #### Blockchain services -| Tool Name | Description | Key Parameters | -|-----------|-------------|----------------| -| `get-chain-info` | Get network information | `network` | -| `get-balance` | Get native token balance | `address` (address/ENS), `network` | -| `transfer-eth` | Send native tokens | `privateKey`, `to` (address/ENS), `amount`, `network` | +| Tool Name | Description | Key Parameters | +|-------------------|-------------|----------------| +| `get-chain-info` | Get network information | `network` | +| `get-balance` | Get native token balance | `address` (address), `network` | +| `transfer-sei` | Send native tokens | `to` (address), `amount`, `network` | | `get-transaction` | Get transaction details | `txHash`, `network` | -| `read-contract` | Read smart contract state | `contractAddress` (address/ENS), `abi`, `functionName`, `args`, `network` | -| `write-contract` | Write to smart contract | `contractAddress` (address/ENS), `abi`, `functionName`, `args`, `privateKey`, `network` | -| `is-contract` | Check if address is a contract | `address` (address/ENS), `network` | -| `resolve-ens` | Resolve ENS name to address | `ensName`, `network` | +| `read-contract` | Read smart contract state | `contractAddress` (address), `abi`, `functionName`, `args` (optional), `network` | +| `write-contract` | Write to smart contract | `contractAddress` (address), `abi`, `functionName`, `args` (optional), `network` | +| `is-contract` | Check if address is a contract | `address` (address), `network` | ### Resources -The server exposes blockchain data through the following MCP resource URIs. All resource URIs that accept addresses also support ENS names, which are automatically resolved to addresses. +The server exposes blockchain data through the following MCP resource URIs. #### Blockchain Resources -| Resource URI Pattern | Description | -|-----------|-------------| +| Resource URI Pattern | Description | +|-----------|------------------------------------------| | `evm://{network}/chain` | Chain information for a specific network | -| `evm://chain` | Ethereum mainnet chain information | -| `evm://{network}/block/{blockNumber}` | Block data by number | -| `evm://{network}/block/latest` | Latest block data | -| `evm://{network}/address/{address}/balance` | Native token balance | -| `evm://{network}/tx/{txHash}` | Transaction details | -| `evm://{network}/tx/{txHash}/receipt` | Transaction receipt with logs | +| `evm://chain` | Sei mainnet chain information | +| `evm://{network}/block/{blockNumber}` | Block data by number | +| `evm://{network}/block/latest` | Latest block data | +| `evm://{network}/address/{address}/balance` | Native token balance | +| `evm://{network}/tx/{txHash}` | Transaction details | +| `evm://{network}/tx/{txHash}/receipt` | Transaction receipt with logs | #### Token Resources diff --git a/package.json b/package.json index 37ab0a7..5e4972e 100644 --- a/package.json +++ b/package.json @@ -1,11 +1,11 @@ { - "name": "@mcpdotdirect/evm-mcp-server", + "name": "@sei-protocol/sei-mcp-server", "module": "src/index.ts", "type": "module", - "version": "1.1.3", + "version": "1.0.0", "description": "Model Context Protocol (MCP) server for interacting with EVM-compatible networks", "bin": { - "evm-mcp-server": "./bin/cli.js" + "sei-mcp-server": "./bin/cli.js" }, "main": "build/index.js", "files": [ @@ -22,6 +22,7 @@ "start:http": "bun run src/server/http-server.ts", "dev:http": "bun --watch src/server/http-server.ts", "prepublishOnly": "bun run build && bun run build:http", + "test": "bun test", "version:patch": "npm version patch", "version:minor": "npm version minor", "version:major": "npm version major", @@ -41,6 +42,7 @@ "dependencies": { "@modelcontextprotocol/sdk": "^1.7.0", "cors": "^2.8.5", + "dotenv": "^16.5.0", "express": "^4.21.2", "viem": "^2.23.9", "zod": "^3.24.2" @@ -50,7 +52,7 @@ "model-context-protocol", "evm", "blockchain", - "ethereum", + "sei", "web3", "smart-contracts", "ai", @@ -63,12 +65,12 @@ }, "repository": { "type": "git", - "url": "https://github.com/mcpdotdirect/evm-mcp-server" + "url": "https://github.com/sei-protocol/sei-mcp-server" }, "bugs": { - "url": "https://github.com/mcpdotdirect/evm-mcp-server/issues" + "url": "https://github.com/sei-protocol/sei-mcp-server/issues" }, - "homepage": "https://github.com/mcpdotdirect/evm-mcp-server#readme", + "homepage": "https://github.com/sei-protocol/seo-mcp-server#readme", "publishConfig": { "access": "public" } diff --git a/src/core/chains.ts b/src/core/chains.ts index 43bac2c..0e7e5b6 100644 --- a/src/core/chains.ts +++ b/src/core/chains.ts @@ -1,281 +1,33 @@ import { type Chain } from 'viem'; import { - // Mainnets - mainnet, - optimism, - arbitrum, - arbitrumNova, - base, - polygon, - polygonZkEvm, - avalanche, - bsc, - zksync, - linea, - celo, - gnosis, - fantom, - filecoin, - moonbeam, - moonriver, - cronos, - lumiaMainnet, - scroll, - mantle, - manta, - blast, - fraxtal, - mode, - metis, - kroma, - zora, - aurora, - canto, - flowMainnet, - - // Testnets - sepolia, - optimismSepolia, - arbitrumSepolia, - baseSepolia, - polygonAmoy, - avalancheFuji, - bscTestnet, - zksyncSepoliaTestnet, - lineaSepolia, - lumiaTestnet, - scrollSepolia, - mantleSepoliaTestnet, - mantaSepoliaTestnet, - blastSepolia, - fraxtalTestnet, - modeTestnet, - metisSepolia, - kromaSepolia, - zoraSepolia, - celoAlfajores, - goerli, - holesky, - flowTestnet + sei, + seiTestnet, + seiDevnet } from 'viem/chains'; // Default configuration values -export const DEFAULT_RPC_URL = 'https://eth.llamarpc.com'; -export const DEFAULT_CHAIN_ID = 1; +export const DEFAULT_RPC_URL = 'https://evm-rpc.sei-apis.com'; +export const DEFAULT_CHAIN_ID = 1329; // Map chain IDs to chains export const chainMap: Record = { - // Mainnets - 1: mainnet, - 10: optimism, - 42161: arbitrum, - 42170: arbitrumNova, - 8453: base, - 137: polygon, - 1101: polygonZkEvm, - 43114: avalanche, - 56: bsc, - 324: zksync, - 59144: linea, - 42220: celo, - 100: gnosis, - 250: fantom, - 314: filecoin, - 1284: moonbeam, - 1285: moonriver, - 25: cronos, - 534352: scroll, - 5000: mantle, - 169: manta, - 994873017: lumiaMainnet, - 81457: blast, - 252: fraxtal, - 34443: mode, - 1088: metis, - 255: kroma, - 7777777: zora, - 1313161554: aurora, - 7700: canto, - 747: flowMainnet, - - // Testnets - 11155111: sepolia, - 11155420: optimismSepolia, - 421614: arbitrumSepolia, - 84532: baseSepolia, - 80002: polygonAmoy, - 43113: avalancheFuji, - 97: bscTestnet, - 300: zksyncSepoliaTestnet, - 59141: lineaSepolia, - 1952959480: lumiaTestnet, - 534351: scrollSepolia, - 5003: mantleSepoliaTestnet, - 3441006: mantaSepoliaTestnet, - 168587773: blastSepolia, - 2522: fraxtalTestnet, - 919: modeTestnet, - 59902: metisSepolia, - 2358: kromaSepolia, - 999999999: zoraSepolia, - 44787: celoAlfajores, - 5: goerli, - 17000: holesky, - 545: flowTestnet, + 1329: sei, + 1328: seiTestnet, + 713_715: seiDevnet }; // Map network names to chain IDs for easier reference export const networkNameMap: Record = { - // Mainnets - 'ethereum': 1, - 'mainnet': 1, - 'eth': 1, - 'optimism': 10, - 'op': 10, - 'arbitrum': 42161, - 'arb': 42161, - 'arbitrum-nova': 42170, - 'arbitrumnova': 42170, - 'base': 8453, - 'polygon': 137, - 'matic': 137, - 'polygon-zkevm': 1101, - 'polygonzkevm': 1101, - 'avalanche': 43114, - 'avax': 43114, - 'binance': 56, - 'bsc': 56, - 'zksync': 324, - 'linea': 59144, - 'celo': 42220, - 'gnosis': 100, - 'xdai': 100, - 'fantom': 250, - 'ftm': 250, - 'filecoin': 314, - 'fil': 314, - 'moonbeam': 1284, - 'moonriver': 1285, - 'cronos': 25, - 'scroll': 534352, - 'mantle': 5000, - 'manta': 169, - 'lumia': 994873017, - 'blast': 81457, - 'fraxtal': 252, - 'mode': 34443, - 'metis': 1088, - 'kroma': 255, - 'zora': 7777777, - 'aurora': 1313161554, - 'canto': 7700, - 'flow': 747, - - // Testnets - 'sepolia': 11155111, - 'optimism-sepolia': 11155420, - 'optimismsepolia': 11155420, - 'arbitrum-sepolia': 421614, - 'arbitrumsepolia': 421614, - 'base-sepolia': 84532, - 'basesepolia': 84532, - 'polygon-amoy': 80002, - 'polygonamoy': 80002, - 'avalanche-fuji': 43113, - 'avalanchefuji': 43113, - 'fuji': 43113, - 'bsc-testnet': 97, - 'bsctestnet': 97, - 'zksync-sepolia': 300, - 'zksyncsepolia': 300, - 'linea-sepolia': 59141, - 'lineasepolia': 59141, - 'lumia-testnet': 1952959480, - 'scroll-sepolia': 534351, - 'scrollsepolia': 534351, - 'mantle-sepolia': 5003, - 'mantlesepolia': 5003, - 'manta-sepolia': 3441006, - 'mantasepolia': 3441006, - 'blast-sepolia': 168587773, - 'blastsepolia': 168587773, - 'fraxtal-testnet': 2522, - 'fraxtaltestnet': 2522, - 'mode-testnet': 919, - 'modetestnet': 919, - 'metis-sepolia': 59902, - 'metissepolia': 59902, - 'kroma-sepolia': 2358, - 'kromasepolia': 2358, - 'zora-sepolia': 999999999, - 'zorasepolia': 999999999, - 'celo-alfajores': 44787, - 'celoalfajores': 44787, - 'alfajores': 44787, - 'goerli': 5, - 'holesky': 17000, - 'flow-testnet': 545, + 'sei': 1329, + 'sei-testnet': 1328, + 'sei-devnet': 713_715, }; // Map chain IDs to RPC URLs export const rpcUrlMap: Record = { - // Mainnets - 1: 'https://eth.llamarpc.com', - 10: 'https://mainnet.optimism.io', - 42161: 'https://arb1.arbitrum.io/rpc', - 42170: 'https://nova.arbitrum.io/rpc', - 8453: 'https://mainnet.base.org', - 137: 'https://polygon-rpc.com', - 1101: 'https://zkevm-rpc.com', - 43114: 'https://api.avax.network/ext/bc/C/rpc', - 56: 'https://bsc-dataseed.binance.org', - 324: 'https://mainnet.era.zksync.io', - 59144: 'https://rpc.linea.build', - 42220: 'https://forno.celo.org', - 100: 'https://rpc.gnosischain.com', - 250: 'https://rpc.ftm.tools', - 314: 'https://api.node.glif.io/rpc/v1', - 1284: 'https://rpc.api.moonbeam.network', - 1285: 'https://rpc.api.moonriver.moonbeam.network', - 25: 'https://evm.cronos.org', - 534352: 'https://rpc.scroll.io', - 5000: 'https://rpc.mantle.xyz', - 169: 'https://pacific-rpc.manta.network/http', - 81457: 'https://rpc.blast.io', - 252: 'https://rpc.frax.com', - 994873017: 'https://mainnet-rpc.lumia.org', - 34443: 'https://mainnet.mode.network', - 1088: 'https://andromeda.metis.io/?owner=1088', - 255: 'https://api.kroma.network', - 7777777: 'https://rpc.zora.energy', - 1313161554: 'https://mainnet.aurora.dev', - 7700: 'https://canto.gravitychain.io', - 747: 'https://mainnet.evm.nodes.onflow.org', - - // Testnets - 11155111: 'https://sepolia.drpc.org', - 11155420: 'https://sepolia.optimism.io', - 421614: 'https://sepolia-rpc.arbitrum.io/rpc', - 84532: 'https://sepolia.base.org', - 80002: 'https://rpc-amoy.polygon.technology', - 43113: 'https://api.avax-test.network/ext/bc/C/rpc', - 97: 'https://data-seed-prebsc-1-s1.binance.org:8545', - 300: 'https://sepolia.era.zksync.dev', - 59141: 'https://rpc.sepolia.linea.build', - 534351: 'https://sepolia-rpc.scroll.io', - 5003: 'https://rpc.sepolia.mantle.xyz', - 3441006: 'https://pacific-rpc.sepolia.manta.network/http', - 1952959480: 'https://testnet-rpc.lumia.org', - 168587773: 'https://sepolia.blast.io', - 2522: 'https://rpc.testnet.frax.com', - 919: 'https://sepolia.mode.network', - 59902: 'https://sepolia.metis.io/?owner=59902', - 2358: 'https://api.sepolia.kroma.network', - 999999999: 'https://sepolia.rpc.zora.energy', - 44787: 'https://alfajores-forno.celo-testnet.org', - 5: 'https://rpc.ankr.com/eth_goerli', - 17000: 'https://ethereum-holesky.publicnode.com', - 545: 'https://testnet.evm.nodes.onflow.org', + 1329: 'https://evm-rpc.sei-apis.com', + 1328: 'https://evm-rpc-testnet.sei-apis.com', + 713_715: 'https://evm-rpc-arctic-1.sei-apis.com' }; /** @@ -287,21 +39,21 @@ export function resolveChainId(chainIdentifier: number | string): number { if (typeof chainIdentifier === 'number') { return chainIdentifier; } - + // Convert to lowercase for case-insensitive matching const networkName = chainIdentifier.toLowerCase(); - + // Check if the network name is in our map if (networkName in networkNameMap) { return networkNameMap[networkName]; } - + // Try parsing as a number const parsedId = parseInt(networkName); if (!isNaN(parsedId)) { return parsedId; } - + // Default to mainnet if not found return DEFAULT_CHAIN_ID; } @@ -317,15 +69,15 @@ export function getChain(chainIdentifier: number | string = DEFAULT_CHAIN_ID): C const networkName = chainIdentifier.toLowerCase(); // Try to get from direct network name mapping first if (networkNameMap[networkName]) { - return chainMap[networkNameMap[networkName]] || mainnet; + return chainMap[networkNameMap[networkName]] || sei; } - + // 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; + return chainMap[chainIdentifier] || sei; } /** @@ -334,10 +86,10 @@ export function getChain(chainIdentifier: number | string = DEFAULT_CHAIN_ID): C * @returns The RPC URL for the specified chain */ export function getRpcUrl(chainIdentifier: number | string = DEFAULT_CHAIN_ID): string { - const chainId = typeof chainIdentifier === 'string' - ? resolveChainId(chainIdentifier) + const chainId = typeof chainIdentifier === 'string' + ? resolveChainId(chainIdentifier) : chainIdentifier; - + return rpcUrlMap[chainId] || DEFAULT_RPC_URL; } @@ -349,4 +101,4 @@ export function getSupportedNetworks(): string[] { return Object.keys(networkNameMap) .filter(name => name.length > 2) // Filter out short aliases .sort(); -} +} diff --git a/src/core/config.ts b/src/core/config.ts new file mode 100644 index 0000000..4412063 --- /dev/null +++ b/src/core/config.ts @@ -0,0 +1,36 @@ +import dotenv from 'dotenv'; +import { z } from 'zod'; +import { type Hex } from 'viem'; + +// Load environment variables from .env file +dotenv.config(); + +// Define environment variable schema +const envSchema = z.object({ + PRIVATE_KEY: z.string().optional(), +}); + +// Parse and validate environment variables +const env = envSchema.safeParse(process.env); + +// Format private key with 0x prefix if it exists +const formatPrivateKey = (key?: string): string | undefined => { + if (!key) return undefined; + + // Ensure the private key has 0x prefix + return key.startsWith('0x') ? key : `0x${key}`; +}; + +// Export validated environment variables with formatted private key +export const config = { + privateKey: env.success ? formatPrivateKey(env.data.PRIVATE_KEY) : undefined, +}; + +/** + * Get the private key from environment variable as a Hex type for viem. + * Returns undefined if the PRIVATE_KEY environment variable is not set. + * @returns Private key from environment variable as Hex or undefined + */ +export function getPrivateKeyAsHex(): Hex | undefined { + return config.privateKey as Hex | undefined; +} diff --git a/src/core/prompts.ts b/src/core/prompts.ts index b7f7bce..d7577a7 100644 --- a/src/core/prompts.ts +++ b/src/core/prompts.ts @@ -12,14 +12,14 @@ export function registerEVMPrompts(server: McpServer) { "Explore information about a specific block", { blockNumber: z.string().optional().describe("Block number to explore. If not provided, latest block will be used."), - network: z.string().optional().describe("Network name (e.g., 'ethereum', 'optimism', 'arbitrum', 'base', etc.) or chain ID. Supports all EVM-compatible networks. Defaults to Ethereum mainnet.") + network: z.string().optional().describe("Network name (e.g., 'sei', 'sei-testnet', 'sei-devnet' etc.) or chain ID. Supports all EVM-compatible networks. Defaults to Sei mainnet.") }, - ({ blockNumber, network = "ethereum" }) => ({ + ({ blockNumber, network = "sei" }) => ({ messages: [{ role: "user", content: { type: "text", - text: blockNumber + text: blockNumber ? `Please analyze block #${blockNumber} on the ${network} network and provide information about its key metrics, transactions, and significance.` : `Please analyze the latest block on the ${network} network and provide information about its key metrics, transactions, and significance.` } @@ -33,9 +33,9 @@ export function registerEVMPrompts(server: McpServer) { "Analyze a specific transaction", { txHash: z.string().describe("Transaction hash to analyze"), - network: z.string().optional().describe("Network name (e.g., 'ethereum', 'optimism', 'arbitrum', 'base', etc.) or chain ID. Supports all EVM-compatible networks. Defaults to Ethereum mainnet.") + network: z.string().optional().describe("Network name (e.g., 'sei', 'sei-testnet', 'sei-devnet' etc.) or chain ID. Supports all EVM-compatible networks. Defaults to Sei mainnet.") }, - ({ txHash, network = "ethereum" }) => ({ + ({ txHash, network = "sei" }) => ({ messages: [{ role: "user", content: { @@ -46,15 +46,32 @@ export function registerEVMPrompts(server: McpServer) { }) ); + // Address analysis prompt + server.prompt( + "my_wallet_address", + "What is my wallet EVM address", + { + }, + () => ({ + messages: [{ + role: "user", + content: { + type: "text", + text: `Please retrieve my wallet EVM address using tools get_address_from_private_key via MCP server.` + } + }] + }) + ); + // Address analysis prompt server.prompt( "analyze_address", "Analyze an EVM address", { - address: z.string().describe("Ethereum address to analyze"), - network: z.string().optional().describe("Network name (e.g., 'ethereum', 'optimism', 'arbitrum', 'base', etc.) or chain ID. Supports all EVM-compatible networks. Defaults to Ethereum mainnet.") + address: z.string().describe("Sei 0x address to analyze"), + network: z.string().optional().describe("Network name (e.g., 'sei', 'sei-testnet', 'sei-devnet' etc.) or chain ID. Supports all EVM-compatible networks. Defaults to Sei mainnet.") }, - ({ address, network = "ethereum" }) => ({ + ({ address, network = "sei" }) => ({ messages: [{ role: "user", content: { @@ -72,9 +89,9 @@ export function registerEVMPrompts(server: McpServer) { { contractAddress: z.string().describe("The contract address"), abiJson: z.string().optional().describe("The contract ABI as a JSON string"), - network: z.string().optional().describe("Network name or chain ID. Defaults to Ethereum mainnet.") + network: z.string().optional().describe("Network name or chain ID. Defaults to Sei mainnet.") }, - ({ contractAddress, abiJson, network = "ethereum" }) => ({ + ({ contractAddress, abiJson, network = "sei" }) => ({ messages: [{ role: "user", content: { @@ -108,9 +125,9 @@ export function registerEVMPrompts(server: McpServer) { // Network comparison server.prompt( "compare_networks", - "Compare different EVM-compatible networks", + "Compare Sei networks", { - networkList: z.string().describe("Comma-separated list of networks to compare (e.g., 'ethereum,optimism,arbitrum')") + networkList: z.string().describe("Comma-separated list of networks to compare (e.g., 'sei,sei-testnet,sei-devnet')") }, ({ networkList }) => { const networks = networkList.split(',').map(n => n.trim()); @@ -119,7 +136,7 @@ export function registerEVMPrompts(server: McpServer) { role: "user", content: { type: "text", - text: `Please compare the following EVM-compatible networks: ${networks.join(', ')}. Include information about their architecture, gas fees, transaction speed, security, and any other relevant differences.` + text: `Please compare the following Sei networks: ${networks.join(', ')}. Include information about their architecture, gas fees, transaction speed, security, and any other relevant differences.` } }] }; @@ -134,11 +151,11 @@ export function registerEVMPrompts(server: McpServer) { tokenAddress: z.string().describe("Token contract address to analyze"), tokenType: z.string().optional().describe("Type of token to analyze (erc20, erc721/nft, or auto-detect). Defaults to auto."), tokenId: z.string().optional().describe("Token ID (required for NFT analysis)"), - network: z.string().optional().describe("Network name (e.g., 'ethereum', 'optimism', 'arbitrum', 'base', etc.) or chain ID. Supports all EVM-compatible networks. Defaults to Ethereum mainnet.") + network: z.string().optional().describe("Network name (e.g., 'sei', 'sei-testnet', 'sei-devnet', etc.) or chain ID. Supports all EVM-compatible networks. Defaults to Sei mainnet.") }, - ({ tokenAddress, tokenType = "auto", tokenId, network = "ethereum" }) => { + ({ tokenAddress, tokenType = "auto", tokenId, network = "sei" }) => { let promptText = ""; - + if (tokenType === "erc20" || tokenType === "auto") { promptText = `Please analyze the ERC20 token at address ${tokenAddress} on the ${network} network. Provide information about its name, symbol, total supply, and any other relevant details. If possible, explain the token's purpose, utility, and market context.`; } else if ((tokenType === "erc721" || tokenType === "nft") && tokenId) { @@ -159,4 +176,4 @@ export function registerEVMPrompts(server: McpServer) { } ); -} \ No newline at end of file +} diff --git a/src/core/resources.ts b/src/core/resources.ts index f8afca9..4a08b85 100644 --- a/src/core/resources.ts +++ b/src/core/resources.ts @@ -10,7 +10,7 @@ import type { Address, Hash } from "viem"; export function registerEVMResources(server: McpServer) { // Get EVM info for a specific network server.resource( - "chain_info_by_network", + "chain_info_by_network", new ResourceTemplate("evm://{network}/chain", { list: undefined }), async (uri, params) => { try { @@ -18,7 +18,7 @@ export function registerEVMResources(server: McpServer) { const chainId = await services.getChainId(network); const blockNumber = await services.getBlockNumber(network); const rpcUrl = getRpcUrl(network); - + return { contents: [{ uri: uri.href, @@ -41,17 +41,17 @@ export function registerEVMResources(server: McpServer) { } ); - // Default chain info (Ethereum mainnet) + // Default chain info (Sei mainnet) server.resource( - "ethereum_chain_info", + "sei_chain_info", "evm://chain", async (uri) => { try { - const network = "ethereum"; + const network = "sei"; const chainId = await services.getChainId(network); const blockNumber = await services.getBlockNumber(network); const rpcUrl = getRpcUrl(network); - + return { contents: [{ uri: uri.href, @@ -83,7 +83,7 @@ export function registerEVMResources(server: McpServer) { const network = params.network as string; const blockNumber = params.blockNumber as string; const block = await services.getBlockByNumber(parseInt(blockNumber), network); - + return { contents: [{ uri: uri.href, @@ -110,7 +110,7 @@ export function registerEVMResources(server: McpServer) { const network = params.network as string; const blockHash = params.blockHash as string; const block = await services.getBlockByHash(blockHash as Hash, network); - + return { contents: [{ uri: uri.href, @@ -136,7 +136,7 @@ export function registerEVMResources(server: McpServer) { try { const network = params.network as string; const block = await services.getLatestBlock(network); - + return { contents: [{ uri: uri.href, @@ -154,15 +154,15 @@ export function registerEVMResources(server: McpServer) { } ); - // Default latest block (Ethereum mainnet) + // Default latest block (Sei mainnet) server.resource( "default_latest_block", "evm://block/latest", async (uri) => { try { - const network = "ethereum"; + const network = "sei"; const block = await services.getLatestBlock(network); - + return { contents: [{ uri: uri.href, @@ -188,8 +188,8 @@ export function registerEVMResources(server: McpServer) { try { const network = params.network as string; const address = params.address as string; - const balance = await services.getETHBalance(address as Address, network); - + const balance = await services.getBalance(address as Address, network); + return { contents: [{ uri: uri.href, @@ -207,23 +207,23 @@ export function registerEVMResources(server: McpServer) { return { contents: [{ uri: uri.href, - text: `Error fetching ETH balance: ${error instanceof Error ? error.message : String(error)}` + text: `Error fetching Sei balance: ${error instanceof Error ? error.message : String(error)}` }] }; } } ); - // Default ETH balance (Ethereum mainnet) + // Default ETH balance (Sei mainnet) server.resource( - "default_eth_balance", + "default_sei_balance", new ResourceTemplate("evm://address/{address}/eth-balance", { list: undefined }), async (uri, params) => { try { - const network = "ethereum"; + const network = "sei"; const address = params.address as string; - const balance = await services.getETHBalance(address as Address, network); - + const balance = await services.getBalance(address as Address, network); + return { contents: [{ uri: uri.href, @@ -241,7 +241,7 @@ export function registerEVMResources(server: McpServer) { return { contents: [{ uri: uri.href, - text: `Error fetching ETH balance: ${error instanceof Error ? error.message : String(error)}` + text: `Error fetching Sei balance: ${error instanceof Error ? error.message : String(error)}` }] }; } @@ -257,13 +257,13 @@ export function registerEVMResources(server: McpServer) { const network = params.network as string; const address = params.address as string; const tokenAddress = params.tokenAddress as string; - + const balance = await services.getERC20Balance( tokenAddress as Address, address as Address, network ); - + return { contents: [{ uri: uri.href, @@ -290,22 +290,22 @@ export function registerEVMResources(server: McpServer) { } ); - // Default ERC20 balance (Ethereum mainnet) + // Default ERC20 balance (Sei mainnet) server.resource( "default_erc20_balance", new ResourceTemplate("evm://address/{address}/token/{tokenAddress}/balance", { list: undefined }), async (uri, params) => { try { - const network = "ethereum"; + const network = "sei"; const address = params.address as string; const tokenAddress = params.tokenAddress as string; - + const balance = await services.getERC20Balance( tokenAddress as Address, address as Address, network ); - + return { contents: [{ uri: uri.href, @@ -341,7 +341,7 @@ export function registerEVMResources(server: McpServer) { const network = params.network as string; const txHash = params.txHash as string; const tx = await services.getTransaction(txHash as Hash, network); - + return { contents: [{ uri: uri.href, @@ -359,16 +359,16 @@ export function registerEVMResources(server: McpServer) { } ); - // Default transaction by hash (Ethereum mainnet) + // Default transaction by hash (Sei mainnet) server.resource( "default_transaction_by_hash", new ResourceTemplate("evm://tx/{txHash}", { list: undefined }), async (uri, params) => { try { - const network = "ethereum"; + const network = "sei"; const txHash = params.txHash as string; const tx = await services.getTransaction(txHash as Hash, network); - + return { contents: [{ uri: uri.href, @@ -393,7 +393,7 @@ export function registerEVMResources(server: McpServer) { async (uri) => { try { const networks = getSupportedNetworks(); - + return { contents: [{ uri: uri.href, @@ -421,9 +421,9 @@ export function registerEVMResources(server: McpServer) { try { const network = params.network as string; const tokenAddress = params.tokenAddress as Address; - + const tokenInfo = await services.getERC20TokenInfo(tokenAddress, network); - + return { contents: [{ uri: uri.href, @@ -454,9 +454,9 @@ export function registerEVMResources(server: McpServer) { const network = params.network as string; const tokenAddress = params.tokenAddress as Address; const address = params.address as Address; - + const balance = await services.getERC20Balance(tokenAddress, address, network); - + return { contents: [{ uri: uri.href, @@ -491,9 +491,9 @@ export function registerEVMResources(server: McpServer) { const network = params.network as string; const tokenAddress = params.tokenAddress as Address; const tokenId = BigInt(params.tokenId as string); - + const nftInfo = await services.getERC721TokenMetadata(tokenAddress, tokenId, network); - + // Get owner separately let owner = "Unknown"; try { @@ -504,7 +504,7 @@ export function registerEVMResources(server: McpServer) { } catch (e) { // Owner info not available } - + return { contents: [{ uri: uri.href, @@ -538,9 +538,9 @@ export function registerEVMResources(server: McpServer) { const tokenAddress = params.tokenAddress as Address; const tokenId = BigInt(params.tokenId as string); const address = params.address as Address; - + const isOwner = await services.isNFTOwner(tokenAddress, address, tokenId, network); - + return { contents: [{ uri: uri.href, @@ -573,9 +573,9 @@ export function registerEVMResources(server: McpServer) { const network = params.network as string; const tokenAddress = params.tokenAddress as Address; const tokenId = BigInt(params.tokenId as string); - + const tokenURI = await services.getERC1155TokenURI(tokenAddress, tokenId, network); - + return { contents: [{ uri: uri.href, @@ -608,9 +608,9 @@ export function registerEVMResources(server: McpServer) { const tokenAddress = params.tokenAddress as Address; const tokenId = BigInt(params.tokenId as string); const address = params.address as Address; - + const balance = await services.getERC1155Balance(tokenAddress, address, tokenId, network); - + return { contents: [{ uri: uri.href, @@ -633,4 +633,4 @@ export function registerEVMResources(server: McpServer) { } } ); -} \ No newline at end of file +} diff --git a/src/core/services/balance.ts b/src/core/services/balance.ts index 0221b9d..33f1670 100644 --- a/src/core/services/balance.ts +++ b/src/core/services/balance.ts @@ -1,13 +1,12 @@ -import { +import { formatEther, formatUnits, type Address, - type Abi, getContract } from 'viem'; import { getPublicClient } from './clients.js'; import { readContract } from './contracts.js'; -import { resolveAddress } from './ens.js'; +import * as services from "./index.js"; // Standard ERC20 ABI (minimal for reading) const erc20Abi = [ @@ -67,21 +66,20 @@ const erc1155Abi = [ ] as const; /** - * Get the ETH balance for an address - * @param addressOrEns Ethereum address or ENS name + * Get the Sei balance for an address + * @param address Sei address * @param network Network name or chain ID - * @returns Balance in wei and ether + * @returns Balance in wei and sei */ -export async function getETHBalance( - addressOrEns: string, - network = 'ethereum' +export async function getBalance( + address: string, + network = 'sei' ): Promise<{ wei: bigint; ether: string }> { - // Resolve ENS name to address if needed - const address = await resolveAddress(addressOrEns, network); - + const validatedAddress = services.helpers.validateAddress(address); + const client = getPublicClient(network); - const balance = await client.getBalance({ address }); - + const balance = await client.getBalance({ address: validatedAddress }); + return { wei: balance, ether: formatEther(balance) @@ -90,15 +88,15 @@ export async function getETHBalance( /** * Get the balance of an ERC20 token for an address - * @param tokenAddressOrEns Token contract address or ENS name - * @param ownerAddressOrEns Owner address or ENS name + * @param tokenAddress Token contract address + * @param ownerAddress Owner address * @param network Network name or chain ID * @returns Token balance with formatting information */ export async function getERC20Balance( - tokenAddressOrEns: string, - ownerAddressOrEns: string, - network = 'ethereum' + tokenAddress: string, + ownerAddress: string, + network = 'sei' ): Promise<{ raw: bigint; formatted: string; @@ -107,20 +105,19 @@ export async function getERC20Balance( decimals: number; } }> { - // Resolve ENS names to addresses if needed - const tokenAddress = await resolveAddress(tokenAddressOrEns, network); - const ownerAddress = await resolveAddress(ownerAddressOrEns, network); - + const validatedTokenAddress = services.helpers.validateAddress(tokenAddress); + const validatedOwnerAddress = services.helpers.validateAddress(ownerAddress); + const publicClient = getPublicClient(network); const contract = getContract({ - address: tokenAddress, + address: validatedTokenAddress, abi: erc20Abi, client: publicClient, }); const [balance, symbol, decimals] = await Promise.all([ - contract.read.balanceOf([ownerAddress]), + contract.read.balanceOf([validatedOwnerAddress]), contract.read.symbol(), contract.read.decimals() ]); @@ -137,31 +134,30 @@ export async function getERC20Balance( /** * Check if an address owns a specific NFT - * @param tokenAddressOrEns NFT contract address or ENS name - * @param ownerAddressOrEns Owner address or ENS name + * @param tokenAddress NFT contract address + * @param ownerAddress Owner address * @param tokenId Token ID to check * @param network Network name or chain ID * @returns True if the address owns the NFT */ export async function isNFTOwner( - tokenAddressOrEns: string, - ownerAddressOrEns: string, + tokenAddress: string, + ownerAddress: string, tokenId: bigint, - network = 'ethereum' + network = 'sei' ): Promise { - // Resolve ENS names to addresses if needed - const tokenAddress = await resolveAddress(tokenAddressOrEns, network); - const ownerAddress = await resolveAddress(ownerAddressOrEns, network); - + const validatedTokenAddress = services.helpers.validateAddress(tokenAddress); + const validatedOwnerAddress = services.helpers.validateAddress(ownerAddress); + try { const actualOwner = await readContract({ - address: tokenAddress, + address: validatedTokenAddress, abi: erc721Abi, functionName: 'ownerOf', args: [tokenId] }, network) as Address; - - return actualOwner.toLowerCase() === ownerAddress.toLowerCase(); + + return actualOwner.toLowerCase() === validatedOwnerAddress.toLowerCase(); } catch (error: any) { console.error(`Error checking NFT ownership: ${error.message}`); return false; @@ -170,50 +166,48 @@ export async function isNFTOwner( /** * Get the number of NFTs owned by an address for a specific collection - * @param tokenAddressOrEns NFT contract address or ENS name - * @param ownerAddressOrEns Owner address or ENS name + * @param tokenAddress NFT contract address + * @param ownerAddress Owner address * @param network Network name or chain ID * @returns Number of NFTs owned */ export async function getERC721Balance( - tokenAddressOrEns: string, - ownerAddressOrEns: string, - network = 'ethereum' + tokenAddress: string, + ownerAddress: string, + network = 'sei' ): Promise { - // Resolve ENS names to addresses if needed - const tokenAddress = await resolveAddress(tokenAddressOrEns, network); - const ownerAddress = await resolveAddress(ownerAddressOrEns, network); - - return readContract({ - address: tokenAddress, + const validatedTokenAddress = services.helpers.validateAddress(tokenAddress); + const validatedOwnerAddress = services.helpers.validateAddress(ownerAddress); + + return await readContract({ + address: validatedTokenAddress, abi: erc721Abi, functionName: 'balanceOf', - args: [ownerAddress] + args: [validatedOwnerAddress] }, network) as Promise; } /** * Get the balance of an ERC1155 token for an address - * @param tokenAddressOrEns ERC1155 contract address or ENS name - * @param ownerAddressOrEns Owner address or ENS name + * @param tokenAddress ERC1155 contract address + * @param ownerAddress Owner address * @param tokenId Token ID to check * @param network Network name or chain ID * @returns Token balance */ export async function getERC1155Balance( - tokenAddressOrEns: string, - ownerAddressOrEns: string, + tokenAddress: string, + ownerAddress: string, tokenId: bigint, - network = 'ethereum' + network = 'sei' ): Promise { - // Resolve ENS names to addresses if needed - const tokenAddress = await resolveAddress(tokenAddressOrEns, network); - const ownerAddress = await resolveAddress(ownerAddressOrEns, network); - - return readContract({ - address: tokenAddress, + const validatedTokenAddress = services.helpers.validateAddress(tokenAddress); + const validatedOwnerAddress = services.helpers.validateAddress(ownerAddress); + + return await readContract({ + address: validatedTokenAddress, abi: erc1155Abi, functionName: 'balanceOf', - args: [ownerAddress, tokenId] + args: [validatedOwnerAddress, tokenId] }, network) as Promise; -} \ No newline at end of file +} diff --git a/src/core/services/blocks.ts b/src/core/services/blocks.ts index aaf8eb9..7b1c951 100644 --- a/src/core/services/blocks.ts +++ b/src/core/services/blocks.ts @@ -1,4 +1,4 @@ -import { +import { type Hash, type Block } from 'viem'; @@ -7,7 +7,7 @@ import { getPublicClient } from './clients.js'; /** * Get the current block number for a specific network */ -export async function getBlockNumber(network = 'ethereum'): Promise { +export async function getBlockNumber(network = 'sei'): Promise { const client = getPublicClient(network); return await client.getBlockNumber(); } @@ -16,8 +16,8 @@ export async function getBlockNumber(network = 'ethereum'): Promise { * Get a block by number for a specific network */ export async function getBlockByNumber( - blockNumber: number, - network = 'ethereum' + blockNumber: number, + network = 'sei' ): Promise { const client = getPublicClient(network); return await client.getBlock({ blockNumber: BigInt(blockNumber) }); @@ -27,8 +27,8 @@ export async function getBlockByNumber( * Get a block by hash for a specific network */ export async function getBlockByHash( - blockHash: Hash, - network = 'ethereum' + blockHash: Hash, + network = 'sei' ): Promise { const client = getPublicClient(network); return await client.getBlock({ blockHash }); @@ -37,7 +37,7 @@ export async function getBlockByHash( /** * Get the latest block for a specific network */ -export async function getLatestBlock(network = 'ethereum'): Promise { +export async function getLatestBlock(network = 'sei'): Promise { const client = getPublicClient(network); return await client.getBlock(); -} \ No newline at end of file +} diff --git a/src/core/services/clients.ts b/src/core/services/clients.ts index c74c125..41594ee 100644 --- a/src/core/services/clients.ts +++ b/src/core/services/clients.ts @@ -1,7 +1,7 @@ -import { - createPublicClient, - createWalletClient, - http, +import { + createPublicClient, + createWalletClient, + http, type PublicClient, type WalletClient, type Hex, @@ -16,37 +16,37 @@ const clientCache = new Map(); /** * Get a public client for a specific network */ -export function getPublicClient(network = 'ethereum'): PublicClient { +export function getPublicClient(network = 'sei'): PublicClient { const cacheKey = String(network); - + // Return cached client if available if (clientCache.has(cacheKey)) { return clientCache.get(cacheKey)!; } - + // Create a new client const chain = getChain(network); const rpcUrl = getRpcUrl(network); - + const client = createPublicClient({ chain, transport: http(rpcUrl) }); - + // Cache the client clientCache.set(cacheKey, client); - + return client; } /** * Create a wallet client for a specific network and private key */ -export function getWalletClient(privateKey: Hex, network = 'ethereum'): WalletClient { +export function getWalletClient(privateKey: Hex, network = 'sei'): WalletClient { const chain = getChain(network); const rpcUrl = getRpcUrl(network); const account = privateKeyToAccount(privateKey); - + return createWalletClient({ account, chain, @@ -55,11 +55,11 @@ export function getWalletClient(privateKey: Hex, network = 'ethereum'): WalletCl } /** - * Get an Ethereum address from a private key + * Get an EVM address from a private key * @param privateKey The private key in hex format (with or without 0x prefix) - * @returns The Ethereum address derived from the private key + * @returns The EVM address derived from the private key */ export function getAddressFromPrivateKey(privateKey: Hex): Address { const account = privateKeyToAccount(privateKey); return account.address; -} \ No newline at end of file +} diff --git a/src/core/services/contracts.ts b/src/core/services/contracts.ts index faebb67..ad065d7 100644 --- a/src/core/services/contracts.ts +++ b/src/core/services/contracts.ts @@ -1,53 +1,62 @@ -import { - type Address, - type Hash, +import { + type Hash, type Hex, type ReadContractParameters, type GetLogsParameters, type Log } from 'viem'; import { getPublicClient, getWalletClient } from './clients.js'; -import { resolveAddress } from './ens.js'; +import * as services from "./index.js"; +import { getPrivateKeyAsHex } from '../config.js'; /** * Read from a contract for a specific network */ -export async function readContract(params: ReadContractParameters, network = 'ethereum') { +export async function readContract(params: ReadContractParameters, network = 'sei') { const client = getPublicClient(network); return await client.readContract(params); } /** * Write to a contract for a specific network + * @param params Contract parameters + * @param network Network name or chain ID + * @returns Transaction hash + * @throws Error if no private key is available */ export async function writeContract( - privateKey: Hex, - params: Record, - network = 'ethereum' + params: Record, + network = 'sei' ): Promise { - const client = getWalletClient(privateKey, network); + // Get private key from environment + const key = getPrivateKeyAsHex(); + + if (!key) { + throw new Error('Private key not available. Set the PRIVATE_KEY environment variable and restart the MCP server.'); + } + + const client = getWalletClient(key, network); return await client.writeContract(params as any); } /** * Get logs for a specific network */ -export async function getLogs(params: GetLogsParameters, network = 'ethereum'): Promise { +export async function getLogs(params: GetLogsParameters, network = 'sei'): Promise { const client = getPublicClient(network); return await client.getLogs(params); } /** * Check if an address is a contract - * @param addressOrEns Address or ENS name to check + * @param address Address * @param network Network name or chain ID * @returns True if the address is a contract, false if it's an EOA */ -export async function isContract(addressOrEns: string, network = 'ethereum'): Promise { - // Resolve ENS name to address if needed - const address = await resolveAddress(addressOrEns, network); - +export async function isContract(address: string, network = 'sei'): Promise { + const validatedAddress = services.helpers.validateAddress(address); + const client = getPublicClient(network); - const code = await client.getBytecode({ address }); + const code = await client.getBytecode({ address: validatedAddress }); return code !== undefined && code !== '0x'; -} \ No newline at end of file +} diff --git a/src/core/services/ens.ts b/src/core/services/ens.ts deleted file mode 100644 index b51784c..0000000 --- a/src/core/services/ens.ts +++ /dev/null @@ -1,46 +0,0 @@ -import { normalize } from 'viem/ens'; -import { getPublicClient } from './clients.js'; -import { type Address } from 'viem'; - -/** - * Resolves an ENS name to an Ethereum address or returns the original address if it's already valid - * @param addressOrEns An Ethereum address or ENS name - * @param network The network to use for ENS resolution (defaults to Ethereum mainnet) - * @returns The resolved Ethereum address - */ -export async function resolveAddress( - addressOrEns: string, - network = 'ethereum' -): Promise
{ - // If it's already a valid Ethereum address (0x followed by 40 hex chars), return it - if (/^0x[a-fA-F0-9]{40}$/.test(addressOrEns)) { - return addressOrEns as Address; - } - - // If it looks like an ENS name (contains a dot), try to resolve it - if (addressOrEns.includes('.')) { - try { - // Normalize the ENS name first - const normalizedEns = normalize(addressOrEns); - - // Get the public client for the network - const publicClient = getPublicClient(network); - - // Resolve the ENS name to an address - const address = await publicClient.getEnsAddress({ - name: normalizedEns, - }); - - if (!address) { - throw new Error(`ENS name ${addressOrEns} could not be resolved to an address`); - } - - return address; - } catch (error: any) { - throw new Error(`Failed to resolve ENS name ${addressOrEns}: ${error.message}`); - } - } - - // If it's neither a valid address nor an ENS name, throw an error - throw new Error(`Invalid address or ENS name: ${addressOrEns}`); -} \ No newline at end of file diff --git a/src/core/services/index.ts b/src/core/services/index.ts index f7da50a..90b40a6 100644 --- a/src/core/services/index.ts +++ b/src/core/services/index.ts @@ -6,15 +6,14 @@ export * from './blocks.js'; export * from './transactions.js'; export * from './contracts.js'; export * from './tokens.js'; -export * from './ens.js'; export { utils as helpers } from './utils.js'; // Re-export common types for convenience -export type { - Address, - Hash, +export type { + Address, + Hash, Hex, Block, TransactionReceipt, Log -} from 'viem'; \ No newline at end of file +} from 'viem'; diff --git a/src/core/services/tokens.ts b/src/core/services/tokens.ts index e0d8306..aef9b0d 100644 --- a/src/core/services/tokens.ts +++ b/src/core/services/tokens.ts @@ -1,5 +1,5 @@ -import { - type Address, +import { + type Address, type Hex, type Hash, formatUnits, @@ -80,7 +80,7 @@ const erc1155Abi = [ */ export async function getERC20TokenInfo( tokenAddress: Address, - network: string = 'ethereum' + network: string = 'sei' ): Promise<{ name: string; symbol: string; @@ -118,7 +118,7 @@ export async function getERC20TokenInfo( export async function getERC721TokenMetadata( tokenAddress: Address, tokenId: bigint, - network: string = 'ethereum' + network: string = 'sei' ): Promise<{ name: string; symbol: string; @@ -151,7 +151,7 @@ export async function getERC721TokenMetadata( export async function getERC1155TokenURI( tokenAddress: Address, tokenId: bigint, - network: string = 'ethereum' + network: string = 'sei' ): Promise { const publicClient = getPublicClient(network); @@ -162,4 +162,4 @@ export async function getERC1155TokenURI( }); return contract.read.uri([tokenId]); -} \ No newline at end of file +} diff --git a/src/core/services/transactions.ts b/src/core/services/transactions.ts index 52954e6..667b073 100644 --- a/src/core/services/transactions.ts +++ b/src/core/services/transactions.ts @@ -1,6 +1,6 @@ -import { - type Address, - type Hash, +import { + type Address, + type Hash, type TransactionReceipt, type EstimateGasParameters } from 'viem'; @@ -9,7 +9,7 @@ import { getPublicClient } from './clients.js'; /** * Get a transaction by hash for a specific network */ -export async function getTransaction(hash: Hash, network = 'ethereum') { +export async function getTransaction(hash: Hash, network = 'sei') { const client = getPublicClient(network); return await client.getTransaction({ hash }); } @@ -17,7 +17,7 @@ export async function getTransaction(hash: Hash, network = 'ethereum') { /** * Get a transaction receipt by hash for a specific network */ -export async function getTransactionReceipt(hash: Hash, network = 'ethereum'): Promise { +export async function getTransactionReceipt(hash: Hash, network = 'sei'): Promise { const client = getPublicClient(network); return await client.getTransactionReceipt({ hash }); } @@ -25,7 +25,7 @@ export async function getTransactionReceipt(hash: Hash, network = 'ethereum'): P /** * Get the transaction count for an address for a specific network */ -export async function getTransactionCount(address: Address, network = 'ethereum'): Promise { +export async function getTransactionCount(address: Address, network = 'sei'): Promise { const client = getPublicClient(network); const count = await client.getTransactionCount({ address }); return Number(count); @@ -34,7 +34,7 @@ export async function getTransactionCount(address: Address, network = 'ethereum' /** * Estimate gas for a transaction for a specific network */ -export async function estimateGas(params: EstimateGasParameters, network = 'ethereum'): Promise { +export async function estimateGas(params: EstimateGasParameters, network = 'sei'): Promise { const client = getPublicClient(network); return await client.estimateGas(params); } @@ -42,8 +42,8 @@ export async function estimateGas(params: EstimateGasParameters, network = 'ethe /** * Get the chain ID for a specific network */ -export async function getChainId(network = 'ethereum'): Promise { +export async function getChainId(network = 'sei'): Promise { const client = getPublicClient(network); const chainId = await client.getChainId(); return Number(chainId); -} \ No newline at end of file +} diff --git a/src/core/services/transfer.ts b/src/core/services/transfer.ts index 06f8f75..d701843 100644 --- a/src/core/services/transfer.ts +++ b/src/core/services/transfer.ts @@ -1,17 +1,14 @@ -import { +import { parseEther, parseUnits, - formatUnits, - type Address, - type Hash, + type Address, + type Hash, type Hex, - type Abi, getContract, - type Account } from 'viem'; import { getPublicClient, getWalletClient } from './clients.js'; -import { getChain } from '../chains.js'; -import { resolveAddress } from './ens.js'; +import * as services from "./index.js"; +import { getPrivateKeyAsHex } from '../config.js'; // Standard ERC20 ABI for transfers const erc20TransferAbi = [ @@ -115,32 +112,31 @@ const erc1155TransferAbi = [ ] as const; /** - * Transfer ETH to an address - * @param privateKey Sender's private key - * @param toAddressOrEns Recipient address or ENS name - * @param amount Amount to send in ETH + * Transfer Sei to an address + * @param toAddress Recipient address + * @param amount Amount to send in Sei * @param network Network name or chain ID * @returns Transaction hash + * @throws Error if no private key is available */ -export async function transferETH( - privateKey: string | Hex, - toAddressOrEns: string, +export async function transferSei( + toAddress: string, amount: string, // in ether - network = 'ethereum' + network = 'sei' ): Promise { - // Resolve ENS name to address if needed - const toAddress = await resolveAddress(toAddressOrEns, network); - - // Ensure the private key has 0x prefix - const formattedKey = typeof privateKey === 'string' && !privateKey.startsWith('0x') - ? `0x${privateKey}` as Hex - : privateKey as Hex; - - const client = getWalletClient(formattedKey, network); + const validatedToAddress = services.helpers.validateAddress(toAddress); + // Get private key from environment + const privateKey = getPrivateKeyAsHex(); + + if (!privateKey) { + throw new Error('Private key not available. Set the PRIVATE_KEY environment variable and restart the MCP server.'); + } + + const client = getWalletClient(privateKey, network); const amountWei = parseEther(amount); - + return client.sendTransaction({ - to: toAddress, + to: validatedToAddress, value: amountWei, account: client.account!, chain: client.chain @@ -149,19 +145,18 @@ export async function transferETH( /** * Transfer ERC20 tokens to an address - * @param tokenAddressOrEns Token contract address or ENS name - * @param toAddressOrEns Recipient address or ENS name + * @param tokenAddress Token contract address + * @param toAddress Recipient address * @param amount Amount to send (in token units) - * @param privateKey Sender's private key * @param network Network name or chain ID * @returns Transaction details + * @throws Error if no private key is available */ export async function transferERC20( - tokenAddressOrEns: string, - toAddressOrEns: string, + tokenAddress: string, + toAddress: string, amount: string, - privateKey: string | `0x${string}`, - network: string = 'ethereum' + network: string = 'sei' ): Promise<{ txHash: Hash; amount: { @@ -173,43 +168,44 @@ export async function transferERC20( decimals: number; }; }> { - // Resolve ENS names to addresses if needed - const tokenAddress = await resolveAddress(tokenAddressOrEns, network) as Address; - const toAddress = await resolveAddress(toAddressOrEns, network) as Address; - - // Ensure the private key has 0x prefix - const formattedKey = typeof privateKey === 'string' && !privateKey.startsWith('0x') - ? `0x${privateKey}` as `0x${string}` - : privateKey as `0x${string}`; - - // Get token details + const validatedTokenAddress = services.helpers.validateAddress(tokenAddress); + const validatedToAddress = services.helpers.validateAddress(toAddress); + // Get private key from environment + const privateKey = getPrivateKeyAsHex(); + + if (!privateKey) { + throw new Error('Private key not available. Set the PRIVATE_KEY environment variable and restart the MCP server.'); + } + const publicClient = getPublicClient(network); + + // Get token details const contract = getContract({ - address: tokenAddress, + address: tokenAddress as Address, abi: erc20TransferAbi, client: publicClient, }); - + // Get token decimals and symbol const decimals = await contract.read.decimals(); const symbol = await contract.read.symbol(); - + // Parse the amount with the correct number of decimals const rawAmount = parseUnits(amount, decimals); - + // Create wallet client for sending the transaction - const walletClient = getWalletClient(formattedKey, network); - + const walletClient = getWalletClient(privateKey, network); + // Send the transaction const hash = await walletClient.writeContract({ - address: tokenAddress, + address: validatedTokenAddress, abi: erc20TransferAbi, functionName: 'transfer', - args: [toAddress, rawAmount], + args: [validatedToAddress, rawAmount], account: walletClient.account!, chain: walletClient.chain }); - + return { txHash: hash, amount: { @@ -225,19 +221,18 @@ export async function transferERC20( /** * Approve ERC20 token spending - * @param tokenAddressOrEns Token contract address or ENS name - * @param spenderAddressOrEns Spender address or ENS name + * @param tokenAddress Token contract address + * @param spenderAddress Spender address * @param amount Amount to approve (in token units) - * @param privateKey Owner's private key * @param network Network name or chain ID * @returns Transaction details + * @throws Error if no private key is available */ export async function approveERC20( - tokenAddressOrEns: string, - spenderAddressOrEns: string, + tokenAddress: string, + spenderAddress: string, amount: string, - privateKey: string | `0x${string}`, - network: string = 'ethereum' + network: string = 'sei' ): Promise<{ txHash: Hash; amount: { @@ -249,43 +244,43 @@ export async function approveERC20( decimals: number; }; }> { - // Resolve ENS names to addresses if needed - const tokenAddress = await resolveAddress(tokenAddressOrEns, network) as Address; - const spenderAddress = await resolveAddress(spenderAddressOrEns, network) as Address; - - // Ensure the private key has 0x prefix - const formattedKey = typeof privateKey === 'string' && !privateKey.startsWith('0x') - ? `0x${privateKey}` as `0x${string}` - : privateKey as `0x${string}`; - - // Get token details + const validatedTokenAddress = services.helpers.validateAddress(tokenAddress); + const validatedSpenderAddress = services.helpers.validateAddress(spenderAddress); + + // Get private key from environment + const privateKey = getPrivateKeyAsHex(); + + if (!privateKey) { + throw new Error('Private key not available. Set the PRIVATE_KEY environment variable and restart the MCP server.'); + } + const publicClient = getPublicClient(network); const contract = getContract({ - address: tokenAddress, + address: validatedTokenAddress, abi: erc20TransferAbi, client: publicClient, }); - + // Get token decimals and symbol const decimals = await contract.read.decimals(); const symbol = await contract.read.symbol(); - + // Parse the amount with the correct number of decimals const rawAmount = parseUnits(amount, decimals); - + // Create wallet client for sending the transaction - const walletClient = getWalletClient(formattedKey, network); - + const walletClient = getWalletClient(privateKey, network); + // Send the transaction const hash = await walletClient.writeContract({ - address: tokenAddress, + address: validatedTokenAddress, abi: erc20TransferAbi, functionName: 'approve', - args: [spenderAddress, rawAmount], + args: [validatedSpenderAddress, rawAmount], account: walletClient.account!, chain: walletClient.chain }); - + return { txHash: hash, amount: { @@ -301,19 +296,18 @@ export async function approveERC20( /** * Transfer an NFT (ERC721) to an address - * @param tokenAddressOrEns NFT contract address or ENS name - * @param toAddressOrEns Recipient address or ENS name + * @param tokenAddress NFT contract address + * @param toAddress Recipient address * @param tokenId Token ID to transfer - * @param privateKey Owner's private key * @param network Network name or chain ID * @returns Transaction details + * @throws Error if no private key is available */ export async function transferERC721( - tokenAddressOrEns: string, - toAddressOrEns: string, + tokenAddress: string, + toAddress: string, tokenId: bigint, - privateKey: string | `0x${string}`, - network: string = 'ethereum' + network: string = 'sei' ): Promise<{ txHash: Hash; tokenId: string; @@ -322,41 +316,41 @@ export async function transferERC721( symbol: string; }; }> { - // Resolve ENS names to addresses if needed - const tokenAddress = await resolveAddress(tokenAddressOrEns, network) as Address; - const toAddress = await resolveAddress(toAddressOrEns, network) as Address; - - // Ensure the private key has 0x prefix - const formattedKey = typeof privateKey === 'string' && !privateKey.startsWith('0x') - ? `0x${privateKey}` as `0x${string}` - : privateKey as `0x${string}`; - + const validatedTokenAddress = services.helpers.validateAddress(tokenAddress); + const validatedToAddress = services.helpers.validateAddress(toAddress); + // Get private key from environment + const privateKey = getPrivateKeyAsHex(); + + if (!privateKey) { + throw new Error('Private key not available. Set the PRIVATE_KEY environment variable and restart the MCP server.'); + } + // Create wallet client for sending the transaction - const walletClient = getWalletClient(formattedKey, network); + const walletClient = getWalletClient(privateKey, network); const fromAddress = walletClient.account!.address; - + // Send the transaction const hash = await walletClient.writeContract({ - address: tokenAddress, + address: validatedTokenAddress, abi: erc721TransferAbi, functionName: 'transferFrom', - args: [fromAddress, toAddress, tokenId], + args: [fromAddress, validatedToAddress, tokenId], account: walletClient.account!, chain: walletClient.chain }); - + // Get token metadata const publicClient = getPublicClient(network); const contract = getContract({ - address: tokenAddress, + address: validatedTokenAddress, abi: erc721TransferAbi, client: publicClient, }); - + // Get token name and symbol let name = 'Unknown'; let symbol = 'NFT'; - + try { [name, symbol] = await Promise.all([ contract.read.name(), @@ -365,7 +359,7 @@ export async function transferERC721( } catch (error) { console.error('Error fetching NFT metadata:', error); } - + return { txHash: hash, tokenId: tokenId.toString(), @@ -378,55 +372,54 @@ export async function transferERC721( /** * Transfer ERC1155 tokens to an address - * @param tokenAddressOrEns Token contract address or ENS name - * @param toAddressOrEns Recipient address or ENS name + * @param tokenAddress Token contract + * @param toAddress Recipient address * @param tokenId Token ID to transfer * @param amount Amount to transfer - * @param privateKey Owner's private key * @param network Network name or chain ID * @returns Transaction details + * @throws Error if no private key is available */ export async function transferERC1155( - tokenAddressOrEns: string, - toAddressOrEns: string, + tokenAddress: string, + toAddress: string, tokenId: bigint, amount: string, - privateKey: string | `0x${string}`, - network: string = 'ethereum' + network: string = 'sei' ): Promise<{ txHash: Hash; tokenId: string; amount: string; }> { - // Resolve ENS names to addresses if needed - const tokenAddress = await resolveAddress(tokenAddressOrEns, network) as Address; - const toAddress = await resolveAddress(toAddressOrEns, network) as Address; - - // Ensure the private key has 0x prefix - const formattedKey = typeof privateKey === 'string' && !privateKey.startsWith('0x') - ? `0x${privateKey}` as `0x${string}` - : privateKey as `0x${string}`; - + const validatedTokenAddress = services.helpers.validateAddress(tokenAddress); + const validatedToAddress = services.helpers.validateAddress(toAddress); + // Get private key from environment + const privateKey = getPrivateKeyAsHex(); + + if (!privateKey) { + throw new Error('Private key not available. Set the PRIVATE_KEY environment variable and restart the MCP server.'); + } + // Create wallet client for sending the transaction - const walletClient = getWalletClient(formattedKey, network); + const walletClient = getWalletClient(privateKey, network); const fromAddress = walletClient.account!.address; - + // Parse amount to bigint const amountBigInt = BigInt(amount); - + // Send the transaction const hash = await walletClient.writeContract({ - address: tokenAddress, + address: validatedTokenAddress, abi: erc1155TransferAbi, functionName: 'safeTransferFrom', - args: [fromAddress, toAddress, tokenId, amountBigInt, '0x'], + args: [fromAddress, validatedToAddress, tokenId, amountBigInt, '0x'], account: walletClient.account!, chain: walletClient.chain }); - + return { txHash: hash, tokenId: tokenId.toString(), amount }; -} \ No newline at end of file +} diff --git a/src/core/services/utils.ts b/src/core/services/utils.ts index f8ed8e5..c2965ec 100644 --- a/src/core/services/utils.ts +++ b/src/core/services/utils.ts @@ -1,43 +1,25 @@ -import { - parseEther, - formatEther, - type Account, - type Hash, - type Chain, - type WalletClient, - type Transport, - type HttpTransport +import { + parseEther, + type Address } from 'viem'; /** * Utility functions for formatting and parsing values */ export const utils = { - // Convert ether to wei - parseEther, - - // Convert wei to ether - formatEther, - - // Format a bigint to a string - formatBigInt: (value: bigint): string => value.toString(), - - // Format an object to JSON with bigint handling - formatJson: (obj: unknown): string => JSON.stringify(obj, (_, value) => - typeof value === 'bigint' ? value.toString() : value, 2), - - // Format a number with commas - formatNumber: (value: number | string): string => { - return Number(value).toLocaleString(); - }, - - // Convert a hex string to a number - hexToNumber: (hex: string): number => { - return parseInt(hex, 16); - }, - - // Convert a number to a hex string - numberToHex: (num: number): string => { - return '0x' + num.toString(16); - } -}; \ No newline at end of file + // Convert ether to wei + parseEther, + + // Format an object to JSON with bigint handling + formatJson: (obj: unknown): string => JSON.stringify(obj, (_, value) => + typeof value === 'bigint' ? value.toString() : value, 2), + + validateAddress: (address: string): Address => { + // If it's already a valid Sei 0x address (0x followed by 40 hex chars), return it + if (/^0x[a-fA-F0-9]{40}$/.test(address)) { + return address as Address; + } + + throw new Error(`Invalid address: ${address}`); + } +}; diff --git a/src/core/tools.ts b/src/core/tools.ts index a01d7a7..8f49822 100644 --- a/src/core/tools.ts +++ b/src/core/tools.ts @@ -2,33 +2,30 @@ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; import { z } from "zod"; import { getSupportedNetworks, getRpcUrl } from "./chains.js"; import * as services from "./services/index.js"; -import { type Address, type Hex, type Hash } from 'viem'; -import { normalize } from 'viem/ens'; +import {type Address, type Hex, type Hash, WriteContractParameters, Abi} from 'viem'; +import { getPrivateKeyAsHex } from "./config.js"; /** * Register all EVM-related tools with the MCP server - * - * All tools that accept Ethereum addresses also support ENS names (e.g., 'vitalik.eth'). - * ENS names are automatically resolved to addresses using the Ethereum Name Service. - * + * * @param server The MCP server instance */ export function registerEVMTools(server: McpServer) { // NETWORK INFORMATION TOOLS - + // Get chain information server.tool( "get_chain_info", - "Get information about an EVM network", + "Get information about Sei network", { - network: z.string().optional().describe("Network name (e.g., 'ethereum', 'optimism', 'arbitrum', 'base', etc.) or chain ID. Supports all EVM-compatible networks. Defaults to Ethereum mainnet.") + network: z.string().optional().describe("Network name (e.g., 'sei', 'sei-testnet', 'sei-devnet', etc.) or chain ID. Supports all EVM-compatible networks. Defaults to Sei mainnet.") }, - async ({ network = "ethereum" }) => { + async ({ network = "sei" }) => { try { const chainId = await services.getChainId(network); const blockNumber = await services.getBlockNumber(network); const rpcUrl = getRpcUrl(network); - + return { content: [{ type: "text", @@ -52,58 +49,6 @@ export function registerEVMTools(server: McpServer) { } ); - // ENS LOOKUP TOOL - - // Resolve ENS name to address - server.tool( - "resolve_ens", - "Resolve an ENS name to an Ethereum address", - { - ensName: z.string().describe("ENS name to resolve (e.g., 'vitalik.eth')"), - network: z.string().optional().describe("Network name (e.g., 'ethereum', 'optimism', 'arbitrum', 'base', etc.) or chain ID. ENS resolution works best on Ethereum mainnet. Defaults to Ethereum mainnet.") - }, - async ({ ensName, network = "ethereum" }) => { - try { - // Validate that the input is an ENS name - if (!ensName.includes('.')) { - return { - content: [{ - type: "text", - text: `Error: Input "${ensName}" is not a valid ENS name. ENS names must contain a dot (e.g., 'name.eth').` - }], - isError: true - }; - } - - // Normalize the ENS name - const normalizedEns = normalize(ensName); - - // Resolve the ENS name to an address - const address = await services.resolveAddress(ensName, network); - - return { - content: [{ - type: "text", - text: JSON.stringify({ - ensName: ensName, - normalizedName: normalizedEns, - resolvedAddress: address, - network - }, null, 2) - }] - }; - } catch (error) { - return { - content: [{ - type: "text", - text: `Error resolving ENS name: ${error instanceof Error ? error.message : String(error)}` - }], - isError: true - }; - } - } - ); - // Get supported networks server.tool( "get_supported_networks", @@ -112,7 +57,7 @@ export function registerEVMTools(server: McpServer) { async () => { try { const networks = getSupportedNetworks(); - + return { content: [{ type: "text", @@ -134,19 +79,19 @@ export function registerEVMTools(server: McpServer) { ); // BLOCK TOOLS - + // Get block by number server.tool( "get_block_by_number", "Get a block by its block number", { blockNumber: z.number().describe("The block number to fetch"), - network: z.string().optional().describe("Network name or chain ID. Defaults to Ethereum mainnet.") + network: z.string().optional().describe("Network name or chain ID. Defaults to Sei mainnet.") }, - async ({ blockNumber, network = "ethereum" }) => { + async ({ blockNumber, network = "sei" }) => { try { const block = await services.getBlockByNumber(blockNumber, network); - + return { content: [{ type: "text", @@ -170,12 +115,12 @@ export function registerEVMTools(server: McpServer) { "get_latest_block", "Get the latest block from the EVM", { - network: z.string().optional().describe("Network name or chain ID. Defaults to Ethereum mainnet.") + network: z.string().optional().describe("Network name or chain ID. Defaults to Sei mainnet.") }, - async ({ network = "ethereum" }) => { + async ({ network = "sei" }) => { try { const block = await services.getLatestBlock(network); - + return { content: [{ type: "text", @@ -195,19 +140,19 @@ export function registerEVMTools(server: McpServer) { ); // BALANCE TOOLS - - // Get ETH balance + + // Get Sei balance server.tool( "get_balance", - "Get the native token balance (ETH, MATIC, etc.) for an address", + "Get the native token balance (Sei) for an address", { - address: z.string().describe("The wallet address or ENS name (e.g., '0x1234...' or 'vitalik.eth') to check the balance for"), - network: z.string().optional().describe("Network name (e.g., 'ethereum', 'optimism', 'arbitrum', 'base', etc.) or chain ID. Supports all EVM-compatible networks. Defaults to Ethereum mainnet.") + address: z.string().describe("The wallet address name (e.g., '0x1234...' or 'vitalik.eth') to check the balance for"), + network: z.string().optional().describe("Network name (e.g., 'sei', 'sei-testnet', 'sei-devnet', etc.) or chain ID. Supports all EVM-compatible networks. Defaults to Sei mainnet.") }, - async ({ address, network = "ethereum" }) => { + async ({ address, network = "sei" }) => { try { - const balance = await services.getETHBalance(address, network); - + const balance = await services.getBalance(address, network); + return { content: [{ type: "text", @@ -234,20 +179,20 @@ export function registerEVMTools(server: McpServer) { // Get ERC20 balance server.tool( "get_erc20_balance", - "Get the ERC20 token balance of an Ethereum address", + "Get the ERC20 token balance of an EVM address", { - address: z.string().describe("The Ethereum address to check"), + address: z.string().describe("The EVM address to check"), tokenAddress: z.string().describe("The ERC20 token contract address"), - network: z.string().optional().describe("Network name or chain ID. Defaults to Ethereum mainnet.") + network: z.string().optional().describe("Network name or chain ID. Defaults to Sei mainnet.") }, - async ({ address, tokenAddress, network = "ethereum" }) => { + async ({ address, tokenAddress, network = "sei" }) => { try { const balance = await services.getERC20Balance( tokenAddress as Address, address as Address, network ); - + return { content: [{ type: "text", @@ -280,14 +225,14 @@ export function registerEVMTools(server: McpServer) { "get_token_balance", "Get the balance of an ERC20 token for an address", { - tokenAddress: z.string().describe("The contract address or ENS name of the ERC20 token (e.g., '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48' for USDC or 'uniswap.eth')"), - ownerAddress: z.string().describe("The wallet address or ENS name to check the balance for (e.g., '0x1234...' or 'vitalik.eth')"), - network: z.string().optional().describe("Network name (e.g., 'ethereum', 'optimism', 'arbitrum', 'base', etc.) or chain ID. Supports all EVM-compatible networks. Defaults to Ethereum mainnet.") + tokenAddress: z.string().describe("The contract address name of the ERC20 token (e.g., '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48')"), + ownerAddress: z.string().describe("The wallet address name to check the balance for (e.g., '0x1234...' or 'vitalik.eth')"), + network: z.string().optional().describe("Network name (e.g., 'sei', 'sei-testnet', 'sei-devnet', etc.) or chain ID. Supports all EVM-compatible networks. Defaults to Sei mainnet.") }, - async ({ tokenAddress, ownerAddress, network = "ethereum" }) => { + async ({ tokenAddress, ownerAddress, network = "sei" }) => { try { const balance = await services.getERC20Balance(tokenAddress, ownerAddress, network); - + return { content: [{ type: "text", @@ -315,19 +260,19 @@ export function registerEVMTools(server: McpServer) { ); // TRANSACTION TOOLS - + // Get transaction by hash server.tool( "get_transaction", "Get detailed information about a specific transaction by its hash. Includes sender, recipient, value, data, and more.", { txHash: z.string().describe("The transaction hash to look up (e.g., '0x1234...')"), - network: z.string().optional().describe("Network name (e.g., 'ethereum', 'optimism', 'arbitrum', 'base', 'polygon') or chain ID. Defaults to Ethereum mainnet.") + network: z.string().optional().describe("Network name (e.g., 'sei', 'sei-testnet', 'sei-devnet', etc.) or chain ID. Defaults to Sei mainnet.") }, - async ({ txHash, network = "ethereum" }) => { + async ({ txHash, network = "sei" }) => { try { const tx = await services.getTransaction(txHash as Hash, network); - + return { content: [{ type: "text", @@ -352,12 +297,12 @@ export function registerEVMTools(server: McpServer) { "Get a transaction receipt by its hash", { txHash: z.string().describe("The transaction hash to look up"), - network: z.string().optional().describe("Network name or chain ID. Defaults to Ethereum mainnet.") + network: z.string().optional().describe("Network name or chain ID. Defaults to Sei mainnet.") }, - async ({ txHash, network = "ethereum" }) => { + async ({ txHash, network = "sei" }) => { try { const receipt = await services.getTransactionReceipt(txHash as Hash, network); - + return { content: [{ type: "text", @@ -382,24 +327,24 @@ export function registerEVMTools(server: McpServer) { "Estimate the gas cost for a transaction", { to: z.string().describe("The recipient address"), - value: z.string().optional().describe("The amount of ETH to send in ether (e.g., '0.1')"), + value: z.string().optional().describe("The amount of Sei to send (e.g., '0.1')"), data: z.string().optional().describe("The transaction data as a hex string"), - network: z.string().optional().describe("Network name or chain ID. Defaults to Ethereum mainnet.") + network: z.string().optional().describe("Network name or chain ID. Defaults to Sei mainnet.") }, - async ({ to, value, data, network = "ethereum" }) => { + async ({ to, value, data, network = "sei" }) => { try { const params: any = { to: to as Address }; - + if (value) { params.value = services.helpers.parseEther(value); } - + if (data) { params.data = data as `0x${string}`; } - + const gas = await services.estimateGas(params, network); - + return { content: [{ type: "text", @@ -422,21 +367,20 @@ export function registerEVMTools(server: McpServer) { ); // TRANSFER TOOLS - - // Transfer ETH + + // Transfer Sei server.tool( - "transfer_eth", - "Transfer native tokens (ETH, MATIC, etc.) to an address", + "transfer_sei", + "Transfer native tokens (Sei) to an address", { - privateKey: z.string().describe("Private key of the sender account in hex format (with or without 0x prefix). SECURITY: This is used only for transaction signing and is not stored."), to: z.string().describe("The recipient address or ENS name (e.g., '0x1234...' or 'vitalik.eth')"), - amount: z.string().describe("Amount to send in ETH (or the native token of the network), as a string (e.g., '0.1')"), - network: z.string().optional().describe("Network name (e.g., 'ethereum', 'optimism', 'arbitrum', 'base', etc.) or chain ID. Supports all EVM-compatible networks. Defaults to Ethereum mainnet.") + amount: z.string().describe("Amount to send in SEI (or the native token of the network), as a string (e.g., '0.1')"), + network: z.string().optional().describe("Network name (e.g., 'sei', 'sei-testnet', 'sei-devnet') or chain ID. Defaults to Sei mainnet.") }, - async ({ privateKey, to, amount, network = "ethereum" }) => { + async ({ to, amount, network = "sei" }) => { try { - const txHash = await services.transferETH(privateKey, to, amount, network); - + const txHash = await services.transferSei(to, amount, network); + return { content: [{ type: "text", @@ -453,7 +397,7 @@ export function registerEVMTools(server: McpServer) { return { content: [{ type: "text", - text: `Error transferring ETH: ${error instanceof Error ? error.message : String(error)}` + text: `Error transferring Sei: ${error instanceof Error ? error.message : String(error)}` }], isError: true }; @@ -466,27 +410,20 @@ export function registerEVMTools(server: McpServer) { "transfer_erc20", "Transfer ERC20 tokens to another address", { - privateKey: z.string().describe("Private key of the sending account (this is used for signing and is never stored)"), tokenAddress: z.string().describe("The address of the ERC20 token contract"), toAddress: z.string().describe("The recipient address"), amount: z.string().describe("The amount of tokens to send (in token units, e.g., '10' for 10 tokens)"), - network: z.string().optional().describe("Network name (e.g., 'ethereum', 'optimism', 'arbitrum', 'base', etc.) or chain ID. Supports all EVM-compatible networks. Defaults to Ethereum mainnet.") + network: z.string().optional().describe("Network name (e.g., 'sei', 'sei-testnet', 'sei-devnet') or chain ID. Defaults to Sei mainnet.") }, - async ({ privateKey, tokenAddress, toAddress, amount, network = "ethereum" }) => { + async ({ tokenAddress, toAddress, amount, network = "sei" }) => { try { - // Get the formattedKey with 0x prefix - const formattedKey = privateKey.startsWith('0x') - ? privateKey as `0x${string}` - : `0x${privateKey}` as `0x${string}`; - const result = await services.transferERC20( - tokenAddress as Address, - toAddress as Address, + tokenAddress, + toAddress, amount, - formattedKey, network ); - + return { content: [{ type: "text", @@ -518,27 +455,20 @@ export function registerEVMTools(server: McpServer) { "approve_token_spending", "Approve another address (like a DeFi protocol or exchange) to spend your ERC20 tokens. This is often required before interacting with DeFi protocols.", { - privateKey: z.string().describe("Private key of the token owner account in hex format (with or without 0x prefix). SECURITY: This is used only for transaction signing and is not stored."), - tokenAddress: z.string().describe("The contract address of the ERC20 token to approve for spending (e.g., '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48' for USDC on Ethereum)"), + tokenAddress: z.string().describe("The contract address of the ERC20 token to approve for spending (e.g., '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48' for USDC)"), spenderAddress: z.string().describe("The contract address being approved to spend your tokens (e.g., a DEX or lending protocol)"), amount: z.string().describe("The amount of tokens to approve in token units, not wei (e.g., '1000' to approve spending 1000 tokens). Use a very large number for unlimited approval."), - network: z.string().optional().describe("Network name (e.g., 'ethereum', 'optimism', 'arbitrum', 'base', 'polygon') or chain ID. Defaults to Ethereum mainnet.") + network: z.string().optional().describe("Network name (e.g., 'sei', 'sei-testnet', 'sei-devnet') or chain ID. Defaults to Sei mainnet.") }, - async ({ privateKey, tokenAddress, spenderAddress, amount, network = "ethereum" }) => { + async ({ tokenAddress, spenderAddress, amount, network = "sei" }) => { try { - // Get the formattedKey with 0x prefix - const formattedKey = privateKey.startsWith('0x') - ? privateKey as `0x${string}` - : `0x${privateKey}` as `0x${string}`; - const result = await services.approveERC20( - tokenAddress as Address, - spenderAddress as Address, + tokenAddress, + spenderAddress, amount, - formattedKey, network ); - + return { content: [{ type: "text", @@ -570,27 +500,20 @@ export function registerEVMTools(server: McpServer) { "transfer_nft", "Transfer an NFT (ERC721 token) from one address to another. Requires the private key of the current owner for signing the transaction.", { - privateKey: z.string().describe("Private key of the NFT owner account in hex format (with or without 0x prefix). SECURITY: This is used only for transaction signing and is not stored."), tokenAddress: z.string().describe("The contract address of the NFT collection (e.g., '0xBC4CA0EdA7647A8aB7C2061c2E118A18a936f13D' for Bored Ape Yacht Club)"), tokenId: z.string().describe("The ID of the specific NFT to transfer (e.g., '1234')"), toAddress: z.string().describe("The recipient wallet address that will receive the NFT"), - network: z.string().optional().describe("Network name (e.g., 'ethereum', 'optimism', 'arbitrum', 'base', 'polygon') or chain ID. Most NFTs are on Ethereum mainnet, which is the default.") + network: z.string().optional().describe("Network name (e.g., 'sei', 'sei-testnet', 'sei-devnet') or chain ID. Most NFTs are on Sei mainnet, which is the default.") }, - async ({ privateKey, tokenAddress, tokenId, toAddress, network = "ethereum" }) => { + async ({ tokenAddress, tokenId, toAddress, network = "sei" }) => { try { - // Get the formattedKey with 0x prefix - const formattedKey = privateKey.startsWith('0x') - ? privateKey as `0x${string}` - : `0x${privateKey}` as `0x${string}`; - const result = await services.transferERC721( - tokenAddress as Address, - toAddress as Address, + tokenAddress, + toAddress, BigInt(tokenId), - formattedKey, network ); - + return { content: [{ type: "text", @@ -623,29 +546,22 @@ export function registerEVMTools(server: McpServer) { "transfer_erc1155", "Transfer ERC1155 tokens to another address. ERC1155 is a multi-token standard that can represent both fungible and non-fungible tokens in a single contract.", { - privateKey: z.string().describe("Private key of the token owner account in hex format (with or without 0x prefix). SECURITY: This is used only for transaction signing and is not stored."), tokenAddress: z.string().describe("The contract address of the ERC1155 token collection (e.g., '0x76BE3b62873462d2142405439777e971754E8E77')"), tokenId: z.string().describe("The ID of the specific token to transfer (e.g., '1234')"), amount: z.string().describe("The quantity of tokens to send (e.g., '1' for a single NFT or '10' for 10 fungible tokens)"), toAddress: z.string().describe("The recipient wallet address that will receive the tokens"), - network: z.string().optional().describe("Network name (e.g., 'ethereum', 'optimism', 'arbitrum', 'base', 'polygon') or chain ID. ERC1155 tokens exist across many networks. Defaults to Ethereum mainnet.") + network: z.string().optional().describe("Network name (e.g., 'sei', 'sei-testnet', 'sei-devnet') or chain ID. ERC1155 tokens exist across many networks. Defaults to Sei mainnet.") }, - async ({ privateKey, tokenAddress, tokenId, amount, toAddress, network = "ethereum" }) => { + async ({ tokenAddress, tokenId, amount, toAddress, network = "sei" }) => { try { - // Get the formattedKey with 0x prefix - const formattedKey = privateKey.startsWith('0x') - ? privateKey as `0x${string}` - : `0x${privateKey}` as `0x${string}`; - const result = await services.transferERC1155( - tokenAddress as Address, - toAddress as Address, + tokenAddress, + toAddress, BigInt(tokenId), amount, - formattedKey, network ); - + return { content: [{ type: "text", @@ -677,22 +593,20 @@ export function registerEVMTools(server: McpServer) { "transfer_token", "Transfer ERC20 tokens to an address", { - privateKey: z.string().describe("Private key of the sender account in hex format (with or without 0x prefix). SECURITY: This is used only for transaction signing and is not stored."), tokenAddress: z.string().describe("The contract address or ENS name of the ERC20 token to transfer (e.g., '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48' for USDC or 'uniswap.eth')"), toAddress: z.string().describe("The recipient address or ENS name that will receive the tokens (e.g., '0x1234...' or 'vitalik.eth')"), amount: z.string().describe("Amount of tokens to send as a string (e.g., '100' for 100 tokens). This will be adjusted for the token's decimals."), - network: z.string().optional().describe("Network name (e.g., 'ethereum', 'optimism', 'arbitrum', 'base', etc.) or chain ID. Supports all EVM-compatible networks. Defaults to Ethereum mainnet.") + network: z.string().optional().describe("Network name (e.g., 'sei', 'sei-testnet', 'sei-devnet') or chain ID. Supports all EVM-compatible networks. Defaults to Sei mainnet.") }, - async ({ privateKey, tokenAddress, toAddress, amount, network = "ethereum" }) => { + async ({ tokenAddress, toAddress, amount, network = "sei" }) => { try { const result = await services.transferERC20( tokenAddress, toAddress, amount, - privateKey, network ); - + return { content: [{ type: "text", @@ -720,7 +634,7 @@ export function registerEVMTools(server: McpServer) { ); // CONTRACT TOOLS - + // Read contract server.tool( "read_contract", @@ -730,22 +644,22 @@ export function registerEVMTools(server: McpServer) { abi: z.array(z.any()).describe("The ABI (Application Binary Interface) of the smart contract function, as a JSON array"), functionName: z.string().describe("The name of the function to call on the contract (e.g., 'balanceOf')"), args: z.array(z.any()).optional().describe("The arguments to pass to the function, as an array (e.g., ['0x1234...'])"), - network: z.string().optional().describe("Network name (e.g., 'ethereum', 'optimism', 'arbitrum', 'base', 'polygon') or chain ID. Defaults to Ethereum mainnet.") + network: z.string().optional().describe("Network name (e.g., 'sei', 'sei-testnet', 'sei-devnet') or chain ID. Defaults to Sei mainnet.") }, - async ({ contractAddress, abi, functionName, args = [], network = "ethereum" }) => { + async ({ contractAddress, abi, functionName, args = [], network = "sei" }) => { try { // Parse ABI if it's a string const parsedAbi = typeof abi === 'string' ? JSON.parse(abi) : abi; - + const params = { address: contractAddress as Address, abi: parsedAbi, functionName, args }; - + const result = await services.readContract(params, network); - + return { content: [{ type: "text", @@ -773,27 +687,25 @@ export function registerEVMTools(server: McpServer) { abi: z.array(z.any()).describe("The ABI (Application Binary Interface) of the smart contract function, as a JSON array"), functionName: z.string().describe("The name of the function to call on the contract (e.g., 'transfer')"), args: z.array(z.any()).describe("The arguments to pass to the function, as an array (e.g., ['0x1234...', '1000000000000000000'])"), - privateKey: z.string().describe("Private key of the sending account in hex format (with or without 0x prefix). SECURITY: This is used only for transaction signing and is not stored."), - network: z.string().optional().describe("Network name (e.g., 'ethereum', 'optimism', 'arbitrum', 'base', 'polygon') or chain ID. Defaults to Ethereum mainnet.") + network: z.string().optional().describe("Network name (e.g., 'sei', 'sei-testnet', 'sei-devnet') or chain ID. Defaults to Sei mainnet.") }, - async ({ contractAddress, abi, functionName, args, privateKey, network = "ethereum" }) => { + async ({ contractAddress, abi, functionName, args, network = "sei" }) => { try { // Parse ABI if it's a string const parsedAbi = typeof abi === 'string' ? JSON.parse(abi) : abi; - + const contractParams: Record = { address: contractAddress as Address, abi: parsedAbi, functionName, args }; - + const txHash = await services.writeContract( - privateKey as Hex, - contractParams, + contractParams, network ); - + return { content: [{ type: "text", @@ -821,13 +733,13 @@ export function registerEVMTools(server: McpServer) { "is_contract", "Check if an address is a smart contract or an externally owned account (EOA)", { - address: z.string().describe("The wallet or contract address or ENS name to check (e.g., '0x1234...' or 'uniswap.eth')"), - network: z.string().optional().describe("Network name (e.g., 'ethereum', 'optimism', 'arbitrum', 'base', etc.) or chain ID. Supports all EVM-compatible networks. Defaults to Ethereum mainnet.") + address: z.string().describe("The wallet or contract address to check (e.g., '0x1234...' or 'uniswap.eth')"), + network: z.string().optional().describe("Network name (e.g., 'sei', 'sei-testnet', 'sei-devnet', etc.) or chain ID. Supports all EVM-compatible networks. Defaults to Sei mainnet.") }, - async ({ address, network = "ethereum" }) => { + async ({ address, network = "sei" }) => { try { const isContract = await services.isContract(address, network); - + return { content: [{ type: "text", @@ -856,13 +768,13 @@ export function registerEVMTools(server: McpServer) { "get_token_info", "Get comprehensive information about an ERC20 token including name, symbol, decimals, total supply, and other metadata. Use this to analyze any token on EVM chains.", { - tokenAddress: z.string().describe("The contract address of the ERC20 token (e.g., '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48' for USDC on Ethereum)"), - network: z.string().optional().describe("Network name (e.g., 'ethereum', 'optimism', 'arbitrum', 'base', 'polygon') or chain ID. Defaults to Ethereum mainnet.") + tokenAddress: z.string().describe("The contract address of the ERC20 token (e.g., '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48')"), + network: z.string().optional().describe("Network name (e.g., 'sei', 'sei-testnet', 'sei-devnet') or chain ID. Defaults to Sei mainnet.") }, - async ({ tokenAddress, network = "ethereum" }) => { + async ({ tokenAddress, network = "sei" }) => { try { const tokenInfo = await services.getERC20TokenInfo(tokenAddress as Address, network); - + return { content: [{ type: "text", @@ -892,16 +804,16 @@ export function registerEVMTools(server: McpServer) { { address: z.string().describe("The address to check balance for"), tokenAddress: z.string().describe("The ERC20 token contract address"), - network: z.string().optional().describe("Network name or chain ID. Defaults to Ethereum mainnet.") + network: z.string().optional().describe("Network name or chain ID. Defaults to Sei mainnet.") }, - async ({ address, tokenAddress, network = "ethereum" }) => { + async ({ address, tokenAddress, network = "sei" }) => { try { const balance = await services.getERC20Balance( tokenAddress as Address, address as Address, network ); - + return { content: [{ type: "text", @@ -936,25 +848,25 @@ export function registerEVMTools(server: McpServer) { { tokenAddress: z.string().describe("The contract address of the NFT collection (e.g., '0xBC4CA0EdA7647A8aB7C2061c2E118A18a936f13D' for Bored Ape Yacht Club)"), tokenId: z.string().describe("The ID of the specific NFT token to query (e.g., '1234')"), - network: z.string().optional().describe("Network name (e.g., 'ethereum', 'optimism', 'arbitrum', 'base', 'polygon') or chain ID. Most NFTs are on Ethereum mainnet, which is the default.") + network: z.string().optional().describe("Network name (e.g., 'sei', ) or chain ID. Most NFTs are on Sei mainnet, which is the default.") }, - async ({ tokenAddress, tokenId, network = "ethereum" }) => { + async ({ tokenAddress, tokenId, network = "sei" }) => { try { const nftInfo = await services.getERC721TokenMetadata( - tokenAddress as Address, - BigInt(tokenId), + tokenAddress as Address, + BigInt(tokenId), network ); - + // Check ownership separately let owner = null; try { // This may fail if tokenId doesn't exist owner = await services.getPublicClient(network).readContract({ address: tokenAddress as Address, - abi: [{ - inputs: [{ type: 'uint256' }], - name: 'ownerOf', + abi: [{ + inputs: [{ type: 'uint256' }], + name: 'ownerOf', outputs: [{ type: 'address' }], stateMutability: 'view', type: 'function' @@ -965,7 +877,7 @@ export function registerEVMTools(server: McpServer) { } catch (e) { // Ownership info not available } - + return { content: [{ type: "text", @@ -995,12 +907,12 @@ export function registerEVMTools(server: McpServer) { "check_nft_ownership", "Check if an address owns a specific NFT", { - tokenAddress: z.string().describe("The contract address or ENS name of the NFT collection (e.g., '0xBC4CA0EdA7647A8aB7C2061c2E118A18a936f13D' for BAYC or 'boredapeyachtclub.eth')"), + tokenAddress: z.string().describe("The contract address of the NFT collection (e.g., '0xBC4CA0EdA7647A8aB7C2061c2E118A18a936f13D' for BAYC or 'boredapeyachtclub.eth')"), tokenId: z.string().describe("The ID of the NFT to check (e.g., '1234')"), - ownerAddress: z.string().describe("The wallet address or ENS name to check ownership against (e.g., '0x1234...' or 'vitalik.eth')"), - network: z.string().optional().describe("Network name (e.g., 'ethereum', 'optimism', 'arbitrum', 'base', etc.) or chain ID. Supports all EVM-compatible networks. Defaults to Ethereum mainnet.") + ownerAddress: z.string().describe("The wallet address to check ownership against (e.g., '0x1234...' or 'vitalik.eth')"), + network: z.string().optional().describe("Network name (e.g., 'sei', 'sei-testnet', 'sei-devnet' etc.) or chain ID. Supports all EVM-compatible networks. Defaults to Sei mainnet.") }, - async ({ tokenAddress, tokenId, ownerAddress, network = "ethereum" }) => { + async ({ tokenAddress, tokenId, ownerAddress, network = "sei" }) => { try { const isOwner = await services.isNFTOwner( tokenAddress, @@ -1008,7 +920,7 @@ export function registerEVMTools(server: McpServer) { BigInt(tokenId), network ); - + return { content: [{ type: "text", @@ -1041,16 +953,16 @@ export function registerEVMTools(server: McpServer) { { tokenAddress: z.string().describe("The contract address of the ERC1155 token collection (e.g., '0x76BE3b62873462d2142405439777e971754E8E77')"), tokenId: z.string().describe("The ID of the specific token to query metadata for (e.g., '1234')"), - network: z.string().optional().describe("Network name (e.g., 'ethereum', 'optimism', 'arbitrum', 'base', 'polygon') or chain ID. ERC1155 tokens exist across many networks. Defaults to Ethereum mainnet.") + network: z.string().optional().describe("Network name (e.g., 'sei', 'sei-testnet', 'sei-devnet') or chain ID. ERC1155 tokens exist across many networks. Defaults to Sei mainnet.") }, - async ({ tokenAddress, tokenId, network = "ethereum" }) => { + async ({ tokenAddress, tokenId, network = "sei" }) => { try { const uri = await services.getERC1155TokenURI( - tokenAddress as Address, - BigInt(tokenId), + tokenAddress as Address, + BigInt(tokenId), network ); - + return { content: [{ type: "text", @@ -1081,16 +993,16 @@ export function registerEVMTools(server: McpServer) { { tokenAddress: z.string().describe("The contract address of the NFT collection (e.g., '0xBC4CA0EdA7647A8aB7C2061c2E118A18a936f13D' for Bored Ape Yacht Club)"), ownerAddress: z.string().describe("The wallet address to check the NFT balance for (e.g., '0x1234...')"), - network: z.string().optional().describe("Network name (e.g., 'ethereum', 'optimism', 'arbitrum', 'base', 'polygon') or chain ID. Most NFTs are on Ethereum mainnet, which is the default.") + network: z.string().optional().describe("Network name (e.g., 'sei', 'sei-testnet', 'sei-devnet') or chain ID. Most NFTs are on Sei mainnet, which is the default.") }, - async ({ tokenAddress, ownerAddress, network = "ethereum" }) => { + async ({ tokenAddress, ownerAddress, network = "sei" }) => { try { const balance = await services.getERC721Balance( - tokenAddress as Address, - ownerAddress as Address, + tokenAddress as Address, + ownerAddress as Address, network ); - + return { content: [{ type: "text", @@ -1122,17 +1034,17 @@ export function registerEVMTools(server: McpServer) { tokenAddress: z.string().describe("The contract address of the ERC1155 token collection (e.g., '0x76BE3b62873462d2142405439777e971754E8E77')"), tokenId: z.string().describe("The ID of the specific token to check the balance for (e.g., '1234')"), ownerAddress: z.string().describe("The wallet address to check the token balance for (e.g., '0x1234...')"), - network: z.string().optional().describe("Network name (e.g., 'ethereum', 'optimism', 'arbitrum', 'base', 'polygon') or chain ID. ERC1155 tokens exist across many networks. Defaults to Ethereum mainnet.") + network: z.string().optional().describe("Network name (e.g., 'sei', 'sei-testnet', 'sei-devnet') or chain ID. ERC1155 tokens exist across many networks. Defaults to Sei mainnet.") }, - async ({ tokenAddress, tokenId, ownerAddress, network = "ethereum" }) => { + async ({ tokenAddress, tokenId, ownerAddress, network = "sei" }) => { try { const balance = await services.getERC1155Balance( - tokenAddress as Address, - ownerAddress as Address, + tokenAddress as Address, + ownerAddress as Address, BigInt(tokenId), network ); - + return { content: [{ type: "text", @@ -1163,22 +1075,25 @@ export function registerEVMTools(server: McpServer) { server.tool( "get_address_from_private_key", "Get the EVM address derived from a private key", - { - privateKey: z.string().describe("Private key in hex format (with or without 0x prefix). SECURITY: This is used only for address derivation and is not stored.") - }, - async ({ privateKey }) => { + {}, // Schema is empty as privateKey parameter was removed + async () => { // Handler function starts here try { - // Ensure the private key has 0x prefix - const formattedKey = privateKey.startsWith('0x') ? privateKey as Hex : `0x${privateKey}` as Hex; - - const address = services.getAddressFromPrivateKey(formattedKey); - + const privateKeyValue = getPrivateKeyAsHex(); + if (!privateKeyValue) { + return { + content: [{ type: "text", text: "Error: The PRIVATE_KEY environment variable is not set. Please set this variable with your private key and restart the MCP server for this tool to function." }], + isError: true + }; + } + + const address = services.getAddressFromPrivateKey(privateKeyValue); + return { content: [{ type: "text", text: JSON.stringify({ address, - privateKey: "0x" + privateKey.replace(/^0x/, '') + // Do not return the private key in the response. }, null, 2) }] }; @@ -1193,4 +1108,4 @@ export function registerEVMTools(server: McpServer) { } } ); -} \ No newline at end of file +} diff --git a/src/tests/core/config.test.ts b/src/tests/core/config.test.ts new file mode 100644 index 0000000..f434c42 --- /dev/null +++ b/src/tests/core/config.test.ts @@ -0,0 +1,72 @@ +import { describe, test, expect, beforeEach, afterEach } from 'bun:test'; +import { config, getPrivateKeyAsHex } from '../../core/config.js'; + +describe('Config Module', () => { + // Store original environment + const originalEnv = { ...process.env }; + + // Reset environment before each test + beforeEach(() => { + // Clear any environment variables that might affect tests + delete process.env.PRIVATE_KEY; + }); + + // Restore original environment after each test + afterEach(() => { + process.env = { ...originalEnv }; + }); + + describe('formatPrivateKey', () => { + test('should add 0x prefix if missing', () => { + // Set environment variable without 0x prefix + process.env.PRIVATE_KEY = 'abcdef1234567890'; + + // Re-import the module to trigger environment variable processing + // Note: In a real test, you'd use a mechanism to reload the module + // For this example, we'll check the behavior indirectly + + // Set the private key directly to simulate reloading + // @ts-ignore - Accessing private implementation + config.privateKey = process.env.PRIVATE_KEY.startsWith('0x') + ? process.env.PRIVATE_KEY + : `0x${process.env.PRIVATE_KEY}`; + + // Check that the private key has 0x prefix + expect(config.privateKey).toBe('0xabcdef1234567890'); + }); + + test('should not modify key if 0x prefix exists', () => { + // Set environment variable with 0x prefix + process.env.PRIVATE_KEY = '0xabcdef1234567890'; + + // Set the private key directly to simulate reloading + // @ts-ignore - Accessing private implementation + config.privateKey = process.env.PRIVATE_KEY.startsWith('0x') + ? process.env.PRIVATE_KEY + : `0x${process.env.PRIVATE_KEY}`; + + // Check that the private key still has just one 0x prefix + expect(config.privateKey).toBe('0xabcdef1234567890'); + }); + }); + + describe('getPrivateKeyAsHex', () => { + test('should return undefined if private key is not set', () => { + // Ensure private key is not set + // @ts-ignore - Accessing private implementation + config.privateKey = undefined; + + // Check that getPrivateKeyAsHex returns undefined + expect(getPrivateKeyAsHex()).toBeUndefined(); + }); + + test('should return private key as Hex if set', () => { + // Set private key + // @ts-ignore - Accessing private implementation + config.privateKey = '0xabcdef1234567890'; + + // Check that getPrivateKeyAsHex returns the key + expect(getPrivateKeyAsHex()).toBe('0xabcdef1234567890'); + }); + }); +}); diff --git a/src/tests/core/services/balance.test.ts b/src/tests/core/services/balance.test.ts new file mode 100644 index 0000000..6afffc7 --- /dev/null +++ b/src/tests/core/services/balance.test.ts @@ -0,0 +1,226 @@ +import { describe, test, expect, mock, spyOn, afterEach } from 'bun:test'; +import { + getBalance, + getERC20Balance, + isNFTOwner, + getERC721Balance, + getERC1155Balance +} from '../../../core/services/balance.js'; + +// Create valid test addresses +const VALID_ADDRESS = '0x1234567890123456789012345678901234567890'; +const VALID_TOKEN_ADDRESS = '0xabcdef1234567890123456789012345678901234'; +const VALID_OWNER_ADDRESS = '0x0987654321098765432109876543210987654321'; + +describe('Balance Service', () => { + // Reset all mocks after each test + afterEach(() => { + mock.restore(); + }); + + describe('getBalance', () => { + test('should return the native token balance', async () => { + // Mock the getPublicClient function + const mockClient = { + getBalance: () => Promise.resolve(1000000000000000000n) + }; + + // Mock the modules + mock.module('../../../core/services/clients.js', () => ({ + getPublicClient: () => mockClient + })); + + mock.module('../../../core/services/utils.js', () => ({ + helpers: { + validateAddress: (address: string) => address + } + })); + + mock.module('viem', () => ({ + formatEther: () => '1', + getContract: () => ({}), + formatUnits: () => '' + })); + + // Call the function + const result = await getBalance(VALID_ADDRESS); + + // Verify results + expect(result).toEqual({ + wei: 1000000000000000000n, + ether: '1' + }); + }); + }); + + describe('getERC20Balance', () => { + test('should return the ERC20 token balance with metadata', async () => { + // Mock the contract object + const mockContract = { + read: { + balanceOf: () => Promise.resolve(1000000000n), + symbol: () => Promise.resolve('TOKEN'), + decimals: () => Promise.resolve(9) + } + }; + + // Mock the modules + mock.module('viem', () => ({ + getContract: () => mockContract, + formatUnits: () => '1', + formatEther: () => '' + })); + + mock.module('../../../core/services/utils.js', () => ({ + helpers: { + validateAddress: (address: string) => address + } + })); + + // Call the function + const result = await getERC20Balance( + VALID_TOKEN_ADDRESS, + VALID_OWNER_ADDRESS + ); + + // Verify results + expect(result).toEqual({ + raw: 1000000000n, + formatted: '1', + token: { + symbol: 'TOKEN', + decimals: 9 + } + }); + }); + }); + + describe('isNFTOwner', () => { + test('should return true if address owns the NFT', async () => { + // Mock the modules + mock.module('../../../core/services/contracts.js', () => ({ + readContract: () => Promise.resolve(VALID_OWNER_ADDRESS) + })); + + mock.module('../../../core/services/utils.js', () => ({ + helpers: { + validateAddress: (address: string) => address + } + })); + + // Call the function + const result = await isNFTOwner( + VALID_TOKEN_ADDRESS, + VALID_OWNER_ADDRESS, + 1n + ); + + // Verify results + expect(result).toBe(true); + }); + + test('should return false if address does not own the NFT', async () => { + // Mock the modules + mock.module('../../../core/services/contracts.js', () => ({ + readContract: () => Promise.resolve('0xDifferentAddress') + })); + + mock.module('../../../core/services/utils.js', () => ({ + helpers: { + validateAddress: (address: string) => address + } + })); + + // Call the function + const result = await isNFTOwner( + VALID_TOKEN_ADDRESS, + VALID_OWNER_ADDRESS, + 1n + ); + + // Verify results + expect(result).toBe(false); + }); + + test('should return false if there is an error', async () => { + // Mock the modules + mock.module('../../../core/services/contracts.js', () => ({ + readContract: () => { + throw new Error('NFT does not exist'); + } + })); + + mock.module('../../../core/services/utils.js', () => ({ + helpers: { + validateAddress: (address: string) => address + } + })); + + // Mock console.error to avoid cluttering test output + const originalConsoleError = console.error; + console.error = () => {}; + + // Call the function + const result = await isNFTOwner( + VALID_TOKEN_ADDRESS, + VALID_OWNER_ADDRESS, + 1n + ); + + // Verify results + expect(result).toBe(false); + + // Restore console.error + console.error = originalConsoleError; + }); + }); + + describe('getERC721Balance', () => { + test('should return the number of NFTs owned', async () => { + // Mock the modules + mock.module('../../../core/services/contracts.js', () => ({ + readContract: () => Promise.resolve(5n) + })); + + mock.module('../../../core/services/utils.js', () => ({ + helpers: { + validateAddress: (address: string) => address + } + })); + + // Call the function + const result = await getERC721Balance( + VALID_TOKEN_ADDRESS, + VALID_OWNER_ADDRESS + ); + + // Verify results + expect(result).toBe(5n); + }); + }); + + describe('getERC1155Balance', () => { + test('should return the balance of the ERC1155 token', async () => { + // Mock the modules + mock.module('../../../core/services/contracts.js', () => ({ + readContract: () => Promise.resolve(10n) + })); + + mock.module('../../../core/services/utils.js', () => ({ + helpers: { + validateAddress: (address: string) => address + } + })); + + // Call the function + const result = await getERC1155Balance( + VALID_TOKEN_ADDRESS, + VALID_OWNER_ADDRESS, + 2n + ); + + // Verify results + expect(result).toBe(10n); + }); + }); +}); diff --git a/src/tests/core/services/tools.test.ts b/src/tests/core/services/tools.test.ts new file mode 100644 index 0000000..cedff27 --- /dev/null +++ b/src/tests/core/services/tools.test.ts @@ -0,0 +1,45 @@ +import { describe, test, expect, beforeEach } from 'bun:test'; +import { registerEVMTools } from '../../../core/tools.js'; + +// Mock the MCP server +class MockServer { + tools: { name: string; description: string; schema: any; handler: Function }[] = []; + tool(name: string, description: string, schema: any, handler: Function) { + this.tools.push({ name, description, schema, handler }); + } +} + +describe('registerEVMTools', () => { + let server: MockServer; + + beforeEach(() => { + server = new MockServer(); + }); + + test('registers all expected tools', () => { + registerEVMTools(server as any); + // Check that some key tools are registered + const toolNames = server.tools.map(t => t.name); + expect(toolNames).toContain('get_chain_info'); + expect(toolNames).toContain('get_supported_networks'); + expect(toolNames).toContain('get_block_by_number'); + expect(toolNames).toContain('get_balance'); + expect(toolNames).toContain('transfer_sei'); + expect(toolNames).toContain('transfer_erc20'); + expect(toolNames).toContain('approve_token_spending'); + expect(toolNames).toContain('transfer_nft'); + expect(toolNames).toContain('get_token_info'); + expect(toolNames).toContain('get_nft_info'); + expect(toolNames).toContain('get_erc1155_token_uri'); + expect(toolNames).toContain('get_nft_balance'); + expect(toolNames).toContain('get_erc1155_balance'); + expect(toolNames).toContain('get_address_from_private_key'); + }); + + test('each registered tool has a handler function', () => { + registerEVMTools(server as any); + server.tools.forEach(tool => { + expect(typeof tool.handler).toBe('function'); + }); + }); +}); \ No newline at end of file diff --git a/src/tests/core/services/transfer.test.ts b/src/tests/core/services/transfer.test.ts new file mode 100644 index 0000000..3382d35 --- /dev/null +++ b/src/tests/core/services/transfer.test.ts @@ -0,0 +1,200 @@ +import { describe, test, expect, mock, beforeEach, afterEach } from 'bun:test'; +import { + transferSei, + transferERC20, + approveERC20, + transferERC721, + transferERC1155 +} from '../../../core/services/transfer.js'; +import { getPublicClient, getWalletClient } from '../../../core/services/clients.js'; +import { getPrivateKeyAsHex } from '../../../core/config.js'; +import type { Hash } from 'viem'; + +// Mock the dependencies +mock.module('../../../core/services/clients.js', () => ({ + getPublicClient: mock(() => {}), + getWalletClient: mock(() => {}) +})); + +mock.module('../../../core/config.js', () => ({ + getPrivateKeyAsHex: mock(() => '0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef') +})); + +describe('Transfer Service', () => { + const mockPublicClient = { + readContract: mock((params: { functionName: string }) => { + if (params.functionName === 'decimals') return 18; + if (params.functionName === 'symbol') return 'TEST'; + if (params.functionName === 'name') return 'Test NFT'; + return null; + }), + getContract: mock(() => {}) + }; + + const mockWalletClient = { + sendTransaction: mock(() => '0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef' as Hash), + writeContract: mock(() => '0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef' as Hash), + account: { address: '0x1234567890123456789012345678901234567890' }, + chain: { id: 1 } + }; + + beforeEach(() => { + // Reset all mocks before each test + mock.restore(); + + // Setup default mock implementations + (getPublicClient as any).mockReturnValue(mockPublicClient); + (getWalletClient as any).mockReturnValue(mockWalletClient); + (getPrivateKeyAsHex as any).mockReturnValue('0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef'); + }); + + describe('transferSei', () => { + test('should transfer SEI tokens successfully', async () => { + const mockHash = '0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef' as Hash; + (mockWalletClient.sendTransaction as any).mockResolvedValue(mockHash); + + const result = await transferSei( + '0x1234567890123456789012345678901234567890', + '1.0', + 'sei' + ); + + expect(result).toBe(mockHash); + expect(mockWalletClient.sendTransaction).toHaveBeenCalledWith({ + to: '0x1234567890123456789012345678901234567890', + value: 1000000000000000000n, + account: mockWalletClient.account, + chain: mockWalletClient.chain + }); + }); + + test('should throw error when private key is not available', async () => { + (getPrivateKeyAsHex as any).mockReturnValue(null); + + await expect(transferSei( + '0x1234567890123456789012345678901234567890', + '1.0' + )).rejects.toThrow('Private key not available'); + }); + }); + + describe('transferERC20', () => { + test('should transfer ERC20 tokens successfully', async () => { + const mockHash = '0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef' as Hash; + const mockDecimals = 18; + const mockSymbol = 'TEST'; + + (mockPublicClient.readContract as any).mockImplementation((params: { functionName: string }) => { + if (params.functionName === 'decimals') return mockDecimals; + if (params.functionName === 'symbol') return mockSymbol; + return null; + }); + + (mockWalletClient.writeContract as any).mockResolvedValue(mockHash); + + const result = await transferERC20( + '0x1234567890123456789012345678901234567890', + '0x0987654321098765432109876543210987654321', + '1.0' + ); + + expect(result).toEqual({ + txHash: mockHash, + amount: { + raw: 1000000000000000000n, + formatted: '1.0' + }, + token: { + symbol: mockSymbol, + decimals: mockDecimals + } + }); + }); + }); + + describe('approveERC20', () => { + test('should approve ERC20 token spending successfully', async () => { + const mockHash = '0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef' as Hash; + const mockDecimals = 18; + const mockSymbol = 'TEST'; + + (mockPublicClient.readContract as any).mockImplementation((params: { functionName: string }) => { + if (params.functionName === 'decimals') return mockDecimals; + if (params.functionName === 'symbol') return mockSymbol; + return null; + }); + + (mockWalletClient.writeContract as any).mockResolvedValue(mockHash); + + const result = await approveERC20( + '0x1234567890123456789012345678901234567890', + '0x0987654321098765432109876543210987654321', + '1.0' + ); + + expect(result).toEqual({ + txHash: mockHash, + amount: { + raw: 1000000000000000000n, + formatted: '1.0' + }, + token: { + symbol: mockSymbol, + decimals: mockDecimals + } + }); + }); + }); + + describe('transferERC721', () => { + test('should transfer ERC721 token successfully', async () => { + const mockHash = '0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef' as Hash; + const mockName = 'Test NFT'; + const mockSymbol = 'TNFT'; + + (mockPublicClient.readContract as any).mockImplementation((params: { functionName: string }) => { + if (params.functionName === 'name') return mockName; + if (params.functionName === 'symbol') return mockSymbol; + return null; + }); + + (mockWalletClient.writeContract as any).mockResolvedValue(mockHash); + + const result = await transferERC721( + '0x1234567890123456789012345678901234567890', + '0x0987654321098765432109876543210987654321', + 1n + ); + + expect(result).toEqual({ + txHash: mockHash, + tokenId: '1', + token: { + name: mockName, + symbol: mockSymbol + } + }); + }); + }); + + describe('transferERC1155', () => { + test('should transfer ERC1155 token successfully', async () => { + const mockHash = '0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef' as Hash; + + (mockWalletClient.writeContract as any).mockResolvedValue(mockHash); + + const result = await transferERC1155( + '0x1234567890123456789012345678901234567890', + '0x0987654321098765432109876543210987654321', + 1n, + '1' + ); + + expect(result).toEqual({ + txHash: mockHash, + tokenId: '1', + amount: '1' + }); + }); + }); +}); \ No newline at end of file diff --git a/src/tests/core/services/utils.test.ts b/src/tests/core/services/utils.test.ts new file mode 100644 index 0000000..a29447d --- /dev/null +++ b/src/tests/core/services/utils.test.ts @@ -0,0 +1,94 @@ +import { describe, test, expect } from 'bun:test'; +import { utils } from '../../../core/services/utils.js'; + +describe('Utils Module', () => { + describe('parseEther', () => { + test('should convert ether to wei', () => { + // Test parseEther with a string value + const result = utils.parseEther('1.0'); + expect(result).toBe(1000000000000000000n); + + // Test with a different value + const result2 = utils.parseEther('2.5'); + expect(result2).toBe(2500000000000000000n); + + // Test with zero + const result3 = utils.parseEther('0'); + expect(result3).toBe(0n); + }); + }); + + describe('formatJson', () => { + test('should format an object to JSON with bigint handling', () => { + // Test with an object containing bigint + const obj = { + amount: 1000000000000000000n, + name: 'test', + nested: { + value: 123456789n + } + }; + + const result = utils.formatJson(obj); + + // Parse the result back to verify + const parsed = JSON.parse(result); + + // Check that the bigints were converted to strings + expect(parsed.amount).toBe('1000000000000000000'); + expect(parsed.name).toBe('test'); + expect(parsed.nested.value).toBe('123456789'); + + // Check that the formatting includes indentation + expect(result).toContain(' "amount": "1000000000000000000"'); + }); + + test('should handle objects without bigints', () => { + const obj = { + name: 'test', + value: 123, + nested: { + flag: true + } + }; + + const result = utils.formatJson(obj); + const parsed = JSON.parse(result); + + expect(parsed.name).toBe('test'); + expect(parsed.value).toBe(123); + expect(parsed.nested.flag).toBe(true); + }); + }); + + describe('validateAddress', () => { + test('should return the address if it is valid', () => { + // Valid Ethereum address + const validAddress = '0x1234567890123456789012345678901234567890'; + const result = utils.validateAddress(validAddress); + expect(result).toBe(validAddress); + + // Valid address with mixed case + const mixedCaseAddress = '0xAbCdEf1234567890123456789012345678901234'; + const result2 = utils.validateAddress(mixedCaseAddress); + expect(result2).toBe(mixedCaseAddress); + }); + + test('should throw an error if the address is invalid', () => { + // Address too short + const shortAddress = '0x123456'; + expect(() => utils.validateAddress(shortAddress)).toThrow('Invalid address'); + + // Address without 0x prefix + const noPrefixAddress = '1234567890123456789012345678901234567890'; + expect(() => utils.validateAddress(noPrefixAddress)).toThrow('Invalid address'); + + // Address with invalid characters + const invalidCharsAddress = '0x123456789012345678901234567890123456789G'; + expect(() => utils.validateAddress(invalidCharsAddress)).toThrow('Invalid address'); + + // Empty address + expect(() => utils.validateAddress('')).toThrow('Invalid address'); + }); + }); +});