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