Skip to content

Commit

Permalink
Added Human-Readable ABI format (#557)
Browse files Browse the repository at this point in the history
  • Loading branch information
derekpierre authored Oct 11, 2024
2 parents 3f15a7f + 596fc99 commit 878cf50
Show file tree
Hide file tree
Showing 2 changed files with 119 additions and 10 deletions.
72 changes: 65 additions & 7 deletions packages/taco/src/conditions/base/contract.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(),
Expand All @@ -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(),
Expand All @@ -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;
Expand Down Expand Up @@ -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<typeof functionAbiSchema>;

export const ContractConditionType = 'contract';
Expand All @@ -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
Expand All @@ -97,10 +148,17 @@ export const contractConditionSchema = rpcConditionSchema
},
);

export type ContractConditionProps = z.infer<typeof contractConditionSchema>;

export type ContractConditionProps = Omit<
z.infer<typeof contractConditionSchema>,
'functionAbi'
> & {
functionAbi?: string | z.infer<typeof functionAbiSchema> | undefined;
};
export class ContractCondition extends Condition {
constructor(value: OmitConditionType<ContractConditionProps>) {
if (typeof value.functionAbi === 'string') {
value.functionAbi = toJsonAbiFormat(value.functionAbi);
}
super(contractConditionSchema, {
conditionType: ContractConditionType,
...value,
Expand Down
57 changes: 54 additions & 3 deletions packages/taco/test/conditions/base/contract.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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([
Expand Down Expand Up @@ -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',
Expand Down

0 comments on commit 878cf50

Please sign in to comment.