UNPKG

@polymerdao/mcp-polymer

Version:

A Model Context Protocol (MCP) server for blockchain event verification using Polymer's Prove API.

296 lines (295 loc) 12.2 kB
import axios from 'axios'; import { ChainlistClient } from './chainlist.js'; export class PolymerClient { constructor(polymerApiBase) { this.polymerApiBase = polymerApiBase; this.chainlistClient = new ChainlistClient(); } async getRpcEndpoint(chainName, environment) { const { rpcUrl } = await this.chainlistClient.getChainIdAndRpc(chainName, environment); return rpcUrl; } async requestProof(chainName, globalLogIndex, blockNumber, apiKey, environment = "mainnet") { const { chainId } = await this.chainlistClient.getChainIdAndRpc(chainName, environment); const url = `${this.polymerApiBase}/`; const payload = { jsonrpc: "2.0", method: "polymer_requestProof", params: [{ srcChainId: chainId, srcBlockNumber: blockNumber, globalLogIndex: globalLogIndex, }], id: 1, }; const response = await axios.post(url, payload, { headers: { 'Authorization': `Bearer ${apiKey}`, 'Content-Type': 'application/json' }, }); const data = response.data; return (data.result || data); } async getProofStatus(jobId, apiKey) { const url = `${this.polymerApiBase}/`; const payload = { jsonrpc: "2.0", method: "polymer_queryProof", params: [parseInt(jobId, 10)], // Convert job ID to number id: 1, }; try { const response = await axios.post(url, payload, { headers: { 'Authorization': `Bearer ${apiKey}`, 'Content-Type': 'application/json' }, }); const data = response.data; if (data.error) { throw new Error(`API Error: ${data.error.message || JSON.stringify(data.error)}`); } if (!data.result) { throw new Error(`No result in response: ${JSON.stringify(data)}`); } return data.result; } catch (error) { const axiosError = error; if (axiosError.response) { throw new Error(`HTTP ${axiosError.response.status}: ${JSON.stringify(axiosError.response.data)}`); } throw error; } } async getConnectedChains(apiKey) { const url = `${this.polymerApiBase}/`; const payload = { jsonrpc: "2.0", method: "info_getConnectedChains", params: [], id: 1, }; const response = await axios.post(url, payload, { headers: { 'Authorization': `Bearer ${apiKey}` }, }); const data = response.data; return this.parseConnectedChainsResponse(data.result); } async getClientUpdates(apiKey, rpcEnvironment = "mainnet") { const url = `${this.polymerApiBase}/`; const payload = { jsonrpc: "2.0", method: "info_getClientUpdates", params: [], id: 1, }; const response = await axios.post(url, payload, { headers: { 'Authorization': `Bearer ${apiKey}` }, }); const data = response.data; return this.parseClientUpdatesResponse(data.result, rpcEnvironment); } async eth_getBlockByNumber(chainName, blockNumber, includeTransactions, _environment) { const result = await this.chainlistClient.executeWithRetry(chainName, 'eth_getBlockByNumber', [blockNumber, includeTransactions], async (rpcUrl) => { const payload = { jsonrpc: "2.0", method: "eth_getBlockByNumber", params: [blockNumber, includeTransactions], id: 1, }; const response = await axios.post(rpcUrl, payload, { headers: { 'Content-Type': 'application/json' }, timeout: 30000 }); const data = response.data; if (data.error) { throw new Error(`RPC Error from ${rpcUrl}: ${data.error.message || JSON.stringify(data.error)}`); } return data.result ?? null; }); if (!result.success) { throw new Error(`eth_getBlockByNumber failed: ${result.error}`); } return result.data; } async eth_getBlockByHash(chainName, blockHash, includeTransactions, _environment) { const result = await this.chainlistClient.executeWithRetry(chainName, 'eth_getBlockByHash', [blockHash, includeTransactions], async (rpcUrl) => { const payload = { jsonrpc: "2.0", method: "eth_getBlockByHash", params: [blockHash, includeTransactions], id: 1, }; const response = await axios.post(rpcUrl, payload, { headers: { 'Content-Type': 'application/json' }, timeout: 30000 }); const data = response.data; if (data.error) { throw new Error(`RPC Error from ${rpcUrl}: ${data.error.message || JSON.stringify(data.error)}`); } return data.result ?? null; }); if (!result.success) { throw new Error(`eth_getBlockByHash failed: ${result.error}`); } return result.data; } async eth_getLogs(chainName, filter, environment) { try { const { rpcUrl } = await this.chainlistClient.getChainIdAndRpc(chainName, environment); const payload = { jsonrpc: "2.0", method: "eth_getLogs", params: [filter], id: 1, }; const response = await axios.post(rpcUrl, payload, { headers: { 'Content-Type': 'application/json' }, timeout: 30000 }); const data = response.data; if (data.error) { throw new Error(`RPC Error: ${data.error.message || JSON.stringify(data.error)}`); } return data.result ?? null; } catch (error) { if (error instanceof Error) { throw new Error(`eth_getLogs failed: ${error.message}`); } throw error; } } getChainlistCacheStats() { return this.chainlistClient.getCacheStats(); } clearChainlistCache() { this.chainlistClient.clearCache(); } getChainRpcs(chainName, environment) { return this.chainlistClient.getChainRpcs(chainName, environment); } parseConnectedChainsResponse(result) { // Parse ConnectedChainsResponse format (API returns "connectedChains") const resultObj = result; if (resultObj.connectedChains && Array.isArray(resultObj.connectedChains)) { const chains = resultObj.connectedChains.map((chainData) => this.parseSingleConnectedChain(chainData)); return { connected_chains: chains }; } // Try direct array format if (Array.isArray(result)) { const chains = result.map((chainData) => this.parseSingleConnectedChain(chainData)); return { connected_chains: chains }; } throw new Error("Unable to parse connected chains response"); } parseSingleConnectedChain(chainData) { // Try multiple field name variations for chain_id const chain_id = chainData.chain_id || chainData.chainId || chainData.id || chainData.chainID; const chain_name = chainData.chainName || chainData.chain_name || chainData.name || "unknown"; const status = "connected"; // API doesn't provide status - infer as "connected" const last_update = chainData.last_update || chainData.lastUpdate; if (!chain_id) { throw new Error(`Missing chain ID in connected chain data: ${JSON.stringify(chainData)}`); } return { chain_id, chain_name, status, last_update }; } async parseClientUpdatesResponse(result, rpcEnvironment) { // Parse ClientUpdateResponse format (API returns "clientStatuses") const resultObj = result; if (resultObj.clientStatuses && Array.isArray(resultObj.clientStatuses)) { const statuses = await Promise.all(resultObj.clientStatuses.map((statusData) => this.parseSingleClientStatus(statusData, rpcEnvironment))); return { client_statuses: statuses }; } // Try direct array format if (Array.isArray(result)) { const statuses = await Promise.all(result.map((statusData) => this.parseSingleClientStatus(statusData, rpcEnvironment))); return { client_statuses: statuses }; } throw new Error("Unable to parse client updates response"); } async parseSingleClientStatus(statusData, rpcEnvironment) { // Try multiple field name variations for chain_id (API uses chainID) const chain_id = statusData.chainID || statusData.chain_id || statusData.chainId || statusData.id || 0; let chain_name = statusData.chainName || statusData.chain_name || statusData.name; // If no chain name provided, try to resolve it from chainlist using chain_id if (!chain_name || chain_name === "unknown") { try { const chainInfo = await this.chainlistClient.getChainInfoById(chain_id, rpcEnvironment); chain_name = chainInfo.name; } catch (error) { chain_name = `Chain ${chain_id}`; } } // Extract timing and block information from API response const clientUpdatedAt = statusData.clientUpdatedAt; const blockNumber = statusData.blockNumber; // Convert clientUpdatedAt to ISO string for last_update const last_update = clientUpdatedAt ? new Date(clientUpdatedAt * 1000).toISOString() : undefined; const block_height = blockNumber || statusData.block_height || statusData.blockHeight; // Use last_update as the status to show when the client was last updated const status = last_update || "unknown"; const sync_status = last_update || "unknown"; if (!chain_id) { throw new Error(`Missing chain ID in client status data: ${JSON.stringify(statusData)}`); } return { chain_id, chain_name, status, last_update, block_height, sync_status }; } async callGenericMethod(methodName, params, apiKey, apiBaseOverride) { const url = apiBaseOverride || `${this.polymerApiBase}/`; const payload = { jsonrpc: "2.0", method: methodName, params: Array.isArray(params) ? params : params ? [params] : [], id: Date.now() }; try { const response = await axios.post(url, payload, { headers: { 'Authorization': `Bearer ${apiKey}`, 'Content-Type': 'application/json' }, timeout: 30000 }); const data = response.data; if (data.error) { throw new Error(`RPC Error: ${data.error.message || 'Unknown error'}`); } return data.result; } catch (error) { if (error instanceof Error && 'response' in error) { const axiosError = error; if (axiosError.response?.data) { const errorData = axiosError.response.data; if (errorData.error) { throw new Error(`API Error: ${errorData.error.message}`); } } } throw new Error(`Failed to call method ${methodName}: ${error}`); } } }