@graphprotocol/graph-cli
Version:
CLI for building for and deploying to The Graph
446 lines (444 loc) • 24.5 kB
JavaScript
/* eslint-disable unicorn/no-array-for-each */
import fs from 'node:fs';
import immutable from 'immutable';
import { decodeLog } from 'web3-eth-abi';
import yaml from 'yaml';
import * as typesCodegen from '../../../codegen/types/index.js';
import * as tsCodegen from '../../../codegen/typescript.js';
import * as util from '../../../codegen/util.js';
const doFixtureCodegen = fs.existsSync('./fixtures.yaml');
export default class AbiCodeGenerator {
abi;
constructor(abi) {
this.abi = abi;
this.abi = abi;
// Sanitize the name of the ABI to make it a valid class name
this.abi.name = abi.name.replace(/[!@#$%^&*()_+\-=[\]{};':"\\|,.<>/?]+/g, '_');
}
generateModuleImports() {
const imports = [
tsCodegen.moduleImports([
// Ethereum integration
'ethereum',
// Base classes
'JSONValue',
'TypedMap',
'Entity',
// AssemblyScript types
'Bytes',
'Address',
'BigInt',
], '@graphprotocol/graph-ts'),
];
if (doFixtureCodegen) {
imports.push(tsCodegen.moduleImports(['newMockEvent'], 'matchstick-as/assembly/index'));
}
return imports;
}
async generateTypes() {
return [
...(await this._generateEventTypes()),
...this._generateSmartContractClass(),
...this._generateCallTypes(),
];
}
_generateCallTypes() {
let callFunctions = util.disambiguateNames({
// @ts-expect-error improve typings of disambiguateNames to handle iterables
values: this.abi.callFunctions(),
getName: fn =>
// @ts-expect-error improve typings of disambiguateNames to handle iterables
fn.get('name') || (fn.get('type') === 'constructor' ? 'constructor' : 'default'),
// @ts-expect-error improve typings of disambiguateNames to handle iterables
setName: (fn, name) => fn.set('_alias', name),
});
callFunctions = callFunctions.map(fn => {
const fnAlias = fn.get('_alias');
const fnClassName = `${fnAlias.charAt(0).toUpperCase()}${fnAlias.slice(1)}Call`;
const tupleClasses = [];
// First, generate a class with the input getters
const inputsClassName = fnClassName + '__Inputs';
const inputsClass = tsCodegen.klass(inputsClassName, { export: true });
inputsClass.addMember(tsCodegen.klassMember('_call', fnClassName));
inputsClass.addMethod(tsCodegen.method(`constructor`, [tsCodegen.param(`call`, fnClassName)], null, `this._call = call`));
// Generate getters and classes for function inputs
util
.disambiguateNames({
values: fn.get('inputs', immutable.List()),
// @ts-expect-error improve typings of disambiguateNames to handle iterables
getName: (input, index) => input.get('name') || `value${index}`,
// @ts-expect-error improve typings of disambiguateNames to handle iterables
setName: (input, name) => input.set('name', name),
})
.forEach((input, index) => {
const callInput = this._generateInputOrOutput(input, index, fnClassName, `call`, `inputValues`);
inputsClass.addMethod(callInput.getter);
tupleClasses.push(...callInput.classes);
});
// Second, generate a class with the output getters
const outputsClassName = fnClassName + '__Outputs';
const outputsClass = tsCodegen.klass(outputsClassName, { export: true });
outputsClass.addMember(tsCodegen.klassMember('_call', fnClassName));
outputsClass.addMethod(tsCodegen.method(`constructor`, [tsCodegen.param(`call`, fnClassName)], null, `this._call = call`));
// Generate getters and classes for function outputs
util
.disambiguateNames({
values: fn.get('outputs', immutable.List()),
// @ts-expect-error improve typings of disambiguateNames to handle iterables
getName: (output, index) => output.get('name') || `value${index}`,
// @ts-expect-error improve typings of disambiguateNames to handle iterables
setName: (output, name) => output.set('name', name),
})
.forEach((output, index) => {
const callInput = this._generateInputOrOutput(output, index, fnClassName, `call`, `outputValues`);
outputsClass.addMethod(callInput.getter);
tupleClasses.push(...callInput.classes);
});
// Then, generate the event class itself
const klass = tsCodegen.klass(fnClassName, {
export: true,
extends: 'ethereum.Call',
});
klass.addMethod(tsCodegen.method(`get inputs`, [], tsCodegen.namedType(inputsClassName), `return new ${inputsClassName}(this)`));
klass.addMethod(tsCodegen.method(`get outputs`, [], tsCodegen.namedType(outputsClassName), `return new ${outputsClassName}(this)`));
return [klass, inputsClass, outputsClass, ...tupleClasses];
});
return callFunctions.reduce(
// flatten the array
(array, classes) => array.concat(classes), []);
}
async _generateEventTypes() {
// Enumerate events with duplicate names
let events = util.disambiguateNames({
// @ts-expect-error improve typings of disambiguateNames to handle iterables
values: this.abi.data.filter(member => member.get('type') === 'event'),
// @ts-expect-error improve typings of disambiguateNames to handle iterables
getName: event => event.get('name'),
// @ts-expect-error improve typings of disambiguateNames to handle iterables
setName: (event, name) => event.set('_alias', name),
});
events = events.map(async (event) => {
const eventClassName = event.get('_alias');
const tupleClasses = [];
// First, generate a class with the param getters
const paramsClassName = eventClassName + '__Params';
const paramsClass = tsCodegen.klass(paramsClassName, { export: true });
paramsClass.addMember(tsCodegen.klassMember('_event', eventClassName));
paramsClass.addMethod(tsCodegen.method(`constructor`, [tsCodegen.param(`event`, eventClassName)], null, `this._event = event`));
// Enumerate inputs with duplicate names
const inputs = util.disambiguateNames({
values: event.get('inputs'),
// @ts-expect-error improve typings of disambiguateNames to handle iterables
getName: (input, index) => input.get('name') || `param${index}`,
// @ts-expect-error improve typings of disambiguateNames to handle iterables
setName: (input, name) => input.set('name', name),
});
const namesAndTypes = [];
for (const [index, input] of inputs.entries()) {
// Generate getters and classes for event params
const paramObject = this._generateInputOrOutput(input, index, eventClassName, `event`, `parameters`);
paramsClass.addMethod(paramObject.getter);
// Fixture generation
if (doFixtureCodegen) {
let ethType = typesCodegen.ethereumTypeForAsc(String(paramObject.getter.returnType));
if (typeof ethType !== 'string' && (ethType.test('int256') || ethType.test('uint256'))) {
ethType = 'int32';
}
namesAndTypes.push({ name: paramObject.getter.name.slice(4), type: ethType });
}
tupleClasses.push(...paramObject.classes);
}
// Then, generate the event class itself
const klass = tsCodegen.klass(eventClassName, {
export: true,
extends: 'ethereum.Event',
});
klass.addMethod(tsCodegen.method(`get params`, [], tsCodegen.namedType(paramsClassName), `return new ${paramsClassName}(this)`));
// Fixture generation
if (doFixtureCodegen) {
const args = yaml.parse(fs.readFileSync('./fixtures.yaml', 'utf8'));
const blockNumber = args['blockNumber'];
const contractAddr = args['contractAddr'];
const topic0 = args['topic0'];
const apiKey = args['apiKey'];
const url = `https://api.etherscan.io/api?module=logs&action=getLogs&fromBlock=${blockNumber}&toBlock=${blockNumber}&address=${contractAddr}&${topic0}=topic0&apikey=${apiKey}`;
const resp = await fetch(url);
const body = JSON.parse(await resp.json());
if (body.status === '0') {
throw new Error(body.result);
}
const res = decodeLog(namesAndTypes, body.result[0].data, []);
let stmnts = '';
for (let i = 0; i < namesAndTypes.length; i++) {
let code = '"' + res[i] + '"';
if (namesAndTypes[i].type.toString() == 'address') {
code = `Address.fromString(${code})`;
}
stmnts = stmnts.concat(`event.parameters.push(new ethereum.EventParam("${namesAndTypes[i].name}", ${typesCodegen.ethereumFromAsc(code, namesAndTypes[i].type)}));`, `\n`);
}
klass.addMethod(tsCodegen.staticMethod(`mock${eventClassName}`, [], tsCodegen.namedType(eventClassName), `
let event = changetype<${eventClassName}>(newMockEvent());
${stmnts}
return event;
`));
}
return [klass, paramsClass, ...tupleClasses];
});
return Promise.all(events).then(events => events.reduce(
// flatten the array
(array, classes) => array.concat(classes), []));
}
_generateInputOrOutput(inputOrOutput, index, parentClass, parentType, parentField) {
// Get name and type of the param, adjusting for indexed params and missing names
let name = inputOrOutput.get('name');
const valueType = parentType === 'event' && inputOrOutput.get('indexed')
? this._indexedInputType(inputOrOutput.get('type'))
: inputOrOutput.get('type');
if (name === undefined || name === null || name === '') {
name = parentType === 'event' ? `param${index}` : `value${index}`;
}
// Generate getters and classes for the param (classes only created for Ethereum tuple types)
return util.containsTupleType(valueType)
? this._generateTupleType(inputOrOutput, index, parentClass, parentType, parentField)
: {
name: [],
getter: tsCodegen.method(`get ${name}`, [], typesCodegen.ascTypeForEthereum(valueType), `
return ${typesCodegen.ethereumToAsc(util.isTupleType(parentType)
? `this[${index}]`
: `this._${parentType}.${parentField}[${index}].value`, valueType)}
`),
classes: [],
};
}
_tupleTypeName(inputOrOutput, index, parentClass, parentType) {
return this._generateTupleType(inputOrOutput, index, parentClass, parentType, '').name;
}
_generateTupleType(inputOrOutput, index, parentClass, parentType, parentField) {
const type = inputOrOutput.get('type');
let name = inputOrOutput.get('name');
if (name === undefined || name === null || name === '') {
name = parentType === 'event' ? `param${index}` : `value${index}`;
}
const tupleIdentifier = parentClass + tsCodegen.namedType(name).capitalize();
const tupleClassName = tupleIdentifier + (parentField === 'outputValues' ? 'Output' : '') + 'Struct';
let tupleClasses = [];
const isTupleType = util.isTupleType(type);
const returnValue = typesCodegen.ethereumToAsc(parentType === 'tuple'
? `this[${index}]`
: `this._${parentType}.${parentField}[${index}].value`, type, tupleClassName);
// Generate getter for parent class
const tupleGetter = tsCodegen.method(`get ${name}`, [], util.isTupleMatrixType(type)
? `Array<Array<${tupleClassName}>>`
: util.isTupleArrayType(type)
? `Array<${tupleClassName}>`
: tupleClassName, `
return ${isTupleType ? `changetype<${tupleClassName}>(${returnValue})` : String(returnValue)}
`);
// Generate tuple class
const baseTupleClass = tsCodegen.klass(tupleClassName, {
export: true,
extends: 'ethereum.Tuple',
});
// Add param getters to tuple class and generate classes for each tuple parameter
inputOrOutput.get('components').forEach((component, index) => {
const paramObject = this._generateInputOrOutput(component, index, tupleIdentifier, `tuple`);
baseTupleClass.addMethod(paramObject.getter);
tupleClasses = tupleClasses.concat(paramObject.classes);
});
// Combine all tuple classes generated
tupleClasses.unshift(baseTupleClass);
return {
name: tupleClassName,
getter: tupleGetter,
classes: tupleClasses,
};
}
_generateSmartContractClass() {
const klass = tsCodegen.klass(this.abi.name, {
export: true,
extends: 'ethereum.SmartContract',
});
let types = immutable.List();
klass.addMethod(tsCodegen.staticMethod('bind',
// TODO: add support for iterable staticMethod params
immutable.List([
tsCodegen.param('address', typesCodegen.ascTypeForEthereum('address')),
]), tsCodegen.namedType(this.abi.name), `
return new ${this.abi.name}('${this.abi.name}', address);
`));
// Get view/pure functions from the contract
let functions = this.callableFunctions();
// Disambiguate functions with duplicate names
functions = util.disambiguateNames({
// @ts-expect-error improve typings of disambiguateNames to handle iterables
values: functions,
// @ts-expect-error improve typings of disambiguateNames to handle iterables
getName: fn => fn.get('name'),
// @ts-expect-error improve typings of disambiguateNames to handle iterables
setName: (fn, name) => fn.set('_alias', name),
});
for (const member of functions) {
const fnName = member.get('name');
const fnAlias = member.get('_alias');
const fnSignature = this.abi.functionSignature(member);
// Generate a type for the result of calling the function
let returnType = undefined;
let simpleReturnType = true;
const tupleResultParentType = this.abi.name + '__' + fnAlias + 'Result';
// Disambiguate outputs with duplicate names
const outputs = util.disambiguateNames({
values: member.get('outputs', immutable.List()),
// @ts-expect-error improve typings of disambiguateNames to handle iterables
getName: (input, index) => input.get('name') || `value${index}`,
// @ts-expect-error improve typings of disambiguateNames to handle iterables
setName: (input, name) => input.set('name', name),
});
if (member.get('outputs', immutable.List()).size > 1) {
simpleReturnType = false;
// Create a type dedicated to holding the return values
returnType = tsCodegen.klass(this.abi.name + '__' + fnAlias + 'Result', {
export: true,
});
// Add a constructor to this type
returnType.addMethod(tsCodegen.method('constructor', outputs.map((output, index) => tsCodegen.param(`value${index}`, this._getTupleParamType(output, index, tupleResultParentType))), null, outputs
.map((_output, index) => `this.value${index} = value${index}`)
.join('\n')));
// Add a `toMap(): TypedMap<string,ethereum.Value>` function to the return type
returnType.addMethod(tsCodegen.method('toMap', [], tsCodegen.namedType('TypedMap<string,ethereum.Value>'), `
let map = new TypedMap<string,ethereum.Value>();
${outputs
.map((output, index) => `map.set('value${index}', ${typesCodegen.ethereumFromAsc(`this.value${index}`, output.get('type'))})`)
.join(';')}
return map;
`));
// Add value0, value1 etc. members to the type
outputs
.map((output, index) => tsCodegen.klassMember(`value${index}`, this._getTupleParamType(output, index, tupleResultParentType)))
.forEach((member) => returnType.addMember(member));
// Add getters to the type
outputs
.map((output, index) => !!output.get('name') &&
tsCodegen.method(`get${output.get('name')[0].toUpperCase()}${output.get('name').slice(1)}`, [], this._getTupleParamType(output, index, tupleResultParentType), `return this.value${index};`))
.forEach((method) => !!method && returnType.addMethod(method));
// Create types for Tuple outputs
outputs.forEach((output, index) => {
if (util.containsTupleType(output.get('type'))) {
types = types.concat(this._generateTupleType(output, index, tupleResultParentType, 'function', this.abi.name).classes);
}
});
// Add the type to the types we'll create
types = types.push(returnType);
returnType = tsCodegen.namedType(returnType.name);
}
else {
const type = outputs.get(0).get('type');
if (util.containsTupleType(type)) {
// Add the Tuple type to the types we'll create
const tuple = this._generateTupleType(outputs.get(0), 0, tupleResultParentType, 'function', this.abi.name);
types = types.concat(tuple.classes);
returnType = util.isTupleType(type)
? tsCodegen.namedType(tuple.name)
: `Array<${tsCodegen.namedType(tuple.name)}>`;
}
else {
returnType = tsCodegen.namedType(typesCodegen.ascTypeForEthereum(type));
}
}
// Disambiguate inputs with duplicate names
const inputs = util.disambiguateNames({
values: member.get('inputs', immutable.List()),
// @ts-expect-error improve typings of disambiguateNames to handle iterables
getName: (input, index) => input.get('name') || `param${index}`,
// @ts-expect-error improve typings of disambiguateNames to handle iterables
setName: (input, name) => input.set('name', name),
});
// Generate a type prefix to identify the Tuple inputs to a function
const tupleInputParentType = this.abi.name + '__' + fnAlias + 'Input';
// Create types for Tuple inputs
inputs.forEach((input, index) => {
if (util.containsTupleType(input.get('type'))) {
types = types.concat(this._generateTupleType(input, index, tupleInputParentType, 'function', this.abi.name)
.classes);
}
});
// Generate and add a method that implements calling the function on
// the smart contract
const params = inputs.map((input, index) => tsCodegen.param(input.get('name'), this._getTupleParamType(input, index, tupleInputParentType)));
const superInputs = `
'${fnName}',
'${fnSignature}',
[${inputs.size > 0
? inputs
.map((input) => typesCodegen.ethereumFromAsc(input.get('name'), input.get('type')))
.map((coercion) => coercion.toString())
.join(', ')
: ''}]`;
const methodCallBody = (isTry) => {
const methodBody = `
${isTry
? `
let result = super.tryCall(${superInputs})
if (result.reverted) {
return new ethereum.CallResult()
}
let value = result.value
return ethereum.CallResult.fromValue(`
: `
let result = super.call(${superInputs})
return (`}`;
const returnVal = simpleReturnType
? typesCodegen.ethereumToAsc(isTry ? 'value[0]' : 'result[0]', outputs.get(0).get('type'), util.isTupleArrayType(outputs.get(0).get('type'))
? this._tupleTypeName(outputs.get(0), 0, tupleResultParentType, this.abi.name)
: '')
: `new ${returnType.name}(
${outputs
.map((output, index) => {
const val = typesCodegen.ethereumToAsc(isTry ? `value[${index}]` : `result[${index}]`, output.get('type'), util.isTupleArrayType(output.get('type'))
? this._tupleTypeName(output, index, tupleResultParentType, this.abi.name)
: '');
return util.isTupleType(output.get('type'))
? `changetype<${this._tupleTypeName(output, index, tupleResultParentType, this.abi.name)}>(${val})`
: val;
})
.join(', ')}
)`;
const isTuple = util.isTupleType(outputs.get(0).get('type'));
return `${methodBody} ${isTuple ? `changetype<${returnType}>(${returnVal})` : returnVal})`;
};
// Generate method with an without `try_`.
klass.addMethod(tsCodegen.method(fnAlias, params, returnType, methodCallBody(false)));
klass.addMethod(tsCodegen.method('try_' + fnAlias, params, 'ethereum.CallResult<' + returnType + '>', methodCallBody(true)));
}
return [...types, klass];
}
_getTupleParamType(inputOrOutput, index, tupleParentType) {
const type = inputOrOutput.get('type');
return util.isTupleType(type)
? this._tupleTypeName(inputOrOutput, index, tupleParentType, this.abi.name)
: util.isTupleMatrixType(type)
? `Array<Array<${this._tupleTypeName(inputOrOutput, index, tupleParentType, this.abi.name)}>>`
: util.isTupleArrayType(type)
? `Array<${this._tupleTypeName(inputOrOutput, index, tupleParentType, this.abi.name)}>`
: typesCodegen.ascTypeForEthereum(type);
}
_indexedInputType(inputType) {
// strings, bytes and arrays are encoded and hashed to a bytes32 value
if (inputType === 'string' ||
inputType === 'bytes' ||
inputType === 'tuple' ||
// the following matches arrays of the forms `uint256[]` and `uint256[12356789]`;
// the value type name doesn't matter here, just that the type name ends with
// brackets and, optionally, a number inside the brackets
inputType.match(/\[[0-9]*\]$/g)) {
return 'bytes32';
}
return inputType;
}
callableFunctions() {
const allowedMutability = ['view', 'pure', 'nonpayable', 'constant'];
return this.abi.data.filter(member => member.get('type') === 'function' &&
member.get('outputs', immutable.List()).size !== 0 &&
(allowedMutability.includes(member.get('stateMutability')) ||
(member.get('stateMutability') === undefined && !member.get('payable', false))));
}
}