From e5b0976b0b5bba07a92aba1e65f338c5f0be440c Mon Sep 17 00:00:00 2001 From: Piotr Roslaniec Date: Mon, 21 Aug 2023 13:38:26 +0200 Subject: [PATCH] feat!: add typed conditions api --- package.json | 8 +- src/conditions/base/condition.ts | 44 ---- src/conditions/base/contract.ts | 155 ++++++------ src/conditions/base/index.ts | 47 +++- src/conditions/base/return-value.ts | 29 --- src/conditions/base/rpc.ts | 57 ++--- src/conditions/base/time.ts | 30 +-- src/conditions/compound-condition.ts | 41 ++-- src/conditions/condition-expr.ts | 54 +++-- src/conditions/condition.ts | 63 +++++ src/conditions/const.ts | 9 - src/conditions/context/context.ts | 2 +- src/conditions/context/index.ts | 3 +- src/conditions/index.ts | 16 +- src/conditions/predefined/erc721.ts | 45 +++- src/index.ts | 4 +- src/utils.ts | 4 - test/docs/cbd.test.ts | 8 +- test/integration/enrico.test.ts | 2 +- test/integration/pre.test.ts | 2 +- test/unit/conditions/base/condition.test.ts | 45 ++-- test/unit/conditions/base/contract.test.ts | 174 ++++++++------ test/unit/conditions/base/rpc.test.ts | 28 ++- test/unit/conditions/base/time.test.ts | 49 ++-- .../conditions/compound-condition.test.ts | 124 ++++++---- test/unit/conditions/condition-expr.test.ts | 61 +---- test/unit/conditions/context.test.ts | 6 +- test/unit/testVariables.ts | 21 +- yarn.lock | 224 ++++++++++-------- 29 files changed, 735 insertions(+), 620 deletions(-) delete mode 100644 src/conditions/base/condition.ts delete mode 100644 src/conditions/base/return-value.ts create mode 100644 src/conditions/condition.ts diff --git a/package.json b/package.json index f292e6aa8..367d4b1cc 100644 --- a/package.json +++ b/package.json @@ -35,7 +35,7 @@ "test:lint": "eslint src test --ext .ts", "test:exports": "ts-unused-exports tsconfig.json --ignoreFiles src/index.ts", "test:prettier": "prettier \"src/**/*.ts\" \"test/**/*.ts\" --list-different", - "test:unit": "jest --detectOpenHandles --forceExit --runInBand", + "test:unit": "jest --detectOpenHandles --forceExit", "watch:build": "tsc -p tsconfig.json -w", "watch:test": "jest --watch", "cov": "run-s build test:unit && open-cli coverage/index.html", @@ -56,9 +56,9 @@ "axios": "^0.21.1", "deep-equal": "^2.2.1", "ethers": "^5.4.1", - "joi": "^17.7.0", "qs": "^6.10.1", - "semver": "^7.5.2" + "semver": "^7.5.2", + "zod": "^3.22.1" }, "devDependencies": { "@babel/core": "^7.18.10", @@ -66,7 +66,7 @@ "@skypack/package-check": "^0.2.2", "@typechain/ethers-v5": "^9.0.0", "@types/deep-equal": "^1.0.1", - "@types/jest": "^26.0.24", + "@types/jest": "^29.5.3", "@types/qs": "^6.9.7", "@types/semver": "^7.5.0", "@typescript-eslint/eslint-plugin": "^4.0.1", diff --git a/src/conditions/base/condition.ts b/src/conditions/base/condition.ts deleted file mode 100644 index f4caa12c2..000000000 --- a/src/conditions/base/condition.ts +++ /dev/null @@ -1,44 +0,0 @@ -import Joi from 'joi'; - -import { objectEquals } from '../../utils'; - -type Map = Record; - -export class Condition { - public readonly schema = Joi.object(); - public readonly defaults: Map = {}; - - constructor(private readonly value: Record = {}) {} - - public validate(override: Map = {}) { - const newValue = { - ...this.defaults, - ...this.value, - ...override, - }; - return this.schema.validate(newValue); - } - - public toObj(): Map { - const { error, value } = this.validate(this.value); - if (error) { - throw `Invalid condition: ${error.message}`; - } - return { - ...value, - }; - } - - public static fromObj( - // We disable the eslint rule here because we have to pass args to the constructor - // eslint-disable-next-line @typescript-eslint/no-explicit-any - this: new (...args: any[]) => T, - obj: Map - ): T { - return new this(obj); - } - - public equals(other: Condition) { - return objectEquals(this, other); - } -} diff --git a/src/conditions/base/contract.ts b/src/conditions/base/contract.ts index c4221e1da..44f13c9d4 100644 --- a/src/conditions/base/contract.ts +++ b/src/conditions/base/contract.ts @@ -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? + }) + .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; - 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; diff --git a/src/conditions/base/index.ts b/src/conditions/base/index.ts index 805717e01..a6a0abe36 100644 --- a/src/conditions/base/index.ts +++ b/src/conditions/base/index.ts @@ -1,4 +1,43 @@ -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 { + contractConditionSchema, + type ContractConditionProps, +} from './contract'; +export { rpcConditionSchema, type RpcConditionProps } from './rpc'; +export { timeConditionSchema, type TimeConditionProps } from './time'; diff --git a/src/conditions/base/return-value.ts b/src/conditions/base/return-value.ts deleted file mode 100644 index 544033cdc..000000000 --- a/src/conditions/base/return-value.ts +++ /dev/null @@ -1,29 +0,0 @@ -import Joi from 'joi'; - -import { ETH_ADDRESS_REGEXP, USER_ADDRESS_PARAM } from '../const'; - -const COMPARATORS = ['==', '>', '<', '>=', '<=', '!=']; - -export interface ReturnValueTestConfig { - index?: number; - comparator: string; - value: string | number; -} - -export const returnValueTestSchema: Joi.ObjectSchema = - Joi.object({ - index: Joi.number().optional(), - comparator: Joi.string() - .valid(...COMPARATORS) - .required(), - value: Joi.alternatives( - Joi.string(), - Joi.number(), - Joi.boolean() - ).required(), - }); - -export const ethAddressOrUserAddressSchema = Joi.alternatives( - Joi.string().pattern(ETH_ADDRESS_REGEXP), - USER_ADDRESS_PARAM -); diff --git a/src/conditions/base/rpc.ts b/src/conditions/base/rpc.ts index 27ed5f0b7..2b16d7823 100644 --- a/src/conditions/base/rpc.ts +++ b/src/conditions/base/rpc.ts @@ -1,39 +1,30 @@ -import Joi from 'joi'; +import { z } from 'zod'; -import { SUPPORTED_CHAINS } from '../const'; +import { ETH_ADDRESS_REGEXP, USER_ADDRESS_PARAM } from '../const'; -import { Condition } from './condition'; -import { - ethAddressOrUserAddressSchema, - returnValueTestSchema, -} from './return-value'; +export const returnValueTestSchema = z.object({ + index: z.number().optional(), + comparator: z.enum(['==', '>', '<', '>=', '<=', '!=']), + value: z.union([z.string(), z.number(), z.boolean()]), +}); -const rpcMethodSchemas: Record = { - eth_getBalance: Joi.array().items(ethAddressOrUserAddressSchema).required(), - balanceOf: Joi.array().items(ethAddressOrUserAddressSchema).required(), -}; +export type ReturnValueTestProps = z.infer; -const makeParameters = () => - Joi.array().when('method', { - switch: Object.keys(rpcMethodSchemas).map((method) => ({ - is: method, - then: rpcMethodSchemas[method], - })), - }); +const EthAddressOrUserAddressSchema = z.array( + z.union([z.string().regex(ETH_ADDRESS_REGEXP), z.literal(USER_ADDRESS_PARAM)]) +); -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 = z.object({ + conditionType: z.literal('rpc').default('rpc'), + chain: z.union([ + z.literal(137), + z.literal(80001), + z.literal(5), + z.literal(1), + ]), + method: z.enum(['eth_getBalance', 'balanceOf']), + parameters: EthAddressOrUserAddressSchema, + returnValueTest: returnValueTestSchema, +}); -export const rpcConditionSchema = Joi.object(rpcConditionRecord); - -export class RpcCondition extends Condition { - public readonly schema = rpcConditionSchema; -} +export type RpcConditionProps = z.infer; diff --git a/src/conditions/base/time.ts b/src/conditions/base/time.ts index 4f4671dd0..0196b728c 100644 --- a/src/conditions/base/time.ts +++ b/src/conditions/base/time.ts @@ -1,23 +1,15 @@ -import Joi from 'joi'; +import { z } from 'zod'; -import { omit } from '../../utils'; +import { rpcConditionSchema } from './rpc'; -import { RpcCondition, rpcConditionRecord } 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 BLOCKTIME_METHOD = 'blocktime'; +export const timeConditionSchema = z.object({ + ...restShape, + conditionType: z.literal('time').default('time'), + method: z.literal('blocktime').default('blocktime'), +}); -export const timeConditionRecord: Record = { - // TimeCondition is an RpcCondition with the method set to 'blocktime' and no parameters - ...omit(rpcConditionRecord, ['parameters']), - method: Joi.string().valid(BLOCKTIME_METHOD).required(), -}; - -export const timeConditionSchema = Joi.object(timeConditionRecord); - -export class TimeCondition extends RpcCondition { - public readonly defaults: Record = { - method: BLOCKTIME_METHOD, - }; - - public readonly schema = timeConditionSchema; -} +export type TimeConditionProps = z.infer; diff --git a/src/conditions/compound-condition.ts b/src/conditions/compound-condition.ts index 3b35f675a..2c88e0bfe 100644 --- a/src/conditions/compound-condition.ts +++ b/src/conditions/compound-condition.ts @@ -1,31 +1,24 @@ -import Joi from 'joi'; +import { z } from 'zod'; -import { Condition } from './base'; import { contractConditionSchema } from './base/contract'; import { rpcConditionSchema } from './base/rpc'; import { timeConditionSchema } from './base/time'; -const OR_OPERATOR = 'or'; -const AND_OPERATOR = 'and'; - -const LOGICAL_OPERATORS = [AND_OPERATOR, OR_OPERATOR]; - -export const compoundConditionSchema = Joi.object({ - operator: Joi.string() - .valid(...LOGICAL_OPERATORS) - .required(), - operands: Joi.array() - .min(2) - .items( - rpcConditionSchema, - timeConditionSchema, - contractConditionSchema, - Joi.link('#compoundCondition') +export const compoundConditionSchema: z.ZodSchema = z.object({ + conditionType: z.literal('compound').default('compound'), + operator: z.enum(['and', 'or']), + operands: z + .array( + z.lazy(() => + z.union([ + rpcConditionSchema, + timeConditionSchema, + contractConditionSchema, + compoundConditionSchema, + ]) + ) ) - .required() - .valid(), -}).id('compoundCondition'); + .min(2), +}); -export class CompoundCondition extends Condition { - public readonly schema = compoundConditionSchema; -} +export type CompoundConditionProps = z.infer; diff --git a/src/conditions/condition-expr.ts b/src/conditions/condition-expr.ts index 61c446e53..a5ff62650 100644 --- a/src/conditions/condition-expr.ts +++ b/src/conditions/condition-expr.ts @@ -2,16 +2,19 @@ import { Conditions as WASMConditions } from '@nucypher/nucypher-core'; import { ethers } from 'ethers'; import { SemVer } from 'semver'; -import { objectEquals, toBytes, toJSON } from '../utils'; +import { toBytes, toJSON } from '../utils'; import { - Condition, + CompoundCondition, ContractCondition, + ContractConditionProps, RpcCondition, + RpcConditionProps, TimeCondition, + TimeConditionProps, } from './base'; -import { BLOCKTIME_METHOD } from './base/time'; -import { CompoundCondition } from './compound-condition'; +import { CompoundConditionProps } from './compound-condition'; +import { Condition, ConditionProps } from './condition'; import { ConditionContext } from './context'; export type ConditionExpressionJSON = { @@ -28,13 +31,28 @@ export class ConditionExpression { ) {} public toObj(): ConditionExpressionJSON { - const conditionData = this.condition.toObj(); + const condition = this.condition.toObj(); return { version: this.version, - condition: conditionData, + condition, }; } + private static conditionFromObject(obj: ConditionProps): Condition { + switch (obj.conditionType) { + case 'rpc': + return new RpcCondition(obj as RpcConditionProps); + case 'time': + return new TimeCondition(obj as TimeConditionProps); + case 'contract': + return new ContractCondition(obj as ContractConditionProps); + case 'compound': + return new CompoundCondition(obj as CompoundConditionProps); + default: + throw new Error(`Invalid conditionType: ${obj.conditionType}`); + } + } + public static fromObj(obj: ConditionExpressionJSON): ConditionExpression { const receivedVersion = new SemVer(obj.version); const currentVersion = new SemVer(ConditionExpression.VERSION); @@ -44,31 +62,15 @@ export class ConditionExpression { ); } - const underlyingConditionData = obj.condition; - let condition: Condition | undefined; - - if (underlyingConditionData.operator) { - condition = new CompoundCondition(underlyingConditionData); - } else if (underlyingConditionData.method) { - if (underlyingConditionData.method === BLOCKTIME_METHOD) { - condition = new TimeCondition(underlyingConditionData); - } else if (underlyingConditionData.contractAddress) { - condition = new ContractCondition(underlyingConditionData); - } else if ( - (underlyingConditionData.method as string).startsWith('eth_') - ) { - condition = new RpcCondition(underlyingConditionData); - } - } - - if (!condition) { + if (!obj.condition) { throw new Error( `Invalid condition: unrecognized condition data ${JSON.stringify( - underlyingConditionData + obj.condition )}` ); } + const condition = this.conditionFromObject(obj.condition as ConditionProps); return new ConditionExpression(condition, obj.version); } @@ -97,7 +99,7 @@ export class ConditionExpression { public equals(other: ConditionExpression): boolean { return [ this.version === other.version, - objectEquals(this.condition.toObj(), other.condition.toObj()), + this.condition.equals(other.condition), ].every(Boolean); } } diff --git a/src/conditions/condition.ts b/src/conditions/condition.ts new file mode 100644 index 000000000..d0a640497 --- /dev/null +++ b/src/conditions/condition.ts @@ -0,0 +1,63 @@ +import { z } from 'zod'; + +import { objectEquals } from '../utils'; + +import { + ContractConditionProps, + RpcConditionProps, + TimeConditionProps, +} from './base'; +import { CompoundConditionProps } from './compound-condition'; + +// Not using discriminated union because of inconsistent Zod types +// Some conditions have ZodEffect types because of .refine() calls +export type ConditionProps = + | RpcConditionProps + | TimeConditionProps + | ContractConditionProps + | CompoundConditionProps; + +export class Condition { + constructor( + public readonly schema: z.ZodSchema, + public readonly value: + | RpcConditionProps + | TimeConditionProps + | ContractConditionProps + | CompoundConditionProps + ) {} + + public validate(override: Partial = {}): { + data?: ConditionProps; + error?: z.ZodError; + } { + const newValue = { + ...this.value, + ...override, + }; + const result = this.schema.safeParse(newValue); + if (result.success) { + return { data: result.data }; + } + return { error: result.error }; + } + + public toObj() { + const { data, error } = this.validate(this.value); + if (error) { + throw new Error(`Invalid condition: ${JSON.stringify(error.issues)}`); + } + return data; + } + + public static fromObj( + this: new (...args: unknown[]) => T, + obj: Record + ): T { + return new this(obj); + } + + public equals(other: Condition) { + return objectEquals(this, other); + } +} diff --git a/src/conditions/const.ts b/src/conditions/const.ts index 19f336649..3a8b941d2 100644 --- a/src/conditions/const.ts +++ b/src/conditions/const.ts @@ -1,12 +1,3 @@ -import { ChainId } from '../types'; - -export const SUPPORTED_CHAINS = [ - ChainId.MAINNET, - ChainId.GOERLI, - ChainId.POLYGON, - ChainId.MUMBAI, -]; - export const USER_ADDRESS_PARAM = ':userAddress'; export const ETH_ADDRESS_REGEXP = new RegExp('^0x[a-fA-F0-9]{40}$'); diff --git a/src/conditions/context/context.ts b/src/conditions/context/context.ts index fcf9939c3..72b6f64a7 100644 --- a/src/conditions/context/context.ts +++ b/src/conditions/context/context.ts @@ -2,7 +2,7 @@ import { Conditions as WASMConditions } from '@nucypher/nucypher-core'; import { ethers } from 'ethers'; import { fromJSON, toJSON } from '../../utils'; -import { Condition } from '../base'; +import { Condition } from '../condition'; import { USER_ADDRESS_PARAM } from '../const'; import { TypedSignature, WalletAuthenticationProvider } from './providers'; diff --git a/src/conditions/context/index.ts b/src/conditions/context/index.ts index 72a893ff3..e18afda2a 100644 --- a/src/conditions/context/index.ts +++ b/src/conditions/context/index.ts @@ -1,2 +1 @@ -export { ConditionContext } from './context'; -export type { CustomContextParam } from './context'; +export { ConditionContext, type CustomContextParam } from './context'; diff --git a/src/conditions/index.ts b/src/conditions/index.ts index 2d5661fdb..365b87a63 100644 --- a/src/conditions/index.ts +++ b/src/conditions/index.ts @@ -2,9 +2,13 @@ import * as base from './base'; import * as predefined from './predefined'; export { predefined, base }; -export { Condition } from './base/condition'; -export type { ConditionExpressionJSON } from './condition-expr'; -export { ConditionExpression } from './condition-expr'; -export { CompoundCondition } from './compound-condition'; -export type { CustomContextParam } from './context'; -export { ConditionContext } from './context'; +export { + ConditionExpression, + type ConditionExpressionJSON, +} from './condition-expr'; +export { ConditionContext, type CustomContextParam } from './context'; +export { Condition, type ConditionProps } from './condition'; +export { + compoundConditionSchema, + type CompoundConditionProps, +} from './compound-condition'; diff --git a/src/conditions/predefined/erc721.ts b/src/conditions/predefined/erc721.ts index beed9a5ce..9fcfe22d5 100644 --- a/src/conditions/predefined/erc721.ts +++ b/src/conditions/predefined/erc721.ts @@ -1,21 +1,35 @@ -import { ContractCondition } from '../base'; +import { ContractCondition, ContractConditionProps } from '../base'; import { USER_ADDRESS_PARAM } from '../const'; +// TODO: Rewrite these using Zod schemas? + +type ERC721OwnershipFields = 'contractAddress' | 'chain' | 'parameters'; + +const ERC721OwnershipDefaults: Omit< + ContractConditionProps, + ERC721OwnershipFields +> = { + conditionType: 'contract', + method: 'ownerOf', + standardContractType: 'ERC721', + returnValueTest: { + index: 0, + comparator: '==', + value: USER_ADDRESS_PARAM, + }, +}; + export class ERC721Ownership extends ContractCondition { - public readonly defaults = { - method: 'ownerOf', - parameters: [], - standardContractType: 'ERC721', - returnValueTest: { - index: 0, - comparator: '==', - value: USER_ADDRESS_PARAM, - }, - }; + constructor(value: Pick) { + super({ ...ERC721OwnershipDefaults, ...value }); + } } -export class ERC721Balance extends ContractCondition { - public readonly defaults = { +type ERC721BalanceFields = 'contractAddress' | 'chain'; + +const ERC721BalanceDefaults: Omit = + { + conditionType: 'contract', method: 'balanceOf', parameters: [USER_ADDRESS_PARAM], standardContractType: 'ERC721', @@ -25,4 +39,9 @@ export class ERC721Balance extends ContractCondition { value: '0', }, }; + +export class ERC721Balance extends ContractCondition { + constructor(value: Pick) { + super({ ...ERC721BalanceDefaults, ...value }); + } } diff --git a/src/index.ts b/src/index.ts index 0045b30bc..0382830de 100644 --- a/src/index.ts +++ b/src/index.ts @@ -22,10 +22,8 @@ export { getPorterUri } from './porter'; export { PolicyMessageKit } from './kits/message'; // Conditions -import type { CustomContextParam } from './conditions'; import * as conditions from './conditions'; -// TODO: Not sure how to re-export this type from the conditions module -export { conditions, CustomContextParam }; +export { conditions }; // DKG export { FerveoVariant } from '@nucypher/nucypher-core'; diff --git a/src/utils.ts b/src/utils.ts index 3fc45a09a..b74a43a1a 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -65,10 +65,6 @@ export const zip = ( export const toEpoch = (date: Date) => (date.getTime() / 1000) | 0; -export const bytesEquals = (first: Uint8Array, second: Uint8Array): boolean => - first.length === second.length && - first.every((value, index) => value === second[index]); - export const objectEquals = (a: unknown, b: unknown, strict = true): boolean => deepEqual(a, b, { strict }); diff --git a/test/docs/cbd.test.ts b/test/docs/cbd.test.ts index e657827c7..a95aa87be 100644 --- a/test/docs/cbd.test.ts +++ b/test/docs/cbd.test.ts @@ -8,6 +8,10 @@ import { PreStrategy, SecretKey, } from '../../src'; +import { + ContractCondition, + ContractConditionProps, +} from '../../src/conditions/base'; import { Ursula } from '../../src/porter'; import { toBytes } from '../../src/utils'; import { @@ -24,7 +28,6 @@ import { const { predefined: { ERC721Ownership }, - base: { ContractCondition }, ConditionExpression, } = conditions; @@ -92,7 +95,8 @@ describe('Get Started (CBD PoC)', () => { const newDeployed = await newStrategy.deploy(web3Provider, 'test'); // 5. Encrypt the plaintext & update conditions - const NFTBalanceConfig = { + const NFTBalanceConfig: ContractConditionProps = { + conditionType: 'contract', contractAddress: '0xBC4CA0EdA7647A8aB7C2061c2E118A18a936f13D', standardContractType: 'ERC721', chain: 5, diff --git a/test/integration/enrico.test.ts b/test/integration/enrico.test.ts index d28ea9f77..2dbdfae96 100644 --- a/test/integration/enrico.test.ts +++ b/test/integration/enrico.test.ts @@ -99,7 +99,7 @@ describe('enrico', () => { const policyKey = alice.getPolicyEncryptingKeyFromLabel(label); - const ownsBufficornNFT = ERC721Ownership.fromObj({ + const ownsBufficornNFT = new ERC721Ownership({ contractAddress: '0x1e988ba4692e52Bc50b375bcC8585b95c48AaD77', parameters: [3591], chain: 5, diff --git a/test/integration/pre.test.ts b/test/integration/pre.test.ts index 6546228f5..b14f2bf18 100644 --- a/test/integration/pre.test.ts +++ b/test/integration/pre.test.ts @@ -1,7 +1,7 @@ import { CapsuleFrag, reencrypt } from '@nucypher/nucypher-core'; import { conditions, Enrico, MessageKit, PolicyMessageKit } from '../../src'; -import { CompoundCondition } from '../../src/conditions'; +import { CompoundCondition } from '../../src/conditions/base'; import { RetrievalResult } from '../../src/kits/retrieval'; import { toBytes, zip } from '../../src/utils'; import { fakeAlice, fakeBob, fakeUrsulas, reencryptKFrags } from '../utils'; diff --git a/test/unit/conditions/base/condition.test.ts b/test/unit/conditions/base/condition.test.ts index 2a567434c..baa0882a0 100644 --- a/test/unit/conditions/base/condition.test.ts +++ b/test/unit/conditions/base/condition.test.ts @@ -11,37 +11,45 @@ import { } from '../../testVariables'; describe('validation', () => { - // TODO: Consider: - // Use Condition here with returnTestValue schema - // Refactor returnTestValue to be the part of the Condition - const condition = new ERC721Balance(); + const condition = new ERC721Balance({ + contractAddress: TEST_CONTRACT_ADDR, + chain: TEST_CHAIN_ID, + }); it('accepts a correct schema', async () => { - const result = condition.validate({ - contractAddress: TEST_CONTRACT_ADDR, - chain: TEST_CHAIN_ID, - }); + const result = condition.validate(); expect(result.error).toBeUndefined(); - expect(result.value.contractAddress).toEqual(TEST_CONTRACT_ADDR); + expect(result.data.contractAddress).toEqual(TEST_CONTRACT_ADDR); }); - it('updates on a valid schema value', async () => { - const result = condition.validate({ + it('accepts on a valid value override', async () => { + const validOverride = { chain: TEST_CHAIN_ID, contractAddress: TEST_CONTRACT_ADDR_2, - }); + }; + const result = condition.validate(validOverride); expect(result.error).toBeUndefined(); - expect(result.value.chain).toEqual(TEST_CHAIN_ID); + expect(result.data).toMatchObject(validOverride); }); - it('rejects on an invalid schema value', async () => { - const result = condition.validate({ + it('rejects on an invalid value override', async () => { + const invalidOverride = { chain: -1, contractAddress: TEST_CONTRACT_ADDR, + }; + const result = condition.validate(invalidOverride); + expect(result.error).toBeDefined(); + expect(result.data).toBeUndefined(); + expect(result.error?.format()).toMatchObject({ + chain: { + _errors: [ + 'Invalid literal value, expected 137', + 'Invalid literal value, expected 80001', + 'Invalid literal value, expected 5', + 'Invalid literal value, expected 1', + ], + }, }); - expect(result.error?.message).toEqual( - '"chain" must be one of [1, 5, 137, 80001]' - ); }); }); @@ -56,7 +64,6 @@ describe('serialization', () => { it('serializes predefined conditions', () => { const contract = new ERC721Ownership(testContractConditionObj); expect(contract.toObj()).toEqual({ - ...contract.defaults, ...testContractConditionObj, }); }); diff --git a/test/unit/conditions/base/contract.test.ts b/test/unit/conditions/base/contract.test.ts index ee988bbb6..90276b156 100644 --- a/test/unit/conditions/base/contract.test.ts +++ b/test/unit/conditions/base/contract.test.ts @@ -4,17 +4,21 @@ import { ConditionExpression, CustomContextParam, } from '../../../../src/conditions'; -import { ContractCondition } from '../../../../src/conditions/base'; +import { + ContractCondition, + ContractConditionProps, +} from '../../../../src/conditions/base'; +import { FunctionAbiProps } from '../../../../src/conditions/base/contract'; import { USER_ADDRESS_PARAM } from '../../../../src/conditions/const'; import { fakeWeb3Provider } from '../../../utils'; import { testContractConditionObj, testFunctionAbi } from '../../testVariables'; describe('validation', () => { it('accepts on a valid schema', () => { - const contract = new ContractCondition(testContractConditionObj); - expect(contract.toObj()).toEqual({ - ...testContractConditionObj, - }); + const result = new ContractCondition(testContractConditionObj).validate(); + + expect(result.error).toBeUndefined(); + expect(result.data).toEqual(testContractConditionObj); }); it('rejects an invalid schema', () => { @@ -22,14 +26,16 @@ describe('validation', () => { ...testContractConditionObj, // Intentionally removing `contractAddress` contractAddress: undefined, - }; - const badEvm = new ContractCondition(badContractCondition); - expect(() => badEvm.toObj()).toThrow( - 'Invalid condition: "contractAddress" is required' - ); - - const { error } = badEvm.validate(badContractCondition); - expect(error?.message).toEqual('"contractAddress" is required'); + } as unknown as ContractConditionProps; + const result = new ContractCondition(badContractCondition).validate(); + + expect(result.error).toBeDefined(); + expect(result.data).toBeUndefined(); + expect(result.error?.format()).toMatchObject({ + contractAddress: { + _errors: ['Required'], + }, + }); }); }); @@ -40,6 +46,7 @@ describe('accepts either standardContractType or functionAbi but not both or non { name: '_owner', type: 'address', + internalType: 'address', }, ], name: 'balanceOf', @@ -47,6 +54,7 @@ describe('accepts either standardContractType or functionAbi but not both or non { name: 'balance', type: 'uint256', + internalType: 'uint256', }, ], stateMutability: 'view', @@ -58,11 +66,11 @@ describe('accepts either standardContractType or functionAbi but not both or non ...testContractConditionObj, standardContractType, functionAbi: undefined, - }; - const contractCondition = new ContractCondition(conditionObj); - expect(contractCondition.toObj()).toEqual({ - ...conditionObj, - }); + } as typeof testContractConditionObj; + const result = new ContractCondition(conditionObj).validate(); + + expect(result.error).toBeUndefined(); + expect(result.data).toEqual(conditionObj); }); it('accepts functionAbi', () => { @@ -70,11 +78,11 @@ describe('accepts either standardContractType or functionAbi but not both or non ...testContractConditionObj, functionAbi, standardContractType: undefined, - }; - const contractCondition = new ContractCondition(conditionObj); - expect(contractCondition.toObj()).toEqual({ - ...conditionObj, - }); + } as typeof testContractConditionObj; + const result = new ContractCondition(conditionObj).validate(); + + expect(result.error).toBeUndefined(); + expect(result.data).toEqual(conditionObj); }); it('rejects both', () => { @@ -82,11 +90,16 @@ describe('accepts either standardContractType or functionAbi but not both or non ...testContractConditionObj, standardContractType, functionAbi, - }; - const contractCondition = new ContractCondition(conditionObj); - expect(() => contractCondition.toObj()).toThrow( - '"value" contains a conflict between exclusive peers [standardContractType, functionAbi]' - ); + } as typeof testContractConditionObj; + const result = new ContractCondition(conditionObj).validate(); + + expect(result.error).toBeDefined(); + expect(result.data).toBeUndefined(); + expect(result.error?.format()).toMatchObject({ + _errors: [ + "At most one of the fields 'standardContractType' and 'functionAbi' must be defined", + ], + }); }); it('rejects none', () => { @@ -94,16 +107,21 @@ describe('accepts either standardContractType or functionAbi but not both or non ...testContractConditionObj, standardContractType: undefined, functionAbi: undefined, - }; - const contractCondition = new ContractCondition(conditionObj); - expect(() => contractCondition.toObj()).toThrow( - '"value" must contain at least one of [standardContractType, functionAbi]' - ); + } as typeof testContractConditionObj; + const result = new ContractCondition(conditionObj).validate(); + + expect(result.error).toBeDefined(); + expect(result.data).toBeUndefined(); + expect(result.error?.format()).toMatchObject({ + _errors: [ + "At most one of the fields 'standardContractType' and 'functionAbi' must be defined", + ], + }); }); }); describe('supports custom function abi', () => { - const contractConditionObj = { + const contractConditionObj: ContractConditionProps = { ...testContractConditionObj, standardContractType: undefined, functionAbi: testFunctionAbi, @@ -127,6 +145,7 @@ describe('supports custom function abi', () => { const asJson = await conditionContext .withCustomParams(customParams) .toJson(); + expect(asJson).toBeDefined(); expect(asJson).toContain(USER_ADDRESS_PARAM); expect(asJson).toContain(myCustomParam); @@ -138,8 +157,10 @@ describe('supports custom function abi', () => { functionAbi: { name: 'balanceOf', type: 'function', - inputs: [{ name: '_owner', type: 'address' }], - outputs: [{ name: 'balance', type: 'uint256' }], + inputs: [{ name: '_owner', type: 'address', internalType: 'address' }], + outputs: [ + { name: 'balance', type: 'uint256', internalType: 'uint256' }, + ], stateMutability: 'view', }, }, @@ -149,61 +170,72 @@ describe('supports custom function abi', () => { name: 'get', type: 'function', inputs: [], - outputs: [], + outputs: [ + { name: 'balance', type: 'uint256', internalType: 'uint256' }, + ], stateMutability: 'pure', }, }, ])('accepts well-formed functionAbi', ({ method, functionAbi }) => { - expect(() => - new ContractCondition({ - ...contractConditionObj, - parameters: functionAbi.inputs.map( - (input) => `fake_parameter_${input}` - ), // - functionAbi, - method, - }).toObj() - ).not.toThrow(); + const result = new ContractCondition({ + ...contractConditionObj, + parameters: functionAbi.inputs.map((input) => `fake_parameter_${input}`), // + functionAbi: functionAbi as FunctionAbiProps, + method, + }).validate(); + + expect(result.error).toBeUndefined(); + expect(result.data).toBeDefined(); + expect(result.data?.method).toEqual(method); + expect(result.data?.functionAbi).toEqual(functionAbi); }); it.each([ { method: '1234', - expectedError: '"functionAbi.name" must be a string', + badField: 'name', + expectedErrors: ['Expected string, received number'], functionAbi: { name: 1234, // invalid value type: 'function', - inputs: [{ name: '_owner', type: 'address' }], - outputs: [{ name: 'balance', type: 'uint256' }], + inputs: [{ name: '_owner', type: 'address', internalType: 'address' }], + outputs: [ + { name: 'balance', type: 'uint256', internalType: 'uint256' }, + ], stateMutability: 'view', }, }, { method: 'transfer', - expectedError: '"functionAbi.inputs" must be an array', + badField: 'inputs', + expectedErrors: ['Expected array, received string'], functionAbi: { name: 'transfer', type: 'function', inputs: 'invalid value', // invalid value - outputs: [{ name: '_status', type: 'bool' }], + outputs: [{ name: '_status', type: 'bool', internalType: 'bool' }], stateMutability: 'pure', }, }, { method: 'get', - expectedError: - '"functionAbi.stateMutability" must be one of [view, pure]', + badField: 'stateMutability', + expectedErrors: [ + 'Invalid literal value, expected "view"', + 'Invalid literal value, expected "pure"', + ], functionAbi: { name: 'get', type: 'function', inputs: [], - outputs: [], + outputs: [{ name: 'result', type: 'uint256', internalType: 'uint256' }], stateMutability: 'invalid', // invalid value }, }, { method: 'test', - expectedError: '"functionAbi.outputs" must be an array', + badField: 'outputs', + expectedErrors: ['Expected array, received string'], functionAbi: { name: 'test', type: 'function', @@ -214,26 +246,34 @@ describe('supports custom function abi', () => { }, { method: 'calculatePow', - expectedError: - 'Invalid condition: "parameters" must have the same length as "functionAbi.inputs"', + badField: 'inputs', + expectedErrors: ['Required'], functionAbi: { name: 'calculatePow', type: 'function', // 'inputs': [] // Missing inputs array - outputs: [{ name: 'result', type: 'uint256' }], + outputs: [{ name: 'result', type: 'uint256', internalType: 'uint256' }], stateMutability: 'view', }, }, ])( 'rejects malformed functionAbi', - ({ method, expectedError, functionAbi }) => { - expect(() => - new ContractCondition({ - ...contractConditionObj, - functionAbi, - method, - }).toObj() - ).toThrow(expectedError); + ({ method, badField, expectedErrors, functionAbi }) => { + const result = new ContractCondition({ + ...contractConditionObj, + functionAbi: functionAbi as unknown as FunctionAbiProps, + method, + }).validate(); + + expect(result.error).toBeDefined(); + expect(result.data).toBeUndefined(); + expect(result.error?.format()).toMatchObject({ + functionAbi: { + [badField]: { + _errors: expectedErrors, + }, + }, + }); } ); }); diff --git a/test/unit/conditions/base/rpc.test.ts b/test/unit/conditions/base/rpc.test.ts index ccd65f63a..a830d0f50 100644 --- a/test/unit/conditions/base/rpc.test.ts +++ b/test/unit/conditions/base/rpc.test.ts @@ -3,10 +3,10 @@ import { testRpcConditionObj } from '../../testVariables'; describe('validation', () => { it('accepts on a valid schema', () => { - const rpc = new RpcCondition(testRpcConditionObj); - expect(rpc.toObj()).toEqual({ - ...testRpcConditionObj, - }); + const result = new RpcCondition(testRpcConditionObj).validate(); + + expect(result.error).toBeUndefined(); + expect(result.data).toEqual(testRpcConditionObj); }); it('rejects an invalid schema', () => { @@ -14,16 +14,18 @@ describe('validation', () => { ...testRpcConditionObj, // Intentionally replacing `method` with an invalid method method: 'fake_invalid_method', - }; + } as unknown as typeof testRpcConditionObj; - const badRpc = new RpcCondition(badRpcObj); - expect(() => badRpc.toObj()).toThrow( - 'Invalid condition: "method" must be one of [eth_getBalance, balanceOf]' - ); + const result = new RpcCondition(badRpcObj).validate(); - const { error } = badRpc.validate(badRpcObj); - expect(error?.message).toEqual( - '"method" must be one of [eth_getBalance, balanceOf]' - ); + expect(result.error).toBeDefined(); + expect(result.data).toBeUndefined(); + expect(result.error?.format()).toMatchObject({ + method: { + _errors: [ + "Invalid enum value. Expected 'eth_getBalance' | 'balanceOf', received 'fake_invalid_method'", + ], + }, + }); }); }); diff --git a/test/unit/conditions/base/time.test.ts b/test/unit/conditions/base/time.test.ts index a043c8eba..e2d30cbed 100644 --- a/test/unit/conditions/base/time.test.ts +++ b/test/unit/conditions/base/time.test.ts @@ -1,42 +1,51 @@ -import { TimeCondition } from '../../../../src/conditions/base'; +import { + TimeCondition, + TimeConditionProps, +} from '../../../../src/conditions/base'; +import { ReturnValueTestProps } from '../../../../src/conditions/base/rpc'; describe('validation', () => { - const returnValueTest = { + const returnValueTest: ReturnValueTestProps = { index: 0, comparator: '>', value: '100', }; it('accepts a valid schema', () => { - const timeCondition = new TimeCondition({ + const conditionObj: TimeConditionProps = { returnValueTest, - chain: 5, - }); - expect(timeCondition.toObj()).toEqual({ - returnValueTest, - chain: 5, + conditionType: 'time', method: 'blocktime', - }); + chain: 1, + }; + const result = new TimeCondition(conditionObj).validate(); + + expect(result.error).toBeUndefined(); + expect(result.data).toEqual(conditionObj); }); it('rejects an invalid schema', () => { - const badTimeObj = { + const badObj = { + conditionType: 'time', // Intentionally replacing `returnValueTest` with an invalid test returnValueTest: { ...returnValueTest, comparator: 'not-a-comparator', }, chain: 5, - }; - - const badTimeCondition = new TimeCondition(badTimeObj); - expect(() => badTimeCondition.toObj()).toThrow( - 'Invalid condition: "returnValueTest.comparator" must be one of [==, >, <, >=, <=, !=]' - ); + } as unknown as TimeConditionProps; + const result = new TimeCondition(badObj).validate(); - const { error } = badTimeCondition.validate(badTimeObj); - expect(error?.message).toEqual( - '"returnValueTest.comparator" must be one of [==, >, <, >=, <=, !=]' - ); + expect(result.error).toBeDefined(); + expect(result.data).toBeUndefined(); + expect(result.error?.format()).toMatchObject({ + returnValueTest: { + comparator: { + _errors: [ + "Invalid enum value. Expected '==' | '>' | '<' | '>=' | '<=' | '!=', received 'not-a-comparator'", + ], + }, + }, + }); }); }); diff --git a/test/unit/conditions/compound-condition.test.ts b/test/unit/conditions/compound-condition.test.ts index 396b6fddf..f558fd8c9 100644 --- a/test/unit/conditions/compound-condition.test.ts +++ b/test/unit/conditions/compound-condition.test.ts @@ -1,85 +1,113 @@ -import { CompoundCondition } from '../../../src/conditions'; -import { ERC721Ownership } from '../../../src/conditions/predefined/erc721'; +import { CompoundCondition } from '../../../src/conditions/base'; import { testContractConditionObj, testRpcConditionObj, testTimeConditionObj, } from '../testVariables'; -describe('validate', () => { - const ownsBufficornNFT = ERC721Ownership.fromObj({ - contractAddress: '0x1e988ba4692e52Bc50b375bcC8585b95c48AaD77', - parameters: [3591], - chain: 5, - }).toObj(); - +describe('validation', () => { it('accepts or operator', () => { - const orCondition = new CompoundCondition({ + const conditionObj = { operator: 'or', - operands: [ownsBufficornNFT, testTimeConditionObj], - }).toObj(); + operands: [testContractConditionObj, testTimeConditionObj], + }; + const result = new CompoundCondition(conditionObj).validate(); - expect(orCondition.operator).toEqual('or'); - expect(orCondition.operands).toEqual([ - ownsBufficornNFT, - testTimeConditionObj, - ]); + expect(result.error).toBeUndefined(); + expect(result.data).toEqual({ + ...conditionObj, + conditionType: 'compound', + }); }); it('accepts and operator', () => { - const orCondition = new CompoundCondition({ + const conditionObj = { operator: 'and', operands: [testContractConditionObj, testTimeConditionObj], - }).toObj(); + }; + const result = new CompoundCondition(conditionObj).validate(); - expect(orCondition.operator).toEqual('and'); - expect(orCondition.operands).toEqual([ - testContractConditionObj, - testTimeConditionObj, - ]); + expect(result.error).toBeUndefined(); + expect(result.data).toEqual({ + ...conditionObj, + conditionType: 'compound', + }); }); it('rejects an invalid operator', () => { - expect(() => - new CompoundCondition({ - operator: 'not-an-operator', - operands: [testRpcConditionObj, testTimeConditionObj], - }).toObj() - ).toThrow('"operator" must be one of [and, or]'); + const result = new CompoundCondition({ + operator: 'not-an-operator', + operands: [testRpcConditionObj, testTimeConditionObj], + }).validate(); + + expect(result.error).toBeDefined(); + expect(result.data).toBeUndefined(); + expect(result.error?.format()).toMatchObject({ + operator: { + _errors: [ + "Invalid enum value. Expected 'and' | 'or', received 'not-an-operator'", + ], + }, + }); }); it('rejects invalid number of operands = 0', () => { - expect(() => - new CompoundCondition({ - operator: 'or', - operands: [], - }).toObj() - ).toThrow('"operands" must contain at least 2 items'); + const result = new CompoundCondition({ + operator: 'or', + operands: [], + }).validate(); + + expect(result.error).toBeDefined(); + expect(result.data).toBeUndefined(); + expect(result.error?.format()).toMatchObject({ + operands: { + _errors: ['Array must contain at least 2 element(s)'], + }, + }); }); it('rejects invalid number of operands = 1', () => { - expect(() => - new CompoundCondition({ - operator: 'or', - operands: [testRpcConditionObj], - }).toObj() - ).toThrow('"operands" must contain at least 2 items'); + const result = new CompoundCondition({ + operator: 'or', + operands: [testRpcConditionObj], + }).validate(); + expect(result.error).toBeDefined(); + expect(result.data).toBeUndefined(); + expect(result.error?.format()).toMatchObject({ + operands: { + _errors: ['Array must contain at least 2 element(s)'], + }, + }); }); - it('it allows recursive compound conditions', () => { - const compoundCondition = new CompoundCondition({ + it('accepts recursive compound conditions', () => { + const conditionObj = { + operator: 'and', + operands: [ + testContractConditionObj, + testTimeConditionObj, + testRpcConditionObj, + { + operator: 'or', + operands: [testTimeConditionObj, testContractConditionObj], + }, + ], + }; + const result = new CompoundCondition(conditionObj).validate(); + expect(result.error).toBeUndefined(); + expect(result.data).toEqual({ + conditionType: 'compound', operator: 'and', operands: [ testContractConditionObj, testTimeConditionObj, testRpcConditionObj, { + conditionType: 'compound', operator: 'or', - operands: [ownsBufficornNFT, testContractConditionObj], + operands: [testTimeConditionObj, testContractConditionObj], }, ], - }).toObj(); - expect(compoundCondition.operator).toEqual('and'); - expect(compoundCondition.operands).toHaveLength(4); + }); }); }); diff --git a/test/unit/conditions/condition-expr.test.ts b/test/unit/conditions/condition-expr.test.ts index 806269a7f..bcc78e7a7 100644 --- a/test/unit/conditions/condition-expr.test.ts +++ b/test/unit/conditions/condition-expr.test.ts @@ -1,17 +1,16 @@ import { SemVer } from 'semver'; +import { ConditionExpression } from '../../../src/conditions'; import { CompoundCondition, - ConditionExpression, -} from '../../../src/conditions'; -import { ContractCondition, + ContractConditionProps, RpcCondition, TimeCondition, } from '../../../src/conditions/base'; import { USER_ADDRESS_PARAM } from '../../../src/conditions/const'; import { ERC721Balance } from '../../../src/conditions/predefined'; -import { toJSON } from '../../../src/utils'; +import { objectEquals, toJSON } from '../../../src/utils'; import { TEST_CHAIN_ID, TEST_CONTRACT_ADDR, @@ -35,7 +34,7 @@ describe('condition set', () => { ); const customParamKey = ':customParam'; - const contractConditionWithAbiObj = { + const contractConditionWithAbiObj: ContractConditionProps = { ...testContractConditionObj, standardContractType: undefined, functionAbi: testFunctionAbi, @@ -68,12 +67,12 @@ describe('condition set', () => { const conditionExprCurrentVersion = new ConditionExpression(rpcCondition); it('same version and condition', async () => { - const conditionExprSameCurrentVerstion = new ConditionExpression( + const conditionExprSameCurrentVersion = new ConditionExpression( rpcCondition, ConditionExpression.VERSION ); expect( - conditionExprCurrentVersion.equals(conditionExprSameCurrentVerstion) + conditionExprCurrentVersion.equals(conditionExprSameCurrentVersion) ).toBeTruthy(); }); @@ -159,7 +158,9 @@ describe('condition set', () => { const contractConditionExpr = new ConditionExpression( sameContractCondition ); - expect(erc721ConditionExpr.equals(contractConditionExpr)).toBeTruthy(); + expect( + objectEquals(erc721ConditionExpr.toObj(), contractConditionExpr.toObj()) + ).toBeTruthy(); }); }); @@ -211,50 +212,6 @@ describe('condition set', () => { } ); - it.each([ - // no "operator" nor "method" value - { - version: ConditionExpression.VERSION, - condition: { - randoKey: 'randoValue', - otherKey: 'otherValue', - }, - }, - // invalid "method" and no "contractAddress" - { - version: ConditionExpression.VERSION, - condition: { - method: 'doWhatIWant', - returnValueTest: { - index: 0, - comparator: '>', - value: '100', - }, - chain: 5, - }, - }, - // condition with wrong method "method" and no contract address - { - version: ConditionExpression.VERSION, - condition: { - ...testTimeConditionObj, - method: 'doWhatIWant', - }, - }, - // rpc condition (no contract address) with disallowed method - { - version: ConditionExpression.VERSION, - condition: { - ...testRpcConditionObj, - method: 'isPolicyActive', - }, - }, - ])("can't determine condition type", async (invalidCondition) => { - expect(() => { - ConditionExpression.fromObj(invalidCondition); - }).toThrow('unrecognized condition data'); - }); - it('erc721 condition serialization', async () => { const conditionExpr = new ConditionExpression(erc721BalanceCondition); diff --git a/test/unit/conditions/context.test.ts b/test/unit/conditions/context.test.ts index 035b704d6..853235460 100644 --- a/test/unit/conditions/context.test.ts +++ b/test/unit/conditions/context.test.ts @@ -1,7 +1,9 @@ import { SecretKey } from '@nucypher/nucypher-core'; -import { CustomContextParam } from '../../../src'; -import { ConditionExpression } from '../../../src/conditions'; +import { + ConditionExpression, + CustomContextParam, +} from '../../../src/conditions'; import { ContractCondition, RpcCondition } from '../../../src/conditions/base'; import { USER_ADDRESS_PARAM } from '../../../src/conditions/const'; import { RESERVED_CONTEXT_PARAMS } from '../../../src/conditions/context/context'; diff --git a/test/unit/testVariables.ts b/test/unit/testVariables.ts index 36031e816..c7f85dec8 100644 --- a/test/unit/testVariables.ts +++ b/test/unit/testVariables.ts @@ -1,3 +1,11 @@ +import { + ContractConditionProps, + RpcConditionProps, + TimeConditionProps, +} from '../../src/conditions/base'; +import { FunctionAbiProps } from '../../src/conditions/base/contract'; +import { ReturnValueTestProps } from '../../src/conditions/base/rpc'; + export const aliceSecretKeyBytes = new Uint8Array([ 55, 82, 190, 189, 203, 164, 60, 148, 36, 86, 46, 123, 63, 152, 215, 113, 174, 86, 244, 44, 23, 227, 197, 68, 5, 85, 116, 31, 208, 152, 88, 53, @@ -13,13 +21,14 @@ export const TEST_CONTRACT_ADDR_2 = '0x0000000000000000000000000000000000000002'; export const TEST_CHAIN_ID = 5; -export const testReturnValueTest = { +export const testReturnValueTest: ReturnValueTestProps = { index: 0, comparator: '>', value: '100', }; -export const testTimeConditionObj = { +export const testTimeConditionObj: TimeConditionProps = { + conditionType: 'time', returnValueTest: { index: 0, comparator: '>', @@ -29,14 +38,16 @@ export const testTimeConditionObj = { chain: 5, }; -export const testRpcConditionObj = { +export const testRpcConditionObj: RpcConditionProps = { + conditionType: 'rpc', chain: TEST_CHAIN_ID, method: 'eth_getBalance', parameters: ['0x1e988ba4692e52Bc50b375bcC8585b95c48AaD77'], returnValueTest: testReturnValueTest, }; -export const testContractConditionObj = { +export const testContractConditionObj: ContractConditionProps = { + conditionType: 'contract', contractAddress: '0x0000000000000000000000000000000000000000', chain: 5, standardContractType: 'ERC20', @@ -45,7 +56,7 @@ export const testContractConditionObj = { returnValueTest: testReturnValueTest, }; -export const testFunctionAbi = { +export const testFunctionAbi: FunctionAbiProps = { name: 'myFunction', type: 'function', stateMutability: 'view', diff --git a/yarn.lock b/yarn.lock index 4e46e2b5b..821c42f4a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1390,18 +1390,6 @@ "@ethersproject/properties" "^5.7.0" "@ethersproject/strings" "^5.7.0" -"@hapi/hoek@^9.0.0": - version "9.3.0" - resolved "https://registry.yarnpkg.com/@hapi/hoek/-/hoek-9.3.0.tgz#8368869dcb735be2e7f5cb7647de78e167a251fb" - integrity sha512-/c6rf4UJlmHlC9b5BaNvzAcFv7HZ2QHaV0D4/HNlBdvFnvQq8RI4kYdhyPCl7Xj+oWvTWQ8ujhqS53LIgAe6KQ== - -"@hapi/topo@^5.0.0": - version "5.1.0" - resolved "https://registry.yarnpkg.com/@hapi/topo/-/topo-5.1.0.tgz#dc448e332c6c6e37a4dc02fd84ba8d44b9afb012" - integrity sha512-foQZKJig7Ob0BMAYBfcJk8d77QtOe7Wo4ox7ff1lQYoNNAb6jwcY1ncdoy2e9wQZzvNy7ODZCYJkK8kzmcAnAg== - dependencies: - "@hapi/hoek" "^9.0.0" - "@humanwhocodes/config-array@^0.5.0": version "0.5.0" resolved "https://registry.yarnpkg.com/@humanwhocodes/config-array/-/config-array-0.5.0.tgz#1407967d4c6eecd7388f83acf1eaf4d0c6e58ef9" @@ -1493,6 +1481,13 @@ "@types/node" "*" jest-mock "^27.5.1" +"@jest/expect-utils@^29.6.2": + version "29.6.2" + resolved "https://registry.yarnpkg.com/@jest/expect-utils/-/expect-utils-29.6.2.tgz#1b97f290d0185d264dd9fdec7567a14a38a90534" + integrity sha512-6zIhM8go3RV2IG4aIZaZbxwpOzz3ZiM23oxAlkquOIole+G6TrbeXnykxWYlqF7kz2HlBjdKtca20x9atkEQYg== + dependencies: + jest-get-type "^29.4.3" + "@jest/fake-timers@^27.5.1": version "27.5.1" resolved "https://registry.yarnpkg.com/@jest/fake-timers/-/fake-timers-27.5.1.tgz#76979745ce0579c8a94a4678af7a748eda8ada74" @@ -1545,6 +1540,13 @@ terminal-link "^2.0.0" v8-to-istanbul "^8.1.0" +"@jest/schemas@^29.6.0": + version "29.6.0" + resolved "https://registry.yarnpkg.com/@jest/schemas/-/schemas-29.6.0.tgz#0f4cb2c8e3dca80c135507ba5635a4fd755b0040" + integrity sha512-rxLjXyJBTL4LQeJW3aKo0M/+GkCOXsO+8i9Iu7eDb6KwtP65ayoDsitrdPBtujxQ88k4wI2FNYfa6TOGwSn6cQ== + dependencies: + "@sinclair/typebox" "^0.27.8" + "@jest/source-map@^27.5.1": version "27.5.1" resolved "https://registry.yarnpkg.com/@jest/source-map/-/source-map-27.5.1.tgz#6608391e465add4205eae073b55e7f279e04e8cf" @@ -1595,26 +1597,27 @@ source-map "^0.6.1" write-file-atomic "^3.0.0" -"@jest/types@^26.6.2": - version "26.6.2" - resolved "https://registry.yarnpkg.com/@jest/types/-/types-26.6.2.tgz#bef5a532030e1d88a2f5a6d933f84e97226ed48e" - integrity sha512-fC6QCp7Sc5sX6g8Tvbmj4XUTbyrik0akgRy03yjXbQaBWWNWGE7SGtJk98m0N8nzegD/7SggrUlivxo5ax4KWQ== +"@jest/types@^27.5.1": + version "27.5.1" + resolved "https://registry.yarnpkg.com/@jest/types/-/types-27.5.1.tgz#3c79ec4a8ba61c170bf937bcf9e98a9df175ec80" + integrity sha512-Cx46iJ9QpwQTjIdq5VJu2QTMMs3QlEjI0x1QbBP5W1+nMzyc2XmimiRR/CbX9TO0cPTeUlxWMOu8mslYsJ8DEw== dependencies: "@types/istanbul-lib-coverage" "^2.0.0" "@types/istanbul-reports" "^3.0.0" "@types/node" "*" - "@types/yargs" "^15.0.0" + "@types/yargs" "^16.0.0" chalk "^4.0.0" -"@jest/types@^27.5.1": - version "27.5.1" - resolved "https://registry.yarnpkg.com/@jest/types/-/types-27.5.1.tgz#3c79ec4a8ba61c170bf937bcf9e98a9df175ec80" - integrity sha512-Cx46iJ9QpwQTjIdq5VJu2QTMMs3QlEjI0x1QbBP5W1+nMzyc2XmimiRR/CbX9TO0cPTeUlxWMOu8mslYsJ8DEw== +"@jest/types@^29.6.1": + version "29.6.1" + resolved "https://registry.yarnpkg.com/@jest/types/-/types-29.6.1.tgz#ae79080278acff0a6af5eb49d063385aaa897bf2" + integrity sha512-tPKQNMPuXgvdOn2/Lg9HNfUvjYVGolt04Hp03f5hAk878uwOLikN+JzeLY0HcVgKgFl9Hs3EIqpu3WX27XNhnw== dependencies: + "@jest/schemas" "^29.6.0" "@types/istanbul-lib-coverage" "^2.0.0" "@types/istanbul-reports" "^3.0.0" "@types/node" "*" - "@types/yargs" "^16.0.0" + "@types/yargs" "^17.0.8" chalk "^4.0.0" "@jridgewell/gen-mapping@^0.3.0", "@jridgewell/gen-mapping@^0.3.2": @@ -1693,22 +1696,10 @@ resolved "https://registry.yarnpkg.com/@nucypher/nucypher-core/-/nucypher-core-0.11.0.tgz#696663586d0dd70eacfd433a75adc045fba7c24f" integrity sha512-vr44+Vo1xKH17MHW+bQtm/fzEejVcZ9grSbHVS+KqkTytKbWb8ulX3Uc5AI0gli1FxwNwM5UbfqGE2IRai0dfQ== -"@sideway/address@^4.1.3": - version "4.1.4" - resolved "https://registry.yarnpkg.com/@sideway/address/-/address-4.1.4.tgz#03dccebc6ea47fdc226f7d3d1ad512955d4783f0" - integrity sha512-7vwq+rOHVWjyXxVlR76Agnvhy8I9rpzjosTESvmhNeXOXdZZB15Fl+TI9x1SiHZH5Jv2wTGduSxFDIaq0m3DUw== - dependencies: - "@hapi/hoek" "^9.0.0" - -"@sideway/formula@^3.0.1": - version "3.0.1" - resolved "https://registry.yarnpkg.com/@sideway/formula/-/formula-3.0.1.tgz#80fcbcbaf7ce031e0ef2dd29b1bfc7c3f583611f" - integrity sha512-/poHZJJVjx3L+zVD6g9KgHfYnb443oi7wLu/XKojDviHy6HOEOA6z1Trk5aR1dGcmPenJEgb2sK2I80LeS3MIg== - -"@sideway/pinpoint@^2.0.0": - version "2.0.0" - resolved "https://registry.yarnpkg.com/@sideway/pinpoint/-/pinpoint-2.0.0.tgz#cff8ffadc372ad29fd3f78277aeb29e632cc70df" - integrity sha512-RNiOoTPkptFtSVzQevY/yWtZwf/RxyVnPy/OcA9HBM3MlGDnBEYL5B41H0MTn0Uec8Hi+2qUtTfG2WWZBmMejQ== +"@sinclair/typebox@^0.27.8": + version "0.27.8" + resolved "https://registry.yarnpkg.com/@sinclair/typebox/-/typebox-0.27.8.tgz#6667fac16c436b5434a387a34dedb013198f6e6e" + integrity sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA== "@sinonjs/commons@^1.7.0": version "1.8.6" @@ -1839,13 +1830,13 @@ dependencies: "@types/istanbul-lib-report" "*" -"@types/jest@^26.0.24": - version "26.0.24" - resolved "https://registry.yarnpkg.com/@types/jest/-/jest-26.0.24.tgz#943d11976b16739185913a1936e0de0c4a7d595a" - integrity sha512-E/X5Vib8BWqZNRlDxj9vYXhsDwPYbPINqKF9BsnSoon4RQ0D9moEuLD8txgyypFLH7J4+Lho9Nr/c8H0Fi+17w== +"@types/jest@^29.5.3": + version "29.5.3" + resolved "https://registry.yarnpkg.com/@types/jest/-/jest-29.5.3.tgz#7a35dc0044ffb8b56325c6802a4781a626b05777" + integrity sha512-1Nq7YrO/vJE/FYnqYyw0FS8LdrjExSgIiHyKg7xPpn+yi8Q4huZryKnkJatN1ZRH89Kw2v33/8ZMB7DuZeSLlA== dependencies: - jest-diff "^26.0.0" - pretty-format "^26.0.0" + expect "^29.0.0" + pretty-format "^29.0.0" "@types/json-schema@^7.0.7": version "7.0.12" @@ -1897,13 +1888,6 @@ resolved "https://registry.yarnpkg.com/@types/yargs-parser/-/yargs-parser-21.0.0.tgz#0c60e537fa790f5f9472ed2776c2b71ec117351b" integrity sha512-iO9ZQHkZxHn4mSakYV0vFHAVDyEOIJQrV2uZ06HxEPcx+mt8swXoZHIbaaJ2crJYFfErySgktuTZ3BeLz+XmFA== -"@types/yargs@^15.0.0": - version "15.0.15" - resolved "https://registry.yarnpkg.com/@types/yargs/-/yargs-15.0.15.tgz#e609a2b1ef9e05d90489c2f5f45bbfb2be092158" - integrity sha512-IziEYMU9XoVj8hWg7k+UJrXALkGFjWJhn5QFEv9q4p+v40oZhSuC135M38st8XPjICL7Ey4TV64ferBGUoJhBg== - dependencies: - "@types/yargs-parser" "*" - "@types/yargs@^16.0.0": version "16.0.5" resolved "https://registry.yarnpkg.com/@types/yargs/-/yargs-16.0.5.tgz#12cc86393985735a283e387936398c2f9e5f88e3" @@ -1911,6 +1895,13 @@ dependencies: "@types/yargs-parser" "*" +"@types/yargs@^17.0.8": + version "17.0.24" + resolved "https://registry.yarnpkg.com/@types/yargs/-/yargs-17.0.24.tgz#b3ef8d50ad4aa6aecf6ddc97c580a00f5aa11902" + integrity sha512-6i0aC7jV6QzQB8ne1joVZ0eSFIstHsCrobmOtghM11yGlH0j43FKL2UhWdELkyps0zuf7qVTUVCCR+tgSlyLLw== + dependencies: + "@types/yargs-parser" "*" + "@typescript-eslint/eslint-plugin@^4.0.1": version "4.33.0" resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-4.33.0.tgz#c24dc7c8069c7706bc40d99f6fa87edcb2005276" @@ -2076,7 +2067,7 @@ ansi-escapes@^4.2.1: dependencies: type-fest "^0.21.3" -ansi-regex@^5.0.0, ansi-regex@^5.0.1: +ansi-regex@^5.0.1: version "5.0.1" resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-5.0.1.tgz#082cb2c89c9fe8659a311a53bd6a4dc5301db304" integrity sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ== @@ -3056,16 +3047,16 @@ detect-newline@^3.0.0, detect-newline@^3.1.0: resolved "https://registry.yarnpkg.com/detect-newline/-/detect-newline-3.1.0.tgz#576f5dfc63ae1a192ff192d8ad3af6308991b651" integrity sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA== -diff-sequences@^26.6.2: - version "26.6.2" - resolved "https://registry.yarnpkg.com/diff-sequences/-/diff-sequences-26.6.2.tgz#48ba99157de1923412eed41db6b6d4aa9ca7c0b1" - integrity sha512-Mv/TDa3nZ9sbc5soK+OoA74BsS3mL37yixCvUAQkiuA4Wz6YtwP/K47n2rv2ovzHZvoiQeA5FTQOschKkEwB0Q== - diff-sequences@^27.5.1: version "27.5.1" resolved "https://registry.yarnpkg.com/diff-sequences/-/diff-sequences-27.5.1.tgz#eaecc0d327fd68c8d9672a1e64ab8dccb2ef5327" integrity sha512-k1gCAXAsNgLwEL+Y8Wvl+M6oEFj5bgazfZULpS5CneoPPXRaCCW7dm+q21Ky2VEE5X+VeRDBVg1Pcvvsr4TtNQ== +diff-sequences@^29.4.3: + version "29.4.3" + resolved "https://registry.yarnpkg.com/diff-sequences/-/diff-sequences-29.4.3.tgz#9314bc1fabe09267ffeca9cbafc457d8499a13f2" + integrity sha512-ofrBgwpPhCD85kMKtE9RYFFq6OC1A89oW2vvgWZNCwxrUpRUILopY7lsYyMDSjc8g6U6aiO0Qubg6r4Wgt5ZnA== + diff@^4.0.1: version "4.0.2" resolved "https://registry.yarnpkg.com/diff/-/diff-4.0.2.tgz#60f3aecb89d5fae520c11aa19efc2bb982aade7d" @@ -3527,6 +3518,18 @@ expect@^27.5.1: jest-matcher-utils "^27.5.1" jest-message-util "^27.5.1" +expect@^29.0.0: + version "29.6.2" + resolved "https://registry.yarnpkg.com/expect/-/expect-29.6.2.tgz#7b08e83eba18ddc4a2cf62b5f2d1918f5cd84521" + integrity sha512-iAErsLxJ8C+S02QbLAwgSGSezLQK+XXRDt8IuFXFpwCNw2ECmzZSmjKcCaFVp5VRMk+WAvz6h6jokzEzBFZEuA== + dependencies: + "@jest/expect-utils" "^29.6.2" + "@types/node" "*" + jest-get-type "^29.4.3" + jest-matcher-utils "^29.6.2" + jest-message-util "^29.6.2" + jest-util "^29.6.2" + external-editor@^3.0.3: version "3.1.0" resolved "https://registry.yarnpkg.com/external-editor/-/external-editor-3.1.0.tgz#cb03f740befae03ea4d283caed2741a83f335495" @@ -4601,16 +4604,6 @@ jest-config@^27.5.1: slash "^3.0.0" strip-json-comments "^3.1.1" -jest-diff@^26.0.0: - version "26.6.2" - resolved "https://registry.yarnpkg.com/jest-diff/-/jest-diff-26.6.2.tgz#1aa7468b52c3a68d7d5c5fdcdfcd5e49bd164394" - integrity sha512-6m+9Z3Gv9wN0WFVasqjCL/06+EFCMTqDEUl/b87HYK2rAPTyfz4ZIuSlPhY51PIQRWx5TaxeF1qmXKe9gfN3sA== - dependencies: - chalk "^4.0.0" - diff-sequences "^26.6.2" - jest-get-type "^26.3.0" - pretty-format "^26.6.2" - jest-diff@^27.5.1: version "27.5.1" resolved "https://registry.yarnpkg.com/jest-diff/-/jest-diff-27.5.1.tgz#a07f5011ac9e6643cf8a95a462b7b1ecf6680def" @@ -4621,6 +4614,16 @@ jest-diff@^27.5.1: jest-get-type "^27.5.1" pretty-format "^27.5.1" +jest-diff@^29.6.2: + version "29.6.2" + resolved "https://registry.yarnpkg.com/jest-diff/-/jest-diff-29.6.2.tgz#c36001e5543e82a0805051d3ceac32e6825c1c46" + integrity sha512-t+ST7CB9GX5F2xKwhwCf0TAR17uNDiaPTZnVymP9lw0lssa9vG+AFyDZoeIHStU3WowFFwT+ky+er0WVl2yGhA== + dependencies: + chalk "^4.0.0" + diff-sequences "^29.4.3" + jest-get-type "^29.4.3" + pretty-format "^29.6.2" + jest-docblock@^27.5.1: version "27.5.1" resolved "https://registry.yarnpkg.com/jest-docblock/-/jest-docblock-27.5.1.tgz#14092f364a42c6108d42c33c8cf30e058e25f6c0" @@ -4664,16 +4667,16 @@ jest-environment-node@^27.5.1: jest-mock "^27.5.1" jest-util "^27.5.1" -jest-get-type@^26.3.0: - version "26.3.0" - resolved "https://registry.yarnpkg.com/jest-get-type/-/jest-get-type-26.3.0.tgz#e97dc3c3f53c2b406ca7afaed4493b1d099199e0" - integrity sha512-TpfaviN1R2pQWkIihlfEanwOXK0zcxrKEE4MlU6Tn7keoXdN6/3gK/xl0yEh8DOunn5pOVGKf8hB4R9gVh04ig== - jest-get-type@^27.5.1: version "27.5.1" resolved "https://registry.yarnpkg.com/jest-get-type/-/jest-get-type-27.5.1.tgz#3cd613c507b0f7ace013df407a1c1cd578bcb4f1" integrity sha512-2KY95ksYSaK7DMBWQn6dQz3kqAf3BB64y2udeG+hv4KfSOb9qwcYQstTJc1KCbsix+wLZWZYN8t7nwX3GOBLRw== +jest-get-type@^29.4.3: + version "29.4.3" + resolved "https://registry.yarnpkg.com/jest-get-type/-/jest-get-type-29.4.3.tgz#1ab7a5207c995161100b5187159ca82dd48b3dd5" + integrity sha512-J5Xez4nRRMjk8emnTpWrlkyb9pfRQQanDrvWHhsR1+VUfbwxi30eVcZFlcdGInRibU4G5LwHXpI7IRHU0CY+gg== + jest-haste-map@^27.5.1: version "27.5.1" resolved "https://registry.yarnpkg.com/jest-haste-map/-/jest-haste-map-27.5.1.tgz#9fd8bd7e7b4fa502d9c6164c5640512b4e811e7f" @@ -4735,6 +4738,16 @@ jest-matcher-utils@^27.5.1: jest-get-type "^27.5.1" pretty-format "^27.5.1" +jest-matcher-utils@^29.6.2: + version "29.6.2" + resolved "https://registry.yarnpkg.com/jest-matcher-utils/-/jest-matcher-utils-29.6.2.tgz#39de0be2baca7a64eacb27291f0bd834fea3a535" + integrity sha512-4LiAk3hSSobtomeIAzFTe+N8kL6z0JtF3n6I4fg29iIW7tt99R7ZcIFW34QkX+DuVrf+CUe6wuVOpm7ZKFJzZQ== + dependencies: + chalk "^4.0.0" + jest-diff "^29.6.2" + jest-get-type "^29.4.3" + pretty-format "^29.6.2" + jest-message-util@^27.5.1: version "27.5.1" resolved "https://registry.yarnpkg.com/jest-message-util/-/jest-message-util-27.5.1.tgz#bdda72806da10d9ed6425e12afff38cd1458b6cf" @@ -4750,6 +4763,21 @@ jest-message-util@^27.5.1: slash "^3.0.0" stack-utils "^2.0.3" +jest-message-util@^29.6.2: + version "29.6.2" + resolved "https://registry.yarnpkg.com/jest-message-util/-/jest-message-util-29.6.2.tgz#af7adc2209c552f3f5ae31e77cf0a261f23dc2bb" + integrity sha512-vnIGYEjoPSuRqV8W9t+Wow95SDp6KPX2Uf7EoeG9G99J2OVh7OSwpS4B6J0NfpEIpfkBNHlBZpA2rblEuEFhZQ== + dependencies: + "@babel/code-frame" "^7.12.13" + "@jest/types" "^29.6.1" + "@types/stack-utils" "^2.0.0" + chalk "^4.0.0" + graceful-fs "^4.2.9" + micromatch "^4.0.4" + pretty-format "^29.6.2" + slash "^3.0.0" + stack-utils "^2.0.3" + jest-mock@^27.5.1: version "27.5.1" resolved "https://registry.yarnpkg.com/jest-mock/-/jest-mock-27.5.1.tgz#19948336d49ef4d9c52021d34ac7b5f36ff967d6" @@ -4896,6 +4924,18 @@ jest-util@^27.0.0, jest-util@^27.5.1: graceful-fs "^4.2.9" picomatch "^2.2.3" +jest-util@^29.6.2: + version "29.6.2" + resolved "https://registry.yarnpkg.com/jest-util/-/jest-util-29.6.2.tgz#8a052df8fff2eebe446769fd88814521a517664d" + integrity sha512-3eX1qb6L88lJNCFlEADKOkjpXJQyZRiavX1INZ4tRnrBVr2COd3RgcTLyUiEXMNBlDU/cgYq6taUS0fExrWW4w== + dependencies: + "@jest/types" "^29.6.1" + "@types/node" "*" + chalk "^4.0.0" + ci-info "^3.2.0" + graceful-fs "^4.2.9" + picomatch "^2.2.3" + jest-validate@^27.5.1: version "27.5.1" resolved "https://registry.yarnpkg.com/jest-validate/-/jest-validate-27.5.1.tgz#9197d54dc0bdb52260b8db40b46ae668e04df067" @@ -4939,17 +4979,6 @@ jest@^27.0.6: import-local "^3.0.2" jest-cli "^27.5.1" -joi@^17.7.0: - version "17.9.2" - resolved "https://registry.yarnpkg.com/joi/-/joi-17.9.2.tgz#8b2e4724188369f55451aebd1d0b1d9482470690" - integrity sha512-Itk/r+V4Dx0V3c7RLFdRh12IOjySm2/WGPMubBT92cQvRfYZhPM2W0hZlctjj72iES8jsRCwp7S/cRmWBnJ4nw== - dependencies: - "@hapi/hoek" "^9.0.0" - "@hapi/topo" "^5.0.0" - "@sideway/address" "^4.1.3" - "@sideway/formula" "^3.0.1" - "@sideway/pinpoint" "^2.0.0" - js-sha3@0.8.0, js-sha3@^0.8.0: version "0.8.0" resolved "https://registry.yarnpkg.com/js-sha3/-/js-sha3-0.8.0.tgz#b9b7a5da73afad7dedd0f8c463954cbde6818840" @@ -5842,16 +5871,6 @@ prettier@^2.1.1, prettier@^2.1.2: resolved "https://registry.yarnpkg.com/prettier/-/prettier-2.8.8.tgz#e8c5d7e98a4305ffe3de2e1fc4aca1a71c28b1da" integrity sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q== -pretty-format@^26.0.0, pretty-format@^26.6.2: - version "26.6.2" - resolved "https://registry.yarnpkg.com/pretty-format/-/pretty-format-26.6.2.tgz#e35c2705f14cb7fe2fe94fa078345b444120fc93" - integrity sha512-7AeGuCYNGmycyQbCqd/3PWH4eOoX/OiCa0uphp57NVTeAGdJGaAliecxwBDHYQCIvrW7aDBZCYeNTP/WX69mkg== - dependencies: - "@jest/types" "^26.6.2" - ansi-regex "^5.0.0" - ansi-styles "^4.0.0" - react-is "^17.0.1" - pretty-format@^27.5.1: version "27.5.1" resolved "https://registry.yarnpkg.com/pretty-format/-/pretty-format-27.5.1.tgz#2181879fdea51a7a5851fb39d920faa63f01d88e" @@ -5861,6 +5880,15 @@ pretty-format@^27.5.1: ansi-styles "^5.0.0" react-is "^17.0.1" +pretty-format@^29.0.0, pretty-format@^29.6.2: + version "29.6.2" + resolved "https://registry.yarnpkg.com/pretty-format/-/pretty-format-29.6.2.tgz#3d5829261a8a4d89d8b9769064b29c50ed486a47" + integrity sha512-1q0oC8eRveTg5nnBEWMXAU2qpv65Gnuf2eCQzSjxpWFkPaPARwqZZDGuNE0zPAZfTCHzIk3A8dIjwlQKKLphyg== + dependencies: + "@jest/schemas" "^29.6.0" + ansi-styles "^5.0.0" + react-is "^18.0.0" + process-nextick-args@~2.0.0: version "2.0.1" resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-2.0.1.tgz#7820d9b16120cc55ca9ae7792680ae7dba6d7fe2" @@ -5921,6 +5949,11 @@ react-is@^17.0.1: resolved "https://registry.yarnpkg.com/react-is/-/react-is-17.0.2.tgz#e691d4a8e9c789365655539ab372762b0efb54f0" integrity sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w== +react-is@^18.0.0: + version "18.2.0" + resolved "https://registry.yarnpkg.com/react-is/-/react-is-18.2.0.tgz#199431eeaaa2e09f86427efbb4f1473edb47609b" + integrity sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w== + read-pkg-up@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/read-pkg-up/-/read-pkg-up-3.0.0.tgz#3ed496685dba0f8fe118d0691dc51f4a1ff96f07" @@ -7282,3 +7315,8 @@ yocto-queue@^0.1.0: version "0.1.0" resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-0.1.0.tgz#0294eb3dee05028d31ee1a5fa2c556a6aaf10a1b" integrity sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q== + +zod@^3.22.1: + version "3.22.1" + resolved "https://registry.yarnpkg.com/zod/-/zod-3.22.1.tgz#815f850baf933fef96c1061322dbe579b1a80c27" + integrity sha512-+qUhAMl414+Elh+fRNtpU+byrwjDFOS1N7NioLY+tSlcADTx4TkCUua/hxJvxwDXcV4397/nZ420jy4n4+3WUg==