From 5b52a3b273607dc0f533079c4afa3a1a6b4e6f17 Mon Sep 17 00:00:00 2001 From: david Date: Tue, 17 Mar 2026 10:15:55 +0100 Subject: [PATCH] Added support for Vector L2 and removed everything else --- .env.example | 63 +-- README.md | 390 ++++-------------- docs/CARDANO.md | 213 ---------- docs/VECTOR.md | 104 +++++ package.json | 62 +-- shell.nix | 8 + src/chains/UTXO/bitcoin.ts | 145 ------- src/chains/UTXO/bitcoincash.ts | 144 ------- src/chains/UTXO/cardano.ts | 657 ------------------------------ src/chains/UTXO/dogecoin.ts | 144 ------- src/chains/UTXO/index.ts | 8 - src/chains/UTXO/litecoin.ts | 144 ------- src/chains/evm/evm.ts | 481 ---------------------- src/chains/index.ts | 26 -- src/chains/ripple/index.ts | 1 - src/chains/ripple/ripple.ts | 505 ----------------------- src/chains/solana/jupiter.ts | 277 ------------- src/chains/solana/solana.ts | 424 ------------------- src/chains/thorchain/thorchain.ts | 173 -------- src/chains/ton/index.ts | 1 - src/chains/ton/ton.ts | 454 --------------------- src/env.d.ts | 30 +- src/general/coingecko.ts | 309 -------------- src/general/index.ts | 8 - src/general/rubic.ts | 478 ---------------------- src/index.ts | 96 +---- src/types.ts | 12 - src/vector/index.ts | 2 + src/vector/ogmios-provider.ts | 391 ++++++++++++++++++ src/vector/safety.ts | 85 ++++ src/vector/types.ts | 60 +++ src/vector/vector.ts | 602 +++++++++++++++++++++++++++ 32 files changed, 1378 insertions(+), 5119 deletions(-) delete mode 100644 docs/CARDANO.md create mode 100644 docs/VECTOR.md create mode 100644 shell.nix delete mode 100644 src/chains/UTXO/bitcoin.ts delete mode 100644 src/chains/UTXO/bitcoincash.ts delete mode 100644 src/chains/UTXO/cardano.ts delete mode 100644 src/chains/UTXO/dogecoin.ts delete mode 100644 src/chains/UTXO/index.ts delete mode 100644 src/chains/UTXO/litecoin.ts delete mode 100644 src/chains/evm/evm.ts delete mode 100644 src/chains/index.ts delete mode 100644 src/chains/ripple/index.ts delete mode 100644 src/chains/ripple/ripple.ts delete mode 100644 src/chains/solana/jupiter.ts delete mode 100644 src/chains/solana/solana.ts delete mode 100644 src/chains/thorchain/thorchain.ts delete mode 100644 src/chains/ton/index.ts delete mode 100644 src/chains/ton/ton.ts delete mode 100644 src/general/coingecko.ts delete mode 100644 src/general/index.ts delete mode 100644 src/general/rubic.ts delete mode 100644 src/types.ts create mode 100644 src/vector/index.ts create mode 100644 src/vector/ogmios-provider.ts create mode 100644 src/vector/safety.ts create mode 100644 src/vector/types.ts create mode 100644 src/vector/vector.ts diff --git a/.env.example b/.env.example index d80d9f9..56363c9 100644 --- a/.env.example +++ b/.env.example @@ -1,53 +1,16 @@ -# Network RPC URLs -SOLANA_RPC_URL=https://api.mainnet-beta.solana.com -ETH_RPC_URL=https://eth-mainnet.g.alchemy.com/v2/your-api-key -ARBITRUM_RPC_URL=https://arb1.arbitrum.io/rpc -BASE_RPC_URL=https://mainnet.base.org -OPTIMISM_RPC_URL=https://mainnet.optimism.io -BSC_RPC_URL=https://bsc-dataseed.binance.org -POLYGON_RPC_URL=https://polygon-rpc.com -AVALANCHE_RPC_URL=https://api.avax.network/ext/bc/C/rpc -BERACHAIN_RPC_URL=https://rpc.berachain.com -SONIC_RPC_URL=https://rpc.soniclabs.com/ -XRP_RPC_URL=https://xrplcluster.com/ -TON_RPC_URL=https://toncenter.com/api/v2/jsonRPC -TON_API_KEY=your-toncenter-api-key +# Vector MCP Server Configuration +# ================================ -# Private keys (DO NOT SHARE) -ETH_PRIVATE_KEY=your-eth-private-key -SOLANA_PRIVATE_KEY=your-solana-private-key -THORCHAIN_PRIVATE_KEY=your-thorchain-private-key -THORCHAIN_MNEMONIC=your-thorchain-mnemonic -# XRP Ledger credentials - can use either private key or mnemonic -XRP_PRIVATE_KEY=your-xrp-private-key # Private key in hex format -XRP_MNEMONIC=your-xrp-mnemonic # Alternative: 12-word recovery phrase -XRP_ADDRESS=your-xrp-address # Optional: Used to verify key derivation +# Vector Network Endpoints +VECTOR_OGMIOS_URL=https://ogmios.vector.testnet.apexfusion.org +VECTOR_KOIOS_URL=https://koios.vector.testnet.apexfusion.org/ +VECTOR_SUBMIT_URL=https://submit.vector.testnet.apexfusion.org/api/submit/tx +VECTOR_EXPLORER_URL=https://vector.testnet.apexscan.org -# TON Configuration -TON_MNEMONIC=word1 word2 word3 ... word24 # 24-word recovery phrase -TON_ADDRESS=your-ton-address # Your TON wallet address +# Wallet Configuration +VECTOR_MNEMONIC=word1 word2 word3 ... word15 # Must be exactly 15 or 24 words +VECTOR_ACCOUNT_INDEX=0 # Wallet account index (default: 0) -# Cardano Configuration -BLOCKFROST_API_KEY=your-blockfrost-api-key # Register at https://blockfrost.io/ -CARDANO_NETWORK=mainnet # or 'testnet', 'preview', 'preprod' -CARDANO_MNEMONIC=word1 word2 word3 ... word15 # Must be exactly 15 or 24 words -CARDANO_ACCOUNT_INDEX=0 # Wallet account index (default: 0) - -# UTXO Chain API Keys (optional) -BLOCKCYPHER_API_KEY=your-blockcypher-api-key -SOCHAIN_API_KEY=your-sochain-api-key - -# Tool Registration Controls (true/false) -ENABLE_SOLANA_TOOLS=true # Enable/disable Solana-specific tools -ENABLE_EVM_TOOLS=true # Enable/disable EVM chain tools (Ethereum, Base, Arbitrum, etc.) -ENABLE_BITCOIN_TOOLS=true -ENABLE_LITECOIN_TOOLS=true -ENABLE_DOGECOIN_TOOLS=true -ENABLE_BITCOINCASH_TOOLS=true -ENABLE_THORCHAIN_TOOLS=true -ENABLE_RIPPLE_TOOLS=true -ENABLE_CARDANO_TOOLS=true # Enable/disable Cardano tools -ENABLE_TON_TOOLS=true # Enable/disable TON tools - -# CoinGecko API Key -COINGECKO_API_KEY=CG-Z9ajuvwG1cheNJSByUgmvTgg #replace with your-coingecko-api-key if you hit rate limits \ No newline at end of file +# Safety Limits (in lovelace, 1 ADA = 1,000,000 lovelace) +VECTOR_SPEND_LIMIT_PER_TX=100000000 # 100 ADA per transaction +VECTOR_SPEND_LIMIT_DAILY=500000000 # 500 ADA daily limit diff --git a/README.md b/README.md index 09f36cc..6872587 100644 --- a/README.md +++ b/README.md @@ -1,354 +1,112 @@ -# Web3 MCP +# Vector MCP Server -A Model-Context-Protocol server for interacting with multiple blockchains including Solana, Ethereum, THORChain, XRP Ledger, TON (The Open Network), Cardano, and UTXO chains. This server provides simple RPC endpoints for common blockchain operations, allowing secure interactions with various blockchains through environment variables. +MCP (Model Context Protocol) server for the **Vector blockchain** — Apex Fusion's L2. Enables AI agents (Claude, GPT, Gemini, or any MCP client) to interact with Vector natively: query balances, send ADA, transfer tokens, and manage spending safely. -Web3 Server MCP server +Built on [Ogmios](https://ogmios.dev/) + [Koios](https://www.koios.rest/) — no Blockfrost dependency. ## Features -Solana Operations: -- Check SOL account balances -- Get detailed account information -- Transfer SOL between accounts -- View SPL token balances -- Get your wallet address from private key -- Swap tokens using Jupiter (Best price routing across all Solana DEXs) +- **Wallet management** — derive addresses from mnemonic, query balances and UTxOs +- **ADA transfers** — build, sign, and submit transactions via submit-api +- **Native token transfers** — send any Vector native asset +- **Safety controls** — per-transaction and daily spend limits with audit logging +- **Ogmios-native** — direct chain access, no third-party indexer lock-in -Ethereum & EVM Chain Operations: -- Check native token balances across multiple networks -- Check ERC-20 token balances -- Send native tokens (using private key from .env) -- Send ERC-20 tokens (using private key from .env) -- Approve ERC-20 token spending (using private key from .env) +## MCP Tools -Cardano Operations: -- Get network information and statistics -- Check address balances and transaction history -- View UTxOs for an address -- Explore stake pools and delegation information -- Get details about native assets -- View detailed transaction information -- Get statistics about the current epoch +| Tool | Description | +|------|-------------| +| `vector_get_balance` | Get ADA and token balances for any Vector address | +| `vector_get_address` | Get the agent's wallet address and balance | +| `vector_get_utxos` | List UTxOs for an address or the agent's wallet | +| `vector_send_ada` | Send ADA (respects spend limits) | +| `vector_send_tokens` | Send native tokens | +| `vector_get_spend_limits` | Check spend limits and remaining budget | -THORChain Operations: -- Check RUNE balances -- Get detailed pool information -- Get swap quotes between any supported assets -- Cross-chain swaps via THORChain protocol +## Quick Start -UTXO Chain Operations: -- Bitcoin (BTC) - - Check address balances - - View transaction history - - Validate addresses - - Get network info and fees -- Litecoin (LTC) - - Check address balances - - View transaction history - - Validate addresses - - Get network info and fees -- Dogecoin (DOGE) - - Check address balances - - View transaction history - - Validate addresses - - Get network info and fees -- Bitcoin Cash (BCH) - - Check address balances - - View transaction history - - Validate addresses - - Get network info and fees +### 1. Install dependencies -XRP Ledger Operations: -- Check XRP account balances -- View transaction history -- Validate XRP addresses -- Send XRP to another address -- Get current XRP Ledger information -- Check token balances -- Create trustlines for tokens - -TON (The Open Network) Operations: -- Get TON account balances -- View transaction history -- Validate TON addresses -- Send TON to another address with optional memo/comment -- Get current TON network information - -Supported EVM Networks: -- Ethereum -- Base -- Arbitrum -- Optimism -- BSC (Binance Smart Chain) -- Polygon -- Avalanche -- Berachain - -## Setup - -1. Clone and install dependencies: ```bash -git clone https://github.com/strangelove-ventures/web3-mcp.git -cd web3-mcp npm install ``` -2. Create a .env file in the root directory: +### 2. Configure environment + ```bash cp .env.example .env +# Edit .env with your mnemonic and endpoint URLs ``` -3. Configure your environment variables in .env: +### 3. Build -### Required Configuration - -```env -# Tool Registration Controls -ENABLE_SOLANA_TOOLS=true # Enable/disable Solana tools -ENABLE_ETHEREUM_TOOLS=true # Enable/disable Ethereum and EVM chain tools -ENABLE_CARDANO_TOOLS=true # Enable/disable Cardano tools - -# UTXO Chain Tools -ENABLE_BITCOIN_TOOLS=true # Enable/disable Bitcoin tools -ENABLE_LITECOIN_TOOLS=true # Enable/disable Litecoin tools -ENABLE_DOGECOIN_TOOLS=true # Enable/disable Dogecoin tools -ENABLE_BITCOINCASH_TOOLS=true # Enable/disable Bitcoin Cash tools -ENABLE_THORCHAIN_TOOLS=true # Enable/disable THORChain tools -ENABLE_RIPPLE_TOOLS=true # Enable/disable XRP Ledger tools -ENABLE_TON_TOOLS=true # Enable/disable TON tools - -# Private Keys (required for transactions) -ETH_PRIVATE_KEY=your-ethereum-private-key -SOLANA_PRIVATE_KEY=your-base58-encoded-solana-private-key - -# XRP Ledger credentials (required for XRP transactions) -# Either private key or mnemonic is required -XRP_PRIVATE_KEY=your-xrp-private-key-in-hex -# OR -XRP_MNEMONIC=your-xrp-mnemonic-recovery-phrase -# Optional - used to verify the derived address -XRP_ADDRESS=your-xrp-account-address - -# TON Configuration -TON_MNEMONIC=word1 word2 word3 ... word24 # 24-word recovery phrase for TON wallet -TON_ADDRESS=your-ton-wallet-address # Your TON wallet address -TON_API_KEY=your-toncenter-api-key # Get from @tonapibot on Telegram (optional but recommended) -TON_RPC_URL=https://toncenter.com/api/v2/jsonRPC # Optional - default is TON Center API - -# Cardano Configuration -BLOCKFROST_API_KEY=your-blockfrost-api-key # Get a real API key from https://blockfrost.io/ -CARDANO_NETWORK=mainnet # or 'testnet', 'preview', 'preprod' -CARDANO_MNEMONIC=your-cardano-mnemonic-phrase # Required for transaction signing -CARDANO_ACCOUNT_INDEX=0 # Optional - defaults to 0 -``` - -### Optional Configuration - -```env -# Network RPC URLs (optional - will use public endpoints if not specified) -SOLANA_RPC_URL=https://api.mainnet-beta.solana.com - -# Ethereum & Layer 2s -ETH_RPC_URL=https://eth-mainnet.g.alchemy.com/v2/your-api-key -ARBITRUM_RPC_URL=https://arb1.arbitrum.io/rpc -BASE_RPC_URL=https://mainnet.base.org -OPTIMISM_RPC_URL=https://mainnet.optimism.io - -# Other EVM Chains -BSC_RPC_URL=https://bsc-dataseed.binance.org -POLYGON_RPC_URL=https://polygon-rpc.com -AVALANCHE_RPC_URL=https://api.avax.network/ext/bc/C/rpc -BERACHAIN_RPC_URL=https://rpc.berachain.com - -# XRP Ledger -XRP_RPC_URL=https://xrplcluster.com/ # Optional - will use public endpoint if not specified - -# TON Network -TON_RPC_URL=https://toncenter.com/api/v2/jsonRPC # Optional - default is TON Center API -TON_API_KEY=your-toncenter-api-key # Get from @tonapibot on Telegram (optional but recommended) - -# THORChain Configuration -THORCHAIN_NODE_URL=https://thornode.ninerealms.com # Optional - will use public endpoint if not specified -THORCHAIN_PRIVATE_KEY=your-thorchain-private-key -THORCHAIN_MNEMONIC=your-thorchain-mnemonic - -# UTXO Chain API Keys (optional) -BLOCKCYPHER_API_KEY=your-blockcypher-api-key -SOCHAIN_API_KEY=your-sochain-api-key -``` - -4. Build the tool: ```bash npm run build ``` -5. Add the tool to your claude_desktop_config.json: +### 4. Add to Claude Desktop + +Add to your Claude Desktop MCP config (`claude_desktop_config.json`): + ```json { "mcpServers": { - "web3-rpc": { + "vector": { "command": "node", - "args": [ - "/PATH/TO/web3-mcp/build/index.js" - ] + "args": ["/path/to/vector-mcp-server/build/index.js"], + "env": { + "VECTOR_MNEMONIC": "your mnemonic words here", + "VECTOR_OGMIOS_URL": "https://ogmios.vector.testnet.apexfusion.org", + "VECTOR_SUBMIT_URL": "https://submit.vector.testnet.apexfusion.org/api/submit/tx", + "VECTOR_KOIOS_URL": "https://koios.vector.testnet.apexfusion.org/", + "VECTOR_EXPLORER_URL": "https://vector.testnet.apexscan.org" + } } } } ``` -## Tool Registration +## Configuration -The Web3 MCP server allows you to control which blockchain tools are registered through environment variables: +| Variable | Description | Default | +|----------|-------------|---------| +| `VECTOR_OGMIOS_URL` | Ogmios HTTP JSON-RPC endpoint | `https://ogmios.vector.testnet.apexfusion.org` | +| `VECTOR_KOIOS_URL` | Koios REST API endpoint | `https://koios.vector.testnet.apexfusion.org/` | +| `VECTOR_SUBMIT_URL` | Transaction submit API | `https://submit.vector.testnet.apexfusion.org/api/submit/tx` | +| `VECTOR_EXPLORER_URL` | Block explorer base URL | `https://vector.testnet.apexscan.org` | +| `VECTOR_MNEMONIC` | 15 or 24-word BIP39 mnemonic | *(required)* | +| `VECTOR_ACCOUNT_INDEX` | HD wallet account index | `0` | +| `VECTOR_SPEND_LIMIT_PER_TX` | Max lovelace per transaction | `100000000` (100 ADA) | +| `VECTOR_SPEND_LIMIT_DAILY` | Max lovelace per day | `500000000` (500 ADA) | -- `ENABLE_SOLANA_TOOLS`: Enable/disable Solana tools -- `ENABLE_ETHEREUM_TOOLS`: Enable/disable Ethereum and EVM chain tools -- `ENABLE_BITCOIN_TOOLS`: Enable/disable Bitcoin tools -- `ENABLE_LITECOIN_TOOLS`: Enable/disable Litecoin tools -- `ENABLE_DOGECOIN_TOOLS`: Enable/disable Dogecoin tools -- `ENABLE_BITCOINCASH_TOOLS`: Enable/disable Bitcoin Cash tools -- `ENABLE_THORCHAIN_TOOLS`: Enable/disable THORChain tools -- `ENABLE_RIPPLE_TOOLS`: Enable/disable XRP Ledger tools -- `ENABLE_CARDANO_TOOLS`: Enable/disable Cardano tools -- `ENABLE_TON_TOOLS`: Enable/disable TON tools +## Architecture -Set these variables to `true` or `false` in your `.env` file to control which tools are available to the server. This allows you to: +``` +┌──────────────────────┐ ┌──────────────────────────┐ +│ Claude / GPT / etc. │◄────►│ vector-mcp-server │ +│ (any MCP client) │ MCP │ │ +└──────────────────────┘ │ ┌────────────────────┐ │ + │ │ Safety Layer │ │ + │ │ - Per-tx limits │ │ + │ │ - Daily limits │ │ + │ │ - Audit log │ │ + │ └────────┬───────────┘ │ + │ │ │ + │ ┌────────▼───────────┐ │ + │ │ Lucid + Ogmios │ │ + │ │ Provider │ │ + │ └────────┬───────────┘ │ + │ │ │ + │ ┌────────▼───────────┐ │ + │ │ Ogmios / Koios / │ │ + │ │ Submit API │ │ + │ └────────────────────┘ │ + └──────────────────────────┘ +``` -- Reduce startup time by only loading required tools -- Minimize security surface area by disabling unused chains -- Customize the server for specific use cases -- Control resource usage by limiting active connections +## About Vector -## Usage Examples +Vector is Apex Fusion's UTXO-based L2 blockchain, running with Cardano's mainnet parameters. It provides near-instant finality and 4x Cardano throughput, making it ideal for AI agent interactions. -Ask Claude (or your MCP client of choice): - -### Solana Operations (when ENABLE_SOLANA_TOOLS=true) -- "What's my Solana address?" - Shows your address derived from private key in .env -- "What's the balance of 62QXuWZ3WT6ws1ZFxJobVDVXn6bEsiYpLo5yG612U6u3?" -- "Transfer 0.001 SOL to Cg6cVS4tjkxHthm3K9BHhmvqF7kSz8GnXqqYXnHBzGXd" -- "Show me my SPL token balances" -- "Swap 0.1 SOL to USDC" (Uses Jupiter for best price routing) - -### EVM Operations (when ENABLE_ETHEREUM_TOOLS=true) -- "What's the ETH balance of 0x556437c4d22ceaeeebf82006b85bdcc0ae67d933?" -- "Check the USDC balance for 0x556437c4d22ceaeeebf82006b85bdcc0ae67d933 on Ethereum" -- "Send 0.1 ETH to 0x556437c4d22ceaeeebf82006b85bdcc0ae67d933" -- "What's the current gas price on Arbitrum?" -- "Send 100 USDC to 0x556437c4d22ceaeeebf82006b85bdcc0ae67d933 on Polygon" - -### Cardano Operations (when ENABLE_CARDANO_TOOLS=true) -- Get the balance of any Cardano -- View your wallet information -- Send ADA to another address -- Send native tokens to another address - -### Bitcoin Operations (when ENABLE_BITCOIN_TOOLS=true) -- "What's the BTC balance of 1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa?" -- "Show me the transaction history for bc1qxy2kgdygjrsqtzq2n0yrf2493p83kkfjhx0wlh" -- "Validate this Bitcoin address: 1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa" - -### Litecoin Operations (when ENABLE_LITECOIN_TOOLS=true) -- "What's the LTC balance of LM2WMpR1Rp6j3Sa59cMXMs1SPzj9eXpGc1?" -- "Show me the transaction history for this Litecoin address" -- "What's the current Litecoin network fee?" - -### Dogecoin Operations (when ENABLE_DOGECOIN_TOOLS=true) -- "Check this DOGE address balance: D8vFz4p1L37jdg47HXKtSHA5uYLYxbGgPD" -- "Show me recent Dogecoin transactions" -- "What are the current DOGE network fees?" - -### Bitcoin Cash Operations (when ENABLE_BITCOINCASH_TOOLS=true) -- "What's the BCH balance of this address?" -- "Show me the BCH transaction history" -- "Validate this Bitcoin Cash address" - -### THORChain Operations (when ENABLE_THORCHAIN_TOOLS=true) -- "What's the RUNE balance of thor13zpdckczd0jvyhwxmrwnpap8gmy9m5kk2gzum3?" -- "Show me the pool information for BTC.BTC" -- "Get a swap quote for 0.1 BTC.BTC to ETH.ETH" - -### XRP Ledger Operations (when ENABLE_RIPPLE_TOOLS=true) -- "What's the XRP balance of rPT1Sjq2YGrBMTttX4GZHjKu9dyfzbpAYe?" -- "Show me the transaction history for rPT1Sjq2YGrBMTttX4GZHjKu9dyfzbpAYe" -- "Is rPT1Sjq2YGrBMTttX4GZHjKu9dyfzbpAYe a valid XRP address?" -- "Send 10 XRP to rPT1Sjq2YGrBMTttX4GZHjKu9dyfzbpAYe" -- "What's the current XRP Ledger information?" -- "Show me token balances for rPT1Sjq2YGrBMTttX4GZHjKu9dyfzbpAYe" -- "Create a trustline for USD with issuer rPT1Sjq2YGrBMTttX4GZHjKu9dyfzbpAYe with a limit of 1000" - -### TON Operations (when ENABLE_TON_TOOLS=true) -- "What's the TON balance of EQAAFhjXzKuQ5N0c96nsdZQWATcJm909LYSaCAvWFQF8tvUZ?" -- "Show me the transaction history for EQAAFhjXzKuQ5N0c96nsdZQWATcJm909LYSaCAvWFQF8tvUZ" -- "Is UQD0BRQt-QdIEbsjuRsMqzDlBkUAEfQixShDECoKEOXRc4eR a valid TON address?" -- "Send 0.1 TON to EQAAFhjXzKuQ5N0c96nsdZQWATcJm909LYSaCAvWFQF8tvUZ" -- "Send 0.01 TON to UQD0BRQt-QdIEbsjuRsMqzDlBkUAEfQixShDECoKEOXRc4eR with comment 'test payment'" -- "What's the current TON Network information?" - -## Security Notes - -1. **Environment Variables**: All private keys are stored in the .env file and never exposed in the conversation history -2. **Private Keys**: Only use this with test wallets containing small amounts of funds -3. **RPC Endpoints**: Custom RPC endpoints can be configured in the .env file for better reliability and rate limits -4. **.env Security**: The .env file is automatically ignored by git to prevent accidental exposure of private keys -5. **Tool Registration**: Use the tool registration controls to minimize security surface area by only enabling required chains - -## Advanced Configuration - -### Custom RPC Endpoints -You can configure custom RPC endpoints in your .env file for better reliability and higher rate limits. If not specified, the tool will fall back to public RPC endpoints. - -### Network Selection -For EVM operations, you can specify the network by name (ethereum, base, arbitrum, optimism, bsc, polygon, avalanche, berachain). The tool will automatically use the appropriate RPC endpoint and network configuration. - -### Cardano Configuration -The tool uses the Blockfrost API and Lucid library to interact with the Cardano blockchain. -- `BLOCKFROST_API_KEY`: Required - Your Blockfrost API key (register at https://blockfrost.io/) -- `CARDANO_NETWORK`: Optional - The Cardano network to use (mainnet, testnet, preview, preprod). Defaults to 'mainnet' -- `CARDANO_MNEMONIC`: Required for transactions - Your Cardano wallet's mnemonic phrase (15 or 24 words) -- `CARDANO_ACCOUNT_INDEX`: Optional - The account index to use (defaults to 0) - -The wallet derived from your mnemonic will be used to sign and send transactions. - -### THORChain Configuration -The tool uses Nine Realms public endpoints by default, but you can configure a custom THORChain node URL in the .env file for better reliability and rate limits. - -### XRP Ledger Configuration -The tool can use either a private key or mnemonic phrase for XRP transactions. Configure these in your .env file: -- `XRP_PRIVATE_KEY`: Your XRP private key in hex format -- `XRP_MNEMONIC`: Alternative to private key - your 12-word recovery phrase -- `XRP_ADDRESS`: Optional - Your XRP account address (used to verify the derived address) -- `XRP_RPC_URL`: Optional - Custom XRP Ledger node URL (defaults to public endpoint) - -### TON Configuration -The tool uses TON Center's API by default and requires a mnemonic phrase for TON transactions. Configure these in your .env file: -- `TON_MNEMONIC`: Required - Your 24-word recovery phrase for TON wallet -- `TON_ADDRESS`: Required - Your TON wallet address -- `TON_API_KEY`: Recommended - API key from @tonapibot on Telegram (for higher rate limits) -- `TON_RPC_URL`: Optional - Custom TON RPC URL (defaults to TON Center API) - -The implementation includes automatic retry logic with exponential backoff for rate limit handling. - -### UTXO Chain Data Providers -The tool uses several data providers for UTXO chains: -- BlockCypher -- SoChain -- Haskoin (for Bitcoin Cash) - -You can configure API keys for these providers in the .env file for better rate limits. - -## Development - -To modify or extend the tool: - -1. Source code is in the `src` directory -2. Chain-specific code in `src/chains` -3. Run `npm run build` after making changes -4. Use TypeScript for all new code - -## Contributing - -Contributions are welcome! Please submit pull requests with any improvements or bug fixes. - -## License - -ISC License \ No newline at end of file +- **Explorer:** https://vector.testnet.apexscan.org +- **Apex Fusion:** https://apexfusion.org diff --git a/docs/CARDANO.md b/docs/CARDANO.md deleted file mode 100644 index 20fc25c..0000000 --- a/docs/CARDANO.md +++ /dev/null @@ -1,213 +0,0 @@ -# Cardano Support for Web3-MCP - -This document covers the Cardano blockchain integration for web3-mcp. - -## Overview - -The Cardano integration in web3-mcp allows you to interact with the Cardano blockchain through the Blockfrost API, providing access to network data, address information, transaction details, stake pools, and more. - -## Prerequisites - -To use the Cardano integration, you need a Blockfrost API key. You can get one by signing up at [blockfrost.io](https://blockfrost.io/). - -## Configuration - -Update your `.env` file with the following settings: - -``` -# Cardano Configuration -BLOCKFROST_API_KEY=your-blockfrost-api-key-here -CARDANO_NETWORK=mainnet # or 'testnet', 'preview', 'preprod' -ENABLE_CARDANO_TOOLS=true -``` - -## Available Tools - -### Network Information - -#### 1. Get Cardano Network Information - -```javascript -const response = await server.call("getCardanoNetworkInfo", {}); -``` - -Returns current information about the Cardano network including: -- Latest block details (height, hash, slot, epoch) -- Network parameters -- Timing information - -#### 2. Get Cardano Network Statistics - -```javascript -const response = await server.call("getCardanoNetworkStats", {}); -``` - -Returns statistics about the Cardano network including: -- Number of blocks -- Number of transactions -- Number of native tokens -- Number of addresses with ADA -- Number of delegated stakes - -#### 3. Get Current Epoch Information - -```javascript -const response = await server.call("getCardanoEpochInfo", {}); -``` - -Returns information about the current epoch including: -- Epoch number -- Start and end time -- Block and transaction counts -- Protocol parameters - -### Address Operations - -#### 1. Get Address Information - -```javascript -const response = await server.call("getCardanoAddressInfo", { - address: "addr1qyx...", -}); -``` - -Returns information about a Cardano address including: -- Balance in ADA -- UTxO count -- Address type (script or key-based) -- Stake address (if delegated) - -#### 2. Get Address Transactions - -```javascript -const response = await server.call("getCardanoAddressTransactions", { - address: "addr1qyx...", - count: 10, // Optional, default is 10 - page: 1 // Optional, default is 1 -}); -``` - -Returns a list of transactions for the specified address, including: -- Transaction hash -- Block and slot -- Output amounts -- Fees -- Date and time - -#### 3. Get Address UTxOs - -```javascript -const response = await server.call("getCardanoAddressUtxos", { - address: "addr1qyx...", -}); -``` - -Returns a list of unspent transaction outputs (UTxOs) for the specified address. - -### Staking Operations - -#### 1. Get Stake Address Information - -```javascript -const response = await server.call("getCardanoStakeAddressInfo", { - stakeAddress: "stake1ux...", -}); -``` - -Returns information about a Cardano stake address including: -- Total balance delegated -- Rewards balance -- Withdrawal history -- Delegation information -- Recent rewards - -#### 2. Get Pool Information - -```javascript -const response = await server.call("getCardanoPoolInfo", { - poolId: "pool1...", -}); -``` - -Returns detailed information about a specific stake pool: -- Pool metadata (name, ticker, description) -- Performance metrics -- Stake and delegator information -- Reward details - -#### 3. List Top Stake Pools - -```javascript -const response = await server.call("listCardanoTopPools", { - count: 10, // Optional, default is 10 -}); -``` - -Returns a list of the top stake pools by live stake, including key metrics for each pool. - -### Asset Operations - -#### 1. Get Asset Information - -```javascript -const response = await server.call("getCardanoAssetInfo", { - assetId: "asset1...", -}); -``` - -Returns detailed information about a Cardano native asset: -- Asset policy and name -- Total supply -- Mint/burn history -- Metadata -- Top addresses holding the asset - -### Transaction Operations - -#### 1. Get Transaction Information - -```javascript -const response = await server.call("getCardanoTransactionInfo", { - txHash: "5c7...", -}); -``` - -Returns detailed information about a Cardano transaction: -- Basic transaction details (block, slot, fees) -- Inputs and outputs -- Metadata -- Validity interval - -## Error Handling - -All tools return a standardized error format if they fail: - -```javascript -{ - content: [ - { - type: "text", - text: "Failed to [operation]: [error message]" - } - ] -} -``` - -## Advanced Usage - -For more advanced Cardano operations that go beyond querying blockchain data, such as: - -1. Creating and submitting transactions -2. Minting native tokens -3. Working with smart contracts - -You may need to extend the functionality by implementing additional tools that use: - -- [Cardano Serialization Library](https://github.com/Emurgo/cardano-serialization-lib) -- [Mesh.js](https://meshjs.dev/) for client-side operations - -## References - -- [Blockfrost Documentation](https://docs.blockfrost.io/) -- [Cardano Developer Portal](https://developers.cardano.org/) -- [Mesh.js Documentation](https://meshjs.dev/) diff --git a/docs/VECTOR.md b/docs/VECTOR.md new file mode 100644 index 0000000..c8b743a --- /dev/null +++ b/docs/VECTOR.md @@ -0,0 +1,104 @@ +# Vector MCP Server — Tool Reference + +## Overview + +The Vector MCP Server exposes blockchain tools that AI agents can use to interact with the Vector network. All tools use the `vector_` prefix and communicate with Vector via Ogmios (chain queries) and the submit API (transaction submission). + +## Prerequisites + +1. A funded Vector wallet (15 or 24-word mnemonic) +2. Access to Vector network endpoints (Ogmios, submit API, Koios) +3. Node.js >= 14.0.0 + +## Tools + +### vector_get_balance + +Get ADA and token balances for any Vector address. + +**Parameters:** +- `address` (string, required) — Vector address to check (addr1...) + +**Example prompt:** "What's the balance of addr1qx..." + +### vector_get_address + +Get the agent's own wallet address, balance, and token holdings. + +**Parameters:** None + +**Example prompt:** "What's my wallet address and balance?" + +### vector_get_utxos + +List unspent transaction outputs for an address or the agent's wallet. + +**Parameters:** +- `address` (string, optional) — Address to query. Uses agent's wallet if omitted. + +**Example prompt:** "Show me the UTxOs in my wallet" + +### vector_send_ada + +Send ADA from the agent's wallet to a recipient. Enforces spend limits. + +**Parameters:** +- `recipientAddress` (string, required) — Recipient address (addr1...) +- `amount` (number, required) — Amount of ADA to send (minimum 1) +- `metadata` (string, optional) — Transaction metadata in JSON format + +**Example prompt:** "Send 5 ADA to addr1qy..." + +### vector_send_tokens + +Send Vector native tokens from the agent's wallet. + +**Parameters:** +- `recipientAddress` (string, required) — Recipient address (addr1...) +- `policyId` (string, required) — Token policy ID +- `assetName` (string, required) — Asset name (can be empty) +- `amount` (string, required) — Amount of tokens to send +- `adaAmount` (number, optional) — ADA to include with tokens + +**Example prompt:** "Send 100 of token with policy abc123... to addr1qy..." + +### vector_get_spend_limits + +Check current spend limits, daily usage, and recent transaction audit log. + +**Parameters:** None + +**Example prompt:** "What are my current spend limits?" + +## Safety Controls + +The server enforces configurable spend limits: + +- **Per-transaction limit** — Maximum ADA per single transaction (default: 100 ADA) +- **Daily limit** — Maximum ADA per 24-hour period (default: 500 ADA) +- **Audit log** — All transactions are logged with timestamp, amount, and recipient + +Limits are configured via environment variables: +``` +VECTOR_SPEND_LIMIT_PER_TX=100000000 # 100 ADA in lovelace +VECTOR_SPEND_LIMIT_DAILY=500000000 # 500 ADA in lovelace +``` + +## Network Endpoints + +| Service | URL | Protocol | +|---------|-----|----------| +| Ogmios | ogmios.vector.testnet.apexfusion.org | HTTP JSON-RPC + WebSocket | +| Submit API | submit.vector.testnet.apexfusion.org/api/submit/tx | HTTP POST (CBOR) | +| Koios | koios.vector.testnet.apexfusion.org | REST API | +| Explorer | vector.testnet.apexscan.org | Web UI | + +## Error Handling + +All tools return helpful error messages with troubleshooting tips. Common issues: + +- **Invalid mnemonic** — Check VECTOR_MNEMONIC has exactly 15 or 24 words +- **Insufficient balance** — Ensure wallet has enough ADA for the transaction + fees +- **Spend limit exceeded** — Check limits with `vector_get_spend_limits` +- **Connection failed** — Verify Ogmios endpoint is reachable +- **Invalid address** — Vector addresses start with `addr1` diff --git a/package.json b/package.json index 4094b38..feb7f18 100644 --- a/package.json +++ b/package.json @@ -1,47 +1,39 @@ { - "name": "web3-mcp", + "name": "vector-mcp-server", "version": "1.0.0", "main": "index.js", "type": "module", "bin": { - "web3-mcp": "./build/index.js" + "vector-mcp": "./build/index.js" }, "scripts": { "clean": "rm -rf build", - "build": "npm run clean && tsc && chmod +x build/index.js", + "build": "npm run clean && NODE_OPTIONS='--max-old-space-size=8192' tsc && chmod +x build/index.js", "start": "node build/index.js" }, "files": [ "build" ], "keywords": [ - "solana", + "vector", "mcp", - "rpc", "blockchain", - "xrp", - "ripple", "cardano", - "ton" + "apex-fusion", + "ai-agent", + "utxo" ], - "author": "", + "author": "Apex Fusion", "license": "ISC", - "description": "Blockchain RPC Model-Context-Protocol Server for Solana, EVM chains, UTXO chains, Ripple (XRP), Cardano, TON and more", + "description": "MCP server for Vector blockchain — AI agent interface for Apex Fusion's L2", "repository": { "type": "git", - "url": "https://github.com/akc2267/web3-mcp.git" + "url": "https://github.com/Apex-Fusion/vector-ai-agents.git" }, - "homepage": "https://github.com/akc2267/web3-mcp#readme", + "homepage": "https://github.com/Apex-Fusion/vector-ai-agents#readme", "bugs": { - "url": "https://github.com/akc2267/web3-mcp/issues" + "url": "https://github.com/Apex-Fusion/vector-ai-agents/issues" }, - "contributors": [ - { - "name": "Alex Cheng", - "email": "alexanderkcheng@gmail.com", - "url": "https://github.com/akc2267" - } - ], "engines": { "node": ">=14.0.0" }, @@ -50,42 +42,14 @@ "registry": "https://registry.npmjs.org/" }, "dependencies": { - "@cosmos-client/core": "^0.46.1", - "@jup-ag/api": "^6.0.12", "@modelcontextprotocol/sdk": "^1.4.1", - "@psf/bitcoincashjs-lib": "^4.0.3", - "@solana/spl-token": "^0.4.1", - "@solana/web3.js": "^1.87.6", - "@xchainjs/xchain-bitcoin": "^1.2.2", - "@xchainjs/xchain-bitcoincash": "^1.0.10", - "@xchainjs/xchain-client": "^1.0.10", - "@xchainjs/xchain-crypto": "^0.3.7", - "@xchainjs/xchain-doge": "^1.0.10", - "@xchainjs/xchain-litecoin": "^1.0.10", - "@xchainjs/xchain-midgard-query": "^1.0.11", - "@xchainjs/xchain-thorchain": "^2.0.13", - "@xchainjs/xchain-thorchain-amm": "^2.0.23", - "@xchainjs/xchain-thorchain-query": "^1.0.13", - "@xchainjs/xchain-util": "^1.0.8", - "bchaddrjs": "^0.5.2", - "bip39": "^3.1.0", - "bitcoinjs-lib": "^6.1.5", - "bs58": "^5.0.0", "cross-fetch": "^4.0.0", "dotenv": "^16.4.5", - "ethers": "^6.11.1", - "fetch-retry": "^5.0.6", "lucid-cardano": "^0.10.11", - "ripple-keypairs": "^1.3.1", - "wif": "^4.0.0", - "xrpl": "^2.14.0", - "@ton/ton": "13.5.1", - "@ton/crypto": "3.2.0", - "@ton/core": "0.52.2", "zod": "^3.24.1" }, "devDependencies": { "@types/node": "^22.13.1", "typescript": "^5.7.3" } -} \ No newline at end of file +} diff --git a/shell.nix b/shell.nix new file mode 100644 index 0000000..9dce029 --- /dev/null +++ b/shell.nix @@ -0,0 +1,8 @@ +{ pkgs ? import {} }: + +pkgs.mkShell { + buildInputs = with pkgs; [ + nodejs_20 + nodePackages.npm + ]; +} diff --git a/src/chains/UTXO/bitcoin.ts b/src/chains/UTXO/bitcoin.ts deleted file mode 100644 index b04d19f..0000000 --- a/src/chains/UTXO/bitcoin.ts +++ /dev/null @@ -1,145 +0,0 @@ -import { Client as BitcoinClient, defaultBTCParams, BlockcypherDataProviders as BTCBlockcypherDataProviders } from '@xchainjs/xchain-bitcoin' -import { Network } from '@xchainjs/xchain-client' -import { z } from "zod" -import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js" - -// Initialize client with mainnet by default -const bitcoinClient = new BitcoinClient({ - ...defaultBTCParams, - network: Network.Mainnet, - dataProviders: [BTCBlockcypherDataProviders], -}) - -// Helper function to format base amounts -function formatBaseAmount(baseAmount: any): string { - return baseAmount.amount().toString() -} - -export function registerBitcoinTools(server: McpServer) { - // Bitcoin Balance - server.tool( - "getBitcoinBalance", - "Get balance for a Bitcoin address", - { - address: z.string().describe("Bitcoin address to check"), - }, - async ({ address }) => { - try { - const balances = await bitcoinClient.getBalance(address) - return { - content: [ - { - type: "text", - text: `Bitcoin Balance for ${address}:\n${formatBaseAmount(balances[0].amount)} BTC`, - }, - ], - } - } catch (err) { - const error = err as Error - return { - content: [{ type: "text", text: `Failed to retrieve BTC balance: ${error.message}` }], - } - } - } - ) - - // Transaction History - server.tool( - "getBTCTransactionHistory", - "Get transaction history for a Bitcoin address", - { - address: z.string().describe("Bitcoin address to check"), - limit: z.number().optional().describe("Maximum number of transactions to return"), - offset: z.number().optional().describe("Number of transactions to skip"), - }, - async ({ address, limit = 10, offset = 0 }) => { - try { - const txs = await bitcoinClient.getTransactions({ address, limit, offset }) - const txList = txs.txs.map((tx: any) => { - const fromAmount = tx.from[0]?.amount ? `${formatBaseAmount(tx.from[0].amount)} BTC` : 'Unknown' - const toAmount = tx.to[0]?.amount ? `${formatBaseAmount(tx.to[0].amount)} BTC` : 'Unknown' - - return ` -Transaction: ${tx.hash} -Type: ${tx.type} -From: ${tx.from[0]?.from || 'Unknown'} (${fromAmount}) -To: ${tx.to[0]?.to || 'Unknown'} (${toAmount}) -Asset: ${tx.asset.ticker} -Date: ${new Date(tx.date).toLocaleString()}` - }).join('\n---\n') - - return { - content: [ - { - type: "text", - text: `Bitcoin Transaction History for ${address}:\n${txList}`, - }, - ], - } - } catch (err) { - const error = err as Error - return { - content: [{ type: "text", text: `Failed to retrieve BTC transaction history: ${error.message}` }], - } - } - } - ) - - // Address Validation - server.tool( - "validateBTCAddress", - "Validate a Bitcoin address format", - { - address: z.string().describe("Bitcoin address to validate"), - }, - async ({ address }) => { - try { - const isValid = bitcoinClient.validateAddress(address) - return { - content: [ - { - type: "text", - text: isValid - ? `The address ${address} is a valid Bitcoin address` - : `The address ${address} is NOT a valid Bitcoin address`, - }, - ], - } - } catch (err) { - const error = err as Error - return { - content: [{ type: "text", text: `Error validating Bitcoin address: ${error.message}` }], - } - } - } - ) - - // Network Info - server.tool( - "getBTCNetworkInfo", - "Get current Bitcoin network information", - {}, - async () => { - try { - const fees = await bitcoinClient.getFeeRates() - return { - content: [ - { - type: "text", - text: `Bitcoin Network Information: -Current Network: ${bitcoinClient.getNetwork()} -Fee Rates (sats/byte): - Fast: ${fees.fast} - Average: ${fees.average}`, - }, - ], - } - } catch (err) { - const error = err as Error - return { - content: [{ type: "text", text: `Failed to retrieve Bitcoin network information: ${error.message}` }], - } - } - } - ) -} \ No newline at end of file diff --git a/src/chains/UTXO/bitcoincash.ts b/src/chains/UTXO/bitcoincash.ts deleted file mode 100644 index 6c727c3..0000000 --- a/src/chains/UTXO/bitcoincash.ts +++ /dev/null @@ -1,144 +0,0 @@ -import { Client as BitcoinCashClient, defaultBchParams } from '@xchainjs/xchain-bitcoincash' -import { Network } from '@xchainjs/xchain-client' -import { z } from "zod" -import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js" - -// Initialize client with mainnet by default -const bitcoinCashClient = new BitcoinCashClient({ - ...defaultBchParams, - network: Network.Mainnet, -}) - -// Helper function to format base amounts -function formatBaseAmount(baseAmount: any): string { - return baseAmount.amount().toString() -} - -export function registerBitcoinCashTools(server: McpServer) { - // Bitcoin Cash Balance - server.tool( - "getBitcoinCashBalance", - "Get balance for a Bitcoin Cash address", - { - address: z.string().describe("Bitcoin Cash address to check"), - }, - async ({ address }) => { - try { - const balances = await bitcoinCashClient.getBalance(address) - return { - content: [ - { - type: "text", - text: `Bitcoin Cash Balance for ${address}:\n${formatBaseAmount(balances[0].amount)} BCH`, - }, - ], - } - } catch (err) { - const error = err as Error - return { - content: [{ type: "text", text: `Failed to retrieve BCH balance: ${error.message}` }], - } - } - } - ) - - // Transaction History - server.tool( - "getBCHTransactionHistory", - "Get transaction history for a Bitcoin Cash address", - { - address: z.string().describe("Bitcoin Cash address to check"), - limit: z.number().optional().describe("Maximum number of transactions to return"), - offset: z.number().optional().describe("Number of transactions to skip"), - }, - async ({ address, limit = 10, offset = 0 }) => { - try { - const txs = await bitcoinCashClient.getTransactions({ address, limit, offset }) - const txList = txs.txs.map((tx: any) => { - const fromAmount = tx.from[0]?.amount ? `${formatBaseAmount(tx.from[0].amount)} BCH` : 'Unknown' - const toAmount = tx.to[0]?.amount ? `${formatBaseAmount(tx.to[0].amount)} BCH` : 'Unknown' - - return ` -Transaction: ${tx.hash} -Type: ${tx.type} -From: ${tx.from[0]?.from || 'Unknown'} (${fromAmount}) -To: ${tx.to[0]?.to || 'Unknown'} (${toAmount}) -Asset: ${tx.asset.ticker} -Date: ${new Date(tx.date).toLocaleString()}` - }).join('\n---\n') - - return { - content: [ - { - type: "text", - text: `Bitcoin Cash Transaction History for ${address}:\n${txList}`, - }, - ], - } - } catch (err) { - const error = err as Error - return { - content: [{ type: "text", text: `Failed to retrieve BCH transaction history: ${error.message}` }], - } - } - } - ) - - // Address Validation - server.tool( - "validateBCHAddress", - "Validate a Bitcoin Cash address format", - { - address: z.string().describe("Bitcoin Cash address to validate"), - }, - async ({ address }) => { - try { - const isValid = bitcoinCashClient.validateAddress(address) - return { - content: [ - { - type: "text", - text: isValid - ? `The address ${address} is a valid Bitcoin Cash address` - : `The address ${address} is NOT a valid Bitcoin Cash address`, - }, - ], - } - } catch (err) { - const error = err as Error - return { - content: [{ type: "text", text: `Error validating Bitcoin Cash address: ${error.message}` }], - } - } - } - ) - - // Network Info - server.tool( - "getBCHNetworkInfo", - "Get current Bitcoin Cash network information", - {}, - async () => { - try { - const fees = await bitcoinCashClient.getFeeRates() - return { - content: [ - { - type: "text", - text: `Bitcoin Cash Network Information: -Current Network: ${bitcoinCashClient.getNetwork()} -Fee Rates (sats/byte): - Fast: ${fees.fast} - Average: ${fees.average}`, - }, - ], - } - } catch (err) { - const error = err as Error - return { - content: [{ type: "text", text: `Failed to retrieve Bitcoin Cash network information: ${error.message}` }], - } - } - } - ) -} \ No newline at end of file diff --git a/src/chains/UTXO/cardano.ts b/src/chains/UTXO/cardano.ts deleted file mode 100644 index 97ad0b8..0000000 --- a/src/chains/UTXO/cardano.ts +++ /dev/null @@ -1,657 +0,0 @@ -import { z } from "zod"; -import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; -import fetch from 'cross-fetch'; -import { Lucid, Blockfrost, fromText } from 'lucid-cardano'; -import * as dotenv from 'dotenv'; -import { resolve } from 'path'; -import { fileURLToPath } from 'url'; -import { existsSync } from 'fs'; - -// Type definitions -export interface CardanoToken { - unit: string; - name: string; - quantity: string; -} - -export interface CardanoWalletInfo { - address: string; - utxoCount: number; - ada: string; - tokens: CardanoToken[]; -} - -export interface CardanoAdaTransactionResult { - txHash: string; - senderAddress: string; - recipientAddress: string; - amount: number; - links: { - explorer: string; - }; -} - -export interface CardanoTokenTransactionResult { - txHash: string; - senderAddress: string; - recipientAddress: string; - token: { - policyId: string; - name: string; - amount: string; - }; - ada: string; - links: { - explorer: string; - }; -} - -// Direct .env loading for debugging -const __filename = fileURLToPath(import.meta.url); -const projectRoot = resolve(__filename, '../../../..'); -const envPath = resolve(projectRoot, '.env'); - -// Load env file directly -if (existsSync(envPath)) { - const result = dotenv.config({ path: envPath }); - if (result.error) { - console.error('Error loading .env file:', result.error); - } -} - -// Configuration from environment variables -const BLOCKFROST_API_KEY = process.env.BLOCKFROST_API_KEY || ''; -const CARDANO_NETWORK = process.env.CARDANO_NETWORK || 'mainnet'; -const BLOCKFROST_BASE_URL = `https://cardano-${CARDANO_NETWORK}.blockfrost.io/api/v0`; -const CARDANO_MNEMONIC = process.env.CARDANO_MNEMONIC || ''; -const CARDANO_ACCOUNT_INDEX = parseInt(process.env.CARDANO_ACCOUNT_INDEX || '0'); - -// Helper function to make Blockfrost API requests -async function blockfrostRequest(endpoint: string): Promise { - const url = `${BLOCKFROST_BASE_URL}${endpoint}`; - - try { - const headers = { - 'project_id': BLOCKFROST_API_KEY - }; - - const response = await fetch(url, { - method: 'GET', - headers: headers - }); - - if (!response.ok) { - let errorText = await response.text(); - throw new Error(`API error ${response.status}: ${errorText}`); - } - - return await response.json() as T; - } catch (error) { - throw error; - } -} - -// Helper function to format ADA amounts -function lovelaceToAda(lovelace: string | number): string { - return (parseInt(String(lovelace)) / 1000000).toFixed(6); -} - -// Helper function to format asset name -function formatAssetName(name: string): string { - try { - // If the name is in hex, try to convert it to ASCII - if (/^[0-9a-fA-F]+$/.test(name)) { - return Buffer.from(name, 'hex').toString('utf8'); - } - return name; - } catch (e) { - return name; // Return original if conversion fails - } -} - -// Initialize Lucid instance -async function initLucid() { - // Map network name to Lucid network name - let network: 'Mainnet' | 'Preprod' | 'Preview'; - if (CARDANO_NETWORK === 'mainnet') { - network = 'Mainnet'; - } else if (['testnet', 'preprod'].includes(CARDANO_NETWORK)) { - network = 'Preprod'; - } else if (CARDANO_NETWORK === 'preview') { - network = 'Preview'; - } else { - network = 'Mainnet'; - } - - try { - // Check for required configurations - if (!BLOCKFROST_API_KEY) { - throw new Error('BLOCKFROST_API_KEY is required in .env file'); - } - - if (!CARDANO_MNEMONIC) { - throw new Error('CARDANO_MNEMONIC is required in .env file'); - } - - // Create Lucid instance - const provider = new Blockfrost( - `https://cardano-${CARDANO_NETWORK}.blockfrost.io/api/v0`, - BLOCKFROST_API_KEY - ); - - const lucid = await Lucid.new(provider, network); - - // Load wallet from mnemonic - try { - // Trim mnemonic and check for valid words - const trimmedMnemonic = CARDANO_MNEMONIC.trim(); - const words = trimmedMnemonic.split(/\s+/); - - // Check if word count is valid (should be 15 or 24 for Cardano) - if (words.length !== 15 && words.length !== 24) { - throw new Error(`Invalid mnemonic: Expected 15 or 24 words, got ${words.length}`); - } - - // Select wallet from seed - lucid.selectWalletFromSeed(trimmedMnemonic, { accountIndex: CARDANO_ACCOUNT_INDEX }); - - // Verify wallet was loaded correctly by checking address - const address = await lucid.wallet.address(); - if (!address) { - throw new Error('Failed to derive address from mnemonic'); - } - - return lucid; - } catch (error) { - throw new Error(`Failed to load wallet: ${error instanceof Error ? error.message : String(error)}`); - } - } catch (error) { - throw new Error(`Failed to initialize Lucid: ${error instanceof Error ? error.message : String(error)}`); - } -} - -// Get wallet info -export async function getWalletInfo(): Promise { - try { - // Initialize Lucid with wallet - const lucid = await initLucid(); - - // Get wallet address - const address = await lucid.wallet.address(); - - // Get UTXOs - const utxos = await lucid.wallet.getUtxos(); - - // Calculate balance - let adaBalance = '0'; - let tokenBalances: CardanoToken[] = []; - - if (utxos.length > 0) { - // Combine all UTXOs to get total balance - const value = utxos.reduce( - (acc, utxo) => acc.add(utxo.assets), - // @ts-ignore: Lucid types are not fully compatible - lucid.newValue() - ).assets; - - // Extract ADA balance - adaBalance = value.lovelace ? lovelaceToAda(value.lovelace) : '0'; - - // Extract token balances - for (const [unit, quantity] of Object.entries(value)) { - if (unit === 'lovelace') continue; - - try { - // Try to get a readable name for the token - // @ts-ignore: Lucid types are not fully compatible - const { policyId, assetName } = lucid.utils.fromUnit(unit); - const displayName = assetName ? - // @ts-ignore: Lucid types are not fully compatible - (lucid.utils.toText(assetName) || formatAssetName(assetName)) : - `${policyId.substring(0, 8)}...`; - - tokenBalances.push({ - unit, - name: displayName, - quantity: (quantity as bigint).toString() - }); - } catch (e) { - tokenBalances.push({ - unit, - name: unit, - quantity: (quantity as bigint).toString() - }); - } - } - } - - return { - address, - utxoCount: utxos.length, - ada: adaBalance, - tokens: tokenBalances - }; - } catch (error) { - throw error; - } -} - -// Send ADA transaction -export async function sendAda( - recipientAddress: string, - amountAda: number, - metadata: any = null -): Promise { - try { - // Validate input - if (!recipientAddress) { - throw new Error('Recipient address is required'); - } - - if (typeof amountAda !== 'number' || amountAda <= 0) { - throw new Error('Amount must be a positive number'); - } - - // Initialize Lucid with wallet - const lucid = await initLucid(); - - // Get sender address for return value - const senderAddress = await lucid.wallet.address(); - - // Validate recipient address - try { - // @ts-ignore: Lucid types are not fully compatible - lucid.utils.getAddressDetails(recipientAddress); - } catch (error) { - throw new Error(`Invalid recipient address: ${error instanceof Error ? error.message : String(error)}`); - } - - // Convert ADA to lovelace - const lovelaceAmount = BigInt(Math.floor(amountAda * 1000000)); - - // Build transaction - // @ts-ignore: Lucid types are not fully compatible - let tx = lucid.newTx() - .payToAddress(recipientAddress, { lovelace: lovelaceAmount }); - - // Add metadata if provided - if (metadata) { - const parsedMetadata = typeof metadata === 'string' ? JSON.parse(metadata) : metadata; - // @ts-ignore: Lucid types are not fully compatible - tx = tx.attachMetadata(674, parsedMetadata); - } - - // Complete the transaction (adds inputs and change output) - // @ts-ignore: Lucid types are not fully compatible - tx = await tx.complete(); - - // Sign the transaction - // @ts-ignore: Lucid types are not fully compatible - const signedTx = await tx.sign().complete(); - - // Submit the transaction - const txHash = await signedTx.submit(); - - return { - txHash, - senderAddress, - recipientAddress, - amount: amountAda, - links: { - explorer: `https://cardanoscan.io/transaction/${txHash}` - } - }; - } catch (error) { - throw error; - } -} - -// Send tokens transaction -export async function sendTokens( - recipientAddress: string, - policyId: string, - assetName: string, - amount: string, - adaAmount: number | null = null -): Promise { - try { - // Validate input - if (!recipientAddress) { - throw new Error('Recipient address is required'); - } - - if (!policyId) { - throw new Error('Policy ID is required'); - } - - if (!amount || isNaN(Number(amount)) || Number(amount) <= 0) { - throw new Error('Amount must be a positive number'); - } - - // Initialize Lucid with wallet - const lucid = await initLucid(); - - // Get sender address for return value - const senderAddress = await lucid.wallet.address(); - - // Validate recipient address - try { - // @ts-ignore: Lucid types are not fully compatible - lucid.utils.getAddressDetails(recipientAddress); - } catch (error) { - throw new Error(`Invalid recipient address: ${error instanceof Error ? error.message : String(error)}`); - } - - // Convert asset name to hex if it's in readable format - let assetNameHex = assetName; - if (assetName && !/^[0-9a-fA-F]+$/.test(assetName)) { - assetNameHex = fromText(assetName); - } - - // Create the unit identifier - const unit = `${policyId}${assetNameHex}`; - - // Create the assets object - const assets = { [unit]: BigInt(amount) }; - - // Calculate minimum required ADA - // @ts-ignore: Lucid types are not fully compatible - const minLovelace = await lucid.utils.minAdaRequired({ - outputs: [{ address: recipientAddress, assets }] - }); - - // Use provided ADA amount or minimum required - const outputLovelace = adaAmount - ? BigInt(Math.floor(adaAmount * 1000000)) - : minLovelace; - - // Check if we're sending at least the minimum required - if (outputLovelace < minLovelace) { - throw new Error(`Minimum ${lovelaceToAda(minLovelace)} ADA required to send these tokens`); - } - - // Build transaction - // @ts-ignore: Lucid types are not fully compatible - let tx = lucid.newTx() - .payToAddress(recipientAddress, { - lovelace: outputLovelace, - ...assets - }); - - // Complete the transaction (adds inputs and change output) - // @ts-ignore: Lucid types are not fully compatible - tx = await tx.complete(); - - // Sign the transaction - // @ts-ignore: Lucid types are not fully compatible - const signedTx = await tx.sign().complete(); - - // Submit the transaction - const txHash = await signedTx.submit(); - - // Format asset name for display - const displayAssetName = assetName ? formatAssetName(assetNameHex) : ''; - - return { - txHash, - senderAddress, - recipientAddress, - token: { - policyId, - name: displayAssetName, - amount - }, - ada: lovelaceToAda(outputLovelace), - links: { - explorer: `https://cardanoscan.io/transaction/${txHash}` - } - }; - } catch (error) { - throw error; - } -} - -// Interface for Cardano amount -interface CardanoAmount { - unit: string; - quantity: string; -} - -// Interface for address info -interface AddressInfo { - address: string; - amount: string; - stake_address: string | null; - type: string; - script: boolean; -} - -export function registerCardanoTools(server: McpServer) { - // Get Address Balance - server.tool( - "getCardanoAddressBalance", - "Get balance and token holdings for a Cardano address", - { - address: z.string().describe("Cardano address to check"), - }, - async ({ address }) => { - try { - // Get address info - const addressInfo = await blockfrostRequest(`/addresses/${address}`); - - // Get address UTXOs to get token info - interface CardanoUtxo { - tx_hash: string; - tx_index: number; - output_index: number; - amount: CardanoAmount[]; - block: string; - } - - const utxos = await blockfrostRequest(`/addresses/${address}/utxos`); - - // Extract all tokens from UTXOs - const tokenMap = new Map(); - - utxos.forEach(utxo => { - utxo.amount.forEach(asset => { - const currentAmount = tokenMap.get(asset.unit) || BigInt(0); - tokenMap.set(asset.unit, currentAmount + BigInt(asset.quantity)); - }); - }); - - // Get ADA balance - const adaBalance = tokenMap.get('lovelace') || BigInt(addressInfo.amount); - - // Remove lovelace from token map for separate display - tokenMap.delete('lovelace'); - - // Convert token map to array for sorting and formatting - const tokens = Array.from(tokenMap.entries()).map(([unit, quantity]) => { - // Split into policy ID and asset name - const policyId = unit.slice(0, 56); - const assetName = unit.slice(56); - const formattedName = formatAssetName(assetName); - - return { - unit, - policyId, - assetName: formattedName, - quantity: quantity.toString() - }; - }); - - // Sort tokens by quantity (descending) - tokens.sort((a, b) => (BigInt(b.quantity) - BigInt(a.quantity)) > 0n ? 1 : -1); - - // Format token list for display - const tokenList = tokens.length > 0 - ? tokens.map(token => - `${token.quantity} ${token.assetName || token.unit} (Policy: ${token.policyId.substring(0, 8)}...)` - ).join('\n') - : 'No tokens found'; - - // Build response - return { - content: [ - { - type: "text", - text: `Cardano Address Balance for ${address}: - -ADA Balance: ${lovelaceToAda(adaBalance.toString())} ADA -Stake Address: ${addressInfo.stake_address || 'Not staked'} -Address Type: ${addressInfo.script ? 'Script' : 'Key-based'} - -${tokens.length > 0 ? `Token Holdings (${tokens.length}):\n${tokenList}` : 'No token holdings found'}`, - }, - ], - } - } catch (err) { - const error = err as Error - return { - content: [{ - type: "text", - text: `Failed to retrieve Cardano address balance: ${error.message}` - }], - } - } - } - ) - - // Get Cardano Wallet Info - server.tool( - "getCardanoWalletInfo", - "Get the current wallet information, including balance and tokens", - {}, - async () => { - try { - // Get wallet info - const walletInfo = await getWalletInfo(); - - // Format token list - const tokenList = walletInfo.tokens.length > 0 - ? walletInfo.tokens.map((token: CardanoToken) => `${token.quantity} ${token.name}`).join('\n') - : 'No tokens found'; - - return { - content: [ - { - type: "text", - text: `# Cardano Wallet Information - -Address: ${walletInfo.address} -ADA Balance: ${walletInfo.ada} ADA -UTXO Count: ${walletInfo.utxoCount} - -${walletInfo.tokens.length > 0 ? `## Token Holdings (${walletInfo.tokens.length}):\n${tokenList}` : 'No token holdings found'}` - }, - ], - }; - } catch (err) { - const error = err as Error; - return { - content: [ - { - type: "text", - text: `Failed to get wallet information: ${error.message}\n\n**Troubleshooting Tips:**\n1. Make sure you have a valid 15 or 24-word Cardano mnemonic in your .env file\n2. Verify your Blockfrost API key is correct and has sufficient access rights\n3. Check the console logs for detailed error information` - }, - ], - }; - } - } - ) - - // Send ADA Transaction - server.tool( - "sendCardanoAda", - "Send ADA from your wallet to a recipient address", - { - recipientAddress: z.string().describe("Recipient Cardano address"), - amount: z.number().min(1).describe("Amount of ADA to send"), - metadata: z.optional(z.string()).describe("Optional transaction metadata in JSON format") - }, - async ({ recipientAddress, amount, metadata }) => { - try { - // Call sendAda - const result = await sendAda(recipientAddress, amount, metadata); - - return { - content: [ - { - type: "text", - text: `# ADA Transaction Successful - -Transaction Hash: ${result.txHash} -From: ${result.senderAddress} -To: ${result.recipientAddress} -Amount: ${result.amount} ADA - -[View on Explorer](${result.links.explorer})` - }, - ], - }; - } catch (err) { - const error = err as Error; - return { - content: [ - { - type: "text", - text: `Failed to send ADA: ${error.message}\n\n**Troubleshooting Tips:**\n1. Make sure you have a valid 15 or 24-word Cardano mnemonic in your .env file\n2. Verify your Blockfrost API key is correct and has sufficient access rights\n3. Check that your wallet has sufficient balance\n4. Verify the recipient address is correct\n5. Check the console logs for detailed error information` - }, - ], - }; - } - } - ) - - // Send Tokens Transaction - server.tool( - "sendCardanoTokens", - "Send Cardano native tokens from your wallet to a recipient address", - { - recipientAddress: z.string().describe("Recipient Cardano address"), - policyId: z.string().describe("Token policy ID"), - assetName: z.string().describe("Asset name (can be empty for policy-only tokens)"), - amount: z.string().describe("Amount of tokens to send"), - adaAmount: z.optional(z.number()).describe("Optional amount of ADA to send with tokens (will use minimum required if not specified)") - }, - async ({ recipientAddress, policyId, assetName, amount, adaAmount }) => { - try { - // Call sendTokens - const result = await sendTokens(recipientAddress, policyId, assetName, amount, adaAmount); - - return { - content: [ - { - type: "text", - text: `# Token Transaction Successful - -Transaction Hash: ${result.txHash} -From: ${result.senderAddress} -To: ${result.recipientAddress} - -Token Details: -- Policy ID: ${result.token.policyId} -- Asset Name: ${result.token.name || '(none)'} -- Amount: ${result.token.amount} - -Included ADA: ${result.ada} ADA - -[View on Explorer](${result.links.explorer})` - }, - ], - }; - } catch (err) { - const error = err as Error; - return { - content: [ - { - type: "text", - text: `Failed to send tokens: ${error.message}\n\n**Troubleshooting Tips:**\n1. Make sure you have a valid 15 or 24-word Cardano mnemonic in your .env file\n2. Verify your Blockfrost API key is correct and has sufficient access rights\n3. Check that your wallet has sufficient ADA balance for the transaction\n4. Verify the policy ID and asset name are correct\n5. Verify the recipient address is correct\n6. Check the console logs for detailed error information` - }, - ], - }; - } - } - ) -} \ No newline at end of file diff --git a/src/chains/UTXO/dogecoin.ts b/src/chains/UTXO/dogecoin.ts deleted file mode 100644 index 40d51be..0000000 --- a/src/chains/UTXO/dogecoin.ts +++ /dev/null @@ -1,144 +0,0 @@ -import { Client as DogeClient, defaultDogeParams } from '@xchainjs/xchain-doge' -import { Network } from '@xchainjs/xchain-client' -import { z } from "zod" -import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js" - -// Initialize client with mainnet by default -const dogeClient = new DogeClient({ - ...defaultDogeParams, - network: Network.Mainnet, -}) - -// Helper function to format base amounts -function formatBaseAmount(baseAmount: any): string { - return baseAmount.amount().toString() -} - -export function registerDogecoinTools(server: McpServer) { - // Dogecoin Balance - server.tool( - "getDogeBalance", - "Get balance for a Dogecoin address", - { - address: z.string().describe("Dogecoin address to check"), - }, - async ({ address }) => { - try { - const balances = await dogeClient.getBalance(address) - return { - content: [ - { - type: "text", - text: `Dogecoin Balance for ${address}:\n${formatBaseAmount(balances[0].amount)} DOGE`, - }, - ], - } - } catch (err) { - const error = err as Error - return { - content: [{ type: "text", text: `Failed to retrieve DOGE balance: ${error.message}` }], - } - } - } - ) - - // Transaction History - server.tool( - "getDOGETransactionHistory", - "Get transaction history for a Dogecoin address", - { - address: z.string().describe("Dogecoin address to check"), - limit: z.number().optional().describe("Maximum number of transactions to return"), - offset: z.number().optional().describe("Number of transactions to skip"), - }, - async ({ address, limit = 10, offset = 0 }) => { - try { - const txs = await dogeClient.getTransactions({ address, limit, offset }) - const txList = txs.txs.map((tx: any) => { - const fromAmount = tx.from[0]?.amount ? `${formatBaseAmount(tx.from[0].amount)} DOGE` : 'Unknown' - const toAmount = tx.to[0]?.amount ? `${formatBaseAmount(tx.to[0].amount)} DOGE` : 'Unknown' - - return ` -Transaction: ${tx.hash} -Type: ${tx.type} -From: ${tx.from[0]?.from || 'Unknown'} (${fromAmount}) -To: ${tx.to[0]?.to || 'Unknown'} (${toAmount}) -Asset: ${tx.asset.ticker} -Date: ${new Date(tx.date).toLocaleString()}` - }).join('\n---\n') - - return { - content: [ - { - type: "text", - text: `Dogecoin Transaction History for ${address}:\n${txList}`, - }, - ], - } - } catch (err) { - const error = err as Error - return { - content: [{ type: "text", text: `Failed to retrieve DOGE transaction history: ${error.message}` }], - } - } - } - ) - - // Address Validation - server.tool( - "validateDOGEAddress", - "Validate a Dogecoin address format", - { - address: z.string().describe("Dogecoin address to validate"), - }, - async ({ address }) => { - try { - const isValid = dogeClient.validateAddress(address) - return { - content: [ - { - type: "text", - text: isValid - ? `The address ${address} is a valid Dogecoin address` - : `The address ${address} is NOT a valid Dogecoin address`, - }, - ], - } - } catch (err) { - const error = err as Error - return { - content: [{ type: "text", text: `Error validating Dogecoin address: ${error.message}` }], - } - } - } - ) - - // Network Info - server.tool( - "getDOGENetworkInfo", - "Get current Dogecoin network information", - {}, - async () => { - try { - const fees = await dogeClient.getFeeRates() - return { - content: [ - { - type: "text", - text: `Dogecoin Network Information: -Current Network: ${dogeClient.getNetwork()} -Fee Rates (sats/byte): - Fast: ${fees.fast} - Average: ${fees.average}`, - }, - ], - } - } catch (err) { - const error = err as Error - return { - content: [{ type: "text", text: `Failed to retrieve Dogecoin network information: ${error.message}` }], - } - } - } - ) -} \ No newline at end of file diff --git a/src/chains/UTXO/index.ts b/src/chains/UTXO/index.ts deleted file mode 100644 index 6882844..0000000 --- a/src/chains/UTXO/index.ts +++ /dev/null @@ -1,8 +0,0 @@ -export { registerBitcoinTools } from './bitcoin.js'; -export { registerLitecoinTools } from './litecoin.js'; -export { registerDogecoinTools } from './dogecoin.js'; -export { registerBitcoinCashTools } from './bitcoincash.js'; -export { registerCardanoTools, getWalletInfo, sendAda, sendTokens } from './cardano.js'; - -// Export Cardano types -export type { CardanoWalletInfo, CardanoToken, CardanoAdaTransactionResult, CardanoTokenTransactionResult } from './cardano.js'; \ No newline at end of file diff --git a/src/chains/UTXO/litecoin.ts b/src/chains/UTXO/litecoin.ts deleted file mode 100644 index 0d38175..0000000 --- a/src/chains/UTXO/litecoin.ts +++ /dev/null @@ -1,144 +0,0 @@ -import { Client as LitecoinClient, defaultLtcParams } from '@xchainjs/xchain-litecoin' -import { Network } from '@xchainjs/xchain-client' -import { z } from "zod" -import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js" - -// Initialize client with mainnet by default -const litecoinClient = new LitecoinClient({ - ...defaultLtcParams, - network: Network.Mainnet, -}) - -// Helper function to format base amounts -function formatBaseAmount(baseAmount: any): string { - return baseAmount.amount().toString() -} - -export function registerLitecoinTools(server: McpServer) { - // Litecoin Balance - server.tool( - "getLitecoinBalance", - "Get balance for a Litecoin address", - { - address: z.string().describe("Litecoin address to check"), - }, - async ({ address }) => { - try { - const balances = await litecoinClient.getBalance(address) - return { - content: [ - { - type: "text", - text: `Litecoin Balance for ${address}:\n${formatBaseAmount(balances[0].amount)} LTC`, - }, - ], - } - } catch (err) { - const error = err as Error - return { - content: [{ type: "text", text: `Failed to retrieve LTC balance: ${error.message}` }], - } - } - } - ) - - // Transaction History - server.tool( - "getLTCTransactionHistory", - "Get transaction history for a Litecoin address", - { - address: z.string().describe("Litecoin address to check"), - limit: z.number().optional().describe("Maximum number of transactions to return"), - offset: z.number().optional().describe("Number of transactions to skip"), - }, - async ({ address, limit = 10, offset = 0 }) => { - try { - const txs = await litecoinClient.getTransactions({ address, limit, offset }) - const txList = txs.txs.map((tx: any) => { - const fromAmount = tx.from[0]?.amount ? `${formatBaseAmount(tx.from[0].amount)} LTC` : 'Unknown' - const toAmount = tx.to[0]?.amount ? `${formatBaseAmount(tx.to[0].amount)} LTC` : 'Unknown' - - return ` -Transaction: ${tx.hash} -Type: ${tx.type} -From: ${tx.from[0]?.from || 'Unknown'} (${fromAmount}) -To: ${tx.to[0]?.to || 'Unknown'} (${toAmount}) -Asset: ${tx.asset.ticker} -Date: ${new Date(tx.date).toLocaleString()}` - }).join('\n---\n') - - return { - content: [ - { - type: "text", - text: `Litecoin Transaction History for ${address}:\n${txList}`, - }, - ], - } - } catch (err) { - const error = err as Error - return { - content: [{ type: "text", text: `Failed to retrieve LTC transaction history: ${error.message}` }], - } - } - } - ) - - // Address Validation - server.tool( - "validateLTCAddress", - "Validate a Litecoin address format", - { - address: z.string().describe("Litecoin address to validate"), - }, - async ({ address }) => { - try { - const isValid = litecoinClient.validateAddress(address) - return { - content: [ - { - type: "text", - text: isValid - ? `The address ${address} is a valid Litecoin address` - : `The address ${address} is NOT a valid Litecoin address`, - }, - ], - } - } catch (err) { - const error = err as Error - return { - content: [{ type: "text", text: `Error validating Litecoin address: ${error.message}` }], - } - } - } - ) - - // Network Info - server.tool( - "getLTCNetworkInfo", - "Get current Litecoin network information", - {}, - async () => { - try { - const fees = await litecoinClient.getFeeRates() - return { - content: [ - { - type: "text", - text: `Litecoin Network Information: -Current Network: ${litecoinClient.getNetwork()} -Fee Rates (sats/byte): - Fast: ${fees.fast} - Average: ${fees.average}`, - }, - ], - } - } catch (err) { - const error = err as Error - return { - content: [{ type: "text", text: `Failed to retrieve Litecoin network information: ${error.message}` }], - } - } - } - ) -} \ No newline at end of file diff --git a/src/chains/evm/evm.ts b/src/chains/evm/evm.ts deleted file mode 100644 index 4c7da74..0000000 --- a/src/chains/evm/evm.ts +++ /dev/null @@ -1,481 +0,0 @@ -import { JsonRpcProvider, formatEther, formatUnits, Contract, Wallet, parseUnits, MaxUint256 } from 'ethers'; -import { z } from "zod"; -import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; - -// Network configurations -interface NetworkConfig { - name: string; - rpc: string; - chainId: number; - currencySymbol: string; - explorer: string; -} - -const NETWORKS: { [key: string]: NetworkConfig } = { - ethereum: { - name: "Ethereum", - rpc: process.env.ETH_RPC_URL || "https://eth-mainnet.g.alchemy.com/v2/demo", - chainId: 1, - currencySymbol: "ETH", - explorer: "https://etherscan.io" - }, - base: { - name: "Base", - rpc: process.env.BASE_RPC_URL || "https://mainnet.base.org", - chainId: 8453, - currencySymbol: "ETH", - explorer: "https://basescan.org" - }, - arbitrum: { - name: "Arbitrum", - rpc: process.env.ARBITRUM_RPC_URL || "https://arb1.arbitrum.io/rpc", - chainId: 42161, - currencySymbol: "ETH", - explorer: "https://arbiscan.io" - }, - optimism: { - name: "Optimism", - rpc: process.env.OPTIMISM_RPC_URL || "https://mainnet.optimism.io", - chainId: 10, - currencySymbol: "ETH", - explorer: "https://optimistic.etherscan.io" - }, - bsc: { - name: "BNB Smart Chain", - rpc: process.env.BSC_RPC_URL || "https://bsc-dataseed.binance.org", - chainId: 56, - currencySymbol: "BNB", - explorer: "https://bscscan.com" - }, - polygon: { - name: "Polygon", - rpc: process.env.POLYGON_RPC_URL || "https://polygon-rpc.com", - chainId: 137, - currencySymbol: "MATIC", - explorer: "https://polygonscan.com" - }, - avalanche: { - name: "Avalanche", - rpc: process.env.AVALANCHE_RPC_URL || "https://api.avax.network/ext/bc/C/rpc", - chainId: 43114, - currencySymbol: "AVAX", - explorer: "https://snowtrace.io" - }, - berachain: { - name: "Berachain", - rpc: process.env.BERACHAIN_RPC_URL || "https://rpc.berachain.com", - chainId: 80094, - currencySymbol: "BERA", - explorer: "https://berascan.com" - }, - sonic: { - name: "Sonic", - rpc: process.env.SONIC_RPC_URL || "https://rpc.soniclabs.com/", - chainId: 2024, - currencySymbol: "SONIC", - explorer: "https://explorer.sonic.ooo" - } -}; - -// Initialize providers for each network -const providers: { [key: string]: JsonRpcProvider } = {}; -for (const [network, config] of Object.entries(NETWORKS)) { - providers[network] = new JsonRpcProvider(config.rpc); -} - -// ERC-20 minimal ABI -const ERC20_ABI = [ - "function balanceOf(address owner) view returns (uint256)", - "function decimals() view returns (uint8)", - "function symbol() view returns (string)", - "function transfer(address to, uint256 amount) returns (bool)", - "function approve(address spender, uint256 amount) returns (bool)" -]; - -export function registerEvmTools(server: McpServer) { - // Get native token balance for any EVM network - server.tool( - "getEvmBalance", - "Get native token balance for an EVM address on any supported network", - { - address: z.string().describe("EVM account address"), - network: z.string().describe("Network name (ethereum, base, arbitrum, optimism, bsc, polygon, avalanche, berachain, sonic)"), - }, - async ({ address, network }) => { - try { - if (!NETWORKS[network]) { - return { - content: [ - { - type: "text", - text: `Unsupported network: ${network}. Supported networks are: ${Object.keys(NETWORKS).join(", ")}`, - }, - ], - }; - } - - const provider = providers[network]; - const balance = await provider.getBalance(address); - const formattedBalance = formatEther(balance); - const networkConfig = NETWORKS[network]; - - return { - content: [ - { - type: "text", - text: `Balance on ${networkConfig.name}:\n${formattedBalance} ${networkConfig.currencySymbol}`, - }, - ], - }; - } catch (err) { - const error = err as Error; - return { - content: [ - { - type: "text", - text: `Failed to retrieve balance: ${error.message}`, - }, - ], - }; - } - } - ); - - // Get ERC-20 token balance for any EVM network - server.tool( - "getEvmTokenBalance", - "Get ERC-20 token balance for an address on any supported EVM network", - { - address: z.string().describe("EVM account address"), - tokenAddress: z.string().describe("ERC-20 token contract address"), - network: z.string().describe("Network name (ethereum, base, arbitrum, optimism, bsc, polygon, avalanche, berachain, sonic)"), - }, - async ({ address, tokenAddress, network }) => { - try { - if (!NETWORKS[network]) { - return { - content: [ - { - type: "text", - text: `Unsupported network: ${network}. Supported networks are: ${Object.keys(NETWORKS).join(", ")}`, - }, - ], - }; - } - - const provider = providers[network]; - const contract = new Contract(tokenAddress, ERC20_ABI, provider); - - const [balance, decimals, symbol] = await Promise.all([ - contract.balanceOf(address), - contract.decimals(), - contract.symbol() - ]); - - const formattedBalance = formatUnits(balance, decimals); - const networkConfig = NETWORKS[network]; - - return { - content: [ - { - type: "text", - text: `Token Balance on ${networkConfig.name}:\n${formattedBalance} ${symbol} (${tokenAddress})\nExplorer: ${networkConfig.explorer}/token/${tokenAddress}` - }, - ], - }; - } catch (err) { - const error = err as Error; - return { - content: [ - { - type: "text", - text: `Failed to retrieve token balance: ${error.message}`, - }, - ], - }; - } - } - ); - - // Get gas price for any EVM network - server.tool( - "getGasPrice", - "Get current gas price for any supported EVM network", - { - network: z.string().describe("Network name (ethereum, base, arbitrum, optimism, bsc, polygon, avalanche, berachain, sonic)"), - }, - async ({ network }) => { - try { - if (!NETWORKS[network]) { - return { - content: [ - { - type: "text", - text: `Unsupported network: ${network}. Supported networks are: ${Object.keys(NETWORKS).join(", ")}`, - }, - ], - }; - } - - const provider = providers[network]; - const feeData = await provider.getFeeData(); - const gasPrice = formatUnits(feeData.gasPrice || 0, 'gwei'); - const maxFeePerGas = feeData.maxFeePerGas ? formatUnits(feeData.maxFeePerGas, 'gwei') : null; - const maxPriorityFeePerGas = feeData.maxPriorityFeePerGas ? formatUnits(feeData.maxPriorityFeePerGas, 'gwei') : null; - const networkConfig = NETWORKS[network]; - - let response = `Gas Prices on ${networkConfig.name}:\nGas Price: ${gasPrice} Gwei`; - if (maxFeePerGas) { - response += `\nMax Fee: ${maxFeePerGas} Gwei`; - } - if (maxPriorityFeePerGas) { - response += `\nMax Priority Fee: ${maxPriorityFeePerGas} Gwei`; - } - - return { - content: [ - { - type: "text", - text: response, - }, - ], - }; - } catch (err) { - const error = err as Error; - return { - content: [ - { - type: "text", - text: `Failed to retrieve gas price: ${error.message}`, - }, - ], - }; - } - } - ); - - // Send native tokens on any EVM network - server.tool( - "sendEvmTransaction", - "Send native tokens on any supported EVM network (using private key from .env)", - { - toAddress: z.string().describe("Recipient's address"), - amount: z.string().describe("Amount to send (in native tokens)"), - network: z.string().describe("Network name (ethereum, base, arbitrum, optimism, bsc, polygon, avalanche, berachain, sonic)"), - }, - async ({ toAddress, amount, network }) => { - try { - if (!NETWORKS[network]) { - return { - content: [ - { - type: "text", - text: `Unsupported network: ${network}. Supported networks are: ${Object.keys(NETWORKS).join(", ")}` - } - ] - }; - } - - const provider = providers[network]; - const networkConfig = NETWORKS[network]; - - // Get private key from environment variables - if (!process.env.ETH_PRIVATE_KEY) { - throw new Error('ETH_PRIVATE_KEY not found in environment variables'); - } - - // Create wallet from private key - const wallet = new Wallet(process.env.ETH_PRIVATE_KEY, provider); - const fromAddress = wallet.address; - - // Get current gas price and nonce - const [gasPrice, nonce] = await Promise.all([ - provider.getFeeData(), - provider.getTransactionCount(fromAddress) - ]); - - // Prepare transaction - const tx = { - to: toAddress, - value: parseUnits(amount), - nonce: nonce, - maxPriorityFeePerGas: gasPrice.maxPriorityFeePerGas, - maxFeePerGas: gasPrice.maxFeePerGas, - gasLimit: 21000, // Standard ETH transfer - chainId: networkConfig.chainId - }; - - // Sign and send transaction - const txResponse = await wallet.sendTransaction(tx); - - return { - content: [ - { - type: "text", - text: `Transaction sent on ${networkConfig.name}!\nFrom: ${fromAddress}\nTo: ${toAddress}\nAmount: ${amount} ${networkConfig.currencySymbol}\nTransaction Hash: ${txResponse.hash}\nExplorer Link: ${networkConfig.explorer}/tx/${txResponse.hash}` - } - ] - }; - } catch (err) { - const error = err as Error; - return { - content: [ - { - type: "text", - text: `Failed to send transaction: ${error.message}` - } - ] - }; - } - } - ); - - // Send ERC-20 tokens on any EVM network - server.tool( - "sendEvmToken", - "Send ERC-20 tokens on any supported EVM network (using private key from .env)", - { - toAddress: z.string().describe("Recipient's address"), - tokenAddress: z.string().describe("Token contract address"), - amount: z.string().describe("Amount to send (in token units)"), - network: z.string().describe("Network name (ethereum, base, arbitrum, optimism, bsc, polygon, avalanche, berachain)"), - }, - async ({ toAddress, tokenAddress, amount, network }) => { - try { - if (!NETWORKS[network]) { - return { - content: [ - { - type: "text", - text: `Unsupported network: ${network}. Supported networks are: ${Object.keys(NETWORKS).join(", ")}` - } - ] - }; - } - - const provider = providers[network]; - const networkConfig = NETWORKS[network]; - - // Get private key from environment variables - if (!process.env.ETH_PRIVATE_KEY) { - throw new Error('ETH_PRIVATE_KEY not found in environment variables'); - } - - // Create wallet from private key - const wallet = new Wallet(process.env.ETH_PRIVATE_KEY, provider); - const fromAddress = wallet.address; - - // Create contract instance - const contract = new Contract(tokenAddress, ERC20_ABI, wallet); - - // Get token details - const [decimals, symbol] = await Promise.all([ - contract.decimals(), - contract.symbol() - ]); - - // Convert amount to token units - const amountInTokenUnits = parseUnits(amount, decimals); - - // Send transaction - const txResponse = await contract.transfer(toAddress, amountInTokenUnits, { - gasLimit: 100000 // Estimated gas limit for token transfers - }); - - return { - content: [ - { - type: "text", - text: `Token transfer sent on ${networkConfig.name}!\nFrom: ${fromAddress}\nTo: ${toAddress}\nAmount: ${amount} ${symbol}\nToken Address: ${tokenAddress}\nTransaction Hash: ${txResponse.hash}\nExplorer Link: ${networkConfig.explorer}/tx/${txResponse.hash}` - } - ] - }; - } catch (err) { - const error = err as Error; - return { - content: [ - { - type: "text", - text: `Failed to send token transfer: ${error.message}` - } - ] - }; - } - } - ); - - // Approve ERC-20 token spending - server.tool( - "approveEvmToken", - "Approve ERC-20 token spending on any supported EVM network (using private key from .env)", - { - spenderAddress: z.string().describe("Address to approve for spending"), - tokenAddress: z.string().describe("Token contract address"), - amount: z.string().optional().describe("Amount to approve (in token units, defaults to unlimited)"), - network: z.string().describe("Network name (ethereum, base, arbitrum, optimism, bsc, polygon, avalanche, berachain)"), - }, - async ({ spenderAddress, tokenAddress, amount, network }) => { - try { - if (!NETWORKS[network]) { - return { - content: [ - { - type: "text", - text: `Unsupported network: ${network}. Supported networks are: ${Object.keys(NETWORKS).join(", ")}` - } - ] - }; - } - - const provider = providers[network]; - const networkConfig = NETWORKS[network]; - - // Get private key from environment variables - if (!process.env.ETH_PRIVATE_KEY) { - throw new Error('ETH_PRIVATE_KEY not found in environment variables'); - } - - // Create wallet from private key - const wallet = new Wallet(process.env.ETH_PRIVATE_KEY, provider); - - // Create contract instance - const contract = new Contract(tokenAddress, ERC20_ABI, wallet); - - // Get token details - const [decimals, symbol] = await Promise.all([ - contract.decimals(), - contract.symbol() - ]); - - // Calculate approval amount - const approvalAmount = amount ? parseUnits(amount, decimals) : MaxUint256; - - // Send approval transaction - const txResponse = await contract.approve(spenderAddress, approvalAmount, { - gasLimit: 60000 // Estimated gas limit for approvals - }); - - const formattedAmount = amount || "unlimited"; - - return { - content: [ - { - type: "text", - text: `Token approval sent on ${networkConfig.name}!\nToken: ${symbol} (${tokenAddress})\nSpender: ${spenderAddress}\nAmount: ${formattedAmount}\nTransaction Hash: ${txResponse.hash}\nExplorer Link: ${networkConfig.explorer}/tx/${txResponse.hash}` - } - ] - }; - } catch (err) { - const error = err as Error; - return { - content: [ - { - type: "text", - text: `Failed to approve token spending: ${error.message}` - } - ] - }; - } - } - ); -} \ No newline at end of file diff --git a/src/chains/index.ts b/src/chains/index.ts deleted file mode 100644 index e2ac0d1..0000000 --- a/src/chains/index.ts +++ /dev/null @@ -1,26 +0,0 @@ -import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js" -import { registerBitcoinTools } from "./UTXO/bitcoin" -import { registerLitecoinTools } from "./UTXO/litecoin" -import { registerDogecoinTools } from "./UTXO/dogecoin" -import { registerBitcoinCashTools } from "./UTXO/bitcoincash" -import { registerCardanoTools } from "./UTXO/cardano" -import { registerThorchainTools } from "./thorchain/thorchain" -import { registerRippleTools } from "./ripple/ripple" -import { registerTonTools } from "./ton/ton" - -// Export all UTXO tools registration function -export function registerUtxoTools(server: McpServer) { - registerBitcoinTools(server) - registerLitecoinTools(server) - registerDogecoinTools(server) - registerBitcoinCashTools(server) - registerCardanoTools(server) -} - -// Export all tools registration function -export function registerAllTools(server: McpServer) { - registerUtxoTools(server) - registerThorchainTools(server) - registerRippleTools(server) - registerTonTools(server) -} \ No newline at end of file diff --git a/src/chains/ripple/index.ts b/src/chains/ripple/index.ts deleted file mode 100644 index 2b7f077..0000000 --- a/src/chains/ripple/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { registerRippleTools } from './ripple.js'; \ No newline at end of file diff --git a/src/chains/ripple/ripple.ts b/src/chains/ripple/ripple.ts deleted file mode 100644 index ea63787..0000000 --- a/src/chains/ripple/ripple.ts +++ /dev/null @@ -1,505 +0,0 @@ -import { z } from "zod"; -import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; -import { Client, Wallet, Payment, TrustSet } from 'xrpl'; - -// Initialize XRP Ledger client -let xrpClient: Client | null = null; - -// XRP Network URL -const XRP_NETWORK_URL = process.env.XRP_RPC_URL || 'wss://s1.ripple.com'; -const XRP_EXPLORER = 'https://livenet.xrpl.org'; - -// Helper function to get or initialize the client -async function getClient(): Promise { - if (!xrpClient || !xrpClient.isConnected()) { - xrpClient = new Client(XRP_NETWORK_URL); - await xrpClient.connect(); - } - return xrpClient; -} - -// Helper function to format drops to XRP -function dropsToXrp(drops: string): string { - return (parseInt(drops) / 1000000).toFixed(6); -} - -// Helper function to format XRP to drops -function xrpToDrops(xrp: string | number): string { - // Ensure xrp is a string - const xrpStr = typeof xrp === 'number' ? xrp.toString() : xrp; - // Use a string to preserve precision and avoid floating point issues - const drops = Math.floor(parseFloat(xrpStr) * 1000000).toString(); - return drops; -} - -// Helper function to clean up resources -async function cleanUp() { - if (xrpClient && xrpClient.isConnected()) { - await xrpClient.disconnect(); - } -} - -// Helper function to create a wallet from available credentials -async function createWallet() { - // Try with private key first if available - if (process.env.XRP_PRIVATE_KEY) { - try { - console.log("Creating wallet from private key..."); - return Wallet.fromSeed(process.env.XRP_PRIVATE_KEY); - } catch (err) { - console.log("Failed to create wallet from private key:", err); - } - } - - // Try with mnemonic if private key failed or isn't available - if (process.env.XRP_MNEMONIC) { - try { - console.log("Creating wallet from mnemonic..."); - return Wallet.fromMnemonic(process.env.XRP_MNEMONIC); - } catch (err) { - console.log("Failed to create wallet from mnemonic:", err); - } - } - - // If we have the address and we get here, throw an error - if (process.env.XRP_ADDRESS) { - throw new Error('Could not create wallet from available credentials'); - } - - throw new Error('No wallet credentials provided. Please add XRP_PRIVATE_KEY or XRP_MNEMONIC to your .env file'); -} - -// Register all XRP tools -export function registerRippleTools(server: McpServer) { - // Get XRP Balance - server.tool( - "getXrpBalance", - "Get balance for an XRP address", - { - address: z.string().describe("XRP address to check"), - }, - async ({ address }) => { - try { - const client = await getClient(); - const response = await client.request({ - command: 'account_info', - account: address, - ledger_index: 'validated' - }); - - const balance = dropsToXrp(response.result.account_data.Balance); - - return { - content: [ - { - type: "text", - text: `XRP Balance for ${address}:\n${balance} XRP`, - }, - ], - }; - } catch (err) { - const error = err as Error; - return { - content: [{ type: "text", text: `Failed to retrieve XRP balance: ${error.message}` }], - }; - } - } - ); - - // Get account transactions - server.tool( - "getXrpTransactionHistory", - "Get transaction history for an XRP address", - { - address: z.string().describe("XRP address to check"), - limit: z.number().optional().describe("Maximum number of transactions to return (default: 10)"), - }, - async ({ address, limit = 10 }) => { - try { - const client = await getClient(); - const response = await client.request({ - command: 'account_tx', - account: address, - limit: limit - }); - - if (!response.result.transactions || response.result.transactions.length === 0) { - return { - content: [{ type: "text", text: `No transactions found for ${address}` }], - }; - } - - const txList = response.result.transactions.map((tx: any) => { - const transaction = tx.tx; - let txInfo = ` -Transaction: ${transaction.hash} -Type: ${transaction.TransactionType} -Date: ${new Date(transaction.date ? (transaction.date + 946684800) * 1000 : 0).toLocaleString()}`; - - if (transaction.TransactionType === 'Payment') { - txInfo += ` -From: ${transaction.Account} -To: ${transaction.Destination} -Amount: ${transaction.Amount.currency ? - `${transaction.Amount.value} ${transaction.Amount.currency}` : - `${dropsToXrp(transaction.Amount)} XRP`}`; - } - - return txInfo; - }).join('\n---\n'); - - return { - content: [ - { - type: "text", - text: `XRP Transaction History for ${address}:\n${txList}`, - }, - ], - }; - } catch (err) { - const error = err as Error; - return { - content: [{ type: "text", text: `Failed to retrieve XRP transaction history: ${error.message}` }], - }; - } - } - ); - - // Validate XRP address - server.tool( - "validateXrpAddress", - "Validate an XRP address format", - { - address: z.string().describe("XRP address to validate"), - }, - async ({ address }) => { - try { - // XRP addresses start with 'r' and are 25-35 characters in length - const isValid = /^r[a-zA-Z0-9]{24,34}$/.test(address); - - return { - content: [ - { - type: "text", - text: isValid - ? `The address ${address} has a valid XRP address format` - : `The address ${address} does NOT have a valid XRP address format`, - }, - ], - }; - } catch (err) { - const error = err as Error; - return { - content: [{ type: "text", text: `Error validating XRP address: ${error.message}` }], - }; - } - } - ); - - // Get XRP Ledger Info - server.tool( - "getXrpLedgerInfo", - "Get current XRP Ledger information", - {}, - async () => { - try { - const client = await getClient(); - const serverInfo = await client.request({ - command: 'server_info' - }); - - const ledgerInfo = await client.request({ - command: 'ledger', - ledger_index: 'validated' - }); - - // Extract values safely with null checks - const serverState = serverInfo.result.info.server_state || 'Unknown'; - const ledgerIndex = ledgerInfo.result.ledger.ledger_index || 'Unknown'; - const ledgerHash = ledgerInfo.result.ledger.ledger_hash || 'Unknown'; - const closeTime = ledgerInfo.result.ledger.close_time - ? new Date((ledgerInfo.result.ledger.close_time + 946684800) * 1000).toLocaleString() - : 'Unknown'; - - // Safe access to validated_ledger properties - const baseFee = serverInfo.result.info.validated_ledger?.base_fee_xrp || 'Unknown'; - const reserveBase = serverInfo.result.info.validated_ledger?.reserve_base_xrp || 'Unknown'; - const reserveInc = serverInfo.result.info.validated_ledger?.reserve_inc_xrp || 'Unknown'; - - return { - content: [ - { - type: "text", - text: `XRP Ledger Information: -Server Status: ${serverState} -Current Ledger: ${ledgerIndex} -Ledger Hash: ${ledgerHash} -Close Time: ${closeTime} -Base Fee: ${baseFee} XRP -Reserve Base: ${reserveBase} XRP -Reserve Inc: ${reserveInc} XRP`, - }, - ], - }; - } catch (err) { - const error = err as Error; - return { - content: [{ type: "text", text: `Failed to retrieve XRP Ledger information: ${error.message}` }], - }; - } finally { - // No need to clean up here as we want to keep the connection for other operations - } - } - ); - - // Send XRP transaction using private key or mnemonic - server.tool( - "sendXrpTransaction", - "Send XRP from your wallet to another address using private key from .env", - { - toAddress: z.string().describe("XRP address to send to"), - amount: z.union([z.string(), z.number()]).describe("Amount of XRP to send (string or number)"), - memo: z.string().optional().describe("Optional memo to include with the transaction"), - }, - async ({ toAddress, amount, memo }: { toAddress: string, amount: string | number, memo?: string }) => { - try { - // Ensure amount is a string for display - const amountStr = typeof amount === 'number' ? amount.toString() : amount; - - // Get client - const client = await getClient(); - - // Create wallet - let wallet; - try { - wallet = await createWallet(); - } catch (walletError) { - console.error('Wallet creation error:', walletError); - throw new Error(`Failed to create wallet: ${(walletError as Error).message}`); - } - - // Log debug info - console.log(`Wallet created. Address: ${wallet.address}, Public Key: ${wallet.publicKey}`); - - // Verify wallet address matches expected address if provided - if (process.env.XRP_ADDRESS && wallet.address !== process.env.XRP_ADDRESS) { - console.log(`Warning: Derived wallet address ${wallet.address} does not match provided XRP_ADDRESS ${process.env.XRP_ADDRESS}`); - } - - try { - // Get account info first to confirm the account exists - console.log(`Getting account info for ${wallet.address}...`); - const accountInfo = await client.request({ - command: 'account_info', - account: wallet.address, - ledger_index: 'validated' - }); - console.log(`Account exists with sequence: ${accountInfo.result.account_data.Sequence}`); - - // Create a payment transaction - const payment: Payment = { - TransactionType: 'Payment', - Account: wallet.address, - Destination: toAddress, - // Convert amount to drops and ensure it's a string - Amount: xrpToDrops(amount) - }; - - // Add memo if provided - if (memo) { - payment.Memos = [{ - Memo: { - MemoData: Buffer.from(memo, 'utf8').toString('hex').toUpperCase() - } - }]; - } - - // Prepare transaction with autofill to get all needed fields - console.log('Preparing transaction...'); - const prepared = await client.autofill(payment); - console.log('Prepared transaction:', JSON.stringify(prepared)); - - // Use the wallet's sign method to create the signed tx_blob - console.log('Signing transaction...'); - const signed = wallet.sign(prepared); - console.log('Signed transaction. tx_blob length:', signed.tx_blob.length); - console.log('Signed transaction hash:', signed.hash); - - // First submit the transaction to get immediate feedback - console.log('Submitting transaction...'); - const submitResult = await client.submit(signed.tx_blob); - console.log('Submit response:', JSON.stringify(submitResult)); - - // If the submit was successful, wait for validation - if (submitResult.result.engine_result === 'tesSUCCESS' || submitResult.result.engine_result.startsWith('tes')) { - // Wait for transaction to be validated - console.log(`Transaction submitted successfully with hash: ${signed.hash}. Waiting for validation...`); - - // Use submitAndWait which will wait for validation - try { - const finalResult = await client.submitAndWait(signed.tx_blob); - console.log('Final transaction result:', JSON.stringify(finalResult)); - - return { - content: [ - { - type: "text", - text: `XRP Transaction Sent! -From: ${wallet.address} -To: ${toAddress} -Amount: ${amountStr} XRP -Transaction Hash: ${signed.hash} -Explorer Link: ${XRP_EXPLORER}/transactions/${signed.hash}`, - }, - ], - }; - } catch (waitError) { - // Even if we can't check the result, if submission was successful we return success - console.log('Could not verify transaction, but submission was successful:', waitError); - return { - content: [ - { - type: "text", - text: `XRP Transaction Submitted! -From: ${wallet.address} -To: ${toAddress} -Amount: ${amountStr} XRP -Transaction Hash: ${signed.hash} -Explorer Link: ${XRP_EXPLORER}/transactions/${signed.hash} - -Note: Transaction was submitted successfully but validation status is pending.`, - }, - ], - }; - } - } else { - // If submission wasn't successful, throw an error with the engine result - throw new Error(`Transaction submission failed: ${submitResult.result.engine_result} - ${submitResult.result.engine_result_message}`); - } - } catch (txError) { - console.error('Transaction error details:', txError); - throw new Error(`Transaction error: ${(txError as Error).message}`); - } - } catch (err) { - const error = err as Error; - console.error('Error in sendXrpTransaction:', error); - return { - content: [{ type: "text", text: `Failed to send XRP transaction: ${error.message}` }], - }; - } - } - ); - - // Check token balances on XRP Ledger - server.tool( - "getXrpTokenBalances", - "Get token balances for an XRP address", - { - address: z.string().describe("XRP address to check"), - }, - async ({ address }) => { - try { - const client = await getClient(); - const response = await client.request({ - command: 'account_lines', - account: address - }); - - if (!response.result.lines || response.result.lines.length === 0) { - return { - content: [ - { - type: "text", - text: `No token balances found for ${address}`, - }, - ], - }; - } - - const tokenBalances = response.result.lines.map((line: any) => { - return `${line.balance} ${line.currency} (Issuer: ${line.account})`; - }).join('\n'); - - return { - content: [ - { - type: "text", - text: `Token Balances for ${address}:\n${tokenBalances}`, - }, - ], - }; - } catch (err) { - const error = err as Error; - return { - content: [{ type: "text", text: `Failed to retrieve XRP token balances: ${error.message}` }], - }; - } - } - ); - - // Create trustline for a token on XRP Ledger - server.tool( - "createXrpTrustline", - "Create a trustline for a token on the XRP Ledger using private key from .env", - { - currency: z.string().describe("Currency code (3-letter ISO code or hex string)"), - issuer: z.string().describe("Issuer's XRP address"), - limit: z.string().describe("Maximum amount of the token to trust"), - }, - async ({ currency, issuer, limit }) => { - try { - // Get client - const client = await getClient(); - - // Create wallet - const wallet = await createWallet(); - - // Create a trustline transaction - const trustSetTx: TrustSet = { - TransactionType: 'TrustSet', - Account: wallet.address, - LimitAmount: { - currency, - issuer, - value: limit - } - }; - - try { - // Prepare and sign the transaction - const prepared = await client.autofill(trustSetTx); - const signed = wallet.sign(prepared); - - // Submit the transaction - const result = await client.submitAndWait(signed.tx_blob); - - return { - content: [ - { - type: "text", - text: `XRP Trustline Created! -Account: ${wallet.address} -Currency: ${currency} -Issuer: ${issuer} -Limit: ${limit} -Transaction Hash: ${result.result.hash} -Explorer Link: ${XRP_EXPLORER}/transactions/${result.result.hash}`, - }, - ], - }; - } catch (signError) { - throw new Error(`Failed to sign/submit transaction: ${(signError as Error).message}`); - } - } catch (err) { - const error = err as Error; - return { - content: [{ type: "text", text: `Failed to create XRP trustline: ${error.message}` }], - }; - } - } - ); - - process.on('beforeExit', async () => { - await cleanUp(); - }); -} \ No newline at end of file diff --git a/src/chains/solana/jupiter.ts b/src/chains/solana/jupiter.ts deleted file mode 100644 index e81f7fe..0000000 --- a/src/chains/solana/jupiter.ts +++ /dev/null @@ -1,277 +0,0 @@ -import { Connection, PublicKey, Transaction, VersionedTransaction } from '@solana/web3.js'; -import { Keypair } from '@solana/web3.js'; -import fetch from 'cross-fetch'; -import bs58 from 'bs58'; - -const JUPITER_API_BASE = 'https://quote-api.jup.ag/v6'; - -// Common token addresses -const COMMON_TOKENS = { - SOL: 'So11111111111111111111111111111111111111112', - USDC: 'EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v', - USDT: 'Es9vMFrzaCERmJfrF4H2FYD4KCoNkY11McCe8BenwNYB', - BONK: 'DezXAZ8z7PnrnRJjz3wXBoRgixCa6xjnB7YaB1pPB263', - ORCA: 'orcaEKTdK7LKz57vaAYr9QeNsVEPfiu6QeMU1kektZE', -} as const; - -interface TokenMetadata { - symbol: string; - decimals: number; - address: string; - name: string; - logoURI?: string; -} - -// Types for Jupiter API responses -interface JupiterQuoteResponse { - inputMint: string; - inAmount: string; - outputMint: string; - outAmount: string; - otherAmountThreshold: string; - swapMode: string; - slippageBps: number; - platformFee: { - amount?: string; - feeBps?: number; - } | null; - priceImpactPct: string; - routePlan: Array<{ - swapInfo: { - ammKey: string; - label?: string; - inputMint: string; - outputMint: string; - inAmount: string; - outAmount: string; - feeAmount: string; - feeMint: string; - }; - percent: number; - }>; - contextSlot?: number; - timeTaken?: number; -} - -interface JupiterSwapResponse { - swapTransaction: string; - lastValidBlockHeight: number; - prioritizationFeeLamports: number; - computeUnitLimit: number; -} - -// Cache for token metadata -const tokenMetadataCache = new Map(); - -// Initialize token list -async function initializeTokenList() { - if (tokenMetadataCache.size > 0) return; - - try { - const response = await fetch('https://token.jup.ag/strict'); - if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`); - const data = await response.json(); - - for (const token of data) { - tokenMetadataCache.set(token.address, { - symbol: token.symbol, - decimals: token.decimals, - address: token.address, - name: token.name, - logoURI: token.logoURI - }); - } - - // Ensure we have the common tokens - ensureCommonTokens(); - - } catch (error) { - console.error('Failed to fetch token list:', error); - // Fallback to common tokens - ensureCommonTokens(); - } -} - -// Ensure we have common tokens in cache -function ensureCommonTokens() { - // SOL - if (!tokenMetadataCache.has(COMMON_TOKENS.SOL)) { - tokenMetadataCache.set(COMMON_TOKENS.SOL, { - symbol: 'SOL', - decimals: 9, - address: COMMON_TOKENS.SOL, - name: 'Solana' - }); - } - // USDC - if (!tokenMetadataCache.has(COMMON_TOKENS.USDC)) { - tokenMetadataCache.set(COMMON_TOKENS.USDC, { - symbol: 'USDC', - decimals: 6, - address: COMMON_TOKENS.USDC, - name: 'USD Coin' - }); - } - // Add other common tokens as needed -} - -// Get token metadata -async function getTokenMetadata(mint: string): Promise { - await initializeTokenList(); - - const tokenInfo = tokenMetadataCache.get(mint); - if (!tokenInfo) { - throw new Error(`Token metadata not found for ${mint}`); - } - - return tokenInfo; -} - -// Format amount based on decimals -function formatTokenAmount(amount: string, decimals: number): string { - const value = parseInt(amount) / Math.pow(10, decimals); - return value.toFixed(decimals).replace(/\.?0+$/, ''); -} - -// Get quote from Jupiter -export async function getJupiterQuote( - inputMint: string, - outputMint: string, - amount: string, - slippageBps: number = 50 -): Promise { - // Initialize token list before getting quote - await initializeTokenList(); - - // Clean input parameters - inputMint = inputMint.toString().trim(); - outputMint = outputMint.toString().trim(); - amount = amount.toString().trim(); - - // Validate addresses - try { - new PublicKey(inputMint); - new PublicKey(outputMint); - } catch (error: any) { - throw new Error(`Invalid token address: ${error?.message || 'Invalid format'}`); - } - - const url = new URL('/quote', JUPITER_API_BASE); - url.searchParams.set('inputMint', inputMint); - url.searchParams.set('outputMint', outputMint); - url.searchParams.set('amount', amount); - url.searchParams.set('slippageBps', slippageBps.toString()); - url.searchParams.set('onlyDirectRoutes', 'false'); - url.searchParams.set('restrictIntermediateTokens', 'true'); - - console.log('Fetching quote from:', url.toString()); - - const response = await fetch(url.toString()); - if (!response.ok) { - const error = await response.text(); - throw new Error(`Failed to get Jupiter quote: ${error}`); - } - - return response.json(); -} - -// Build swap transaction -export async function buildJupiterSwapTransaction( - quoteResponse: JupiterQuoteResponse, - userPublicKey: string -): Promise { - const url = new URL('/swap', JUPITER_API_BASE); - - const body = { - quoteResponse, - userPublicKey, - dynamicComputeUnitLimit: true, - dynamicSlippage: true, - prioritizationFeeLamports: { - priorityLevelWithMaxLamports: { - maxLamports: 1000000, - priorityLevel: "veryHigh" - } - } - }; - - const response = await fetch(url.toString(), { - method: 'POST', - headers: { - 'Content-Type': 'application/json' - }, - body: JSON.stringify(body) - }); - - if (!response.ok) { - const error = await response.text(); - throw new Error(`Failed to build swap transaction: ${error}`); - } - - const { swapTransaction } = await response.json(); - return swapTransaction; -} - -// Execute swap transaction -export async function executeJupiterSwap( - connection: Connection, - swapTransaction: string, - privateKey: Uint8Array -): Promise { - try { - const keypair = Keypair.fromSecretKey(privateKey); - - // Convert base64 transaction to Uint8Array - const transactionBinary = Buffer.from(swapTransaction, 'base64'); - - // Deserialize and sign the transaction - const transaction = VersionedTransaction.deserialize(transactionBinary); - transaction.sign([keypair]); - - // Send the transaction with optimized parameters - const signature = await connection.sendRawTransaction( - transaction.serialize(), - { - skipPreflight: true, - maxRetries: 2, - preflightCommitment: 'confirmed' - } - ); - - // Wait for confirmation - await connection.confirmTransaction(signature, 'confirmed'); - - return signature; - } catch (error) { - console.error('Error executing swap:', error); - throw error; - } -} - -// Format quote details for display -export async function formatQuoteDetails(quote: JupiterQuoteResponse): Promise { - const [inputToken, outputToken] = await Promise.all([ - getTokenMetadata(quote.inputMint), - getTokenMetadata(quote.outputMint) - ]); - - const inputAmount = formatTokenAmount(quote.inAmount, inputToken.decimals); - const outputAmount = formatTokenAmount(quote.outAmount, outputToken.decimals); - - const routeSteps = quote.routePlan - .map(r => r.swapInfo.label || 'Unknown AMM') - .join(' → '); - - // Calculate rate - const inAmountNum = parseFloat(quote.inAmount) / Math.pow(10, inputToken.decimals); - const outAmountNum = parseFloat(quote.outAmount) / Math.pow(10, outputToken.decimals); - const rate = outAmountNum / inAmountNum; - - return `Swap Quote Details: -Input: ${inputAmount} ${inputToken.symbol} -Output: ${outputAmount} ${outputToken.symbol} -Rate: 1 ${inputToken.symbol} = ${rate.toFixed(6)} ${outputToken.symbol} -Price Impact: ${quote.priceImpactPct}% -Slippage Tolerance: ${quote.slippageBps / 100}% -Route: ${routeSteps}`; -} diff --git a/src/chains/solana/solana.ts b/src/chains/solana/solana.ts deleted file mode 100644 index 986a759..0000000 --- a/src/chains/solana/solana.ts +++ /dev/null @@ -1,424 +0,0 @@ -import { config } from 'dotenv'; -config(); - -import { - Connection, - PublicKey, - Keypair, - Transaction, - SystemProgram, - sendAndConfirmTransaction, - LAMPORTS_PER_SOL, -} from '@solana/web3.js'; -import { - TOKEN_PROGRAM_ID, - AccountLayout, -} from '@solana/spl-token'; -import { z } from "zod"; -import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; -import bs58 from 'bs58'; -import { getJupiterQuote, buildJupiterSwapTransaction, executeJupiterSwap, formatQuoteDetails } from './jupiter.js'; - -const SOLANA_RPC = process.env.SOLANA_RPC_URL || "https://api.mainnet-beta.solana.com"; - -// Initialize Solana connection -const solanaConnection = new Connection(SOLANA_RPC, 'confirmed'); - -// Helper function to get token accounts for a wallet -async function getTokenAccounts(walletAddress: string) { - try { - const owner = new PublicKey(walletAddress); - const tokenAccounts = await solanaConnection.getParsedTokenAccountsByOwner( - owner, - { - programId: TOKEN_PROGRAM_ID - } - ); - - return tokenAccounts.value.map(account => { - const parsedAccountInfo = account.account.data.parsed.info; - return { - mint: new PublicKey(parsedAccountInfo.mint), - amount: parsedAccountInfo.tokenAmount.uiAmount, - decimals: parsedAccountInfo.tokenAmount.decimals, - tokenAccount: account.pubkey.toString() - }; - }); - } catch (error) { - console.error('Error fetching token accounts:', error); - return []; - } -} - -interface SwapParams { - inputMint: string; - outputMint: string; - amount: string; - slippageBps?: number; -} - -export function registerSolanaTools(server: McpServer) { - // Debug: Check environment variables - console.error('Debug - ENV vars available:', Object.keys(process.env)); - console.error('Debug - SOLANA_PRIVATE_KEY exists:', !!process.env.SOLANA_PRIVATE_KEY); - if (!process.env.SOLANA_PRIVATE_KEY) { - console.error('Warning: SOLANA_PRIVATE_KEY not found in environment'); - } - - server.tool( - "getMyAddress", - "Get your Solana public address from private key in .env", - {}, - async () => { - try { - if (!process.env.SOLANA_PRIVATE_KEY) { - throw new Error('SOLANA_PRIVATE_KEY not found in environment variables'); - } - - const privateKeyBytes = bs58.decode(process.env.SOLANA_PRIVATE_KEY); - const keypair = Keypair.fromSecretKey(privateKeyBytes); - const publicKey = keypair.publicKey.toString(); - - // Get SOL balance - const balance = await solanaConnection.getBalance(keypair.publicKey); - const solBalance = balance / LAMPORTS_PER_SOL; - - // Get token balances - const tokenAccounts = await getTokenAccounts(publicKey); - const tokenBalances = tokenAccounts - .filter(account => account.amount > 0) - .map(account => `${account.amount} (Mint: ${account.mint.toString()})`) - .join('\n'); - - return { - content: [ - { - type: "text", - text: `Your Solana Address: ${publicKey}\nSOL Balance: ${solBalance} SOL\n\nToken Balances:\n${tokenBalances}`, - }, - ], - }; - } catch (err) { - const error = err as Error; - return { - content: [ - { - type: "text", - text: `Failed to get address: ${error.message}`, - }, - ], - }; - } - } - ); - - server.tool( - "getBalance", - "Get balance for a Solana address", - { - address: z.string().describe("Solana account address"), - }, - async ({ address }) => { - try { - const publicKey = new PublicKey(address); - const balance = await solanaConnection.getBalance(publicKey); - const solBalance = balance / LAMPORTS_PER_SOL; - - return { - content: [ - { - type: "text", - text: `Balance for ${address}:\n${solBalance} SOL`, - }, - ], - }; - } catch (err) { - const error = err as Error; - return { - content: [ - { - type: "text", - text: `Failed to retrieve balance for address: ${error.message}`, - }, - ], - }; - } - } - ); - - server.tool( - "getAccountInfo", - "Get detailed account information for a Solana address", - { - address: z.string().describe("Solana account address"), - encoding: z.enum(['base58', 'base64', 'jsonParsed']).optional().describe("Data encoding format"), - }, - async ({ address, encoding = 'base64' }) => { - try { - const publicKey = new PublicKey(address); - const accountInfo = await solanaConnection.getAccountInfo( - publicKey, - 'confirmed' - ); - - if (!accountInfo) { - return { - content: [ - { - type: "text", - text: `No account found for address: ${address}`, - }, - ], - }; - } - - let formattedData: string; - if (encoding === 'base58') { - formattedData = bs58.encode(accountInfo.data); - } else if (encoding === 'base64') { - formattedData = Buffer.from(accountInfo.data).toString('base64'); - } else { - formattedData = Buffer.from(accountInfo.data).toString('base64'); - } - - return { - content: [ - { - type: "text", - text: `Account Information for ${address}: -Lamports: ${accountInfo.lamports} (${accountInfo.lamports / LAMPORTS_PER_SOL} SOL) -Owner: ${accountInfo.owner.toBase58()} -Executable: ${accountInfo.executable} -Rent Epoch: ${accountInfo.rentEpoch} -Data Length: ${accountInfo.data.length} bytes -Data (${encoding}): ${formattedData}`, - }, - ], - }; - } catch (err) { - const error = err as Error; - return { - content: [ - { - type: "text", - text: `Failed to retrieve account information: ${error.message}`, - }, - ], - }; - } - } - ); - - server.tool( - "getSplTokenBalances", - "Get SPL token balances for a Solana address", - { - address: z.string().describe("Solana account address"), - }, - async ({ address }) => { - try { - const tokenAccounts = await getTokenAccounts(address); - - if (tokenAccounts.length === 0) { - return { - content: [ - { - type: "text", - text: `No token accounts found for address: ${address}`, - }, - ], - }; - } - - const balancesList = tokenAccounts - .filter(account => account.amount > 0) - .map(account => `Mint: ${account.mint.toString()}\nBalance: ${account.amount}\nDecimals: ${account.decimals}\nToken Account: ${account.tokenAccount}`) - .join('\n\n'); - - return { - content: [ - { - type: "text", - text: `Token Balances for ${address}:\n\n${balancesList}`, - }, - ], - }; - } catch (err) { - const error = err as Error; - return { - content: [ - { - type: "text", - text: `Failed to retrieve token balances: ${error.message}`, - }, - ], - }; - } - } - ); - - server.tool( - "getSwapQuote", - "Get best swap quote from Jupiter DEX aggregator", - { - inputMint: z.string().describe("Input token mint address"), - outputMint: z.string().describe("Output token mint address"), - amount: z.string().describe("Amount of input tokens (in smallest denomination)"), - slippageBps: z.number().optional().describe("Slippage tolerance in basis points (optional, default 50 = 0.5%)"), - }, - async ({ inputMint, outputMint, amount, slippageBps }: SwapParams) => { - try { - // Validate input parameters - inputMint = inputMint.trim(); - outputMint = outputMint.trim(); - amount = amount.toString().trim(); - - const quote = await getJupiterQuote(inputMint, outputMint, amount, slippageBps); - const formattedDetails = await formatQuoteDetails(quote); - - return { - content: [ - { - type: "text", - text: formattedDetails, - }, - ], - quote, // Store quote for subsequent swap - }; - } catch (err) { - console.error('Error getting swap quote:', err); - return { - content: [ - { - type: "text", - text: `Failed to get swap quote: ${err instanceof Error ? err.message : 'Unknown error'}`, - }, - ], - }; - } - } - ); - - server.tool( - "executeSwap", - "Execute a token swap using Jupiter DEX aggregator (using private key from .env)", - { - inputMint: z.string().describe("Input token mint address"), - outputMint: z.string().describe("Output token mint address"), - amount: z.string().describe("Amount of input tokens (in smallest denomination)"), - slippageBps: z.number().optional().describe("Slippage tolerance in basis points (optional, default 50 = 0.5%)"), - }, - async ({ inputMint, outputMint, amount, slippageBps }: SwapParams) => { - try { - // Validate input parameters - inputMint = inputMint.trim(); - outputMint = outputMint.trim(); - amount = amount.toString().trim(); - - // Check for private key - if (!process.env.SOLANA_PRIVATE_KEY) { - throw new Error('SOLANA_PRIVATE_KEY not found in environment variables'); - } - - // Decode private key - const privateKeyBytes = bs58.decode(process.env.SOLANA_PRIVATE_KEY); - const keypair = Keypair.fromSecretKey(privateKeyBytes); - - // Get quote - const quote = await getJupiterQuote(inputMint, outputMint, amount, slippageBps); - const formattedQuote = await formatQuoteDetails(quote); - - // Build transaction - const swapTransaction = await buildJupiterSwapTransaction( - quote, - keypair.publicKey.toString() - ); - - // Execute swap - const signature = await executeJupiterSwap( - solanaConnection, - swapTransaction, - privateKeyBytes - ); - - return { - content: [ - { - type: "text", - text: `${formattedQuote}\n\nSwap executed successfully!\nTransaction signature: ${signature}\nExplorer URL: https://explorer.solana.com/tx/${signature}`, - }, - ], - }; - } catch (err) { - console.error('Error executing swap:', err); - return { - content: [ - { - type: "text", - text: `Failed to execute swap: ${err instanceof Error ? err.message : 'Unknown error'}`, - }, - ], - }; - } - } - ); - - server.tool( - "transfer", - "Transfer SOL from your keypair (using private key from .env) to another address", - { - toAddress: z.string().describe("Destination wallet address"), - amount: z.number().positive().describe("Amount of SOL to send"), - }, - async ({ toAddress, amount }) => { - try { - if (!process.env.SOLANA_PRIVATE_KEY) { - throw new Error('SOLANA_PRIVATE_KEY not found in environment variables'); - } - - const privateKeyBytes = bs58.decode(process.env.SOLANA_PRIVATE_KEY); - const fromKeypair = Keypair.fromSecretKey(privateKeyBytes); - - const lamports = amount * LAMPORTS_PER_SOL; - - const transaction = new Transaction().add( - SystemProgram.transfer({ - fromPubkey: fromKeypair.publicKey, - toPubkey: new PublicKey(toAddress), - lamports, - }) - ); - - const signature = await sendAndConfirmTransaction( - solanaConnection, - transaction, - [fromKeypair] - ); - - return { - content: [ - { - type: "text", - text: `Transfer successful! -From: ${fromKeypair.publicKey.toBase58()} -To: ${toAddress} -Amount: ${amount} SOL -Transaction signature: ${signature} -Explorer URL: https://explorer.solana.com/tx/${signature}`, - }, - ], - }; - } catch (err) { - const error = err as Error; - return { - content: [ - { - type: "text", - text: `Failed to transfer SOL: ${error.message}`, - }, - ], - }; - } - } - ); -} \ No newline at end of file diff --git a/src/chains/thorchain/thorchain.ts b/src/chains/thorchain/thorchain.ts deleted file mode 100644 index 83b18e4..0000000 --- a/src/chains/thorchain/thorchain.ts +++ /dev/null @@ -1,173 +0,0 @@ -import { Client as ThorchainClient } from '@xchainjs/xchain-thorchain' -import { ThorchainAMM } from '@xchainjs/xchain-thorchain-amm' -import { ThorchainQuery } from '@xchainjs/xchain-thorchain-query' -import { Thornode } from '@xchainjs/xchain-thorchain-query' -import { Network } from '@xchainjs/xchain-client' -import { Asset, assetFromString, assetToString } from '@xchainjs/xchain-util' -import { BigNumber } from 'bignumber.js' -import { z } from "zod" -import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js" -import { config } from 'dotenv' -import axios from 'axios' -import { register9Rheader } from '@xchainjs/xchain-util' -import fetch from 'cross-fetch' - -config() - -// Initialize Nine Realms headers -register9Rheader(axios) - -// Initialize Thorchain clients -const thorchainClient = new ThorchainClient({ - network: Network.Mainnet, - phrase: process.env.THORCHAIN_MNEMONIC || '' -}) - -const thornode = new Thornode(Network.Mainnet) -const thorchainQuery = new ThorchainQuery() -const thorchainAmm = new ThorchainAMM(thorchainQuery) - -// Helper function to format base amounts -function formatBaseAmount(baseAmount: any): string { - return baseAmount.amount().toString() -} - -export function registerThorchainTools(server: McpServer) { - // Get THORChain balance - server.tool( - "getThorchainBalance", - "Get RUNE balance for an address", - { - address: z.string().describe("THORChain address to check"), - }, - async ({ address }) => { - try { - const balances = await thorchainClient.getBalance(address) - return { - content: [{ - type: "text", - text: `THORChain Balance for ${address}:\n${formatBaseAmount(balances[0].amount)} RUNE`, - }], - } - } catch (err) { - const error = err as Error - return { - content: [{ type: "text", text: `Failed to retrieve RUNE balance: ${error.message}` }], - } - } - } - ) - - // Get pool info - server.tool( - "getThorchainPoolInfo", - "Get information about a THORChain liquidity pool", - { - asset: z.string().describe("Asset symbol (e.g., 'BTC.BTC', 'ETH.ETH')"), - }, - async ({ asset }) => { - try { - const pool = await thornode.getPool(asset) - return { - content: [{ - type: "text", - text: `Pool Information for ${asset}:\n` + - `Status: ${pool.status}\n` + - `Asset Depth: ${pool.balance_asset}\n` + - `RUNE Depth: ${pool.balance_rune}\n` + - `LP Units: ${pool.LP_units}\n` + - `Synth Units: ${pool.synth_units}`, - }], - } - } catch (err) { - const error = err as Error - return { - content: [{ type: "text", text: `Failed to retrieve pool information: ${error.message}` }], - } - } - } - ) - - // Get swap quote - server.tool( - "getThorchainSwapQuote", - "Get a quote for swapping assets on THORChain", - { - fromAsset: z.string().describe("Source asset (e.g., 'BTC.BTC')"), - toAsset: z.string().describe("Destination asset (e.g., 'ETH.ETH')"), - amount: z.string().describe("Amount to swap"), - }, - async ({ fromAsset: fromAssetString, toAsset: toAssetString, amount: amountString }) => { - try { - // Parse assets - const fromAsset = assetFromString(fromAssetString); - const toAsset = assetFromString(toAssetString); - if (!fromAsset || !toAsset) { - return { - content: [{ type: "text", text: `Invalid asset format. Expected format: 'CHAIN.SYMBOL' (e.g., 'BTC.BTC', 'ETH.ETH')` }], - } - } - - // Parse amount - let numAmount; - try { - numAmount = new BigNumber(amountString); - if (numAmount.isNaN() || numAmount.isLessThanOrEqualTo(0)) { - throw new Error('Invalid amount'); - } - } catch (error) { - return { - content: [{ type: "text", text: `Invalid amount format. Please provide a valid positive number.` }], - } - } - - // Convert amount to base units - const amountInBaseUnits = numAmount.multipliedBy(10 ** 8).toFixed(0); - - // Format the quote request parameters - const quoteParams = { - amount: amountInBaseUnits, - from_asset: assetToString(fromAsset), - to_asset: assetToString(toAsset).replace('-B1A', ''), // Remove B1A suffix - destination: '', // Optional destination address - streaming_interval: '1', - streaming_quantity: '0' - }; - - // Get quote from THORNode directly - const response = await fetch(`https://thornode.ninerealms.com/thorchain/quote/swap?${new URLSearchParams(quoteParams)}`); - if (!response.ok) { - throw new Error(`THORNode API error: ${response.status} ${response.statusText}`); - } - const quote = await response.json(); - - // Helper function to format asset amounts with proper decimals - const formatAssetAmount = (amount: string | number, decimals: number = 8) => { - const num = Number(amount) / Math.pow(10, decimals); - return num.toLocaleString('en-US', { maximumFractionDigits: decimals }); - }; - - return { - content: [{ - type: "text", - text: `Swap Quote:\n` + - `Expected Output: ${formatAssetAmount(quote.expected_amount_out)} ${quoteParams.to_asset}\n` + - `Fees:\n` + - `- Affiliate Fee: ${formatAssetAmount(quote.fees.affiliate)} ${quote.fees.asset}\n` + - `- Outbound Fee: ${formatAssetAmount(quote.fees.outbound)} ${quote.fees.asset}\n` + - `- Liquidity Fee: ${formatAssetAmount(quote.fees.liquidity)} ${quote.fees.asset}\n` + - `- Total Fee: ${formatAssetAmount(quote.fees.total)} ${quote.fees.asset}\n` + - `Slippage: ${quote.fees.slippage_bps / 100}%\n` + - `Expires: ${new Date(quote.expiry * 1000).toLocaleString()}\n` + - `Total Swap Time: ~${quote.total_swap_seconds} seconds`, - }], - } - } catch (err) { - const error = err as Error; - return { - content: [{ type: "text", text: `Failed to get swap quote: ${error.message}` }], - } - } - } - ) -} \ No newline at end of file diff --git a/src/chains/ton/index.ts b/src/chains/ton/index.ts deleted file mode 100644 index 07bfb25..0000000 --- a/src/chains/ton/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { registerTonTools } from './ton'; diff --git a/src/chains/ton/ton.ts b/src/chains/ton/ton.ts deleted file mode 100644 index 392a825..0000000 --- a/src/chains/ton/ton.ts +++ /dev/null @@ -1,454 +0,0 @@ -import { z } from "zod"; -import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; -import { TonClient, Address, toNano, fromNano, beginCell, WalletContractV4, internal, SendMode } from '@ton/ton'; -import { mnemonicToWalletKey, KeyPair } from '@ton/crypto'; -import { Contract, Builder } from '@ton/core'; - -// Initialize TON client -let tonClient: TonClient | null = null; - -// TON Network URL - Default to mainnet -const TON_MAINNET_ENDPOINT = process.env.TON_RPC_URL || 'https://toncenter.com/api/v2/jsonRPC'; -const TON_TESTNET_ENDPOINT = 'https://testnet.toncenter.com/api/v2/jsonRPC'; -const TON_API_KEY = process.env.TON_API_KEY || ''; -const TON_EXPLORER = 'https://tonscan.org'; - -// Helper function to get or initialize the client -function getClient(testnet: boolean = false): TonClient { - const endpoint = testnet ? TON_TESTNET_ENDPOINT : TON_MAINNET_ENDPOINT; - - if (!tonClient) { - tonClient = new TonClient({ - endpoint, - apiKey: TON_API_KEY - }); - } - return tonClient; -} - -// Helper function to create a wallet from mnemonic -async function createWalletFromMnemonic(mnemonic: string): Promise { - const mnemonicArray = mnemonic.split(' '); - const keyPair = await mnemonicToWalletKey(mnemonicArray); - return keyPair; -} - -// Helper function to determine wallet version and create appropriate contract -async function createWalletContract(client: TonClient, address: Address, keyPair: KeyPair): Promise<{ contract: WalletContractV4, walletVersion: string }> { - // We'll use WalletV4 (most common and recent standard wallet) - // For production use, you might want to detect the wallet version from on-chain data - const walletContract = WalletContractV4.create({ - publicKey: keyPair.publicKey, - workchain: 0 - }); - - // Check if the derived address matches the expected address - const derivedAddress = walletContract.address; - if (!derivedAddress.equals(address)) { - console.warn('Warning: Derived wallet address does not match provided address.'); - console.warn(`Derived: ${derivedAddress.toString()}`); - console.warn(`Provided: ${address.toString()}`); - - // We'll use the provided address anyway, but this is a warning sign - } - - return { - contract: walletContract, - walletVersion: 'v4r2' - }; -} - -// Helper function to implement exponential backoff for API rate limits -async function withRetry(fn: () => Promise, maxRetries = 5): Promise { - let retries = 0; - - while (true) { - try { - return await fn(); - } catch (error: any) { - retries++; - - // If we've exceeded our max retries or it's not a rate limit error, rethrow - if (retries > maxRetries || !error.message.includes('429')) { - throw error; - } - - // Calculate exponential backoff time (1s, 2s, 4s, 8s, 16s) - const delay = Math.pow(2, retries - 1) * 1000; - console.log(`Rate limit hit, retrying in ${delay}ms (attempt ${retries} of ${maxRetries})...`); - - // Wait for the calculated time before retrying - await new Promise(resolve => setTimeout(resolve, delay)); - } - } -} - -// Register all TON tools -export function registerTonTools(server: McpServer) { - // Get TON Balance - server.tool( - "getTonBalance", - "Get balance for a TON address", - { - address: z.string().describe("TON address to check"), - testnet: z.boolean().optional().describe("Use testnet instead of mainnet"), - }, - async ({ address, testnet = false }) => { - try { - const client = getClient(testnet); - - // Validate the address - let validAddress; - try { - validAddress = Address.parse(address); - } catch (error) { - return { - content: [{ type: "text", text: `Invalid TON address format: ${address}` }], - }; - } - - // Get account information including balance with retry logic - const accountInfo = await withRetry(() => client.getBalance(validAddress)); - const balance = fromNano(accountInfo); - - return { - content: [ - { - type: "text", - text: `TON Balance for ${validAddress.toString()}:\n${balance} TON`, - }, - ], - }; - } catch (err) { - const error = err as Error; - return { - content: [{ type: "text", text: `Failed to retrieve TON balance: ${error.message}` }], - }; - } - } - ); - - // Get TON transaction history - server.tool( - "getTonTransactionHistory", - "Get transaction history for a TON address", - { - address: z.string().describe("TON address to check"), - limit: z.number().optional().describe("Maximum number of transactions to return (default: 10)"), - testnet: z.boolean().optional().describe("Use testnet instead of mainnet"), - }, - async ({ address, limit = 10, testnet = false }) => { - try { - const client = getClient(testnet); - - // Validate the address - let validAddress; - try { - validAddress = Address.parse(address); - } catch (error) { - return { - content: [{ type: "text", text: `Invalid TON address format: ${address}` }], - }; - } - - // Get transactions with retry logic - const transactions = await withRetry(() => - client.getTransactions(validAddress, { limit: limit }) - ); - - if (transactions.length === 0) { - return { - content: [{ type: "text", text: `No transactions found for ${validAddress.toString()}` }], - }; - } - - const txList = transactions.map((tx: any) => { - const timestamp = new Date(tx.time * 1000).toLocaleString(); - const inMsg = tx.inMessage; - const outMsgs = tx.outMessages; - - let txType = "Unknown"; - let fromAddress = "N/A"; - let toAddress = "N/A"; - let amount = "0"; - let comment = ""; - - // Determine if it's incoming or outgoing - if (inMsg && inMsg.source) { - txType = "Incoming"; - fromAddress = inMsg.source.toString(); - toAddress = validAddress.toString(); - amount = inMsg.value ? fromNano(inMsg.value) : "0"; - - // Try to extract comment if present - if (inMsg.body) { - try { - const msgBody = inMsg.body; - // Attempt to decode comment if it's an op=0 message - if (msgBody && msgBody.beginParse) { - const slice = msgBody.beginParse(); - if (slice.loadUint(32) === 0) { // op=0 indicates text comment - comment = slice.loadStringTail(); - } - } - } catch (e) { - // Silent fail if we can't parse the comment - } - } - } else if (outMsgs && outMsgs.length > 0) { - txType = "Outgoing"; - fromAddress = validAddress.toString(); - - // Sum total sent amount across all outgoing messages - let totalAmount = 0n; - outMsgs.forEach((msg: any) => { - if (msg.destination) { - toAddress = msg.destination.toString(); - } - if (msg.value) { - totalAmount += msg.value; - } - - // Try to extract comment from the first message if present - if (!comment && msg.body) { - try { - const msgBody = msg.body; - // Attempt to decode comment if it's an op=0 message - if (msgBody && msgBody.beginParse) { - const slice = msgBody.beginParse(); - if (slice.loadUint(32) === 0) { // op=0 indicates text comment - comment = slice.loadStringTail(); - } - } - } catch (e) { - // Silent fail if we can't parse the comment - } - } - }); - - amount = fromNano(totalAmount); - } - - return ` -Transaction: ${tx.hash} -Type: ${txType} -Date: ${timestamp} -From: ${fromAddress} -To: ${toAddress} -Amount: ${amount} TON${comment ? `\nComment: ${comment}` : ""}`; - }).join('\n---\n'); - - return { - content: [ - { - type: "text", - text: `TON Transaction History for ${validAddress.toString()}:\n${txList}`, - }, - ], - }; - } catch (err) { - const error = err as Error; - return { - content: [{ type: "text", text: `Failed to retrieve TON transaction history: ${error.message}` }], - }; - } - } - ); - - // Validate TON address - server.tool( - "validateTonAddress", - "Validate a TON address format", - { - address: z.string().describe("TON address to validate"), - }, - async ({ address }) => { - try { - let isValid = false; - let normalized = ""; - - try { - const parsedAddress = Address.parse(address); - isValid = true; - normalized = parsedAddress.toString(); - } catch (e) { - isValid = false; - } - - return { - content: [ - { - type: "text", - text: isValid - ? `The address ${address} has a valid TON address format.\nNormalized format: ${normalized}` - : `The address ${address} does NOT have a valid TON address format.`, - }, - ], - }; - } catch (err) { - const error = err as Error; - return { - content: [{ type: "text", text: `Error validating TON address: ${error.message}` }], - }; - } - } - ); - - // Get TON Network Info - server.tool( - "getTonNetworkInfo", - "Get current TON network information", - { - testnet: z.boolean().optional().describe("Use testnet instead of mainnet"), - }, - async ({ testnet = false }) => { - try { - const client = getClient(testnet); - - // Get masterchain info with retry logic - const masterchainInfo = await withRetry(() => client.getMasterchainInfo()); - - return { - content: [ - { - type: "text", - text: `TON Network Information (${testnet ? 'Testnet' : 'Mainnet'}): -Current Workchain: ${masterchainInfo.workchain} -Current Shard: ${masterchainInfo.shard} -Initial Seqno: ${masterchainInfo.initSeqno} -Latest Seqno: ${masterchainInfo.latestSeqno}`, - }, - ], - }; - } catch (err) { - const error = err as Error; - return { - content: [{ type: "text", text: `Failed to retrieve TON Network information: ${error.message}` }], - }; - } - } - ); - - // Send TON transaction using mnemonic - server.tool( - "sendTonTransaction", - "Send TON from your wallet to another address using mnemonic from .env", - { - toAddress: z.string().describe("TON address to send to"), - amount: z.union([z.string(), z.number()]).describe("Amount of TON to send"), - comment: z.string().optional().describe("Optional comment to include with the transaction"), - testnet: z.boolean().optional().describe("Use testnet instead of mainnet"), - }, - async ({ toAddress, amount, comment, testnet = false }) => { - try { - // Check if mnemonic is available - if (!process.env.TON_MNEMONIC) { - throw new Error("TON_MNEMONIC is required in the .env file"); - } - - // Check if address is available - if (!process.env.TON_ADDRESS) { - throw new Error("TON_ADDRESS is required in the .env file"); - } - - // Get client - const client = getClient(testnet); - - // Parse sender and destination address - const walletAddress = Address.parse(process.env.TON_ADDRESS); - let destinationAddress: Address; - try { - destinationAddress = Address.parse(toAddress); - } catch (error) { - throw new Error(`Invalid destination address: ${toAddress}`); - } - - // Create wallet from mnemonic - const keyPair = await createWalletFromMnemonic(process.env.TON_MNEMONIC); - - // Convert amount to nanoTON - const amountInNano = toNano(typeof amount === 'number' ? amount.toString() : amount); - - // Create wallet contract and determine wallet version - const { contract: walletContract, walletVersion } = - await createWalletContract(client, walletAddress, keyPair); - - // Connect to the wallet and ensure we're working with the right contract - const wallet = client.open(walletContract); - - // Check wallet balance with retry logic - const balance = await withRetry(() => wallet.getBalance()); - const balanceInTon = fromNano(balance); - console.log(`Wallet balance: ${balanceInTon} TON`); - - // Ensure wallet has enough funds for the transaction - const sendAmount = BigInt(amountInNano); - const gasFee = toNano('0.05'); // rough estimate for fees - if (balance < sendAmount + gasFee) { - throw new Error(`Insufficient funds: ${balanceInTon} TON available, need at least ${fromNano(sendAmount + gasFee)} TON (including fees)`); - } - - // Create transfer payload with comment if provided - const body = comment - ? beginCell().storeUint(0, 32).storeStringTail(comment).endCell() - : undefined; - - // Create internal message for the transfer - const message = internal({ - to: destinationAddress, - value: amountInNano, - body: body, - }); - - // Send the transaction with retry logic for rate limits - console.log(`Sending ${fromNano(amountInNano)} TON to ${destinationAddress.toString()}`); - - // Get seqno with retry - const seqno = await withRetry(() => wallet.getSeqno()); - - // Get current time to create transaction ID - const timestamp = Math.floor(Date.now() / 1000); - const txId = `${timestamp}-${walletAddress.toString().slice(0, 8)}-${destinationAddress.toString().slice(0, 8)}`; - - // Send transfer with retry - await withRetry(() => - wallet.sendTransfer({ - seqno: seqno, - secretKey: keyPair.secretKey, - messages: [message], - sendMode: SendMode.PAY_GAS_SEPARATELY | SendMode.IGNORE_ERRORS, - }) - ); - - // For explorers, we'll create links based on the addresses since we can't get the hash directly - const explorerLink = testnet - ? `https://testnet.tonscan.org/address/${destinationAddress.toString()}` - : `${TON_EXPLORER}/address/${destinationAddress.toString()}`; - - return { - content: [ - { - type: "text", - text: `TON Transaction Sent Successfully! -From: ${walletAddress.toString()} -To: ${destinationAddress.toString()} -Amount: ${typeof amount === 'number' ? amount.toString() : amount} TON -${comment ? `Comment: ${comment}\n` : ''} -Wallet Version: ${walletVersion} -Transaction ID: ${txId} -Explorer Link: ${explorerLink} - -Note: The transaction has been sent to the network. Check the explorer link to see when it appears (usually within seconds).`, - }, - ], - }; - } catch (err) { - const error = err as Error; - console.error('Error in sendTonTransaction:', error); - return { - content: [{ type: "text", text: `Failed to send TON transaction: ${error.message}` }], - }; - } - } - ); -} diff --git a/src/env.d.ts b/src/env.d.ts index 8da7d54..c20fc82 100644 --- a/src/env.d.ts +++ b/src/env.d.ts @@ -1,23 +1,17 @@ declare namespace NodeJS { interface ProcessEnv { - // Solana - SOLANA_RPC_URL: string; - SOLANA_PRIVATE_KEY: string; + // Vector Network Endpoints + VECTOR_OGMIOS_URL: string; + VECTOR_KOIOS_URL: string; + VECTOR_SUBMIT_URL: string; + VECTOR_EXPLORER_URL: string; - // Ethereum and other EVM chains - ETH_RPC_URL: string; - ETH_PRIVATE_KEY: string; - BASE_RPC_URL: string; - ARBITRUM_RPC_URL: string; - OPTIMISM_RPC_URL: string; - BSC_RPC_URL: string; - POLYGON_RPC_URL: string; - AVALANCHE_RPC_URL: string; + // Wallet Configuration + VECTOR_MNEMONIC: string; + VECTOR_ACCOUNT_INDEX: string; - // Ripple (XRP) - XRP_RPC_URL: string; - XRP_ADDRESS: string; - XRP_PRIVATE_KEY: string; - XRP_PUBLIC_KEY: string; + // Safety Limits + VECTOR_SPEND_LIMIT_PER_TX: string; + VECTOR_SPEND_LIMIT_DAILY: string; } -} \ No newline at end of file +} diff --git a/src/general/coingecko.ts b/src/general/coingecko.ts deleted file mode 100644 index 4be29bb..0000000 --- a/src/general/coingecko.ts +++ /dev/null @@ -1,309 +0,0 @@ -import { z } from "zod"; -import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; - -const COINGECKO_API_BASE = 'https://api.coingecko.com/api/v3'; - -// Define interfaces for type safety -interface CoinGeckoSearchCoin { - id: string; - symbol: string; - name: string; - market_cap_rank?: number; -} - -interface FormattedCoin { - id: string; - symbol: string; - name: string; - marketCapRank: number | string; -} - -interface PlatformInfo { - name: string; - address: string; - decimal_place?: number; -} - -export function registerCoinGeckoTools(server: McpServer) { - // Search tokens - server.tool( - "searchCoinGecko", - "Search for coins by ticker symbol OR name to get their CoinGecko ID. IMPORTANT: Only search with one term - either ticker (e.g., 'BTC', 'BERA') or name (e.g., 'Bitcoin', 'Berachain'), but not both. $ symbol will be automatically removed from tickers. Use this first to find the coin's CoinGecko ID before querying detailed information.", - { - query: z.string().describe("Search query (e.g., 'BTC' or 'Bitcoin', but not 'BTC Bitcoin')"), - }, - async ({ query }) => { - try { - const apiKey = process.env.COINGECKO_API_KEY; - if (!apiKey) { - throw new Error('COINGECKO_API_KEY not found in environment variables'); - } - - // Remove $ symbol and trim whitespace - const cleanQuery = query.replace('$', '').trim(); - - const url = new URL(`${COINGECKO_API_BASE}/search`); - url.searchParams.append('query', cleanQuery); - - const headers: Record = { - 'accept': 'application/json', - 'x-cg-demo-api-key': apiKey - }; - - const response = await fetch(url.toString(), { - headers, - method: 'GET' - }); - - if (!response.ok) { - const errorText = await response.text(); - throw new Error(`CoinGecko API error (${response.status}): ${errorText}`); - } - - const data = await response.json(); - const coins = (data.coins || []) as CoinGeckoSearchCoin[]; - - const formattedCoins: FormattedCoin[] = coins.map(coin => ({ - id: coin.id, - symbol: coin.symbol.toUpperCase(), - name: coin.name, - marketCapRank: coin.market_cap_rank || 'N/A' - })); - - let textResponse: string; - if (formattedCoins.length === 0) { - textResponse = `No coins found matching '${cleanQuery}'.\n\nTips:\n1. Search with just the ticker (e.g., 'BERA') or just the name (e.g., 'Berachain'), but not both\n2. Try searching with the ticker first, then try the name if no results\n3. Make sure the ticker or name is spelled correctly`; - } else { - textResponse = `Found ${formattedCoins.length} coins matching '${cleanQuery}':\n\n${ - formattedCoins.map(coin => - `${coin.name} (${coin.symbol})\n` + - `CoinGecko ID: ${coin.id}\n` + - `Market Cap Rank: ${coin.marketCapRank}\n` - ).join('\n')}`; - } - - return { - content: [ - { - type: "text", - text: textResponse - } - ], - data: formattedCoins - }; - } catch (err) { - const error = err as Error; - return { - content: [ - { - type: "text", - text: `Failed to search CoinGecko: ${error.message}` - } - ] - }; - } - } - ); - - // Get token prices - server.tool( - "getCoinGeckoPrices", - "Get current prices of tokens using their CoinGecko IDs. Must provide valid CoinGecko IDs (use searchCoinGecko to find IDs first).", - { - ids: z.array(z.string()).describe("Array of CoinGecko token IDs"), - vsCurrencies: z.array(z.string()).describe("Array of currencies to get prices in (e.g., ['usd', 'eur'])"), - includeMarketCap: z.boolean().optional().describe("Include market cap data"), - include24hrVol: z.boolean().optional().describe("Include 24h volume data"), - include24hrChange: z.boolean().optional().describe("Include 24h price change data"), - includeLastUpdatedAt: z.boolean().optional().describe("Include last updated timestamp"), - }, - async ({ ids, vsCurrencies, includeMarketCap, include24hrVol, include24hrChange, includeLastUpdatedAt }) => { - try { - const apiKey = process.env.COINGECKO_API_KEY; - if (!apiKey) { - throw new Error('COINGECKO_API_KEY not found in environment variables'); - } - - const url = new URL(`${COINGECKO_API_BASE}/simple/price`); - - const params = { - ids: ids.join(','), - vs_currencies: vsCurrencies.join(','), - include_market_cap: includeMarketCap?.toString() ?? 'false', - include_24hr_vol: include24hrVol?.toString() ?? 'false', - include_24hr_change: include24hrChange?.toString() ?? 'false', - include_last_updated_at: includeLastUpdatedAt?.toString() ?? 'false', - }; - - Object.entries(params).forEach(([key, value]) => { - url.searchParams.append(key, value); - }); - - const headers: Record = { - 'accept': 'application/json', - 'x-cg-demo-api-key': apiKey - }; - - const response = await fetch(url.toString(), { - headers, - method: 'GET' - }); - - if (!response.ok) { - const errorText = await response.text(); - throw new Error(`CoinGecko API error (${response.status}): ${errorText}`); - } - - const data = await response.json(); - - // Format the response - let textResponse = ''; - for (const [coinId, prices] of Object.entries(data)) { - textResponse += `${coinId}:\n`; - for (const [currency, value] of Object.entries(prices as Record)) { - const formattedValue = currency.includes('last_updated') ? new Date(value * 1000).toISOString() - : currency.includes('market_cap') ? `$${value.toLocaleString()}` - : currency.includes('24h_vol') ? `$${value.toLocaleString()}` - : currency.includes('24h_change') ? `${value.toFixed(2)}%` - : `${value} ${currency.toUpperCase()}`; - - const label = currency.includes('market_cap') ? 'Market Cap' - : currency.includes('24h_vol') ? '24h Volume' - : currency.includes('24h_change') ? '24h Change' - : currency.includes('last_updated') ? 'Last Updated' - : `Price (${currency.toUpperCase()})`; - - textResponse += ` ${label}: ${formattedValue}\n`; - } - textResponse += '\n'; - } - - return { - content: [ - { - type: "text", - text: textResponse || 'No price data found.' - } - ], - data - }; - } catch (err) { - const error = err as Error; - return { - content: [ - { - type: "text", - text: `Failed to get CoinGecko prices: ${error.message}` - } - ] - }; - } - } - ); - - // Get detailed coin information including contract addresses - server.tool( - "getCoinInfoByCoingeckoId", - "Look up contract addresses and chain information for a token using its CoinGecko ID. Use searchCoinGecko first to find the correct CoinGecko ID before using this tool.", - { - id: z.string().describe("CoinGecko coin ID (get this from searchCoinGecko)"), - marketData: z.boolean().optional().describe("Include market data"), - localization: z.boolean().optional().describe("Include localized data"), - tickers: z.boolean().optional().describe("Include ticker data"), - communityData: z.boolean().optional().describe("Include community data"), - developerData: z.boolean().optional().describe("Include developer data"), - sparkline: z.boolean().optional().describe("Include sparkline data"), - }, - async ({ id, localization = false, tickers = false, marketData = true, communityData = false, developerData = false, sparkline = false }) => { - try { - const apiKey = process.env.COINGECKO_API_KEY; - if (!apiKey) { - throw new Error('COINGECKO_API_KEY not found in environment variables'); - } - - const url = new URL(`${COINGECKO_API_BASE}/coins/${encodeURIComponent(id)}`); - url.searchParams.append('localization', localization.toString()); - url.searchParams.append('tickers', tickers.toString()); - url.searchParams.append('market_data', marketData.toString()); - url.searchParams.append('community_data', communityData.toString()); - url.searchParams.append('developer_data', developerData.toString()); - url.searchParams.append('sparkline', sparkline.toString()); - - const headers: Record = { - 'accept': 'application/json', - 'x-cg-demo-api-key': apiKey - }; - - const response = await fetch(url.toString(), { - headers, - method: 'GET' - }); - - if (!response.ok) { - const errorText = await response.text(); - throw new Error(`CoinGecko API error (${response.status}): ${errorText}`); - } - - const data = await response.json(); - - // Format platform and contract info first as it's the primary purpose - let contractInfo = ''; - if (data.platforms && Object.keys(data.platforms).length > 0) { - contractInfo = '\nContract Addresses:\n'; - for (const [platform, address] of Object.entries(data.platforms)) { - if (address && address !== '') { - contractInfo += ` ${platform}: ${address}\n`; - } - } - } - - // Format general info - let generalInfo = `${data.name} (${data.symbol.toUpperCase()})`; - if (data.market_cap_rank) { - generalInfo += `\nMarket Cap Rank: #${data.market_cap_rank}`; - } - - // Format market data if requested - let marketInfo = ''; - if (marketData && data.market_data) { - const md = data.market_data; - marketInfo = '\nMarket Data:\n'; - if (md.current_price?.usd) { - marketInfo += ` Current Price: $${md.current_price.usd.toLocaleString()}\n`; - } - if (md.market_cap?.usd) { - marketInfo += ` Market Cap: $${md.market_cap.usd.toLocaleString()}\n`; - } - if (md.total_volume?.usd) { - marketInfo += ` 24h Volume: $${md.total_volume.usd.toLocaleString()}\n`; - } - if (md.price_change_percentage_24h) { - marketInfo += ` 24h Change: ${md.price_change_percentage_24h.toFixed(2)}%\n`; - } - } - - const textResponse = `${generalInfo}${contractInfo}${marketInfo}\n\nLast Updated: ${data.last_updated}`; - - return { - content: [ - { - type: "text", - text: textResponse - } - ], - data // Include raw data for potential future use - }; - } catch (err) { - const error = err as Error; - return { - content: [ - { - type: "text", - text: `Failed to get coin info: ${error.message}` - } - ] - }; - } - } - ); -} \ No newline at end of file diff --git a/src/general/index.ts b/src/general/index.ts deleted file mode 100644 index daaf167..0000000 --- a/src/general/index.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; -import { registerCoinGeckoTools } from "./coingecko.js"; -import { registerRubicTools } from "./rubic.js"; - -export function registerGeneralTools(server: McpServer) { - registerCoinGeckoTools(server); - registerRubicTools(server); -} \ No newline at end of file diff --git a/src/general/rubic.ts b/src/general/rubic.ts deleted file mode 100644 index 83b79fd..0000000 --- a/src/general/rubic.ts +++ /dev/null @@ -1,478 +0,0 @@ -import { z } from "zod"; -import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; - -const RUBIC_API_BASE = 'https://api-v2.rubic.exchange/api'; - -// Define interfaces for type safety matching the actual Rubic API response -interface TokenInfo { - address: string; - blockchain: string; - blockchainId: number; - decimals: number; - name: string; - symbol: string; - price?: number; -} - -interface RubicQuoteResponse { - id: string; - estimate: { - destinationTokenAmount: string; - destinationTokenMinAmount: string; - destinationUsdAmount: number; - destinationUsdMinAmount: number; - destinationWeiAmount: string; - destinationWeiMinAmount: string; - durationInMinutes: number; - priceImpact: number; - slippage: number; - }; - fees: { - gasTokenFees: { - nativeToken: TokenInfo; - protocol: { - fixedAmount: string; - fixedUsdAmount: number; - fixedWeiAmount: string; - }; - provider: { - fixedAmount: string; - fixedUsdAmount: number; - fixedWeiAmount: string; - }; - }; - percentFees: { - percent: number; - token: TokenInfo | null; - }; - }; - providerType: string; - routing: Array<{ - path: Array; - provider: string; - type: string; - }>; - swapType: 'cross-chain' | 'on-chain'; - tokens: { - from: TokenInfo & { amount: string }; - to: TokenInfo; - }; - transaction: { - approvalAddress: string; - }; - warnings: Array; -} - -interface AvailableBlockchain { - name: string; - id: number; - testnet: boolean; - providers: { - crossChain: string[]; - onChain: string[]; - }; - proxyAvailable: boolean; - type: 'EVM' | 'TRON' | 'SOLANA' | 'Other'; -} - -interface CrossChainStatusResponse { - srcTxHash: string; - dstTxHash?: string; - status: 'pending' | 'indexing' | 'revert' | 'failed' | 'claim' | 'success' | 'error'; - message?: string; - error?: string; - bridgeName?: string; -} - -export function registerRubicTools(server: McpServer) { - // Get available blockchains - server.tool( - "getRubicSupportedChains", - "Get a list of all blockchains supported by Rubic for cross-chain bridging.", - { - includeTestnets: z.boolean().optional().describe("Include testnet blockchains in the results."), - }, - async ({ includeTestnets = false }) => { - try { - const url = new URL(`${RUBIC_API_BASE}/info/chains`); - url.searchParams.append('includeTestnets', includeTestnets.toString()); - - const response = await fetch(url.toString(), { - method: 'GET', - headers: { - 'Accept': 'application/json', - }, - }); - - if (!response.ok) { - const errorText = await response.text(); - throw new Error(`Rubic API error (${response.status}): ${errorText}`); - } - - const data = await response.json() as AvailableBlockchain[]; - - // Format the response for readable output - const formattedChains = data.map(chain => ({ - name: chain.name, - id: chain.id, - testnet: chain.testnet, - type: chain.type, - crossChainProviders: chain.providers.crossChain, - onChainProviders: chain.providers.onChain, - proxyAvailable: chain.proxyAvailable - })); - - // Create readable text response - const textResponse = `Available blockchains for cross-chain bridging:\n\n${ - formattedChains.map(chain => - `${chain.name} (ID: ${chain.id})${chain.testnet ? ' [TESTNET]' : ''}\n` + - `Type: ${chain.type}\n` + - `Cross-Chain Providers: ${chain.crossChainProviders.join(', ')}\n` + - `On-Chain Providers: ${chain.onChainProviders.join(', ')}\n` + - `Fee Collection Available: ${chain.proxyAvailable ? 'Yes' : 'No'}\n` - ).join('\n') - }`; - - return { - content: [ - { - type: "text", - text: textResponse - } - ], - data: formattedChains - }; - } catch (err) { - const error = err as Error; - return { - content: [ - { - type: "text", - text: `Failed to get supported blockchains: ${error.message}` - } - ] - }; - } - } - ); - - // Get bridge quote - server.tool( - "getRubicBridgeQuote", - "Get the best cross-chain bridge route for swapping tokens between different blockchains.", - { - srcTokenAddress: z.string().describe("Source token address. Use 0x0000000000000000000000000000000000000000 for native tokens like ETH, BNB, etc."), - srcTokenBlockchain: z.string().describe("Source blockchain name (e.g., ETH, BSC, POLYGON, etc.)"), - srcTokenAmount: z.string().describe("Amount of source token to bridge (as a string with decimals)"), - dstTokenAddress: z.string().describe("Destination token address. Use 0x0000000000000000000000000000000000000000 for native tokens."), - dstTokenBlockchain: z.string().describe("Destination blockchain name (e.g., ETH, BSC, POLYGON, etc.)"), - walletAddress: z.string().optional().describe("Wallet address to send tokens to on the destination blockchain"), - slippageTolerance: z.number().min(0.01).max(50).optional().describe("Slippage tolerance in percentage (min: 0.01, max: 50)"), - showFailedRoutes: z.boolean().optional().describe("Show failed routes in the response"), - includeTestnets: z.boolean().optional().describe("Include testnets in calculations"), - timeout: z.number().min(5).max(60).optional().describe("Calculation timeout in seconds (min: 5, max: 60)"), - }, - async ({ srcTokenAddress, srcTokenBlockchain, srcTokenAmount, dstTokenAddress, dstTokenBlockchain, walletAddress, slippageTolerance = 1, showFailedRoutes = false, includeTestnets = false, timeout = 30 }) => { - try { - const url = new URL(`${RUBIC_API_BASE}/routes/quoteBest`); - - // Ensure all values are properly formatted as strings where needed - const requestBody = { - srcTokenBlockchain: String(srcTokenBlockchain), - srcTokenAddress: String(srcTokenAddress), - srcTokenAmount: String(srcTokenAmount), - dstTokenBlockchain: String(dstTokenBlockchain), - dstTokenAddress: String(dstTokenAddress), - referrer: "web3-mcp", // Recommended to set your application name as referrer - timeout: Number(timeout), - includeTestnets: Boolean(includeTestnets), - showFailedRoutes: Boolean(showFailedRoutes), - slippageTolerance: Number(slippageTolerance) / 100, // Convert percentage to decimal - ...(walletAddress ? { walletAddress: String(walletAddress) } : {}) - }; - - const response = await fetch(url.toString(), { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - 'Accept': 'application/json', - }, - body: JSON.stringify(requestBody), - }); - - if (!response.ok) { - const errorText = await response.text(); - throw new Error(`Rubic API error (${response.status}): ${errorText}`); - } - - const data = await response.json() as RubicQuoteResponse; - - // Format response for readable output - let textResponse = `Bridge Quote Results:\n\n`; - textResponse += `From: ${data.tokens.from.amount} ${data.tokens.from.symbol} (${data.tokens.from.blockchain})\n`; - textResponse += `To: ${data.estimate.destinationTokenAmount} ${data.tokens.to.symbol} (${data.tokens.to.blockchain})\n\n`; - - if (data.tokens.from.price && data.tokens.to.price) { - textResponse += `USD Value: $${(parseFloat(data.tokens.from.amount) * data.tokens.from.price).toFixed(2)} → $${data.estimate.destinationUsdAmount.toFixed(2)}\n\n`; - } - - textResponse += `Provider: ${data.providerType.toUpperCase()}\n`; - textResponse += `Type: ${data.swapType}\n`; - - textResponse += `Estimated Duration: ${data.estimate.durationInMinutes} minutes\n`; - - if (data.fees.gasTokenFees) { - textResponse += `Gas Fee: ${data.fees.gasTokenFees.provider.fixedAmount} ${data.fees.gasTokenFees.nativeToken.symbol} (≈$${data.fees.gasTokenFees.provider.fixedUsdAmount.toFixed(2)})\n`; - } - - textResponse += `\nFees:\n`; - if (data.fees.percentFees) { - textResponse += `Percent Fee: ${data.fees.percentFees.percent}%\n`; - } - - if (data.estimate.priceImpact) { - textResponse += `\nPrice Impact: ${(data.estimate.priceImpact * 100).toFixed(2)}%\n`; - } - - if (data.warnings && data.warnings.length > 0) { - textResponse += `\nWarnings: ${data.warnings.length}\n`; - } - - // Add routing path details - if (data.routing && data.routing.length > 0) { - textResponse += `\nRouting Path:\n`; - data.routing.forEach((route, i) => { - textResponse += `Step ${i + 1}: ${route.provider} (${route.type})\n`; - if (route.path.length > 0) { - const fromToken = route.path[0]; - const toToken = route.path[route.path.length - 1]; - textResponse += ` ${fromToken.amount} ${fromToken.symbol} → ${toToken.amount} ${toToken.symbol}\n`; - } - }); - } - - return { - content: [ - { - type: "text", - text: textResponse - } - ], - data - }; - } catch (err) { - const error = err as Error; - return { - content: [ - { - type: "text", - text: `Failed to get bridge quote: ${error.message}` - } - ] - }; - } - } - ); - - // Get multiple bridge quotes - server.tool( - "getRubicBridgeQuotes", - "Get all available cross-chain bridge routes for swapping tokens between different blockchains.", - { - srcTokenAddress: z.string().describe("Source token address. Use 0x0000000000000000000000000000000000000000 for native tokens like ETH, BNB, etc."), - srcTokenBlockchain: z.string().describe("Source blockchain name (e.g., ETH, BSC, POLYGON, etc.)"), - srcTokenAmount: z.string().describe("Amount of source token to bridge (as a string with decimals)"), - dstTokenAddress: z.string().describe("Destination token address. Use 0x0000000000000000000000000000000000000000 for native tokens."), - dstTokenBlockchain: z.string().describe("Destination blockchain name (e.g., ETH, BSC, POLYGON, etc.)"), - walletAddress: z.string().optional().describe("Wallet address to send tokens to on the destination blockchain"), - slippageTolerance: z.number().min(0.01).max(50).optional().describe("Slippage tolerance in percentage (min: 0.01, max: 50)"), - showFailedRoutes: z.boolean().optional().describe("Show failed routes in the response"), - includeTestnets: z.boolean().optional().describe("Include testnets in calculations"), - timeout: z.number().min(5).max(60).optional().describe("Calculation timeout in seconds (min: 5, max: 60)"), - }, - async ({ srcTokenAddress, srcTokenBlockchain, srcTokenAmount, dstTokenAddress, dstTokenBlockchain, walletAddress, slippageTolerance = 1, showFailedRoutes = false, includeTestnets = false, timeout = 30 }) => { - try { - const url = new URL(`${RUBIC_API_BASE}/routes/quoteAll`); - - // Ensure all values are properly formatted as strings where needed - const requestBody = { - srcTokenBlockchain: String(srcTokenBlockchain), - srcTokenAddress: String(srcTokenAddress), - srcTokenAmount: String(srcTokenAmount), - dstTokenBlockchain: String(dstTokenBlockchain), - dstTokenAddress: String(dstTokenAddress), - referrer: "web3-mcp", // Recommended to set your application name as referrer - timeout: Number(timeout), - includeTestnets: Boolean(includeTestnets), - showFailedRoutes: Boolean(showFailedRoutes), - slippageTolerance: Number(slippageTolerance) / 100, // Convert percentage to decimal - ...(walletAddress ? { walletAddress: String(walletAddress) } : {}) - }; - - const response = await fetch(url.toString(), { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - 'Accept': 'application/json', - }, - body: JSON.stringify(requestBody), - }); - - if (!response.ok) { - const errorText = await response.text(); - throw new Error(`Rubic API error (${response.status}): ${errorText}`); - } - - const data = await response.json() as RubicQuoteResponse[]; - - // Format response for readable output - let textResponse = `Available Bridge Routes:\n\n`; - - if (data.length === 0) { - textResponse += `No available routes found.`; - } else { - data.forEach((route, index) => { - textResponse += `Route ${index + 1}: ${route.providerType.toUpperCase()}\n`; - textResponse += `From: ${route.tokens.from.amount} ${route.tokens.from.symbol} (${route.tokens.from.blockchain})\n`; - textResponse += `To: ${route.estimate.destinationTokenAmount} ${route.tokens.to.symbol} (${route.tokens.to.blockchain})\n`; - - if (route.tokens.from.price && route.tokens.to.price) { - textResponse += `USD Value: $${(parseFloat(route.tokens.from.amount) * route.tokens.from.price).toFixed(2)} → $${route.estimate.destinationUsdAmount.toFixed(2)}\n`; - } - - textResponse += `Estimated Time: ${route.estimate.durationInMinutes} minutes\n`; - - if (route.fees.gasTokenFees) { - textResponse += `Gas Fee: ${route.fees.gasTokenFees.provider.fixedAmount} ${route.fees.gasTokenFees.nativeToken.symbol} (≈$${route.fees.gasTokenFees.provider.fixedUsdAmount.toFixed(2)})\n`; - } - - if (route.estimate.priceImpact) { - textResponse += `Price Impact: ${(route.estimate.priceImpact * 100).toFixed(2)}%\n`; - } - - textResponse += `\n`; - }); - } - - return { - content: [ - { - type: "text", - text: textResponse - } - ], - data - }; - } catch (err) { - const error = err as Error; - return { - content: [ - { - type: "text", - text: `Failed to get bridge quotes: ${error.message}` - } - ] - }; - } - } - ); - - // Check cross-chain transaction status - server.tool( - "getRubicBridgeStatus", - "Check the status of a cross-chain bridge transaction.", - { - srcTxHash: z.string().describe("Source transaction hash to check status"), - }, - async ({ srcTxHash }) => { - try { - const url = new URL(`${RUBIC_API_BASE}/info/status`); - url.searchParams.append('srcTxHash', String(srcTxHash)); - - const response = await fetch(url.toString(), { - method: 'GET', - headers: { - 'Accept': 'application/json', - }, - }); - - if (!response.ok) { - const errorText = await response.text(); - throw new Error(`Rubic API error (${response.status}): ${errorText}`); - } - - const data = await response.json() as CrossChainStatusResponse; - - // Format response for readable output - let textResponse = `Cross-Chain Transaction Status:\n\n`; - textResponse += `Source Transaction: ${data.srcTxHash}\n`; - - if (data.dstTxHash) { - textResponse += `Destination Transaction: ${data.dstTxHash}\n`; - } - - textResponse += `Status: ${data.status.toUpperCase()}\n`; - - if (data.message) { - textResponse += `Message: ${data.message}\n`; - } - - if (data.error) { - textResponse += `Error: ${data.error}\n`; - } - - if (data.bridgeName) { - textResponse += `Bridge Provider: ${data.bridgeName}\n`; - } - - // Provide a human-readable explanation of the status - textResponse += `\nStatus Explanation:\n`; - switch (data.status) { - case 'pending': - textResponse += `Your transaction is still in progress. This could take a few minutes to complete.`; - break; - case 'indexing': - textResponse += `The transaction has been detected but is still being indexed. Please check back soon.`; - break; - case 'revert': - textResponse += `The transaction on the destination chain failed and needs to be reverted. You should collect your funds.`; - break; - case 'failed': - textResponse += `The transaction has failed. Your funds may be reverted automatically.`; - break; - case 'claim': - textResponse += `The transaction was successful! You can now claim your tokens on the destination chain.`; - break; - case 'success': - textResponse += `The transaction was completed successfully! Your tokens have been sent to the destination address.`; - break; - case 'error': - textResponse += `An error occurred during the transaction. Please check the error message for details.`; - break; - default: - textResponse += `Unknown status. Please check the Rubic interface for more information.`; - } - - return { - content: [ - { - type: "text", - text: textResponse - } - ], - data - }; - } catch (err) { - const error = err as Error; - return { - content: [ - { - type: "text", - text: `Failed to get bridge transaction status: ${error.message}` - } - ] - }; - } - } - ); -} \ No newline at end of file diff --git a/src/index.ts b/src/index.ts index 286e054..58d1cf7 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,5 +1,4 @@ import { config } from 'dotenv'; - import { fileURLToPath } from 'url'; import { dirname, resolve } from 'path'; import { existsSync } from 'fs'; @@ -13,96 +12,29 @@ const envPath = resolve(__dirname, '../.env'); console.error('Looking for .env file at:', envPath); console.error('.env file exists:', existsSync(envPath)); -// Load environment variables -const result = config({ path: envPath }); -if (result.error) { - console.error('Error loading .env file:', result.error); - process.exit(1); +// Load environment variables (non-fatal if missing — env vars can be passed directly) +if (existsSync(envPath)) { + const result = config({ path: envPath }); + if (result.error) { + console.error('Warning: Error loading .env file:', result.error); + } +} else { + console.error('No .env file found — using environment variables directly'); } import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; -import { registerSolanaTools } from "./chains/solana/solana.js"; -import { registerEvmTools } from "./chains/evm/evm.js"; -import { - registerBitcoinTools, - registerLitecoinTools, - registerDogecoinTools, - registerBitcoinCashTools, - registerCardanoTools, -} from "./chains/UTXO/index.js"; -import { registerThorchainTools } from "./chains/thorchain/thorchain.js"; -import { registerRippleTools } from "./chains/ripple/ripple.js"; -import { registerTonTools } from "./chains/ton/ton.js"; -import { registerGeneralTools } from "./general/index.js"; +import { registerVectorTools } from "./vector/index.js"; // Create server instance const server = new McpServer({ - name: "web3-rpc", + name: "vector-mcp-server", version: "1.0.0", }); -// Helper function to check if a feature is enabled -const isEnabled = (envVar: string): boolean => { - const value = process.env[envVar]?.toLowerCase(); - return value === 'true' || value === '1' || value === 'yes'; -}; - -// Register chain-specific tools based on environment variables -if (isEnabled('ENABLE_SOLANA_TOOLS')) { - console.error('Registering Solana tools...'); - registerSolanaTools(server); -} - -if (isEnabled('ENABLE_EVM_TOOLS')) { - console.error('Registering EVM chain tools...'); - registerEvmTools(server); -} - -// UTXO Chain Tools -if (isEnabled('ENABLE_BITCOIN_TOOLS')) { - console.error('Registering Bitcoin tools...'); - registerBitcoinTools(server); -} - -if (isEnabled('ENABLE_LITECOIN_TOOLS')) { - console.error('Registering Litecoin tools...'); - registerLitecoinTools(server); -} - -if (isEnabled('ENABLE_DOGECOIN_TOOLS')) { - console.error('Registering Dogecoin tools...'); - registerDogecoinTools(server); -} - -if (isEnabled('ENABLE_BITCOINCASH_TOOLS')) { - console.error('Registering Bitcoin Cash tools...'); - registerBitcoinCashTools(server); -} - -if (isEnabled('ENABLE_CARDANO_TOOLS')) { - console.error('Registering Cardano tools...'); - registerCardanoTools(server); -} - -if (isEnabled('ENABLE_THORCHAIN_TOOLS')) { - console.error('Registering THORChain tools...'); - registerThorchainTools(server); -} - -if (isEnabled('ENABLE_RIPPLE_TOOLS')) { - console.error('Registering Ripple (XRP) tools...'); - registerRippleTools(server); -} - -if (isEnabled('ENABLE_TON_TOOLS')) { - console.error('Registering TON tools...'); - registerTonTools(server); -} - -// Register general tools -console.error('Registering general tools...'); -registerGeneralTools(server); +// Register Vector tools +console.error('Registering Vector tools...'); +registerVectorTools(server); async function main() { const transport = new StdioServerTransport(); @@ -113,4 +45,4 @@ main().catch((err: unknown) => { const error = err as Error; console.error("Fatal error in main():", error.message); process.exit(1); -}); \ No newline at end of file +}); diff --git a/src/types.ts b/src/types.ts deleted file mode 100644 index 41daf5b..0000000 --- a/src/types.ts +++ /dev/null @@ -1,12 +0,0 @@ -export interface MCPRequest { - method: string; - params?: Record; -} - -export interface MCPResponse { - success: boolean; - data?: any; - error?: string; -} - -export type MCPHandler = (params: any) => Promise; \ No newline at end of file diff --git a/src/vector/index.ts b/src/vector/index.ts new file mode 100644 index 0000000..06c70ba --- /dev/null +++ b/src/vector/index.ts @@ -0,0 +1,2 @@ +export { registerVectorTools } from './vector.js'; +export type { VectorWalletInfo, VectorToken, VectorAdaTransactionResult, VectorTokenTransactionResult } from './types.js'; diff --git a/src/vector/ogmios-provider.ts b/src/vector/ogmios-provider.ts new file mode 100644 index 0000000..a3d8981 --- /dev/null +++ b/src/vector/ogmios-provider.ts @@ -0,0 +1,391 @@ +/** + * Custom Lucid Provider backed by Ogmios HTTP JSON-RPC + Vector submit-api. + * + * Implements the lucid-cardano Provider interface so we can use Lucid's + * transaction building/signing without depending on Blockfrost. + */ +import fetch from 'cross-fetch'; +import type { + Provider, + ProtocolParameters, + UTxO, + OutRef, + Credential, + Delegation, + // @ts-ignore - type aliases used as string in lucid +} from 'lucid-cardano'; + +interface OgmiosProviderConfig { + ogmiosUrl: string; + submitUrl: string; + koiosUrl?: string; +} + +export class OgmiosProvider implements Provider { + private ogmiosUrl: string; + private submitUrl: string; + private koiosUrl: string | undefined; + + constructor(config: OgmiosProviderConfig) { + // Strip trailing slashes for consistent URL building + this.ogmiosUrl = config.ogmiosUrl.replace(/\/+$/, ''); + this.submitUrl = config.submitUrl.replace(/\/+$/, ''); + this.koiosUrl = config.koiosUrl?.replace(/\/+$/, ''); + } + + /** + * Send a JSON-RPC request to Ogmios + */ + private async rpc(method: string, params?: unknown): Promise { + const response = await fetch(this.ogmiosUrl, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + jsonrpc: '2.0', + method, + params: params || {}, + id: null, + }), + }); + + if (!response.ok) { + const text = await response.text(); + throw new Error(`Ogmios RPC error (${response.status}): ${text}`); + } + + const json = await response.json(); + if (json.error) { + throw new Error(`Ogmios RPC error: ${JSON.stringify(json.error)}`); + } + + return json.result; + } + + async getProtocolParameters(): Promise { + const result = await this.rpc('queryLedgerState/protocolParameters'); + + // Map Ogmios v6 response to Lucid's ProtocolParameters format + return { + minFeeA: result.minFeeCoefficient, + minFeeB: result.minFeeConstant?.ada?.lovelace ?? result.minFeeConstant, + maxTxSize: result.maxTransactionSize?.bytes ?? result.maxTransactionSize, + maxValSize: result.maxValueSize?.bytes ?? result.maxValueSize, + keyDeposit: BigInt(result.stakeCredentialDeposit?.ada?.lovelace ?? result.stakeCredentialDeposit ?? 2000000), + poolDeposit: BigInt(result.stakePoolDeposit?.ada?.lovelace ?? result.stakePoolDeposit ?? 500000000), + priceMem: result.scriptExecutionPrices?.memory + ? this.parseFraction(result.scriptExecutionPrices.memory) + : (result.prices?.memory ?? 0.0577), + priceStep: result.scriptExecutionPrices?.cpu + ? this.parseFraction(result.scriptExecutionPrices.cpu) + : (result.prices?.steps ?? 0.0000721), + maxTxExMem: BigInt(result.maxExecutionUnitsPerTransaction?.memory ?? 14000000), + maxTxExSteps: BigInt(result.maxExecutionUnitsPerTransaction?.cpu ?? 10000000000), + coinsPerUtxoByte: BigInt(result.minUtxoDepositCoefficient ?? result.coinsPerUtxoByte ?? 4310), + collateralPercentage: result.collateralPercentage ?? 150, + maxCollateralInputs: result.maxCollateralInputs ?? 3, + costModels: this.parseCostModels(result.plutusCostModels ?? result.costModels ?? {}), + minfeeRefscriptCostPerByte: result.minFeeReferenceScripts?.base ?? 15, + } as ProtocolParameters; + } + + private parseFraction(value: string | number): number { + if (typeof value === 'number') return value; + const parts = value.split('/'); + if (parts.length === 2) { + return Number(parts[0]) / Number(parts[1]); + } + return parseFloat(value); + } + + private parseCostModels(raw: any): any { + const costModels: any = {}; + if (raw['plutus:v1']) { + costModels.PlutusV1 = raw['plutus:v1']; + } + if (raw['plutus:v2']) { + costModels.PlutusV2 = raw['plutus:v2']; + } + if (raw['plutus:v3']) { + costModels.PlutusV3 = raw['plutus:v3']; + } + // Fallback: if keys are already PlutusV1/V2 format + if (raw.PlutusV1) costModels.PlutusV1 = raw.PlutusV1; + if (raw.PlutusV2) costModels.PlutusV2 = raw.PlutusV2; + if (raw.PlutusV3) costModels.PlutusV3 = raw.PlutusV3; + return costModels; + } + + async getUtxos(addressOrCredential: string | Credential): Promise { + let address: string; + if (typeof addressOrCredential === 'string') { + address = addressOrCredential; + } else { + throw new Error('Credential-based UTxO lookup not supported via Ogmios. Use an address string.'); + } + + const result = await this.rpc('queryLedgerState/utxo', { addresses: [address] }); + return this.ogmiosUtxosToLucid(result); + } + + async getUtxosWithUnit(addressOrCredential: string | Credential, unit: string): Promise { + const utxos = await this.getUtxos(addressOrCredential); + return utxos.filter((utxo) => utxo.assets[unit]); + } + + async getUtxoByUnit(unit: string): Promise { + // This requires an indexer; try Koios if available + if (this.koiosUrl) { + const policyId = unit.slice(0, 56); + const assetName = unit.slice(56); + const response = await fetch(`${this.koiosUrl}/api/v1/asset_utxos`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ _asset_list: [[policyId, assetName]] }), + }); + if (response.ok) { + const data = await response.json(); + if (data.length > 0) { + return this.koiosUtxoToLucid(data[0]); + } + } + } + throw new Error('getUtxoByUnit requires an indexer (Koios). Not available or asset not found.'); + } + + async getUtxosByOutRef(outRefs: OutRef[]): Promise { + const outputReferences = outRefs.map((ref) => ({ + transaction: { id: ref.txHash }, + index: ref.outputIndex, + })); + + const result = await this.rpc('queryLedgerState/utxo', { outputReferences }); + return this.ogmiosUtxosToLucid(result); + } + + async getDelegation(rewardAddress: string): Promise { + // Try Koios first if available + if (this.koiosUrl) { + try { + const response = await fetch(`${this.koiosUrl}/api/v1/account_info`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ _stake_addresses: [rewardAddress] }), + }); + if (response.ok) { + const data = await response.json(); + if (data.length > 0) { + return { + poolId: data[0].delegated_pool || null, + rewards: BigInt(data[0].rewards_available || 0), + }; + } + } + } catch { + // Fall through to default + } + } + return { poolId: null, rewards: 0n }; + } + + async getDatum(datumHash: string): Promise { + // Try Koios if available + if (this.koiosUrl) { + try { + const response = await fetch(`${this.koiosUrl}/api/v1/datum_info`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ _datum_hashes: [datumHash] }), + }); + if (response.ok) { + const data = await response.json(); + if (data.length > 0 && data[0].bytes) { + return data[0].bytes; + } + } + } catch { + // Fall through + } + } + throw new Error(`Datum not found for hash: ${datumHash}. Datum lookups require Koios indexer.`); + } + + async awaitTx(txHash: string, checkInterval: number = 3000): Promise { + // Poll for tx confirmation + const maxAttempts = 60; // ~3 minutes at 3s intervals + for (let i = 0; i < maxAttempts; i++) { + await new Promise((resolve) => setTimeout(resolve, checkInterval)); + + // Try Koios tx_status if available + if (this.koiosUrl) { + try { + const response = await fetch(`${this.koiosUrl}/api/v1/tx_status`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ _tx_hashes: [txHash] }), + }); + if (response.ok) { + const data = await response.json(); + if (data.length > 0 && data[0].num_confirmations > 0) { + return true; + } + } + } catch { + // Continue polling + } + } else { + // Without Koios, try querying the UTxO by outRef + try { + const utxos = await this.getUtxosByOutRef([{ txHash, outputIndex: 0 }]); + if (utxos.length > 0) { + return true; + } + } catch { + // Not yet confirmed, continue + } + } + } + return false; + } + + async submitTx(tx: string): Promise { + // tx is a hex-encoded CBOR transaction + // submit-api expects raw CBOR bytes with Content-Type: application/cbor + const bytes = Buffer.from(tx, 'hex'); + + const response = await fetch(this.submitUrl, { + method: 'POST', + headers: { 'Content-Type': 'application/cbor' }, + body: bytes, + }); + + if (!response.ok) { + const errorText = await response.text(); + throw new Error(`Transaction submission failed (${response.status}): ${errorText}`); + } + + const result = await response.text(); + // submit-api returns the tx hash as a JSON string + try { + const parsed = JSON.parse(result); + return typeof parsed === 'string' ? parsed : parsed.txId || parsed.txHash || tx.slice(0, 64); + } catch { + // If response is plain text tx hash + return result.replace(/"/g, '').trim(); + } + } + + /** + * Convert Ogmios UTxO response format to Lucid UTxO format + */ + private ogmiosUtxosToLucid(ogmiosUtxos: any[]): UTxO[] { + if (!Array.isArray(ogmiosUtxos)) return []; + + return ogmiosUtxos.map((utxo) => { + const txHash = utxo.transaction?.id ?? utxo.txId; + const outputIndex = utxo.index ?? utxo.outputIndex; + const output = utxo.output ?? utxo; + + // Parse assets + const assets: Record = {}; + + // Handle different Ogmios response formats for value + const value = output.value ?? output.amount; + + if (typeof value === 'object' && value !== null) { + // Ogmios v6 format: { ada: { lovelace: N }, policyId: { assetName: N } } + if (value.ada?.lovelace !== undefined) { + assets['lovelace'] = BigInt(value.ada.lovelace); + } else if (value.coins !== undefined) { + // Older format + assets['lovelace'] = BigInt(value.coins); + } else if (typeof value === 'bigint' || typeof value === 'number') { + assets['lovelace'] = BigInt(value); + } + + // Parse native assets + for (const [key, val] of Object.entries(value)) { + if (key === 'ada' || key === 'coins' || key === 'lovelace') continue; + if (typeof val === 'object' && val !== null) { + // key is policy ID, val is { assetName: quantity } + for (const [assetName, quantity] of Object.entries(val as Record)) { + const unit = `${key}${assetName === '' ? '' : assetName}`; + assets[unit] = BigInt(quantity); + } + } + } + } else if (typeof value === 'number' || typeof value === 'string') { + assets['lovelace'] = BigInt(value); + } + + // Handle address + const address = output.address ?? ''; + + // Handle datum + let datumHash: string | null = null; + let datum: string | null = null; + + if (output.datumHash) { + datumHash = output.datumHash; + } + if (output.datum) { + if (typeof output.datum === 'string') { + datum = output.datum; + } else if (output.datum.hash) { + datumHash = output.datum.hash; + } else if (output.datum.value || output.datum.bytes) { + datum = output.datum.value || output.datum.bytes; + } + } + + // Handle script reference + let scriptRef: any = null; + if (output.script) { + const script = output.script; + if (script['plutus:v2']) { + scriptRef = { type: 'PlutusV2', script: script['plutus:v2'] }; + } else if (script['plutus:v1']) { + scriptRef = { type: 'PlutusV1', script: script['plutus:v1'] }; + } else if (script['plutus:v3']) { + scriptRef = { type: 'PlutusV3', script: script['plutus:v3'] }; + } else if (script.native) { + scriptRef = { type: 'Native', script: JSON.stringify(script.native) }; + } + } + + return { + txHash, + outputIndex, + assets, + address, + datumHash, + datum, + scriptRef, + } as UTxO; + }); + } + + /** + * Convert a Koios UTxO response to Lucid format + */ + private koiosUtxoToLucid(koiosUtxo: any): UTxO { + const assets: Record = {}; + assets['lovelace'] = BigInt(koiosUtxo.value || 0); + + if (koiosUtxo.asset_list) { + for (const asset of koiosUtxo.asset_list) { + const unit = `${asset.policy_id}${asset.asset_name || ''}`; + assets[unit] = BigInt(asset.quantity || 0); + } + } + + return { + txHash: koiosUtxo.tx_hash, + outputIndex: koiosUtxo.tx_index, + assets, + address: koiosUtxo.address, + datumHash: koiosUtxo.datum_hash || null, + datum: koiosUtxo.inline_datum?.bytes || null, + scriptRef: null, + } as UTxO; + } +} diff --git a/src/vector/safety.ts b/src/vector/safety.ts new file mode 100644 index 0000000..84194fb --- /dev/null +++ b/src/vector/safety.ts @@ -0,0 +1,85 @@ +import type { SpendLimits, SpendStatus, AuditEntry } from './types.js'; + +const VECTOR_SPEND_LIMIT_PER_TX = parseInt(process.env.VECTOR_SPEND_LIMIT_PER_TX || '100000000'); // 100 ADA +const VECTOR_SPEND_LIMIT_DAILY = parseInt(process.env.VECTOR_SPEND_LIMIT_DAILY || '500000000'); // 500 ADA + +export class SafetyLayer { + private dailySpent: number = 0; + private dailyResetTime: number; + private auditLog: AuditEntry[] = []; + private limits: SpendLimits; + + constructor() { + this.limits = { + perTransaction: VECTOR_SPEND_LIMIT_PER_TX, + daily: VECTOR_SPEND_LIMIT_DAILY, + }; + // Reset daily limit at midnight UTC + this.dailyResetTime = this.getNextMidnightUTC(); + } + + private getNextMidnightUTC(): number { + const now = new Date(); + const tomorrow = new Date(Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate() + 1)); + return tomorrow.getTime(); + } + + private checkAndResetDaily(): void { + if (Date.now() >= this.dailyResetTime) { + this.dailySpent = 0; + this.dailyResetTime = this.getNextMidnightUTC(); + } + } + + checkTransaction(amountLovelace: number): { allowed: boolean; reason?: string } { + this.checkAndResetDaily(); + + if (amountLovelace > this.limits.perTransaction) { + return { + allowed: false, + reason: `Transaction amount ${(amountLovelace / 1_000_000).toFixed(6)} ADA exceeds per-transaction limit of ${(this.limits.perTransaction / 1_000_000).toFixed(6)} ADA`, + }; + } + + if (this.dailySpent + amountLovelace > this.limits.daily) { + const remaining = this.limits.daily - this.dailySpent; + return { + allowed: false, + reason: `Transaction would exceed daily spend limit. Daily limit: ${(this.limits.daily / 1_000_000).toFixed(6)} ADA, already spent: ${(this.dailySpent / 1_000_000).toFixed(6)} ADA, remaining: ${(remaining / 1_000_000).toFixed(6)} ADA`, + }; + } + + return { allowed: true }; + } + + recordTransaction(txHash: string, amountLovelace: number, recipient: string): void { + this.checkAndResetDaily(); + this.dailySpent += amountLovelace; + + this.auditLog.push({ + timestamp: new Date().toISOString(), + txHash, + amountLovelace, + recipient, + action: 'send', + }); + } + + getSpendStatus(): SpendStatus { + this.checkAndResetDaily(); + return { + perTransactionLimit: this.limits.perTransaction, + dailyLimit: this.limits.daily, + dailySpent: this.dailySpent, + dailyRemaining: Math.max(0, this.limits.daily - this.dailySpent), + resetTime: new Date(this.dailyResetTime).toISOString(), + }; + } + + getAuditLog(): AuditEntry[] { + return [...this.auditLog]; + } +} + +// Singleton instance +export const safetyLayer = new SafetyLayer(); diff --git a/src/vector/types.ts b/src/vector/types.ts new file mode 100644 index 0000000..2e5182d --- /dev/null +++ b/src/vector/types.ts @@ -0,0 +1,60 @@ +// Vector-specific type definitions + +export interface VectorToken { + unit: string; + name: string; + quantity: string; +} + +export interface VectorWalletInfo { + address: string; + utxoCount: number; + ada: string; + tokens: VectorToken[]; +} + +export interface VectorAdaTransactionResult { + txHash: string; + senderAddress: string; + recipientAddress: string; + amount: number; + links: { + explorer: string; + }; +} + +export interface VectorTokenTransactionResult { + txHash: string; + senderAddress: string; + recipientAddress: string; + token: { + policyId: string; + name: string; + amount: string; + }; + ada: string; + links: { + explorer: string; + }; +} + +export interface SpendLimits { + perTransaction: number; // lovelace + daily: number; // lovelace +} + +export interface SpendStatus { + perTransactionLimit: number; + dailyLimit: number; + dailySpent: number; + dailyRemaining: number; + resetTime: string; +} + +export interface AuditEntry { + timestamp: string; + txHash: string; + amountLovelace: number; + recipient: string; + action: string; +} diff --git a/src/vector/vector.ts b/src/vector/vector.ts new file mode 100644 index 0000000..3230822 --- /dev/null +++ b/src/vector/vector.ts @@ -0,0 +1,602 @@ +import { z } from "zod"; +import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; +import { Lucid, fromText } from 'lucid-cardano'; +import * as dotenv from 'dotenv'; +import { resolve } from 'path'; +import { fileURLToPath } from 'url'; +import { existsSync } from 'fs'; +import { OgmiosProvider } from './ogmios-provider.js'; +import { safetyLayer } from './safety.js'; +import type { + VectorToken, + VectorWalletInfo, + VectorAdaTransactionResult, + VectorTokenTransactionResult, +} from './types.js'; + +// Direct .env loading +const __filename = fileURLToPath(import.meta.url); +const projectRoot = resolve(__filename, '../../../..'); +const envPath = resolve(projectRoot, '.env'); + +if (existsSync(envPath)) { + const result = dotenv.config({ path: envPath }); + if (result.error) { + console.error('Error loading .env file:', result.error); + } +} + +// Configuration from environment variables +const VECTOR_OGMIOS_URL = process.env.VECTOR_OGMIOS_URL || 'https://ogmios.vector.testnet.apexfusion.org'; +const VECTOR_SUBMIT_URL = process.env.VECTOR_SUBMIT_URL || 'https://submit.vector.testnet.apexfusion.org/api/submit/tx'; +const VECTOR_KOIOS_URL = process.env.VECTOR_KOIOS_URL || 'https://koios.vector.testnet.apexfusion.org/'; +const VECTOR_EXPLORER_URL = process.env.VECTOR_EXPLORER_URL || 'https://vector.testnet.apexscan.org'; +const VECTOR_MNEMONIC = process.env.VECTOR_MNEMONIC || ''; +const VECTOR_ACCOUNT_INDEX = parseInt(process.env.VECTOR_ACCOUNT_INDEX || '0'); + +// Helper function to format ADA amounts +function lovelaceToAda(lovelace: string | number | bigint): string { + return (Number(BigInt(String(lovelace))) / 1_000_000).toFixed(6); +} + +// Helper function to format asset name +function formatAssetName(name: string): string { + try { + if (/^[0-9a-fA-F]+$/.test(name) && name.length > 0) { + return Buffer.from(name, 'hex').toString('utf8'); + } + return name; + } catch { + return name; + } +} + +// Explorer link helper +function explorerTxLink(txHash: string): string { + return `${VECTOR_EXPLORER_URL}/transaction/${txHash}`; +} + +// Initialize Lucid instance with Ogmios provider +async function initLucid() { + const provider = new OgmiosProvider({ + ogmiosUrl: VECTOR_OGMIOS_URL, + submitUrl: VECTOR_SUBMIT_URL, + koiosUrl: VECTOR_KOIOS_URL, + }); + + // Vector uses --mainnet flag, so addresses are addr1... format + const lucid = await Lucid.new(provider, 'Mainnet'); + + if (!VECTOR_MNEMONIC) { + throw new Error('VECTOR_MNEMONIC is required in .env file'); + } + + const trimmedMnemonic = VECTOR_MNEMONIC.trim(); + const words = trimmedMnemonic.split(/\s+/); + + if (words.length !== 15 && words.length !== 24) { + throw new Error(`Invalid mnemonic: Expected 15 or 24 words, got ${words.length}`); + } + + lucid.selectWalletFromSeed(trimmedMnemonic, { accountIndex: VECTOR_ACCOUNT_INDEX }); + + const address = await lucid.wallet.address(); + if (!address) { + throw new Error('Failed to derive address from mnemonic'); + } + + return lucid; +} + +// Get wallet info +export async function getWalletInfo(): Promise { + const lucid = await initLucid(); + const address = await lucid.wallet.address(); + const utxos = await lucid.wallet.getUtxos(); + + let adaBalance = '0'; + let tokenBalances: VectorToken[] = []; + + if (utxos.length > 0) { + // Aggregate all UTxO assets + const aggregated: Record = {}; + for (const utxo of utxos) { + for (const [unit, qty] of Object.entries(utxo.assets)) { + aggregated[unit] = (aggregated[unit] || 0n) + BigInt(qty); + } + } + + adaBalance = aggregated['lovelace'] ? lovelaceToAda(aggregated['lovelace']) : '0'; + + for (const [unit, quantity] of Object.entries(aggregated)) { + if (unit === 'lovelace') continue; + + try { + const policyId = unit.slice(0, 56); + const assetNameHex = unit.slice(56); + const displayName = assetNameHex + ? formatAssetName(assetNameHex) + : `${policyId.substring(0, 8)}...`; + + tokenBalances.push({ + unit, + name: displayName, + quantity: quantity.toString(), + }); + } catch { + tokenBalances.push({ + unit, + name: unit, + quantity: quantity.toString(), + }); + } + } + } + + return { + address, + utxoCount: utxos.length, + ada: adaBalance, + tokens: tokenBalances, + }; +} + +// Send ADA transaction +export async function sendAda( + recipientAddress: string, + amountAda: number, + metadata: any = null +): Promise { + if (!recipientAddress) { + throw new Error('Recipient address is required'); + } + + if (typeof amountAda !== 'number' || amountAda <= 0) { + throw new Error('Amount must be a positive number'); + } + + const lovelaceAmount = Math.floor(amountAda * 1_000_000); + + // Safety check + const safetyCheck = safetyLayer.checkTransaction(lovelaceAmount); + if (!safetyCheck.allowed) { + throw new Error(`Safety limit exceeded: ${safetyCheck.reason}`); + } + + const lucid = await initLucid(); + const senderAddress = await lucid.wallet.address(); + + // Validate recipient address + try { + // @ts-ignore + lucid.utils.getAddressDetails(recipientAddress); + } catch (error) { + throw new Error(`Invalid recipient address: ${error instanceof Error ? error.message : String(error)}`); + } + + // Build transaction + // @ts-ignore + let tx = lucid.newTx() + .payToAddress(recipientAddress, { lovelace: BigInt(lovelaceAmount) }); + + if (metadata) { + const parsedMetadata = typeof metadata === 'string' ? JSON.parse(metadata) : metadata; + // @ts-ignore + tx = tx.attachMetadata(674, parsedMetadata); + } + + // @ts-ignore + tx = await tx.complete(); + // @ts-ignore + const signedTx = await tx.sign().complete(); + const txHash = await signedTx.submit(); + + // Record in safety layer + safetyLayer.recordTransaction(txHash, lovelaceAmount, recipientAddress); + + return { + txHash, + senderAddress, + recipientAddress, + amount: amountAda, + links: { + explorer: explorerTxLink(txHash), + }, + }; +} + +// Send tokens transaction +export async function sendTokens( + recipientAddress: string, + policyId: string, + assetName: string, + amount: string, + adaAmount: number | null = null +): Promise { + if (!recipientAddress) throw new Error('Recipient address is required'); + if (!policyId) throw new Error('Policy ID is required'); + if (!amount || isNaN(Number(amount)) || Number(amount) <= 0) { + throw new Error('Amount must be a positive number'); + } + + // Safety check on ADA portion + const adaLovelace = adaAmount ? Math.floor(adaAmount * 1_000_000) : 2_000_000; // min ~2 ADA + const safetyCheck = safetyLayer.checkTransaction(adaLovelace); + if (!safetyCheck.allowed) { + throw new Error(`Safety limit exceeded: ${safetyCheck.reason}`); + } + + const lucid = await initLucid(); + const senderAddress = await lucid.wallet.address(); + + try { + // @ts-ignore + lucid.utils.getAddressDetails(recipientAddress); + } catch (error) { + throw new Error(`Invalid recipient address: ${error instanceof Error ? error.message : String(error)}`); + } + + let assetNameHex = assetName; + if (assetName && !/^[0-9a-fA-F]+$/.test(assetName)) { + assetNameHex = fromText(assetName); + } + + const unit = `${policyId}${assetNameHex}`; + const assets = { [unit]: BigInt(amount) }; + + const outputLovelace = adaAmount + ? BigInt(Math.floor(adaAmount * 1_000_000)) + : BigInt(2_000_000); // Default min ADA + + // @ts-ignore + let tx = lucid.newTx() + .payToAddress(recipientAddress, { + lovelace: outputLovelace, + ...assets, + }); + + // @ts-ignore + tx = await tx.complete(); + // @ts-ignore + const signedTx = await tx.sign().complete(); + const txHash = await signedTx.submit(); + + safetyLayer.recordTransaction(txHash, Number(outputLovelace), recipientAddress); + + const displayAssetName = assetName ? formatAssetName(assetNameHex) : ''; + + return { + txHash, + senderAddress, + recipientAddress, + token: { + policyId, + name: displayAssetName, + amount, + }, + ada: lovelaceToAda(outputLovelace), + links: { + explorer: explorerTxLink(txHash), + }, + }; +} + +// Register all Vector MCP tools +export function registerVectorTools(server: McpServer) { + + // vector_get_balance — Get balance for any address + // @ts-ignore: MCP SDK deep type instantiation + server.tool( + "vector_get_balance", + "Get ADA and token balances for a Vector address", + { + address: z.string().describe("Vector address to check (addr1...)"), + }, + async ({ address }) => { + try { + const provider = new OgmiosProvider({ + ogmiosUrl: VECTOR_OGMIOS_URL, + submitUrl: VECTOR_SUBMIT_URL, + koiosUrl: VECTOR_KOIOS_URL, + }); + + const utxos = await provider.getUtxos(address); + + // Aggregate all assets + const tokenMap = new Map(); + for (const utxo of utxos) { + for (const [unit, qty] of Object.entries(utxo.assets)) { + const current = tokenMap.get(unit) || 0n; + tokenMap.set(unit, current + BigInt(qty)); + } + } + + const adaBalance = tokenMap.get('lovelace') || 0n; + tokenMap.delete('lovelace'); + + const tokens = Array.from(tokenMap.entries()).map(([unit, quantity]) => { + const policyId = unit.slice(0, 56); + const assetNameHex = unit.slice(56); + return { + unit, + policyId, + assetName: formatAssetName(assetNameHex), + quantity: quantity.toString(), + }; + }); + + tokens.sort((a, b) => (BigInt(b.quantity) - BigInt(a.quantity)) > 0n ? 1 : -1); + + const tokenList = tokens.length > 0 + ? tokens.map(t => `${t.quantity} ${t.assetName || t.unit} (Policy: ${t.policyId.substring(0, 8)}...)`).join('\n') + : 'No tokens found'; + + return { + content: [{ + type: "text", + text: `Vector Address Balance for ${address}: + +ADA Balance: ${lovelaceToAda(adaBalance)} ADA +UTxO Count: ${utxos.length} + +${tokens.length > 0 ? `Token Holdings (${tokens.length}):\n${tokenList}` : 'No token holdings found'}`, + }], + }; + } catch (err) { + const error = err as Error; + return { + content: [{ + type: "text", + text: `Failed to retrieve Vector address balance: ${error.message}`, + }], + }; + } + } + ); + + // vector_get_address — Get the agent's wallet address and balance + // @ts-ignore: MCP SDK deep type instantiation + server.tool( + "vector_get_address", + "Get the agent's Vector wallet address, balance, and token holdings", + {}, + async () => { + try { + const walletInfo = await getWalletInfo(); + + const tokenList = walletInfo.tokens.length > 0 + ? walletInfo.tokens.map((t: VectorToken) => `${t.quantity} ${t.name}`).join('\n') + : 'No tokens found'; + + return { + content: [{ + type: "text", + text: `# Vector Wallet Information + +Address: ${walletInfo.address} +ADA Balance: ${walletInfo.ada} ADA +UTXO Count: ${walletInfo.utxoCount} + +${walletInfo.tokens.length > 0 ? `## Token Holdings (${walletInfo.tokens.length}):\n${tokenList}` : 'No token holdings found'}`, + }], + }; + } catch (err) { + const error = err as Error; + return { + content: [{ + type: "text", + text: `Failed to get wallet information: ${error.message} + +**Troubleshooting Tips:** +1. Make sure you have a valid 15 or 24-word mnemonic in VECTOR_MNEMONIC +2. Verify the Ogmios endpoint is reachable: ${VECTOR_OGMIOS_URL} +3. Check the console logs for detailed error information`, + }], + }; + } + } + ); + + // vector_get_utxos — List UTxOs for an address or the wallet + // @ts-ignore: MCP SDK deep type instantiation + server.tool( + "vector_get_utxos", + "List unspent transaction outputs (UTxOs) for a Vector address or the agent's wallet", + { + address: z.string().optional().describe("Vector address to query UTxOs for. If omitted, uses the agent's wallet."), + }, + async ({ address }) => { + try { + let utxos; + let queryAddress: string; + + if (address) { + const provider = new OgmiosProvider({ + ogmiosUrl: VECTOR_OGMIOS_URL, + submitUrl: VECTOR_SUBMIT_URL, + koiosUrl: VECTOR_KOIOS_URL, + }); + utxos = await provider.getUtxos(address); + queryAddress = address; + } else { + const lucid = await initLucid(); + queryAddress = await lucid.wallet.address(); + utxos = await lucid.wallet.getUtxos(); + } + + if (utxos.length === 0) { + return { + content: [{ + type: "text", + text: `No UTxOs found for ${queryAddress}`, + }], + }; + } + + const utxoList = utxos.map((utxo, i) => { + const ada = utxo.assets['lovelace'] ? lovelaceToAda(utxo.assets['lovelace']) : '0'; + const tokenCount = Object.keys(utxo.assets).filter(k => k !== 'lovelace').length; + return `${i + 1}. ${utxo.txHash}#${utxo.outputIndex} — ${ada} ADA${tokenCount > 0 ? ` + ${tokenCount} token(s)` : ''}`; + }).join('\n'); + + return { + content: [{ + type: "text", + text: `# UTxOs for ${queryAddress} + +Total: ${utxos.length} UTxO(s) + +${utxoList}`, + }], + }; + } catch (err) { + const error = err as Error; + return { + content: [{ + type: "text", + text: `Failed to get UTxOs: ${error.message}`, + }], + }; + } + } + ); + + // vector_send_ada — Send ADA with safety limits + // @ts-ignore: MCP SDK deep type instantiation + server.tool( + "vector_send_ada", + "Send ADA from the agent's wallet to a recipient address (respects spend limits)", + { + recipientAddress: z.string().describe("Recipient Vector address (addr1...)"), + amount: z.number().min(1).describe("Amount of ADA to send"), + metadata: z.string().optional().describe("Optional transaction metadata in JSON format"), + }, + async ({ recipientAddress, amount, metadata }) => { + try { + const result = await sendAda(recipientAddress, amount, metadata); + + return { + content: [{ + type: "text", + text: `# ADA Transaction Successful + +Transaction Hash: ${result.txHash} +From: ${result.senderAddress} +To: ${result.recipientAddress} +Amount: ${result.amount} ADA + +[View on Explorer](${result.links.explorer})`, + }], + }; + } catch (err) { + const error = err as Error; + return { + content: [{ + type: "text", + text: `Failed to send ADA: ${error.message} + +**Troubleshooting Tips:** +1. Check that your wallet has sufficient balance +2. Verify the recipient address is correct (addr1...) +3. Check spend limits with vector_get_spend_limits +4. Verify the Ogmios endpoint is reachable`, + }], + }; + } + } + ); + + // vector_send_tokens — Send native tokens with safety limits + // @ts-ignore: MCP SDK deep type instantiation + server.tool( + "vector_send_tokens", + "Send Vector native tokens from the agent's wallet to a recipient address", + { + recipientAddress: z.string().describe("Recipient Vector address (addr1...)"), + policyId: z.string().describe("Token policy ID"), + assetName: z.string().describe("Asset name (can be empty for policy-only tokens)"), + amount: z.string().describe("Amount of tokens to send"), + adaAmount: z.number().optional().describe("Optional ADA to include (uses minimum required if not specified)"), + }, + async ({ recipientAddress, policyId, assetName, amount, adaAmount }) => { + try { + const result = await sendTokens(recipientAddress, policyId, assetName, amount, adaAmount); + + return { + content: [{ + type: "text", + text: `# Token Transaction Successful + +Transaction Hash: ${result.txHash} +From: ${result.senderAddress} +To: ${result.recipientAddress} + +Token Details: +- Policy ID: ${result.token.policyId} +- Asset Name: ${result.token.name || '(none)'} +- Amount: ${result.token.amount} + +Included ADA: ${result.ada} ADA + +[View on Explorer](${result.links.explorer})`, + }], + }; + } catch (err) { + const error = err as Error; + return { + content: [{ + type: "text", + text: `Failed to send tokens: ${error.message} + +**Troubleshooting Tips:** +1. Check that your wallet has sufficient ADA and token balance +2. Verify the policy ID and asset name are correct +3. Verify the recipient address is correct (addr1...) +4. Check spend limits with vector_get_spend_limits`, + }], + }; + } + } + ); + + // vector_get_spend_limits — Check safety layer status + // @ts-ignore: MCP SDK deep type instantiation + server.tool( + "vector_get_spend_limits", + "Check current spend limits and remaining daily budget", + {}, + async () => { + try { + const status = safetyLayer.getSpendStatus(); + const log = safetyLayer.getAuditLog(); + + const recentTxs = log.slice(-5).reverse().map((entry) => + `- ${entry.timestamp}: ${lovelaceToAda(entry.amountLovelace)} ADA → ${entry.recipient.substring(0, 20)}... (${entry.txHash.substring(0, 16)}...)` + ).join('\n'); + + return { + content: [{ + type: "text", + text: `# Vector Spend Limits + +Per-Transaction Limit: ${lovelaceToAda(status.perTransactionLimit)} ADA +Daily Limit: ${lovelaceToAda(status.dailyLimit)} ADA +Daily Spent: ${lovelaceToAda(status.dailySpent)} ADA +Daily Remaining: ${lovelaceToAda(status.dailyRemaining)} ADA +Resets At: ${status.resetTime} + +${log.length > 0 ? `## Recent Transactions (last ${Math.min(5, log.length)}):\n${recentTxs}` : 'No transactions recorded yet.'}`, + }], + }; + } catch (err) { + const error = err as Error; + return { + content: [{ + type: "text", + text: `Failed to get spend limits: ${error.message}`, + }], + }; + } + } + ); +}