From 8b3003099dd9e98bfd532a541963bd31c3990773 Mon Sep 17 00:00:00 2001 From: therealemjy Date: Mon, 3 Jul 2023 11:48:20 +0700 Subject: [PATCH] refactor logic to define call arguments so they are automatically ordered correctly --- .changeset/fresh-pans-jump.md | 5 ++ .../e2e-tests/test/finalBossCallFragment.ts | 42 +++++----- packages/e2e-tests/test/index.ts | 20 ++--- .../eth-graphql/src/createSchema/constants.ts | 4 + .../eth-graphql/src/createSchema/index.ts | 14 ++-- .../formatArgumentNodes.test.ts.snap | 21 +++++ .../formatGraphQlArgs.test.ts.snap | 22 ------ ...gs.test.ts => formatArgumentNodes.test.ts} | 6 +- .../makeCalls/formatArgumentNodes.ts | 69 +++++++++++++++++ .../makeCalls/formatGraphQlArgs.ts | 76 +++++-------------- .../src/createSchema/makeCalls/index.ts | 25 +++--- .../src/createSchema/makeCalls/types.ts | 6 +- 12 files changed, 182 insertions(+), 128 deletions(-) create mode 100644 .changeset/fresh-pans-jump.md create mode 100644 packages/eth-graphql/src/createSchema/constants.ts create mode 100644 packages/eth-graphql/src/createSchema/makeCalls/__tests__/__snapshots__/formatArgumentNodes.test.ts.snap delete mode 100644 packages/eth-graphql/src/createSchema/makeCalls/__tests__/__snapshots__/formatGraphQlArgs.test.ts.snap rename packages/eth-graphql/src/createSchema/makeCalls/__tests__/{formatGraphQlArgs.test.ts => formatArgumentNodes.test.ts} (95%) create mode 100644 packages/eth-graphql/src/createSchema/makeCalls/formatArgumentNodes.ts diff --git a/.changeset/fresh-pans-jump.md b/.changeset/fresh-pans-jump.md new file mode 100644 index 0000000..cbeedea --- /dev/null +++ b/.changeset/fresh-pans-jump.md @@ -0,0 +1,5 @@ +--- +"eth-graphql": patch +--- + +Fix issue with argument order diff --git a/packages/e2e-tests/test/finalBossCallFragment.ts b/packages/e2e-tests/test/finalBossCallFragment.ts index 8637706..b4a7822 100644 --- a/packages/e2e-tests/test/finalBossCallFragment.ts +++ b/packages/e2e-tests/test/finalBossCallFragment.ts @@ -6,29 +6,29 @@ export const FINAL_BOSS_CALL_FRAGMENT = /* GraphQL */ ` } movies(arg0: 0) { id - title - status director { name walletAddress } + status + title } getAllMovies { id - title status director { name walletAddress } + title } getMultipleValues { value0 - value1 value2 { name walletAddress } + value1 } getNothing getString @@ -44,10 +44,10 @@ export const FINAL_BOSS_CALL_FRAGMENT = /* GraphQL */ ` value1 } overloadedFn1 { - value0 value1 + value0 } - overloadedFn2(arg0: "some-string", arg1: "10000000000000000000", arg2: "") { + overloadedFn2(arg1: "10000000000000000000", arg0: "some-string", arg2: "") { value0 value1 } @@ -64,12 +64,12 @@ export const FINAL_BOSS_CALL_FRAGMENT = /* GraphQL */ ` passMovie( someMovie: { id: "0" - title: "fake movie" - status: "1" director: { name: "fake director" walletAddress: "0xA5ae0b2386De51Aba852551A1EE828BfD598E111" } + status: "1" + title: "fake movie" } ) { id @@ -134,8 +134,8 @@ export const FINAL_BOSS_CALL_FRAGMENT = /* GraphQL */ ` } mayhem_MULT(args: [{ arg0: "fake string 0" - someUint: 671432189 arg2: ["1000000000000000000", "200000000000000000"] + someUint: 671432189 someMovies: [ { id: "0" @@ -156,6 +156,7 @@ export const FINAL_BOSS_CALL_FRAGMENT = /* GraphQL */ ` } } ] + someStatuses: [0, 1, 1, 1] someDirectors: [ { name: "fake director 0" @@ -166,11 +167,20 @@ export const FINAL_BOSS_CALL_FRAGMENT = /* GraphQL */ ` walletAddress: "0xA5ae0b2386De51Aba852551A1EE828BfD598E111" } ] - someStatuses: [0, 1, 1, 1] }, { - arg0: "fake string 1" someUint: 671432189 arg2: ["3000000000000000000", "400000000000000000"] + arg0: "fake string 1" + someDirectors: [ + { + name: "fake director 2" + walletAddress: "0xA5ae0b2386De51Aba852551A1EE828BfD598E111" + } + { + name: "fake director 3" + walletAddress: "0xA5ae0b2386De51Aba852551A1EE828BfD598E111" + } + ] someMovies: [ { id: "2" @@ -191,16 +201,6 @@ export const FINAL_BOSS_CALL_FRAGMENT = /* GraphQL */ ` } } ] - someDirectors: [ - { - name: "fake director 2" - walletAddress: "0xA5ae0b2386De51Aba852551A1EE828BfD598E111" - } - { - name: "fake director 3" - walletAddress: "0xA5ae0b2386De51Aba852551A1EE828BfD598E111" - } - ] someStatuses: [1, 0, 1, 0] }]) { passedMovies { diff --git a/packages/e2e-tests/test/index.ts b/packages/e2e-tests/test/index.ts index 8b47bc9..e850d27 100644 --- a/packages/e2e-tests/test/index.ts +++ b/packages/e2e-tests/test/index.ts @@ -303,14 +303,14 @@ describe('end-to-end tests', function () { value0 value1 } - overloadedFn2(arg0: "some-string", arg1: "10000000000000000000", arg2: "") { + overloadedFn2(arg0: "some-string", arg2: "", arg1: "10000000000000000000") { value0 value1 } overloadedFn2_MULT( args: [ - { arg0: "some-string-0", arg1: "10000000000000000000", arg2: "1" } - { arg0: "some-string-1", arg1: "20000000000000000000", arg2: "8" } + { arg1: "10000000000000000000", arg0: "some-string-0", arg2: "1" } + { arg2: "8", arg0: "some-string-1", arg1: "20000000000000000000" } ] ) { value0 @@ -325,13 +325,13 @@ describe('end-to-end tests', function () { const data = await makeQuery(/* GraphQL */ ` passMovie( someMovie: { - id: "0" - title: "fake movie" status: "1" + id: "0" director: { name: "fake director" walletAddress: "0xA5ae0b2386De51Aba852551A1EE828BfD598E111" } + title: "fake movie" } ) { id @@ -346,24 +346,24 @@ describe('end-to-end tests', function () { args: [ { someMovie: { - id: "0" - title: "fake movie 0" - status: "1" director: { name: "fake director 0" walletAddress: "0xA5ae0b2386De51Aba852551A1EE828BfD598E111" } + id: "0" + status: "1" + title: "fake movie 0" } } { someMovie: { - id: "1" title: "fake movie 1" - status: "0" + id: "1" director: { name: "fake director 1" walletAddress: "0xA5ae0b2386De51Aba852551A1EE828BfD598E111" } + status: "0" } } ] diff --git a/packages/eth-graphql/src/createSchema/constants.ts b/packages/eth-graphql/src/createSchema/constants.ts new file mode 100644 index 0000000..430929a --- /dev/null +++ b/packages/eth-graphql/src/createSchema/constants.ts @@ -0,0 +1,4 @@ +export const ROOT_FIELD_NAME = 'contracts'; +export const ROOT_FIELD_SINGLE_ARGUMENT_NAME = 'addresses'; +export const MULT_FIELD_SUFFIX = '_MULT'; +export const MULT_FIELD_SINGLE_ARGUMENT_NAME = 'args'; diff --git a/packages/eth-graphql/src/createSchema/index.ts b/packages/eth-graphql/src/createSchema/index.ts index 6c98a0e..80a0d7e 100644 --- a/packages/eth-graphql/src/createSchema/index.ts +++ b/packages/eth-graphql/src/createSchema/index.ts @@ -13,6 +13,12 @@ import { import EthGraphQlError from '../EthGraphQlError'; import { Config, SolidityValue } from '../types'; import filterAbiItems from '../utilities/filterAbiItems'; +import { + MULT_FIELD_SINGLE_ARGUMENT_NAME, + MULT_FIELD_SUFFIX, + ROOT_FIELD_NAME, + ROOT_FIELD_SINGLE_ARGUMENT_NAME, +} from './constants'; import createGraphQlInputTypes from './createGraphQlInputTypes'; import createGraphQlMultInputTypes from './createGraphQlMultInputTypes'; import createGraphQlMultOutputType from './createGraphQlMultOutputType'; @@ -23,8 +29,6 @@ import resolve from './resolve'; import { FieldNameMapping, SharedGraphQlTypes } from './types'; import validateConfig from './validateConfig'; -const ROOT_FIELD_NAME = 'contracts'; -const MULT_CONTRACT_FIELD_SUFFIX = '_MULT'; const addressesGraphQlInputType = new GraphQLNonNull( new GraphQLList(new GraphQLNonNull(GraphQLString)), ); @@ -102,7 +106,7 @@ const createSchema = (config: Config) => { // Handle argument types if (abiInputs.length > 0) { - contractField.args = createGraphQlInputTypes({ + contractField[MULT_FIELD_SINGLE_ARGUMENT_NAME] = createGraphQlInputTypes({ components: abiInputs, contractName: contract.name, sharedGraphQlTypes, @@ -110,7 +114,7 @@ const createSchema = (config: Config) => { // Create a MULT field that can be used to call the same method // with multiple sets of arguments - const multContractFieldName = `${contractFieldName}${MULT_CONTRACT_FIELD_SUFFIX}`; + const multContractFieldName = `${contractFieldName}${MULT_FIELD_SUFFIX}`; // Add MULT field to field mapping fieldMapping[contract.name][multContractFieldName] = { @@ -154,7 +158,7 @@ const createSchema = (config: Config) => { // wish to put the addresses of inside the config. if (!contract.address) { contractType.args = { - addresses: { type: addressesGraphQlInputType }, + [ROOT_FIELD_SINGLE_ARGUMENT_NAME]: { type: addressesGraphQlInputType }, }; // Transform type into a list to reflect the fact an array of results diff --git a/packages/eth-graphql/src/createSchema/makeCalls/__tests__/__snapshots__/formatArgumentNodes.test.ts.snap b/packages/eth-graphql/src/createSchema/makeCalls/__tests__/__snapshots__/formatArgumentNodes.test.ts.snap new file mode 100644 index 0000000..c02e392 --- /dev/null +++ b/packages/eth-graphql/src/createSchema/makeCalls/__tests__/__snapshots__/formatArgumentNodes.test.ts.snap @@ -0,0 +1,21 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`createSchema/makeCalls/formatArgumentNodes converts argument nodes to valid arguments 1`] = ` +{ + "fake name 0": [ + [ + "fake string 0", + ], + ], + "fake name 1": { + "fake name 2": { + "fake name 3": "fake string 1", + }, + }, + "fake name 4": "fake string 2", + "fake name 5": true, + "fake name 6": "100", + "fake name 7": "0", + "fake name 8": "fake variable value", +} +`; diff --git a/packages/eth-graphql/src/createSchema/makeCalls/__tests__/__snapshots__/formatGraphQlArgs.test.ts.snap b/packages/eth-graphql/src/createSchema/makeCalls/__tests__/__snapshots__/formatGraphQlArgs.test.ts.snap deleted file mode 100644 index cdd7bee..0000000 --- a/packages/eth-graphql/src/createSchema/makeCalls/__tests__/__snapshots__/formatGraphQlArgs.test.ts.snap +++ /dev/null @@ -1,22 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`createSchema/makeCalls/formatGraphQlArgs converts argument nodes to valid arguments 1`] = ` -[ - [ - [ - "fake string 0", - ], - ], - [ - [ - "fake string 1", - ], - ], - "fake string 2", - true, - "100", - "0.1", - "0", - "fake variable value", -] -`; diff --git a/packages/eth-graphql/src/createSchema/makeCalls/__tests__/formatGraphQlArgs.test.ts b/packages/eth-graphql/src/createSchema/makeCalls/__tests__/formatArgumentNodes.test.ts similarity index 95% rename from packages/eth-graphql/src/createSchema/makeCalls/__tests__/formatGraphQlArgs.test.ts rename to packages/eth-graphql/src/createSchema/makeCalls/__tests__/formatArgumentNodes.test.ts index 45d9134..9d60aa7 100644 --- a/packages/eth-graphql/src/createSchema/makeCalls/__tests__/formatGraphQlArgs.test.ts +++ b/packages/eth-graphql/src/createSchema/makeCalls/__tests__/formatArgumentNodes.test.ts @@ -1,8 +1,8 @@ import { ArgumentNode, Kind } from 'graphql'; -import formatGraphQlArgs from '../formatGraphQlArgs'; +import formatArgumentNodes from '../formatArgumentNodes'; -describe('createSchema/makeCalls/formatGraphQlArgs', () => { +describe('createSchema/makeCalls/formatArgumentNodes', () => { it('converts argument nodes to valid arguments', () => { const fakeVariableName = 'fakeVariable'; const argumentNodes: ArgumentNode[] = [ @@ -133,7 +133,7 @@ describe('createSchema/makeCalls/formatGraphQlArgs', () => { }, ]; - const res = formatGraphQlArgs({ + const res = formatArgumentNodes({ argumentNodes, variableValues: { [fakeVariableName]: 'fake variable value', diff --git a/packages/eth-graphql/src/createSchema/makeCalls/formatArgumentNodes.ts b/packages/eth-graphql/src/createSchema/makeCalls/formatArgumentNodes.ts new file mode 100644 index 0000000..b1d1735 --- /dev/null +++ b/packages/eth-graphql/src/createSchema/makeCalls/formatArgumentNodes.ts @@ -0,0 +1,69 @@ +import { ArgumentNode, GraphQLResolveInfo, Kind, ValueNode } from 'graphql'; + +import { SolidityValue } from '../../types'; +import { ContractCallArgs } from './types'; + +// Extract a node's value +const getNodeValue = ({ + valueNode, + variableValues, +}: { + valueNode: ValueNode; + variableValues: GraphQLResolveInfo['variableValues']; +}): SolidityValue => { + if (valueNode.kind === Kind.NULL) { + return null; + } + + // Get variable node value + if (valueNode.kind === Kind.VARIABLE) { + const variableName = valueNode.name.value; + return variableValues[variableName] as SolidityValue; + } + + // Convert list to array + if (valueNode.kind === Kind.LIST) { + return valueNode.values.map(node => + getNodeValue({ + valueNode: node, + variableValues, + }), + ); + } + + // Convert object to object + if (valueNode.kind === Kind.OBJECT) { + return valueNode.fields.reduce( + (accObject, field) => ({ + ...accObject, + [field.name.value]: getNodeValue({ + valueNode: field.value, + variableValues, + }), + }), + {}, + ); + } + + return valueNode.value; +}; + +const formatArgumentNodes = ({ + argumentNodes, + variableValues, +}: { + argumentNodes: ReadonlyArray; + variableValues: GraphQLResolveInfo['variableValues']; +}) => + argumentNodes.reduce( + (accArguments, argument) => ({ + ...accArguments, + [argument.name.value]: getNodeValue({ + valueNode: argument.value, + variableValues, + }), + }), + {}, + ); + +export default formatArgumentNodes; diff --git a/packages/eth-graphql/src/createSchema/makeCalls/formatGraphQlArgs.ts b/packages/eth-graphql/src/createSchema/makeCalls/formatGraphQlArgs.ts index a64ff1a..a6fb1f6 100644 --- a/packages/eth-graphql/src/createSchema/makeCalls/formatGraphQlArgs.ts +++ b/packages/eth-graphql/src/createSchema/makeCalls/formatGraphQlArgs.ts @@ -1,64 +1,26 @@ -import { ArgumentNode, GraphQLResolveInfo, Kind, ValueNode } from 'graphql'; +import { JsonFragmentType } from '@ethersproject/abi'; -import { SolidityValue } from '../../types'; - -// Extract a node's value -const getNodeValue = ({ - valueNode, - variableValues, -}: { - valueNode: ValueNode; - variableValues: GraphQLResolveInfo['variableValues']; -}): SolidityValue => { - if (valueNode.kind === Kind.NULL) { - return null; - } - - // Get variable node value - if (valueNode.kind === Kind.VARIABLE) { - const variableName = valueNode.name.value; - return variableValues[variableName] as SolidityValue; - } - - // Convert list to array - if (valueNode.kind === Kind.LIST) { - return valueNode.values.map(node => - getNodeValue({ - valueNode: node, - variableValues, - }), - ); - } - - // Convert object to tuple - if (valueNode.kind === Kind.OBJECT) { - return valueNode.fields.map(field => - getNodeValue({ - valueNode: field.value, - variableValues, - }), - ); - } - - return valueNode.value; -}; +import formatToEntityName from '../../utilities/formatToEntityName'; +import { ContractCallArgs } from './types'; const formatGraphQlArgs = ({ - argumentNodes, - variableValues, + callArguments, + abiItemInputs, }: { - argumentNodes: ReadonlyArray; - variableValues: GraphQLResolveInfo['variableValues']; + callArguments: ContractCallArgs; + abiItemInputs: readonly JsonFragmentType[]; }) => - argumentNodes.reduce>( - (accArguments, argument) => [ - ...accArguments, - getNodeValue({ - valueNode: argument.value, - variableValues, - }), - ], - [], - ); + // Go through ABI item to format call args and return them in the correct + // order + abiItemInputs.map((component, componentIndex) => { + const argumentName = formatToEntityName({ + name: component.name, + index: componentIndex, + type: 'arg', + }); + + const argumentValue = callArguments[argumentName]; + return argumentValue; + }); export default formatGraphQlArgs; diff --git a/packages/eth-graphql/src/createSchema/makeCalls/index.ts b/packages/eth-graphql/src/createSchema/makeCalls/index.ts index 679fbe4..74e42be 100644 --- a/packages/eth-graphql/src/createSchema/makeCalls/index.ts +++ b/packages/eth-graphql/src/createSchema/makeCalls/index.ts @@ -3,12 +3,14 @@ import { Contract } from 'ethers'; import { GraphQLResolveInfo, Kind } from 'graphql'; import EthGraphQlError from '../../EthGraphQlError'; -import { Config, SolidityValue } from '../../types'; +import { Config } from '../../types'; import formatToSignature from '../../utilities/formatToSignature'; +import { MULT_FIELD_SINGLE_ARGUMENT_NAME, ROOT_FIELD_SINGLE_ARGUMENT_NAME } from '../constants'; import { FieldNameMapping } from '../types'; +import formatArgumentNodes from './formatArgumentNodes'; import formatGraphQlArgs from './formatGraphQlArgs'; import formatMulticallResults from './formatMulticallResults'; -import { ContractCall } from './types'; +import { ContractCall, ContractCallArgs } from './types'; export interface MakeCallsInput { graphqlResolveInfo: GraphQLResolveInfo; @@ -80,20 +82,20 @@ const makeCalls = async ({ graphqlResolveInfo, fieldMapping, config, chainId }: // means an array of addresses to call was passed as the first and only // argument of the contract field if (!contractAddresses) { - const contractArguments = formatGraphQlArgs({ + const contractArguments = formatArgumentNodes({ argumentNodes: contractSelection.arguments || [], variableValues: graphqlResolveInfo.variableValues, }); // Since the a contract field only accepts one "addresses" argument, // then we know it is the first (and only) argument of the array - contractAddresses = contractArguments[0] as string[]; + contractAddresses = contractArguments[ROOT_FIELD_SINGLE_ARGUMENT_NAME] as string[]; } const hasDefinedAddress = !!field.contract.address; // Format arguments - const contractCallArguments = formatGraphQlArgs({ + const contractCallArguments = formatArgumentNodes({ argumentNodes: callSelection.arguments || [], variableValues: graphqlResolveInfo.variableValues, }); @@ -110,7 +112,7 @@ const makeCalls = async ({ graphqlResolveInfo, fieldMapping, config, chainId }: // Shape a contract call for each set of arguments const callArgumentSets = field.isMult - ? (contractCallArguments[0] as readonly SolidityValue[][]) + ? (contractCallArguments[MULT_FIELD_SINGLE_ARGUMENT_NAME] as ContractCallArgs[]) : [contractCallArguments]; callArgumentSets.forEach(callArguments => { @@ -136,9 +138,14 @@ const makeCalls = async ({ graphqlResolveInfo, fieldMapping, config, chainId }: // Merge all calls into one using multicall contract const multicallResults = await Promise.all( - calls.map(({ contractInstance, contractFunctionSignature, callArguments }) => - contractInstance[contractFunctionSignature](...callArguments), - ), + calls.map(({ contractInstance, contractFunctionSignature, callArguments, abiItem }) => { + const formattedCallArguments = formatGraphQlArgs({ + callArguments, + abiItemInputs: abiItem.inputs || [], + }); + + return contractInstance[contractFunctionSignature](...formattedCallArguments); + }), ); // Format and return results diff --git a/packages/eth-graphql/src/createSchema/makeCalls/types.ts b/packages/eth-graphql/src/createSchema/makeCalls/types.ts index 13af441..e8be739 100644 --- a/packages/eth-graphql/src/createSchema/makeCalls/types.ts +++ b/packages/eth-graphql/src/createSchema/makeCalls/types.ts @@ -3,11 +3,15 @@ import { Contract } from 'ethers'; import { SolidityValue } from '../../types'; +export interface ContractCallArgs { + [argumentName: string]: SolidityValue; +} + export interface ContractCall { contractName: string; contractInstance: Contract; contractFunctionSignature: string; - callArguments: readonly SolidityValue[]; + callArguments: ContractCallArgs; fieldName: string; abiItem: JsonFragment; isMult: boolean;