forked from nucypher/taco-web
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add schemas folder to house all zod schemas for conditions, context, …
…etc. Add common single schema that encompasses all possible condition schemas.
- Loading branch information
1 parent
3bd8816
commit ebd5f9e
Showing
10 changed files
with
356 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,65 @@ | ||
import { EthAddressSchema } from '@nucypher/shared'; | ||
import { | ||
USER_ADDRESS_PARAM_DEFAULT, | ||
USER_ADDRESS_PARAM_EXTERNAL_EIP4361, | ||
} from '@nucypher/taco-auth'; | ||
import { Primitive, z, ZodLiteral } 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}"`, | ||
}, | ||
); | ||
|
||
export const UserAddressSchema = z.enum([ | ||
USER_ADDRESS_PARAM_DEFAULT, | ||
USER_ADDRESS_PARAM_EXTERNAL_EIP4361, | ||
]); | ||
|
||
export const EthAddressOrUserAddressSchema = z.union([ | ||
EthAddressSchema, | ||
UserAddressSchema, | ||
]); | ||
|
||
export const baseConditionSchema = z.object({ | ||
conditionType: z.string(), | ||
}); | ||
|
||
// 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<Primitive>, | ||
ZodLiteral<Primitive>, | ||
...ZodLiteral<Primitive>[], | ||
]; | ||
return z.union(zodLiterals); | ||
}; | ||
|
||
function createUnionSchema<T extends readonly Primitive[]>(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; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,46 @@ | ||
import { z } from 'zod'; | ||
|
||
import { maxNestedDepth } from '../multi-condition'; | ||
|
||
import { baseConditionSchema } from './common'; | ||
import { anyConditionSchema } from './utils'; | ||
|
||
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(anyConditionSchema).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>; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,15 @@ | ||
import { z } from 'zod'; | ||
|
||
import { CONTEXT_PARAM_REGEXP } from '../const'; | ||
|
||
import { plainStringSchema } from './common'; | ||
|
||
export const contextParamSchema = z.string().regex(CONTEXT_PARAM_REGEXP); | ||
|
||
const paramSchema = z.union([plainStringSchema, z.boolean(), z.number()]); | ||
|
||
export const paramOrContextParamSchema: z.ZodSchema = z.union([ | ||
paramSchema, | ||
contextParamSchema, | ||
z.lazy(() => z.array(paramOrContextParamSchema)), | ||
]); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,97 @@ | ||
import { ETH_ADDRESS_REGEXP } from '@nucypher/shared'; | ||
import { ethers } from 'ethers'; | ||
import { z } from 'zod'; | ||
|
||
import { paramOrContextParamSchema } from './context'; | ||
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>; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,8 @@ | ||
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'; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,11 @@ | ||
import { z } from 'zod'; | ||
|
||
import { paramOrContextParamSchema } from './context'; | ||
|
||
export const returnValueTestSchema = z.object({ | ||
index: z.number().int().nonnegative().optional(), | ||
comparator: z.enum(['==', '>', '<', '>=', '<=', '!=']), | ||
value: paramOrContextParamSchema, | ||
}); | ||
|
||
export type ReturnValueTestProps = z.infer<typeof returnValueTestSchema>; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,26 @@ | ||
import { z } from 'zod'; | ||
|
||
import { SUPPORTED_CHAIN_IDS } from '../const'; | ||
|
||
import createUnionSchema, { | ||
baseConditionSchema, | ||
EthAddressOrUserAddressSchema, | ||
} from './common'; | ||
import { paramOrContextParamSchema } from './context'; | ||
import { returnValueTestSchema } from './return-value-test'; | ||
|
||
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 | ||
}); | ||
|
||
export type RpcConditionProps = z.infer<typeof rpcConditionSchema>; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,52 @@ | ||
import { z } from 'zod'; | ||
|
||
import { maxNestedDepth } from '../multi-condition'; | ||
|
||
import { baseConditionSchema, plainStringSchema } from './common'; | ||
import { anyConditionSchema } from './utils'; | ||
|
||
export const SequentialConditionType = 'sequential'; | ||
|
||
export const conditionVariableSchema: z.ZodSchema = z.object({ | ||
varName: plainStringSchema, | ||
condition: anyConditionSchema, | ||
}); | ||
export type ConditionVariableProps = z.infer<typeof conditionVariableSchema>; | ||
|
||
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 SequentialConditionProps = z.infer< | ||
typeof sequentialConditionSchema | ||
>; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,18 @@ | ||
import { z } from 'zod'; | ||
|
||
import { rpcConditionSchema } from './rpc'; | ||
|
||
// TimeCondition is an RpcCondition with the method set to 'blocktime' and no parameters | ||
|
||
export const TimeConditionType = 'time'; | ||
export const TimeConditionMethod = 'blocktime'; | ||
|
||
// eslint-disable-next-line @typescript-eslint/no-unused-vars | ||
const { parameters: _, ...restShape } = rpcConditionSchema.shape; | ||
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>; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,18 @@ | ||
import { z } from 'zod'; | ||
|
||
import { compoundConditionSchema } from '../compound-condition'; | ||
|
||
import { contractConditionSchema } from './contract'; | ||
import { rpcConditionSchema } from './rpc'; | ||
import { sequentialConditionSchema } from './sequential'; | ||
import { timeConditionSchema } from './time'; | ||
|
||
export const anyConditionSchema: z.ZodSchema = z.lazy(() => | ||
z.union([ | ||
rpcConditionSchema, | ||
timeConditionSchema, | ||
contractConditionSchema, | ||
compoundConditionSchema, | ||
sequentialConditionSchema, | ||
]), | ||
); |