Skip to main content
Glama

Grove's MCP Server for Pocket Network

advanced-blockchain-service.ts15.9 kB
import { EndpointResponse } from '../types.js'; import { BlockchainRPCService } from './blockchain-service.js'; /** * Advanced blockchain service for transactions, tokens, blocks, and utilities */ export class AdvancedBlockchainService { private blockchainService: BlockchainRPCService; // Standard ERC-20 ABI methods private static readonly ERC20_BALANCE_OF = '0x70a08231'; // balanceOf(address) private static readonly ERC20_DECIMALS = '0x313ce567'; // decimals() private static readonly ERC20_SYMBOL = '0x95d89b41'; // symbol() private static readonly ERC20_NAME = '0x06fdde03'; // name() private static readonly ERC20_TOTAL_SUPPLY = '0x18160ddd'; // totalSupply() constructor(blockchainService: BlockchainRPCService) { this.blockchainService = blockchainService; } /** * Get transaction details by hash */ async getTransaction( blockchain: string, txHash: string, network: 'mainnet' | 'testnet' = 'mainnet' ): Promise<EndpointResponse> { const service = this.blockchainService.getServiceByBlockchain(blockchain, network); if (!service) { return { success: false, error: `Blockchain service not found: ${blockchain} (${network})`, }; } // Use appropriate method based on blockchain const method = service.category === 'evm' || service.category === 'layer2' ? 'eth_getTransactionByHash' : service.blockchain === 'solana' ? 'getTransaction' : 'eth_getTransactionByHash'; return this.blockchainService.callRPCMethod(service.id, method, [txHash]); } /** * Get transaction receipt */ async getTransactionReceipt( blockchain: string, txHash: string, network: 'mainnet' | 'testnet' = 'mainnet' ): Promise<EndpointResponse> { const service = this.blockchainService.getServiceByBlockchain(blockchain, network); if (!service) { return { success: false, error: `Blockchain service not found: ${blockchain} (${network})`, }; } return this.blockchainService.callRPCMethod( service.id, 'eth_getTransactionReceipt', [txHash] ); } /** * Estimate gas for a transaction */ async estimateGas( blockchain: string, transaction: any, network: 'mainnet' | 'testnet' = 'mainnet' ): Promise<EndpointResponse> { const service = this.blockchainService.getServiceByBlockchain(blockchain, network); if (!service) { return { success: false, error: `Blockchain service not found: ${blockchain} (${network})`, }; } return this.blockchainService.callRPCMethod( service.id, 'eth_estimateGas', [transaction] ); } /** * Get ERC-20 token balance for an address */ async getTokenBalance( blockchain: string, tokenAddress: string, walletAddress: string, network: 'mainnet' | 'testnet' = 'mainnet' ): Promise<EndpointResponse> { const service = this.blockchainService.getServiceByBlockchain(blockchain, network); if (!service) { return { success: false, error: `Blockchain service not found: ${blockchain} (${network})`, }; } // Encode balanceOf(address) call const paddedAddress = walletAddress.replace('0x', '').padStart(64, '0'); const data = AdvancedBlockchainService.ERC20_BALANCE_OF + paddedAddress; const result = await this.blockchainService.callRPCMethod( service.id, 'eth_call', [ { to: tokenAddress, data: data, }, 'latest', ] ); if (result.success && result.data) { // Convert hex to decimal const balance = BigInt(result.data).toString(); return { success: true, data: { balance, balanceHex: result.data, }, metadata: result.metadata, }; } return result; } /** * Get token metadata (name, symbol, decimals, totalSupply) */ async getTokenMetadata( blockchain: string, tokenAddress: string, network: 'mainnet' | 'testnet' = 'mainnet' ): Promise<EndpointResponse> { const service = this.blockchainService.getServiceByBlockchain(blockchain, network); if (!service) { return { success: false, error: `Blockchain service not found: ${blockchain} (${network})`, }; } try { // Call all methods in parallel const [decimalsResult, symbolResult, nameResult, totalSupplyResult] = await Promise.all([ this.blockchainService.callRPCMethod( service.id, 'eth_call', [{ to: tokenAddress, data: AdvancedBlockchainService.ERC20_DECIMALS }, 'latest'] ), this.blockchainService.callRPCMethod( service.id, 'eth_call', [{ to: tokenAddress, data: AdvancedBlockchainService.ERC20_SYMBOL }, 'latest'] ), this.blockchainService.callRPCMethod( service.id, 'eth_call', [{ to: tokenAddress, data: AdvancedBlockchainService.ERC20_NAME }, 'latest'] ), this.blockchainService.callRPCMethod( service.id, 'eth_call', [{ to: tokenAddress, data: AdvancedBlockchainService.ERC20_TOTAL_SUPPLY }, 'latest'] ), ]); return { success: true, data: { address: tokenAddress, decimals: decimalsResult.success ? parseInt(decimalsResult.data, 16) : null, symbol: symbolResult.success ? this.decodeString(symbolResult.data) : null, name: nameResult.success ? this.decodeString(nameResult.data) : null, totalSupply: totalSupplyResult.success ? BigInt(totalSupplyResult.data).toString() : null, }, metadata: { timestamp: new Date().toISOString(), endpoint: service.rpcUrl, }, }; } catch (error) { return { success: false, error: error instanceof Error ? error.message : 'Failed to fetch token metadata', }; } } /** * Get block details with transactions */ async getBlockDetails( blockchain: string, blockNumber: string | number, includeTransactions: boolean = false, network: 'mainnet' | 'testnet' = 'mainnet' ): Promise<EndpointResponse> { const service = this.blockchainService.getServiceByBlockchain(blockchain, network); if (!service) { return { success: false, error: `Blockchain service not found: ${blockchain} (${network})`, }; } const blockParam = typeof blockNumber === 'number' ? '0x' + blockNumber.toString(16) : blockNumber; return this.blockchainService.callRPCMethod( service.id, 'eth_getBlockByNumber', [blockParam, includeTransactions] ); } /** * Search event logs */ async searchLogs( blockchain: string, filter: { fromBlock?: string; toBlock?: string; address?: string | string[]; topics?: (string | string[] | null)[]; }, network: 'mainnet' | 'testnet' = 'mainnet' ): Promise<EndpointResponse> { const service = this.blockchainService.getServiceByBlockchain(blockchain, network); if (!service) { return { success: false, error: `Blockchain service not found: ${blockchain} (${network})`, }; } return this.blockchainService.callRPCMethod( service.id, 'eth_getLogs', [filter] ); } /** * Compare balance across multiple chains for the same address */ async compareBalances( address: string, blockchains?: string[], network: 'mainnet' | 'testnet' = 'mainnet' ): Promise<EndpointResponse> { try { // Get all EVM chains if not specified const chainsToCheck = blockchains || this.getEVMChains(); const results = await Promise.all( chainsToCheck.map(async (blockchain) => { const service = this.blockchainService.getServiceByBlockchain(blockchain, network); if (!service) { return { blockchain, error: 'Service not found', balance: null }; } const result = await this.blockchainService.callRPCMethod( service.id, 'eth_getBalance', [address, 'latest'] ); if (result.success && result.data) { const weiBalance = BigInt(result.data); const ethBalance = Number(weiBalance) / 1e18; return { blockchain, balance: ethBalance, balanceWei: weiBalance.toString(), balanceHex: result.data, }; } return { blockchain, error: result.error, balance: null }; }) ); const totalBalance = results .filter(r => r.balance !== null) .reduce((sum, r) => sum + (r.balance || 0), 0); return { success: true, data: { address, balances: results, totalBalance, chainsWithBalance: results.filter(r => r.balance && r.balance > 0).length, }, metadata: { timestamp: new Date().toISOString(), endpoint: 'multi-chain', }, }; } catch (error) { return { success: false, error: error instanceof Error ? error.message : 'Failed to compare balances', }; } } /** * Get current gas price */ async getGasPrice( blockchain: string, network: 'mainnet' | 'testnet' = 'mainnet' ): Promise<EndpointResponse> { const service = this.blockchainService.getServiceByBlockchain(blockchain, network); if (!service) { return { success: false, error: `Blockchain service not found: ${blockchain} (${network})`, }; } const result = await this.blockchainService.callRPCMethod( service.id, 'eth_gasPrice', [] ); if (result.success && result.data) { const gasWei = BigInt(result.data); const gasGwei = Number(gasWei) / 1e9; return { success: true, data: { gasPrice: gasGwei, gasPriceWei: gasWei.toString(), gasPriceHex: result.data, }, metadata: result.metadata, }; } return result; } /** * Convert between units (wei/gwei/eth, etc.) */ convertUnits( value: string, fromUnit: 'wei' | 'gwei' | 'eth', toUnit: 'wei' | 'gwei' | 'eth' ): EndpointResponse { try { const unitMap = { wei: 1n, gwei: 1000000000n, eth: 1000000000000000000n, }; const valueWei = BigInt(value) * unitMap[fromUnit]; const result = valueWei / unitMap[toUnit]; return { success: true, data: { value: result.toString(), fromUnit, toUnit, exact: valueWei % unitMap[toUnit] === 0n, }, }; } catch (error) { return { success: false, error: error instanceof Error ? error.message : 'Failed to convert units', }; } } /** * Validate address format */ validateAddress(address: string, blockchain: string): EndpointResponse { try { const service = this.blockchainService.getServiceByBlockchain(blockchain); if (!service) { return { success: false, error: `Blockchain service not found: ${blockchain}`, }; } let isValid = false; let format = ''; if (service.category === 'evm' || service.category === 'layer2') { // EVM address validation isValid = /^0x[a-fA-F0-9]{40}$/.test(address); format = 'EVM (0x + 40 hex characters)'; } else if (service.blockchain === 'solana') { // Solana address validation (base58, 32-44 chars typically) isValid = /^[1-9A-HJ-NP-Za-km-z]{32,44}$/.test(address); format = 'Solana (base58, 32-44 characters)'; } else if (service.category === 'cosmos') { // Cosmos address validation (bech32) isValid = /^[a-z]+1[a-z0-9]{38,}$/.test(address); format = 'Cosmos (bech32 format)'; } return { success: true, data: { address, blockchain, isValid, format, }, }; } catch (error) { return { success: false, error: error instanceof Error ? error.message : 'Failed to validate address', }; } } /** * Decode hex string to UTF-8 */ decodeHex(hex: string): EndpointResponse { try { const cleanHex = hex.startsWith('0x') ? hex.slice(2) : hex; const bytes = Buffer.from(cleanHex, 'hex'); const utf8 = bytes.toString('utf8'); const ascii = bytes.toString('ascii'); return { success: true, data: { hex, utf8, ascii, bytes: Array.from(bytes), length: bytes.length, }, }; } catch (error) { return { success: false, error: error instanceof Error ? error.message : 'Failed to decode hex', }; } } /** * Call a contract view function */ async callContractView( blockchain: string, contractAddress: string, data: string, network: 'mainnet' | 'testnet' = 'mainnet' ): Promise<EndpointResponse> { const service = this.blockchainService.getServiceByBlockchain(blockchain, network); if (!service) { return { success: false, error: `Blockchain service not found: ${blockchain} (${network})`, }; } return this.blockchainService.callRPCMethod( service.id, 'eth_call', [ { to: contractAddress, data: data, }, 'latest', ] ); } /** * Get historical balance at a specific block */ async getHistoricalBalance( blockchain: string, address: string, blockNumber: string | number, network: 'mainnet' | 'testnet' = 'mainnet' ): Promise<EndpointResponse> { const service = this.blockchainService.getServiceByBlockchain(blockchain, network); if (!service) { return { success: false, error: `Blockchain service not found: ${blockchain} (${network})`, }; } const blockParam = typeof blockNumber === 'number' ? '0x' + blockNumber.toString(16) : blockNumber; const result = await this.blockchainService.callRPCMethod( service.id, 'eth_getBalance', [address, blockParam] ); if (result.success && result.data) { const weiBalance = BigInt(result.data); const ethBalance = Number(weiBalance) / 1e18; return { success: true, data: { address, blockNumber: blockParam, balance: ethBalance, balanceWei: weiBalance.toString(), balanceHex: result.data, }, metadata: result.metadata, }; } return result; } /** * Get EVM-compatible chains */ private getEVMChains(): string[] { const evmChains = this.blockchainService.getAllServices() .filter(s => (s.category === 'evm' || s.category === 'layer2') && s.network === 'mainnet') .map(s => s.blockchain); return [...new Set(evmChains)]; } /** * Decode string from contract return data */ private decodeString(hex: string): string { try { if (!hex || hex === '0x') return ''; const cleanHex = hex.startsWith('0x') ? hex.slice(2) : hex; // Skip the first 64 bytes (offset) and next 64 bytes (length) const lengthHex = cleanHex.slice(64, 128); const length = parseInt(lengthHex, 16); if (length === 0) return ''; const dataHex = cleanHex.slice(128, 128 + length * 2); return Buffer.from(dataHex, 'hex').toString('utf8'); } catch { return ''; } } }

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/buildwithgrove/mcp-pocket'

If you have feedback or need assistance with the MCP directory API, please join our Discord server