From 86a785c70b5010f1004823f3c6110dbe3973790c Mon Sep 17 00:00:00 2001 From: Piotr Roslaniec Date: Mon, 21 Aug 2023 13:38:26 +0200 Subject: [PATCH 1/3] feat!: add typed conditions api --- package.json | 6 +- src/conditions/base/condition.ts | 49 ----- 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 | 47 ++--- src/conditions/condition-expr.ts | 54 +++--- src/conditions/condition.ts | 68 +++++++ 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 | 125 +++++++++---- 29 files changed, 697 insertions(+), 573 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 0da9ef763..76fade158 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": "^1.5.0", "deep-equal": "^2.2.1", "ethers": "^5.7.2", - "joi": "^17.10.0", "qs": "^6.10.1", - "semver": "^7.5.2" + "semver": "^7.5.2", + "zod": "^3.22.1" }, "devDependencies": { "@babel/core": "^7.22.11", diff --git a/src/conditions/base/condition.ts b/src/conditions/base/condition.ts deleted file mode 100644 index d77fb587e..000000000 --- a/src/conditions/base/condition.ts +++ /dev/null @@ -1,49 +0,0 @@ -import Joi from 'joi'; - -import { objectEquals } from '../../utils'; -import { USER_ADDRESS_PARAM } from '../const'; - -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 requiresSigner(): boolean { - return JSON.stringify(this.value).includes(USER_ADDRESS_PARAM); - } - - 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..33c137a99 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'; +import { contractConditionSchema } from './base'; +import { rpcConditionSchema } from './base'; +import { timeConditionSchema } from './base'; -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 a8fcb6be8..99358f84c 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, CustomContextParam } 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); } @@ -108,7 +110,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..fe957bf32 --- /dev/null +++ b/src/conditions/condition.ts @@ -0,0 +1,68 @@ +import { z } from 'zod'; + +import { objectEquals } from '../utils'; + +import { + ContractConditionProps, + RpcConditionProps, + TimeConditionProps, +} from './base'; +import { CompoundConditionProps } from './compound-condition'; +import { USER_ADDRESS_PARAM } from './const'; + +// 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 requiresSigner(): boolean { + return JSON.stringify(this.value).includes(USER_ADDRESS_PARAM); + } + + 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 0f1f7fc04..b32e5a4e1 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 e0e0c40af..9d863f8c0 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; @@ -91,7 +94,8 @@ describe('Get Started (CBD PoC)', () => { const newDeployed = await newStrategy.deploy(provider, signer, '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 ef926e640..11ff45f29 100644 --- a/test/unit/conditions/base/contract.test.ts +++ b/test/unit/conditions/base/contract.test.ts @@ -2,17 +2,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 { fakeProvider, fakeSigner } 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', () => { @@ -20,14 +24,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'], + }, + }); }); }); @@ -38,6 +44,7 @@ describe('accepts either standardContractType or functionAbi but not both or non { name: '_owner', type: 'address', + internalType: 'address', }, ], name: 'balanceOf', @@ -45,6 +52,7 @@ describe('accepts either standardContractType or functionAbi but not both or non { name: 'balance', type: 'uint256', + internalType: 'uint256', }, ], stateMutability: 'view', @@ -56,11 +64,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', () => { @@ -68,11 +76,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', () => { @@ -80,11 +88,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', () => { @@ -92,16 +105,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, @@ -126,6 +144,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); @@ -137,8 +156,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', }, }, @@ -148,61 +169,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', @@ -213,26 +245,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 126f05df3..3f0f9e501 100644 --- a/test/unit/conditions/context.test.ts +++ b/test/unit/conditions/context.test.ts @@ -1,5 +1,7 @@ -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 89631bb51..1176f978f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1487,18 +1487,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.11.10": version "0.11.10" resolved "https://registry.yarnpkg.com/@humanwhocodes/config-array/-/config-array-0.11.10.tgz#5a3ffe32cc9306365fb3fd572596cd602d5e12d2" @@ -1595,6 +1583,13 @@ "@types/node" "*" jest-mock "^29.6.3" +"@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/expect-utils@^29.6.4": version "29.6.4" resolved "https://registry.yarnpkg.com/@jest/expect-utils/-/expect-utils-29.6.4.tgz#17c7dfe6cec106441f218b0aff4b295f98346679" @@ -1662,6 +1657,13 @@ strip-ansi "^6.0.0" v8-to-istanbul "^9.0.1" +"@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/schemas@^29.6.3": version "29.6.3" resolved "https://registry.yarnpkg.com/@jest/schemas/-/schemas-29.6.3.tgz#430b5ce8a4e0044a7e3819663305a7b3091c8e03" @@ -1805,22 +1807,10 @@ resolved "https://registry.yarnpkg.com/@nucypher/nucypher-core/-/nucypher-core-0.12.0.tgz#0274026f3996601994e9639c06ab79c62c8f5d7c" integrity sha512-hEjjnTSLNqHUiIF6U02j+M8jUOBCOt5mGG78lMHoathbak15AmYX4gkdkHTg2Ssvt5o77Cwzsn5YgGM4Haf7AQ== -"@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== "@sinclair/typebox@^0.27.8": version "0.27.8" @@ -3702,6 +3692,18 @@ expect@^29.0.0, expect@^29.6.4: jest-message-util "^29.6.3" jest-util "^29.6.3" +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" @@ -4901,6 +4903,16 @@ jest-matcher-utils@^29.6.4: jest-get-type "^29.6.3" pretty-format "^29.6.3" +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@^29.6.3: version "29.6.3" resolved "https://registry.yarnpkg.com/jest-message-util/-/jest-message-util-29.6.3.tgz#bce16050d86801b165f20cfde34dc01d3cf85fbf" @@ -4916,6 +4928,21 @@ jest-message-util@^29.6.3: 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@^29.6.3: version "29.6.3" resolved "https://registry.yarnpkg.com/jest-mock/-/jest-mock-29.6.3.tgz#433f3fd528c8ec5a76860177484940628bdf5e0a" @@ -5051,6 +5078,18 @@ jest-util@^29.0.0, jest-util@^29.6.3: 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@^29.6.3: version "29.6.3" resolved "https://registry.yarnpkg.com/jest-validate/-/jest-validate-29.6.3.tgz#a75fca774cfb1c5758c70d035d30a1f9c2784b4d" @@ -5097,17 +5136,6 @@ jest@^29.6.4: import-local "^3.0.2" jest-cli "^29.6.4" -joi@^17.10.0: - version "17.10.0" - resolved "https://registry.yarnpkg.com/joi/-/joi-17.10.0.tgz#04e249daa24d48fada2d34046a8262e474b1326f" - integrity sha512-hrazgRSlhzacZ69LdcKfhi3Vu13z2yFfoAzmEov3yFIJlatTdVGUW6vle1zjH8qkzdCn/qGw8rapjqsObbYXAg== - 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" @@ -6044,6 +6072,15 @@ pretty-format@^29.0.0, pretty-format@^29.6.3: ansi-styles "^5.0.0" react-is "^18.0.0" +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" @@ -6113,6 +6150,11 @@ react-is@^18.0.0: resolved "https://registry.yarnpkg.com/react-is/-/react-is-18.2.0.tgz#199431eeaaa2e09f86427efbb4f1473edb47609b" integrity sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w== +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" @@ -7420,3 +7462,8 @@ yocto-queue@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-1.0.0.tgz#7f816433fb2cbc511ec8bf7d263c3b58a1a3c251" integrity sha512-9bnSc/HEW2uRy67wc+T8UwauLuPJVn28jb+GtJY16iiKWyvmYJRXVT4UamsAEGQfPohgr2q4Tq0sQbQlxTfi1g== + +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== From 8eaf2a7f2cfd08c0de7a8de2034ec27790f6089b Mon Sep 17 00:00:00 2001 From: Piotr Roslaniec Date: Wed, 30 Aug 2023 18:40:54 +0200 Subject: [PATCH 2/3] apply pr suggestions --- src/agents/contracts.ts | 3 +- src/conditions/base/index.ts | 9 +-- src/conditions/base/rpc.ts | 24 ++----- src/conditions/base/shared.ts | 18 ++++++ src/conditions/compound-condition.ts | 6 +- src/conditions/condition-expr.ts | 29 +-------- src/conditions/condition.ts | 64 +++++++++++-------- src/conditions/const.ts | 9 +++ src/conditions/index.ts | 5 +- src/conditions/zod.ts | 31 +++++++++ src/types.ts | 7 -- src/web3.ts | 8 +++ test/unit/conditions/base/condition.test.ts | 34 +--------- test/unit/conditions/base/contract.test.ts | 43 +++++++++---- test/unit/conditions/base/rpc.test.ts | 8 ++- test/unit/conditions/base/time.test.ts | 7 +- .../conditions/compound-condition.test.ts | 26 +++++--- test/unit/conditions/condition-expr.test.ts | 58 +++++++++++++---- test/unit/testVariables.ts | 2 +- 19 files changed, 226 insertions(+), 165 deletions(-) create mode 100644 src/conditions/base/shared.ts create mode 100644 src/conditions/zod.ts diff --git a/src/agents/contracts.ts b/src/agents/contracts.ts index 0ed03352f..a25d89115 100644 --- a/src/agents/contracts.ts +++ b/src/agents/contracts.ts @@ -1,4 +1,5 @@ -import { ChainId, ChecksumAddress } from '../types'; +import { ChecksumAddress } from '../types'; +import { ChainId } from '../web3'; type Contracts = { readonly SUBSCRIPTION_MANAGER: ChecksumAddress | undefined; diff --git a/src/conditions/base/index.ts b/src/conditions/base/index.ts index a6a0abe36..b17914f34 100644 --- a/src/conditions/base/index.ts +++ b/src/conditions/base/index.ts @@ -35,9 +35,6 @@ export class TimeCondition extends Condition { } } -export { - contractConditionSchema, - type ContractConditionProps, -} from './contract'; -export { rpcConditionSchema, type RpcConditionProps } from './rpc'; -export { timeConditionSchema, type TimeConditionProps } from './time'; +export { type ContractConditionProps } from './contract'; +export { type RpcConditionProps } from './rpc'; +export { type TimeConditionProps } from './time'; diff --git a/src/conditions/base/rpc.ts b/src/conditions/base/rpc.ts index 2b16d7823..ea232d50f 100644 --- a/src/conditions/base/rpc.ts +++ b/src/conditions/base/rpc.ts @@ -1,29 +1,15 @@ import { z } from 'zod'; -import { ETH_ADDRESS_REGEXP, USER_ADDRESS_PARAM } from '../const'; +import { SUPPORTED_CHAIN_IDS } from '../const'; +import createUnionSchema from '../zod'; -export const returnValueTestSchema = z.object({ - index: z.number().optional(), - comparator: z.enum(['==', '>', '<', '>=', '<=', '!=']), - value: z.union([z.string(), z.number(), z.boolean()]), -}); - -export type ReturnValueTestProps = z.infer; - -const EthAddressOrUserAddressSchema = z.array( - z.union([z.string().regex(ETH_ADDRESS_REGEXP), z.literal(USER_ADDRESS_PARAM)]) -); +import { EthAddressOrUserAddressSchema, returnValueTestSchema } from './shared'; 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), - ]), + chain: createUnionSchema(SUPPORTED_CHAIN_IDS), method: z.enum(['eth_getBalance', 'balanceOf']), - parameters: EthAddressOrUserAddressSchema, + parameters: z.array(EthAddressOrUserAddressSchema), returnValueTest: returnValueTestSchema, }); diff --git a/src/conditions/base/shared.ts b/src/conditions/base/shared.ts new file mode 100644 index 000000000..904329521 --- /dev/null +++ b/src/conditions/base/shared.ts @@ -0,0 +1,18 @@ +import { z } from 'zod'; + +import { ETH_ADDRESS_REGEXP, USER_ADDRESS_PARAM } from '../const'; + +export const returnValueTestSchema = z.object({ + index: z.number().optional(), + comparator: z.enum(['==', '>', '<', '>=', '<=', '!=']), + value: z.unknown(), +}); + +export type ReturnValueTestProps = z.infer; + +const EthAddressSchema = z.string().regex(ETH_ADDRESS_REGEXP); +const UserAddressSchema = z.literal(USER_ADDRESS_PARAM); +export const EthAddressOrUserAddressSchema = z.union([ + EthAddressSchema, + UserAddressSchema, +]); diff --git a/src/conditions/compound-condition.ts b/src/conditions/compound-condition.ts index 33c137a99..2c88e0bfe 100644 --- a/src/conditions/compound-condition.ts +++ b/src/conditions/compound-condition.ts @@ -1,8 +1,8 @@ import { z } from 'zod'; -import { contractConditionSchema } from './base'; -import { rpcConditionSchema } from './base'; -import { timeConditionSchema } from './base'; +import { contractConditionSchema } from './base/contract'; +import { rpcConditionSchema } from './base/rpc'; +import { timeConditionSchema } from './base/time'; export const compoundConditionSchema: z.ZodSchema = z.object({ conditionType: z.literal('compound').default('compound'), diff --git a/src/conditions/condition-expr.ts b/src/conditions/condition-expr.ts index 99358f84c..697687088 100644 --- a/src/conditions/condition-expr.ts +++ b/src/conditions/condition-expr.ts @@ -4,17 +4,7 @@ import { SemVer } from 'semver'; import { toBytes, toJSON } from '../utils'; -import { - CompoundCondition, - ContractCondition, - ContractConditionProps, - RpcCondition, - RpcConditionProps, - TimeCondition, - TimeConditionProps, -} from './base'; -import { CompoundConditionProps } from './compound-condition'; -import { Condition, ConditionProps } from './condition'; +import { Condition } from './condition'; import { ConditionContext, CustomContextParam } from './context'; export type ConditionExpressionJSON = { @@ -38,21 +28,6 @@ export class ConditionExpression { }; } - 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); @@ -70,7 +45,7 @@ export class ConditionExpression { ); } - const condition = this.conditionFromObject(obj.condition as ConditionProps); + const condition = Condition.fromObj(obj.condition); return new ConditionExpression(condition, obj.version); } diff --git a/src/conditions/condition.ts b/src/conditions/condition.ts index fe957bf32..b16444ef1 100644 --- a/src/conditions/condition.ts +++ b/src/conditions/condition.ts @@ -3,40 +3,40 @@ import { z } from 'zod'; import { objectEquals } from '../utils'; import { + CompoundCondition, + ContractCondition, ContractConditionProps, + RpcCondition, RpcConditionProps, + TimeCondition, TimeConditionProps, } from './base'; import { CompoundConditionProps } from './compound-condition'; import { USER_ADDRESS_PARAM } from './const'; -// 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; +type ConditionSchema = z.ZodSchema; +export type ConditionProps = z.infer; export class Condition { constructor( - public readonly schema: z.ZodSchema, - public readonly value: - | RpcConditionProps - | TimeConditionProps - | ContractConditionProps - | CompoundConditionProps - ) {} + public readonly schema: ConditionSchema, + public readonly value: ConditionProps + ) { + const { data, error } = Condition.validate(schema, value); + if (error) { + throw new Error(`Invalid condition: ${JSON.stringify(error.issues)}`); + } + this.value = data; + } - public validate(override: Partial = {}): { + public static validate( + schema: ConditionSchema, + value: ConditionProps + ): { data?: ConditionProps; error?: z.ZodError; } { - const newValue = { - ...this.value, - ...override, - }; - const result = this.schema.safeParse(newValue); + const result = schema.safeParse(value); if (result.success) { return { data: result.data }; } @@ -48,18 +48,30 @@ export class Condition { } public toObj() { - const { data, error } = this.validate(this.value); + const { data, error } = Condition.validate(this.schema, 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); + 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: ConditionProps): Condition { + return Condition.conditionFromObject(obj); } public equals(other: Condition) { diff --git a/src/conditions/const.ts b/src/conditions/const.ts index 3a8b941d2..9854c6a13 100644 --- a/src/conditions/const.ts +++ b/src/conditions/const.ts @@ -1,3 +1,12 @@ +import { ChainId } from '../web3'; + export const USER_ADDRESS_PARAM = ':userAddress'; export const ETH_ADDRESS_REGEXP = new RegExp('^0x[a-fA-F0-9]{40}$'); + +export const SUPPORTED_CHAIN_IDS = [ + ChainId.POLYGON, + ChainId.MUMBAI, + ChainId.GOERLI, + ChainId.MAINNET, +]; diff --git a/src/conditions/index.ts b/src/conditions/index.ts index 365b87a63..ac71ad556 100644 --- a/src/conditions/index.ts +++ b/src/conditions/index.ts @@ -8,7 +8,4 @@ export { } from './condition-expr'; export { ConditionContext, type CustomContextParam } from './context'; export { Condition, type ConditionProps } from './condition'; -export { - compoundConditionSchema, - type CompoundConditionProps, -} from './compound-condition'; +export { type CompoundConditionProps } from './compound-condition'; diff --git a/src/conditions/zod.ts b/src/conditions/zod.ts new file mode 100644 index 000000000..1829e1201 --- /dev/null +++ b/src/conditions/zod.ts @@ -0,0 +1,31 @@ +import { Primitive, z, ZodLiteral } from 'zod'; + +// Source: https://github.com/colinhacks/zod/issues/831#issuecomment-1063481764 +const createUnion = < + T extends Readonly<[Primitive, Primitive, ...Primitive[]]> +>( + values: T +) => { + const zodLiterals = values.map((value) => z.literal(value)) as unknown as [ + ZodLiteral, + ZodLiteral, + ...ZodLiteral[] + ]; + return z.union(zodLiterals); +}; + +function createUnionSchema(values: T) { + if (values.length === 0) { + return z.never(); + } + + if (values.length === 1) { + return z.literal(values[0]); + } + + return createUnion( + values as unknown as Readonly<[Primitive, Primitive, ...Primitive[]]> + ); +} + +export default createUnionSchema; diff --git a/src/types.ts b/src/types.ts index 2440dcc53..9befc497e 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1,10 +1,3 @@ export type ChecksumAddress = string; export type HexEncodedBytes = string; export type Base64EncodedBytes = string; - -export enum ChainId { - POLYGON = 137, - MUMBAI = 80001, - GOERLI = 5, - MAINNET = 1, -} diff --git a/src/web3.ts b/src/web3.ts index 1b5dcd11a..a55508f8f 100644 --- a/src/web3.ts +++ b/src/web3.ts @@ -1,4 +1,12 @@ import { fromHexString } from './utils'; + +export enum ChainId { + POLYGON = 137, + MUMBAI = 80001, + GOERLI = 5, + MAINNET = 1, +} + export const toCanonicalAddress = (address: string): Uint8Array => { const ETH_ADDRESS_STRING_PREFIX = '0x'; const nonPrefixed = address.startsWith(ETH_ADDRESS_STRING_PREFIX) diff --git a/test/unit/conditions/base/condition.test.ts b/test/unit/conditions/base/condition.test.ts index baa0882a0..e91322af0 100644 --- a/test/unit/conditions/base/condition.test.ts +++ b/test/unit/conditions/base/condition.test.ts @@ -1,3 +1,4 @@ +import { Condition } from '../../../../src/conditions'; import { ContractCondition } from '../../../../src/conditions/base'; import { ERC721Balance, @@ -6,7 +7,6 @@ import { import { TEST_CHAIN_ID, TEST_CONTRACT_ADDR, - TEST_CONTRACT_ADDR_2, testContractConditionObj, } from '../../testVariables'; @@ -17,40 +17,10 @@ describe('validation', () => { }); it('accepts a correct schema', async () => { - const result = condition.validate(); + const result = Condition.validate(condition.schema, condition.value); expect(result.error).toBeUndefined(); expect(result.data.contractAddress).toEqual(TEST_CONTRACT_ADDR); }); - - 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.data).toMatchObject(validOverride); - }); - - 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', - ], - }, - }); - }); }); describe('serialization', () => { diff --git a/test/unit/conditions/base/contract.test.ts b/test/unit/conditions/base/contract.test.ts index 11ff45f29..002467210 100644 --- a/test/unit/conditions/base/contract.test.ts +++ b/test/unit/conditions/base/contract.test.ts @@ -6,14 +6,20 @@ import { ContractCondition, ContractConditionProps, } from '../../../../src/conditions/base'; -import { FunctionAbiProps } from '../../../../src/conditions/base/contract'; +import { + contractConditionSchema, + FunctionAbiProps, +} from '../../../../src/conditions/base/contract'; import { USER_ADDRESS_PARAM } from '../../../../src/conditions/const'; import { fakeProvider, fakeSigner } from '../../../utils'; import { testContractConditionObj, testFunctionAbi } from '../../testVariables'; describe('validation', () => { it('accepts on a valid schema', () => { - const result = new ContractCondition(testContractConditionObj).validate(); + const result = ContractCondition.validate( + contractConditionSchema, + testContractConditionObj + ); expect(result.error).toBeUndefined(); expect(result.data).toEqual(testContractConditionObj); @@ -25,7 +31,10 @@ describe('validation', () => { // Intentionally removing `contractAddress` contractAddress: undefined, } as unknown as ContractConditionProps; - const result = new ContractCondition(badContractCondition).validate(); + const result = ContractCondition.validate( + contractConditionSchema, + badContractCondition + ); expect(result.error).toBeDefined(); expect(result.data).toBeUndefined(); @@ -65,7 +74,10 @@ describe('accepts either standardContractType or functionAbi but not both or non standardContractType, functionAbi: undefined, } as typeof testContractConditionObj; - const result = new ContractCondition(conditionObj).validate(); + const result = ContractCondition.validate( + contractConditionSchema, + conditionObj + ); expect(result.error).toBeUndefined(); expect(result.data).toEqual(conditionObj); @@ -77,7 +89,10 @@ describe('accepts either standardContractType or functionAbi but not both or non functionAbi, standardContractType: undefined, } as typeof testContractConditionObj; - const result = new ContractCondition(conditionObj).validate(); + const result = ContractCondition.validate( + contractConditionSchema, + conditionObj + ); expect(result.error).toBeUndefined(); expect(result.data).toEqual(conditionObj); @@ -89,7 +104,10 @@ describe('accepts either standardContractType or functionAbi but not both or non standardContractType, functionAbi, } as typeof testContractConditionObj; - const result = new ContractCondition(conditionObj).validate(); + const result = ContractCondition.validate( + contractConditionSchema, + conditionObj + ); expect(result.error).toBeDefined(); expect(result.data).toBeUndefined(); @@ -106,7 +124,10 @@ describe('accepts either standardContractType or functionAbi but not both or non standardContractType: undefined, functionAbi: undefined, } as typeof testContractConditionObj; - const result = new ContractCondition(conditionObj).validate(); + const result = ContractCondition.validate( + contractConditionSchema, + conditionObj + ); expect(result.error).toBeDefined(); expect(result.data).toBeUndefined(); @@ -176,12 +197,12 @@ describe('supports custom function abi', () => { }, }, ])('accepts well-formed functionAbi', ({ method, functionAbi }) => { - const result = new ContractCondition({ + const result = ContractCondition.validate(contractConditionSchema, { ...contractConditionObj, parameters: functionAbi.inputs.map((input) => `fake_parameter_${input}`), // functionAbi: functionAbi as FunctionAbiProps, method, - }).validate(); + }); expect(result.error).toBeUndefined(); expect(result.data).toBeDefined(); @@ -258,11 +279,11 @@ describe('supports custom function abi', () => { ])( 'rejects malformed functionAbi', ({ method, badField, expectedErrors, functionAbi }) => { - const result = new ContractCondition({ + const result = ContractCondition.validate(contractConditionSchema, { ...contractConditionObj, functionAbi: functionAbi as unknown as FunctionAbiProps, method, - }).validate(); + }); expect(result.error).toBeDefined(); expect(result.data).toBeUndefined(); diff --git a/test/unit/conditions/base/rpc.test.ts b/test/unit/conditions/base/rpc.test.ts index a830d0f50..c4f658092 100644 --- a/test/unit/conditions/base/rpc.test.ts +++ b/test/unit/conditions/base/rpc.test.ts @@ -1,9 +1,13 @@ import { RpcCondition } from '../../../../src/conditions/base'; +import { rpcConditionSchema } from '../../../../src/conditions/base/rpc'; import { testRpcConditionObj } from '../../testVariables'; describe('validation', () => { it('accepts on a valid schema', () => { - const result = new RpcCondition(testRpcConditionObj).validate(); + const result = RpcCondition.validate( + rpcConditionSchema, + testRpcConditionObj + ); expect(result.error).toBeUndefined(); expect(result.data).toEqual(testRpcConditionObj); @@ -16,7 +20,7 @@ describe('validation', () => { method: 'fake_invalid_method', } as unknown as typeof testRpcConditionObj; - const result = new RpcCondition(badRpcObj).validate(); + const result = RpcCondition.validate(rpcConditionSchema, badRpcObj); expect(result.error).toBeDefined(); expect(result.data).toBeUndefined(); diff --git a/test/unit/conditions/base/time.test.ts b/test/unit/conditions/base/time.test.ts index e2d30cbed..0ace6466f 100644 --- a/test/unit/conditions/base/time.test.ts +++ b/test/unit/conditions/base/time.test.ts @@ -2,7 +2,8 @@ import { TimeCondition, TimeConditionProps, } from '../../../../src/conditions/base'; -import { ReturnValueTestProps } from '../../../../src/conditions/base/rpc'; +import { ReturnValueTestProps } from '../../../../src/conditions/base/shared'; +import { timeConditionSchema } from '../../../../src/conditions/base/time'; describe('validation', () => { const returnValueTest: ReturnValueTestProps = { @@ -18,7 +19,7 @@ describe('validation', () => { method: 'blocktime', chain: 1, }; - const result = new TimeCondition(conditionObj).validate(); + const result = TimeCondition.validate(timeConditionSchema, conditionObj); expect(result.error).toBeUndefined(); expect(result.data).toEqual(conditionObj); @@ -34,7 +35,7 @@ describe('validation', () => { }, chain: 5, } as unknown as TimeConditionProps; - const result = new TimeCondition(badObj).validate(); + const result = TimeCondition.validate(timeConditionSchema, badObj); expect(result.error).toBeDefined(); expect(result.data).toBeUndefined(); diff --git a/test/unit/conditions/compound-condition.test.ts b/test/unit/conditions/compound-condition.test.ts index f558fd8c9..2f81a8bc8 100644 --- a/test/unit/conditions/compound-condition.test.ts +++ b/test/unit/conditions/compound-condition.test.ts @@ -1,4 +1,6 @@ +import { Condition } from '../../../src/conditions'; import { CompoundCondition } from '../../../src/conditions/base'; +import { compoundConditionSchema } from '../../../src/conditions/compound-condition'; import { testContractConditionObj, testRpcConditionObj, @@ -11,7 +13,7 @@ describe('validation', () => { operator: 'or', operands: [testContractConditionObj, testTimeConditionObj], }; - const result = new CompoundCondition(conditionObj).validate(); + const result = Condition.validate(compoundConditionSchema, conditionObj); expect(result.error).toBeUndefined(); expect(result.data).toEqual({ @@ -25,7 +27,10 @@ describe('validation', () => { operator: 'and', operands: [testContractConditionObj, testTimeConditionObj], }; - const result = new CompoundCondition(conditionObj).validate(); + const result = CompoundCondition.validate( + compoundConditionSchema, + conditionObj + ); expect(result.error).toBeUndefined(); expect(result.data).toEqual({ @@ -35,10 +40,10 @@ describe('validation', () => { }); it('rejects an invalid operator', () => { - const result = new CompoundCondition({ + const result = CompoundCondition.validate(compoundConditionSchema, { operator: 'not-an-operator', operands: [testRpcConditionObj, testTimeConditionObj], - }).validate(); + }); expect(result.error).toBeDefined(); expect(result.data).toBeUndefined(); @@ -52,10 +57,10 @@ describe('validation', () => { }); it('rejects invalid number of operands = 0', () => { - const result = new CompoundCondition({ + const result = CompoundCondition.validate(compoundConditionSchema, { operator: 'or', operands: [], - }).validate(); + }); expect(result.error).toBeDefined(); expect(result.data).toBeUndefined(); @@ -67,10 +72,10 @@ describe('validation', () => { }); it('rejects invalid number of operands = 1', () => { - const result = new CompoundCondition({ + const result = CompoundCondition.validate(compoundConditionSchema, { operator: 'or', operands: [testRpcConditionObj], - }).validate(); + }); expect(result.error).toBeDefined(); expect(result.data).toBeUndefined(); expect(result.error?.format()).toMatchObject({ @@ -93,7 +98,10 @@ describe('validation', () => { }, ], }; - const result = new CompoundCondition(conditionObj).validate(); + const result = CompoundCondition.validate( + compoundConditionSchema, + conditionObj + ); expect(result.error).toBeUndefined(); expect(result.data).toEqual({ conditionType: 'compound', diff --git a/test/unit/conditions/condition-expr.test.ts b/test/unit/conditions/condition-expr.test.ts index bcc78e7a7..ff53730e1 100644 --- a/test/unit/conditions/condition-expr.test.ts +++ b/test/unit/conditions/condition-expr.test.ts @@ -7,6 +7,7 @@ import { ContractConditionProps, RpcCondition, TimeCondition, + TimeConditionProps, } from '../../../src/conditions/base'; import { USER_ADDRESS_PARAM } from '../../../src/conditions/const'; import { ERC721Balance } from '../../../src/conditions/predefined'; @@ -66,7 +67,7 @@ describe('condition set', () => { describe('equality', () => { const conditionExprCurrentVersion = new ConditionExpression(rpcCondition); - it('same version and condition', async () => { + it('same version and condition', () => { const conditionExprSameCurrentVersion = new ConditionExpression( rpcCondition, ConditionExpression.VERSION @@ -76,7 +77,7 @@ describe('condition set', () => { ).toBeTruthy(); }); - it('different minor/patch version but same condition', async () => { + it('different minor/patch version but same condition', () => { const conditionExprOlderMinorVersion = new ConditionExpression( rpcCondition, '0.1.0' @@ -96,7 +97,7 @@ describe('condition set', () => { ).not.toBeTruthy(); }); - it('minor/patch number greater than major; still older', async () => { + it('minor/patch number greater than major; still older', () => { const conditionExprOlderMinorVersion = new ConditionExpression( rpcCondition, '0.9.0' @@ -139,7 +140,7 @@ describe('condition set', () => { contractConditionWithAbi, timeCondition, compoundCondition, - ])('same version but different condition', async (condition) => { + ])('same version but different condition', (condition) => { const conditionExprSameVersionDifferentCondition = new ConditionExpression(condition); expect( @@ -149,7 +150,7 @@ describe('condition set', () => { ).not.toBeTruthy(); }); - it('same contract condition although using erc721 helper', async () => { + it('same contract condition although using erc721 helper', () => { const erc721ConditionExpr = new ConditionExpression( erc721BalanceCondition ); @@ -172,7 +173,7 @@ describe('condition set', () => { rpcCondition, timeCondition, compoundCondition, - ])('serializes to and from json', async (condition) => { + ])('serializes to and from json', (condition) => { const conditionExpr = new ConditionExpression(condition); const conditionExprJson = conditionExpr.toJson(); expect(conditionExprJson).toBeDefined(); @@ -187,7 +188,7 @@ describe('condition set', () => { expect(conditionExprFromJson.equals(conditionExprFromJson)).toBeTruthy(); }); - it('incompatible version', async () => { + it('incompatible version', () => { const currentVersion = new SemVer(ConditionExpression.VERSION); const invalidVersion = currentVersion.inc('major'); expect(() => { @@ -202,7 +203,7 @@ describe('condition set', () => { it.each(['version', 'x.y', 'x.y.z', '-1,0.0', '1.0.0.0.0.0.0'])( 'invalid versions', - async (invalidVersion) => { + (invalidVersion) => { expect(() => { ConditionExpression.fromObj({ version: invalidVersion, @@ -212,7 +213,36 @@ describe('condition set', () => { } ); - it('erc721 condition serialization', async () => { + it.each(['_invalid_condition_type_', undefined as unknown as string])( + 'rejects an invalid condition type', + (invalidConditionType) => { + const conditionObj = { + ...testTimeConditionObj, + conditionType: invalidConditionType, + } as unknown as TimeConditionProps; + expect(() => { + ConditionExpression.fromObj({ + version: ConditionExpression.VERSION, + condition: conditionObj, + }); + }).toThrow(`Invalid conditionType: ${invalidConditionType}`); + } + ); + + it('rejects a mismatched condition type', () => { + const conditionObj = { + ...testTimeConditionObj, + conditionType: 'rpc', + } as unknown as TimeConditionProps; + expect(() => { + ConditionExpression.fromObj({ + version: ConditionExpression.VERSION, + condition: conditionObj, + }); + }).toThrow(/^Invalid condition/); + }); + + it('erc721 condition serialization', () => { const conditionExpr = new ConditionExpression(erc721BalanceCondition); const erc721BalanceConditionObj = erc721BalanceCondition.toObj(); @@ -240,7 +270,7 @@ describe('condition set', () => { expect(conditionExprFromJson.condition).toBeInstanceOf(ContractCondition); }); - it('contract condition no abi serialization', async () => { + it('contract condition no abi serialization', () => { const conditionExpr = new ConditionExpression(contractConditionNoAbi); const conditionExprJson = conditionExpr.toJson(); @@ -272,7 +302,7 @@ describe('condition set', () => { expect(conditionExprFromJson.condition).toBeInstanceOf(ContractCondition); }); - it('contract condition with abi serialization', async () => { + it('contract condition with abi serialization', () => { const conditionExpr = new ConditionExpression(contractConditionWithAbi); const conditionExprJson = conditionExpr.toJson(); @@ -305,7 +335,7 @@ describe('condition set', () => { expect(conditionExprFromJson.condition).toBeInstanceOf(ContractCondition); }); - it('time condition serialization', async () => { + it('time condition serialization', () => { const conditionExpr = new ConditionExpression(timeCondition); const conditionExprJson = conditionExpr.toJson(); @@ -328,7 +358,7 @@ describe('condition set', () => { expect(conditionExprFromJson.condition).toBeInstanceOf(TimeCondition); }); - it('rpc condition serialization', async () => { + it('rpc condition serialization', () => { const conditionExpr = new ConditionExpression(rpcCondition); const conditionExprJson = conditionExpr.toJson(); @@ -352,7 +382,7 @@ describe('condition set', () => { expect(conditionExprFromJson.condition).toBeInstanceOf(RpcCondition); }); - it('compound condition serialization', async () => { + it('compound condition serialization', () => { const conditionExpr = new ConditionExpression(compoundCondition); const compoundConditionObj = compoundCondition.toObj(); diff --git a/test/unit/testVariables.ts b/test/unit/testVariables.ts index c7f85dec8..1acfe86eb 100644 --- a/test/unit/testVariables.ts +++ b/test/unit/testVariables.ts @@ -4,7 +4,7 @@ import { TimeConditionProps, } from '../../src/conditions/base'; import { FunctionAbiProps } from '../../src/conditions/base/contract'; -import { ReturnValueTestProps } from '../../src/conditions/base/rpc'; +import { ReturnValueTestProps } from '../../src/conditions/base/shared'; export const aliceSecretKeyBytes = new Uint8Array([ 55, 82, 190, 189, 203, 164, 60, 148, 36, 86, 46, 123, 63, 152, 215, 113, 174, From 84c8ef4b63c68a39e727ec856241d4988c8a1672 Mon Sep 17 00:00:00 2001 From: Piotr Roslaniec Date: Tue, 5 Sep 2023 20:37:24 +0200 Subject: [PATCH 3/3] update after rebase --- src/conditions/zod.ts | 4 +- yarn.lock | 301 ++++++++++++++++++------------------------ 2 files changed, 132 insertions(+), 173 deletions(-) diff --git a/src/conditions/zod.ts b/src/conditions/zod.ts index 1829e1201..3e5ca9e68 100644 --- a/src/conditions/zod.ts +++ b/src/conditions/zod.ts @@ -2,14 +2,14 @@ import { Primitive, z, ZodLiteral } from 'zod'; // Source: https://github.com/colinhacks/zod/issues/831#issuecomment-1063481764 const createUnion = < - T extends Readonly<[Primitive, Primitive, ...Primitive[]]> + T extends Readonly<[Primitive, Primitive, ...Primitive[]]>, >( values: T ) => { const zodLiterals = values.map((value) => z.literal(value)) as unknown as [ ZodLiteral, ZodLiteral, - ...ZodLiteral[] + ...ZodLiteral[], ]; return z.union(zodLiterals); }; diff --git a/yarn.lock b/yarn.lock index 1176f978f..7a7ef1720 100644 --- a/yarn.lock +++ b/yarn.lock @@ -23,12 +23,12 @@ "@babel/highlight" "^7.22.10" chalk "^2.4.2" -"@babel/code-frame@^7.22.10": - version "7.22.10" - resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.22.10.tgz#1c20e612b768fefa75f6e90d6ecb86329247f0a3" - integrity sha512-/KKIMG4UEL35WmI9OlvMhurwtytjvXoFcGNrOvyG9zIzA8YmPjVtIZUf7b05+TPO7G7/GEmLHDaoCgACHl9hhA== +"@babel/code-frame@^7.22.13": + version "7.22.13" + resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.22.13.tgz#e3c1c099402598483b7a8c46a721d1038803755e" + integrity sha512-XktuhWlJ5g+3TJXc5upd9Ks1HutSArik6jf2eAjYFyIOf4ej3RN+184cZbzDvbPnuTJIUhPKKJE3cIsYTiAT3w== dependencies: - "@babel/highlight" "^7.22.10" + "@babel/highlight" "^7.22.13" chalk "^2.4.2" "@babel/compat-data@^7.22.5", "@babel/compat-data@^7.22.6", "@babel/compat-data@^7.22.9": @@ -78,7 +78,7 @@ json5 "^2.2.2" semver "^6.3.1" -"@babel/generator@^7.22.10": +"@babel/generator@^7.22.10", "@babel/generator@^7.7.2": version "7.22.10" resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.22.10.tgz#c92254361f398e160645ac58831069707382b722" integrity sha512-79KIf7YiWjjdZ81JnLujDRApWtl7BxTqWD88+FFdQEIOG8LJ0etDOM7CXuIgGJa55sGOwZVwuEsaLEm0PJ5/+A== @@ -88,12 +88,12 @@ "@jridgewell/trace-mapping" "^0.3.17" jsesc "^2.5.1" -"@babel/generator@^7.22.10", "@babel/generator@^7.7.2": - version "7.22.10" - resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.22.10.tgz#c92254361f398e160645ac58831069707382b722" - integrity sha512-79KIf7YiWjjdZ81JnLujDRApWtl7BxTqWD88+FFdQEIOG8LJ0etDOM7CXuIgGJa55sGOwZVwuEsaLEm0PJ5/+A== +"@babel/generator@^7.22.15": + version "7.22.15" + resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.22.15.tgz#1564189c7ec94cb8f77b5e8a90c4d200d21b2339" + integrity sha512-Zu9oWARBqeVOW0dZOjXc3JObrzuqothQ3y/n1kUtrjCoCPLkXUwMvOo/F/TCfoHMbWIFlWwpZtkZVb9ga4U2pA== dependencies: - "@babel/types" "^7.22.10" + "@babel/types" "^7.22.15" "@jridgewell/gen-mapping" "^0.3.2" "@jridgewell/trace-mapping" "^0.3.17" jsesc "^2.5.1" @@ -112,17 +112,6 @@ dependencies: "@babel/types" "^7.22.10" -"@babel/helper-compilation-targets@^7.22.10": - version "7.22.10" - resolved "https://registry.yarnpkg.com/@babel/helper-compilation-targets/-/helper-compilation-targets-7.22.10.tgz#01d648bbc25dd88f513d862ee0df27b7d4e67024" - integrity sha512-JMSwHD4J7SLod0idLq5PKgI+6g/hLD/iuWBq08ZX49xE14VpVEojJ5rHWptpirV2j020MvypRLAXAO50igCJ5Q== - dependencies: - "@babel/compat-data" "^7.22.9" - "@babel/helper-validator-option" "^7.22.5" - browserslist "^4.21.9" - lru-cache "^5.1.1" - semver "^6.3.1" - "@babel/helper-compilation-targets@^7.22.10", "@babel/helper-compilation-targets@^7.22.5", "@babel/helper-compilation-targets@^7.22.6": version "7.22.10" resolved "https://registry.yarnpkg.com/@babel/helper-compilation-targets/-/helper-compilation-targets-7.22.10.tgz#01d648bbc25dd88f513d862ee0df27b7d4e67024" @@ -270,6 +259,11 @@ resolved "https://registry.yarnpkg.com/@babel/helper-string-parser/-/helper-string-parser-7.22.5.tgz#533f36457a25814cf1df6488523ad547d784a99f" integrity sha512-mM4COjgZox8U+JcXQwPijIZLElkgEpO5rsERVDJTc2qfCDfERyob6k5WegS14SX18IIjv+XD+GrqNumY5JRCDw== +"@babel/helper-validator-identifier@^7.22.15": + version "7.22.15" + resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.15.tgz#601fa28e4cc06786c18912dca138cec73b882044" + integrity sha512-4E/F9IIEi8WR94324mbDUMo074YTheJmd7eZF5vITTeYchqAi6sYXRLHUVsmkdmY4QjfKTcB2jB7dVP3NaBElQ== + "@babel/helper-validator-identifier@^7.22.5": version "7.22.5" resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.5.tgz#9544ef6a33999343c8740fa51350f30eeaaaf193" @@ -289,15 +283,6 @@ "@babel/template" "^7.22.5" "@babel/types" "^7.22.10" -"@babel/helpers@^7.22.11": - version "7.22.11" - resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.22.11.tgz#b02f5d5f2d7abc21ab59eeed80de410ba70b056a" - integrity sha512-vyOXC8PBWaGc5h7GMsNx68OH33cypkEDJCHvYVVgVbbxJDROYVtexSk0gK5iCF1xNjRIN2s8ai7hwkWDq5szWg== - dependencies: - "@babel/template" "^7.22.5" - "@babel/traverse" "^7.22.11" - "@babel/types" "^7.22.11" - "@babel/helpers@^7.22.10": version "7.22.10" resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.22.10.tgz#ae6005c539dfbcb5cd71fb51bfc8a52ba63bc37a" @@ -307,6 +292,15 @@ "@babel/traverse" "^7.22.10" "@babel/types" "^7.22.10" +"@babel/helpers@^7.22.11": + version "7.22.11" + resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.22.11.tgz#b02f5d5f2d7abc21ab59eeed80de410ba70b056a" + integrity sha512-vyOXC8PBWaGc5h7GMsNx68OH33cypkEDJCHvYVVgVbbxJDROYVtexSk0gK5iCF1xNjRIN2s8ai7hwkWDq5szWg== + dependencies: + "@babel/template" "^7.22.5" + "@babel/traverse" "^7.22.11" + "@babel/types" "^7.22.11" + "@babel/highlight@^7.22.10": version "7.22.10" resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.22.10.tgz#02a3f6d8c1cb4521b2fd0ab0da8f4739936137d7" @@ -316,10 +310,10 @@ chalk "^2.4.2" js-tokens "^4.0.0" -"@babel/highlight@^7.22.10": - version "7.22.10" - resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.22.10.tgz#02a3f6d8c1cb4521b2fd0ab0da8f4739936137d7" - integrity sha512-78aUtVcT7MUscr0K5mIEnkwxPE0MaxkR5RxRwuHaQ+JuU5AmTPhY+do2mdzVTnIJJpyBglql2pehuBIWHug+WQ== +"@babel/highlight@^7.22.13": + version "7.22.13" + resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.22.13.tgz#9cda839e5d3be9ca9e8c26b6dd69e7548f0cbf16" + integrity sha512-C/BaXcnnvBCmHTpz/VGZ8jgtE2aYlW4hxDhseJAWZb7gqGM/qtCK6iZUb0TyKFf7BOUsBH7Q7fkRsDRhg1XklQ== dependencies: "@babel/helper-validator-identifier" "^7.22.5" chalk "^2.4.2" @@ -335,6 +329,11 @@ resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.22.11.tgz#becf8ee33aad2a35ed5607f521fe6e72a615f905" integrity sha512-R5zb8eJIBPJriQtbH/htEQy4k7E2dHWlD2Y2VT07JCzwYZHBxV5ZYtM0UhXSNMT74LyxuM+b1jdL7pSesXbC/g== +"@babel/parser@^7.22.15": + version "7.22.15" + resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.22.15.tgz#d34592bfe288a32e741aa0663dbc4829fcd55160" + integrity sha512-RWmQ/sklUN9BvGGpCDgSubhHWfAx24XDTDObup4ffvxaYsptOg2P3KG0j+1eWKLxpkX0j0uHxmpq2Z1SP/VhxA== + "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@^7.22.5": version "7.22.5" resolved "https://registry.yarnpkg.com/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression/-/plugin-bugfix-safari-id-destructuring-collision-in-function-expression-7.22.5.tgz#87245a21cd69a73b0b81bcda98d443d6df08f05e" @@ -1015,7 +1014,7 @@ "@babel/parser" "^7.22.5" "@babel/types" "^7.22.5" -"@babel/traverse@^7.22.10", "@babel/traverse@^7.7.2": +"@babel/traverse@^7.22.10": version "7.22.10" resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.22.10.tgz#20252acb240e746d27c2e82b4484f199cf8141aa" integrity sha512-Q/urqV4pRByiNNpb/f5OSv28ZlGJiFiiTh+GAHktbIrkPhPbl90+uW6SmpoLyZqutrg9AEaEf3Q/ZBRHBXgxig== @@ -1031,7 +1030,23 @@ debug "^4.1.0" globals "^11.1.0" -"@babel/types@^7.0.0", "@babel/types@^7.20.7", "@babel/types@^7.22.10", "@babel/types@^7.22.5", "@babel/types@^7.3.3", "@babel/types@^7.4.4": +"@babel/traverse@^7.22.11": + version "7.22.15" + resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.22.15.tgz#75be4d2d6e216e880e93017f4e2389aeb77ef2d9" + integrity sha512-DdHPwvJY0sEeN4xJU5uRLmZjgMMDIvMPniLuYzUVXj/GGzysPl0/fwt44JBkyUIzGJPV8QgHMcQdQ34XFuKTYQ== + dependencies: + "@babel/code-frame" "^7.22.13" + "@babel/generator" "^7.22.15" + "@babel/helper-environment-visitor" "^7.22.5" + "@babel/helper-function-name" "^7.22.5" + "@babel/helper-hoist-variables" "^7.22.5" + "@babel/helper-split-export-declaration" "^7.22.6" + "@babel/parser" "^7.22.15" + "@babel/types" "^7.22.15" + debug "^4.1.0" + globals "^11.1.0" + +"@babel/types@^7.0.0", "@babel/types@^7.20.7", "@babel/types@^7.22.5", "@babel/types@^7.3.3", "@babel/types@^7.4.4": version "7.22.10" resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.22.10.tgz#4a9e76446048f2c66982d1a989dd12b8a2d2dc03" integrity sha512-obaoigiLrlDZ7TUQln/8m4mSqIW2QFeOrCQc9r+xsaHGNoplVNYlRVpsfE8Vj35GEm2ZH4ZhrNYogs/3fj85kg== @@ -1049,6 +1064,15 @@ "@babel/helper-validator-identifier" "^7.22.5" to-fast-properties "^2.0.0" +"@babel/types@^7.22.15": + version "7.22.15" + resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.22.15.tgz#266cb21d2c5fd0b3931e7a91b6dd72d2f617d282" + integrity sha512-X+NLXr0N8XXmN5ZsaQdm9U2SSC3UbIYq/doL++sueHOTisgZHoKaQtZxGuV2cUPQHMfjKEfg/g6oy7Hm6SKFtA== + dependencies: + "@babel/helper-string-parser" "^7.22.5" + "@babel/helper-validator-identifier" "^7.22.15" + to-fast-properties "^2.0.0" + "@bcoe/v8-coverage@^0.2.3": version "0.2.3" resolved "https://registry.yarnpkg.com/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz#75a2e8b51cb758a7553d6804a5932d7aace75c39" @@ -1721,7 +1745,7 @@ slash "^3.0.0" write-file-atomic "^4.0.2" -"@jest/types@^29.6.3": +"@jest/types@^29.6.1", "@jest/types@^29.6.3": version "29.6.3" resolved "https://registry.yarnpkg.com/@jest/types/-/types-29.6.3.tgz#1131f8cf634e7e84c5e77bab12f052af585fba59" integrity sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw== @@ -1742,7 +1766,7 @@ "@jridgewell/sourcemap-codec" "^1.4.10" "@jridgewell/trace-mapping" "^0.3.9" -"@jridgewell/resolve-uri@^3.0.3", "@jridgewell/resolve-uri@^3.1.0", "@jridgewell/resolve-uri@^3.1.0": +"@jridgewell/resolve-uri@^3.0.3", "@jridgewell/resolve-uri@^3.1.0": version "3.1.1" resolved "https://registry.yarnpkg.com/@jridgewell/resolve-uri/-/resolve-uri-3.1.1.tgz#c08679063f279615a3326583ba3a90d1d82cc721" integrity sha512-dSYZh7HhCDtCKm4QakX0xFpsRDqjjtZf/kjI/v3T3Nwt5r8/qz/M19F9ySyOqU94SXBmeG9ttTul+YnR4LOxFA== @@ -1752,7 +1776,7 @@ resolved "https://registry.yarnpkg.com/@jridgewell/set-array/-/set-array-1.1.2.tgz#7c6cf998d6d20b914c0a55a91ae928ff25965e72" integrity sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw== -"@jridgewell/sourcemap-codec@^1.4.10", "@jridgewell/sourcemap-codec@^1.4.14", "@jridgewell/sourcemap-codec@^1.4.14": +"@jridgewell/sourcemap-codec@^1.4.10", "@jridgewell/sourcemap-codec@^1.4.14": version "1.4.15" resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz#d7c6e6755c78567a951e04ab52ef0fd26de59f32" integrity sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg== @@ -1765,15 +1789,7 @@ "@jridgewell/resolve-uri" "^3.0.3" "@jridgewell/sourcemap-codec" "^1.4.10" -"@jridgewell/trace-mapping@^0.3.12", "@jridgewell/trace-mapping@^0.3.18": - version "0.3.19" - resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.19.tgz#f8a3249862f91be48d3127c3cfe992f79b4b8811" - integrity sha512-kf37QtfW+Hwx/buWGMPcR60iF9ziHa6r/CZJIHbmcm4+0qrXiVdxegAH0F6yddEVQ7zdkjcGCgCzUu+BcbhQxw== - dependencies: - "@jridgewell/resolve-uri" "^3.1.0" - "@jridgewell/sourcemap-codec" "^1.4.14" - -"@jridgewell/trace-mapping@^0.3.17", "@jridgewell/trace-mapping@^0.3.9": +"@jridgewell/trace-mapping@^0.3.12", "@jridgewell/trace-mapping@^0.3.17", "@jridgewell/trace-mapping@^0.3.18", "@jridgewell/trace-mapping@^0.3.9": version "0.3.19" resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.19.tgz#f8a3249862f91be48d3127c3cfe992f79b4b8811" integrity sha512-kf37QtfW+Hwx/buWGMPcR60iF9ziHa6r/CZJIHbmcm4+0qrXiVdxegAH0F6yddEVQ7zdkjcGCgCzUu+BcbhQxw== @@ -1812,11 +1828,6 @@ resolved "https://registry.yarnpkg.com/@sinclair/typebox/-/typebox-0.27.8.tgz#6667fac16c436b5434a387a34dedb013198f6e6e" integrity sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA== -"@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@^3.0.0": version "3.0.0" resolved "https://registry.yarnpkg.com/@sinonjs/commons/-/commons-3.0.0.tgz#beb434fe875d965265e04722ccfc21df7f755d72" @@ -2334,6 +2345,14 @@ available-typed-arrays@^1.0.5: resolved "https://registry.yarnpkg.com/available-typed-arrays/-/available-typed-arrays-1.0.5.tgz#92f95616501069d07d10edb2fc37d3e1c65123b7" integrity sha512-DMD0KiN46eipeziST1LPP/STfDU0sufISXmjSgvVsoU2tqxctQeASejWcfNtxYKqETM1UxQ8sp2OrSBWpHY6sw== +axios@^0.27.2: + version "0.27.2" + resolved "https://registry.yarnpkg.com/axios/-/axios-0.27.2.tgz#207658cc8621606e586c85db4b41a750e756d972" + integrity sha512-t+yRIyySRTp/wua5xEr+z1q60QmLq8ABsS5O9Me1AsE5dfKqgnCFzwiCZZ/cGNd1lq4/7akDWMxdhVlucjmnOQ== + dependencies: + follow-redirects "^1.14.9" + form-data "^4.0.0" + axios@^1.5.0: version "1.5.0" resolved "https://registry.yarnpkg.com/axios/-/axios-1.5.0.tgz#f02e4af823e2e46a9768cfc74691fdd0517ea267" @@ -2343,14 +2362,6 @@ axios@^1.5.0: form-data "^4.0.0" proxy-from-env "^1.1.0" -axios@^0.27.2: - version "0.27.2" - resolved "https://registry.yarnpkg.com/axios/-/axios-0.27.2.tgz#207658cc8621606e586c85db4b41a750e756d972" - integrity sha512-t+yRIyySRTp/wua5xEr+z1q60QmLq8ABsS5O9Me1AsE5dfKqgnCFzwiCZZ/cGNd1lq4/7akDWMxdhVlucjmnOQ== - dependencies: - follow-redirects "^1.14.9" - form-data "^4.0.0" - babel-jest@^29.6.4: version "29.6.4" resolved "https://registry.yarnpkg.com/babel-jest/-/babel-jest-29.6.4.tgz#98dbc45d1c93319c82a8ab4a478b670655dd2585" @@ -2515,11 +2526,6 @@ brotli-size@^4.0.0: dependencies: duplexer "0.1.1" -browser-process-hrtime@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/browser-process-hrtime/-/browser-process-hrtime-1.0.0.tgz#3c9b4b7d782c8121e56f10106d84c0d0ffc94626" - integrity sha512-9o5UecI3GhkpM6DrXr69PblIuWxPKk9Y0jHBRhdocZ2y7YECBFCsHm79Pr3OyR2AvjhDkabFJaDJMYRazHgsow== - browserslist@^4.21.10, browserslist@^4.21.9: version "4.21.10" resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.21.10.tgz#dbbac576628c13d3b2231332cb2ec5a46e015bb0" @@ -3681,17 +3687,6 @@ expand-tilde@^2.0.0, expand-tilde@^2.0.2: dependencies: homedir-polyfill "^1.0.1" -expect@^29.0.0, expect@^29.6.4: - version "29.6.4" - resolved "https://registry.yarnpkg.com/expect/-/expect-29.6.4.tgz#a6e6f66d4613717859b2fe3da98a739437b6f4b8" - integrity sha512-F2W2UyQ8XYyftHT57dtfg8Ue3X5qLgm2sSug0ivvLRH/VKNRL/pDxg/TH7zVzbQB0tu80clNFy6LU7OS/VSEKA== - dependencies: - "@jest/expect-utils" "^29.6.4" - jest-get-type "^29.6.3" - jest-matcher-utils "^29.6.4" - jest-message-util "^29.6.3" - jest-util "^29.6.3" - expect@^29.0.0: version "29.6.2" resolved "https://registry.yarnpkg.com/expect/-/expect-29.6.2.tgz#7b08e83eba18ddc4a2cf62b5f2d1918f5cd84521" @@ -3704,6 +3699,17 @@ expect@^29.0.0: jest-message-util "^29.6.2" jest-util "^29.6.2" +expect@^29.6.4: + version "29.6.4" + resolved "https://registry.yarnpkg.com/expect/-/expect-29.6.4.tgz#a6e6f66d4613717859b2fe3da98a739437b6f4b8" + integrity sha512-F2W2UyQ8XYyftHT57dtfg8Ue3X5qLgm2sSug0ivvLRH/VKNRL/pDxg/TH7zVzbQB0tu80clNFy6LU7OS/VSEKA== + dependencies: + "@jest/expect-utils" "^29.6.4" + jest-get-type "^29.6.3" + jest-matcher-utils "^29.6.4" + jest-message-util "^29.6.3" + jest-util "^29.6.3" + external-editor@^3.0.3: version "3.1.0" resolved "https://registry.yarnpkg.com/external-editor/-/external-editor-3.1.0.tgz#cb03f740befae03ea4d283caed2741a83f335495" @@ -3887,7 +3893,7 @@ flatted@^3.1.0: resolved "https://registry.yarnpkg.com/flatted/-/flatted-3.2.7.tgz#609f39207cb614b89d0765b477cb2d437fbf9787" integrity sha512-5nqDSxl8nn5BSNxyR3n4I6eDmbolI6WT+QqR547RwxQapgjQBmtktdP+HTBb/a/zLsbzERTONyUB5pefh5TtjQ== -follow-redirects@^1.15.0, follow-redirects@^1.14.9: +follow-redirects@^1.14.9, follow-redirects@^1.15.0: version "1.15.2" resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.2.tgz#b460864144ba63f2681096f274c4e57026da2c13" integrity sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA== @@ -3908,15 +3914,6 @@ form-data@^4.0.0: combined-stream "^1.0.8" mime-types "^2.1.12" -form-data@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/form-data/-/form-data-4.0.0.tgz#93919daeaf361ee529584b9b31664dc12c9fa452" - integrity sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww== - dependencies: - asynckit "^0.4.0" - combined-stream "^1.0.8" - mime-types "^2.1.12" - fs-extra@9.1.0: version "9.1.0" resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-9.1.0.tgz#5954460c764a8da2094ba3554bf839e6b9a7c86d" @@ -4470,14 +4467,7 @@ is-callable@^1.1.3, is-callable@^1.1.4, is-callable@^1.2.7: resolved "https://registry.yarnpkg.com/is-callable/-/is-callable-1.2.7.tgz#3bc2a85ea742d9e36205dcacdd72ca1fdc51b055" integrity sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA== -is-core-module@^2.13.0, is-core-module@^2.5.0: - version "2.13.0" - resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.13.0.tgz#bb52aa6e2cbd49a30c2ba68c42bf3435ba6072db" - integrity sha512-Z7dk6Qo8pOCp3l4tsX2C5ZVas4V+UxwQodwZhLopL91TX8UyyHEXafPcyoeeWuLrwzHcr3igO78wNLwHJHsMCQ== - dependencies: - has "^1.0.3" - -is-core-module@^2.13.0, is-core-module@^2.8.1: +is-core-module@^2.13.0, is-core-module@^2.5.0, is-core-module@^2.8.1: version "2.13.0" resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.13.0.tgz#bb52aa6e2cbd49a30c2ba68c42bf3435ba6072db" integrity sha512-Z7dk6Qo8pOCp3l4tsX2C5ZVas4V+UxwQodwZhLopL91TX8UyyHEXafPcyoeeWuLrwzHcr3igO78wNLwHJHsMCQ== @@ -4821,7 +4811,7 @@ jest-config@^29.6.4: slash "^3.0.0" strip-json-comments "^3.1.1" -jest-diff@^29.6.4: +jest-diff@^29.6.2, jest-diff@^29.6.4: version "29.6.4" resolved "https://registry.yarnpkg.com/jest-diff/-/jest-diff-29.6.4.tgz#85aaa6c92a79ae8cd9a54ebae8d5b6d9a513314a" integrity sha512-9F48UxR9e4XOEZvoUXEHSWY4qC4zERJaOfrbBg9JpbJOO43R1vN76REt/aMGZoY6GD5g84nnJiBIVlscegefpw== @@ -4861,7 +4851,7 @@ jest-environment-node@^29.6.4: jest-mock "^29.6.3" jest-util "^29.6.3" -jest-get-type@^29.6.3: +jest-get-type@^29.4.3, jest-get-type@^29.6.3: version "29.6.3" resolved "https://registry.yarnpkg.com/jest-get-type/-/jest-get-type-29.6.3.tgz#36f499fdcea197c1045a127319c0481723908fd1" integrity sha512-zrteXnqYxfQh7l5FHyL38jL39di8H8rHoecLH3JNxH3BwOrBsNeabdap5e0I23lD4HHI8W5VFBZqG4Eaq5LNcw== @@ -4893,6 +4883,16 @@ jest-leak-detector@^29.6.3: jest-get-type "^29.6.3" pretty-format "^29.6.3" +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-matcher-utils@^29.6.4: version "29.6.4" resolved "https://registry.yarnpkg.com/jest-matcher-utils/-/jest-matcher-utils-29.6.4.tgz#327db7ababea49455df3b23e5d6109fe0c709d24" @@ -4903,15 +4903,20 @@ jest-matcher-utils@^29.6.4: jest-get-type "^29.6.3" pretty-format "^29.6.3" -jest-matcher-utils@^29.6.2: +jest-message-util@^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== + 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" - jest-diff "^29.6.2" - jest-get-type "^29.4.3" + graceful-fs "^4.2.9" + micromatch "^4.0.4" pretty-format "^29.6.2" + slash "^3.0.0" + stack-utils "^2.0.3" jest-message-util@^29.6.3: version "29.6.3" @@ -4928,21 +4933,6 @@ jest-message-util@^29.6.3: 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@^29.6.3: version "29.6.3" resolved "https://registry.yarnpkg.com/jest-mock/-/jest-mock-29.6.3.tgz#433f3fd528c8ec5a76860177484940628bdf5e0a" @@ -6063,15 +6053,6 @@ prettier@^3.0.2: resolved "https://registry.yarnpkg.com/prettier/-/prettier-3.0.2.tgz#78fcecd6d870551aa5547437cdae39d4701dca5b" integrity sha512-o2YR9qtniXvwEZlOKbveKfDQVyqxbEIWn48Z8m3ZJjBjcCmUy3xZGIv+7AkaeuaTr6yPXJjwv07ZWlsWbEy1rQ== -pretty-format@^29.0.0, pretty-format@^29.6.3: - version "29.6.3" - resolved "https://registry.yarnpkg.com/pretty-format/-/pretty-format-29.6.3.tgz#d432bb4f1ca6f9463410c3fb25a0ba88e594ace7" - integrity sha512-ZsBgjVhFAj5KeK+nHfF1305/By3lechHQSMWCTl8iHSbfOm2TN5nHEtFc/+W7fAyUeCs2n5iow72gld4gW0xDw== - dependencies: - "@jest/schemas" "^29.6.3" - ansi-styles "^5.0.0" - react-is "^18.0.0" - 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" @@ -6081,6 +6062,15 @@ pretty-format@^29.0.0, pretty-format@^29.6.2: ansi-styles "^5.0.0" react-is "^18.0.0" +pretty-format@^29.6.3: + version "29.6.3" + resolved "https://registry.yarnpkg.com/pretty-format/-/pretty-format-29.6.3.tgz#d432bb4f1ca6f9463410c3fb25a0ba88e594ace7" + integrity sha512-ZsBgjVhFAj5KeK+nHfF1305/By3lechHQSMWCTl8iHSbfOm2TN5nHEtFc/+W7fAyUeCs2n5iow72gld4gW0xDw== + dependencies: + "@jest/schemas" "^29.6.3" + 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" @@ -6094,19 +6084,15 @@ prompts@^2.0.1: kleur "^3.0.3" sisteransi "^1.0.5" -proxy-from-env@^1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/proxy-from-env/-/proxy-from-env-1.1.0.tgz#e102f16ca355424865755d2c9e8ea4f24d58c3e2" - integrity sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg== property-expr@^2.0.4: version "2.0.5" resolved "https://registry.yarnpkg.com/property-expr/-/property-expr-2.0.5.tgz#278bdb15308ae16af3e3b9640024524f4dc02cb4" integrity sha512-IJUkICM5dP5znhCckHSv30Q4b5/JA5enCtkRHYaOVOAocnH/1BQEYTC5NMfT3AVl/iXKdr3aqQbQn9DxyWknwA== -psl@^1.1.33: - version "1.9.0" - resolved "https://registry.yarnpkg.com/psl/-/psl-1.9.0.tgz#d0df2a137f00794565fcaf3b2c00cd09f8d5a5a7" - integrity sha512-E/ZsdU4HLs/68gYzgGTkMicWTLPdAftJLfJFlLUAAKZGkStNU72sZjT66SnMDVOfOWY/YAoiD7Jxa9iHvngcag== +proxy-from-env@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/proxy-from-env/-/proxy-from-env-1.1.0.tgz#e102f16ca355424865755d2c9e8ea4f24d58c3e2" + integrity sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg== punycode@^2.1.0: version "2.3.0" @@ -6150,11 +6136,6 @@ react-is@^18.0.0: resolved "https://registry.yarnpkg.com/react-is/-/react-is-18.2.0.tgz#199431eeaaa2e09f86427efbb4f1473edb47609b" integrity sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w== -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" @@ -6895,23 +6876,6 @@ toposort@^2.0.2: resolved "https://registry.yarnpkg.com/toposort/-/toposort-2.0.2.tgz#ae21768175d1559d48bef35420b2f4962f09c330" integrity sha512-0a5EOkAUp8D4moMi2W8ZF8jcga7BgZd91O/yabJCFY8az+XSzeGyTKs0Aoo897iV1Nj6guFq8orWDS96z91oGg== -tough-cookie@^4.0.0: - version "4.1.3" - resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-4.1.3.tgz#97b9adb0728b42280aa3d814b6b999b2ff0318bf" - integrity sha512-aX/y5pVRkfRnfmuX+OdbSdXvPe6ieKX/G2s7e98f4poJHnqH3281gDPm/metm6E/WRamfx7WC4HUqkWHfQHprw== - dependencies: - psl "^1.1.33" - punycode "^2.1.1" - universalify "^0.2.0" - url-parse "^1.5.3" - -tr46@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/tr46/-/tr46-2.1.0.tgz#fa87aa81ca5d5941da8cbf1f9b749dc969a4e240" - integrity sha512-15Ih7phfcdP5YxqiB+iDtLoaTz4Nd35+IiAv0kQ5FNKHzXgdWqPoTIqEDDJmXceQt4JZk6lVPT8lnDlPpGDppw== - dependencies: - punycode "^2.1.1" - trim-newlines@^3.0.0: version "3.0.1" resolved "https://registry.yarnpkg.com/trim-newlines/-/trim-newlines-3.0.1.tgz#260a5d962d8b752425b32f3a7db0dcacd176c144" @@ -7237,15 +7201,10 @@ v8-compile-cache-lib@^3.0.1: resolved "https://registry.yarnpkg.com/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz#6336e8d71965cb3d35a1bbb7868445a7c05264bf" integrity sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg== -v8-compile-cache@^2.0.3: - version "2.4.0" - resolved "https://registry.yarnpkg.com/v8-compile-cache/-/v8-compile-cache-2.4.0.tgz#cdada8bec61e15865f05d097c5f4fd30e94dc128" - integrity sha512-ocyWc3bAHBB/guyqJQVI5o4BZkPhznPYUG2ea80Gond/BgNWpap8TOmLSeeQG7bnh2KMISxskdADG59j7zruhw== - -v8-to-istanbul@^8.1.0: - version "8.1.1" - resolved "https://registry.yarnpkg.com/v8-to-istanbul/-/v8-to-istanbul-8.1.1.tgz#77b752fd3975e31bbcef938f85e9bd1c7a8d60ed" - integrity sha512-FGtKtv3xIpR6BYhvgH8MI/y78oT7d8Au3ww4QIxymrCtZEh5b8gCw2siywE+puhEmuWKDtmfrvF5UlB298ut3w== +v8-to-istanbul@^9.0.1: + version "9.1.0" + resolved "https://registry.yarnpkg.com/v8-to-istanbul/-/v8-to-istanbul-9.1.0.tgz#1b83ed4e397f58c85c266a570fc2558b5feb9265" + integrity sha512-6z3GW9x8G1gd+JIIgQQQxXuiJtCXeAjp6RaPEPLv62mH3iPHPxV6W3robxtCzNErRo6ZwTmzWhsbNvjyEBKzKA== dependencies: "@jridgewell/trace-mapping" "^0.3.12" "@types/istanbul-lib-coverage" "^2.0.1" @@ -7445,6 +7404,11 @@ yocto-queue@^0.1.0: resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-0.1.0.tgz#0294eb3dee05028d31ee1a5fa2c556a6aaf10a1b" integrity sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q== +yocto-queue@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-1.0.0.tgz#7f816433fb2cbc511ec8bf7d263c3b58a1a3c251" + integrity sha512-9bnSc/HEW2uRy67wc+T8UwauLuPJVn28jb+GtJY16iiKWyvmYJRXVT4UamsAEGQfPohgr2q4Tq0sQbQlxTfi1g== + yup@^0.32.11: version "0.32.11" resolved "https://registry.yarnpkg.com/yup/-/yup-0.32.11.tgz#d67fb83eefa4698607982e63f7ca4c5ed3cf18c5" @@ -7458,11 +7422,6 @@ yup@^0.32.11: property-expr "^2.0.4" toposort "^2.0.2" -yocto-queue@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-1.0.0.tgz#7f816433fb2cbc511ec8bf7d263c3b58a1a3c251" - integrity sha512-9bnSc/HEW2uRy67wc+T8UwauLuPJVn28jb+GtJY16iiKWyvmYJRXVT4UamsAEGQfPohgr2q4Tq0sQbQlxTfi1g== - zod@^3.22.1: version "3.22.1" resolved "https://registry.yarnpkg.com/zod/-/zod-3.22.1.tgz#815f850baf933fef96c1061322dbe579b1a80c27"