From b6c30b63be3f28a863dd11b2e7dd2f1160d01121 Mon Sep 17 00:00:00 2001 From: andresceballosm Date: Mon, 29 Jul 2024 12:49:23 -0500 Subject: [PATCH] Added Human-Readable ABI format --- packages/taco/src/conditions/base/contract.ts | 55 ++++++++++++++++++- .../test/conditions/base/contract.test.ts | 51 ++++++++++++++++- 2 files changed, 101 insertions(+), 5 deletions(-) diff --git a/packages/taco/src/conditions/base/contract.ts b/packages/taco/src/conditions/base/contract.ts index b170849ab..dfcc6b361 100644 --- a/packages/taco/src/conditions/base/contract.ts +++ b/packages/taco/src/conditions/base/contract.ts @@ -70,6 +70,31 @@ const functionAbiSchema = z }, ); +function toJsonAbiFormat(humanReadableAbi: string): any { + const abiWithoutFunctionKeyword = humanReadableAbi.replace( + /^function\s+/, + '', + ); + const fragment = ethers.utils.FunctionFragment.from( + abiWithoutFunctionKeyword, + ); + const jsonAbi = JSON.parse(fragment.format(ethers.utils.FormatTypes.json)); + + delete jsonAbi.constant; + delete jsonAbi.payable; + + jsonAbi.inputs = jsonAbi.inputs.map((input: any) => ({ + ...input, + internalType: input.type, + })); + jsonAbi.outputs = jsonAbi.outputs.map((output: any) => ({ + ...output, + internalType: output.type, + })); + + return jsonAbi; +} + export type FunctionAbiProps = z.infer; export const ContractConditionType = 'contract'; @@ -82,7 +107,27 @@ export const contractConditionSchema = rpcConditionSchema contractAddress: z.string().regex(ETH_ADDRESS_REGEXP).length(42), standardContractType: z.enum(['ERC20', 'ERC721']).optional(), method: z.string(), - functionAbi: functionAbiSchema.optional(), + functionAbi: z + .union([ + functionAbiSchema, + z + .string() + .refine( + (abi) => { + try { + toJsonAbiFormat(abi); + return true; + } catch (e) { + return false; + } + }, + { + message: 'Invalid Human-Readable ABI format', + }, + ) + .transform(toJsonAbiFormat), + ]) + .optional(), parameters: z.array(paramOrContextParamSchema), }) // Adding this custom logic causes the return type to be ZodEffects instead of ZodObject @@ -98,9 +143,15 @@ export const contractConditionSchema = rpcConditionSchema ); export type ContractConditionProps = z.infer; +interface ContractConditionHumanReadableAbi extends ContractConditionProps { + functionAbi: string; +} export class ContractCondition extends Condition { - constructor(value: OmitConditionType) { + constructor(value: OmitConditionType) { + if (typeof value.functionAbi === 'string') { + value.functionAbi = toJsonAbiFormat(value.functionAbi); + } super(contractConditionSchema, { conditionType: ContractConditionType, ...value, diff --git a/packages/taco/test/conditions/base/contract.test.ts b/packages/taco/test/conditions/base/contract.test.ts index 849ae2b5f..7ddfeebac 100644 --- a/packages/taco/test/conditions/base/contract.test.ts +++ b/packages/taco/test/conditions/base/contract.test.ts @@ -226,18 +226,29 @@ describe('supports custom function abi', () => { stateMutability: 'pure', }, }, + { + method: 'balanceOf', + functionAbi: 'balanceOf(address _owner) view returns (uint256 balance)', + }, ])('accepts well-formed functionAbi', ({ method, functionAbi }) => { const result = ContractCondition.validate(contractConditionSchema, { ...contractConditionObj, - parameters: functionAbi.inputs.map((input) => `fake_parameter_${input}`), // - functionAbi: functionAbi as FunctionAbiProps, + parameters: + typeof functionAbi === 'string' + ? ['fake_parameter'] + : functionAbi.inputs.map((input) => `fake_parameter_${input}`), + functionAbi: functionAbi as FunctionAbiProps | string, method, }); expect(result.error).toBeUndefined(); expect(result.data).toBeDefined(); expect(result.data?.method).toEqual(method); - expect(result.data?.functionAbi).toEqual(functionAbi); + if (typeof functionAbi === 'string') { + expect(typeof result.data?.functionAbi).toBe('object'); + } else { + expect(result.data?.functionAbi).toEqual(functionAbi); + } }); it.each([ @@ -327,6 +338,40 @@ describe('supports custom function abi', () => { }, ); + it('rejects malformed human-readable functionAbi', () => { + const result = ContractCondition.validate(contractConditionSchema, { + ...contractConditionObj, + functionAbi: 'invalid human-readable ABI', + method: 'invalidMethod', + }); + + expect(result.error).toBeDefined(); + expect(result.data).toBeUndefined(); + expect(result.error?.format()).toMatchObject({ + functionAbi: { + _errors: ['Invalid Human-Readable ABI format'], + }, + }); + }); + + it('converts human-readable ABI to JSON ABI format', () => { + const humanReadableAbi = + 'balanceOf(address _owner) view returns (uint256 balance)'; + const condition = new ContractCondition({ + ...contractConditionObj, + functionAbi: humanReadableAbi, + method: 'balanceOf', + }); + + expect(condition.value.functionAbi).toEqual({ + name: 'balanceOf', + type: 'function', + inputs: [{ name: '_owner', type: 'address', internalType: 'address' }], + outputs: [{ name: 'balance', type: 'uint256', internalType: 'uint256' }], + stateMutability: 'view', + }); + }); + it.each([ { contractAddress: '0x123',