linea-mcp
Version:
A Model Context Protocol server for interacting with the Linea blockchain
508 lines (507 loc) • 21.9 kB
JavaScript
import { ethers } from 'ethers';
import axios from 'axios';
import BlockchainService from '../../services/blockchain.js';
import KeyManagementService from '../../services/keyManagement.js';
import config from '../../config/index.js';
// ERC721 ABI (minimal for NFT operations)
const ERC721_ABI = [
// Read-only functions
'function balanceOf(address owner) view returns (uint256)',
'function ownerOf(uint256 tokenId) view returns (address)',
'function tokenURI(uint256 tokenId) view returns (string)',
'function name() view returns (string)',
'function symbol() view returns (string)',
// Authenticated functions
'function transferFrom(address from, address to, uint256 tokenId)',
'function safeTransferFrom(address from, address to, uint256 tokenId)',
'function approve(address to, uint256 tokenId)',
// Events
'event Transfer(address indexed from, address indexed to, uint256 indexed tokenId)'
];
// ERC1155 ABI (minimal for NFT operations)
const ERC1155_ABI = [
// Read-only functions
'function balanceOf(address account, uint256 id) view returns (uint256)',
'function balanceOfBatch(address[] accounts, uint256[] ids) view returns (uint256[])',
'function uri(uint256 id) view returns (string)',
'function name() view returns (string)',
'function symbol() view returns (string)',
// Authenticated functions
'function safeTransferFrom(address from, address to, uint256 id, uint256 amount, bytes data)',
'function safeBatchTransferFrom(address from, address to, uint256[] ids, uint256[] amounts, bytes data)',
'function setApprovalForAll(address operator, bool approved)',
'function isApprovedForAll(address account, address operator) view returns (bool)',
// Events
'event TransferSingle(address indexed operator, address indexed from, address indexed to, uint256 id, uint256 value)',
'event TransferBatch(address indexed operator, address indexed from, address indexed to, uint256[] ids, uint256[] values)'
];
/**
* Detect NFT contract standard (ERC721 or ERC1155)
* @param contractAddress Contract address to check
* @param blockchain Blockchain service instance
* @returns The detected standard or 'UNKNOWN'
*/
async function detectNftStandard(contractAddress, blockchain) {
try {
// First check if it's in our verified collections
const verifiedCollection = config.nft.verifiedCollections.find(collection => collection.contractAddress.toLowerCase() === contractAddress.toLowerCase());
if (verifiedCollection) {
return verifiedCollection.standard;
}
// Try to detect by calling contract methods
const contract = new ethers.Contract(contractAddress, [...ERC721_ABI, ...ERC1155_ABI], blockchain.provider);
try {
// Try ERC721 method
await contract.ownerOf(0);
return 'ERC721';
}
catch (err721) {
try {
// Try ERC1155 method
await contract.balanceOf(ethers.constants.AddressZero, 0);
return 'ERC1155';
}
catch (err1155) {
return 'UNKNOWN';
}
}
}
catch (error) {
console.error('Error detecting NFT standard:', error);
return 'UNKNOWN';
}
}
/**
* Get NFTs for address using Alchemy API
* @param address Owner address
* @param options Additional options like limit and cursor
* @returns NFT list and pagination info
*/
async function getNftsFromAlchemy(address, options) {
try {
if (!config.apiKeys.alchemy) {
throw new Error('Alchemy API key not configured');
}
const apiUrl = `${config.nft.alchemy.apiUrl}${config.apiKeys.alchemy}${config.nft.alchemy.endpoints.getNFTs}`;
const params = {
owner: address,
pageSize: options.limit || config.nft.batchSize,
chain: 'LINEA-MAINNET',
excludeFilters: [],
};
if (options.cursor) {
params.pageKey = options.cursor;
}
if (options.contractAddresses && options.contractAddresses.length > 0) {
params.contractAddresses = options.contractAddresses;
}
if (options.standard && options.standard !== 'ALL') {
params.tokenType = options.standard;
}
const response = await axios.get(apiUrl, { params });
return response.data;
}
catch (error) {
console.error('Error fetching NFTs from Alchemy:', error);
if (axios.isAxiosError(error) && error.response) {
throw new Error(`Alchemy API error: ${error.response.status} - ${error.response.data?.error || 'Unknown error'}`);
}
throw error;
}
}
/**
* List NFTs owned by an address
* @param params The parameters for listing NFTs
* @returns The NFTs owned by the address
*/
export async function listNfts(params) {
try {
const { contractAddress, tokenId, standard, limit, cursor } = params;
const blockchain = new BlockchainService('mainnet');
// If no address is provided, generate a new one
let address;
if (!params.address) {
const keyService = new KeyManagementService();
const wallet = keyService.generateWallet();
address = wallet.address;
}
else {
address = params.address;
}
// If a specific NFT is requested by contract address and token ID
if (contractAddress && tokenId) {
// Detect contract type if not specified
const nftStandard = params.standard && params.standard !== 'ALL'
? params.standard
: await detectNftStandard(contractAddress, blockchain);
if (nftStandard === 'UNKNOWN') {
return {
success: false,
address,
nfts: [],
message: `Could not detect NFT standard for contract ${contractAddress}`,
};
}
if (nftStandard === 'ERC721') {
const nftContract = new ethers.Contract(contractAddress, ERC721_ABI, blockchain.provider);
try {
// Check if the address owns this NFT
const owner = await nftContract.ownerOf(tokenId);
const isOwner = owner.toLowerCase() === address.toLowerCase();
if (isOwner) {
// Get NFT details
const [name, symbol, tokenURI] = await Promise.all([
nftContract.name().catch(() => 'Unknown'),
nftContract.symbol().catch(() => 'NFT'),
nftContract.tokenURI(tokenId).catch(() => ''),
]);
return {
success: true,
address,
nfts: [
{
contractAddress,
tokenId,
name,
symbol,
tokenURI,
owner: address,
standard: 'ERC721',
},
],
};
}
else {
return {
success: true,
address,
nfts: [],
message: `Address does not own NFT with ID ${tokenId} in contract ${contractAddress}`,
};
}
}
catch (error) {
return {
success: false,
address,
nfts: [],
message: `Error fetching ERC721 NFT: ${error instanceof Error ? error.message : 'Unknown error'}`,
};
}
}
else if (nftStandard === 'ERC1155') {
const nftContract = new ethers.Contract(contractAddress, ERC1155_ABI, blockchain.provider);
try {
// Check if the address owns this token
const balance = await nftContract.balanceOf(address, tokenId);
if (balance.gt(0)) {
// Get NFT details
const [name, symbol, uri] = await Promise.all([
nftContract.name().catch(() => 'Unknown'),
nftContract.symbol().catch(() => 'NFT'),
nftContract.uri(tokenId).catch(() => ''),
]);
return {
success: true,
address,
nfts: [
{
contractAddress,
tokenId,
name,
symbol,
tokenURI: uri,
owner: address,
balance: balance.toString(),
standard: 'ERC1155',
},
],
};
}
else {
return {
success: true,
address,
nfts: [],
message: `Address does not own ERC1155 token with ID ${tokenId} in contract ${contractAddress}`,
};
}
}
catch (error) {
return {
success: false,
address,
nfts: [],
message: `Error fetching ERC1155 NFT: ${error instanceof Error ? error.message : 'Unknown error'}`,
};
}
}
}
// If no specific NFT is requested, use Alchemy API to get all NFTs
try {
if (!config.apiKeys.alchemy) {
return {
success: false,
address,
nfts: [],
message: 'Alchemy API key not configured. Please add an Alchemy API key to list all NFTs.',
};
}
const contractAddresses = contractAddress ? [contractAddress] : undefined;
const alchemyNfts = await getNftsFromAlchemy(address, {
limit,
cursor,
contractAddresses,
standard: standard,
});
return {
success: true,
address,
nfts: alchemyNfts.ownedNfts.map((nft) => ({
contractAddress: nft.contract.address,
tokenId: nft.tokenId,
name: nft.title || nft.contract.name || 'Unknown',
symbol: nft.contract.symbol || 'NFT',
tokenURI: nft.tokenUri?.raw || '',
standard: nft.tokenType,
balance: nft.balance || '1',
metadata: nft.raw?.metadata || null,
owner: address,
media: nft.media,
})),
pageKey: alchemyNfts.pageKey,
totalCount: alchemyNfts.totalCount,
};
}
catch (error) {
if (config.nft.enabled) {
return {
success: false,
address,
nfts: [],
message: `Error fetching NFTs from indexer: ${error instanceof Error ? error.message : 'Unknown error'}`,
};
}
else {
return {
success: true,
address,
nfts: [],
message: 'NFT indexing is disabled. Enable NFT indexing to list all NFTs.',
};
}
}
}
catch (error) {
console.error('Error in listNfts:', error);
const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred';
throw new Error(`Failed to list NFTs: ${errorMessage}`);
}
}
/**
* Transfer an NFT to another address
* @param params The parameters for transferring an NFT
* @returns The transaction details
*/
export async function transferNft(params) {
try {
const { contractAddress, tokenId, destination, amount, data, standard } = params;
// Validate destination address
if (!ethers.utils.isAddress(destination)) {
throw new Error('Invalid destination address');
}
// Initialize services
const blockchain = new BlockchainService('mainnet');
const keyService = new KeyManagementService();
// In a real implementation, you would retrieve the user's wallet
// Here, we're generating a new one for demonstration purposes
const wallet = keyService.getDefaultWallet();
const connectedWallet = wallet.connect(blockchain.provider);
// Detect contract type if not specified
const nftStandard = standard || await detectNftStandard(contractAddress, blockchain);
if (nftStandard === 'UNKNOWN') {
throw new Error(`Could not detect NFT standard for contract ${contractAddress}`);
}
if (nftStandard === 'ERC721') {
// Create NFT contract instance with signer
const nftContract = new ethers.Contract(contractAddress, ERC721_ABI, connectedWallet);
// Check if the wallet owns the NFT
const owner = await nftContract.ownerOf(tokenId);
if (owner.toLowerCase() !== wallet.address.toLowerCase()) {
throw new Error(`Wallet does not own NFT with ID ${tokenId}`);
}
// Execute the transfer (using safeTransferFrom for better compatibility)
const tx = await nftContract.safeTransferFrom(wallet.address, destination, tokenId);
await tx.wait();
// Get NFT details
const [name, symbol] = await Promise.all([
nftContract.name().catch(() => 'Unknown'),
nftContract.symbol().catch(() => 'NFT'),
]);
return {
success: true,
transactionHash: tx.hash,
from: wallet.address,
to: destination,
contractAddress,
tokenId,
name,
symbol,
standard: 'ERC721',
};
}
else if (nftStandard === 'ERC1155') {
// Create NFT contract instance with signer
const nftContract = new ethers.Contract(contractAddress, ERC1155_ABI, connectedWallet);
// Check if the wallet owns the token
const balance = await nftContract.balanceOf(wallet.address, tokenId);
if (balance.lt(ethers.BigNumber.from(amount || '1'))) {
throw new Error(`Wallet does not own enough tokens with ID ${tokenId}. Available: ${balance.toString()}, Requested: ${amount || '1'}`);
}
// Execute the transfer
const tx = await nftContract.safeTransferFrom(wallet.address, destination, tokenId, amount || '1', data || '0x');
await tx.wait();
// Get NFT details
const [name, symbol] = await Promise.all([
nftContract.name().catch(() => 'Unknown'),
nftContract.symbol().catch(() => 'NFT'),
]);
return {
success: true,
transactionHash: tx.hash,
from: wallet.address,
to: destination,
contractAddress,
tokenId,
name,
symbol,
amount: amount || '1',
standard: 'ERC1155',
};
}
throw new Error(`Unsupported NFT standard: ${nftStandard}`);
}
catch (error) {
console.error('Error in transferNft:', error);
const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred';
throw new Error(`Failed to transfer NFT: ${errorMessage}`);
}
}
/**
* Get NFT metadata
* @param params The parameters for retrieving NFT metadata
* @returns The NFT metadata
*/
export async function getNftMetadata(params) {
try {
const { contractAddress, tokenId, standard } = params;
const blockchain = new BlockchainService('mainnet');
// First try to get metadata from Alchemy
if (config.apiKeys.alchemy) {
try {
const apiUrl = `${config.nft.alchemy.apiUrl}${config.apiKeys.alchemy}${config.nft.alchemy.endpoints.getNFTMetadata}`;
const response = await axios.get(apiUrl, {
params: {
contractAddress,
tokenId,
tokenType: standard || 'ERC721', // Default to ERC721 if not specified
refreshCache: false,
chain: 'LINEA-MAINNET'
}
});
return {
success: true,
contractAddress,
tokenId,
metadata: response.data,
standard: response.data.tokenType || standard || 'ERC721',
};
}
catch (alchemyError) {
console.warn('Failed to get metadata from Alchemy, falling back to contract calls:', alchemyError);
}
}
// If Alchemy fails or is not configured, fall back to contract calls
const nftStandard = standard || await detectNftStandard(contractAddress, blockchain);
if (nftStandard === 'UNKNOWN') {
throw new Error(`Could not detect NFT standard for contract ${contractAddress}`);
}
if (nftStandard === 'ERC721') {
const nftContract = new ethers.Contract(contractAddress, ERC721_ABI, blockchain.provider);
const [name, symbol, tokenURI] = await Promise.all([
nftContract.name().catch(() => 'Unknown'),
nftContract.symbol().catch(() => 'NFT'),
nftContract.tokenURI(tokenId).catch(() => ''),
]);
// Try to fetch metadata from tokenURI if it's valid
let metadata = null;
if (tokenURI && (tokenURI.startsWith('http') || tokenURI.startsWith('ipfs'))) {
try {
let metadataUrl = tokenURI;
if (tokenURI.startsWith('ipfs://')) {
metadataUrl = `https://ipfs.io/ipfs/${tokenURI.replace('ipfs://', '')}`;
}
const metadataResponse = await axios.get(metadataUrl);
metadata = metadataResponse.data;
}
catch (metadataError) {
console.warn('Failed to fetch metadata from tokenURI:', metadataError);
}
}
return {
success: true,
contractAddress,
tokenId,
name,
symbol,
tokenURI,
metadata,
standard: 'ERC721',
};
}
else if (nftStandard === 'ERC1155') {
const nftContract = new ethers.Contract(contractAddress, ERC1155_ABI, blockchain.provider);
const [name, symbol, uri] = await Promise.all([
nftContract.name().catch(() => 'Unknown'),
nftContract.symbol().catch(() => 'NFT'),
nftContract.uri(tokenId).catch(() => ''),
]);
// Try to fetch metadata from uri if it's valid
let metadata = null;
if (uri && (uri.startsWith('http') || uri.startsWith('ipfs'))) {
try {
let metadataUrl = uri;
// Handle ERC1155 URI template with {id}
if (metadataUrl.includes('{id}')) {
const hexTokenId = ethers.BigNumber.from(tokenId).toHexString().slice(2).padStart(64, '0');
metadataUrl = metadataUrl.replace('{id}', hexTokenId);
}
if (metadataUrl.startsWith('ipfs://')) {
metadataUrl = `https://ipfs.io/ipfs/${metadataUrl.replace('ipfs://', '')}`;
}
const metadataResponse = await axios.get(metadataUrl);
metadata = metadataResponse.data;
}
catch (metadataError) {
console.warn('Failed to fetch metadata from uri:', metadataError);
}
}
return {
success: true,
contractAddress,
tokenId,
name,
symbol,
tokenURI: uri,
metadata,
standard: 'ERC1155',
};
}
throw new Error(`Unsupported NFT standard: ${nftStandard}`);
}
catch (error) {
console.error('Error in getNftMetadata:', error);
const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred';
throw new Error(`Failed to get NFT metadata: ${errorMessage}`);
}
}