Handle multiple return type contract functions in codegen (#369)

* Genrate schema GQL for multiple return types

* Generate indexer file for multiple return types

* Fix whitespaces in generated watcher

* Refactor storage mode queries after multiple return type changes
This commit is contained in:
Nabarun Gogoi 2023-04-25 18:18:01 +05:30 committed by GitHub
parent 096a0081e6
commit 11cab24505
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 195 additions and 108 deletions

View File

@ -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<Param>, typeName: any, stateVariableType?: string): void {
addQuery (
contract: string,
mode: string,
name: string,
params: Array<Param>,
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) === '_') {

View File

@ -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<Param>, typeName: any): void {
addQuery (name: string, params: Array<Param>): 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) => {

View File

@ -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<Param>, typeName: any): void {
addQuery (name: string, params: Array<Param>, 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<any, any> {
const value = `${typeName}!`;
_getOrCreateResultType (functionName: string, returnParameters: VariableDeclaration[]): ObjectTypeComposer<any, any> {
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')
});
}

View File

@ -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.

View File

@ -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}}

View File

@ -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;

View File

@ -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: