diff --git a/packages/taco/src/conditions/base/contract.ts b/packages/taco/src/conditions/base/contract.ts index b170849a..54df510d 100644 --- a/packages/taco/src/conditions/base/contract.ts +++ b/packages/taco/src/conditions/base/contract.ts @@ -1,103 +1,17 @@ -import { ETH_ADDRESS_REGEXP } from '@nucypher/shared'; -import { ethers } from 'ethers'; -import { z } from 'zod'; - import { Condition } from '../condition'; -import { OmitConditionType, paramOrContextParamSchema } from '../shared'; - -import { rpcConditionSchema } from './rpc'; - -// 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', - 'address payable', - ...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 functionAbiVariableSchema = z - .object({ - name: z.string(), - type: z.enum(EthBaseTypes), - internalType: z.enum(EthBaseTypes), // TODO: Do we need to validate this? - }) - .strict(); - -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; - } - - const functionsInAbi = Object.values(asInterface.functions || {}); - return functionsInAbi.length === 1; - }, - { - message: '"functionAbi" must contain a single function definition', - path: ['functionAbi'], - }, - ) - .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"', - path: ['parameters'], - }, - ); - -export type FunctionAbiProps = z.infer; - -export const ContractConditionType = 'contract'; - -export const contractConditionSchema = rpcConditionSchema - .extend({ - conditionType: z - .literal(ContractConditionType) - .default(ContractConditionType), - contractAddress: z.string().regex(ETH_ADDRESS_REGEXP).length(42), - standardContractType: z.enum(['ERC20', 'ERC721']).optional(), - method: z.string(), - functionAbi: functionAbiSchema.optional(), - parameters: z.array(paramOrContextParamSchema), - }) - // 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", - path: ['standardContractType'], - }, - ); - -export type ContractConditionProps = z.infer; +import { + ContractConditionProps, + contractConditionSchema, + ContractConditionType, +} from '../schemas/contract'; +import { OmitConditionType } from '../shared'; + +export { + ContractConditionProps, + contractConditionSchema, + ContractConditionType, + FunctionAbiProps, +} from '../schemas/contract'; export class ContractCondition extends Condition { constructor(value: OmitConditionType) { diff --git a/packages/taco/src/conditions/base/rpc.ts b/packages/taco/src/conditions/base/rpc.ts index d7ab306b..8f73c79c 100644 --- a/packages/taco/src/conditions/base/rpc.ts +++ b/packages/taco/src/conditions/base/rpc.ts @@ -1,30 +1,16 @@ -import { z } from 'zod'; - -import { baseConditionSchema, Condition } from '../condition'; -import { SUPPORTED_CHAIN_IDS } from '../const'; +import { Condition } from '../condition'; import { - EthAddressOrUserAddressSchema, - OmitConditionType, - paramOrContextParamSchema, - returnValueTestSchema, -} from '../shared'; -import createUnionSchema from '../zod'; - -export const RpcConditionType = 'rpc'; - -export const rpcConditionSchema = baseConditionSchema.extend({ - conditionType: z.literal(RpcConditionType).default(RpcConditionType), - chain: createUnionSchema(SUPPORTED_CHAIN_IDS), - method: z.enum(['eth_getBalance']), - parameters: z.union([ - z.array(EthAddressOrUserAddressSchema).nonempty(), - // Using tuple here because ordering matters - z.tuple([EthAddressOrUserAddressSchema, paramOrContextParamSchema]), - ]), - returnValueTest: returnValueTestSchema, // Update to allow multiple return values after expanding supported methods -}); + RpcConditionProps, + rpcConditionSchema, + RpcConditionType, +} from '../schemas/rpc'; +import { OmitConditionType } from '../shared'; -export type RpcConditionProps = z.infer; +export { + RpcConditionProps, + rpcConditionSchema, + RpcConditionType, +} from '../schemas/rpc'; export class RpcCondition extends Condition { constructor(value: OmitConditionType) { diff --git a/packages/taco/src/conditions/base/time.ts b/packages/taco/src/conditions/base/time.ts index 2e4739e1..2d8be8b6 100644 --- a/packages/taco/src/conditions/base/time.ts +++ b/packages/taco/src/conditions/base/time.ts @@ -1,24 +1,17 @@ -import { z } from 'zod'; - import { Condition } from '../condition'; +import { + TimeConditionProps, + timeConditionSchema, + TimeConditionType, +} from '../schemas/time'; import { OmitConditionType } from '../shared'; -import { rpcConditionSchema } from './rpc'; - -// TimeCondition is an RpcCondition with the method set to 'blocktime' and no parameters -// eslint-disable-next-line @typescript-eslint/no-unused-vars -const { parameters: _, ...restShape } = rpcConditionSchema.shape; - -export const TimeConditionType = 'time'; -export const TimeConditionMethod = 'blocktime'; - -export const timeConditionSchema = z.object({ - ...restShape, - conditionType: z.literal(TimeConditionType).default(TimeConditionType), - method: z.literal(TimeConditionMethod).default(TimeConditionMethod), -}); - -export type TimeConditionProps = z.infer; +export { + TimeConditionMethod, + TimeConditionProps, + timeConditionSchema, + TimeConditionType, +} from '../schemas/time'; export class TimeCondition extends Condition { constructor(value: OmitConditionType) { diff --git a/packages/taco/src/conditions/compound-condition.ts b/packages/taco/src/conditions/compound-condition.ts index 86135341..d7de784e 100644 --- a/packages/taco/src/conditions/compound-condition.ts +++ b/packages/taco/src/conditions/compound-condition.ts @@ -1,65 +1,16 @@ -import { z } from 'zod'; - -import { contractConditionSchema } from './base/contract'; -import { rpcConditionSchema } from './base/rpc'; -import { timeConditionSchema } from './base/time'; -import { baseConditionSchema, Condition, ConditionProps } from './condition'; -import { maxNestedDepth } from './multi-condition'; -import { sequentialConditionSchema } from './sequential'; +import { Condition, ConditionProps } from './condition'; +import { + CompoundConditionProps, + compoundConditionSchema, + CompoundConditionType, +} from './schemas/compound'; import { OmitConditionType } from './shared'; -export const CompoundConditionType = 'compound'; - -export const compoundConditionSchema: z.ZodSchema = baseConditionSchema - .extend({ - conditionType: z - .literal(CompoundConditionType) - .default(CompoundConditionType), - operator: z.enum(['and', 'or', 'not']), - operands: z - .array( - z.lazy(() => - z.union([ - rpcConditionSchema, - timeConditionSchema, - contractConditionSchema, - compoundConditionSchema, - sequentialConditionSchema, - ]), - ), - ) - .min(1) - .max(5), - }) - .refine( - (condition) => { - // 'and' and 'or' operators must have at least 2 operands - if (['and', 'or'].includes(condition.operator)) { - return condition.operands.length >= 2; - } - - // 'not' operator must have exactly 1 operand - if (condition.operator === 'not') { - return condition.operands.length === 1; - } - - // We test positive cases exhaustively, so we return false here: - return false; - }, - ({ operands, operator }) => ({ - message: `Invalid number of operands ${operands.length} for operator "${operator}"`, - path: ['operands'], - }), - ) - .refine( - (condition) => maxNestedDepth(2)(condition), - { - message: 'Exceeded max nested depth of 2 for multi-condition type', - path: ['operands'], - }, // Max nested depth of 2 - ); - -export type CompoundConditionProps = z.infer; +export { + CompoundConditionProps, + compoundConditionSchema, + CompoundConditionType, +} from './schemas/compound'; export type ConditionOrProps = Condition | ConditionProps; diff --git a/packages/taco/src/conditions/condition.ts b/packages/taco/src/conditions/condition.ts index 29ab267b..f0c0ae93 100644 --- a/packages/taco/src/conditions/condition.ts +++ b/packages/taco/src/conditions/condition.ts @@ -3,9 +3,7 @@ import { z } from 'zod'; import { USER_ADDRESS_PARAMS } from './const'; -export const baseConditionSchema = z.object({ - conditionType: z.string(), -}); +export { baseConditionSchema } from './schemas/common'; type ConditionSchema = z.ZodSchema; export type ConditionProps = z.infer; diff --git a/packages/taco/src/conditions/schemas/index.ts b/packages/taco/src/conditions/schemas/index.ts deleted file mode 100644 index 7e862693..00000000 --- a/packages/taco/src/conditions/schemas/index.ts +++ /dev/null @@ -1,8 +0,0 @@ -export * as common from './common'; -export * as compound from './compound'; -export * as context from './context'; -export * as contract from './contract'; -export * as returnValueTest from './return-value-test'; -export * as rpc from './rpc'; -export * as sequential from './sequential'; -export * as time from './time'; diff --git a/packages/taco/src/conditions/sequential.ts b/packages/taco/src/conditions/sequential.ts index 2f996eb2..65c11111 100644 --- a/packages/taco/src/conditions/sequential.ts +++ b/packages/taco/src/conditions/sequential.ts @@ -1,67 +1,17 @@ -import { z } from 'zod'; +import { Condition } from './condition'; +import { + SequentialConditionProps, + sequentialConditionSchema, + SequentialConditionType, +} from './schemas/sequential'; +import { OmitConditionType } from './shared'; -import { contractConditionSchema } from './base/contract'; -import { rpcConditionSchema } from './base/rpc'; -import { timeConditionSchema } from './base/time'; -import { compoundConditionSchema } from './compound-condition'; -import { baseConditionSchema, Condition } from './condition'; -import { maxNestedDepth } from './multi-condition'; -import { OmitConditionType, plainStringSchema } from './shared'; - -export const SequentialConditionType = 'sequential'; - -export const conditionVariableSchema: z.ZodSchema = z.object({ - varName: plainStringSchema, - condition: z.lazy(() => - z.union([ - rpcConditionSchema, - timeConditionSchema, - contractConditionSchema, - compoundConditionSchema, - sequentialConditionSchema, - ]), - ), -}); - -export const sequentialConditionSchema: z.ZodSchema = baseConditionSchema - .extend({ - conditionType: z - .literal(SequentialConditionType) - .default(SequentialConditionType), - conditionVariables: z.array(conditionVariableSchema).min(2).max(5), - }) - .refine( - (condition) => maxNestedDepth(2)(condition), - { - message: 'Exceeded max nested depth of 2 for multi-condition type', - path: ['conditionVariables'], - }, // Max nested depth of 2 - ) - .refine( - // check for duplicate var names - (condition) => { - const seen = new Set(); - return condition.conditionVariables.every( - (child: ConditionVariableProps) => { - if (seen.has(child.varName)) { - return false; - } - seen.add(child.varName); - return true; - }, - ); - }, - { - message: 'Duplicate variable names are not allowed', - path: ['conditionVariables'], - }, - ); - -export type ConditionVariableProps = z.infer; - -export type SequentialConditionProps = z.infer< - typeof sequentialConditionSchema ->; +export { + ConditionVariableProps, + SequentialConditionProps, + sequentialConditionSchema, + SequentialConditionType, +} from './schemas/sequential'; export class SequentialCondition extends Condition { constructor(value: OmitConditionType) { diff --git a/packages/taco/src/conditions/shared.ts b/packages/taco/src/conditions/shared.ts index 1e63b056..15af59ee 100644 --- a/packages/taco/src/conditions/shared.ts +++ b/packages/taco/src/conditions/shared.ts @@ -1,47 +1,11 @@ -import { EthAddressSchema } from '@nucypher/shared'; -import { - USER_ADDRESS_PARAM_DEFAULT, - USER_ADDRESS_PARAM_EXTERNAL_EIP4361, -} from '@nucypher/taco-auth'; -import { z } from 'zod'; - -import { CONTEXT_PARAM_PREFIX, CONTEXT_PARAM_REGEXP } from './const'; - -export const contextParamSchema = z.string().regex(CONTEXT_PARAM_REGEXP); -// We want to discriminate between ContextParams and plain strings -// If a string starts with `:`, it's a ContextParam -export const plainStringSchema = z.string().refine( - (str) => { - return !str.startsWith(CONTEXT_PARAM_PREFIX); - }, - { - message: `String must not be a context parameter i.e. not start with "${CONTEXT_PARAM_PREFIX}"`, - }, -); - -const paramSchema = z.union([plainStringSchema, z.boolean(), z.number()]); +export type OmitConditionType = Omit; -export const paramOrContextParamSchema: z.ZodSchema = z.union([ - paramSchema, +export { contextParamSchema, - z.lazy(() => z.array(paramOrContextParamSchema)), -]); + paramOrContextParamSchema, +} from './schemas/context'; -export const returnValueTestSchema = z.object({ - index: z.number().int().nonnegative().optional(), - comparator: z.enum(['==', '>', '<', '>=', '<=', '!=']), - value: paramOrContextParamSchema, -}); - -export type ReturnValueTestProps = z.infer; - -const UserAddressSchema = z.enum([ - USER_ADDRESS_PARAM_DEFAULT, - USER_ADDRESS_PARAM_EXTERNAL_EIP4361, -]); -export const EthAddressOrUserAddressSchema = z.union([ - EthAddressSchema, - UserAddressSchema, -]); - -export type OmitConditionType = Omit; +export { + ReturnValueTestProps, + returnValueTestSchema, +} from './schemas/return-value-test'; diff --git a/packages/taco/src/conditions/zod.ts b/packages/taco/src/conditions/zod.ts deleted file mode 100644 index 75aa1b4d..00000000 --- a/packages/taco/src/conditions/zod.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { Primitive, z, ZodLiteral } from 'zod'; - -// Source: https://github.com/colinhacks/zod/issues/831#issuecomment-1063481764 -const createUnion = < - T extends Readonly<[Primitive, Primitive, ...Primitive[]]>, ->( - values: T, -) => { - const zodLiterals = values.map((value) => z.literal(value)) as unknown as [ - ZodLiteral, - ZodLiteral, - ...ZodLiteral[], - ]; - return z.union(zodLiterals); -}; - -function createUnionSchema(values: T) { - if (values.length === 0) { - return z.never(); - } - - if (values.length === 1) { - return z.literal(values[0]); - } - - return createUnion( - values as unknown as Readonly<[Primitive, Primitive, ...Primitive[]]>, - ); -} - -export default createUnionSchema;