From b6c30b63be3f28a863dd11b2e7dd2f1160d01121 Mon Sep 17 00:00:00 2001 From: andresceballosm Date: Mon, 29 Jul 2024 12:49:23 -0500 Subject: [PATCH 1/4] 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', From 2f4dd0bb8c9443c29ebba74add87d91a48f390ca Mon Sep 17 00:00:00 2001 From: andresceballosm Date: Mon, 29 Jul 2024 15:17:07 -0500 Subject: [PATCH 2/4] Resolved comments PR and updated unit test --- packages/taco/src/conditions/base/contract.ts | 50 ++++++++----------- .../test/conditions/base/contract.test.ts | 8 +++ 2 files changed, 30 insertions(+), 28 deletions(-) diff --git a/packages/taco/src/conditions/base/contract.ts b/packages/taco/src/conditions/base/contract.ts index dfcc6b361..449f727a8 100644 --- a/packages/taco/src/conditions/base/contract.ts +++ b/packages/taco/src/conditions/base/contract.ts @@ -29,6 +29,23 @@ const functionAbiVariableSchema = z }) .strict(); +export const humanReadableAbiSchema = z + .string() + .refine( + (abi) => { + try { + toJsonAbiFormat(abi); + return true; + } catch (e) { + return false; + } + }, + { + message: 'Invalid Human-Readable ABI format', + }, + ) + .transform(toJsonAbiFormat); + const functionAbiSchema = z .object({ name: z.string(), @@ -78,10 +95,9 @@ function toJsonAbiFormat(humanReadableAbi: string): any { const fragment = ethers.utils.FunctionFragment.from( abiWithoutFunctionKeyword, ); - const jsonAbi = JSON.parse(fragment.format(ethers.utils.FormatTypes.json)); - - delete jsonAbi.constant; - delete jsonAbi.payable; + const { constant, payable, ...jsonAbi } = JSON.parse( + fragment.format(ethers.utils.FormatTypes.json), + ); jsonAbi.inputs = jsonAbi.inputs.map((input: any) => ({ ...input, @@ -108,25 +124,7 @@ export const contractConditionSchema = rpcConditionSchema standardContractType: z.enum(['ERC20', 'ERC721']).optional(), method: z.string(), 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), - ]) + .union([functionAbiSchema, humanReadableAbiSchema]) .optional(), parameters: z.array(paramOrContextParamSchema), }) @@ -143,12 +141,8 @@ 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); } diff --git a/packages/taco/test/conditions/base/contract.test.ts b/packages/taco/test/conditions/base/contract.test.ts index 7ddfeebac..0a8e66217 100644 --- a/packages/taco/test/conditions/base/contract.test.ts +++ b/packages/taco/test/conditions/base/contract.test.ts @@ -9,6 +9,7 @@ import { contractConditionSchema, ContractConditionType, FunctionAbiProps, + humanReadableAbiSchema, } from '../../../src/conditions/base/contract'; import { ConditionExpression } from '../../../src/conditions/condition-expr'; import { USER_ADDRESS_PARAMS } from '../../../src/conditions/const'; @@ -362,7 +363,14 @@ describe('supports custom function abi', () => { functionAbi: humanReadableAbi, method: 'balanceOf', }); + const invalidHumanReadableAbi = 'function invalidAbi'; + expect(() => + humanReadableAbiSchema.parse(humanReadableAbi), + ).not.toThrow(); + expect(() => humanReadableAbiSchema.parse(invalidHumanReadableAbi)).toThrow( + 'Invalid Human-Readable ABI format', + ); expect(condition.value.functionAbi).toEqual({ name: 'balanceOf', type: 'function', From b29b131501797734b72fb2d43e37ecf1194247c2 Mon Sep 17 00:00:00 2001 From: andresceballosm Date: Wed, 31 Jul 2024 12:35:43 -0500 Subject: [PATCH 3/4] Resolved linter issues --- packages/taco/src/conditions/base/contract.ts | 42 +++++++++++-------- .../test/conditions/base/contract.test.ts | 4 +- 2 files changed, 26 insertions(+), 20 deletions(-) diff --git a/packages/taco/src/conditions/base/contract.ts b/packages/taco/src/conditions/base/contract.ts index 449f727a8..370f57c1d 100644 --- a/packages/taco/src/conditions/base/contract.ts +++ b/packages/taco/src/conditions/base/contract.ts @@ -15,12 +15,18 @@ const EthBaseTypes: [string, ...string[]] = [ 'string', 'address', 'address payable', - ...Array.from({ length: 32 }, (_v, i) => `bytes${i + 1}`), // bytes1 through bytes32 + ...Array.from({ length: 32 }, (_v, i) => `bytes${i + 1}`), 'bytes', - ...Array.from({ length: 32 }, (_v, i) => `uint${8 * (i + 1)}`), // uint8 through uint256 - ...Array.from({ length: 32 }, (_v, i) => `int${8 * (i + 1)}`), // int8 through int256 + ...Array.from({ length: 32 }, (_v, i) => `uint${8 * (i + 1)}`), + ...Array.from({ length: 32 }, (_v, i) => `int${8 * (i + 1)}`), ]; +type AbiVariable = { + name: string; + type: string; + internalType: string; +}; + const functionAbiVariableSchema = z .object({ name: z.string(), @@ -59,7 +65,6 @@ const functionAbiSchema = z (functionAbi) => { let asInterface; try { - // `stringify` here because ethers.utils.Interface doesn't accept a Zod schema asInterface = new ethers.utils.Interface(JSON.stringify([functionAbi])); } catch (e) { return false; @@ -87,7 +92,7 @@ const functionAbiSchema = z }, ); -function toJsonAbiFormat(humanReadableAbi: string): any { +function toJsonAbiFormat(humanReadableAbi: string) { const abiWithoutFunctionKeyword = humanReadableAbi.replace( /^function\s+/, '', @@ -95,20 +100,23 @@ function toJsonAbiFormat(humanReadableAbi: string): any { const fragment = ethers.utils.FunctionFragment.from( abiWithoutFunctionKeyword, ); - const { constant, payable, ...jsonAbi } = JSON.parse( - fragment.format(ethers.utils.FormatTypes.json), - ); + const jsonAbi = JSON.parse(fragment.format(ethers.utils.FormatTypes.json)); - jsonAbi.inputs = jsonAbi.inputs.map((input: any) => ({ - ...input, - internalType: input.type, - })); - jsonAbi.outputs = jsonAbi.outputs.map((output: any) => ({ - ...output, - internalType: output.type, - })); + const filteredJsonAbi = { + name: jsonAbi.name, + type: jsonAbi.type, + stateMutability: jsonAbi.stateMutability, + inputs: jsonAbi.inputs.map((input: AbiVariable) => ({ + ...input, + internalType: input.type, + })), + outputs: jsonAbi.outputs.map((output: AbiVariable) => ({ + ...output, + internalType: output.type, + })), + }; - return jsonAbi; + return filteredJsonAbi; } export type FunctionAbiProps = z.infer; diff --git a/packages/taco/test/conditions/base/contract.test.ts b/packages/taco/test/conditions/base/contract.test.ts index 0a8e66217..4869695b7 100644 --- a/packages/taco/test/conditions/base/contract.test.ts +++ b/packages/taco/test/conditions/base/contract.test.ts @@ -365,9 +365,7 @@ describe('supports custom function abi', () => { }); const invalidHumanReadableAbi = 'function invalidAbi'; - expect(() => - humanReadableAbiSchema.parse(humanReadableAbi), - ).not.toThrow(); + expect(() => humanReadableAbiSchema.parse(humanReadableAbi)).not.toThrow(); expect(() => humanReadableAbiSchema.parse(invalidHumanReadableAbi)).toThrow( 'Invalid Human-Readable ABI format', ); From 596fc99cbd28eef78da9981f1ae9a85a152e7d41 Mon Sep 17 00:00:00 2001 From: andresceballosm Date: Thu, 1 Aug 2024 11:17:28 -0500 Subject: [PATCH 4/4] Updated type ContractConditionProps --- packages/taco/src/conditions/base/contract.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/packages/taco/src/conditions/base/contract.ts b/packages/taco/src/conditions/base/contract.ts index 370f57c1d..4291aea11 100644 --- a/packages/taco/src/conditions/base/contract.ts +++ b/packages/taco/src/conditions/base/contract.ts @@ -148,7 +148,12 @@ export const contractConditionSchema = rpcConditionSchema }, ); -export type ContractConditionProps = z.infer; +export type ContractConditionProps = Omit< + z.infer, + 'functionAbi' +> & { + functionAbi?: string | z.infer | undefined; +}; export class ContractCondition extends Condition { constructor(value: OmitConditionType) { if (typeof value.functionAbi === 'string') {