@graphprotocol/graph-cli
Version:
CLI for building for and deploying to The Graph
732 lines (657 loc) • 22.7 kB
JavaScript
const fs = require('fs')
const yaml = require('yaml')
const request = require('sync-request')
const Web3EthAbi = require('web3-eth-abi')
const tsCodegen = require('../../../codegen/typescript')
const typesCodegen = require('../../../codegen/types')
const util = require('../../../codegen/util')
const doFixtureCodegen = fs.existsSync('./fixtures.yaml')
module.exports = class AbiCodeGenerator {
constructor(abi) {
this.abi = abi
}
generateModuleImports() {
let 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
}
generateTypes() {
return [
...this._generateEventTypes(),
...this._generateSmartContractClass(),
...this._generateCallTypes(),
]
}
_generateCallTypes() {
let callFunctions = util.disambiguateNames({
values: this.abi.callFunctions(),
getName: fn =>
fn.get('name') || (fn.get('type') === 'constructor' ? 'constructor' : 'default'),
setName: (fn, name) => fn.set('_alias', name),
})
callFunctions = callFunctions.map(fn => {
let fnAlias = fn.get('_alias')
let fnClassName = `${fnAlias.charAt(0).toUpperCase()}${fnAlias.slice(1)}Call`
let tupleClasses = []
// First, generate a class with the input getters
let inputsClassName = fnClassName + '__Inputs'
let 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', []),
getName: (input, index) => input.get('name') || `value${index}`,
setName: (input, name) => input.set('name', name),
})
.forEach((input, index) => {
let callInput = this._generateInputOrOutput(
input,
index,
fnClassName,
`call`,
`inputValues`,
)
inputsClass.addMethod(callInput.getter)
tupleClasses.push(...callInput.classes)
})
// Second, generate a class with the output getters
let outputsClassName = fnClassName + '__Outputs'
let 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', []),
getName: (output, index) => output.get('name') || `value${index}`,
setName: (output, name) => output.set('name', name),
})
.forEach((output, index) => {
let callInput = this._generateInputOrOutput(
output,
index,
fnClassName,
`call`,
`outputValues`,
)
outputsClass.addMethod(callInput.getter)
tupleClasses.push(...callInput.classes)
})
// Then, generate the event class itself
let 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),
[],
)
}
_generateEventTypes() {
// Enumerate events with duplicate names
let events = util.disambiguateNames({
values: this.abi.data.filter(member => member.get('type') === 'event'),
getName: event => event.get('name'),
setName: (event, name) => event.set('_alias', name),
})
events = events.map(event => {
let eventClassName = event.get('_alias')
let tupleClasses = []
// First, generate a class with the param getters
let paramsClassName = eventClassName + '__Params'
let 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
let inputs = util.disambiguateNames({
values: event.get('inputs'),
getName: (input, index) => input.get('name') || `param${index}`,
setName: (input, name) => input.set('name', name),
})
let namesAndTypes = []
inputs.forEach((input, index) => {
// Generate getters and classes for event params
let paramObject = this._generateInputOrOutput(
input,
index,
eventClassName,
`event`,
`parameters`,
)
paramsClass.addMethod(paramObject.getter)
// Fixture generation
if (doFixtureCodegen) {
let ethType = typesCodegen.ethereumTypeForAsc(paramObject.getter.returnType)
if (
typeof ethType === typeof {} &&
(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
let 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}`
let resp = request('GET', url)
let body = JSON.parse(resp.getBody('utf8'))
if (body.status === '0') {
throw new Error(body.result)
}
let res = Web3EthAbi.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 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')
let 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) {
let type = inputOrOutput.get('type')
let name = inputOrOutput.get('name')
if (name === undefined || name === null || name === '') {
name = parentType === 'event' ? `param${index}` : `value${index}`
}
let tupleIdentifier = parentClass + tsCodegen.namedType(name).capitalize()
let tupleClassName = tupleIdentifier + 'Struct'
let tupleClasses = []
let isTupleType = util.isTupleType(type)
let returnValue = typesCodegen.ethereumToAsc(
parentType === 'tuple'
? `this[${index}]`
: `this._${parentType}.${parentField}[${index}].value`,
type,
tupleClassName,
)
// Generate getter for parent class
let tupleGetter = tsCodegen.method(
`get ${name}`,
[],
util.isTupleMatrixType(type)
? `Array<Array<${tupleClassName}>>`
: util.isTupleArrayType(type)
? `Array<${tupleClassName}>`
: tupleClassName,
`
return ${
isTupleType ? `changetype<${tupleClassName}>(${returnValue})` : `${returnValue}`
}
`,
)
// Generate tuple class
let 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) => {
let name = component.get('name')
let 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() {
let klass = tsCodegen.klass(this.abi.name, {
export: true,
extends: 'ethereum.SmartContract',
})
let types = []
klass.addMethod(
tsCodegen.staticMethod(
'bind',
[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({
values: functions,
getName: fn => fn.get('name'),
setName: (fn, name) => fn.set('_alias', name),
})
functions.forEach(member => {
let fnName = member.get('name')
let fnAlias = member.get('_alias')
let fnSignature = this.abi.functionSignature(member)
// Generate a type for the result of calling the function
let returnType = undefined
let simpleReturnType = true
let tupleResultParentType = this.abi.name + '__' + fnAlias + 'Result'
// Disambiguate outputs with duplicate names
let outputs = util.disambiguateNames({
values: member.get('outputs', []),
getName: (input, index) => input.get('name') || `value${index}`,
setName: (input, name) => input.set('name', name),
})
if (member.get('outputs', []).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 {
let type = outputs.get(0).get('type')
if (util.containsTupleType(type)) {
// Add the Tuple type to the types we'll create
let 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
let inputs = util.disambiguateNames({
values: member.get('inputs', []),
getName: (input, index) => input.get('name') || `param${index}`,
setName: (input, name) => input.set('name', name),
})
// Generate a type prefix to identify the Tuple inputs to a function
let 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
let params = inputs.map((input, index) =>
tsCodegen.param(
input.get('name'),
this._getTupleParamType(input, index, tupleInputParentType),
),
)
let superInputs = `
'${fnName}',
'${fnSignature}',
[${
inputs.size > 0
? inputs
.map(input =>
typesCodegen.ethereumFromAsc(input.get('name'), input.get('type')),
)
.map(coercion => coercion.toString())
.join(', ')
: ''
}]`
let 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'
} else {
return inputType
}
}
callableFunctions() {
let allowedMutability = ['view', 'pure', 'nonpayable', 'constant']
return this.abi.data.filter(
member =>
member.get('type') === 'function' &&
member.get('outputs', []).size !== 0 &&
(allowedMutability.includes(member.get('stateMutability')) ||
(member.get('stateMutability') === undefined && !member.get('payable', false))),
)
}
}