@graphprotocol/graph-cli
Version:
CLI for building for and deploying to The Graph
193 lines (192 loc) • 8.05 kB
JavaScript
import immutable from 'immutable';
import debugFactory from '../debug.js';
import fetch from '../fetch.js';
const logger = debugFactory('graph-cli:contract-service');
export class ContractService {
registry;
constructor(registry) {
this.registry = registry;
}
async fetchFromEtherscan(url) {
const result = await fetch(url).catch(_error => {
throw new Error(`Contract API is unreachable`);
});
let json = {};
if (result.ok) {
json = await result.json().catch(error => {
throw new Error(`Invalid JSON: ${error}`);
});
if (json.status === '1')
return json;
}
logger('Failed to fetch from contract API: [%s] %s (%s)\n%O', result.status, result.statusText, result.url, json);
if (json.message) {
throw new Error(`${json.message ?? ''} - ${json.result ?? ''}`);
}
return null;
}
// replace {api_key} with process.env[api_key]
// return empty string if env var not found
applyVars(url) {
const match = url.match(/\{([^}]+)\}/);
if (!match)
return url;
const key = match[1];
return process.env[key] ? url.replace(`{${key}}`, process.env[key]) : '';
}
getEtherscanUrls(networkId) {
const network = this.registry.getNetworkById(networkId);
if (!network) {
throw new Error(`Invalid network ${networkId}`);
}
return (network.apiUrls
?.filter(item => ['etherscan', 'blockscout'].includes(item.kind))
?.map(item => item.url)
.map(this.applyVars)
.filter(Boolean) ?? []);
}
getRpcUrls(networkId) {
const network = this.registry.getNetworkById(networkId);
if (!network) {
throw new Error(`Invalid network ${networkId}`);
}
return network.rpcUrls?.map(this.applyVars).filter(Boolean) ?? [];
}
async getABI(ABICtor, networkId, address) {
const urls = this.getEtherscanUrls(networkId);
const errors = [];
if (!urls.length) {
throw new Error(`No contract API available for ${networkId} in the registry`);
}
for (const url of urls) {
try {
const json = await this.fetchFromEtherscan(`${url}?module=contract&action=getabi&address=${address}`);
if (json) {
return new ABICtor('Contract', undefined, immutable.fromJS(JSON.parse(json.result)));
}
throw new Error(`no result: ${JSON.stringify(json)}`);
}
catch (error) {
logger(`Failed to fetch from ${url}: ${error}`);
errors.push(String(error));
}
}
throw new Error(errors?.[0]);
}
async getStartBlock(networkId, address) {
const urls = this.getEtherscanUrls(networkId);
if (!urls.length) {
throw new Error(`No contract API available for ${networkId} in the registry`);
}
for (const url of urls) {
try {
const json = await this.fetchFromEtherscan(`${url}?module=contract&action=getcontractcreation&contractaddresses=${address}`);
if (json?.result?.length) {
if (json.result[0]?.blockNumber) {
return json.result[0].blockNumber;
}
const txHash = json.result[0].txHash;
const tx = await this.fetchTransactionByHash(networkId, txHash);
if (!tx?.blockNumber) {
throw new Error(`no blockNumber: ${JSON.stringify(tx)}`);
}
return Number(tx.blockNumber).toString();
}
throw new Error(`no result: ${JSON.stringify(json)}`);
}
catch (error) {
logger(`Failed to fetch start block from ${url}: ${error}`);
}
}
throw new Error(`Failed to fetch deploy contract transaction for ${address}`);
}
async getContractName(networkId, address) {
const urls = this.getEtherscanUrls(networkId);
if (!urls.length) {
throw new Error(`No contract API available for ${networkId} in the registry`);
}
for (const url of urls) {
try {
const json = await this.fetchFromEtherscan(`${url}?module=contract&action=getsourcecode&address=${address}`);
if (json) {
const { ContractName } = json.result[0];
if (ContractName !== '') {
return ContractName;
}
}
throw new Error(`no result: ${JSON.stringify(json)}`);
}
catch (error) {
logger(`Failed to fetch from ${url}: ${error}`);
}
}
throw new Error(`Failed to fetch contract name for ${address}`);
}
async getFromSourcify(ABICtor, networkId, address) {
try {
const network = this.registry.getNetworkById(networkId);
if (!network)
throw new Error(`Invalid network ${networkId}`);
if (!network.caip2Id.startsWith('eip155'))
throw new Error(`Invalid chainId, Sourcify API only supports EVM chains`);
if (!address.startsWith('0x') || address.length != 42)
throw new Error(`Invalid address, must start with 0x prefix and be 20 bytes long`);
const chainId = network.caip2Id.split(':')[1];
const url = `https://sourcify.dev/server/v2/contract/${chainId}/${address}?fields=abi,compilation,deployment`;
const resp = await fetch(url).catch(error => {
throw new Error(`Sourcify API is unreachable: ${error}`);
});
if (resp.status === 404)
throw new Error(`Sourcify API says contract is not verified`);
if (!resp.ok)
throw new Error(`Sourcify API returned status ${resp.status}`);
const json = await resp.json();
if (json) {
const abi = json.abi;
const contractName = json.compilation?.name;
const blockNumber = json.deployment?.blockNumber;
if (!abi || !contractName || !blockNumber)
throw new Error('Contract is missing metadata');
return {
abi: new ABICtor(contractName, undefined, immutable.fromJS(abi)),
startBlock: Number(blockNumber).toString(),
name: contractName,
};
}
throw new Error(`No result: ${JSON.stringify(json)}`);
}
catch (error) {
logger(`Failed to fetch from Sourcify: ${error}`);
}
return null;
}
async fetchTransactionByHash(networkId, txHash) {
const urls = this.getRpcUrls(networkId);
if (!urls.length) {
throw new Error(`No JSON-RPC available for ${networkId} in the registry`);
}
for (const url of urls) {
try {
const response = await fetch(url, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
jsonrpc: '2.0',
method: 'eth_getTransactionByHash',
params: [txHash],
id: 1,
}),
});
const json = await response.json();
if (json.result) {
return json.result;
}
throw new Error(JSON.stringify(json));
}
catch (error) {
logger(`Failed to fetch tx ${txHash} from ${url}: ${error}`);
}
}
throw new Error(`JSON-RPC is unreachable`);
}
}