cardano support
This commit is contained in:
@@ -21,6 +21,12 @@ 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
|
||||
|
||||
# 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
|
||||
@@ -34,6 +40,7 @@ ENABLE_DOGECOIN_TOOLS=true
|
||||
ENABLE_BITCOINCASH_TOOLS=true
|
||||
ENABLE_THORCHAIN_TOOLS=true
|
||||
ENABLE_RIPPLE_TOOLS=true
|
||||
ENABLE_CARDANO_TOOLS=true # Enable/disable Cardano tools
|
||||
|
||||
# CoinGecko API Key
|
||||
COINGECKO_API_KEY=CG-Z9ajuvwG1cheNJSByUgmvTgg #replace with your-coingecko-api-key if you hit rate limits
|
||||
34
README.md
34
README.md
@@ -1,6 +1,6 @@
|
||||
# Web3 MCP
|
||||
|
||||
A Model-Context-Protocol server for interacting with multiple blockchains including Solana, Ethereum, THORChain, XRP Ledger, and UTXO chains. This server provides simple RPC endpoints for common blockchain operations, allowing secure interactions with various blockchains through environment variables.
|
||||
A Model-Context-Protocol server for interacting with multiple blockchains including Solana, Ethereum, THORChain, XRP Ledger, Cardano, and UTXO chains. This server provides simple RPC endpoints for common blockchain operations, allowing secure interactions with various blockchains through environment variables.
|
||||
|
||||
<a href="https://glama.ai/mcp/servers/an8x6gmzdn"><img width="380" height="200" src="https://glama.ai/mcp/servers/an8x6gmzdn/badge" alt="Web3 Server MCP server" /></a>
|
||||
|
||||
@@ -21,6 +21,15 @@ Ethereum & EVM Chain Operations:
|
||||
- Send ERC-20 tokens (using private key from .env)
|
||||
- Approve ERC-20 token spending (using private key from .env)
|
||||
|
||||
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
|
||||
|
||||
THORChain Operations:
|
||||
- Check RUNE balances
|
||||
- Get detailed pool information
|
||||
@@ -90,6 +99,7 @@ cp .env.example .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
|
||||
@@ -109,6 +119,12 @@ XRP_PRIVATE_KEY=your-xrp-private-key-in-hex
|
||||
XRP_MNEMONIC=your-xrp-mnemonic-recovery-phrase
|
||||
# Optional - used to verify the derived address
|
||||
XRP_ADDRESS=your-xrp-account-address
|
||||
|
||||
# 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
|
||||
@@ -173,6 +189,7 @@ The Web3 MCP server allows you to control which blockchain tools are registered
|
||||
- `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
|
||||
|
||||
Set these variables to `true` or `false` in your `.env` file to control which tools are available to the server. This allows you to:
|
||||
|
||||
@@ -199,6 +216,12 @@ Ask Claude (or your MCP client of choice):
|
||||
- "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"
|
||||
@@ -249,6 +272,15 @@ You can configure custom RPC endpoints in your .env file for better reliability
|
||||
### 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.
|
||||
|
||||
|
||||
213
docs/CARDANO.md
Normal file
213
docs/CARDANO.md
Normal file
@@ -0,0 +1,213 @@
|
||||
# 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/)
|
||||
@@ -20,11 +20,12 @@
|
||||
"rpc",
|
||||
"blockchain",
|
||||
"xrp",
|
||||
"ripple"
|
||||
"ripple",
|
||||
"cardano"
|
||||
],
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"description": "Blockchain RPC Model-Context-Protocol Server for Solana, EVM chains, UTXO chains, Ripple (XRP) and more",
|
||||
"description": "Blockchain RPC Model-Context-Protocol Server for Solana, EVM chains, UTXO chains, Ripple (XRP), Cardano and more",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/akc2267/web3-mcp.git"
|
||||
@@ -66,12 +67,14 @@
|
||||
"@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",
|
||||
|
||||
657
src/chains/UTXO/cardano.ts
Normal file
657
src/chains/UTXO/cardano.ts
Normal file
@@ -0,0 +1,657 @@
|
||||
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<T>(endpoint: string): Promise<T> {
|
||||
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<CardanoWalletInfo> {
|
||||
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<CardanoAdaTransactionResult> {
|
||||
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<CardanoTokenTransactionResult> {
|
||||
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<AddressInfo>(`/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<CardanoUtxo[]>(`/addresses/${address}/utxos`);
|
||||
|
||||
// Extract all tokens from UTXOs
|
||||
const tokenMap = new Map<string, bigint>();
|
||||
|
||||
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`
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
@@ -2,3 +2,7 @@ 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';
|
||||
@@ -3,6 +3,7 @@ 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"
|
||||
|
||||
@@ -12,6 +13,7 @@ export function registerUtxoTools(server: McpServer) {
|
||||
registerLitecoinTools(server)
|
||||
registerDogecoinTools(server)
|
||||
registerBitcoinCashTools(server)
|
||||
registerCardanoTools(server)
|
||||
}
|
||||
|
||||
// Export all tools registration function
|
||||
|
||||
@@ -29,6 +29,7 @@ import {
|
||||
registerLitecoinTools,
|
||||
registerDogecoinTools,
|
||||
registerBitcoinCashTools,
|
||||
registerCardanoTools,
|
||||
} from "./chains/UTXO/index.js";
|
||||
import { registerThorchainTools } from "./chains/thorchain/thorchain.js";
|
||||
import { registerRippleTools } from "./chains/ripple/ripple.js";
|
||||
@@ -78,6 +79,11 @@ if (isEnabled('ENABLE_BITCOINCASH_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);
|
||||
|
||||
@@ -8,7 +8,10 @@
|
||||
"strict": true,
|
||||
"skipLibCheck": true,
|
||||
"outDir": "build",
|
||||
"rootDir": "src"
|
||||
"rootDir": "src",
|
||||
"allowJs": true,
|
||||
"checkJs": false,
|
||||
"resolveJsonModule": true
|
||||
},
|
||||
"include": ["src/**/*"],
|
||||
"exclude": ["node_modules", "build"]
|
||||
|
||||
Reference in New Issue
Block a user