mirror of
https://github.com/cerc-io/watcher-ts
synced 2025-01-06 19:38:05 +00:00
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:
parent
096a0081e6
commit
11cab24505
@ -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) === '_') {
|
||||
|
@ -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) => {
|
||||
|
@ -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')
|
||||
});
|
||||
}
|
||||
|
@ -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.
|
||||
|
@ -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}}
|
||||
|
@ -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;
|
||||
|
@ -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:
|
||||
|
Loading…
Reference in New Issue
Block a user