Skip to main content
Glama

Grove's MCP Server for Pocket Network

blockchain-service.test.ts12.5 kB
import { describe, it, expect, beforeEach, vi } from 'vitest'; import { BlockchainRPCService } from '../blockchain-service.js'; import type { BlockchainService, RPCMethod } from '../../types.js'; describe('BlockchainRPCService', () => { let service: BlockchainRPCService; let mockServicesData: any; beforeEach(() => { // Mock services data mockServicesData = { methodAliases: { balance: ['eth_getBalance', 'getBalance'], 'block height': ['eth_blockNumber', 'getBlockHeight'], }, services: [ { id: 'ethereum-mainnet', name: 'Ethereum Mainnet', blockchain: 'ethereum', network: 'mainnet', rpcUrl: 'https://ethereum.rpc.grove.city/v1/test-app-id', protocol: 'json-rpc', category: 'evm', supportedMethods: [ { name: 'eth_getBalance', description: 'Get balance of an address', params: [ { name: 'address', type: 'string', required: true }, { name: 'block', type: 'string', required: true, default: 'latest' }, ], }, { name: 'eth_blockNumber', description: 'Get current block number', params: [], }, ] as RPCMethod[], }, { id: 'ethereum-foundation-mainnet', name: 'Ethereum Foundation Mainnet', blockchain: 'ethereum-foundation', network: 'mainnet', rpcUrl: 'https://ethereum-foundation.rpc.grove.city/v1/test-app-id', protocol: 'json-rpc', category: 'evm', supportedMethods: [ { name: 'eth_getBalance', description: 'Get balance of an address', params: [], }, ] as RPCMethod[], }, { id: 'polygon-mainnet', name: 'Polygon Mainnet', blockchain: 'polygon', network: 'mainnet', rpcUrl: 'https://polygon.rpc.grove.city/v1/test-app-id', protocol: 'json-rpc', category: 'layer2', supportedMethods: [ { name: 'eth_getBalance', description: 'Get balance of an address', params: [], }, ] as RPCMethod[], }, ] as BlockchainService[], }; service = new BlockchainRPCService(mockServicesData); // Reset fetch mock vi.restoreAllMocks(); }); describe('Service Initialization', () => { it('should index services by ID', () => { const ethereum = service.getServiceById('ethereum-mainnet'); expect(ethereum).toBeDefined(); // Note: Due to foundation preference, ethereum-mainnet key maps to ethereum-foundation service expect(ethereum?.blockchain).toBe('ethereum-foundation'); }); it('should prefer foundation endpoints when available', () => { const ethereum = service.getServiceByBlockchain('ethereum', 'mainnet'); expect(ethereum).toBeDefined(); expect(ethereum?.id).toBe('ethereum-foundation-mainnet'); }); it('should return all unique services', () => { const allServices = service.getAllServices(); // Only 2 unique services (ethereum-foundation and polygon, ethereum is overridden) expect(allServices).toHaveLength(2); }); }); describe('getServicesByCategory', () => { it('should filter services by category', () => { const evmServices = service.getServicesByCategory('evm'); // Only ethereum-foundation is in the unique services list (ethereum was overridden) expect(evmServices).toHaveLength(1); expect(evmServices.every(s => s.category === 'evm')).toBe(true); }); it('should return empty array for non-existent category', () => { const services = service.getServicesByCategory('non-existent'); expect(services).toHaveLength(0); }); }); describe('getServiceByBlockchain', () => { it('should get service by blockchain name', () => { const polygon = service.getServiceByBlockchain('polygon', 'mainnet'); expect(polygon).toBeDefined(); expect(polygon?.blockchain).toBe('polygon'); }); it('should return undefined for non-existent blockchain', () => { const result = service.getServiceByBlockchain('non-existent', 'mainnet'); expect(result).toBeUndefined(); }); }); describe('parseQuery', () => { it('should extract blockchain from query', () => { const result = service.parseQuery('get ethereum balance'); expect(result.blockchain).toBe('ethereum'); expect(result.network).toBe('mainnet'); }); it('should detect testnet in query', () => { const result = service.parseQuery('get polygon test balance'); expect(result.blockchain).toBe('polygon'); expect(result.network).toBe('testnet'); }); it('should handle queries without blockchain', () => { const result = service.parseQuery('get balance'); expect(result.blockchain).toBeUndefined(); expect(result.intent).toBe('get balance'); }); it('should recognize various blockchain aliases', () => { expect(service.parseQuery('eth balance').blockchain).toBe('ethereum'); expect(service.parseQuery('matic balance').blockchain).toBe('polygon'); expect(service.parseQuery('arb balance').blockchain).toBe('arbitrum'); }); }); describe('findMethodByQuery', () => { it('should find methods by alias', () => { const results = service.findMethodByQuery('get balance'); expect(results.length).toBeGreaterThan(0); expect(results.some(r => r.method.name === 'eth_getBalance')).toBe(true); }); it('should find methods by method name', () => { const results = service.findMethodByQuery('eth_getBalance'); expect(results.length).toBeGreaterThan(0); expect(results.some(r => r.method.name === 'eth_getBalance')).toBe(true); }); it('should find methods by description', () => { const results = service.findMethodByQuery('Get balance'); expect(results.length).toBeGreaterThan(0); }); it('should return empty array for no matches', () => { const results = service.findMethodByQuery('nonexistent method xyz'); expect(results).toHaveLength(0); }); }); describe('callRPCMethod - Success Cases', () => { it('should make successful RPC call', async () => { const mockResponse = { jsonrpc: '2.0', id: 1, result: '0x1234567890abcdef', }; global.fetch = vi.fn().mockResolvedValue({ ok: true, json: async () => mockResponse, }); const result = await service.callRPCMethod('ethereum-mainnet', 'eth_getBalance', [ '0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb', 'latest', ]); expect(result.success).toBe(true); expect(result.data).toBe('0x1234567890abcdef'); expect(result.metadata).toBeDefined(); expect(result.metadata?.endpoint).toContain('ethereum-foundation.rpc.grove.city'); }); it('should use environment appId when not provided', async () => { process.env.GROVE_APP_ID = 'env-app-id'; global.fetch = vi.fn().mockResolvedValue({ ok: true, json: async () => ({ jsonrpc: '2.0', id: 1, result: '0x123' }), }); await service.callRPCMethod('ethereum-mainnet', 'eth_blockNumber', []); expect(global.fetch).toHaveBeenCalledWith( expect.stringContaining('/v1/env-app-id'), expect.any(Object) ); delete process.env.GROVE_APP_ID; }); }); describe('callRPCMethod - Error Cases', () => { it('should handle service not found', async () => { const result = await service.callRPCMethod('non-existent-service', 'eth_blockNumber'); expect(result.success).toBe(false); expect(result.error).toContain('Service not found'); }); it('should handle HTTP 429 rate limit error', async () => { global.fetch = vi.fn().mockResolvedValue({ ok: false, status: 429, statusText: 'Too Many Requests', text: async () => 'Rate limit exceeded', }); const result = await service.callRPCMethod('ethereum-mainnet', 'eth_getBalance', [ '0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb', ]); expect(result.success).toBe(false); expect(result.error).toContain('Rate limit exceeded'); expect(result.error).toContain('portal.grove.city'); expect(result.data?.httpStatus).toBe(429); }); it('should handle HTTP 503 service unavailable', async () => { global.fetch = vi.fn().mockResolvedValue({ ok: false, status: 503, statusText: 'Service Unavailable', text: async () => 'Service temporarily unavailable', }); const result = await service.callRPCMethod('ethereum-mainnet', 'eth_blockNumber'); expect(result.success).toBe(false); expect(result.error).toContain('Service temporarily unavailable'); expect(result.data?.httpStatus).toBe(503); }); it('should handle HTTP 500 server error', async () => { global.fetch = vi.fn().mockResolvedValue({ ok: false, status: 500, statusText: 'Internal Server Error', text: async () => 'Internal server error', }); const result = await service.callRPCMethod('ethereum-mainnet', 'eth_blockNumber'); expect(result.success).toBe(false); expect(result.error).toContain('Server error'); expect(result.data?.httpStatus).toBe(500); }); it('should handle JSON-RPC errors', async () => { global.fetch = vi.fn().mockResolvedValue({ ok: true, json: async () => ({ jsonrpc: '2.0', id: 1, error: { code: -32602, message: 'Invalid params', }, }), }); const result = await service.callRPCMethod('ethereum-mainnet', 'eth_getBalance', ['invalid']); expect(result.success).toBe(false); expect(result.error).toBe('Invalid params'); expect(result.data).toEqual({ code: -32602, message: 'Invalid params' }); }); it('should handle network failures', async () => { global.fetch = vi.fn().mockRejectedValue(new Error('Network error')); const result = await service.callRPCMethod('ethereum-mainnet', 'eth_blockNumber'); expect(result.success).toBe(false); expect(result.error).toBe('Network error'); expect(result.data?.errorType).toBe('Error'); }); }); describe('executeQuery', () => { it('should execute natural language query successfully', async () => { global.fetch = vi.fn().mockResolvedValue({ ok: true, json: async () => ({ jsonrpc: '2.0', id: 1, result: '0x123' }), }); const result = await service.executeQuery('get ethereum balance'); expect(result.success).toBe(true); }); it('should return error for queries with no matches', async () => { const result = await service.executeQuery('completely unknown method xyz'); expect(result.success).toBe(false); expect(result.error).toContain('No matching methods found'); }); it('should filter by blockchain when specified', async () => { global.fetch = vi.fn().mockResolvedValue({ ok: true, json: async () => ({ jsonrpc: '2.0', id: 1, result: '0x123' }), }); const result = await service.executeQuery('get polygon balance'); expect(result.success).toBe(true); expect(global.fetch).toHaveBeenCalledWith( expect.stringContaining('polygon.rpc.grove.city'), expect.any(Object) ); }); }); describe('getServiceMethods', () => { it('should get methods for a service', () => { const methods = service.getServiceMethods('ethereum-foundation-mainnet'); expect(methods).toHaveLength(1); expect(methods.map(m => m.name)).toContain('eth_getBalance'); }); it('should return empty array for non-existent service', () => { const methods = service.getServiceMethods('non-existent'); expect(methods).toHaveLength(0); }); }); describe('getCategories', () => { it('should return all unique categories', () => { const categories = service.getCategories(); expect(categories).toContain('evm'); expect(categories).toContain('layer2'); expect(categories.length).toBeGreaterThan(0); }); }); });

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