diff --git a/packages/taco/src/conditions/base/contract.ts b/packages/taco/src/conditions/base/contract.ts index b170849ab..4291aea11 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(), @@ -29,6 +35,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(), @@ -42,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; @@ -70,6 +92,33 @@ const functionAbiSchema = z }, ); +function toJsonAbiFormat(humanReadableAbi: string) { + const abiWithoutFunctionKeyword = humanReadableAbi.replace( + /^function\s+/, + '', + ); + const fragment = ethers.utils.FunctionFragment.from( + abiWithoutFunctionKeyword, + ); + const jsonAbi = JSON.parse(fragment.format(ethers.utils.FormatTypes.json)); + + 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 filteredJsonAbi; +} + export type FunctionAbiProps = z.infer; export const ContractConditionType = 'contract'; @@ -82,7 +131,9 @@ 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, humanReadableAbiSchema]) + .optional(), parameters: z.array(paramOrContextParamSchema), }) // Adding this custom logic causes the return type to be ZodEffects instead of ZodObject @@ -97,10 +148,17 @@ 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') { + 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..4869695b7 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'; @@ -226,18 +227,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 +339,45 @@ 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', + }); + 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', + inputs: [{ name: '_owner', type: 'address', internalType: 'address' }], + outputs: [{ name: 'balance', type: 'uint256', internalType: 'uint256' }], + stateMutability: 'view', + }); + }); + it.each([ { contractAddress: '0x123',