Skip to content

Commit

Permalink
Use schemas from schemas folder, and re-export relevant types etc.
Browse files Browse the repository at this point in the history
  • Loading branch information
derekpierre committed Oct 10, 2024
1 parent 422fc1f commit ae4e402
Show file tree
Hide file tree
Showing 9 changed files with 68 additions and 351 deletions.
112 changes: 13 additions & 99 deletions packages/taco/src/conditions/base/contract.ts
Original file line number Diff line number Diff line change
@@ -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<typeof functionAbiSchema>;

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<typeof contractConditionSchema>;
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<ContractConditionProps>) {
Expand Down
36 changes: 11 additions & 25 deletions packages/taco/src/conditions/base/rpc.ts
Original file line number Diff line number Diff line change
@@ -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<typeof rpcConditionSchema>;
export {
RpcConditionProps,
rpcConditionSchema,
RpcConditionType,
} from '../schemas/rpc';

export class RpcCondition extends Condition {
constructor(value: OmitConditionType<RpcConditionProps>) {
Expand Down
29 changes: 11 additions & 18 deletions packages/taco/src/conditions/base/time.ts
Original file line number Diff line number Diff line change
@@ -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<typeof timeConditionSchema>;
export {
TimeConditionMethod,
TimeConditionProps,
timeConditionSchema,
TimeConditionType,
} from '../schemas/time';

export class TimeCondition extends Condition {
constructor(value: OmitConditionType<TimeConditionProps>) {
Expand Down
71 changes: 11 additions & 60 deletions packages/taco/src/conditions/compound-condition.ts
Original file line number Diff line number Diff line change
@@ -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<typeof compoundConditionSchema>;
export {
CompoundConditionProps,
compoundConditionSchema,
CompoundConditionType,
} from './schemas/compound';

export type ConditionOrProps = Condition | ConditionProps;

Expand Down
4 changes: 1 addition & 3 deletions packages/taco/src/conditions/condition.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<ConditionSchema>;
Expand Down
8 changes: 0 additions & 8 deletions packages/taco/src/conditions/schemas/index.ts

This file was deleted.

76 changes: 13 additions & 63 deletions packages/taco/src/conditions/sequential.ts
Original file line number Diff line number Diff line change
@@ -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<typeof conditionVariableSchema>;

export type SequentialConditionProps = z.infer<
typeof sequentialConditionSchema
>;
export {
ConditionVariableProps,
SequentialConditionProps,
sequentialConditionSchema,
SequentialConditionType,
} from './schemas/sequential';

export class SequentialCondition extends Condition {
constructor(value: OmitConditionType<SequentialConditionProps>) {
Expand Down
Loading

0 comments on commit ae4e402

Please sign in to comment.