diff --git a/packages/codegen/src/indexer.ts b/packages/codegen/src/indexer.ts index adc48e58..4ec46dc5 100644 --- a/packages/codegen/src/indexer.ts +++ b/packages/codegen/src/indexer.ts @@ -9,6 +9,8 @@ import Handlebars from 'handlebars'; import { Writable } from 'stream'; import _ from 'lodash'; +import { VariableDeclaration } from '@solidity-parser/parser/dist/src/ast-types'; + import { getGqlForSol, getTsForGql } from './utils/type-mappings'; import { Param } from './utils/types'; import { MODE_ETH_CALL, MODE_STORAGE } from './utils/constants'; @@ -42,23 +44,46 @@ export class Indexer { * @param returnType Return type for the query. * @param stateVariableType Type of the state variable in case of state variable query. */ - addQuery (contract: string, mode: string, name: string, params: Array, typeName: any, stateVariableType?: string): void { + addQuery ( + contract: string, + mode: string, + name: string, + params: Array, + returnParameters: VariableDeclaration[], + stateVariableType?: string + ): void { // Check if the query is already added. if (this._queries.some(query => query.name === name)) { return; } - const baseType = getBaseType(typeName); - assert(baseType); - const gqlReturnType = getGqlForSol(baseType); - assert(gqlReturnType); - let tsReturnType = getTsForGql(gqlReturnType); - assert(tsReturnType); + // Disable DB caching if more than 1 return params. + let disableCaching = returnParameters.length > 1; - const isArray = isArrayType(typeName); - if (isArray) { - tsReturnType = tsReturnType.concat('[]'); - } + const returnTypes = returnParameters.map(returnParameter => { + let typeName = returnParameter.typeName; + assert(typeName); + + // Handle Mapping type for state variable queries + while (typeName.type === 'Mapping') { + typeName = typeName.valueType; + } + + const baseType = getBaseType(typeName); + assert(baseType); + const gqlReturnType = getGqlForSol(baseType); + assert(gqlReturnType); + let tsReturnType = getTsForGql(gqlReturnType); + assert(tsReturnType); + + const isArray = isArrayType(typeName); + if (isArray) { + disableCaching = true; + tsReturnType = tsReturnType.concat('[]'); + } + + return tsReturnType; + }); const queryObject = { name, @@ -66,11 +91,11 @@ export class Indexer { getQueryName: '', saveQueryName: '', params: _.cloneDeep(params), - returnType: tsReturnType, + returnTypes, mode, stateVariableType, contract, - disableCaching: isArray + disableCaching }; if (name.charAt(0) === '_') { diff --git a/packages/codegen/src/resolvers.ts b/packages/codegen/src/resolvers.ts index 701dfedf..8e997721 100644 --- a/packages/codegen/src/resolvers.ts +++ b/packages/codegen/src/resolvers.ts @@ -11,7 +11,6 @@ import _ from 'lodash'; import { getGqlForSol, getTsForGql } from './utils/type-mappings'; import { Param } from './utils/types'; -import { getBaseType } from './utils/helpers'; const TEMPLATE_FILE = './templates/resolvers-template.handlebars'; @@ -30,20 +29,16 @@ export class Resolvers { * Stores the query to be passed to the template. * @param name Name of the query. * @param params Parameters to the query. - * @param returnType Return type for the query. */ - addQuery (name: string, params: Array, typeName: any): void { + addQuery (name: string, params: Array): void { // Check if the query is already added. if (this._queries.some(query => query.name === name)) { return; } - const returnType = getBaseType(typeName); - assert(returnType); const queryObject = { name, - params: _.cloneDeep(params), - returnType + params: _.cloneDeep(params) }; queryObject.params = queryObject.params.map((param) => { diff --git a/packages/codegen/src/schema.ts b/packages/codegen/src/schema.ts index 401056db..09dac1e7 100644 --- a/packages/codegen/src/schema.ts +++ b/packages/codegen/src/schema.ts @@ -7,6 +7,7 @@ import { GraphQLSchema, parse, printSchema, print, GraphQLDirective, GraphQLInt, import { ObjectTypeComposer, ObjectTypeComposerDefinition, ObjectTypeComposerFieldConfigMapDefinition, SchemaComposer } from 'graphql-compose'; import { Writable } from 'stream'; import { utils } from 'ethers'; +import { VariableDeclaration } from '@solidity-parser/parser/dist/src/ast-types'; import { getGqlForTs, getGqlForSol } from './utils/type-mappings'; import { Param } from './utils/types'; @@ -30,21 +31,13 @@ export class Schema { * @param params Parameters to the query. * @param returnType Return type for the query. */ - addQuery (name: string, params: Array, typeName: any): void { + addQuery (name: string, params: Array, returnParameters: VariableDeclaration[]): void { // Check if the query is already added. if (this._composer.Query.hasField(name)) { return; } - // TODO: Handle cases where returnType/params type is an array. - const isReturnTypeArray = isArrayType(typeName); - const baseTypeName = getBaseType(typeName); - assert(baseTypeName); - - const gqlReturnType = getGqlForSol(baseTypeName); - assert(gqlReturnType, `gql type for sol type ${baseTypeName} for ${name} not found`); - - const objectTC = this._getOrCreateResultType(gqlReturnType, isReturnTypeArray); + const objectTC = this._getOrCreateResultType(name, returnParameters); const queryObject: { [key: string]: any; } = {}; queryObject[name] = { @@ -57,6 +50,7 @@ export class Schema { }; if (params.length > 0) { + // TODO: Handle cases where params type is an array. queryObject[name].args = params.reduce((acc, curr) => { acc[curr.name] = `${getGqlForSol(curr.type)}!`; return acc; @@ -242,19 +236,65 @@ export class Schema { /** * Adds Result types to the schema and typemapping. */ - _getOrCreateResultType (typeName: string, isArray = false): ObjectTypeComposer { - const value = `${typeName}!`; + _getOrCreateResultType (functionName: string, returnParameters: VariableDeclaration[]): ObjectTypeComposer { + const returnValueTypes = returnParameters.map((returnParameter) => { + let typeName = returnParameter.typeName; + assert(typeName); - let objectTCName = `Result${typeName}`; - if (isArray) { - objectTCName = objectTCName.concat('Array'); + // Handle Mapping type for state variable queries + while (typeName.type === 'Mapping') { + typeName = typeName.valueType; + } + + const isReturnTypeArray = isArrayType(typeName); + const baseTypeName = getBaseType(typeName); + assert(baseTypeName); + + const gqlReturnType = getGqlForSol(baseTypeName); + assert(gqlReturnType, `gql type for sol type ${baseTypeName} for ${functionName} not found`); + + return { + type: gqlReturnType, + isArray: isReturnTypeArray + }; + }); + + let objectTCName = 'Result'; + let value = ''; + + if (returnParameters.length > 1) { + const returnValueTypesMap = returnParameters.reduce((acc: {[key: string]: string}, _, index) => { + const { type, isArray } = returnValueTypes[index]; + acc[`value${index}`] = (isArray) ? `[${type}!]!` : `${type}!`; + return acc; + }, {}); + + const capitalizedFunctionName = `${functionName.charAt(0).toUpperCase()}${functionName.slice(1)}`; + + this._composer.getOrCreateOTC( + `${capitalizedFunctionName}Type`, + (tc) => { + tc.addFields(returnValueTypesMap); + } + ); + + objectTCName = objectTCName.concat(`${capitalizedFunctionName}Type`); + value = `${capitalizedFunctionName}Type!`; + } else { + const { type, isArray } = returnValueTypes[0]; + value = (isArray) ? `[${type}!]!` : `${type}!`; + objectTCName = objectTCName.concat(type); + + if (isArray) { + objectTCName = objectTCName.concat('Array'); + } } const typeComposer = this._composer.getOrCreateOTC( objectTCName, (tc) => { tc.addFields({ - value: (isArray) ? `[${value}]!` : value, + value, proof: () => this._composer.getOTC('Proof') }); } diff --git a/packages/codegen/src/templates/database-template.handlebars b/packages/codegen/src/templates/database-template.handlebars index 86bd860b..413747b1 100644 --- a/packages/codegen/src/templates/database-template.handlebars +++ b/packages/codegen/src/templates/database-template.handlebars @@ -37,7 +37,7 @@ export const SUBGRAPH_ENTITIES = new Set([ {{~/each}}]); {{/if}} export const ENTITIES = [ - {{~#each queries as | query |}}{{query.entityName}}{{#unless @last}}, {{/unless}}{{/each}}{{#if (subgraphPath)}}, {{/if}} + {{~#each queries as | query |}}{{query.entityName}}{{#if @last}}{{#if (subgraphPath)}}, {{/if}}{{else}}, {{/if}}{{/each}} {{~#if (subgraphPath)}}...SUBGRAPH_ENTITIES{{/if}}]; {{#if (subgraphPath)}} // Map: Entity to suitable query type. diff --git a/packages/codegen/src/templates/indexer-template.handlebars b/packages/codegen/src/templates/indexer-template.handlebars index 2e804a4e..e453159b 100644 --- a/packages/codegen/src/templates/indexer-template.handlebars +++ b/packages/codegen/src/templates/indexer-template.handlebars @@ -201,22 +201,34 @@ export class Indexer implements IndexerInterface { assert(abi); const contract = new ethers.Contract(contractAddress, abi, this._ethProvider); - {{#if (compare query.returnType 'bigint')}} - let value = await contract.{{query.name}}( + const contractResult = await contract.{{query.name}}( {{~#each query.params}}{{this.name}}, {{/each}}{ blockTag: blockHash }); - value = value.toString(); - value = BigInt(value); + + {{#if (compare query.returnTypes.length 1 operator=">")}} + const value = { + {{#each query.returnTypes as |returnType index|}} + {{#if (compare returnType 'bigint')}} + value{{index}}: ethers.BigNumber.from(contractResult[{{index}}]).toBigInt() + {{~else}} + {{!-- https://github.com/handlebars-lang/handlebars.js/issues/1716 --}} + {{#if (compare returnType 'bigint[]')}} + value{{index}}: contractResult[{{index}}].map((val: ethers.BigNumber | number) => ethers.BigNumber.from(val).toBigInt()) + {{~else}} + value{{index}}: contractResult[{{index}}] + {{~/if}} + {{/if}} + {{~#unless @last}},{{/unless}} + {{/each}} + }; {{else}} - {{!-- Using nested if-else to avoid indentation issue --}} - {{#if (compare query.returnType 'bigint[]')}} - let value = await contract.{{query.name}}( - {{~#each query.params}}{{this.name}}, {{/each}}{ blockTag: blockHash }); - value = value.map((val: ethers.BigNumber) => ethers.BigNumber.from(val).toBigInt()); + {{#if (compare query.returnTypes.[0] 'bigint')}} + const value = ethers.BigNumber.from(contractResult).toBigInt(); + {{else if (compare query.returnTypes.[0] 'bigint[]')}} + const value = contractResult.map((val: ethers.BigNumber | number) => ethers.BigNumber.from(val).toBigInt()); {{else}} - const value = await contract.{{query.name}}( - {{~#each query.params}}{{this.name}}, {{/each}}{ blockTag: blockHash }); - {{/if}} - {{/if}} + const value = contractResult; + {{~/if}} + {{~/if}} const result: ValueResult = { value }; {{/if}} diff --git a/packages/codegen/src/utils/helpers.ts b/packages/codegen/src/utils/helpers.ts index 857afb9a..6bae01ee 100644 --- a/packages/codegen/src/utils/helpers.ts +++ b/packages/codegen/src/utils/helpers.ts @@ -4,14 +4,14 @@ import fs from 'fs'; import { Writable } from 'stream'; +import { TypeName } from '@solidity-parser/parser/dist/src/ast-types'; -const isElementaryType = (typeName: any): boolean => (typeName.type === 'ElementaryTypeName'); -export const isArrayType = (typeName: any): boolean => (typeName.type === 'ArrayTypeName'); +export const isArrayType = (typeName: TypeName): boolean => (typeName.type === 'ArrayTypeName'); -export const getBaseType = (typeName: any): string | undefined => { - if (isElementaryType(typeName)) { +export const getBaseType = (typeName: TypeName): string | undefined => { + if (typeName.type === 'ElementaryTypeName') { return typeName.name; - } else if (isArrayType(typeName)) { + } else if (typeName.type === 'ArrayTypeName') { return getBaseType(typeName.baseTypeName); } else { return undefined; diff --git a/packages/codegen/src/visitor.ts b/packages/codegen/src/visitor.ts index 28bd3890..3fb1f665 100644 --- a/packages/codegen/src/visitor.ts +++ b/packages/codegen/src/visitor.ts @@ -5,6 +5,7 @@ import { Writable } from 'stream'; import assert from 'assert'; import { utils } from 'ethers'; +import { FunctionDefinition, StateVariableDeclaration } from '@solidity-parser/parser/dist/src/ast-types'; import { Database } from './database'; import { Entity } from './entity'; @@ -51,51 +52,58 @@ export class Visitor { * Visitor function for function definitions. * @param node ASTNode for a function definition. */ - functionDefinitionVisitor (node: any): void { - if (node.stateMutability === 'view' && (node.visibility === 'external' || node.visibility === 'public')) { - const name = node.name; - const params = node.parameters.map((item: any) => { - return { name: item.name, type: item.typeName.name }; - }); + functionDefinitionVisitor (node: FunctionDefinition): void { + if (node.stateMutability !== 'view' || !(node.visibility === 'external' || node.visibility === 'public')) { + return; + } - let errorMessage = ''; + // If function doesn't return anything skip creating watcher query + if (!node.returnParameters) { + return; + } - if (node.returnParameters.length > 1) { - errorMessage = `No support in codegen for multiple returned values from method ${node.name}`; - } else { - const typeName = node.returnParameters[0].typeName; - switch (typeName.type) { - case 'ElementaryTypeName': - this._entity.addQuery(name, params, typeName); - this._database.addQuery(name, params, typeName); - this._client.addQuery(name, params, typeName); - // falls through + const name = node.name; + assert(name); - case 'ArrayTypeName': - this._schema.addQuery(name, params, typeName); - this._resolvers.addQuery(name, params, typeName); + const params = node.parameters.map((item: any) => { + return { name: item.name, type: item.typeName.name }; + }); - assert(this._contract); - this._indexer.addQuery(this._contract.name, MODE_ETH_CALL, name, params, typeName); - break; + let errorMessage = ''; - case 'UserDefinedTypeName': - errorMessage = `No support in codegen for user defined return type from method "${node.name}"`; - break; + const typeName = node.returnParameters[0].typeName; + assert(typeName); - default: - errorMessage = `No support in codegen for return type "${typeName.type}" from method "${node.name}"`; - } + switch (typeName.type) { + case 'ElementaryTypeName': + this._entity.addQuery(name, params, typeName); + this._database.addQuery(name, params, typeName); + this._client.addQuery(name, params, typeName); + // falls through + + case 'ArrayTypeName': + this._schema.addQuery(name, params, node.returnParameters); + this._resolvers.addQuery(name, params); + + assert(this._contract); + this._indexer.addQuery(this._contract.name, MODE_ETH_CALL, name, params, node.returnParameters); + break; + + case 'UserDefinedTypeName': + errorMessage = `No support in codegen for user defined return type from method "${node.name}"`; + break; + + default: + errorMessage = `No support in codegen for return type "${typeName.type}" from method "${node.name}"`; + } + + if (errorMessage !== '') { + if (this._continueOnError) { + console.log(errorMessage); + return; } - if (errorMessage !== '') { - if (this._continueOnError) { - console.log(errorMessage); - return; - } - - throw new Error(errorMessage); - } + throw new Error(errorMessage); } } @@ -103,12 +111,14 @@ export class Visitor { * Visitor function for state variable declarations. * @param node ASTNode for a state variable declaration. */ - stateVariableDeclarationVisitor (node: any): void { + stateVariableDeclarationVisitor (node: StateVariableDeclaration): void { // TODO Handle multiples variables in a single line. // TODO Handle array types. // TODO Handle user defined type . const variable = node.variables[0]; + assert(variable.name); const name: string = variable.name; + assert(variable.typeName); const stateVariableType: string = variable.typeName.type; const params: Param[] = []; @@ -118,38 +128,43 @@ export class Visitor { } let typeName = variable.typeName; - let numParams = 0; - - // If the variable type is mapping, extract key as a param: - // Eg. mapping(address => mapping(address => uint256)) private _allowances; - while (typeName.type === 'Mapping') { - params.push({ name: `key${numParams.toString()}`, type: typeName.keyType.name }); - typeName = typeName.valueType; - numParams++; - } - let errorMessage = ''; switch (typeName.type) { + case 'Mapping': { + let numParams = 0; + + // If the variable type is mapping, extract key as a param: + // Eg. mapping(address => mapping(address => uint256)) private _allowances; + while (typeName.type === 'Mapping') { + assert(typeName.keyType.type === 'ElementaryTypeName', 'UserDefinedTypeName map keys like enum type not handled'); + params.push({ name: `key${numParams.toString()}`, type: typeName.keyType.name }); + typeName = typeName.valueType; + numParams++; + } + + // falls through + } + case 'ElementaryTypeName': { - this._schema.addQuery(name, params, typeName); - this._resolvers.addQuery(name, params, typeName); + this._schema.addQuery(name, params, [variable]); + this._resolvers.addQuery(name, params); this._entity.addQuery(name, params, typeName); this._database.addQuery(name, params, typeName); this._client.addQuery(name, params, typeName); assert(this._contract); - this._indexer.addQuery(this._contract.name, MODE_STORAGE, name, params, typeName, stateVariableType); + this._indexer.addQuery(this._contract.name, MODE_STORAGE, name, params, [variable], stateVariableType); break; } case 'UserDefinedTypeName': - errorMessage = `No support in codegen for user defined return type from method "${name}"`; + errorMessage = `No support in codegen for user defined type state variable "${name}"`; break; case 'ArrayTypeName': - errorMessage = `No support in codegen for return type "${typeName.baseTypeName.name}[]" from method "${name}"`; + errorMessage = `No support in codegen for array type state variable "${name}"`; break; default: