-
Notifications
You must be signed in to change notification settings - Fork 23
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
Update Conditions API #267
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
This file was deleted.
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,87 +1,90 @@ | ||
import { ethers } from 'ethers'; | ||
import Joi from 'joi'; | ||
import { z } from 'zod'; | ||
|
||
import { ETH_ADDRESS_REGEXP } from '../const'; | ||
|
||
import { RpcCondition, rpcConditionRecord } from './rpc'; | ||
import { rpcConditionSchema } from './rpc'; | ||
|
||
export const STANDARD_CONTRACT_TYPES = ['ERC20', 'ERC721']; | ||
// TODO: Consider replacing with `z.unknown`: | ||
// Since Solidity types are tied to Solidity version, we may not be able to accurately represent them in Zod. | ||
// Alternatively, find a TS Solidity type lib. | ||
const EthBaseTypes: [string, ...string[]] = [ | ||
'bool', | ||
'string', | ||
'address', | ||
...Array.from({ length: 32 }, (_v, i) => `bytes${i + 1}`), // bytes1 through bytes32 | ||
'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 | ||
]; | ||
|
||
const functionAbiSchema = Joi.object({ | ||
name: Joi.string().required(), | ||
type: Joi.string().valid('function').required(), | ||
inputs: Joi.array(), | ||
outputs: Joi.array(), | ||
stateMutability: Joi.string().valid('view', 'pure').required(), | ||
}).custom((functionAbi, helper) => { | ||
// Is `functionABI` a valid function fragment? | ||
let asInterface; | ||
try { | ||
asInterface = new ethers.utils.Interface([functionAbi]); | ||
} catch (e: unknown) { | ||
const { message } = e as Error; | ||
return helper.message({ | ||
custom: message, | ||
}); | ||
} | ||
const functionAbiVariableSchema = z | ||
.object({ | ||
name: z.string(), | ||
type: z.enum(EthBaseTypes), | ||
internalType: z.enum(EthBaseTypes), // TODO: Do we need to validate this? | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. RFC There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. All this contract validation seems a little bit tricky, and I think there is a low probability of having a bad ABI contract. I would leave this as is. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. yeah, it seems likely that |
||
}) | ||
.strict(); | ||
|
||
if (!asInterface.functions) { | ||
return helper.message({ | ||
custom: '"functionAbi" is missing a function fragment', | ||
}); | ||
} | ||
const functionAbiSchema = z | ||
.object({ | ||
name: z.string(), | ||
type: z.literal('function'), | ||
inputs: z.array(functionAbiVariableSchema).min(0), | ||
outputs: z.array(functionAbiVariableSchema).nonempty(), | ||
stateMutability: z.union([z.literal('view'), z.literal('pure')]), | ||
}) | ||
.strict() | ||
.refine( | ||
(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; | ||
} | ||
|
||
if (Object.values(asInterface.functions).length !== 1) { | ||
return helper.message({ | ||
custom: '"functionAbi" must contain exactly one function fragment', | ||
}); | ||
} | ||
const functionsInAbi = Object.values(asInterface.functions || {}); | ||
return functionsInAbi.length === 1; | ||
}, | ||
{ | ||
message: '"functionAbi" must contain a single function definition', | ||
} | ||
) | ||
.refine( | ||
(functionAbi) => { | ||
const asInterface = new ethers.utils.Interface( | ||
JSON.stringify([functionAbi]) | ||
); | ||
const nrOfInputs = asInterface.fragments[0].inputs.length; | ||
return functionAbi.inputs.length === nrOfInputs; | ||
}, | ||
{ | ||
message: '"parameters" must have the same length as "functionAbi.inputs"', | ||
} | ||
); | ||
|
||
// Now we just need to validate against the parent schema | ||
// Validate method name | ||
const method = helper.state.ancestors[0].method; | ||
export type FunctionAbiProps = z.infer<typeof functionAbiSchema>; | ||
|
||
let functionFragment; | ||
try { | ||
functionFragment = asInterface.getFunction(method); | ||
} catch (e) { | ||
return helper.message({ | ||
custom: `"functionAbi" has no matching function for "${method}"`, | ||
}); | ||
} | ||
export const contractConditionSchema = rpcConditionSchema | ||
.extend({ | ||
conditionType: z.literal('contract').default('contract'), | ||
contractAddress: z.string().regex(ETH_ADDRESS_REGEXP), | ||
standardContractType: z.enum(['ERC20', 'ERC721']).optional(), | ||
method: z.string(), | ||
functionAbi: functionAbiSchema.optional(), | ||
parameters: z.array(z.unknown()), | ||
}) | ||
// Adding this custom logic causes the return type to be ZodEffects instead of ZodObject | ||
// https://github.com/colinhacks/zod/issues/2474 | ||
.refine( | ||
// A check to see if either 'standardContractType' or 'functionAbi' is set | ||
(data) => Boolean(data.standardContractType) !== Boolean(data.functionAbi), | ||
{ | ||
message: | ||
"At most one of the fields 'standardContractType' and 'functionAbi' must be defined", | ||
} | ||
); | ||
|
||
if (!functionFragment) { | ||
return helper.message({ | ||
custom: `"functionAbi" not valid for method: "${method}"`, | ||
}); | ||
} | ||
|
||
// Validate nr of parameters | ||
const parameters = helper.state.ancestors[0].parameters; | ||
if (functionFragment.inputs.length !== parameters.length) { | ||
return helper.message({ | ||
custom: '"parameters" must have the same length as "functionAbi.inputs"', | ||
}); | ||
} | ||
|
||
return functionAbi; | ||
}); | ||
|
||
export const contractConditionRecord = { | ||
...rpcConditionRecord, | ||
contractAddress: Joi.string().pattern(ETH_ADDRESS_REGEXP).required(), | ||
standardContractType: Joi.string() | ||
.valid(...STANDARD_CONTRACT_TYPES) | ||
.optional(), | ||
method: Joi.string().required(), | ||
functionAbi: functionAbiSchema.optional(), | ||
parameters: Joi.array().required(), | ||
}; | ||
|
||
export const contractConditionSchema = Joi.object(contractConditionRecord) | ||
// At most one of these keys needs to be present | ||
.xor('standardContractType', 'functionAbi'); | ||
|
||
export class ContractCondition extends RpcCondition { | ||
public readonly schema = contractConditionSchema; | ||
} | ||
export type ContractConditionProps = z.infer<typeof contractConditionSchema>; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,4 +1,40 @@ | ||
export { Condition } from './condition'; | ||
export { ContractCondition } from './contract'; | ||
export { RpcCondition } from './rpc'; | ||
export { TimeCondition } from './time'; | ||
import { | ||
CompoundConditionProps, | ||
compoundConditionSchema, | ||
} from '../compound-condition'; | ||
import { Condition } from '../condition'; | ||
|
||
import { ContractConditionProps, contractConditionSchema } from './contract'; | ||
import { RpcConditionProps, rpcConditionSchema } from './rpc'; | ||
import { TimeConditionProps, timeConditionSchema } from './time'; | ||
|
||
// Exporting classes here instead of their respective schema files to | ||
// avoid circular dependency on Condition class. | ||
|
||
export class CompoundCondition extends Condition { | ||
constructor(value: CompoundConditionProps) { | ||
super(compoundConditionSchema, value); | ||
} | ||
} | ||
|
||
export class ContractCondition extends Condition { | ||
constructor(value: ContractConditionProps) { | ||
super(contractConditionSchema, value); | ||
} | ||
} | ||
|
||
export class RpcCondition extends Condition { | ||
constructor(value: RpcConditionProps) { | ||
super(rpcConditionSchema, value); | ||
} | ||
} | ||
|
||
export class TimeCondition extends Condition { | ||
constructor(value: TimeConditionProps) { | ||
super(timeConditionSchema, value); | ||
} | ||
} | ||
|
||
export { type ContractConditionProps } from './contract'; | ||
export { type RpcConditionProps } from './rpc'; | ||
export { type TimeConditionProps } from './time'; |
This file was deleted.
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,39 +1,16 @@ | ||
import Joi from 'joi'; | ||
import { z } from 'zod'; | ||
|
||
import { SUPPORTED_CHAINS } from '../const'; | ||
import { SUPPORTED_CHAIN_IDS } from '../const'; | ||
import createUnionSchema from '../zod'; | ||
|
||
import { Condition } from './condition'; | ||
import { | ||
ethAddressOrUserAddressSchema, | ||
returnValueTestSchema, | ||
} from './return-value'; | ||
import { EthAddressOrUserAddressSchema, returnValueTestSchema } from './shared'; | ||
|
||
const rpcMethodSchemas: Record<string, Joi.Schema> = { | ||
eth_getBalance: Joi.array().items(ethAddressOrUserAddressSchema).required(), | ||
balanceOf: Joi.array().items(ethAddressOrUserAddressSchema).required(), | ||
}; | ||
export const rpcConditionSchema = z.object({ | ||
conditionType: z.literal('rpc').default('rpc'), | ||
chain: createUnionSchema(SUPPORTED_CHAIN_IDS), | ||
method: z.enum(['eth_getBalance', 'balanceOf']), | ||
parameters: z.array(EthAddressOrUserAddressSchema), | ||
returnValueTest: returnValueTestSchema, | ||
}); | ||
|
||
const makeParameters = () => | ||
Joi.array().when('method', { | ||
switch: Object.keys(rpcMethodSchemas).map((method) => ({ | ||
is: method, | ||
then: rpcMethodSchemas[method], | ||
})), | ||
}); | ||
|
||
export const rpcConditionRecord = { | ||
chain: Joi.number() | ||
.valid(...SUPPORTED_CHAINS) | ||
.required(), | ||
method: Joi.string() | ||
.valid(...Object.keys(rpcMethodSchemas)) | ||
.required(), | ||
parameters: makeParameters(), | ||
returnValueTest: returnValueTestSchema.required(), | ||
}; | ||
|
||
export const rpcConditionSchema = Joi.object(rpcConditionRecord); | ||
|
||
export class RpcCondition extends Condition { | ||
public readonly schema = rpcConditionSchema; | ||
} | ||
export type RpcConditionProps = z.infer<typeof rpcConditionSchema>; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,18 @@ | ||
import { z } from 'zod'; | ||
|
||
import { ETH_ADDRESS_REGEXP, USER_ADDRESS_PARAM } from '../const'; | ||
|
||
export const returnValueTestSchema = z.object({ | ||
index: z.number().optional(), | ||
comparator: z.enum(['==', '>', '<', '>=', '<=', '!=']), | ||
value: z.unknown(), | ||
}); | ||
|
||
export type ReturnValueTestProps = z.infer<typeof returnValueTestSchema>; | ||
|
||
const EthAddressSchema = z.string().regex(ETH_ADDRESS_REGEXP); | ||
const UserAddressSchema = z.literal(USER_ADDRESS_PARAM); | ||
export const EthAddressOrUserAddressSchema = z.union([ | ||
EthAddressSchema, | ||
UserAddressSchema, | ||
]); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
RFC
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Potentially use this?
https://abitype.dev/api/zod
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Potentially use this?
https://abitype.dev/api/zod
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actually I think you could probably replace pretty much anything having to do with interfacing with contracts with this maybe?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, it's likely. I've already started looking into
viem
: #271