Skip to content

Commit

Permalink
Handle multiple return type contract functions in codegen (#369)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
nikugogoi authored Apr 25, 2023
1 parent 096a008 commit 11cab24
Show file tree
Hide file tree
Showing 7 changed files with 200 additions and 113 deletions.
51 changes: 38 additions & 13 deletions packages/codegen/src/indexer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -42,35 +44,58 @@ 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,
entityName: '',
getQueryName: '',
saveQueryName: '',
params: _.cloneDeep(params),
returnType: tsReturnType,
returnTypes,
mode,
stateVariableType,
contract,
disableCaching: isArray
disableCaching
};

if (name.charAt(0) === '_') {
Expand Down
9 changes: 2 additions & 7 deletions packages/codegen/src/resolvers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand All @@ -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) => {
Expand Down
72 changes: 56 additions & 16 deletions packages/codegen/src/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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] = {
Expand All @@ -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;
Expand Down Expand Up @@ -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);

// 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 = '';

let objectTCName = `Result${typeName}`;
if (isArray) {
objectTCName = objectTCName.concat('Array');
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')
});
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
38 changes: 25 additions & 13 deletions packages/codegen/src/templates/indexer-template.handlebars
Original file line number Diff line number Diff line change
Expand Up @@ -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}}
Expand Down
10 changes: 5 additions & 5 deletions packages/codegen/src/utils/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Loading

0 comments on commit 11cab24

Please sign in to comment.