Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added Human-Readable ABI format #557

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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', () => {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can test the low-level behavior of humanReadableAbiSchema here

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
Loading