From 84f19301997d568cc0d8867f5e9d3e6267629838 Mon Sep 17 00:00:00 2001 From: derekpierre Date: Thu, 19 Sep 2024 11:05:55 -0400 Subject: [PATCH 1/9] Use common condition schema for conditions that use multi-conditions. This is not directly exported, but is used internally by other conditions. Update compound condition to use common condition schema. --- .../taco/src/conditions/compound-condition.ts | 17 ++--------------- packages/taco/src/conditions/multi-condition.ts | 17 +++++++++++++++++ 2 files changed, 19 insertions(+), 15 deletions(-) create mode 100644 packages/taco/src/conditions/multi-condition.ts diff --git a/packages/taco/src/conditions/compound-condition.ts b/packages/taco/src/conditions/compound-condition.ts index 09c9186fd..1afac2a52 100644 --- a/packages/taco/src/conditions/compound-condition.ts +++ b/packages/taco/src/conditions/compound-condition.ts @@ -1,9 +1,7 @@ import { z } from 'zod'; -import { contractConditionSchema } from './base/contract'; -import { rpcConditionSchema } from './base/rpc'; -import { timeConditionSchema } from './base/time'; import { Condition, ConditionProps } from './condition'; +import { commonConditionSchema } from './multi-condition'; import { OmitConditionType } from './shared'; export const CompoundConditionType = 'compound'; @@ -14,18 +12,7 @@ export const compoundConditionSchema: z.ZodSchema = z .literal(CompoundConditionType) .default(CompoundConditionType), operator: z.enum(['and', 'or', 'not']), - operands: z - .array( - z.lazy(() => - z.union([ - rpcConditionSchema, - timeConditionSchema, - contractConditionSchema, - compoundConditionSchema, - ]), - ), - ) - .min(1), + operands: z.array(commonConditionSchema).min(1).max(5), }) .refine( (condition) => { diff --git a/packages/taco/src/conditions/multi-condition.ts b/packages/taco/src/conditions/multi-condition.ts new file mode 100644 index 000000000..c3834d6f6 --- /dev/null +++ b/packages/taco/src/conditions/multi-condition.ts @@ -0,0 +1,17 @@ +import { z } from 'zod'; + +import { contractConditionSchema } from './base/contract'; +import { rpcConditionSchema } from './base/rpc'; +import { timeConditionSchema } from './base/time'; +import { compoundConditionSchema } from './compound-condition'; +import { sequentialConditionSchema } from './sequential'; + +export const commonConditionSchema: z.ZodSchema = z.lazy(() => + z.union([ + rpcConditionSchema, + timeConditionSchema, + contractConditionSchema, + compoundConditionSchema, + sequentialConditionSchema, + ]), +); From b7cc7f799e59a755c0d77c7f8ca9ab6f7282db0b Mon Sep 17 00:00:00 2001 From: derekpierre Date: Thu, 19 Sep 2024 11:07:14 -0400 Subject: [PATCH 2/9] Add initial code for sequential conditions based on reuse of existing building blocks. --- packages/taco/src/conditions/index.ts | 1 + packages/taco/src/conditions/sequential.ts | 33 ++++++++++++++++++++++ 2 files changed, 34 insertions(+) create mode 100644 packages/taco/src/conditions/sequential.ts diff --git a/packages/taco/src/conditions/index.ts b/packages/taco/src/conditions/index.ts index 320b52a68..3ec09a76a 100644 --- a/packages/taco/src/conditions/index.ts +++ b/packages/taco/src/conditions/index.ts @@ -6,4 +6,5 @@ export * as condition from './condition'; export * as conditionExpr from './condition-expr'; export { ConditionFactory } from './condition-factory'; export * as context from './context'; +export * as sequential from './sequential'; export { base, predefined }; diff --git a/packages/taco/src/conditions/sequential.ts b/packages/taco/src/conditions/sequential.ts new file mode 100644 index 000000000..b12eedf6e --- /dev/null +++ b/packages/taco/src/conditions/sequential.ts @@ -0,0 +1,33 @@ +import { z } from 'zod'; + +import { Condition } from './condition'; +import { commonConditionSchema } from './multi-condition'; +import { OmitConditionType, plainStringSchema } from './shared'; + +export const SequentialConditionType = 'sequential'; + +export const conditionVariableSchema: z.ZodSchema = z.object({ + varName: plainStringSchema, + condition: commonConditionSchema, +}); + +export const sequentialConditionSchema: z.ZodSchema = z.object({ + conditionType: z + .literal(SequentialConditionType) + .default(SequentialConditionType), + conditionVariables: z.array(conditionVariableSchema).min(2).max(5), + // TODO nesting +}); + +export type SequentialConditionProps = z.infer< + typeof sequentialConditionSchema +>; + +export class SequentialCondition extends Condition { + constructor(value: OmitConditionType) { + super(sequentialConditionSchema, { + conditionType: SequentialConditionType, + ...value, + }); + } +} From ec9e423209bb31b2a4acd95506a798d67c83eeda Mon Sep 17 00:00:00 2001 From: derekpierre Date: Thu, 19 Sep 2024 11:57:12 -0400 Subject: [PATCH 3/9] Remove multi-condition modules since circular dependency caused issues; It would be nice to have something common like this but I'm not sure how to do that currently. Explicitly define union of all conditions in compound and sequential modules. --- .../taco/src/conditions/compound-condition.ts | 20 +++++++++++++++++-- .../taco/src/conditions/multi-condition.ts | 17 ---------------- packages/taco/src/conditions/sequential.ts | 17 +++++++++++++--- 3 files changed, 32 insertions(+), 22 deletions(-) delete mode 100644 packages/taco/src/conditions/multi-condition.ts diff --git a/packages/taco/src/conditions/compound-condition.ts b/packages/taco/src/conditions/compound-condition.ts index 1afac2a52..9f8f5d3aa 100644 --- a/packages/taco/src/conditions/compound-condition.ts +++ b/packages/taco/src/conditions/compound-condition.ts @@ -1,7 +1,10 @@ import { z } from 'zod'; +import { contractConditionSchema } from './base/contract'; +import { rpcConditionSchema } from './base/rpc'; +import { timeConditionSchema } from './base/time'; import { Condition, ConditionProps } from './condition'; -import { commonConditionSchema } from './multi-condition'; +import { sequentialConditionSchema } from './sequential'; import { OmitConditionType } from './shared'; export const CompoundConditionType = 'compound'; @@ -12,7 +15,20 @@ export const compoundConditionSchema: z.ZodSchema = z .literal(CompoundConditionType) .default(CompoundConditionType), operator: z.enum(['and', 'or', 'not']), - operands: z.array(commonConditionSchema).min(1).max(5), + operands: z + .array( + z.lazy(() => + z.union([ + rpcConditionSchema, + timeConditionSchema, + contractConditionSchema, + compoundConditionSchema, + sequentialConditionSchema, + ]), + ), + ) + .min(1) + .max(5), }) .refine( (condition) => { diff --git a/packages/taco/src/conditions/multi-condition.ts b/packages/taco/src/conditions/multi-condition.ts deleted file mode 100644 index c3834d6f6..000000000 --- a/packages/taco/src/conditions/multi-condition.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { z } from 'zod'; - -import { contractConditionSchema } from './base/contract'; -import { rpcConditionSchema } from './base/rpc'; -import { timeConditionSchema } from './base/time'; -import { compoundConditionSchema } from './compound-condition'; -import { sequentialConditionSchema } from './sequential'; - -export const commonConditionSchema: z.ZodSchema = z.lazy(() => - z.union([ - rpcConditionSchema, - timeConditionSchema, - contractConditionSchema, - compoundConditionSchema, - sequentialConditionSchema, - ]), -); diff --git a/packages/taco/src/conditions/sequential.ts b/packages/taco/src/conditions/sequential.ts index b12eedf6e..75664d31d 100644 --- a/packages/taco/src/conditions/sequential.ts +++ b/packages/taco/src/conditions/sequential.ts @@ -1,14 +1,25 @@ import { z } from 'zod'; +import { contractConditionSchema } from './base/contract'; +import { rpcConditionSchema } from './base/rpc'; +import { timeConditionSchema } from './base/time'; +import { compoundConditionSchema } from './compound-condition'; import { Condition } from './condition'; -import { commonConditionSchema } from './multi-condition'; import { OmitConditionType, plainStringSchema } from './shared'; export const SequentialConditionType = 'sequential'; export const conditionVariableSchema: z.ZodSchema = z.object({ varName: plainStringSchema, - condition: commonConditionSchema, + condition: z.lazy(() => + z.union([ + rpcConditionSchema, + timeConditionSchema, + contractConditionSchema, + compoundConditionSchema, + sequentialConditionSchema, + ]), + ), }); export const sequentialConditionSchema: z.ZodSchema = z.object({ @@ -16,7 +27,7 @@ export const sequentialConditionSchema: z.ZodSchema = z.object({ .literal(SequentialConditionType) .default(SequentialConditionType), conditionVariables: z.array(conditionVariableSchema).min(2).max(5), - // TODO nesting + // TODO nesting validation }); export type SequentialConditionProps = z.infer< From b032a4f77852b284b5e470ca43396de862fdfebe Mon Sep 17 00:00:00 2001 From: derekpierre Date: Thu, 19 Sep 2024 11:59:50 -0400 Subject: [PATCH 4/9] Update compound condition tests to check max operands and include nested sequential conditions. --- .../taco/src/conditions/compound-condition.ts | 1 + .../conditions/compound-condition.test.ts | 64 ++++++++++++++++++- packages/taco/test/test-utils.ts | 22 +++++++ 3 files changed, 86 insertions(+), 1 deletion(-) diff --git a/packages/taco/src/conditions/compound-condition.ts b/packages/taco/src/conditions/compound-condition.ts index 9f8f5d3aa..d8906ce3e 100644 --- a/packages/taco/src/conditions/compound-condition.ts +++ b/packages/taco/src/conditions/compound-condition.ts @@ -29,6 +29,7 @@ export const compoundConditionSchema: z.ZodSchema = z ) .min(1) .max(5), + // TODO nesting validation }) .refine( (condition) => { diff --git a/packages/taco/test/conditions/compound-condition.test.ts b/packages/taco/test/conditions/compound-condition.test.ts index 0d349b0b4..6405f015c 100644 --- a/packages/taco/test/conditions/compound-condition.test.ts +++ b/packages/taco/test/conditions/compound-condition.test.ts @@ -12,6 +12,7 @@ import { SUPPORTED_CHAIN_IDS } from '../../src/conditions/const'; import { testContractConditionObj, testRpcConditionObj, + testSequentialConditionObj, testTimeConditionObj, } from '../test-utils'; @@ -97,7 +98,31 @@ describe('validation', () => { }, ); - it('accepts recursive compound conditions', () => { + it.each([ + { + operator: 'and', + numOperands: 6, + }, + { + operator: 'or', + numOperands: 6, + }, + ])('rejects > max number of operands', ({ operator, numOperands }) => { + const result = CompoundCondition.validate(compoundConditionSchema, { + operator, + operands: Array(numOperands).fill(testContractConditionObj), + }); + + expect(result.error).toBeDefined(); + expect(result.data).toBeUndefined(); + expect(result.error?.format()).toMatchObject({ + operands: { + _errors: [`Array must contain at most 5 element(s)`], + }, + }); + }); + + it('accepts nested compound conditions', () => { const conditionObj = { conditionType: CompoundConditionType, operator: 'and', @@ -132,6 +157,43 @@ describe('validation', () => { }); }); + it('accepts nested sequential and compound conditions', () => { + const conditionObj = { + conditionType: CompoundConditionType, + operator: 'or', + operands: [ + testContractConditionObj, + testTimeConditionObj, + testRpcConditionObj, + { + operator: 'or', + operands: [testTimeConditionObj, testContractConditionObj], + }, + testSequentialConditionObj, + ], + }; + const result = CompoundCondition.validate( + compoundConditionSchema, + conditionObj, + ); + expect(result.error).toBeUndefined(); + expect(result.data).toEqual({ + conditionType: CompoundConditionType, + operator: 'or', + operands: [ + testContractConditionObj, + testTimeConditionObj, + testRpcConditionObj, + { + conditionType: CompoundConditionType, + operator: 'or', + operands: [testTimeConditionObj, testContractConditionObj], + }, + testSequentialConditionObj, + ], + }); + }); + const multichainCondition: CompoundConditionProps = { conditionType: CompoundConditionType, operator: 'and', diff --git a/packages/taco/test/test-utils.ts b/packages/taco/test/test-utils.ts index 546b7c697..f5df8f92d 100644 --- a/packages/taco/test/test-utils.ts +++ b/packages/taco/test/test-utils.ts @@ -51,6 +51,10 @@ import { } from '../src/conditions/base/time'; import { ConditionExpression } from '../src/conditions/condition-expr'; import { ERC721Balance } from '../src/conditions/predefined/erc721'; +import { + SequentialConditionProps, + SequentialConditionType, +} from '../src/conditions/sequential'; import { ReturnValueTestProps } from '../src/conditions/shared'; import { DkgClient, DkgRitual } from '../src/dkg'; import { encryptMessage } from '../src/tdec'; @@ -253,6 +257,24 @@ export const testContractConditionObj: ContractConditionProps = { returnValueTest: testReturnValueTest, }; +export const testSequentialConditionObj: SequentialConditionProps = { + conditionType: SequentialConditionType, + conditionVariables: [ + { + varName: 'rpc', + condition: testRpcConditionObj, + }, + { + varName: 'time', + condition: testTimeConditionObj, + }, + { + varName: 'contract', + condition: testContractConditionObj, + }, + ], +}; + export const testFunctionAbi: FunctionAbiProps = { name: 'myFunction', type: 'function', From b81e1eeaae504255bf86bbb64dd3ffd885586a7e Mon Sep 17 00:00:00 2001 From: derekpierre Date: Thu, 19 Sep 2024 15:50:34 -0400 Subject: [PATCH 5/9] Use baseConditionSchema that all sub-types of conditions extend their schema from. --- packages/taco/src/conditions/base/rpc.ts | 4 ++-- .../taco/src/conditions/compound-condition.ts | 8 ++++---- packages/taco/src/conditions/condition.ts | 4 ++++ packages/taco/src/conditions/sequential.ts | 17 +++++++++-------- 4 files changed, 19 insertions(+), 14 deletions(-) diff --git a/packages/taco/src/conditions/base/rpc.ts b/packages/taco/src/conditions/base/rpc.ts index f84821743..d7ab306bb 100644 --- a/packages/taco/src/conditions/base/rpc.ts +++ b/packages/taco/src/conditions/base/rpc.ts @@ -1,6 +1,6 @@ import { z } from 'zod'; -import { Condition } from '../condition'; +import { baseConditionSchema, Condition } from '../condition'; import { SUPPORTED_CHAIN_IDS } from '../const'; import { EthAddressOrUserAddressSchema, @@ -12,7 +12,7 @@ import createUnionSchema from '../zod'; export const RpcConditionType = 'rpc'; -export const rpcConditionSchema = z.object({ +export const rpcConditionSchema = baseConditionSchema.extend({ conditionType: z.literal(RpcConditionType).default(RpcConditionType), chain: createUnionSchema(SUPPORTED_CHAIN_IDS), method: z.enum(['eth_getBalance']), diff --git a/packages/taco/src/conditions/compound-condition.ts b/packages/taco/src/conditions/compound-condition.ts index d8906ce3e..aaf6d6f37 100644 --- a/packages/taco/src/conditions/compound-condition.ts +++ b/packages/taco/src/conditions/compound-condition.ts @@ -3,14 +3,14 @@ import { z } from 'zod'; import { contractConditionSchema } from './base/contract'; import { rpcConditionSchema } from './base/rpc'; import { timeConditionSchema } from './base/time'; -import { Condition, ConditionProps } from './condition'; +import { baseConditionSchema, Condition, ConditionProps } from './condition'; import { sequentialConditionSchema } from './sequential'; import { OmitConditionType } from './shared'; export const CompoundConditionType = 'compound'; -export const compoundConditionSchema: z.ZodSchema = z - .object({ +export const compoundConditionSchema: z.ZodSchema = baseConditionSchema + .extend({ conditionType: z .literal(CompoundConditionType) .default(CompoundConditionType), @@ -29,7 +29,7 @@ export const compoundConditionSchema: z.ZodSchema = z ) .min(1) .max(5), - // TODO nesting validation + // TODO nesting validation }) .refine( (condition) => { diff --git a/packages/taco/src/conditions/condition.ts b/packages/taco/src/conditions/condition.ts index 519bd189b..29ab267b4 100644 --- a/packages/taco/src/conditions/condition.ts +++ b/packages/taco/src/conditions/condition.ts @@ -3,6 +3,10 @@ import { z } from 'zod'; import { USER_ADDRESS_PARAMS } from './const'; +export const baseConditionSchema = z.object({ + conditionType: z.string(), +}); + type ConditionSchema = z.ZodSchema; export type ConditionProps = z.infer; diff --git a/packages/taco/src/conditions/sequential.ts b/packages/taco/src/conditions/sequential.ts index 75664d31d..4eee5d264 100644 --- a/packages/taco/src/conditions/sequential.ts +++ b/packages/taco/src/conditions/sequential.ts @@ -4,7 +4,7 @@ import { contractConditionSchema } from './base/contract'; import { rpcConditionSchema } from './base/rpc'; import { timeConditionSchema } from './base/time'; import { compoundConditionSchema } from './compound-condition'; -import { Condition } from './condition'; +import { baseConditionSchema, Condition } from './condition'; import { OmitConditionType, plainStringSchema } from './shared'; export const SequentialConditionType = 'sequential'; @@ -22,13 +22,14 @@ export const conditionVariableSchema: z.ZodSchema = z.object({ ), }); -export const sequentialConditionSchema: z.ZodSchema = z.object({ - conditionType: z - .literal(SequentialConditionType) - .default(SequentialConditionType), - conditionVariables: z.array(conditionVariableSchema).min(2).max(5), - // TODO nesting validation -}); +export const sequentialConditionSchema: z.ZodSchema = + baseConditionSchema.extend({ + conditionType: z + .literal(SequentialConditionType) + .default(SequentialConditionType), + conditionVariables: z.array(conditionVariableSchema).min(2).max(5), + // TODO nesting validation + }); export type SequentialConditionProps = z.infer< typeof sequentialConditionSchema From 69605f9c06abd70166499b50c80ae5f00c1d884a Mon Sep 17 00:00:00 2001 From: derekpierre Date: Thu, 19 Sep 2024 20:00:09 -0400 Subject: [PATCH 6/9] Implement validation of nested depth limit for multi-condition types. --- .../taco/src/conditions/compound-condition.ts | 9 +++++- .../taco/src/conditions/multi-condition.ts | 30 +++++++++++++++++++ packages/taco/src/conditions/sequential.ts | 17 ++++++++--- 3 files changed, 51 insertions(+), 5 deletions(-) create mode 100644 packages/taco/src/conditions/multi-condition.ts diff --git a/packages/taco/src/conditions/compound-condition.ts b/packages/taco/src/conditions/compound-condition.ts index aaf6d6f37..861353414 100644 --- a/packages/taco/src/conditions/compound-condition.ts +++ b/packages/taco/src/conditions/compound-condition.ts @@ -4,6 +4,7 @@ import { contractConditionSchema } from './base/contract'; import { rpcConditionSchema } from './base/rpc'; import { timeConditionSchema } from './base/time'; import { baseConditionSchema, Condition, ConditionProps } from './condition'; +import { maxNestedDepth } from './multi-condition'; import { sequentialConditionSchema } from './sequential'; import { OmitConditionType } from './shared'; @@ -29,7 +30,6 @@ export const compoundConditionSchema: z.ZodSchema = baseConditionSchema ) .min(1) .max(5), - // TODO nesting validation }) .refine( (condition) => { @@ -50,6 +50,13 @@ export const compoundConditionSchema: z.ZodSchema = baseConditionSchema message: `Invalid number of operands ${operands.length} for operator "${operator}"`, path: ['operands'], }), + ) + .refine( + (condition) => maxNestedDepth(2)(condition), + { + message: 'Exceeded max nested depth of 2 for multi-condition type', + path: ['operands'], + }, // Max nested depth of 2 ); export type CompoundConditionProps = z.infer; diff --git a/packages/taco/src/conditions/multi-condition.ts b/packages/taco/src/conditions/multi-condition.ts new file mode 100644 index 000000000..542b09335 --- /dev/null +++ b/packages/taco/src/conditions/multi-condition.ts @@ -0,0 +1,30 @@ +import { CompoundConditionType } from './compound-condition'; +import { ConditionProps } from './condition'; +import { ConditionVariableProps, SequentialConditionType } from './sequential'; + +export const maxNestedDepth = + (maxDepth: number) => + (condition: ConditionProps, currentDepth = 1) => { + if ( + condition.conditionType === CompoundConditionType || + condition.conditionType === SequentialConditionType + ) { + if (currentDepth > maxDepth) { + // no more multi-condition types allowed at this level + return false; + } + + if (condition.conditionType === CompoundConditionType) { + return condition.operands.every((child: ConditionProps) => + maxNestedDepth(maxDepth)(child, currentDepth + 1), + ); + } else { + return condition.conditionVariables.every( + (child: ConditionVariableProps) => + maxNestedDepth(maxDepth)(child.condition, currentDepth + 1), + ); + } + } + + return true; + }; diff --git a/packages/taco/src/conditions/sequential.ts b/packages/taco/src/conditions/sequential.ts index 4eee5d264..ae45bc2d8 100644 --- a/packages/taco/src/conditions/sequential.ts +++ b/packages/taco/src/conditions/sequential.ts @@ -5,6 +5,7 @@ import { rpcConditionSchema } from './base/rpc'; import { timeConditionSchema } from './base/time'; import { compoundConditionSchema } from './compound-condition'; import { baseConditionSchema, Condition } from './condition'; +import { maxNestedDepth } from './multi-condition'; import { OmitConditionType, plainStringSchema } from './shared'; export const SequentialConditionType = 'sequential'; @@ -22,14 +23,22 @@ export const conditionVariableSchema: z.ZodSchema = z.object({ ), }); -export const sequentialConditionSchema: z.ZodSchema = - baseConditionSchema.extend({ +export const sequentialConditionSchema: z.ZodSchema = baseConditionSchema + .extend({ conditionType: z .literal(SequentialConditionType) .default(SequentialConditionType), conditionVariables: z.array(conditionVariableSchema).min(2).max(5), - // TODO nesting validation - }); + }) + .refine( + (condition) => maxNestedDepth(2)(condition), + { + message: 'Exceeded max nested depth of 2 for multi-condition type', + path: ['conditionVariables'], + }, // Max nested depth of 2 + ); + +export type ConditionVariableProps = z.infer; export type SequentialConditionProps = z.infer< typeof sequentialConditionSchema From 64d93954129cfaf8d3c5c73d3182e9cd8c34ad49 Mon Sep 17 00:00:00 2001 From: derekpierre Date: Thu, 19 Sep 2024 20:01:02 -0400 Subject: [PATCH 7/9] Add test for checking nested depth limit for compound condition. --- .../conditions/compound-condition.test.ts | 50 +++++++++++++++++++ 1 file changed, 50 insertions(+) diff --git a/packages/taco/test/conditions/compound-condition.test.ts b/packages/taco/test/conditions/compound-condition.test.ts index 6405f015c..809914872 100644 --- a/packages/taco/test/conditions/compound-condition.test.ts +++ b/packages/taco/test/conditions/compound-condition.test.ts @@ -194,6 +194,56 @@ describe('validation', () => { }); }); + it('limits max depth of nested compound condition', () => { + const result = CompoundCondition.validate(compoundConditionSchema, { + operator: 'or', + operands: [ + testRpcConditionObj, + testContractConditionObj, + { + conditionType: CompoundConditionType, + operator: 'and', + operands: [ + testTimeConditionObj, + { + conditionType: CompoundConditionType, + operator: 'or', + operands: [testTimeConditionObj, testRpcConditionObj], + }, + ], + }, + ], + }); + expect(result.error).toBeDefined(); + expect(result.data).toBeUndefined(); + expect(result.error?.format()).toMatchObject({ + operands: { + _errors: [`Exceeded max nested depth of 2 for multi-condition type`], + }, + }); + }); + it('limits max depth of nested sequential condition', () => { + const result = CompoundCondition.validate(compoundConditionSchema, { + operator: 'or', + operands: [ + testRpcConditionObj, + testContractConditionObj, + { + conditionType: CompoundConditionType, + operator: 'not', + operands: [testSequentialConditionObj], + }, + ], + }); + expect(result.error).toBeDefined(); + expect(result.data).toBeUndefined(); + expect(result.error?.format()).toMatchObject({ + operands: { + _errors: ['Exceeded max nested depth of 2 for multi-condition type'], + }, + }); + }); + const multichainCondition: CompoundConditionProps = { conditionType: CompoundConditionType, operator: 'and', From 268271832c6b48075079887269629b11b8385977 Mon Sep 17 00:00:00 2001 From: derekpierre Date: Fri, 20 Sep 2024 10:21:43 -0400 Subject: [PATCH 8/9] Initial tests for sequential conditions. --- .../taco/test/conditions/sequential.test.ts | 264 ++++++++++++++++++ packages/taco/test/test-utils.ts | 14 + 2 files changed, 278 insertions(+) create mode 100644 packages/taco/test/conditions/sequential.test.ts diff --git a/packages/taco/test/conditions/sequential.test.ts b/packages/taco/test/conditions/sequential.test.ts new file mode 100644 index 000000000..1fde28599 --- /dev/null +++ b/packages/taco/test/conditions/sequential.test.ts @@ -0,0 +1,264 @@ +import { describe, expect, it } from 'vitest'; + +import { CompoundConditionType } from '../../src/conditions/compound-condition'; +import { SUPPORTED_CHAIN_IDS } from '../../src/conditions/const'; +import { + ConditionVariableProps, + SequentialCondition, + SequentialConditionProps, + sequentialConditionSchema, + SequentialConditionType, +} from '../../src/conditions/sequential'; +import { + testCompoundConditionObj, + testContractConditionObj, + testRpcConditionObj, + testSequentialConditionObj, + testTimeConditionObj, +} from '../test-utils'; + +describe('validation', () => { + const rpcConditionVariable: ConditionVariableProps = { + varName: 'rpc', + condition: testRpcConditionObj, + }; + const timeConditionVariable: ConditionVariableProps = { + varName: 'time', + condition: testTimeConditionObj, + }; + const contractConditionVariable: ConditionVariableProps = { + varName: 'contract', + condition: testContractConditionObj, + }; + const compoundConditionVariable: ConditionVariableProps = { + varName: 'compound', + condition: testCompoundConditionObj, + }; + const sequentialConditionVariable: ConditionVariableProps = { + varName: 'nestedSequential', + condition: testSequentialConditionObj, + }; + + it('rejects no varName', () => { + const result = SequentialCondition.validate(sequentialConditionSchema, { + conditionVariables: [ + rpcConditionVariable, + { + condition: testTimeConditionObj, + }, + ], + }); + + expect(result.error).toBeDefined(); + expect(result.data).toBeUndefined(); + expect(result.error?.format()).toMatchObject({ + conditionVariables: { + '1': { + varName: { + _errors: ['Required'], + }, + }, + }, + }); + }); + + it('rejects > max number of condition variables', () => { + const tooManyConditionVariables = new Array(6); + for (let i = 0; i < tooManyConditionVariables.length; i++) { + tooManyConditionVariables[i] = { + varName: `var${i}`, + condition: testRpcConditionObj, + }; + } + const result = SequentialCondition.validate(sequentialConditionSchema, { + conditionVariables: tooManyConditionVariables, + }); + + expect(result.error).toBeDefined(); + expect(result.data).toBeUndefined(); + expect(result.error?.format()).toMatchObject({ + conditionVariables: { + _errors: [`Array must contain at most 5 element(s)`], + }, + }); + }); + + it('accepts nested compound conditions', () => { + const conditionObj = { + conditionType: SequentialConditionType, + conditionVariables: [ + rpcConditionVariable, + timeConditionVariable, + contractConditionVariable, + compoundConditionVariable, + ], + }; + const result = SequentialCondition.validate( + sequentialConditionSchema, + conditionObj, + ); + expect(result.error).toBeUndefined(); + expect(result.data).toEqual({ + conditionType: SequentialConditionType, + conditionVariables: [ + rpcConditionVariable, + timeConditionVariable, + contractConditionVariable, + compoundConditionVariable, + ], + }); + }); + + it('accepts nested sequential and compound conditions', () => { + const conditionObj = { + conditionType: SequentialConditionType, + conditionVariables: [ + rpcConditionVariable, + timeConditionVariable, + contractConditionVariable, + compoundConditionVariable, + sequentialConditionVariable, + ], + }; + const result = SequentialCondition.validate( + sequentialConditionSchema, + conditionObj, + ); + expect(result.error).toBeUndefined(); + expect(result.data).toEqual({ + conditionType: SequentialConditionType, + conditionVariables: [ + rpcConditionVariable, + timeConditionVariable, + contractConditionVariable, + compoundConditionVariable, + sequentialConditionVariable, + ], + }); + }); + + it('limits max depth of nested compound condition', () => { + const result = SequentialCondition.validate(sequentialConditionSchema, { + conditionVariables: [ + rpcConditionVariable, + contractConditionVariable, + { + varName: 'compound', + condition: { + conditionType: CompoundConditionType, + operator: 'not', + operands: [ + { + conditionType: CompoundConditionType, + operator: 'and', + operands: [testTimeConditionObj, testRpcConditionObj], + }, + ], + }, + }, + ], + }); + expect(result.error).toBeDefined(); + expect(result.data).toBeUndefined(); + expect(result.error?.format()).toMatchObject({ + conditionVariables: { + _errors: [`Exceeded max nested depth of 2 for multi-condition type`], + }, + }); + }); + + it('limits max depth of nested sequential condition', () => { + const result = SequentialCondition.validate(sequentialConditionSchema, { + conditionVariables: [ + rpcConditionVariable, + contractConditionVariable, + { + varName: 'sequentialNested', + condition: { + conditionType: SequentialConditionType, + conditionVariables: [ + timeConditionVariable, + sequentialConditionVariable, + ], + }, + }, + ], + }); + expect(result.error).toBeDefined(); + expect(result.data).toBeUndefined(); + expect(result.error?.format()).toMatchObject({ + conditionVariables: { + _errors: ['Exceeded max nested depth of 2 for multi-condition type'], + }, + }); + }); + + it('accepts on a valid multichain condition schema', () => { + const multichainCondition: SequentialConditionProps = { + conditionType: SequentialConditionType, + conditionVariables: SUPPORTED_CHAIN_IDS.map((chain) => ({ + varName: `chain_${chain}`, + condition: { + ...testRpcConditionObj, + chain, + }, + })), + }; + + const result = SequentialCondition.validate( + sequentialConditionSchema, + multichainCondition, + ); + + expect(result.error).toBeUndefined(); + expect(result.data).toEqual(multichainCondition); + }); + + it('rejects an invalid multichain condition schema', () => { + const badMultichainCondition: SequentialConditionProps = { + conditionType: SequentialConditionType, + conditionVariables: [ + { + varName: 'chain_1', + condition: { + ...testRpcConditionObj, + chain: 1, + }, + }, + { + varName: 'chain_137', + condition: { + ...testRpcConditionObj, + chain: 137, + }, + }, + { + varName: `invalid_chain`, + condition: { + ...testRpcConditionObj, + chain: -1, + }, + }, + ], + }; + + const result = SequentialCondition.validate( + sequentialConditionSchema, + badMultichainCondition, + ); + + expect(result.error).toBeDefined(); + expect(result.data).toBeUndefined(); + }); + + it('infers default condition type from constructor', () => { + const condition = new SequentialCondition({ + conditionVariables: [ + contractConditionVariable, + timeConditionVariable, + rpcConditionVariable, + ], + }); + expect(condition.value.conditionType).toEqual(SequentialConditionType); + }); +}); diff --git a/packages/taco/test/test-utils.ts b/packages/taco/test/test-utils.ts index f5df8f92d..5622d27dc 100644 --- a/packages/taco/test/test-utils.ts +++ b/packages/taco/test/test-utils.ts @@ -49,6 +49,10 @@ import { TimeConditionProps, TimeConditionType, } from '../src/conditions/base/time'; +import { + CompoundConditionProps, + CompoundConditionType, +} from '../src/conditions/compound-condition'; import { ConditionExpression } from '../src/conditions/condition-expr'; import { ERC721Balance } from '../src/conditions/predefined/erc721'; import { @@ -257,6 +261,16 @@ export const testContractConditionObj: ContractConditionProps = { returnValueTest: testReturnValueTest, }; +export const testCompoundConditionObj: CompoundConditionProps = { + conditionType: CompoundConditionType, + operator: 'or', + operands: [ + testRpcConditionObj, + testTimeConditionObj, + testContractConditionObj, + ], +}; + export const testSequentialConditionObj: SequentialConditionProps = { conditionType: SequentialConditionType, conditionVariables: [ From 20b6617223a5a19a0ebbe05493a85816949ced51 Mon Sep 17 00:00:00 2001 From: derekpierre Date: Fri, 20 Sep 2024 10:35:08 -0400 Subject: [PATCH 9/9] Add sequential condition validation to ensure duplicate varNames are not provided for conditionVariables. --- packages/taco/src/conditions/sequential.ts | 19 ++++++++++++ .../taco/test/conditions/sequential.test.ts | 29 ++++++++++++++++++- 2 files changed, 47 insertions(+), 1 deletion(-) diff --git a/packages/taco/src/conditions/sequential.ts b/packages/taco/src/conditions/sequential.ts index ae45bc2d8..2f996eb2c 100644 --- a/packages/taco/src/conditions/sequential.ts +++ b/packages/taco/src/conditions/sequential.ts @@ -36,6 +36,25 @@ export const sequentialConditionSchema: z.ZodSchema = baseConditionSchema message: 'Exceeded max nested depth of 2 for multi-condition type', path: ['conditionVariables'], }, // Max nested depth of 2 + ) + .refine( + // check for duplicate var names + (condition) => { + const seen = new Set(); + return condition.conditionVariables.every( + (child: ConditionVariableProps) => { + if (seen.has(child.varName)) { + return false; + } + seen.add(child.varName); + return true; + }, + ); + }, + { + message: 'Duplicate variable names are not allowed', + path: ['conditionVariables'], + }, ); export type ConditionVariableProps = z.infer; diff --git a/packages/taco/test/conditions/sequential.test.ts b/packages/taco/test/conditions/sequential.test.ts index 1fde28599..08ad83971 100644 --- a/packages/taco/test/conditions/sequential.test.ts +++ b/packages/taco/test/conditions/sequential.test.ts @@ -62,6 +62,33 @@ describe('validation', () => { }); }); + it('rejects duplication variable names', () => { + const result = SequentialCondition.validate(sequentialConditionSchema, { + conditionVariables: [ + { + varName: 'var1', + condition: testRpcConditionObj, + }, + { + varName: 'var2', + condition: testTimeConditionObj, + }, + { + varName: 'var1', + condition: testContractConditionObj, + }, + ], + }); + + expect(result.error).toBeDefined(); + expect(result.data).toBeUndefined(); + expect(result.error?.format()).toMatchObject({ + conditionVariables: { + _errors: ['Duplicate variable names are not allowed'], + }, + }); + }); + it('rejects > max number of condition variables', () => { const tooManyConditionVariables = new Array(6); for (let i = 0; i < tooManyConditionVariables.length; i++) { @@ -78,7 +105,7 @@ describe('validation', () => { expect(result.data).toBeUndefined(); expect(result.error?.format()).toMatchObject({ conditionVariables: { - _errors: [`Array must contain at most 5 element(s)`], + _errors: ['Array must contain at most 5 element(s)'], }, }); });