@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
JavaScript
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}`);
}
}
}