From e2cb183b8ef53c40f3029fb2d2f83b821a3f744e Mon Sep 17 00:00:00 2001 From: James Campbell Date: Wed, 17 Jul 2024 13:37:31 +0200 Subject: [PATCH 01/75] Add basic types, props, and classes for JsonApiCondition --- packages/taco/src/conditions/base/json-api.ts | 32 +++++++++++++++++++ .../taco/src/conditions/condition-factory.ts | 7 ++++ 2 files changed, 39 insertions(+) create mode 100644 packages/taco/src/conditions/base/json-api.ts diff --git a/packages/taco/src/conditions/base/json-api.ts b/packages/taco/src/conditions/base/json-api.ts new file mode 100644 index 000000000..5593c1efa --- /dev/null +++ b/packages/taco/src/conditions/base/json-api.ts @@ -0,0 +1,32 @@ +import { z } from 'zod'; + +import { Condition } from '../condition'; +import { + EthAddressOrUserAddressSchema, + OmitConditionType, + paramOrContextParamSchema, + returnValueTestSchema, +} from '../shared'; + +export const JsonApiConditionType = 'json'; + +export const JsonApiConditionSchema = z.object({ + conditionType: z.literal(JsonApiConditionType).default(JsonApiConditionType), + parameters: z.union([ + z.array(EthAddressOrUserAddressSchema).nonempty(), + // Using tuple here because ordering matters + z.tuple([EthAddressOrUserAddressSchema, paramOrContextParamSchema]), + ]), + returnValueTest: returnValueTestSchema, // Update to allow multiple return values after expanding supported methods +}); + +export type JsonApiConditionProps = z.infer; + +export class JsonApiCondition extends Condition { + constructor(value: OmitConditionType) { + super(JsonApiConditionSchema, { + conditionType: JsonApiConditionType, + ...value, + }); + } +} diff --git a/packages/taco/src/conditions/condition-factory.ts b/packages/taco/src/conditions/condition-factory.ts index 93e34452f..b22a34d2a 100644 --- a/packages/taco/src/conditions/condition-factory.ts +++ b/packages/taco/src/conditions/condition-factory.ts @@ -9,6 +9,11 @@ import { TimeConditionProps, TimeConditionType, } from './base/time'; +import { + JsonApiCondition, + JsonApiConditionProps, + JsonApiConditionType, +} from './base/json-api'; import { CompoundCondition, CompoundConditionProps, @@ -30,6 +35,8 @@ export class ConditionFactory { return new ContractCondition(props as ContractConditionProps); case CompoundConditionType: return new CompoundCondition(props as CompoundConditionProps); + case JsonApiConditionType: + return new JsonApiCondition(props as JsonApiConditionProps); default: throw new Error(ERR_INVALID_CONDITION_TYPE(props.conditionType)); } From 28d5a34558b3221e92972e660dfaaf6480805a4d Mon Sep 17 00:00:00 2001 From: James Campbell Date: Fri, 19 Jul 2024 09:46:45 +0200 Subject: [PATCH 02/75] Update `JsonApiConditionSchema` to match lingo Update `JsonApiConditionSchema` to match lingo --- packages/taco/src/conditions/base/json-api.ts | 24 +++++++++++++------ 1 file changed, 17 insertions(+), 7 deletions(-) diff --git a/packages/taco/src/conditions/base/json-api.ts b/packages/taco/src/conditions/base/json-api.ts index 5593c1efa..92aaffc7f 100644 --- a/packages/taco/src/conditions/base/json-api.ts +++ b/packages/taco/src/conditions/base/json-api.ts @@ -1,22 +1,32 @@ +import { JSONPath } from 'jsonpath-plus'; import { z } from 'zod'; import { Condition } from '../condition'; import { - EthAddressOrUserAddressSchema, OmitConditionType, paramOrContextParamSchema, returnValueTestSchema, } from '../shared'; -export const JsonApiConditionType = 'json'; +export const JsonApiConditionType = 'json-api'; export const JsonApiConditionSchema = z.object({ conditionType: z.literal(JsonApiConditionType).default(JsonApiConditionType), - parameters: z.union([ - z.array(EthAddressOrUserAddressSchema).nonempty(), - // Using tuple here because ordering matters - z.tuple([EthAddressOrUserAddressSchema, paramOrContextParamSchema]), - ]), + endpoint: z.string().url(), + parameters: z.array(paramOrContextParamSchema), + query: z.string().refine( + (path) => { + try { + JSONPath.toPathArray(path); + return true; + } catch (error) { + return false; + } + }, + { + message: "Invalid JSON path", + } + ), returnValueTest: returnValueTestSchema, // Update to allow multiple return values after expanding supported methods }); From d697794364102d17f5cc6ce9fae8fa7ae550edf2 Mon Sep 17 00:00:00 2001 From: James Campbell Date: Fri, 19 Jul 2024 11:18:40 +0200 Subject: [PATCH 03/75] Add basic tests for json api condition type --- .../taco/test/conditions/base/json.test.ts | 38 +++++++++++++++++++ packages/taco/test/test-utils.ts | 15 ++++++++ 2 files changed, 53 insertions(+) create mode 100644 packages/taco/test/conditions/base/json.test.ts diff --git a/packages/taco/test/conditions/base/json.test.ts b/packages/taco/test/conditions/base/json.test.ts new file mode 100644 index 000000000..6344b1a96 --- /dev/null +++ b/packages/taco/test/conditions/base/json.test.ts @@ -0,0 +1,38 @@ +import { describe, expect, it } from 'vitest'; + +import { + JsonApiCondition, + JsonApiConditionSchema, +} from '../../../src/conditions/base/json-api'; +import { testJsonApiConditionObj } from '../../test-utils'; + +describe('JsonApiCondition', () => { + describe('validation', () => { + it('accepts a valid schema', () => { + const result = JsonApiCondition.validate( + JsonApiConditionSchema, + testJsonApiConditionObj, + ); + + expect(result.error).toBeUndefined(); + expect(result.data).toEqual(testJsonApiConditionObj); + }); + + it('rejects an invalid schema', () => { + const badJsonApiObj = { + ...testJsonApiConditionObj, + endpoint: 'not-a-url', + }; + + const result = JsonApiCondition.validate(JsonApiConditionSchema, badJsonApiObj); + + expect(result.error).toBeDefined(); + expect(result.data).toBeUndefined(); + expect(result.error?.format()).toMatchObject({ + endpoint: { + _errors: ['Invalid url'], + }, + }); + }); + }); +}); diff --git a/packages/taco/test/test-utils.ts b/packages/taco/test/test-utils.ts index 360585b1b..feac6a829 100644 --- a/packages/taco/test/test-utils.ts +++ b/packages/taco/test/test-utils.ts @@ -39,6 +39,10 @@ import { ContractConditionType, FunctionAbiProps, } from '../src/conditions/base/contract'; +import { + JsonApiConditionProps, + JsonApiConditionType +} from '../src/conditions/base/json-api'; import { RpcConditionProps, RpcConditionType, @@ -223,6 +227,17 @@ export const testTimeConditionObj: TimeConditionProps = { chain: TEST_CHAIN_ID, }; +export const testJsonApiConditionObj = { + conditionType: JsonApiConditionType, + endpoint: 'https://api.coingecko.com/api/v3/simple/price', + parameters: { + 'ids': 'ethereum', + 'vs_currencies': 'usd', + }, + query: '$.ethereum.usd', + returnValueTest: testReturnValueTest, +}; + export const testRpcConditionObj: RpcConditionProps = { conditionType: RpcConditionType, chain: TEST_CHAIN_ID, From d9e40d24a1f496a170fb1605014f3e42310e3a63 Mon Sep 17 00:00:00 2001 From: derekpierre Date: Thu, 19 Sep 2024 11:05:55 -0400 Subject: [PATCH 04/75] 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 579acacc29e566d1c7b6ca7d79ee51e297b5cbd7 Mon Sep 17 00:00:00 2001 From: andresceballosm Date: Tue, 30 Jul 2024 15:34:23 -0500 Subject: [PATCH 05/75] Added JSONPath syntactic validation --- packages/taco/package.json | 4 +- packages/taco/src/conditions/base/json-api.ts | 29 +++++++--- .../test/conditions/base/json-api.test.ts | 56 +++++++++++++++++++ 3 files changed, 80 insertions(+), 9 deletions(-) create mode 100644 packages/taco/test/conditions/base/json-api.test.ts diff --git a/packages/taco/package.json b/packages/taco/package.json index 88bf9143e..2048bf56a 100644 --- a/packages/taco/package.json +++ b/packages/taco/package.json @@ -44,11 +44,13 @@ "@nucypher/taco-auth": "workspace:*", "ethers": "*", "semver": "^7.6.3", + "jsonpath": "^1.1.1", "zod": "*" }, "devDependencies": { "@nucypher/test-utils": "workspace:*", - "@types/semver": "^7.5.8" + "@types/semver": "^7.5.8", + "@types/jsonpath": "^0.2.4" }, "engines": { "node": ">=18", diff --git a/packages/taco/src/conditions/base/json-api.ts b/packages/taco/src/conditions/base/json-api.ts index 0a55b20b8..f8835913e 100644 --- a/packages/taco/src/conditions/base/json-api.ts +++ b/packages/taco/src/conditions/base/json-api.ts @@ -1,18 +1,31 @@ -import { z } from 'zod'; +import { parse } from "jsonpath"; +import { z } from "zod"; -import { Condition } from '../condition'; -import { - OmitConditionType, - returnValueTestSchema, -} from '../shared'; +import { Condition } from "../condition"; +import { OmitConditionType, returnValueTestSchema } from "../shared"; -export const JsonApiConditionType = 'json-api'; +export const JsonApiConditionType = "json-api"; + +const validateJSONPath = (jsonPath: string): boolean => { + try { + parse(jsonPath); + return true; + } catch (error) { + return false; + } +}; + +export const jsonPathSchema = z + .string() + .refine((val) => validateJSONPath(val), { + message: "Invalid JSONPath expression", + }); export const JsonApiConditionSchema = z.object({ conditionType: z.literal(JsonApiConditionType).default(JsonApiConditionType), endpoint: z.string().url(), parameters: z.record(z.string(), z.unknown()).optional(), - query: z.string().optional(), + query: jsonPathSchema.optional(), returnValueTest: returnValueTestSchema, // Update to allow multiple return values after expanding supported methods }); diff --git a/packages/taco/test/conditions/base/json-api.test.ts b/packages/taco/test/conditions/base/json-api.test.ts new file mode 100644 index 000000000..94b07af81 --- /dev/null +++ b/packages/taco/test/conditions/base/json-api.test.ts @@ -0,0 +1,56 @@ +import { describe, expect, it } from 'vitest'; + +import { jsonPathSchema } from '../../../src/conditions/base/json-api'; + +describe('JSONPath Validation', () => { + it('Invalid JSONPath: Incomplete filter expression', () => { + const invalidPath = '$.store.book[?(@.price < ]'; + const result = jsonPathSchema.safeParse(invalidPath); + expect(result.success).toBe(false); + if (!result.success) { + expect(result.error.errors[0].message).toBe( + 'Invalid JSONPath expression', + ); + } + }); + + it('Invalid JSONPath: Incorrect use of brackets', () => { + const invalidPath = '$[store][book]'; + const result = jsonPathSchema.safeParse(invalidPath); + expect(result.success).toBe(false); + if (!result.success) { + expect(result.error.errors[0].message).toBe( + 'Invalid JSONPath expression', + ); + } + }); + + it('Invalid JSONPath: Unclosed wildcard asterisk', () => { + const invalidPath = '$.store.book[*'; + const result = jsonPathSchema.safeParse(invalidPath); + expect(result.success).toBe(false); + if (!result.success) { + expect(result.error.errors[0].message).toBe( + 'Invalid JSONPath expression', + ); + } + }); + + it('Valid JSONPath expression', () => { + const validPath = '$.store.book[?(@.price < 10)]'; + const result = jsonPathSchema.safeParse(validPath); + expect(result.success).toBe(true); + }); + + it('Valid JSONPath with correct quotes', () => { + const validPath = "$.store['book[?(@.price < ]']"; + const result = jsonPathSchema.safeParse(validPath); + expect(result.success).toBe(true); + }); + + it('Valid JSONPath with correct wildcard', () => { + const validPath = '$.store.book[*]'; + const result = jsonPathSchema.safeParse(validPath); + expect(result.success).toBe(true); + }); +}); From 9f9be4502d58a0bef60f3ae6f4030907bccb8085 Mon Sep 17 00:00:00 2001 From: James Campbell Date: Fri, 19 Jul 2024 11:19:08 +0200 Subject: [PATCH 06/75] Fix schema for json api condition JSON path will always be able to be parsed. It only becomes invalid when combined with JSON, and this will only be known at decryption time. --- packages/taco/src/conditions/base/json-api.ts | 18 ++---------------- 1 file changed, 2 insertions(+), 16 deletions(-) diff --git a/packages/taco/src/conditions/base/json-api.ts b/packages/taco/src/conditions/base/json-api.ts index 92aaffc7f..b4b9efcab 100644 --- a/packages/taco/src/conditions/base/json-api.ts +++ b/packages/taco/src/conditions/base/json-api.ts @@ -1,10 +1,8 @@ -import { JSONPath } from 'jsonpath-plus'; import { z } from 'zod'; import { Condition } from '../condition'; import { OmitConditionType, - paramOrContextParamSchema, returnValueTestSchema, } from '../shared'; @@ -13,20 +11,8 @@ export const JsonApiConditionType = 'json-api'; export const JsonApiConditionSchema = z.object({ conditionType: z.literal(JsonApiConditionType).default(JsonApiConditionType), endpoint: z.string().url(), - parameters: z.array(paramOrContextParamSchema), - query: z.string().refine( - (path) => { - try { - JSONPath.toPathArray(path); - return true; - } catch (error) { - return false; - } - }, - { - message: "Invalid JSON path", - } - ), + parameters: z.record(z.string(), z.unknown()), + query: z.string(), returnValueTest: returnValueTestSchema, // Update to allow multiple return values after expanding supported methods }); From 229a04e755c399f64178c9a1599b5952341f1597 Mon Sep 17 00:00:00 2001 From: derekpierre Date: Thu, 19 Sep 2024 11:07:14 -0400 Subject: [PATCH 07/75] 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 51796756bc11f6b164bbdb12ca30b074ecf80ac3 Mon Sep 17 00:00:00 2001 From: andresceballosm Date: Wed, 31 Jul 2024 10:07:44 -0500 Subject: [PATCH 08/75] Updated pnpm-lock.yamls and fix linter errors --- packages/taco/src/conditions/base/json-api.ts | 12 ++++++------ .../taco/test/conditions/base/json.test.ts | 19 +++++++++++-------- .../test/conditions/condition-expr.test.ts | 11 +++++++---- packages/taco/test/test-utils.ts | 9 +++------ pnpm-lock.yaml | 11 +++++++++++ 5 files changed, 38 insertions(+), 24 deletions(-) diff --git a/packages/taco/src/conditions/base/json-api.ts b/packages/taco/src/conditions/base/json-api.ts index f8835913e..7e96cd648 100644 --- a/packages/taco/src/conditions/base/json-api.ts +++ b/packages/taco/src/conditions/base/json-api.ts @@ -1,10 +1,10 @@ -import { parse } from "jsonpath"; -import { z } from "zod"; +import { parse } from 'jsonpath'; +import { z } from 'zod'; -import { Condition } from "../condition"; -import { OmitConditionType, returnValueTestSchema } from "../shared"; +import { Condition } from '../condition'; +import { OmitConditionType, returnValueTestSchema } from '../shared'; -export const JsonApiConditionType = "json-api"; +export const JsonApiConditionType = 'json-api'; const validateJSONPath = (jsonPath: string): boolean => { try { @@ -18,7 +18,7 @@ const validateJSONPath = (jsonPath: string): boolean => { export const jsonPathSchema = z .string() .refine((val) => validateJSONPath(val), { - message: "Invalid JSONPath expression", + message: 'Invalid JSONPath expression', }); export const JsonApiConditionSchema = z.object({ diff --git a/packages/taco/test/conditions/base/json.test.ts b/packages/taco/test/conditions/base/json.test.ts index 9c26a7f7a..0166485a2 100644 --- a/packages/taco/test/conditions/base/json.test.ts +++ b/packages/taco/test/conditions/base/json.test.ts @@ -25,7 +25,10 @@ describe('JsonApiCondition', () => { endpoint: 'not-a-url', }; - const result = JsonApiCondition.validate(JsonApiConditionSchema, badJsonApiObj); + const result = JsonApiCondition.validate( + JsonApiConditionSchema, + badJsonApiObj, + ); expect(result.error).toBeDefined(); expect(result.data).toBeUndefined(); @@ -35,26 +38,26 @@ describe('JsonApiCondition', () => { }, }); }); - + describe('parameters', () => { it('accepts conditions without query path', () => { - const { query, ...noQueryObj} = testJsonApiConditionObj; + const { query, ...noQueryObj } = testJsonApiConditionObj; const result = JsonApiCondition.validate( JsonApiConditionSchema, - noQueryObj + noQueryObj, ); - + expect(result.error).toBeUndefined(); expect(result.data).toEqual(noQueryObj); }); it('accepts conditions without parameters', () => { - const { query, ...noParamsObj} = testJsonApiConditionObj; + const { query, ...noParamsObj } = testJsonApiConditionObj; const result = JsonApiCondition.validate( JsonApiConditionSchema, - noParamsObj + noParamsObj, ); - + expect(result.error).toBeUndefined(); expect(result.data).toEqual(noParamsObj); }); diff --git a/packages/taco/test/conditions/condition-expr.test.ts b/packages/taco/test/conditions/condition-expr.test.ts index 4defa46cf..66bf48299 100644 --- a/packages/taco/test/conditions/condition-expr.test.ts +++ b/packages/taco/test/conditions/condition-expr.test.ts @@ -406,17 +406,20 @@ describe('condition set', () => { it('json api condition serialization', () => { const conditionExpr = new ConditionExpression(jsonApiCondition); - + const conditionExprJson = conditionExpr.toJson(); expect(conditionExprJson).toBeDefined(); expect(conditionExprJson).toContain('endpoint'); - expect(conditionExprJson).toContain('https://_this_would_totally_fail.com'); + expect(conditionExprJson).toContain( + 'https://_this_would_totally_fail.com', + ); expect(conditionExprJson).toContain('parameters'); expect(conditionExprJson).toContain('query'); expect(conditionExprJson).toContain('$.ethereum.usd'); expect(conditionExprJson).toContain('returnValueTest'); - - const conditionExprFromJson = ConditionExpression.fromJSON(conditionExprJson); + + const conditionExprFromJson = + ConditionExpression.fromJSON(conditionExprJson); expect(conditionExprFromJson).toBeDefined(); expect(conditionExprFromJson.condition).toBeInstanceOf(JsonApiCondition); }); diff --git a/packages/taco/test/test-utils.ts b/packages/taco/test/test-utils.ts index 60ebab597..546b7c697 100644 --- a/packages/taco/test/test-utils.ts +++ b/packages/taco/test/test-utils.ts @@ -39,10 +39,7 @@ import { ContractConditionType, FunctionAbiProps, } from '../src/conditions/base/contract'; -import { - JsonApiConditionProps, - JsonApiConditionType -} from '../src/conditions/base/json-api'; +import { JsonApiConditionType } from '../src/conditions/base/json-api'; import { RpcConditionProps, RpcConditionType, @@ -231,8 +228,8 @@ export const testJsonApiConditionObj = { conditionType: JsonApiConditionType, endpoint: 'https://_this_would_totally_fail.com', parameters: { - 'ids': 'ethereum', - 'vs_currencies': 'usd', + ids: 'ethereum', + vs_currencies: 'usd', }, query: '$.ethereum.usd', returnValueTest: testReturnValueTest, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index d8586b46b..90f7616ed 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -573,6 +573,9 @@ importers: ethers: specifier: '*' version: 5.7.2(bufferutil@4.0.8)(utf-8-validate@5.0.10) + jsonpath: + specifier: ^1.1.1 + version: 1.1.1 semver: specifier: ^7.6.3 version: 7.6.3 @@ -583,6 +586,9 @@ importers: '@nucypher/test-utils': specifier: workspace:* version: link:../test-utils + '@types/jsonpath': + specifier: ^0.2.4 + version: 0.2.4 '@types/semver': specifier: ^7.5.8 version: 7.5.8 @@ -2607,6 +2613,9 @@ packages: '@types/json5@0.0.29': resolution: {integrity: sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==} + '@types/jsonpath@0.2.4': + resolution: {integrity: sha512-K3hxB8Blw0qgW6ExKgMbXQv2UPZBoE2GqLpVY+yr7nMD2Pq86lsuIzyAaiQ7eMqFL5B6di6pxSkogLJEyEHoGA==} + '@types/keyv@3.1.4': resolution: {integrity: sha512-BQ5aZNSCpj7D6K2ksrRCTmKRLEpnPvWDiLPfoGyhZ++8YtiK9d/3DBKPJgry359X/P1PfruyYwvnvwFjuEiEIg==} @@ -11565,6 +11574,8 @@ snapshots: '@types/json5@0.0.29': {} + '@types/jsonpath@0.2.4': {} + '@types/keyv@3.1.4': dependencies: '@types/node': 20.16.3 From 2ad6f6f5eeb883de223a5e56be659f7a889e990f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20N=C3=BA=C3=B1ez?= Date: Fri, 19 Jul 2024 13:20:41 +0200 Subject: [PATCH 09/75] query and parameters in JSONConditionScheme should be optional --- packages/taco/src/conditions/base/json-api.ts | 4 ++-- .../taco/test/conditions/base/json.test.ts | 24 +++++++++++++++++++ 2 files changed, 26 insertions(+), 2 deletions(-) diff --git a/packages/taco/src/conditions/base/json-api.ts b/packages/taco/src/conditions/base/json-api.ts index b4b9efcab..0a55b20b8 100644 --- a/packages/taco/src/conditions/base/json-api.ts +++ b/packages/taco/src/conditions/base/json-api.ts @@ -11,8 +11,8 @@ export const JsonApiConditionType = 'json-api'; export const JsonApiConditionSchema = z.object({ conditionType: z.literal(JsonApiConditionType).default(JsonApiConditionType), endpoint: z.string().url(), - parameters: z.record(z.string(), z.unknown()), - query: z.string(), + parameters: z.record(z.string(), z.unknown()).optional(), + query: z.string().optional(), returnValueTest: returnValueTestSchema, // Update to allow multiple return values after expanding supported methods }); diff --git a/packages/taco/test/conditions/base/json.test.ts b/packages/taco/test/conditions/base/json.test.ts index 6344b1a96..def687c1e 100644 --- a/packages/taco/test/conditions/base/json.test.ts +++ b/packages/taco/test/conditions/base/json.test.ts @@ -34,5 +34,29 @@ describe('JsonApiCondition', () => { }, }); }); + + describe('parameters', () => { + it('accepts conditions without query path', () => { + const {query, ...noQueryObj} = testJsonApiConditionObj; + const result = JsonApiCondition.validate( + JsonApiConditionSchema, + noQueryObj + ); + + expect(result.error).toBeUndefined(); + expect(result.data).toEqual(noQueryObj); + }); + + it('accepts conditions without parameters', () => { + const {parameters, ...noParamsObj} = testJsonApiConditionObj; + const result = JsonApiCondition.validate( + JsonApiConditionSchema, + noParamsObj + ); + + expect(result.error).toBeUndefined(); + expect(result.data).toEqual(noParamsObj); + }); + }); }); }); From 2395b2efa0414a8f251dc29ba5cb95cf9aefd3dd Mon Sep 17 00:00:00 2001 From: derekpierre Date: Thu, 19 Sep 2024 11:57:12 -0400 Subject: [PATCH 10/75] 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 a1c98cf63b744bca1c239cca2cd87b9bb724ebd6 Mon Sep 17 00:00:00 2001 From: andresceballosm Date: Wed, 31 Jul 2024 14:33:57 -0500 Subject: [PATCH 11/75] Removed jsonpath library --- packages/taco/package.json | 2 - packages/taco/src/conditions/base/json-api.ts | 69 ++++++++++++++++--- pnpm-lock.yaml | 11 --- 3 files changed, 61 insertions(+), 21 deletions(-) diff --git a/packages/taco/package.json b/packages/taco/package.json index 2048bf56a..f41506c8a 100644 --- a/packages/taco/package.json +++ b/packages/taco/package.json @@ -44,13 +44,11 @@ "@nucypher/taco-auth": "workspace:*", "ethers": "*", "semver": "^7.6.3", - "jsonpath": "^1.1.1", "zod": "*" }, "devDependencies": { "@nucypher/test-utils": "workspace:*", "@types/semver": "^7.5.8", - "@types/jsonpath": "^0.2.4" }, "engines": { "node": ">=18", diff --git a/packages/taco/src/conditions/base/json-api.ts b/packages/taco/src/conditions/base/json-api.ts index 7e96cd648..01ac73e77 100644 --- a/packages/taco/src/conditions/base/json-api.ts +++ b/packages/taco/src/conditions/base/json-api.ts @@ -1,4 +1,3 @@ -import { parse } from 'jsonpath'; import { z } from 'zod'; import { Condition } from '../condition'; @@ -6,14 +5,68 @@ import { OmitConditionType, returnValueTestSchema } from '../shared'; export const JsonApiConditionType = 'json-api'; -const validateJSONPath = (jsonPath: string): boolean => { - try { - parse(jsonPath); - return true; - } catch (error) { - return false; +function tokenize(expression: string): string[] { + const regex = + /(\$|@|\.\.|\.|[[\]]|\?|\(|\)|==|!=|<=|>=|<|>|&&|\|\||[a-zA-Z_][\w]*|\d+|'[^']*')/g; + return expression.match(regex) || []; +} + +function validateJSONPath(expression: string): boolean { + const tokens = tokenize(expression); + + let depth = 0; + let inBracket = false; + let inFilter = false; + let lastTokenWasCloseBracket = false; + + for (let i = 0; i < tokens.length; i++) { + const token = tokens[i]; + + if (token === '$' && i !== 0) return false; // $ only at the beginning + if (token === '@' && !inFilter) return false; // @ only in filters + + if (token === '[') { + if (lastTokenWasCloseBracket) return false; // Don't allow [...][] + depth++; + inBracket = true; + lastTokenWasCloseBracket = false; + } else if (token === ']') { + if (depth === 0) return false; + depth--; + inBracket = false; + lastTokenWasCloseBracket = true; + } else { + lastTokenWasCloseBracket = false; + } + + if (token === '?') { + if (!inBracket) return false; + inFilter = true; + } else if (token === '(') { + if (!inFilter) return false; + } else if (token === ')') { + if (!inFilter) return false; + inFilter = false; + } + + // Check for valid operators in filters + if ( + inFilter && + ['==', '!=', '<', '<=', '>', '>=', '&&', '||'].includes(token) + ) { + if (i === 0 || i === tokens.length - 1) return false; + } + + // Check that there are no two consecutive dots outside brackets + if (token === '.' && i > 0 && tokens[i - 1] === '.' && !inBracket) + return false; } -}; + + if (depth !== 0) return false; // Unclosed brackets + if (inFilter) return false; // Unclosed filter + + return true; +} export const jsonPathSchema = z .string() diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 90f7616ed..d8586b46b 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -573,9 +573,6 @@ importers: ethers: specifier: '*' version: 5.7.2(bufferutil@4.0.8)(utf-8-validate@5.0.10) - jsonpath: - specifier: ^1.1.1 - version: 1.1.1 semver: specifier: ^7.6.3 version: 7.6.3 @@ -586,9 +583,6 @@ importers: '@nucypher/test-utils': specifier: workspace:* version: link:../test-utils - '@types/jsonpath': - specifier: ^0.2.4 - version: 0.2.4 '@types/semver': specifier: ^7.5.8 version: 7.5.8 @@ -2613,9 +2607,6 @@ packages: '@types/json5@0.0.29': resolution: {integrity: sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==} - '@types/jsonpath@0.2.4': - resolution: {integrity: sha512-K3hxB8Blw0qgW6ExKgMbXQv2UPZBoE2GqLpVY+yr7nMD2Pq86lsuIzyAaiQ7eMqFL5B6di6pxSkogLJEyEHoGA==} - '@types/keyv@3.1.4': resolution: {integrity: sha512-BQ5aZNSCpj7D6K2ksrRCTmKRLEpnPvWDiLPfoGyhZ++8YtiK9d/3DBKPJgry359X/P1PfruyYwvnvwFjuEiEIg==} @@ -11574,8 +11565,6 @@ snapshots: '@types/json5@0.0.29': {} - '@types/jsonpath@0.2.4': {} - '@types/keyv@3.1.4': dependencies: '@types/node': 20.16.3 From 0cd6e6c03cfd4e10f2e8a0a2770f62100abe698b Mon Sep 17 00:00:00 2001 From: James Campbell Date: Fri, 19 Jul 2024 15:54:54 +0200 Subject: [PATCH 12/75] Fix import ordering --- packages/taco/src/conditions/condition-factory.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/taco/src/conditions/condition-factory.ts b/packages/taco/src/conditions/condition-factory.ts index b22a34d2a..273cbe742 100644 --- a/packages/taco/src/conditions/condition-factory.ts +++ b/packages/taco/src/conditions/condition-factory.ts @@ -3,17 +3,17 @@ import { ContractConditionProps, ContractConditionType, } from './base/contract'; +import { + JsonApiCondition, + JsonApiConditionProps, + JsonApiConditionType, +} from './base/json-api'; import { RpcCondition, RpcConditionProps, RpcConditionType } from './base/rpc'; import { TimeCondition, TimeConditionProps, TimeConditionType, } from './base/time'; -import { - JsonApiCondition, - JsonApiConditionProps, - JsonApiConditionType, -} from './base/json-api'; import { CompoundCondition, CompoundConditionProps, From aa05e729078e168c25484e220c7a8cfe533bb6af Mon Sep 17 00:00:00 2001 From: derekpierre Date: Thu, 19 Sep 2024 11:59:50 -0400 Subject: [PATCH 13/75] 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 b5a18bf65e73c31d782b31510618fb75b3c0b9b0 Mon Sep 17 00:00:00 2001 From: andresceballosm Date: Thu, 1 Aug 2024 10:47:16 -0500 Subject: [PATCH 14/75] Added Library @astronautlabs/jsonpath --- packages/taco/package.json | 1 + packages/taco/src/conditions/base/json-api.ts | 69 +++---------------- pnpm-lock.yaml | 10 +++ 3 files changed, 19 insertions(+), 61 deletions(-) diff --git a/packages/taco/package.json b/packages/taco/package.json index f41506c8a..f9a68b827 100644 --- a/packages/taco/package.json +++ b/packages/taco/package.json @@ -39,6 +39,7 @@ "typedoc": "typedoc" }, "dependencies": { + "@astronautlabs/jsonpath": "^1.1.2", "@nucypher/nucypher-core": "*", "@nucypher/shared": "workspace:*", "@nucypher/taco-auth": "workspace:*", diff --git a/packages/taco/src/conditions/base/json-api.ts b/packages/taco/src/conditions/base/json-api.ts index 01ac73e77..22032cb34 100644 --- a/packages/taco/src/conditions/base/json-api.ts +++ b/packages/taco/src/conditions/base/json-api.ts @@ -1,3 +1,4 @@ +import { JSONPath } from '@astronautlabs/jsonpath'; import { z } from 'zod'; import { Condition } from '../condition'; @@ -5,68 +6,14 @@ import { OmitConditionType, returnValueTestSchema } from '../shared'; export const JsonApiConditionType = 'json-api'; -function tokenize(expression: string): string[] { - const regex = - /(\$|@|\.\.|\.|[[\]]|\?|\(|\)|==|!=|<=|>=|<|>|&&|\|\||[a-zA-Z_][\w]*|\d+|'[^']*')/g; - return expression.match(regex) || []; -} - -function validateJSONPath(expression: string): boolean { - const tokens = tokenize(expression); - - let depth = 0; - let inBracket = false; - let inFilter = false; - let lastTokenWasCloseBracket = false; - - for (let i = 0; i < tokens.length; i++) { - const token = tokens[i]; - - if (token === '$' && i !== 0) return false; // $ only at the beginning - if (token === '@' && !inFilter) return false; // @ only in filters - - if (token === '[') { - if (lastTokenWasCloseBracket) return false; // Don't allow [...][] - depth++; - inBracket = true; - lastTokenWasCloseBracket = false; - } else if (token === ']') { - if (depth === 0) return false; - depth--; - inBracket = false; - lastTokenWasCloseBracket = true; - } else { - lastTokenWasCloseBracket = false; - } - - if (token === '?') { - if (!inBracket) return false; - inFilter = true; - } else if (token === '(') { - if (!inFilter) return false; - } else if (token === ')') { - if (!inFilter) return false; - inFilter = false; - } - - // Check for valid operators in filters - if ( - inFilter && - ['==', '!=', '<', '<=', '>', '>=', '&&', '||'].includes(token) - ) { - if (i === 0 || i === tokens.length - 1) return false; - } - - // Check that there are no two consecutive dots outside brackets - if (token === '.' && i > 0 && tokens[i - 1] === '.' && !inBracket) - return false; +const validateJSONPath = (jsonPath: string): boolean => { + try { + JSONPath.parse(jsonPath); + return true; + } catch (error) { + return false; } - - if (depth !== 0) return false; // Unclosed brackets - if (inFilter) return false; // Unclosed filter - - return true; -} +}; export const jsonPathSchema = z .string() diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index d8586b46b..eedf8cd18 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -561,6 +561,9 @@ importers: packages/taco: dependencies: + '@astronautlabs/jsonpath': + specifier: ^1.1.2 + version: 1.1.2 '@nucypher/nucypher-core': specifier: ^0.14.5 version: 0.14.5 @@ -659,6 +662,9 @@ packages: resolution: {integrity: sha512-QS4BlivXQy/uJgXcNOfXNjv8l+MSd+qQ256mY/Jc6iaWbfn69nRYh6chjSyLot4fHA49QxlZlWh1mJLlfNdtow==} engines: {node: '>=11.0.0'} + '@astronautlabs/jsonpath@1.1.2': + resolution: {integrity: sha512-FqL/muoreH7iltYC1EB5Tvox5E8NSOOPGkgns4G+qxRKl6k5dxEVljUjB5NcKESzkqwnUqWjSZkL61XGYOuV+A==} + '@babel/code-frame@7.23.5': resolution: {integrity: sha512-CgH3s1a96LipHCmSUmYFPwY7MNx8C3avkq7i4Wl3cfa662ldtUe4VM1TPXX70pfmrlWTb6jLqTYrZyT2ZTJBgA==} engines: {node: '>=6.9.0'} @@ -9082,6 +9088,10 @@ snapshots: transitivePeerDependencies: - debug + '@astronautlabs/jsonpath@1.1.2': + dependencies: + static-eval: 2.0.2 + '@babel/code-frame@7.23.5': dependencies: '@babel/highlight': 7.23.4 From a05595b03695f7c924b5fac68f6cc502c588afc7 Mon Sep 17 00:00:00 2001 From: James Campbell Date: Fri, 19 Jul 2024 17:06:32 +0200 Subject: [PATCH 15/75] Update test json url and add serialization test --- .../test/conditions/condition-expr.test.ts | 20 +++++++++++++++++++ packages/taco/test/test-utils.ts | 2 +- 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/packages/taco/test/conditions/condition-expr.test.ts b/packages/taco/test/conditions/condition-expr.test.ts index bf4fe8946..4defa46cf 100644 --- a/packages/taco/test/conditions/condition-expr.test.ts +++ b/packages/taco/test/conditions/condition-expr.test.ts @@ -9,6 +9,7 @@ import { ContractCondition, ContractConditionProps, } from '../../src/conditions/base/contract'; +import { JsonApiCondition } from '../../src/conditions/base/json-api'; import { RpcCondition, RpcConditionType } from '../../src/conditions/base/rpc'; import { TimeCondition, @@ -20,6 +21,7 @@ import { ERC721Balance } from '../../src/conditions/predefined/erc721'; import { testContractConditionObj, testFunctionAbi, + testJsonApiConditionObj, testReturnValueTest, testRpcConditionObj, testTimeConditionObj, @@ -56,6 +58,7 @@ describe('condition set', () => { const rpcCondition = new RpcCondition(testRpcConditionObj); const timeCondition = new TimeCondition(testTimeConditionObj); + const jsonApiCondition = new JsonApiCondition(testJsonApiConditionObj); const compoundCondition = new CompoundCondition({ operator: 'and', operands: [ @@ -401,6 +404,23 @@ describe('condition set', () => { expect(conditionExprFromJson.condition).toBeInstanceOf(RpcCondition); }); + it('json api condition serialization', () => { + const conditionExpr = new ConditionExpression(jsonApiCondition); + + const conditionExprJson = conditionExpr.toJson(); + expect(conditionExprJson).toBeDefined(); + expect(conditionExprJson).toContain('endpoint'); + expect(conditionExprJson).toContain('https://_this_would_totally_fail.com'); + expect(conditionExprJson).toContain('parameters'); + expect(conditionExprJson).toContain('query'); + expect(conditionExprJson).toContain('$.ethereum.usd'); + expect(conditionExprJson).toContain('returnValueTest'); + + const conditionExprFromJson = ConditionExpression.fromJSON(conditionExprJson); + expect(conditionExprFromJson).toBeDefined(); + expect(conditionExprFromJson.condition).toBeInstanceOf(JsonApiCondition); + }); + it('compound condition serialization', () => { const conditionExpr = new ConditionExpression(compoundCondition); const compoundConditionObj = compoundCondition.toObj(); diff --git a/packages/taco/test/test-utils.ts b/packages/taco/test/test-utils.ts index feac6a829..60ebab597 100644 --- a/packages/taco/test/test-utils.ts +++ b/packages/taco/test/test-utils.ts @@ -229,7 +229,7 @@ export const testTimeConditionObj: TimeConditionProps = { export const testJsonApiConditionObj = { conditionType: JsonApiConditionType, - endpoint: 'https://api.coingecko.com/api/v3/simple/price', + endpoint: 'https://_this_would_totally_fail.com', parameters: { 'ids': 'ethereum', 'vs_currencies': 'usd', From 046e351364b5a806928143de9ee3de88259219fa Mon Sep 17 00:00:00 2001 From: derekpierre Date: Thu, 19 Sep 2024 15:50:34 -0400 Subject: [PATCH 16/75] 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 0da3f2d76fec62eef907ba85118130cfb57c21ec Mon Sep 17 00:00:00 2001 From: andresceballosm Date: Thu, 1 Aug 2024 14:31:32 -0500 Subject: [PATCH 17/75] Updated unit test --- .../taco/test/conditions/base/json-api.test.ts | 15 --------------- 1 file changed, 15 deletions(-) diff --git a/packages/taco/test/conditions/base/json-api.test.ts b/packages/taco/test/conditions/base/json-api.test.ts index 94b07af81..d71d1ee3f 100644 --- a/packages/taco/test/conditions/base/json-api.test.ts +++ b/packages/taco/test/conditions/base/json-api.test.ts @@ -7,33 +7,18 @@ describe('JSONPath Validation', () => { const invalidPath = '$.store.book[?(@.price < ]'; const result = jsonPathSchema.safeParse(invalidPath); expect(result.success).toBe(false); - if (!result.success) { - expect(result.error.errors[0].message).toBe( - 'Invalid JSONPath expression', - ); - } }); it('Invalid JSONPath: Incorrect use of brackets', () => { const invalidPath = '$[store][book]'; const result = jsonPathSchema.safeParse(invalidPath); expect(result.success).toBe(false); - if (!result.success) { - expect(result.error.errors[0].message).toBe( - 'Invalid JSONPath expression', - ); - } }); it('Invalid JSONPath: Unclosed wildcard asterisk', () => { const invalidPath = '$.store.book[*'; const result = jsonPathSchema.safeParse(invalidPath); expect(result.success).toBe(false); - if (!result.success) { - expect(result.error.errors[0].message).toBe( - 'Invalid JSONPath expression', - ); - } }); it('Valid JSONPath expression', () => { From 130ecb377840d993920829ef0a0bfaddabe6672f Mon Sep 17 00:00:00 2001 From: James Campbell Date: Mon, 29 Jul 2024 09:04:18 +0200 Subject: [PATCH 18/75] Fix linting error --- packages/taco/test/conditions/base/json.test.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/taco/test/conditions/base/json.test.ts b/packages/taco/test/conditions/base/json.test.ts index def687c1e..9c26a7f7a 100644 --- a/packages/taco/test/conditions/base/json.test.ts +++ b/packages/taco/test/conditions/base/json.test.ts @@ -1,3 +1,4 @@ +/* eslint-disable @typescript-eslint/no-unused-vars */ import { describe, expect, it } from 'vitest'; import { @@ -37,7 +38,7 @@ describe('JsonApiCondition', () => { describe('parameters', () => { it('accepts conditions without query path', () => { - const {query, ...noQueryObj} = testJsonApiConditionObj; + const { query, ...noQueryObj} = testJsonApiConditionObj; const result = JsonApiCondition.validate( JsonApiConditionSchema, noQueryObj @@ -48,7 +49,7 @@ describe('JsonApiCondition', () => { }); it('accepts conditions without parameters', () => { - const {parameters, ...noParamsObj} = testJsonApiConditionObj; + const { query, ...noParamsObj} = testJsonApiConditionObj; const result = JsonApiCondition.validate( JsonApiConditionSchema, noParamsObj From deea3f8251bf767cf3bd552c0530502d6e1bdadd Mon Sep 17 00:00:00 2001 From: derekpierre Date: Thu, 19 Sep 2024 20:00:09 -0400 Subject: [PATCH 19/75] 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 2dc5a197fb777b28dc5f035b04c80fa9c3317f6e Mon Sep 17 00:00:00 2001 From: andresceballosm Date: Thu, 1 Aug 2024 14:53:41 -0500 Subject: [PATCH 20/75] Update in unit tests --- packages/taco/test/conditions/base/json-api.test.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/packages/taco/test/conditions/base/json-api.test.ts b/packages/taco/test/conditions/base/json-api.test.ts index d71d1ee3f..09c622e6e 100644 --- a/packages/taco/test/conditions/base/json-api.test.ts +++ b/packages/taco/test/conditions/base/json-api.test.ts @@ -7,18 +7,21 @@ describe('JSONPath Validation', () => { const invalidPath = '$.store.book[?(@.price < ]'; const result = jsonPathSchema.safeParse(invalidPath); expect(result.success).toBe(false); + expect(result.error!.errors[0].message).toBe('Invalid JSONPath expression'); }); it('Invalid JSONPath: Incorrect use of brackets', () => { const invalidPath = '$[store][book]'; const result = jsonPathSchema.safeParse(invalidPath); expect(result.success).toBe(false); + expect(result.error!.errors[0].message).toBe('Invalid JSONPath expression'); }); it('Invalid JSONPath: Unclosed wildcard asterisk', () => { const invalidPath = '$.store.book[*'; const result = jsonPathSchema.safeParse(invalidPath); expect(result.success).toBe(false); + expect(result.error!.errors[0].message).toBe('Invalid JSONPath expression'); }); it('Valid JSONPath expression', () => { From cfcb53b2e7c55d79953be72eea985b0c3b8cd153 Mon Sep 17 00:00:00 2001 From: derekpierre Date: Thu, 19 Sep 2024 20:01:02 -0400 Subject: [PATCH 21/75] 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 9339828870ec3d48588b573b30628e1179533956 Mon Sep 17 00:00:00 2001 From: derekpierre Date: Fri, 20 Sep 2024 10:21:43 -0400 Subject: [PATCH 22/75] 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 9157f0a2b12689f0b5913e980915e0b7bcbc99ec Mon Sep 17 00:00:00 2001 From: derekpierre Date: Fri, 4 Oct 2024 14:15:39 -0400 Subject: [PATCH 23/75] Update package.json post-rebase. --- packages/taco/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/taco/package.json b/packages/taco/package.json index f9a68b827..e1914d58f 100644 --- a/packages/taco/package.json +++ b/packages/taco/package.json @@ -49,7 +49,7 @@ }, "devDependencies": { "@nucypher/test-utils": "workspace:*", - "@types/semver": "^7.5.8", + "@types/semver": "^7.5.8" }, "engines": { "node": ">=18", From 556c7878873d8983aed756bd933393756f46db61 Mon Sep 17 00:00:00 2001 From: derekpierre Date: Fri, 20 Sep 2024 10:35:08 -0400 Subject: [PATCH 24/75] 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)'], }, }); }); From f198489ae129fa30597a0fd949085399a472985b Mon Sep 17 00:00:00 2001 From: derekpierre Date: Fri, 4 Oct 2024 16:14:20 -0400 Subject: [PATCH 25/75] Appease linter since safe parse returns a discriminated union i.e. "error" is not present when success is true. --- .../test/conditions/base/json-api.test.ts | 21 ++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) diff --git a/packages/taco/test/conditions/base/json-api.test.ts b/packages/taco/test/conditions/base/json-api.test.ts index 09c622e6e..affaa1c8a 100644 --- a/packages/taco/test/conditions/base/json-api.test.ts +++ b/packages/taco/test/conditions/base/json-api.test.ts @@ -7,21 +7,36 @@ describe('JSONPath Validation', () => { const invalidPath = '$.store.book[?(@.price < ]'; const result = jsonPathSchema.safeParse(invalidPath); expect(result.success).toBe(false); - expect(result.error!.errors[0].message).toBe('Invalid JSONPath expression'); + if (!result.success) { + // appease linter + expect(result.error!.errors[0].message).toBe( + 'Invalid JSONPath expression', + ); + } }); it('Invalid JSONPath: Incorrect use of brackets', () => { const invalidPath = '$[store][book]'; const result = jsonPathSchema.safeParse(invalidPath); expect(result.success).toBe(false); - expect(result.error!.errors[0].message).toBe('Invalid JSONPath expression'); + if (!result.success) { + // appease linter + expect(result.error!.errors[0].message).toBe( + 'Invalid JSONPath expression', + ); + } }); it('Invalid JSONPath: Unclosed wildcard asterisk', () => { const invalidPath = '$.store.book[*'; const result = jsonPathSchema.safeParse(invalidPath); expect(result.success).toBe(false); - expect(result.error!.errors[0].message).toBe('Invalid JSONPath expression'); + if (!result.success) { + // appease linter + expect(result.error!.errors[0].message).toBe( + 'Invalid JSONPath expression', + ); + } }); it('Valid JSONPath expression', () => { From 6b6748998966a8e75a574adbc6d8f0b7752e82ca Mon Sep 17 00:00:00 2001 From: derekpierre Date: Mon, 7 Oct 2024 15:11:10 -0400 Subject: [PATCH 26/75] Include taco-auth in API documentation. At @ts-expect-error annotation so that docs can build. --- CONTRIBUTING.md | 4 ++-- packages/taco-auth/test/auth-provider.test.ts | 1 + typedoc.json | 2 +- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 693df9bda..c0888d438 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -24,8 +24,8 @@ pnpm fix Build and publish documentation with: ```bash -pnpm doc -pnpm doc:publish +pnpm typedoc +pnpm typedoc:publish ``` ## Publishing diff --git a/packages/taco-auth/test/auth-provider.test.ts b/packages/taco-auth/test/auth-provider.test.ts index b9157be0a..611cbb712 100644 --- a/packages/taco-auth/test/auth-provider.test.ts +++ b/packages/taco-auth/test/auth-provider.test.ts @@ -77,6 +77,7 @@ describe('auth provider caching', () => { it('caches auth signature, but regenerates when expired', async () => { const createAuthSignatureSpy = vi.spyOn( eip4361Provider, + // @ts-expect-error -- spying on private function 'createSIWEAuthMessage', ); diff --git a/typedoc.json b/typedoc.json index 14547f69c..ec18be54e 100644 --- a/typedoc.json +++ b/typedoc.json @@ -1,6 +1,6 @@ { "extends": ["./typedoc.base.json"], - "entryPoints": ["packages/pre", "packages/shared", "packages/taco"], + "entryPoints": ["packages/pre", "packages/shared", "packages/taco", "packages/taco-auth"], "entryPointStrategy": "packages", "name": "@nucypher/taco-web" } From 02ab6c02b032e416917fd0a5128ea3d829aed72a Mon Sep 17 00:00:00 2001 From: derekpierre Date: Fri, 27 Sep 2024 10:55:00 -0400 Subject: [PATCH 27/75] Add schemas folder to house all zod schemas for conditions, context, etc. Add common single schema that encompasses all possible condition schemas. --- .../taco/src/conditions/schemas/common.ts | 65 +++++++++++++ .../taco/src/conditions/schemas/compound.ts | 46 +++++++++ .../taco/src/conditions/schemas/context.ts | 15 +++ .../taco/src/conditions/schemas/contract.ts | 97 +++++++++++++++++++ packages/taco/src/conditions/schemas/index.ts | 8 ++ .../conditions/schemas/return-value-test.ts | 11 +++ packages/taco/src/conditions/schemas/rpc.ts | 26 +++++ .../taco/src/conditions/schemas/sequential.ts | 52 ++++++++++ packages/taco/src/conditions/schemas/time.ts | 18 ++++ packages/taco/src/conditions/schemas/utils.ts | 18 ++++ 10 files changed, 356 insertions(+) create mode 100644 packages/taco/src/conditions/schemas/common.ts create mode 100644 packages/taco/src/conditions/schemas/compound.ts create mode 100644 packages/taco/src/conditions/schemas/context.ts create mode 100644 packages/taco/src/conditions/schemas/contract.ts create mode 100644 packages/taco/src/conditions/schemas/index.ts create mode 100644 packages/taco/src/conditions/schemas/return-value-test.ts create mode 100644 packages/taco/src/conditions/schemas/rpc.ts create mode 100644 packages/taco/src/conditions/schemas/sequential.ts create mode 100644 packages/taco/src/conditions/schemas/time.ts create mode 100644 packages/taco/src/conditions/schemas/utils.ts diff --git a/packages/taco/src/conditions/schemas/common.ts b/packages/taco/src/conditions/schemas/common.ts new file mode 100644 index 000000000..66263bab3 --- /dev/null +++ b/packages/taco/src/conditions/schemas/common.ts @@ -0,0 +1,65 @@ +import { EthAddressSchema } from '@nucypher/shared'; +import { + USER_ADDRESS_PARAM_DEFAULT, + USER_ADDRESS_PARAM_EXTERNAL_EIP4361, +} from '@nucypher/taco-auth'; +import { Primitive, z, ZodLiteral } from 'zod'; + +import { CONTEXT_PARAM_PREFIX, CONTEXT_PARAM_REGEXP } from '../const'; + +export const contextParamSchema = z.string().regex(CONTEXT_PARAM_REGEXP); + +// We want to discriminate between ContextParams and plain strings +// If a string starts with `:`, it's a ContextParam +export const plainStringSchema = z.string().refine( + (str) => { + return !str.startsWith(CONTEXT_PARAM_PREFIX); + }, + { + message: `String must not be a context parameter i.e. not start with "${CONTEXT_PARAM_PREFIX}"`, + }, +); + +export const UserAddressSchema = z.enum([ + USER_ADDRESS_PARAM_DEFAULT, + USER_ADDRESS_PARAM_EXTERNAL_EIP4361, +]); + +export const EthAddressOrUserAddressSchema = z.union([ + EthAddressSchema, + UserAddressSchema, +]); + +export const baseConditionSchema = z.object({ + conditionType: z.string(), +}); + +// Source: https://github.com/colinhacks/zod/issues/831#issuecomment-1063481764 +const createUnion = < + T extends Readonly<[Primitive, Primitive, ...Primitive[]]>, +>( + values: T, +) => { + const zodLiterals = values.map((value) => z.literal(value)) as unknown as [ + ZodLiteral, + ZodLiteral, + ...ZodLiteral[], + ]; + return z.union(zodLiterals); +}; + +function createUnionSchema(values: T) { + if (values.length === 0) { + return z.never(); + } + + if (values.length === 1) { + return z.literal(values[0]); + } + + return createUnion( + values as unknown as Readonly<[Primitive, Primitive, ...Primitive[]]>, + ); +} + +export default createUnionSchema; diff --git a/packages/taco/src/conditions/schemas/compound.ts b/packages/taco/src/conditions/schemas/compound.ts new file mode 100644 index 000000000..d3b6a297d --- /dev/null +++ b/packages/taco/src/conditions/schemas/compound.ts @@ -0,0 +1,46 @@ +import { z } from 'zod'; + +import { maxNestedDepth } from '../multi-condition'; + +import { baseConditionSchema } from './common'; +import { anyConditionSchema } from './utils'; + +export const CompoundConditionType = 'compound'; + +export const compoundConditionSchema: z.ZodSchema = baseConditionSchema + .extend({ + conditionType: z + .literal(CompoundConditionType) + .default(CompoundConditionType), + operator: z.enum(['and', 'or', 'not']), + operands: z.array(anyConditionSchema).min(1).max(5), + }) + .refine( + (condition) => { + // 'and' and 'or' operators must have at least 2 operands + if (['and', 'or'].includes(condition.operator)) { + return condition.operands.length >= 2; + } + + // 'not' operator must have exactly 1 operand + if (condition.operator === 'not') { + return condition.operands.length === 1; + } + + // We test positive cases exhaustively, so we return false here: + return false; + }, + ({ operands, operator }) => ({ + message: `Invalid number of operands ${operands.length} for operator "${operator}"`, + path: ['operands'], + }), + ) + .refine( + (condition) => maxNestedDepth(2)(condition), + { + message: 'Exceeded max nested depth of 2 for multi-condition type', + path: ['operands'], + }, // Max nested depth of 2 + ); + +export type CompoundConditionProps = z.infer; diff --git a/packages/taco/src/conditions/schemas/context.ts b/packages/taco/src/conditions/schemas/context.ts new file mode 100644 index 000000000..3c2f2a92a --- /dev/null +++ b/packages/taco/src/conditions/schemas/context.ts @@ -0,0 +1,15 @@ +import { z } from 'zod'; + +import { CONTEXT_PARAM_REGEXP } from '../const'; + +import { plainStringSchema } from './common'; + +export const contextParamSchema = z.string().regex(CONTEXT_PARAM_REGEXP); + +const paramSchema = z.union([plainStringSchema, z.boolean(), z.number()]); + +export const paramOrContextParamSchema: z.ZodSchema = z.union([ + paramSchema, + contextParamSchema, + z.lazy(() => z.array(paramOrContextParamSchema)), +]); diff --git a/packages/taco/src/conditions/schemas/contract.ts b/packages/taco/src/conditions/schemas/contract.ts new file mode 100644 index 000000000..bf06c38c1 --- /dev/null +++ b/packages/taco/src/conditions/schemas/contract.ts @@ -0,0 +1,97 @@ +import { ETH_ADDRESS_REGEXP } from '@nucypher/shared'; +import { ethers } from 'ethers'; +import { z } from 'zod'; + +import { paramOrContextParamSchema } from './context'; +import { rpcConditionSchema } from './rpc'; + +// TODO: Consider replacing with `z.unknown`: +// Since Solidity types are tied to Solidity version, we may not be able to accurately represent them in Zod. +// Alternatively, find a TS Solidity type lib. +const EthBaseTypes: [string, ...string[]] = [ + 'bool', + 'string', + 'address', + 'address payable', + ...Array.from({ length: 32 }, (_v, i) => `bytes${i + 1}`), // bytes1 through bytes32 + 'bytes', + ...Array.from({ length: 32 }, (_v, i) => `uint${8 * (i + 1)}`), // uint8 through uint256 + ...Array.from({ length: 32 }, (_v, i) => `int${8 * (i + 1)}`), // int8 through int256 +]; + +const functionAbiVariableSchema = z + .object({ + name: z.string(), + type: z.enum(EthBaseTypes), + internalType: z.enum(EthBaseTypes), // TODO: Do we need to validate this? + }) + .strict(); + +const functionAbiSchema = z + .object({ + name: z.string(), + type: z.literal('function'), + inputs: z.array(functionAbiVariableSchema).min(0), + outputs: z.array(functionAbiVariableSchema).nonempty(), + stateMutability: z.union([z.literal('view'), z.literal('pure')]), + }) + .strict() + .refine( + (functionAbi) => { + let asInterface; + try { + // `stringify` here because ethers.utils.Interface doesn't accept a Zod schema + asInterface = new ethers.utils.Interface(JSON.stringify([functionAbi])); + } catch (e) { + return false; + } + + const functionsInAbi = Object.values(asInterface.functions || {}); + return functionsInAbi.length === 1; + }, + { + message: '"functionAbi" must contain a single function definition', + path: ['functionAbi'], + }, + ) + .refine( + (functionAbi) => { + const asInterface = new ethers.utils.Interface( + JSON.stringify([functionAbi]), + ); + const nrOfInputs = asInterface.fragments[0].inputs.length; + return functionAbi.inputs.length === nrOfInputs; + }, + { + message: '"parameters" must have the same length as "functionAbi.inputs"', + path: ['parameters'], + }, + ); + +export type FunctionAbiProps = z.infer; + +export const ContractConditionType = 'contract'; +export const contractConditionSchema = rpcConditionSchema + .extend({ + conditionType: z + .literal(ContractConditionType) + .default(ContractConditionType), + contractAddress: z.string().regex(ETH_ADDRESS_REGEXP).length(42), + standardContractType: z.enum(['ERC20', 'ERC721']).optional(), + method: z.string(), + functionAbi: functionAbiSchema.optional(), + parameters: z.array(paramOrContextParamSchema), + }) + // Adding this custom logic causes the return type to be ZodEffects instead of ZodObject + // https://github.com/colinhacks/zod/issues/2474 + .refine( + // A check to see if either 'standardContractType' or 'functionAbi' is set + (data) => Boolean(data.standardContractType) !== Boolean(data.functionAbi), + { + message: + "At most one of the fields 'standardContractType' and 'functionAbi' must be defined", + path: ['standardContractType'], + }, + ); + +export type ContractConditionProps = z.infer; diff --git a/packages/taco/src/conditions/schemas/index.ts b/packages/taco/src/conditions/schemas/index.ts new file mode 100644 index 000000000..7e862693f --- /dev/null +++ b/packages/taco/src/conditions/schemas/index.ts @@ -0,0 +1,8 @@ +export * as common from './common'; +export * as compound from './compound'; +export * as context from './context'; +export * as contract from './contract'; +export * as returnValueTest from './return-value-test'; +export * as rpc from './rpc'; +export * as sequential from './sequential'; +export * as time from './time'; diff --git a/packages/taco/src/conditions/schemas/return-value-test.ts b/packages/taco/src/conditions/schemas/return-value-test.ts new file mode 100644 index 000000000..63b038c5b --- /dev/null +++ b/packages/taco/src/conditions/schemas/return-value-test.ts @@ -0,0 +1,11 @@ +import { z } from 'zod'; + +import { paramOrContextParamSchema } from './context'; + +export const returnValueTestSchema = z.object({ + index: z.number().int().nonnegative().optional(), + comparator: z.enum(['==', '>', '<', '>=', '<=', '!=']), + value: paramOrContextParamSchema, +}); + +export type ReturnValueTestProps = z.infer; diff --git a/packages/taco/src/conditions/schemas/rpc.ts b/packages/taco/src/conditions/schemas/rpc.ts new file mode 100644 index 000000000..9752bae85 --- /dev/null +++ b/packages/taco/src/conditions/schemas/rpc.ts @@ -0,0 +1,26 @@ +import { z } from 'zod'; + +import { SUPPORTED_CHAIN_IDS } from '../const'; + +import createUnionSchema, { + baseConditionSchema, + EthAddressOrUserAddressSchema, +} from './common'; +import { paramOrContextParamSchema } from './context'; +import { returnValueTestSchema } from './return-value-test'; + +export const RpcConditionType = 'rpc'; + +export const rpcConditionSchema = baseConditionSchema.extend({ + conditionType: z.literal(RpcConditionType).default(RpcConditionType), + chain: createUnionSchema(SUPPORTED_CHAIN_IDS), + method: z.enum(['eth_getBalance']), + parameters: z.union([ + z.array(EthAddressOrUserAddressSchema).nonempty(), + // Using tuple here because ordering matters + z.tuple([EthAddressOrUserAddressSchema, paramOrContextParamSchema]), + ]), + returnValueTest: returnValueTestSchema, // Update to allow multiple return values after expanding supported methods +}); + +export type RpcConditionProps = z.infer; diff --git a/packages/taco/src/conditions/schemas/sequential.ts b/packages/taco/src/conditions/schemas/sequential.ts new file mode 100644 index 000000000..5b11de9b0 --- /dev/null +++ b/packages/taco/src/conditions/schemas/sequential.ts @@ -0,0 +1,52 @@ +import { z } from 'zod'; + +import { maxNestedDepth } from '../multi-condition'; + +import { baseConditionSchema, plainStringSchema } from './common'; +import { anyConditionSchema } from './utils'; + +export const SequentialConditionType = 'sequential'; + +export const conditionVariableSchema: z.ZodSchema = z.object({ + varName: plainStringSchema, + condition: anyConditionSchema, +}); +export type ConditionVariableProps = z.infer; + +export const sequentialConditionSchema: z.ZodSchema = baseConditionSchema + .extend({ + conditionType: z + .literal(SequentialConditionType) + .default(SequentialConditionType), + conditionVariables: z.array(conditionVariableSchema).min(2).max(5), + }) + .refine( + (condition) => maxNestedDepth(2)(condition), + { + message: 'Exceeded max nested depth of 2 for multi-condition type', + path: ['conditionVariables'], + }, // Max nested depth of 2 + ) + .refine( + // check for duplicate var names + (condition) => { + const seen = new Set(); + return condition.conditionVariables.every( + (child: ConditionVariableProps) => { + if (seen.has(child.varName)) { + return false; + } + seen.add(child.varName); + return true; + }, + ); + }, + { + message: 'Duplicate variable names are not allowed', + path: ['conditionVariables'], + }, + ); + +export type SequentialConditionProps = z.infer< + typeof sequentialConditionSchema +>; diff --git a/packages/taco/src/conditions/schemas/time.ts b/packages/taco/src/conditions/schemas/time.ts new file mode 100644 index 000000000..65e57ac95 --- /dev/null +++ b/packages/taco/src/conditions/schemas/time.ts @@ -0,0 +1,18 @@ +import { z } from 'zod'; + +import { rpcConditionSchema } from './rpc'; + +// TimeCondition is an RpcCondition with the method set to 'blocktime' and no parameters + +export const TimeConditionType = 'time'; +export const TimeConditionMethod = 'blocktime'; + +// eslint-disable-next-line @typescript-eslint/no-unused-vars +const { parameters: _, ...restShape } = rpcConditionSchema.shape; +export const timeConditionSchema = z.object({ + ...restShape, + conditionType: z.literal(TimeConditionType).default(TimeConditionType), + method: z.literal(TimeConditionMethod).default(TimeConditionMethod), +}); + +export type TimeConditionProps = z.infer; diff --git a/packages/taco/src/conditions/schemas/utils.ts b/packages/taco/src/conditions/schemas/utils.ts new file mode 100644 index 000000000..6f7a15682 --- /dev/null +++ b/packages/taco/src/conditions/schemas/utils.ts @@ -0,0 +1,18 @@ +import { z } from 'zod'; + +import { compoundConditionSchema } from '../compound-condition'; + +import { contractConditionSchema } from './contract'; +import { rpcConditionSchema } from './rpc'; +import { sequentialConditionSchema } from './sequential'; +import { timeConditionSchema } from './time'; + +export const anyConditionSchema: z.ZodSchema = z.lazy(() => + z.union([ + rpcConditionSchema, + timeConditionSchema, + contractConditionSchema, + compoundConditionSchema, + sequentialConditionSchema, + ]), +); From 99f9907d1ec5fc678e3fa2530b82acd365438a1b Mon Sep 17 00:00:00 2001 From: derekpierre Date: Tue, 8 Oct 2024 08:22:04 -0400 Subject: [PATCH 28/75] Remove packages/pre from taco-web docs. --- typedoc.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/typedoc.json b/typedoc.json index ec18be54e..d2662cd5d 100644 --- a/typedoc.json +++ b/typedoc.json @@ -1,6 +1,6 @@ { "extends": ["./typedoc.base.json"], - "entryPoints": ["packages/pre", "packages/shared", "packages/taco", "packages/taco-auth"], + "entryPoints": ["packages/shared", "packages/taco", "packages/taco-auth"], "entryPointStrategy": "packages", "name": "@nucypher/taco-web" } From 8c4999e26e43e48dea3377beaaae1bfa279c6521 Mon Sep 17 00:00:00 2001 From: derekpierre Date: Fri, 27 Sep 2024 10:56:24 -0400 Subject: [PATCH 29/75] Use schemas from schemas folder, and re-export relevant types etc. --- packages/taco/src/conditions/base/contract.ts | 112 ++---------------- packages/taco/src/conditions/base/rpc.ts | 36 ++---- packages/taco/src/conditions/base/time.ts | 29 ++--- .../taco/src/conditions/compound-condition.ts | 71 ++--------- packages/taco/src/conditions/condition.ts | 4 +- packages/taco/src/conditions/schemas/index.ts | 8 -- packages/taco/src/conditions/sequential.ts | 76 ++---------- packages/taco/src/conditions/shared.ts | 52 ++------ packages/taco/src/conditions/zod.ts | 31 ----- 9 files changed, 68 insertions(+), 351 deletions(-) delete mode 100644 packages/taco/src/conditions/schemas/index.ts delete mode 100644 packages/taco/src/conditions/zod.ts diff --git a/packages/taco/src/conditions/base/contract.ts b/packages/taco/src/conditions/base/contract.ts index b170849ab..54df510dd 100644 --- a/packages/taco/src/conditions/base/contract.ts +++ b/packages/taco/src/conditions/base/contract.ts @@ -1,103 +1,17 @@ -import { ETH_ADDRESS_REGEXP } from '@nucypher/shared'; -import { ethers } from 'ethers'; -import { z } from 'zod'; - import { Condition } from '../condition'; -import { OmitConditionType, paramOrContextParamSchema } from '../shared'; - -import { rpcConditionSchema } from './rpc'; - -// TODO: Consider replacing with `z.unknown`: -// Since Solidity types are tied to Solidity version, we may not be able to accurately represent them in Zod. -// Alternatively, find a TS Solidity type lib. -const EthBaseTypes: [string, ...string[]] = [ - 'bool', - 'string', - 'address', - 'address payable', - ...Array.from({ length: 32 }, (_v, i) => `bytes${i + 1}`), // bytes1 through bytes32 - 'bytes', - ...Array.from({ length: 32 }, (_v, i) => `uint${8 * (i + 1)}`), // uint8 through uint256 - ...Array.from({ length: 32 }, (_v, i) => `int${8 * (i + 1)}`), // int8 through int256 -]; - -const functionAbiVariableSchema = z - .object({ - name: z.string(), - type: z.enum(EthBaseTypes), - internalType: z.enum(EthBaseTypes), // TODO: Do we need to validate this? - }) - .strict(); - -const functionAbiSchema = z - .object({ - name: z.string(), - type: z.literal('function'), - inputs: z.array(functionAbiVariableSchema).min(0), - outputs: z.array(functionAbiVariableSchema).nonempty(), - stateMutability: z.union([z.literal('view'), z.literal('pure')]), - }) - .strict() - .refine( - (functionAbi) => { - let asInterface; - try { - // `stringify` here because ethers.utils.Interface doesn't accept a Zod schema - asInterface = new ethers.utils.Interface(JSON.stringify([functionAbi])); - } catch (e) { - return false; - } - - const functionsInAbi = Object.values(asInterface.functions || {}); - return functionsInAbi.length === 1; - }, - { - message: '"functionAbi" must contain a single function definition', - path: ['functionAbi'], - }, - ) - .refine( - (functionAbi) => { - const asInterface = new ethers.utils.Interface( - JSON.stringify([functionAbi]), - ); - const nrOfInputs = asInterface.fragments[0].inputs.length; - return functionAbi.inputs.length === nrOfInputs; - }, - { - message: '"parameters" must have the same length as "functionAbi.inputs"', - path: ['parameters'], - }, - ); - -export type FunctionAbiProps = z.infer; - -export const ContractConditionType = 'contract'; - -export const contractConditionSchema = rpcConditionSchema - .extend({ - conditionType: z - .literal(ContractConditionType) - .default(ContractConditionType), - contractAddress: z.string().regex(ETH_ADDRESS_REGEXP).length(42), - standardContractType: z.enum(['ERC20', 'ERC721']).optional(), - method: z.string(), - functionAbi: functionAbiSchema.optional(), - parameters: z.array(paramOrContextParamSchema), - }) - // Adding this custom logic causes the return type to be ZodEffects instead of ZodObject - // https://github.com/colinhacks/zod/issues/2474 - .refine( - // A check to see if either 'standardContractType' or 'functionAbi' is set - (data) => Boolean(data.standardContractType) !== Boolean(data.functionAbi), - { - message: - "At most one of the fields 'standardContractType' and 'functionAbi' must be defined", - path: ['standardContractType'], - }, - ); - -export type ContractConditionProps = z.infer; +import { + ContractConditionProps, + contractConditionSchema, + ContractConditionType, +} from '../schemas/contract'; +import { OmitConditionType } from '../shared'; + +export { + ContractConditionProps, + contractConditionSchema, + ContractConditionType, + FunctionAbiProps, +} from '../schemas/contract'; export class ContractCondition extends Condition { constructor(value: OmitConditionType) { diff --git a/packages/taco/src/conditions/base/rpc.ts b/packages/taco/src/conditions/base/rpc.ts index d7ab306bb..8f73c79c1 100644 --- a/packages/taco/src/conditions/base/rpc.ts +++ b/packages/taco/src/conditions/base/rpc.ts @@ -1,30 +1,16 @@ -import { z } from 'zod'; - -import { baseConditionSchema, Condition } from '../condition'; -import { SUPPORTED_CHAIN_IDS } from '../const'; +import { Condition } from '../condition'; import { - EthAddressOrUserAddressSchema, - OmitConditionType, - paramOrContextParamSchema, - returnValueTestSchema, -} from '../shared'; -import createUnionSchema from '../zod'; - -export const RpcConditionType = 'rpc'; - -export const rpcConditionSchema = baseConditionSchema.extend({ - conditionType: z.literal(RpcConditionType).default(RpcConditionType), - chain: createUnionSchema(SUPPORTED_CHAIN_IDS), - method: z.enum(['eth_getBalance']), - parameters: z.union([ - z.array(EthAddressOrUserAddressSchema).nonempty(), - // Using tuple here because ordering matters - z.tuple([EthAddressOrUserAddressSchema, paramOrContextParamSchema]), - ]), - returnValueTest: returnValueTestSchema, // Update to allow multiple return values after expanding supported methods -}); + RpcConditionProps, + rpcConditionSchema, + RpcConditionType, +} from '../schemas/rpc'; +import { OmitConditionType } from '../shared'; -export type RpcConditionProps = z.infer; +export { + RpcConditionProps, + rpcConditionSchema, + RpcConditionType, +} from '../schemas/rpc'; export class RpcCondition extends Condition { constructor(value: OmitConditionType) { diff --git a/packages/taco/src/conditions/base/time.ts b/packages/taco/src/conditions/base/time.ts index 2e4739e19..2d8be8b63 100644 --- a/packages/taco/src/conditions/base/time.ts +++ b/packages/taco/src/conditions/base/time.ts @@ -1,24 +1,17 @@ -import { z } from 'zod'; - import { Condition } from '../condition'; +import { + TimeConditionProps, + timeConditionSchema, + TimeConditionType, +} from '../schemas/time'; import { OmitConditionType } from '../shared'; -import { rpcConditionSchema } 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 TimeConditionType = 'time'; -export const TimeConditionMethod = 'blocktime'; - -export const timeConditionSchema = z.object({ - ...restShape, - conditionType: z.literal(TimeConditionType).default(TimeConditionType), - method: z.literal(TimeConditionMethod).default(TimeConditionMethod), -}); - -export type TimeConditionProps = z.infer; +export { + TimeConditionMethod, + TimeConditionProps, + timeConditionSchema, + TimeConditionType, +} from '../schemas/time'; export class TimeCondition extends Condition { constructor(value: OmitConditionType) { diff --git a/packages/taco/src/conditions/compound-condition.ts b/packages/taco/src/conditions/compound-condition.ts index 861353414..d7de784e0 100644 --- a/packages/taco/src/conditions/compound-condition.ts +++ b/packages/taco/src/conditions/compound-condition.ts @@ -1,65 +1,16 @@ -import { z } from 'zod'; - -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 { Condition, ConditionProps } from './condition'; +import { + CompoundConditionProps, + compoundConditionSchema, + CompoundConditionType, +} from './schemas/compound'; import { OmitConditionType } from './shared'; -export const CompoundConditionType = 'compound'; - -export const compoundConditionSchema: z.ZodSchema = baseConditionSchema - .extend({ - conditionType: z - .literal(CompoundConditionType) - .default(CompoundConditionType), - operator: z.enum(['and', 'or', 'not']), - operands: z - .array( - z.lazy(() => - z.union([ - rpcConditionSchema, - timeConditionSchema, - contractConditionSchema, - compoundConditionSchema, - sequentialConditionSchema, - ]), - ), - ) - .min(1) - .max(5), - }) - .refine( - (condition) => { - // 'and' and 'or' operators must have at least 2 operands - if (['and', 'or'].includes(condition.operator)) { - return condition.operands.length >= 2; - } - - // 'not' operator must have exactly 1 operand - if (condition.operator === 'not') { - return condition.operands.length === 1; - } - - // We test positive cases exhaustively, so we return false here: - return false; - }, - ({ operands, operator }) => ({ - message: `Invalid number of operands ${operands.length} for operator "${operator}"`, - path: ['operands'], - }), - ) - .refine( - (condition) => maxNestedDepth(2)(condition), - { - message: 'Exceeded max nested depth of 2 for multi-condition type', - path: ['operands'], - }, // Max nested depth of 2 - ); - -export type CompoundConditionProps = z.infer; +export { + CompoundConditionProps, + compoundConditionSchema, + CompoundConditionType, +} from './schemas/compound'; export type ConditionOrProps = Condition | ConditionProps; diff --git a/packages/taco/src/conditions/condition.ts b/packages/taco/src/conditions/condition.ts index 29ab267b4..f0c0ae930 100644 --- a/packages/taco/src/conditions/condition.ts +++ b/packages/taco/src/conditions/condition.ts @@ -3,9 +3,7 @@ import { z } from 'zod'; import { USER_ADDRESS_PARAMS } from './const'; -export const baseConditionSchema = z.object({ - conditionType: z.string(), -}); +export { baseConditionSchema } from './schemas/common'; type ConditionSchema = z.ZodSchema; export type ConditionProps = z.infer; diff --git a/packages/taco/src/conditions/schemas/index.ts b/packages/taco/src/conditions/schemas/index.ts deleted file mode 100644 index 7e862693f..000000000 --- a/packages/taco/src/conditions/schemas/index.ts +++ /dev/null @@ -1,8 +0,0 @@ -export * as common from './common'; -export * as compound from './compound'; -export * as context from './context'; -export * as contract from './contract'; -export * as returnValueTest from './return-value-test'; -export * as rpc from './rpc'; -export * as sequential from './sequential'; -export * as time from './time'; diff --git a/packages/taco/src/conditions/sequential.ts b/packages/taco/src/conditions/sequential.ts index 2f996eb2c..65c111112 100644 --- a/packages/taco/src/conditions/sequential.ts +++ b/packages/taco/src/conditions/sequential.ts @@ -1,67 +1,17 @@ -import { z } from 'zod'; +import { Condition } from './condition'; +import { + SequentialConditionProps, + sequentialConditionSchema, + SequentialConditionType, +} from './schemas/sequential'; +import { OmitConditionType } from './shared'; -import { contractConditionSchema } from './base/contract'; -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'; - -export const conditionVariableSchema: z.ZodSchema = z.object({ - varName: plainStringSchema, - condition: z.lazy(() => - z.union([ - rpcConditionSchema, - timeConditionSchema, - contractConditionSchema, - compoundConditionSchema, - sequentialConditionSchema, - ]), - ), -}); - -export const sequentialConditionSchema: z.ZodSchema = baseConditionSchema - .extend({ - conditionType: z - .literal(SequentialConditionType) - .default(SequentialConditionType), - conditionVariables: z.array(conditionVariableSchema).min(2).max(5), - }) - .refine( - (condition) => maxNestedDepth(2)(condition), - { - message: 'Exceeded max nested depth of 2 for multi-condition type', - path: ['conditionVariables'], - }, // Max nested depth of 2 - ) - .refine( - // check for duplicate var names - (condition) => { - const seen = new Set(); - return condition.conditionVariables.every( - (child: ConditionVariableProps) => { - if (seen.has(child.varName)) { - return false; - } - seen.add(child.varName); - return true; - }, - ); - }, - { - message: 'Duplicate variable names are not allowed', - path: ['conditionVariables'], - }, - ); - -export type ConditionVariableProps = z.infer; - -export type SequentialConditionProps = z.infer< - typeof sequentialConditionSchema ->; +export { + ConditionVariableProps, + SequentialConditionProps, + sequentialConditionSchema, + SequentialConditionType, +} from './schemas/sequential'; export class SequentialCondition extends Condition { constructor(value: OmitConditionType) { diff --git a/packages/taco/src/conditions/shared.ts b/packages/taco/src/conditions/shared.ts index 1e63b0560..15af59eec 100644 --- a/packages/taco/src/conditions/shared.ts +++ b/packages/taco/src/conditions/shared.ts @@ -1,47 +1,11 @@ -import { EthAddressSchema } from '@nucypher/shared'; -import { - USER_ADDRESS_PARAM_DEFAULT, - USER_ADDRESS_PARAM_EXTERNAL_EIP4361, -} from '@nucypher/taco-auth'; -import { z } from 'zod'; - -import { CONTEXT_PARAM_PREFIX, CONTEXT_PARAM_REGEXP } from './const'; - -export const contextParamSchema = z.string().regex(CONTEXT_PARAM_REGEXP); -// We want to discriminate between ContextParams and plain strings -// If a string starts with `:`, it's a ContextParam -export const plainStringSchema = z.string().refine( - (str) => { - return !str.startsWith(CONTEXT_PARAM_PREFIX); - }, - { - message: `String must not be a context parameter i.e. not start with "${CONTEXT_PARAM_PREFIX}"`, - }, -); - -const paramSchema = z.union([plainStringSchema, z.boolean(), z.number()]); +export type OmitConditionType = Omit; -export const paramOrContextParamSchema: z.ZodSchema = z.union([ - paramSchema, +export { contextParamSchema, - z.lazy(() => z.array(paramOrContextParamSchema)), -]); + paramOrContextParamSchema, +} from './schemas/context'; -export const returnValueTestSchema = z.object({ - index: z.number().int().nonnegative().optional(), - comparator: z.enum(['==', '>', '<', '>=', '<=', '!=']), - value: paramOrContextParamSchema, -}); - -export type ReturnValueTestProps = z.infer; - -const UserAddressSchema = z.enum([ - USER_ADDRESS_PARAM_DEFAULT, - USER_ADDRESS_PARAM_EXTERNAL_EIP4361, -]); -export const EthAddressOrUserAddressSchema = z.union([ - EthAddressSchema, - UserAddressSchema, -]); - -export type OmitConditionType = Omit; +export { + ReturnValueTestProps, + returnValueTestSchema, +} from './schemas/return-value-test'; diff --git a/packages/taco/src/conditions/zod.ts b/packages/taco/src/conditions/zod.ts deleted file mode 100644 index 75aa1b4de..000000000 --- a/packages/taco/src/conditions/zod.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { Primitive, z, ZodLiteral } from 'zod'; - -// Source: https://github.com/colinhacks/zod/issues/831#issuecomment-1063481764 -const createUnion = < - T extends Readonly<[Primitive, Primitive, ...Primitive[]]>, ->( - values: T, -) => { - const zodLiterals = values.map((value) => z.literal(value)) as unknown as [ - ZodLiteral, - ZodLiteral, - ...ZodLiteral[], - ]; - return z.union(zodLiterals); -}; - -function createUnionSchema(values: T) { - if (values.length === 0) { - return z.never(); - } - - if (values.length === 1) { - return z.literal(values[0]); - } - - return createUnion( - values as unknown as Readonly<[Primitive, Primitive, ...Primitive[]]>, - ); -} - -export default createUnionSchema; From 971199f4f5452b32adb3fec309ea147a5512fbd1 Mon Sep 17 00:00:00 2001 From: derekpierre Date: Thu, 3 Oct 2024 18:10:51 -0400 Subject: [PATCH 30/75] Make schemas for compound condition and sequential condition be lazy in the correct way. --- .../taco/src/conditions/schemas/compound.ts | 68 ++++++++++--------- .../taco/src/conditions/schemas/sequential.ts | 10 +-- 2 files changed, 41 insertions(+), 37 deletions(-) diff --git a/packages/taco/src/conditions/schemas/compound.ts b/packages/taco/src/conditions/schemas/compound.ts index d3b6a297d..df8dd40ce 100644 --- a/packages/taco/src/conditions/schemas/compound.ts +++ b/packages/taco/src/conditions/schemas/compound.ts @@ -7,40 +7,42 @@ import { anyConditionSchema } from './utils'; export const CompoundConditionType = 'compound'; -export const compoundConditionSchema: z.ZodSchema = baseConditionSchema - .extend({ - conditionType: z - .literal(CompoundConditionType) - .default(CompoundConditionType), - operator: z.enum(['and', 'or', 'not']), - operands: z.array(anyConditionSchema).min(1).max(5), - }) - .refine( - (condition) => { - // 'and' and 'or' operators must have at least 2 operands - if (['and', 'or'].includes(condition.operator)) { - return condition.operands.length >= 2; - } +export const compoundConditionSchema: z.ZodSchema = z.lazy(() => + baseConditionSchema + .extend({ + conditionType: z + .literal(CompoundConditionType) + .default(CompoundConditionType), + operator: z.enum(['and', 'or', 'not']), + operands: z.array(anyConditionSchema).min(1).max(5), + }) + .refine( + (condition) => { + // 'and' and 'or' operators must have at least 2 operands + if (['and', 'or'].includes(condition.operator)) { + return condition.operands.length >= 2; + } - // 'not' operator must have exactly 1 operand - if (condition.operator === 'not') { - return condition.operands.length === 1; - } + // 'not' operator must have exactly 1 operand + if (condition.operator === 'not') { + return condition.operands.length === 1; + } - // We test positive cases exhaustively, so we return false here: - return false; - }, - ({ operands, operator }) => ({ - message: `Invalid number of operands ${operands.length} for operator "${operator}"`, - path: ['operands'], - }), - ) - .refine( - (condition) => maxNestedDepth(2)(condition), - { - message: 'Exceeded max nested depth of 2 for multi-condition type', - path: ['operands'], - }, // Max nested depth of 2 - ); + // We test positive cases exhaustively, so we return false here: + return false; + }, + ({ operands, operator }) => ({ + message: `Invalid number of operands ${operands.length} for operator "${operator}"`, + path: ['operands'], + }), + ) + .refine( + (condition) => maxNestedDepth(2)(condition), + { + message: 'Exceeded max nested depth of 2 for multi-condition type', + path: ['operands'], + }, // Max nested depth of 2 + ), +); export type CompoundConditionProps = z.infer; diff --git a/packages/taco/src/conditions/schemas/sequential.ts b/packages/taco/src/conditions/schemas/sequential.ts index 5b11de9b0..60942d047 100644 --- a/packages/taco/src/conditions/schemas/sequential.ts +++ b/packages/taco/src/conditions/schemas/sequential.ts @@ -7,10 +7,12 @@ import { anyConditionSchema } from './utils'; export const SequentialConditionType = 'sequential'; -export const conditionVariableSchema: z.ZodSchema = z.object({ - varName: plainStringSchema, - condition: anyConditionSchema, -}); +export const conditionVariableSchema: z.ZodSchema = z.lazy(() => + z.object({ + varName: plainStringSchema, + condition: anyConditionSchema, + }), +); export type ConditionVariableProps = z.infer; export const sequentialConditionSchema: z.ZodSchema = baseConditionSchema From 66a8340c4b2584205d3022d20988afa695e272dc Mon Sep 17 00:00:00 2001 From: derekpierre Date: Fri, 4 Oct 2024 16:58:35 -0400 Subject: [PATCH 31/75] Ensure that json-api module is exported in base folder. --- packages/taco/src/conditions/base/index.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/taco/src/conditions/base/index.ts b/packages/taco/src/conditions/base/index.ts index 0364f1c1d..4214fa7d2 100644 --- a/packages/taco/src/conditions/base/index.ts +++ b/packages/taco/src/conditions/base/index.ts @@ -2,5 +2,6 @@ // avoid circular dependency on Condition class. export * as contract from './contract'; +export * as jsonApi from './json-api'; export * as rpc from './rpc'; export * as time from './time'; From 3209ae34ba59b1d159d01f450a9a619843bfc598 Mon Sep 17 00:00:00 2001 From: derekpierre Date: Fri, 4 Oct 2024 17:00:06 -0400 Subject: [PATCH 32/75] Split JsonAPICondition into its schema and its condition. Include jsonApiConditionSchema in anyConditionSchema. --- packages/taco/src/conditions/base/json-api.ts | 44 ++++++------------- .../taco/src/conditions/schemas/json-api.ts | 31 +++++++++++++ packages/taco/src/conditions/schemas/utils.ts | 2 + .../{json-api.test.ts => json-path.test.ts} | 0 .../taco/test/conditions/base/json.test.ts | 10 ++--- 5 files changed, 51 insertions(+), 36 deletions(-) create mode 100644 packages/taco/src/conditions/schemas/json-api.ts rename packages/taco/test/conditions/base/{json-api.test.ts => json-path.test.ts} (100%) diff --git a/packages/taco/src/conditions/base/json-api.ts b/packages/taco/src/conditions/base/json-api.ts index 22032cb34..0203284c2 100644 --- a/packages/taco/src/conditions/base/json-api.ts +++ b/packages/taco/src/conditions/base/json-api.ts @@ -1,39 +1,21 @@ -import { JSONPath } from '@astronautlabs/jsonpath'; -import { z } from 'zod'; - import { Condition } from '../condition'; -import { OmitConditionType, returnValueTestSchema } from '../shared'; - -export const JsonApiConditionType = 'json-api'; - -const validateJSONPath = (jsonPath: string): boolean => { - try { - JSONPath.parse(jsonPath); - return true; - } catch (error) { - return false; - } -}; - -export const jsonPathSchema = z - .string() - .refine((val) => validateJSONPath(val), { - message: 'Invalid JSONPath expression', - }); - -export const JsonApiConditionSchema = z.object({ - conditionType: z.literal(JsonApiConditionType).default(JsonApiConditionType), - endpoint: z.string().url(), - parameters: z.record(z.string(), z.unknown()).optional(), - query: jsonPathSchema.optional(), - returnValueTest: returnValueTestSchema, // Update to allow multiple return values after expanding supported methods -}); +import { + JsonApiConditionProps, + jsonApiConditionSchema, + JsonApiConditionType, +} from '../schemas/json-api'; +import { OmitConditionType } from '../shared'; -export type JsonApiConditionProps = z.infer; +export { + JsonApiConditionProps, + jsonApiConditionSchema, + JsonApiConditionType, + jsonPathSchema, +} from '../schemas/json-api'; export class JsonApiCondition extends Condition { constructor(value: OmitConditionType) { - super(JsonApiConditionSchema, { + super(jsonApiConditionSchema, { conditionType: JsonApiConditionType, ...value, }); diff --git a/packages/taco/src/conditions/schemas/json-api.ts b/packages/taco/src/conditions/schemas/json-api.ts new file mode 100644 index 000000000..4a9afb822 --- /dev/null +++ b/packages/taco/src/conditions/schemas/json-api.ts @@ -0,0 +1,31 @@ +import { JSONPath } from '@astronautlabs/jsonpath'; +import { z } from 'zod'; + +import { returnValueTestSchema } from './return-value-test'; + +export const JsonApiConditionType = 'json-api'; + +const validateJSONPath = (jsonPath: string): boolean => { + try { + JSONPath.parse(jsonPath); + return true; + } catch (error) { + return false; + } +}; + +export const jsonPathSchema = z + .string() + .refine((val) => validateJSONPath(val), { + message: 'Invalid JSONPath expression', + }); + +export const jsonApiConditionSchema = z.object({ + conditionType: z.literal(JsonApiConditionType).default(JsonApiConditionType), + endpoint: z.string().url(), + parameters: z.record(z.string(), z.unknown()).optional(), + query: jsonPathSchema.optional(), + returnValueTest: returnValueTestSchema, // Update to allow multiple return values after expanding supported methods +}); + +export type JsonApiConditionProps = z.infer; diff --git a/packages/taco/src/conditions/schemas/utils.ts b/packages/taco/src/conditions/schemas/utils.ts index 6f7a15682..c84cc8d38 100644 --- a/packages/taco/src/conditions/schemas/utils.ts +++ b/packages/taco/src/conditions/schemas/utils.ts @@ -3,6 +3,7 @@ import { z } from 'zod'; import { compoundConditionSchema } from '../compound-condition'; import { contractConditionSchema } from './contract'; +import { jsonApiConditionSchema } from './json-api'; import { rpcConditionSchema } from './rpc'; import { sequentialConditionSchema } from './sequential'; import { timeConditionSchema } from './time'; @@ -13,6 +14,7 @@ export const anyConditionSchema: z.ZodSchema = z.lazy(() => timeConditionSchema, contractConditionSchema, compoundConditionSchema, + jsonApiConditionSchema, sequentialConditionSchema, ]), ); diff --git a/packages/taco/test/conditions/base/json-api.test.ts b/packages/taco/test/conditions/base/json-path.test.ts similarity index 100% rename from packages/taco/test/conditions/base/json-api.test.ts rename to packages/taco/test/conditions/base/json-path.test.ts diff --git a/packages/taco/test/conditions/base/json.test.ts b/packages/taco/test/conditions/base/json.test.ts index 0166485a2..2f9291d4d 100644 --- a/packages/taco/test/conditions/base/json.test.ts +++ b/packages/taco/test/conditions/base/json.test.ts @@ -3,7 +3,7 @@ import { describe, expect, it } from 'vitest'; import { JsonApiCondition, - JsonApiConditionSchema, + jsonApiConditionSchema, } from '../../../src/conditions/base/json-api'; import { testJsonApiConditionObj } from '../../test-utils'; @@ -11,7 +11,7 @@ describe('JsonApiCondition', () => { describe('validation', () => { it('accepts a valid schema', () => { const result = JsonApiCondition.validate( - JsonApiConditionSchema, + jsonApiConditionSchema, testJsonApiConditionObj, ); @@ -26,7 +26,7 @@ describe('JsonApiCondition', () => { }; const result = JsonApiCondition.validate( - JsonApiConditionSchema, + jsonApiConditionSchema, badJsonApiObj, ); @@ -43,7 +43,7 @@ describe('JsonApiCondition', () => { it('accepts conditions without query path', () => { const { query, ...noQueryObj } = testJsonApiConditionObj; const result = JsonApiCondition.validate( - JsonApiConditionSchema, + jsonApiConditionSchema, noQueryObj, ); @@ -54,7 +54,7 @@ describe('JsonApiCondition', () => { it('accepts conditions without parameters', () => { const { query, ...noParamsObj } = testJsonApiConditionObj; const result = JsonApiCondition.validate( - JsonApiConditionSchema, + jsonApiConditionSchema, noParamsObj, ); From e06ab283e826d8af91ba9649bd0e80e98a25d10d Mon Sep 17 00:00:00 2001 From: derekpierre Date: Thu, 3 Oct 2024 18:13:39 -0400 Subject: [PATCH 33/75] Add support for if-then-else condition. --- .../src/conditions/if-then-else-condition.ts | 22 ++++++++++++++ packages/taco/src/conditions/index.ts | 1 + .../taco/src/conditions/multi-condition.ts | 19 ++++++++++-- .../src/conditions/schemas/if-then-else.ts | 30 +++++++++++++++++++ packages/taco/src/conditions/schemas/utils.ts | 2 ++ 5 files changed, 71 insertions(+), 3 deletions(-) create mode 100644 packages/taco/src/conditions/if-then-else-condition.ts create mode 100644 packages/taco/src/conditions/schemas/if-then-else.ts diff --git a/packages/taco/src/conditions/if-then-else-condition.ts b/packages/taco/src/conditions/if-then-else-condition.ts new file mode 100644 index 000000000..79d99b49c --- /dev/null +++ b/packages/taco/src/conditions/if-then-else-condition.ts @@ -0,0 +1,22 @@ +import { Condition } from './condition'; +import { + IfThenElseConditionProps, + ifThenElseConditionSchema, + IfThenElseConditionType, +} from './schemas/if-then-else'; +import { OmitConditionType } from './shared'; + +export { + IfThenElseConditionProps, + ifThenElseConditionSchema, + IfThenElseConditionType, +} from './schemas/if-then-else'; + +export class IfThenElseCondition extends Condition { + constructor(value: OmitConditionType) { + super(ifThenElseConditionSchema, { + conditionType: IfThenElseConditionType, + ...value, + }); + } +} diff --git a/packages/taco/src/conditions/index.ts b/packages/taco/src/conditions/index.ts index 3ec09a76a..0c8c3d5c8 100644 --- a/packages/taco/src/conditions/index.ts +++ b/packages/taco/src/conditions/index.ts @@ -6,5 +6,6 @@ export * as condition from './condition'; export * as conditionExpr from './condition-expr'; export { ConditionFactory } from './condition-factory'; export * as context from './context'; +export * as ifThenElse from './if-then-else-condition'; export * as sequential from './sequential'; export { base, predefined }; diff --git a/packages/taco/src/conditions/multi-condition.ts b/packages/taco/src/conditions/multi-condition.ts index 542b09335..434ab812a 100644 --- a/packages/taco/src/conditions/multi-condition.ts +++ b/packages/taco/src/conditions/multi-condition.ts @@ -1,13 +1,15 @@ import { CompoundConditionType } from './compound-condition'; import { ConditionProps } from './condition'; +import { IfThenElseConditionType } from './if-then-else-condition'; import { ConditionVariableProps, SequentialConditionType } from './sequential'; export const maxNestedDepth = (maxDepth: number) => - (condition: ConditionProps, currentDepth = 1) => { + (condition: ConditionProps, currentDepth = 1): boolean => { if ( condition.conditionType === CompoundConditionType || - condition.conditionType === SequentialConditionType + condition.conditionType === SequentialConditionType || + condition.conditionType === IfThenElseConditionType ) { if (currentDepth > maxDepth) { // no more multi-condition types allowed at this level @@ -18,11 +20,22 @@ export const maxNestedDepth = return condition.operands.every((child: ConditionProps) => maxNestedDepth(maxDepth)(child, currentDepth + 1), ); - } else { + } else if (condition.conditionType === SequentialConditionType) { return condition.conditionVariables.every( (child: ConditionVariableProps) => maxNestedDepth(maxDepth)(child.condition, currentDepth + 1), ); + } else { + // if-then-else condition + const ifThenElseConditions = []; + ifThenElseConditions.push(condition.ifCondition); + ifThenElseConditions.push(condition.thenCondition); + if (typeof condition.elseCondition !== 'boolean') { + ifThenElseConditions.push(condition.elseCondition); + } + return ifThenElseConditions.every((child: ConditionProps) => + maxNestedDepth(maxDepth)(child, currentDepth + 1), + ); } } diff --git a/packages/taco/src/conditions/schemas/if-then-else.ts b/packages/taco/src/conditions/schemas/if-then-else.ts new file mode 100644 index 000000000..cbe35fc5c --- /dev/null +++ b/packages/taco/src/conditions/schemas/if-then-else.ts @@ -0,0 +1,30 @@ +import { z } from 'zod'; + +import { maxNestedDepth } from '../multi-condition'; + +import { baseConditionSchema } from './common'; +import { anyConditionSchema } from './utils'; + +export const IfThenElseConditionType = 'if-then-else'; + +export const ifThenElseConditionSchema: z.ZodSchema = z.lazy(() => + baseConditionSchema + .extend({ + conditionType: z + .literal(IfThenElseConditionType) + .default(IfThenElseConditionType), + ifCondition: anyConditionSchema, + thenCondition: anyConditionSchema, + elseCondition: z.union([anyConditionSchema, z.boolean()]), + }) + .refine( + (condition) => maxNestedDepth(2)(condition), + { + message: 'Exceeded max nested depth of 2 for multi-condition type', + }, // Max nested depth of 2 + ), +); + +export type IfThenElseConditionProps = z.infer< + typeof ifThenElseConditionSchema +>; diff --git a/packages/taco/src/conditions/schemas/utils.ts b/packages/taco/src/conditions/schemas/utils.ts index c84cc8d38..4f5e919e5 100644 --- a/packages/taco/src/conditions/schemas/utils.ts +++ b/packages/taco/src/conditions/schemas/utils.ts @@ -3,6 +3,7 @@ import { z } from 'zod'; import { compoundConditionSchema } from '../compound-condition'; import { contractConditionSchema } from './contract'; +import { ifThenElseConditionSchema } from './if-then-else'; import { jsonApiConditionSchema } from './json-api'; import { rpcConditionSchema } from './rpc'; import { sequentialConditionSchema } from './sequential'; @@ -16,5 +17,6 @@ export const anyConditionSchema: z.ZodSchema = z.lazy(() => compoundConditionSchema, jsonApiConditionSchema, sequentialConditionSchema, + ifThenElseConditionSchema, ]), ); From 8eaa5a70dc30bff3a5e10f5672262f651f4aa103 Mon Sep 17 00:00:00 2001 From: derekpierre Date: Tue, 8 Oct 2024 12:04:29 -0400 Subject: [PATCH 34/75] Update refinement of if-then-else condition to report on specific condition exceeding max depth. --- .../src/conditions/schemas/if-then-else.ts | 25 ++++++++++++++++++- 1 file changed, 24 insertions(+), 1 deletion(-) diff --git a/packages/taco/src/conditions/schemas/if-then-else.ts b/packages/taco/src/conditions/schemas/if-then-else.ts index cbe35fc5c..dc90ee992 100644 --- a/packages/taco/src/conditions/schemas/if-then-else.ts +++ b/packages/taco/src/conditions/schemas/if-then-else.ts @@ -18,9 +18,32 @@ export const ifThenElseConditionSchema: z.ZodSchema = z.lazy(() => elseCondition: z.union([anyConditionSchema, z.boolean()]), }) .refine( - (condition) => maxNestedDepth(2)(condition), + // already at 2nd level since checking member condition + (condition) => maxNestedDepth(2)(condition.ifCondition, 2), { message: 'Exceeded max nested depth of 2 for multi-condition type', + path: ['ifCondition'], + }, // Max nested depth of 2 + ) + .refine( + // already at 2nd level since checking member condition + (condition) => maxNestedDepth(2)(condition.thenCondition, 2), + { + message: 'Exceeded max nested depth of 2 for multi-condition type', + path: ['thenCondition'], + }, // Max nested depth of 2 + ) + .refine( + (condition) => { + if (typeof condition.elseCondition !== 'boolean') { + // already at 2nd level since checking member condition + return maxNestedDepth(2)(condition.elseCondition, 2); + } + return true; + }, + { + message: 'Exceeded max nested depth of 2 for multi-condition type', + path: ['elseCondition'], }, // Max nested depth of 2 ), ); From e7da9ecd659baf00803d3275ef3d8783a1d601f6 Mon Sep 17 00:00:00 2001 From: derekpierre Date: Tue, 8 Oct 2024 12:06:02 -0400 Subject: [PATCH 35/75] Add tests for if-then-else condition. --- .../conditions/if-then-else-condition.test.ts | 176 ++++++++++++++++++ 1 file changed, 176 insertions(+) create mode 100644 packages/taco/test/conditions/if-then-else-condition.test.ts diff --git a/packages/taco/test/conditions/if-then-else-condition.test.ts b/packages/taco/test/conditions/if-then-else-condition.test.ts new file mode 100644 index 000000000..50897ec9f --- /dev/null +++ b/packages/taco/test/conditions/if-then-else-condition.test.ts @@ -0,0 +1,176 @@ +import { describe, expect, it } from 'vitest'; + +import { + IfThenElseCondition, + ifThenElseConditionSchema, + IfThenElseConditionType, +} from '../../src/conditions/if-then-else-condition'; +import { TimeConditionType } from '../../src/conditions/schemas/time'; +import { + testCompoundConditionObj, + testContractConditionObj, + testRpcConditionObj, + testSequentialConditionObj, + testTimeConditionObj, +} from '../test-utils'; + +describe('validation', () => { + it('infers default condition type from constructor', () => { + const condition = new IfThenElseCondition({ + ifCondition: testRpcConditionObj, + thenCondition: testTimeConditionObj, + elseCondition: testContractConditionObj, + }); + expect(condition.value.conditionType).toEqual(IfThenElseConditionType); + }); + + it('validates type', () => { + const result = IfThenElseCondition.validate(ifThenElseConditionSchema, { + conditionType: TimeConditionType, + ifCondition: testRpcConditionObj, + thenCondition: testTimeConditionObj, + elseCondition: testContractConditionObj, + }); + expect(result.error).toBeDefined(); + expect(result.data).toBeUndefined(); + expect(result.error?.format()).toMatchObject({ + conditionType: { + _errors: [ + `Invalid literal value, expected "${IfThenElseConditionType}"`, + ], + }, + }); + }); + + it('accepts recursive if-then-else conditions', () => { + const nestedIfThenElseConditionObj = { + conditionType: IfThenElseConditionType, + ifCondition: testRpcConditionObj, + thenCondition: testTimeConditionObj, + elseCondition: testContractConditionObj, + }; + + const conditionObj = { + conditionType: IfThenElseConditionType, + ifCondition: testTimeConditionObj, + thenCondition: nestedIfThenElseConditionObj, + elseCondition: nestedIfThenElseConditionObj, + }; + + const result = IfThenElseCondition.validate( + ifThenElseConditionSchema, + conditionObj, + ); + expect(result.error).toBeUndefined(); + expect(result.data).toEqual({ + conditionType: IfThenElseConditionType, + ifCondition: testTimeConditionObj, + thenCondition: nestedIfThenElseConditionObj, + elseCondition: nestedIfThenElseConditionObj, + }); + }); + + it('accepts nested sequential and compound conditions', () => { + const conditionObj = { + conditionType: IfThenElseConditionType, + ifCondition: testRpcConditionObj, + thenCondition: testSequentialConditionObj, + elseCondition: testCompoundConditionObj, + }; + const result = IfThenElseCondition.validate( + ifThenElseConditionSchema, + conditionObj, + ); + expect(result.error).toBeUndefined(); + expect(result.data).toEqual({ + conditionType: IfThenElseConditionType, + ifCondition: testRpcConditionObj, + thenCondition: testSequentialConditionObj, + elseCondition: testCompoundConditionObj, + }); + }); + + it.each([true, false])( + 'accepts boolean for else condition', + (booleanValue) => { + const conditionObj = { + conditionType: IfThenElseConditionType, + ifCondition: testTimeConditionObj, + thenCondition: testRpcConditionObj, + elseCondition: booleanValue, + }; + + const result = IfThenElseCondition.validate( + ifThenElseConditionSchema, + conditionObj, + ); + expect(result.error).toBeUndefined(); + expect(result.data).toEqual({ + conditionType: IfThenElseConditionType, + ifCondition: testTimeConditionObj, + thenCondition: testRpcConditionObj, + elseCondition: booleanValue, + }); + }, + ); + + it('limits max depth of nested if condition', () => { + const result = IfThenElseCondition.validate(ifThenElseConditionSchema, { + ifCondition: { + conditionType: IfThenElseConditionType, + ifCondition: testRpcConditionObj, + thenCondition: testCompoundConditionObj, + elseCondition: testTimeConditionObj, + }, + thenCondition: testRpcConditionObj, + elseCondition: testTimeConditionObj, + }); + expect(result.error).toBeDefined(); + expect(result.data).toBeUndefined(); + expect(result.error?.format()).toMatchObject({ + ifCondition: { + _errors: [`Exceeded max nested depth of 2 for multi-condition type`], + }, + }); + }); + + it('limits max depth of nested then condition', () => { + const result = IfThenElseCondition.validate(ifThenElseConditionSchema, { + ifCondition: testRpcConditionObj, + thenCondition: { + conditionType: IfThenElseConditionType, + ifCondition: testRpcConditionObj, + thenCondition: testCompoundConditionObj, + elseCondition: true, + }, + elseCondition: testTimeConditionObj, + }); + expect(result.error).toBeDefined(); + expect(result.data).toBeUndefined(); + expect(result.error?.format()).toMatchObject({ + thenCondition: { + _errors: [`Exceeded max nested depth of 2 for multi-condition type`], + }, + }); + }); + + it('limits max depth of nested else condition', () => { + const result = IfThenElseCondition.validate(ifThenElseConditionSchema, { + ifCondition: testRpcConditionObj, + thenCondition: testTimeConditionObj, + elseCondition: { + conditionType: IfThenElseConditionType, + ifCondition: testRpcConditionObj, + thenCondition: testSequentialConditionObj, + elseCondition: testTimeConditionObj, + }, + }); + expect(result.error).toBeDefined(); + expect(result.data).toBeUndefined(); + expect(result.error?.format()).toMatchObject({ + elseCondition: { + _errors: [`Exceeded max nested depth of 2 for multi-condition type`], + }, + }); + }); +}); From 84ab89e58297bb41c6400905226abd93ee53626d Mon Sep 17 00:00:00 2001 From: derekpierre Date: Tue, 15 Oct 2024 15:20:10 -0400 Subject: [PATCH 36/75] Ensure that all condition types are accounted for in ConditionFactory. --- .../taco/src/conditions/condition-factory.ts | 20 +++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/packages/taco/src/conditions/condition-factory.ts b/packages/taco/src/conditions/condition-factory.ts index 273cbe742..806dc9816 100644 --- a/packages/taco/src/conditions/condition-factory.ts +++ b/packages/taco/src/conditions/condition-factory.ts @@ -20,6 +20,16 @@ import { CompoundConditionType, } from './compound-condition'; import { Condition, ConditionProps } from './condition'; +import { + IfThenElseCondition, + IfThenElseConditionProps, + IfThenElseConditionType, +} from './if-then-else-condition'; +import { + SequentialCondition, + SequentialConditionProps, + SequentialConditionType, +} from './sequential'; const ERR_INVALID_CONDITION_TYPE = (type: string) => `Invalid condition type: ${type}`; @@ -27,16 +37,22 @@ const ERR_INVALID_CONDITION_TYPE = (type: string) => export class ConditionFactory { public static conditionFromProps(props: ConditionProps): Condition { switch (props.conditionType) { + // Base Conditions case RpcConditionType: return new RpcCondition(props as RpcConditionProps); case TimeConditionType: return new TimeCondition(props as TimeConditionProps); case ContractConditionType: return new ContractCondition(props as ContractConditionProps); - case CompoundConditionType: - return new CompoundCondition(props as CompoundConditionProps); case JsonApiConditionType: return new JsonApiCondition(props as JsonApiConditionProps); + // Logical Conditions + case CompoundConditionType: + return new CompoundCondition(props as CompoundConditionProps); + case SequentialConditionType: + return new SequentialCondition(props as SequentialConditionProps); + case IfThenElseConditionType: + return new IfThenElseCondition(props as IfThenElseConditionProps); default: throw new Error(ERR_INVALID_CONDITION_TYPE(props.conditionType)); } From bf85a9cb63a88751e7ae4acf61a1022c87d6989f Mon Sep 17 00:00:00 2001 From: derekpierre Date: Tue, 15 Oct 2024 15:21:18 -0400 Subject: [PATCH 37/75] Add lingo test to ensure that dictionaries used in `nucypher/nucypher` (nodes) are also valid in `taco-web` (client library). --- packages/taco/test/conditions/lingo.test.ts | 122 ++++++++++++++++++++ 1 file changed, 122 insertions(+) create mode 100644 packages/taco/test/conditions/lingo.test.ts diff --git a/packages/taco/test/conditions/lingo.test.ts b/packages/taco/test/conditions/lingo.test.ts new file mode 100644 index 000000000..bf3c37713 --- /dev/null +++ b/packages/taco/test/conditions/lingo.test.ts @@ -0,0 +1,122 @@ +import { TEST_CHAIN_ID } from '@nucypher/test-utils'; +import { describe, expect, it } from 'vitest'; + +import { ConditionExpression } from '../../src/conditions/condition-expr'; + +describe('check that valid lingo in python is valid in typescript', () => { + const timeConditionProps = { + conditionType: 'time', + method: 'blocktime', + chain: TEST_CHAIN_ID, + returnValueTest: { value: 0, comparator: '>' }, + }; + + const contractConditionProps = { + conditionType: 'contract', + chain: TEST_CHAIN_ID, + method: 'isPolicyActive', + parameters: [':hrac'], + contractAddress: '0xA1bd3630a13D54EDF7320412B5C9F289230D260d', + functionAbi: { + type: 'function', + name: 'isPolicyActive', + stateMutability: 'view', + inputs: [ + { + name: '_policyID', + type: 'bytes16', + internalType: 'bytes16', + }, + ], + outputs: [{ name: '', type: 'bool', internalType: 'bool' }], + }, + returnValueTest: { + comparator: '==', + value: true, + }, + }; + const rpcConditionProps = { + conditionType: 'rpc', + chain: TEST_CHAIN_ID, + method: 'eth_getBalance', + parameters: ['0x3d2Bed3259b165EB02A7F0D0753e7a01912A68f8', 'latest'], + returnValueTest: { + comparator: '>=', + value: 10000000000000, + }, + }; + const jsonApiConditionProps = { + conditionType: 'json-api', + endpoint: 'https://api.example.com/data', + query: '$.store.book[0].price', + parameters: { + ids: 'ethereum', + vs_currencies: 'usd', + }, + returnValueTest: { + comparator: '==', + value: 2, + }, + }; + const sequentialConditionProps = { + conditionType: 'sequential', + conditionVariables: [ + { + varName: 'timeValue', + condition: timeConditionProps, + }, + { + varName: 'rpcValue', + condition: rpcConditionProps, + }, + { + varName: 'contractValue', + condition: contractConditionProps, + }, + { + varName: 'jsonValue', + condition: jsonApiConditionProps, + }, + ], + }; + const ifThenElseConditionProps = { + conditionType: 'if-then-else', + ifCondition: rpcConditionProps, + thenCondition: jsonApiConditionProps, + elseCondition: timeConditionProps, + }; + + const compoundConditionProps = { + conditionType: 'compound', + operator: 'and', + operands: [ + contractConditionProps, + ifThenElseConditionProps, + sequentialConditionProps, + rpcConditionProps, + { + conditionType: 'compound', + operator: 'not', + operands: [timeConditionProps], + }, + ], + }; + + it.each([ + rpcConditionProps, + timeConditionProps, + contractConditionProps, + jsonApiConditionProps, + compoundConditionProps, + sequentialConditionProps, + ifThenElseConditionProps, + ])('parsing of all condition types', (conditionProps) => { + const conditionExprJSON = { + version: ConditionExpression.version, + condition: conditionProps, + }; + const conditionExpr = ConditionExpression.fromObj(conditionExprJSON); + expect(conditionExpr.toObj()).toBeDefined(); + expect(conditionExpr.condition.toObj()).toEqual(conditionProps); + }); +}); From 3ecfeede6c0ea7aaa9f4710a9aebcb01d8eaa408 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20N=C3=BA=C3=B1ez?= Date: Fri, 18 Oct 2024 12:47:22 +0200 Subject: [PATCH 38/75] refactor: improvements to eth_getBalance schema * Allow any type of context variable for the eth address parameter * The non-empty array is not an acceptable schema for parameters. What we want is a one-element tuple --- .../taco/src/conditions/schemas/common.ts | 6 ---- packages/taco/src/conditions/schemas/rpc.ts | 23 +++++++++--- .../taco/test/conditions/base/rpc.test.ts | 35 +++++++++++++++---- 3 files changed, 46 insertions(+), 18 deletions(-) diff --git a/packages/taco/src/conditions/schemas/common.ts b/packages/taco/src/conditions/schemas/common.ts index 66263bab3..38a617dff 100644 --- a/packages/taco/src/conditions/schemas/common.ts +++ b/packages/taco/src/conditions/schemas/common.ts @@ -1,4 +1,3 @@ -import { EthAddressSchema } from '@nucypher/shared'; import { USER_ADDRESS_PARAM_DEFAULT, USER_ADDRESS_PARAM_EXTERNAL_EIP4361, @@ -25,11 +24,6 @@ export const UserAddressSchema = z.enum([ USER_ADDRESS_PARAM_EXTERNAL_EIP4361, ]); -export const EthAddressOrUserAddressSchema = z.union([ - EthAddressSchema, - UserAddressSchema, -]); - export const baseConditionSchema = z.object({ conditionType: z.string(), }); diff --git a/packages/taco/src/conditions/schemas/rpc.ts b/packages/taco/src/conditions/schemas/rpc.ts index 9752bae85..02197fc25 100644 --- a/packages/taco/src/conditions/schemas/rpc.ts +++ b/packages/taco/src/conditions/schemas/rpc.ts @@ -1,24 +1,37 @@ import { z } from 'zod'; +import { EthAddressSchema } from '@nucypher/shared'; import { SUPPORTED_CHAIN_IDS } from '../const'; import createUnionSchema, { baseConditionSchema, - EthAddressOrUserAddressSchema, + UserAddressSchema, } from './common'; -import { paramOrContextParamSchema } from './context'; +import { contextParamSchema, paramOrContextParamSchema } from './context'; import { returnValueTestSchema } from './return-value-test'; export const RpcConditionType = 'rpc'; +const EthAddressOrContextVariableSchema = z.union([ + EthAddressSchema, + UserAddressSchema, + contextParamSchema +]); + + +// eth_getBalance schema specification +// - Ethereum spec: https://ethereum.github.io/execution-apis/api-documentation/ +// - web3py: https://web3py.readthedocs.io/en/stable/web3.eth.html#web3.eth.Eth.get_balance export const rpcConditionSchema = baseConditionSchema.extend({ conditionType: z.literal(RpcConditionType).default(RpcConditionType), chain: createUnionSchema(SUPPORTED_CHAIN_IDS), method: z.enum(['eth_getBalance']), parameters: z.union([ - z.array(EthAddressOrUserAddressSchema).nonempty(), - // Using tuple here because ordering matters - z.tuple([EthAddressOrUserAddressSchema, paramOrContextParamSchema]), + // Spec requires 2 parameters: an address and a block identifier + // TODO: Restrict block identifier schema + z.tuple([EthAddressOrContextVariableSchema, paramOrContextParamSchema]), + // Block identifier can be omitted, since web3py (which runs on TACo exec layer) defaults to 'latest', + z.tuple([EthAddressOrContextVariableSchema]), ]), returnValueTest: returnValueTestSchema, // Update to allow multiple return values after expanding supported methods }); diff --git a/packages/taco/test/conditions/base/rpc.test.ts b/packages/taco/test/conditions/base/rpc.test.ts index 69f352e05..860c929b4 100644 --- a/packages/taco/test/conditions/base/rpc.test.ts +++ b/packages/taco/test/conditions/base/rpc.test.ts @@ -60,6 +60,32 @@ describe('validation', () => { }); }); + it('accepts a single UserAddress as address', () => { + const result = RpcCondition.validate(rpcConditionSchema, { + ...testRpcConditionObj, + parameters: [":userAddress"], + }); + + expect(result.error).toBeUndefined(); + expect(result.data).toEqual({ + ...testRpcConditionObj, + parameters: [":userAddress"], + }); + }); + + it('accepts a single context variable as address', () => { + const result = RpcCondition.validate(rpcConditionSchema, { + ...testRpcConditionObj, + parameters: [":testContextVar"], + }); + + expect(result.error).toBeUndefined(); + expect(result.data).toEqual({ + ...testRpcConditionObj, + parameters: [":testContextVar"], + }); + }); + it('accepts a single address and a block number', () => { const result = RpcCondition.validate(rpcConditionSchema, { ...testRpcConditionObj, @@ -88,12 +114,7 @@ describe('validation', () => { expect(result.data).toBeUndefined(); expect(result.error?.format()).toMatchObject({ parameters: { - '1': { - _errors: ['Invalid', 'Invalid Ethereum address'], - }, - '2': { - _errors: ['Invalid', 'Invalid Ethereum address'], - }, + _errors: ['Array must contain at most 2 element(s)'], }, }); }); @@ -109,7 +130,7 @@ describe('validation', () => { expect(result.data).toBeUndefined(); expect(result.error?.format()).toMatchObject({ parameters: { - _errors: ['Array must contain at least 1 element(s)'], + _errors: ['Array must contain at least 2 element(s)', 'Array must contain at least 1 element(s)'], }, }); }); From 978f56bdf034e020ab8118ca9a68850145211b05 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20N=C3=BA=C3=B1ez?= Date: Fri, 18 Oct 2024 13:19:07 +0200 Subject: [PATCH 39/75] refactor: Don't use eth address regex, use existing schema instead --- packages/shared/src/schemas.ts | 3 --- packages/taco/src/conditions/schemas/contract.ts | 4 ++-- packages/taco/test/conditions/base/condition.test.ts | 2 +- packages/taco/test/conditions/base/contract.test.ts | 2 +- 4 files changed, 4 insertions(+), 7 deletions(-) diff --git a/packages/shared/src/schemas.ts b/packages/shared/src/schemas.ts index b404ec6ee..e33d923be 100644 --- a/packages/shared/src/schemas.ts +++ b/packages/shared/src/schemas.ts @@ -1,8 +1,6 @@ import { ethers } from 'ethers'; import { z } from 'zod'; -export const ETH_ADDRESS_REGEXP = new RegExp('^0x[a-fA-F0-9]{40}$'); - const isAddress = (address: string) => { try { return ethers.utils.getAddress(address); @@ -13,5 +11,4 @@ const isAddress = (address: string) => { export const EthAddressSchema = z .string() - .regex(ETH_ADDRESS_REGEXP) .refine(isAddress, { message: 'Invalid Ethereum address' }); diff --git a/packages/taco/src/conditions/schemas/contract.ts b/packages/taco/src/conditions/schemas/contract.ts index bf06c38c1..c22474b00 100644 --- a/packages/taco/src/conditions/schemas/contract.ts +++ b/packages/taco/src/conditions/schemas/contract.ts @@ -1,4 +1,4 @@ -import { ETH_ADDRESS_REGEXP } from '@nucypher/shared'; +import { EthAddressSchema } from '@nucypher/shared'; import { ethers } from 'ethers'; import { z } from 'zod'; @@ -76,7 +76,7 @@ export const contractConditionSchema = rpcConditionSchema conditionType: z .literal(ContractConditionType) .default(ContractConditionType), - contractAddress: z.string().regex(ETH_ADDRESS_REGEXP).length(42), + contractAddress: EthAddressSchema, standardContractType: z.enum(['ERC20', 'ERC721']).optional(), method: z.string(), functionAbi: functionAbiSchema.optional(), diff --git a/packages/taco/test/conditions/base/condition.test.ts b/packages/taco/test/conditions/base/condition.test.ts index 75b2d7acc..2fd4ce2ec 100644 --- a/packages/taco/test/conditions/base/condition.test.ts +++ b/packages/taco/test/conditions/base/condition.test.ts @@ -24,7 +24,7 @@ describe('validation', () => { expect(result.data).toBeUndefined(); expect(result.error?.format()).toMatchObject({ contractAddress: { - _errors: ['Invalid', 'String must contain exactly 42 character(s)'], + _errors: ['Invalid Ethereum address'], }, }); }); diff --git a/packages/taco/test/conditions/base/contract.test.ts b/packages/taco/test/conditions/base/contract.test.ts index 67e3ec26f..634369ee0 100644 --- a/packages/taco/test/conditions/base/contract.test.ts +++ b/packages/taco/test/conditions/base/contract.test.ts @@ -339,7 +339,7 @@ describe('supports custom function abi', async () => { it.each([ { contractAddress: '0x123', - error: ['Invalid', 'String must contain exactly 42 character(s)'], + error: ['Invalid Ethereum address'], }, { contractAddress: undefined, error: ['Required'] }, ])('rejects invalid contract address', async ({ contractAddress, error }) => { From f6084dfaa4965ac94d4104fdcaa8dc7dc09f098c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20N=C3=BA=C3=B1ez?= Date: Fri, 18 Oct 2024 13:28:30 +0200 Subject: [PATCH 40/75] test: improve eth address schema unit tests --- packages/shared/test/schemas.test.ts | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/packages/shared/test/schemas.test.ts b/packages/shared/test/schemas.test.ts index a082be503..eaa31c4d9 100644 --- a/packages/shared/test/schemas.test.ts +++ b/packages/shared/test/schemas.test.ts @@ -8,8 +8,23 @@ describe('ethereum address schema', () => { EthAddressSchema.parse(validAddress); }); - it('should reject invalid ethereum address', () => { + it('should accept unchecksummed ethereum address', () => { + const validAddress = '0x0123456789abcdefedcb0123456789abcdefedcb'; + EthAddressSchema.parse(validAddress); + }); + + it('should accept checksummed ethereum address', () => { + const validAddress = '0x0123456789aBcDeFEdCb0123456789abcdEfeDcb'; + EthAddressSchema.parse(validAddress); + }); + + it('should reject invalid ethereum address (shorter)', () => { const invalidAddress = '0x123456789012345678901234567890123456789'; expect(() => EthAddressSchema.parse(invalidAddress)).toThrow(); }); + + it('should reject invalid ethereum address (longer)', () => { + const invalidAddress = '0x12345678901234567890123456789012345678901'; + expect(() => EthAddressSchema.parse(invalidAddress)).toThrow(); + }); }); From 942258bf2cef93a7247517620e27751ec4816dab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20N=C3=BA=C3=B1ez?= Date: Fri, 18 Oct 2024 13:20:02 +0200 Subject: [PATCH 41/75] feat: new BlockIdentifierSchema See https://ethereum.github.io/execution-apis/api-documentation/ --- packages/shared/src/schemas.ts | 7 +++++ packages/shared/test/schemas.test.ts | 41 +++++++++++++++++++++++++++- 2 files changed, 47 insertions(+), 1 deletion(-) diff --git a/packages/shared/src/schemas.ts b/packages/shared/src/schemas.ts index e33d923be..39c727358 100644 --- a/packages/shared/src/schemas.ts +++ b/packages/shared/src/schemas.ts @@ -12,3 +12,10 @@ const isAddress = (address: string) => { export const EthAddressSchema = z .string() .refine(isAddress, { message: 'Invalid Ethereum address' }); + +const BLOCK_HASH_REGEXP = new RegExp('^0x[a-fA-F0-9]{64}$'); +const BlockNumber = z.number().int().nonnegative(); +const BlockHash = z.string().regex(BLOCK_HASH_REGEXP); +const BlockTag = z.enum(['earliest', 'finalized', 'safe', 'latest', 'pending']); + +export const BlockIdentifierSchema = z.union([BlockNumber, BlockHash, BlockTag]); \ No newline at end of file diff --git a/packages/shared/test/schemas.test.ts b/packages/shared/test/schemas.test.ts index eaa31c4d9..86bc7daed 100644 --- a/packages/shared/test/schemas.test.ts +++ b/packages/shared/test/schemas.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it } from 'vitest'; -import { EthAddressSchema } from '../src'; +import { EthAddressSchema, BlockIdentifierSchema } from '../src'; describe('ethereum address schema', () => { it('should accept valid ethereum address', () => { @@ -28,3 +28,42 @@ describe('ethereum address schema', () => { expect(() => EthAddressSchema.parse(invalidAddress)).toThrow(); }); }); + +describe('block identifier address schema', () => { + it('should accept valid block numbers (ints >= 0)', () => { + BlockIdentifierSchema.parse(0); + BlockIdentifierSchema.parse(1); + BlockIdentifierSchema.parse(1234); + BlockIdentifierSchema.parse(9007199254740991); // Max safe integer + }); + + it('should accept valid block tags', () => { + BlockIdentifierSchema.parse('earliest'); + BlockIdentifierSchema.parse('finalized'); + BlockIdentifierSchema.parse('safe'); + BlockIdentifierSchema.parse('latest'); + BlockIdentifierSchema.parse('pending'); + }); + + it('should accept valid block hashes', () => { + const validBlockHash = '0x1234567890123456789012345678901234567890123456789012345678901234'; + BlockIdentifierSchema.parse(validBlockHash); + }); + + it('should reject invalid block numbers (e.g., negative ints)', () => { + expect(() => BlockIdentifierSchema.parse(-42)).toThrow(); + }); + + it('should reject invalid block numbers (e.g., float)', () => { + expect(() => BlockIdentifierSchema.parse(34.56)).toThrow(); + }); + + it('should reject invalid block identifiers', () => { + expect(() => BlockIdentifierSchema.parse('foo')).toThrow(); + }); + + it('should reject invalid block hashes', () => { + const invalidBlockHash = '0x1234'; + expect(() => BlockIdentifierSchema.parse(invalidBlockHash)).toThrow(); + }); +}); From 677e7e7a39a875746bcdc1dad6a75f68020cb7f1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20N=C3=BA=C3=B1ez?= Date: Fri, 18 Oct 2024 17:07:55 +0200 Subject: [PATCH 42/75] feat: Add block identifier schema validation to eth_getBalance --- packages/taco/src/conditions/schemas/rpc.ts | 9 ++++----- packages/taco/test/conditions/base/rpc.test.ts | 17 +++++++++++++++-- 2 files changed, 19 insertions(+), 7 deletions(-) diff --git a/packages/taco/src/conditions/schemas/rpc.ts b/packages/taco/src/conditions/schemas/rpc.ts index 02197fc25..771c4d2fe 100644 --- a/packages/taco/src/conditions/schemas/rpc.ts +++ b/packages/taco/src/conditions/schemas/rpc.ts @@ -1,13 +1,13 @@ +import { BlockIdentifierSchema, EthAddressSchema } from '@nucypher/shared'; import { z } from 'zod'; -import { EthAddressSchema } from '@nucypher/shared'; import { SUPPORTED_CHAIN_IDS } from '../const'; import createUnionSchema, { baseConditionSchema, UserAddressSchema, } from './common'; -import { contextParamSchema, paramOrContextParamSchema } from './context'; +import { contextParamSchema } from './context'; import { returnValueTestSchema } from './return-value-test'; export const RpcConditionType = 'rpc'; @@ -17,7 +17,7 @@ const EthAddressOrContextVariableSchema = z.union([ UserAddressSchema, contextParamSchema ]); - +const BlockOrContextParamSchema = z.union([BlockIdentifierSchema, contextParamSchema]) // eth_getBalance schema specification // - Ethereum spec: https://ethereum.github.io/execution-apis/api-documentation/ @@ -28,8 +28,7 @@ export const rpcConditionSchema = baseConditionSchema.extend({ method: z.enum(['eth_getBalance']), parameters: z.union([ // Spec requires 2 parameters: an address and a block identifier - // TODO: Restrict block identifier schema - z.tuple([EthAddressOrContextVariableSchema, paramOrContextParamSchema]), + z.tuple([EthAddressOrContextVariableSchema, BlockOrContextParamSchema]), // Block identifier can be omitted, since web3py (which runs on TACo exec layer) defaults to 'latest', z.tuple([EthAddressOrContextVariableSchema]), ]), diff --git a/packages/taco/test/conditions/base/rpc.test.ts b/packages/taco/test/conditions/base/rpc.test.ts index 860c929b4..35c0d22b1 100644 --- a/packages/taco/test/conditions/base/rpc.test.ts +++ b/packages/taco/test/conditions/base/rpc.test.ts @@ -89,13 +89,26 @@ describe('validation', () => { it('accepts a single address and a block number', () => { const result = RpcCondition.validate(rpcConditionSchema, { ...testRpcConditionObj, - parameters: [TEST_CONTRACT_ADDR, 'latest'], + parameters: [TEST_CONTRACT_ADDR, 2345], }); expect(result.error).toBeUndefined(); expect(result.data).toEqual({ ...testRpcConditionObj, - parameters: [TEST_CONTRACT_ADDR, 'latest'], + parameters: [TEST_CONTRACT_ADDR, 2345], + }); + }); + + it('accepts context params for address and block number', () => { + const result = RpcCondition.validate(rpcConditionSchema, { + ...testRpcConditionObj, + parameters: [":testAddress", ":testBlockNumber"], + }); + + expect(result.error).toBeUndefined(); + expect(result.data).toEqual({ + ...testRpcConditionObj, + parameters: [":testAddress", ":testBlockNumber"], }); }); From fa81b97611af0f1f53d30e5f46d4647d74a2dd62 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20N=C3=BA=C3=B1ez?= Date: Thu, 24 Oct 2024 18:34:45 +0200 Subject: [PATCH 43/75] fix: contextParamSchema is already exported by context.ts --- packages/taco/src/conditions/schemas/common.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/packages/taco/src/conditions/schemas/common.ts b/packages/taco/src/conditions/schemas/common.ts index 38a617dff..dba897839 100644 --- a/packages/taco/src/conditions/schemas/common.ts +++ b/packages/taco/src/conditions/schemas/common.ts @@ -4,9 +4,7 @@ import { } from '@nucypher/taco-auth'; import { Primitive, z, ZodLiteral } from 'zod'; -import { CONTEXT_PARAM_PREFIX, CONTEXT_PARAM_REGEXP } from '../const'; - -export const contextParamSchema = z.string().regex(CONTEXT_PARAM_REGEXP); +import { CONTEXT_PARAM_PREFIX } from '../const'; // We want to discriminate between ContextParams and plain strings // If a string starts with `:`, it's a ContextParam From f971fead495334653c0a4b7de4157605aa2afccb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20N=C3=BA=C3=B1ez?= Date: Thu, 24 Oct 2024 18:38:15 +0200 Subject: [PATCH 44/75] feat: Add error message to BlockHash schema validation Co-authored-by: Derek Pierre --- packages/shared/src/schemas.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/shared/src/schemas.ts b/packages/shared/src/schemas.ts index 39c727358..1ca394a3c 100644 --- a/packages/shared/src/schemas.ts +++ b/packages/shared/src/schemas.ts @@ -15,7 +15,7 @@ export const EthAddressSchema = z const BLOCK_HASH_REGEXP = new RegExp('^0x[a-fA-F0-9]{64}$'); const BlockNumber = z.number().int().nonnegative(); -const BlockHash = z.string().regex(BLOCK_HASH_REGEXP); +const BlockHash = z.string().regex(BLOCK_HASH_REGEXP, 'Invalid block hash'); const BlockTag = z.enum(['earliest', 'finalized', 'safe', 'latest', 'pending']); export const BlockIdentifierSchema = z.union([BlockNumber, BlockHash, BlockTag]); \ No newline at end of file From b3ed1c4ab7ceb9ae72411cc8b2319af69dc955b8 Mon Sep 17 00:00:00 2001 From: derekpierre Date: Mon, 28 Oct 2024 09:31:15 -0400 Subject: [PATCH 45/75] Allow authorization token to be optionally specified for JsonApiCondition. The value for an authorization token must be a context variable. --- packages/taco/src/conditions/schemas/json-api.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/taco/src/conditions/schemas/json-api.ts b/packages/taco/src/conditions/schemas/json-api.ts index 4a9afb822..0bc20e0ab 100644 --- a/packages/taco/src/conditions/schemas/json-api.ts +++ b/packages/taco/src/conditions/schemas/json-api.ts @@ -25,6 +25,7 @@ export const jsonApiConditionSchema = z.object({ endpoint: z.string().url(), parameters: z.record(z.string(), z.unknown()).optional(), query: jsonPathSchema.optional(), + authorizationToken: contextParamSchema.optional(), returnValueTest: returnValueTestSchema, // Update to allow multiple return values after expanding supported methods }); From 9527c42aca04a6a7ccc687d0378698beff1b15f9 Mon Sep 17 00:00:00 2001 From: derekpierre Date: Mon, 28 Oct 2024 09:33:24 -0400 Subject: [PATCH 46/75] Allow query for JsonApiCondition to contain context variables. There is a difference between checking that an entire string is a context variable, and checking whether a context variable is a substring within a string - make that difference clearer with respect to regexp constants. --- packages/taco/src/conditions/const.ts | 9 ++++++++- packages/taco/src/conditions/context/context.ts | 4 ++-- packages/taco/src/conditions/schemas/context.ts | 6 ++++-- packages/taco/src/conditions/schemas/json-api.ts | 9 +++++++++ 4 files changed, 23 insertions(+), 5 deletions(-) diff --git a/packages/taco/src/conditions/const.ts b/packages/taco/src/conditions/const.ts index 4cb67d6c2..82370ca0a 100644 --- a/packages/taco/src/conditions/const.ts +++ b/packages/taco/src/conditions/const.ts @@ -5,7 +5,14 @@ import { } from '@nucypher/taco-auth'; // Only allow alphanumeric characters and underscores -export const CONTEXT_PARAM_REGEXP = new RegExp('^:[a-zA-Z_][a-zA-Z0-9_]*$'); +const contextParamRegexString = ':[a-zA-Z_][a-zA-Z0-9_]*'; + +export const CONTEXT_PARAM_REGEXP = new RegExp(contextParamRegexString); + +// Entire string is context param +export const CONTEXT_PARAM_FULL_MATCH_REGEXP = new RegExp( + `^${contextParamRegexString}$`, +); export const CONTEXT_PARAM_PREFIX = ':'; diff --git a/packages/taco/src/conditions/context/context.ts b/packages/taco/src/conditions/context/context.ts index 8b4992b8a..116d04af0 100644 --- a/packages/taco/src/conditions/context/context.ts +++ b/packages/taco/src/conditions/context/context.ts @@ -14,8 +14,8 @@ import { CompoundConditionType } from '../compound-condition'; import { Condition, ConditionProps } from '../condition'; import { ConditionExpression } from '../condition-expr'; import { + CONTEXT_PARAM_FULL_MATCH_REGEXP, CONTEXT_PARAM_PREFIX, - CONTEXT_PARAM_REGEXP, USER_ADDRESS_PARAMS, } from '../const'; @@ -138,7 +138,7 @@ export class ConditionContext { } private static isContextParameter(param: unknown): boolean { - return !!String(param).match(CONTEXT_PARAM_REGEXP); + return !!String(param).match(CONTEXT_PARAM_FULL_MATCH_REGEXP); } private static findContextParameters(condition: ConditionProps) { diff --git a/packages/taco/src/conditions/schemas/context.ts b/packages/taco/src/conditions/schemas/context.ts index 3c2f2a92a..1b3208fc1 100644 --- a/packages/taco/src/conditions/schemas/context.ts +++ b/packages/taco/src/conditions/schemas/context.ts @@ -1,10 +1,12 @@ import { z } from 'zod'; -import { CONTEXT_PARAM_REGEXP } from '../const'; +import { CONTEXT_PARAM_FULL_MATCH_REGEXP } from '../const'; import { plainStringSchema } from './common'; -export const contextParamSchema = z.string().regex(CONTEXT_PARAM_REGEXP); +export const contextParamSchema = z + .string() + .regex(CONTEXT_PARAM_FULL_MATCH_REGEXP); const paramSchema = z.union([plainStringSchema, z.boolean(), z.number()]); diff --git a/packages/taco/src/conditions/schemas/json-api.ts b/packages/taco/src/conditions/schemas/json-api.ts index 0bc20e0ab..626a4c159 100644 --- a/packages/taco/src/conditions/schemas/json-api.ts +++ b/packages/taco/src/conditions/schemas/json-api.ts @@ -1,11 +1,20 @@ import { JSONPath } from '@astronautlabs/jsonpath'; import { z } from 'zod'; +import { CONTEXT_PARAM_REGEXP } from '../const'; + +import { contextParamSchema } from './context'; import { returnValueTestSchema } from './return-value-test'; export const JsonApiConditionType = 'json-api'; const validateJSONPath = (jsonPath: string): boolean => { + // account for embedded context variables + if (CONTEXT_PARAM_REGEXP.test(jsonPath)) { + // skip validation + return true; + } + try { JSONPath.parse(jsonPath); return true; From 521d56ebb53b23f7ace939e8e6e678ed30e8a02f Mon Sep 17 00:00:00 2001 From: derekpierre Date: Mon, 28 Oct 2024 09:35:19 -0400 Subject: [PATCH 47/75] Add tests for authorization token property for JsonApiCondition. --- .../taco/test/conditions/base/json.test.ts | 33 +++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/packages/taco/test/conditions/base/json.test.ts b/packages/taco/test/conditions/base/json.test.ts index 2f9291d4d..5f8ea0452 100644 --- a/packages/taco/test/conditions/base/json.test.ts +++ b/packages/taco/test/conditions/base/json.test.ts @@ -39,6 +39,39 @@ describe('JsonApiCondition', () => { }); }); + describe('authorizationToken', () => { + it('accepts context variable', () => { + const result = JsonApiCondition.validate(jsonApiConditionSchema, { + ...testJsonApiConditionObj, + authorizationToken: ':authToken', + }); + expect(result.error).toBeUndefined(); + expect(result.data).toEqual({ + ...testJsonApiConditionObj, + authorizationToken: ':authToken', + }); + }); + it.each([ + 'authToken', + 'ABCDEF1234567890', + ':authToken?', + '$:authToken', + ':auth-Token', + ])('rejects invalid context variable', (contextVar) => { + const result = JsonApiCondition.validate(jsonApiConditionSchema, { + ...testJsonApiConditionObj, + authorizationToken: `${contextVar}`, + }); + expect(result.error).toBeDefined(); + expect(result.data).toBeUndefined(); + expect(result.error?.format()).toMatchObject({ + authorizationToken: { + _errors: ['Invalid'], + }, + }); + }); + }); + describe('parameters', () => { it('accepts conditions without query path', () => { const { query, ...noQueryObj } = testJsonApiConditionObj; From 6d4babe7bb5ea5df8397c814d362a204d9216116 Mon Sep 17 00:00:00 2001 From: derekpierre Date: Mon, 28 Oct 2024 09:35:55 -0400 Subject: [PATCH 48/75] Add test for context variables within various properties of a JsonApiCondition. --- .../taco/test/conditions/base/json.test.ts | 27 +++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/packages/taco/test/conditions/base/json.test.ts b/packages/taco/test/conditions/base/json.test.ts index 5f8ea0452..cc04fe31b 100644 --- a/packages/taco/test/conditions/base/json.test.ts +++ b/packages/taco/test/conditions/base/json.test.ts @@ -4,6 +4,7 @@ import { describe, expect, it } from 'vitest'; import { JsonApiCondition, jsonApiConditionSchema, + JsonApiConditionType, } from '../../../src/conditions/base/json-api'; import { testJsonApiConditionObj } from '../../test-utils'; @@ -95,5 +96,31 @@ describe('JsonApiCondition', () => { expect(result.data).toEqual(noParamsObj); }); }); + + describe('context variables', () => { + it('allow context variables for various values including as substring', () => { + const jsonApiConditionObj = { + conditionType: JsonApiConditionType, + endpoint: + 'https://api.coingecko.com/api/:version/simple/:endpointPath', + parameters: { + ids: 'ethereum', + vs_currencies: ':vsCurrency', + }, + query: 'ethereum.:vsCurrency', + authorizationToken: ':authToken', + returnValueTest: { + comparator: '==', + value: ':expectedPrice', + }, + }; + const result = JsonApiCondition.validate( + jsonApiConditionSchema, + jsonApiConditionObj, + ); + expect(result.error).toBeUndefined(); + expect(result.data).toEqual(jsonApiConditionObj); + }); + }); }); }); From 0e25a114efcdf00ef8ed55bd35bfed9bce81099e Mon Sep 17 00:00:00 2001 From: derekpierre Date: Mon, 28 Oct 2024 09:37:30 -0400 Subject: [PATCH 49/75] Fix linting. --- packages/shared/src/schemas.ts | 6 +++++- packages/shared/test/schemas.test.ts | 5 +++-- packages/taco/src/conditions/schemas/rpc.ts | 11 +++++++---- packages/taco/test/conditions/base/rpc.test.ts | 17 ++++++++++------- 4 files changed, 25 insertions(+), 14 deletions(-) diff --git a/packages/shared/src/schemas.ts b/packages/shared/src/schemas.ts index 1ca394a3c..37dfa2653 100644 --- a/packages/shared/src/schemas.ts +++ b/packages/shared/src/schemas.ts @@ -18,4 +18,8 @@ const BlockNumber = z.number().int().nonnegative(); const BlockHash = z.string().regex(BLOCK_HASH_REGEXP, 'Invalid block hash'); const BlockTag = z.enum(['earliest', 'finalized', 'safe', 'latest', 'pending']); -export const BlockIdentifierSchema = z.union([BlockNumber, BlockHash, BlockTag]); \ No newline at end of file +export const BlockIdentifierSchema = z.union([ + BlockNumber, + BlockHash, + BlockTag, +]); diff --git a/packages/shared/test/schemas.test.ts b/packages/shared/test/schemas.test.ts index 86bc7daed..7b1803945 100644 --- a/packages/shared/test/schemas.test.ts +++ b/packages/shared/test/schemas.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it } from 'vitest'; -import { EthAddressSchema, BlockIdentifierSchema } from '../src'; +import { BlockIdentifierSchema, EthAddressSchema } from '../src'; describe('ethereum address schema', () => { it('should accept valid ethereum address', () => { @@ -46,7 +46,8 @@ describe('block identifier address schema', () => { }); it('should accept valid block hashes', () => { - const validBlockHash = '0x1234567890123456789012345678901234567890123456789012345678901234'; + const validBlockHash = + '0x1234567890123456789012345678901234567890123456789012345678901234'; BlockIdentifierSchema.parse(validBlockHash); }); diff --git a/packages/taco/src/conditions/schemas/rpc.ts b/packages/taco/src/conditions/schemas/rpc.ts index 771c4d2fe..a80a59560 100644 --- a/packages/taco/src/conditions/schemas/rpc.ts +++ b/packages/taco/src/conditions/schemas/rpc.ts @@ -15,9 +15,12 @@ export const RpcConditionType = 'rpc'; const EthAddressOrContextVariableSchema = z.union([ EthAddressSchema, UserAddressSchema, - contextParamSchema + contextParamSchema, +]); +const BlockOrContextParamSchema = z.union([ + BlockIdentifierSchema, + contextParamSchema, ]); -const BlockOrContextParamSchema = z.union([BlockIdentifierSchema, contextParamSchema]) // eth_getBalance schema specification // - Ethereum spec: https://ethereum.github.io/execution-apis/api-documentation/ @@ -29,8 +32,8 @@ export const rpcConditionSchema = baseConditionSchema.extend({ parameters: z.union([ // Spec requires 2 parameters: an address and a block identifier z.tuple([EthAddressOrContextVariableSchema, BlockOrContextParamSchema]), - // Block identifier can be omitted, since web3py (which runs on TACo exec layer) defaults to 'latest', - z.tuple([EthAddressOrContextVariableSchema]), + // Block identifier can be omitted, since web3py (which runs on TACo exec layer) defaults to 'latest', + z.tuple([EthAddressOrContextVariableSchema]), ]), returnValueTest: returnValueTestSchema, // Update to allow multiple return values after expanding supported methods }); diff --git a/packages/taco/test/conditions/base/rpc.test.ts b/packages/taco/test/conditions/base/rpc.test.ts index 35c0d22b1..e1cb99cb6 100644 --- a/packages/taco/test/conditions/base/rpc.test.ts +++ b/packages/taco/test/conditions/base/rpc.test.ts @@ -63,26 +63,26 @@ describe('validation', () => { it('accepts a single UserAddress as address', () => { const result = RpcCondition.validate(rpcConditionSchema, { ...testRpcConditionObj, - parameters: [":userAddress"], + parameters: [':userAddress'], }); expect(result.error).toBeUndefined(); expect(result.data).toEqual({ ...testRpcConditionObj, - parameters: [":userAddress"], + parameters: [':userAddress'], }); }); it('accepts a single context variable as address', () => { const result = RpcCondition.validate(rpcConditionSchema, { ...testRpcConditionObj, - parameters: [":testContextVar"], + parameters: [':testContextVar'], }); expect(result.error).toBeUndefined(); expect(result.data).toEqual({ ...testRpcConditionObj, - parameters: [":testContextVar"], + parameters: [':testContextVar'], }); }); @@ -102,13 +102,13 @@ describe('validation', () => { it('accepts context params for address and block number', () => { const result = RpcCondition.validate(rpcConditionSchema, { ...testRpcConditionObj, - parameters: [":testAddress", ":testBlockNumber"], + parameters: [':testAddress', ':testBlockNumber'], }); expect(result.error).toBeUndefined(); expect(result.data).toEqual({ ...testRpcConditionObj, - parameters: [":testAddress", ":testBlockNumber"], + parameters: [':testAddress', ':testBlockNumber'], }); }); @@ -143,7 +143,10 @@ describe('validation', () => { expect(result.data).toBeUndefined(); expect(result.error?.format()).toMatchObject({ parameters: { - _errors: ['Array must contain at least 2 element(s)', 'Array must contain at least 1 element(s)'], + _errors: [ + 'Array must contain at least 2 element(s)', + 'Array must contain at least 1 element(s)', + ], }, }); }); From 3e6f4ceec64cb8445d5d95c88017b7da219b673e Mon Sep 17 00:00:00 2001 From: derekpierre Date: Mon, 4 Nov 2024 09:19:30 -0500 Subject: [PATCH 50/75] chore(release): release @nucypher/shared:0.5.0-alpha.0 --- packages/shared/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/shared/package.json b/packages/shared/package.json index 2a7fde264..3d83a882b 100644 --- a/packages/shared/package.json +++ b/packages/shared/package.json @@ -1,6 +1,6 @@ { "name": "@nucypher/shared", - "version": "0.4.0", + "version": "0.5.0-alpha.0", "keywords": [ "pre", "taco", From b8d64e49c21fed2b2079fd92ce69f29952e30b88 Mon Sep 17 00:00:00 2001 From: derekpierre Date: Mon, 4 Nov 2024 09:21:42 -0500 Subject: [PATCH 51/75] chore(release): release @nucypher/taco-auth:0.3.0-alpha.0 --- packages/taco-auth/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/taco-auth/package.json b/packages/taco-auth/package.json index f829e8eb5..39ace5fcb 100644 --- a/packages/taco-auth/package.json +++ b/packages/taco-auth/package.json @@ -1,6 +1,6 @@ { "name": "@nucypher/taco-auth", - "version": "0.2.0", + "version": "0.3.0-alpha.0", "keywords": [ "pre", "threshold", From 6b8184f04d329b41d502c0b32e25879600dd2c56 Mon Sep 17 00:00:00 2001 From: derekpierre Date: Mon, 4 Nov 2024 09:24:56 -0500 Subject: [PATCH 52/75] chore(release): release @nucypher/taco:0.6.0-alpha.0 --- packages/taco/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/taco/package.json b/packages/taco/package.json index e1914d58f..6b4eb8171 100644 --- a/packages/taco/package.json +++ b/packages/taco/package.json @@ -1,6 +1,6 @@ { "name": "@nucypher/taco", - "version": "0.5.0", + "version": "0.6.0-alpha.0", "keywords": [ "taco", "threshold", From 7868bc81a3e7d2fb7768b7ed3e62d9057e7b71fe Mon Sep 17 00:00:00 2001 From: James Campbell Date: Thu, 7 Nov 2024 16:47:42 +0700 Subject: [PATCH 53/75] Parse context parameters in JSON API condition --- .../taco/src/conditions/context/context.ts | 21 ++++++++++ packages/taco/test/taco.test.ts | 38 ++++++++++++++++++- 2 files changed, 58 insertions(+), 1 deletion(-) diff --git a/packages/taco/src/conditions/context/context.ts b/packages/taco/src/conditions/context/context.ts index 116d04af0..ffb5b0da0 100644 --- a/packages/taco/src/conditions/context/context.ts +++ b/packages/taco/src/conditions/context/context.ts @@ -18,6 +18,7 @@ import { CONTEXT_PARAM_PREFIX, USER_ADDRESS_PARAMS, } from '../const'; +import { JsonApiConditionType } from '../schemas/json-api'; export type CustomContextParam = string | number | boolean; export type ContextParam = CustomContextParam | AuthSignature; @@ -178,6 +179,26 @@ export class ConditionContext { } } } + // If it's a JSON API condition, check url and query + if (condition.conditionType === JsonApiConditionType) { + const urlComponents = condition.endpoint.replace("https://", "").split("/"); + for (const param of urlComponents ?? []) { + if (this.isContextParameter(param)) { + requestedParameters.add(param); + } + } + if (condition.query) { + const queryParams = condition.query.match(":[a-zA-Z_]*"); + if (queryParams) { + for (const param of queryParams) { + requestedParameters.add(param); + } + } + } + if (this.isContextParameter(condition.authorizationToken)) { + requestedParameters.add(condition.authorizationToken); + } + } return requestedParameters; } diff --git a/packages/taco/test/taco.test.ts b/packages/taco/test/taco.test.ts index 5f8b59f3d..539294bd9 100644 --- a/packages/taco/test/taco.test.ts +++ b/packages/taco/test/taco.test.ts @@ -136,4 +136,40 @@ describe('taco', () => { new Set([customParamKey, USER_ADDRESS_PARAM_DEFAULT]), ); }); -}); + // test json api condition exposes requested parameters + it('jsonapi condition exposes requested parameters', async () => { + const mockedDkg = fakeDkgFlow(FerveoVariant.precomputed, 0, 4, 4); + const mockedDkgRitual = fakeDkgRitual(mockedDkg); + const provider = fakeProvider(aliceSecretKeyBytes); + const signer = provider.getSigner(); + const getFinalizedRitualSpy = mockGetActiveRitual(mockedDkgRitual); + + const jsonApiCondition = new conditions.base.jsonApi.JsonApiCondition({ + endpoint: 'https://api.example.com/:userId/data', + query: '$.data[?(@.owner == :userAddress)].value', + authorizationToken: ':authToken', + returnValueTest: { + comparator: '==', + value: true + } + }); + + const messageKit = await taco.encrypt( + provider, + domains.DEVNET, + message, + jsonApiCondition, + mockedDkg.ritualId, + signer, + ); + expect(getFinalizedRitualSpy).toHaveBeenCalled(); + + const conditionContext = ConditionContext.fromMessageKit(messageKit); + const requestedParameters = conditionContext.requestedContextParameters; + + // Verify all context parameters from endpoint, query and authToken are detected + expect(requestedParameters).toEqual( + new Set([':userId', ':userAddress', ':authToken']) + ); + })} +); From 9fac7b230638823657ac68f72fd33c2845dd84f8 Mon Sep 17 00:00:00 2001 From: James Campbell Date: Thu, 7 Nov 2024 10:55:45 +0100 Subject: [PATCH 54/75] Use + instead of * in regex * would match 0 - infinity Co-authored-by: KPrasch --- packages/taco/src/conditions/context/context.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/taco/src/conditions/context/context.ts b/packages/taco/src/conditions/context/context.ts index ffb5b0da0..3f9cf1f6e 100644 --- a/packages/taco/src/conditions/context/context.ts +++ b/packages/taco/src/conditions/context/context.ts @@ -188,7 +188,7 @@ export class ConditionContext { } } if (condition.query) { - const queryParams = condition.query.match(":[a-zA-Z_]*"); + const queryParams = condition.query.match(":[a-zA-Z_]+"); if (queryParams) { for (const param of queryParams) { requestedParameters.add(param); From f27aebbeddd6de32d1aea1d4e1e973c651dfe2d3 Mon Sep 17 00:00:00 2001 From: James Campbell Date: Thu, 7 Nov 2024 17:01:51 +0700 Subject: [PATCH 55/75] chore(release): release @nucypher/shared:0.5.0-alpha.1 --- packages/shared/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/shared/package.json b/packages/shared/package.json index 3d83a882b..d7498aa6c 100644 --- a/packages/shared/package.json +++ b/packages/shared/package.json @@ -1,6 +1,6 @@ { "name": "@nucypher/shared", - "version": "0.5.0-alpha.0", + "version": "0.5.0-alpha.1", "keywords": [ "pre", "taco", From afd4305b01b0ac2e3616ba2562775627121511bc Mon Sep 17 00:00:00 2001 From: James Campbell Date: Thu, 7 Nov 2024 17:03:43 +0700 Subject: [PATCH 56/75] chore(release): release @nucypher/taco-auth:0.3.0-alpha.1 --- packages/taco-auth/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/taco-auth/package.json b/packages/taco-auth/package.json index 39ace5fcb..20e8d195d 100644 --- a/packages/taco-auth/package.json +++ b/packages/taco-auth/package.json @@ -1,6 +1,6 @@ { "name": "@nucypher/taco-auth", - "version": "0.3.0-alpha.0", + "version": "0.3.0-alpha.1", "keywords": [ "pre", "threshold", From c1a7617a296630f9d1797edafd542acd6c3ee4f0 Mon Sep 17 00:00:00 2001 From: James Campbell Date: Thu, 7 Nov 2024 17:04:36 +0700 Subject: [PATCH 57/75] chore(release): release @nucypher/taco:0.6.0-alpha.1 --- packages/taco/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/taco/package.json b/packages/taco/package.json index 6b4eb8171..a3ee5f618 100644 --- a/packages/taco/package.json +++ b/packages/taco/package.json @@ -1,6 +1,6 @@ { "name": "@nucypher/taco", - "version": "0.6.0-alpha.0", + "version": "0.6.0-alpha.1", "keywords": [ "taco", "threshold", From 5477f6f5666300d46a54936fd3bd75401ae07dfc Mon Sep 17 00:00:00 2001 From: derekpierre Date: Wed, 18 Dec 2024 08:45:25 -0500 Subject: [PATCH 58/75] Fix linting. --- packages/taco/src/conditions/context/context.ts | 6 ++++-- packages/taco/test/taco.test.ts | 12 ++++++------ 2 files changed, 10 insertions(+), 8 deletions(-) diff --git a/packages/taco/src/conditions/context/context.ts b/packages/taco/src/conditions/context/context.ts index 3f9cf1f6e..dd4a2815a 100644 --- a/packages/taco/src/conditions/context/context.ts +++ b/packages/taco/src/conditions/context/context.ts @@ -181,14 +181,16 @@ export class ConditionContext { } // If it's a JSON API condition, check url and query if (condition.conditionType === JsonApiConditionType) { - const urlComponents = condition.endpoint.replace("https://", "").split("/"); + const urlComponents = condition.endpoint + .replace('https://', '') + .split('/'); for (const param of urlComponents ?? []) { if (this.isContextParameter(param)) { requestedParameters.add(param); } } if (condition.query) { - const queryParams = condition.query.match(":[a-zA-Z_]+"); + const queryParams = condition.query.match(':[a-zA-Z_]+'); if (queryParams) { for (const param of queryParams) { requestedParameters.add(param); diff --git a/packages/taco/test/taco.test.ts b/packages/taco/test/taco.test.ts index 539294bd9..1906c34a2 100644 --- a/packages/taco/test/taco.test.ts +++ b/packages/taco/test/taco.test.ts @@ -150,8 +150,8 @@ describe('taco', () => { authorizationToken: ':authToken', returnValueTest: { comparator: '==', - value: true - } + value: true, + }, }); const messageKit = await taco.encrypt( @@ -166,10 +166,10 @@ describe('taco', () => { const conditionContext = ConditionContext.fromMessageKit(messageKit); const requestedParameters = conditionContext.requestedContextParameters; - + // Verify all context parameters from endpoint, query and authToken are detected expect(requestedParameters).toEqual( - new Set([':userId', ':userAddress', ':authToken']) + new Set([':userId', ':userAddress', ':authToken']), ); - })} -); + }); +}); From 36d30f53ef97be242e33280068fabf9fd8dfdfee Mon Sep 17 00:00:00 2001 From: derekpierre Date: Wed, 18 Dec 2024 11:53:37 -0500 Subject: [PATCH 59/75] Apply outstanding RFCs from #602. --- packages/taco/src/conditions/context/context.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/packages/taco/src/conditions/context/context.ts b/packages/taco/src/conditions/context/context.ts index dd4a2815a..2636a2b84 100644 --- a/packages/taco/src/conditions/context/context.ts +++ b/packages/taco/src/conditions/context/context.ts @@ -16,6 +16,7 @@ import { ConditionExpression } from '../condition-expr'; import { CONTEXT_PARAM_FULL_MATCH_REGEXP, CONTEXT_PARAM_PREFIX, + CONTEXT_PARAM_REGEXP, USER_ADDRESS_PARAMS, } from '../const'; import { JsonApiConditionType } from '../schemas/json-api'; @@ -190,14 +191,15 @@ export class ConditionContext { } } if (condition.query) { - const queryParams = condition.query.match(':[a-zA-Z_]+'); + const queryParams = condition.query.match(CONTEXT_PARAM_REGEXP); if (queryParams) { for (const param of queryParams) { requestedParameters.add(param); } } } - if (this.isContextParameter(condition.authorizationToken)) { + // always a context variable, so simply check whether defined + if (condition.authorizationToken) { requestedParameters.add(condition.authorizationToken); } } From e7910d10fcd494390559d6c32670b2e90dab8291 Mon Sep 17 00:00:00 2001 From: James Campbell Date: Mon, 16 Dec 2024 17:03:45 +0100 Subject: [PATCH 60/75] Remove references to supported chainID --- packages/taco/src/conditions/const.ts | 8 -------- packages/taco/test/conditions/base/condition.test.ts | 5 +---- packages/taco/test/conditions/compound-condition.test.ts | 4 ++-- packages/taco/test/conditions/conditions.test.ts | 7 ------- 4 files changed, 3 insertions(+), 21 deletions(-) diff --git a/packages/taco/src/conditions/const.ts b/packages/taco/src/conditions/const.ts index 82370ca0a..f74b4fbd9 100644 --- a/packages/taco/src/conditions/const.ts +++ b/packages/taco/src/conditions/const.ts @@ -1,4 +1,3 @@ -import { ChainId } from '@nucypher/shared'; import { USER_ADDRESS_PARAM_DEFAULT, USER_ADDRESS_PARAM_EXTERNAL_EIP4361, @@ -16,13 +15,6 @@ export const CONTEXT_PARAM_FULL_MATCH_REGEXP = new RegExp( export const CONTEXT_PARAM_PREFIX = ':'; -export const SUPPORTED_CHAIN_IDS = [ - ChainId.POLYGON, - ChainId.AMOY, - ChainId.SEPOLIA, - ChainId.ETHEREUM_MAINNET, -]; - export const USER_ADDRESS_PARAMS = [ USER_ADDRESS_PARAM_EXTERNAL_EIP4361, // Ordering matters, this should always be last diff --git a/packages/taco/test/conditions/base/condition.test.ts b/packages/taco/test/conditions/base/condition.test.ts index 2fd4ce2ec..e1473308c 100644 --- a/packages/taco/test/conditions/base/condition.test.ts +++ b/packages/taco/test/conditions/base/condition.test.ts @@ -41,10 +41,7 @@ describe('validation', () => { expect(result.error?.format()).toMatchObject({ chain: { _errors: [ - 'Invalid literal value, expected 137', - 'Invalid literal value, expected 80002', - 'Invalid literal value, expected 11155111', - 'Invalid literal value, expected 1', + "Expected number, received string", ], }, }); diff --git a/packages/taco/test/conditions/compound-condition.test.ts b/packages/taco/test/conditions/compound-condition.test.ts index 809914872..90099cd58 100644 --- a/packages/taco/test/conditions/compound-condition.test.ts +++ b/packages/taco/test/conditions/compound-condition.test.ts @@ -1,3 +1,4 @@ +import { ChainId } from '@nucypher/shared'; import { describe, expect, it } from 'vitest'; import { ContractCondition } from '../../src/conditions/base/contract'; @@ -8,7 +9,6 @@ import { compoundConditionSchema, CompoundConditionType, } from '../../src/conditions/compound-condition'; -import { SUPPORTED_CHAIN_IDS } from '../../src/conditions/const'; import { testContractConditionObj, testRpcConditionObj, @@ -247,7 +247,7 @@ describe('validation', () => { const multichainCondition: CompoundConditionProps = { conditionType: CompoundConditionType, operator: 'and', - operands: SUPPORTED_CHAIN_IDS.map((chain) => ({ + operands: [ChainId.SEPOLIA, ChainId.AMOY].map((chain) => ({ ...testRpcConditionObj, chain, })), diff --git a/packages/taco/test/conditions/conditions.test.ts b/packages/taco/test/conditions/conditions.test.ts index 3be3f664a..6c80d6142 100644 --- a/packages/taco/test/conditions/conditions.test.ts +++ b/packages/taco/test/conditions/conditions.test.ts @@ -5,7 +5,6 @@ import { beforeAll, describe, expect, it } from 'vitest'; import { initialize } from '../../src'; import { CompoundCondition } from '../../src/conditions/compound-condition'; -import { SUPPORTED_CHAIN_IDS } from '../../src/conditions/const'; import { ConditionContext } from '../../src/conditions/context'; describe('conditions', () => { @@ -53,10 +52,4 @@ describe('conditions', () => { expect(asObj).toBeDefined(); expect(asObj[':time']).toBe(100); }); - - it('has supported chains consistent with shared', async () => { - const chainIdsAndNames = Object.values(ChainId); - const chainIds = chainIdsAndNames.filter((id) => typeof id === 'number'); - expect(SUPPORTED_CHAIN_IDS).toEqual(chainIds); - }); }); From ef55822f1bf99f132e30b23f83f40b061fc08d52 Mon Sep 17 00:00:00 2001 From: James Campbell Date: Wed, 18 Dec 2024 14:40:45 +0100 Subject: [PATCH 61/75] Use all chains in test Co-authored-by: Derek Pierre --- packages/taco/src/conditions/schemas/rpc.ts | 6 ++---- packages/taco/test/conditions/compound-condition.test.ts | 2 +- packages/taco/test/conditions/sequential.test.ts | 4 ++-- 3 files changed, 5 insertions(+), 7 deletions(-) diff --git a/packages/taco/src/conditions/schemas/rpc.ts b/packages/taco/src/conditions/schemas/rpc.ts index a80a59560..e409c9990 100644 --- a/packages/taco/src/conditions/schemas/rpc.ts +++ b/packages/taco/src/conditions/schemas/rpc.ts @@ -1,9 +1,7 @@ import { BlockIdentifierSchema, EthAddressSchema } from '@nucypher/shared'; import { z } from 'zod'; -import { SUPPORTED_CHAIN_IDS } from '../const'; - -import createUnionSchema, { +import { baseConditionSchema, UserAddressSchema, } from './common'; @@ -27,7 +25,7 @@ const BlockOrContextParamSchema = z.union([ // - web3py: https://web3py.readthedocs.io/en/stable/web3.eth.html#web3.eth.Eth.get_balance export const rpcConditionSchema = baseConditionSchema.extend({ conditionType: z.literal(RpcConditionType).default(RpcConditionType), - chain: createUnionSchema(SUPPORTED_CHAIN_IDS), + chain: z.number().int().nonnegative(), method: z.enum(['eth_getBalance']), parameters: z.union([ // Spec requires 2 parameters: an address and a block identifier diff --git a/packages/taco/test/conditions/compound-condition.test.ts b/packages/taco/test/conditions/compound-condition.test.ts index 90099cd58..96a6448ef 100644 --- a/packages/taco/test/conditions/compound-condition.test.ts +++ b/packages/taco/test/conditions/compound-condition.test.ts @@ -247,7 +247,7 @@ describe('validation', () => { const multichainCondition: CompoundConditionProps = { conditionType: CompoundConditionType, operator: 'and', - operands: [ChainId.SEPOLIA, ChainId.AMOY].map((chain) => ({ + operands: [ChainId.ETHEREUM_MAINNET, ChainId.POLYGON, ChainId.SEPOLIA, ChainId.AMOY].map((chain) => ({ ...testRpcConditionObj, chain, })), diff --git a/packages/taco/test/conditions/sequential.test.ts b/packages/taco/test/conditions/sequential.test.ts index 08ad83971..85c1cdd34 100644 --- a/packages/taco/test/conditions/sequential.test.ts +++ b/packages/taco/test/conditions/sequential.test.ts @@ -1,7 +1,7 @@ +import { ChainId } from '@nucypher/shared'; import { describe, expect, it } from 'vitest'; import { CompoundConditionType } from '../../src/conditions/compound-condition'; -import { SUPPORTED_CHAIN_IDS } from '../../src/conditions/const'; import { ConditionVariableProps, SequentialCondition, @@ -223,7 +223,7 @@ describe('validation', () => { it('accepts on a valid multichain condition schema', () => { const multichainCondition: SequentialConditionProps = { conditionType: SequentialConditionType, - conditionVariables: SUPPORTED_CHAIN_IDS.map((chain) => ({ + conditionVariables: [ChainId.AMOY, ChainId.POLYGON, ChainId.ETHEREUM_MAINNET, ChainId.SEPOLIA].map((chain) => ({ varName: `chain_${chain}`, condition: { ...testRpcConditionObj, From 4f56bd487e585ed896b7399615ef6a096f93f912 Mon Sep 17 00:00:00 2001 From: derekpierre Date: Tue, 17 Dec 2024 16:43:50 -0500 Subject: [PATCH 62/75] Commonize jsonPathSchema so that it can be used by different modules. --- packages/taco/src/conditions/base/json-api.ts | 1 - .../taco/src/conditions/schemas/common.ts | 24 ++++++++++++++++- .../taco/src/conditions/schemas/json-api.ts | 27 ++----------------- .../test/conditions/base/json-path.test.ts | 2 +- 4 files changed, 26 insertions(+), 28 deletions(-) diff --git a/packages/taco/src/conditions/base/json-api.ts b/packages/taco/src/conditions/base/json-api.ts index 0203284c2..c972b5868 100644 --- a/packages/taco/src/conditions/base/json-api.ts +++ b/packages/taco/src/conditions/base/json-api.ts @@ -10,7 +10,6 @@ export { JsonApiConditionProps, jsonApiConditionSchema, JsonApiConditionType, - jsonPathSchema, } from '../schemas/json-api'; export class JsonApiCondition extends Condition { diff --git a/packages/taco/src/conditions/schemas/common.ts b/packages/taco/src/conditions/schemas/common.ts index dba897839..74fd399e6 100644 --- a/packages/taco/src/conditions/schemas/common.ts +++ b/packages/taco/src/conditions/schemas/common.ts @@ -1,10 +1,11 @@ +import { JSONPath } from '@astronautlabs/jsonpath'; import { USER_ADDRESS_PARAM_DEFAULT, USER_ADDRESS_PARAM_EXTERNAL_EIP4361, } from '@nucypher/taco-auth'; import { Primitive, z, ZodLiteral } from 'zod'; -import { CONTEXT_PARAM_PREFIX } from '../const'; +import { CONTEXT_PARAM_PREFIX, CONTEXT_PARAM_REGEXP } from '../const'; // We want to discriminate between ContextParams and plain strings // If a string starts with `:`, it's a ContextParam @@ -55,3 +56,24 @@ function createUnionSchema(values: T) { } export default createUnionSchema; + +const validateJSONPath = (jsonPath: string): boolean => { + // account for embedded context variables + if (CONTEXT_PARAM_REGEXP.test(jsonPath)) { + // skip validation + return true; + } + + try { + JSONPath.parse(jsonPath); + return true; + } catch (error) { + return false; + } +}; + +export const jsonPathSchema = z + .string() + .refine((val) => validateJSONPath(val), { + message: 'Invalid JSONPath expression', + }); diff --git a/packages/taco/src/conditions/schemas/json-api.ts b/packages/taco/src/conditions/schemas/json-api.ts index 626a4c159..ff16623ba 100644 --- a/packages/taco/src/conditions/schemas/json-api.ts +++ b/packages/taco/src/conditions/schemas/json-api.ts @@ -1,35 +1,12 @@ -import { JSONPath } from '@astronautlabs/jsonpath'; import { z } from 'zod'; -import { CONTEXT_PARAM_REGEXP } from '../const'; - +import { baseConditionSchema, jsonPathSchema } from './common'; import { contextParamSchema } from './context'; import { returnValueTestSchema } from './return-value-test'; export const JsonApiConditionType = 'json-api'; -const validateJSONPath = (jsonPath: string): boolean => { - // account for embedded context variables - if (CONTEXT_PARAM_REGEXP.test(jsonPath)) { - // skip validation - return true; - } - - try { - JSONPath.parse(jsonPath); - return true; - } catch (error) { - return false; - } -}; - -export const jsonPathSchema = z - .string() - .refine((val) => validateJSONPath(val), { - message: 'Invalid JSONPath expression', - }); - -export const jsonApiConditionSchema = z.object({ +export const jsonApiConditionSchema = baseConditionSchema.extend({ conditionType: z.literal(JsonApiConditionType).default(JsonApiConditionType), endpoint: z.string().url(), parameters: z.record(z.string(), z.unknown()).optional(), diff --git a/packages/taco/test/conditions/base/json-path.test.ts b/packages/taco/test/conditions/base/json-path.test.ts index affaa1c8a..deac6351b 100644 --- a/packages/taco/test/conditions/base/json-path.test.ts +++ b/packages/taco/test/conditions/base/json-path.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it } from 'vitest'; -import { jsonPathSchema } from '../../../src/conditions/base/json-api'; +import { jsonPathSchema } from '../../../src/conditions/schemas/common'; describe('JSONPath Validation', () => { it('Invalid JSONPath: Incomplete filter expression', () => { From 73092d0502ebe0bc9570e990a14031077ebf1a60 Mon Sep 17 00:00:00 2001 From: derekpierre Date: Tue, 17 Dec 2024 20:40:15 -0500 Subject: [PATCH 63/75] Create separate httpsUrlSchema to validate url provided for json endpoint. --- .../taco/src/conditions/schemas/common.ts | 7 ++++ .../taco/src/conditions/schemas/json-api.ts | 4 +-- .../taco/test/conditions/base/json.test.ts | 36 ++++++++++--------- 3 files changed, 28 insertions(+), 19 deletions(-) diff --git a/packages/taco/src/conditions/schemas/common.ts b/packages/taco/src/conditions/schemas/common.ts index 74fd399e6..21d33841d 100644 --- a/packages/taco/src/conditions/schemas/common.ts +++ b/packages/taco/src/conditions/schemas/common.ts @@ -77,3 +77,10 @@ export const jsonPathSchema = z .refine((val) => validateJSONPath(val), { message: 'Invalid JSONPath expression', }); + +export const httpsURLSchema = z + .string() + .url() + .refine((url) => url.startsWith('https://'), { + message: 'Invalid url - must start with https://', + }); diff --git a/packages/taco/src/conditions/schemas/json-api.ts b/packages/taco/src/conditions/schemas/json-api.ts index ff16623ba..8fa6e7928 100644 --- a/packages/taco/src/conditions/schemas/json-api.ts +++ b/packages/taco/src/conditions/schemas/json-api.ts @@ -1,6 +1,6 @@ import { z } from 'zod'; -import { baseConditionSchema, jsonPathSchema } from './common'; +import { baseConditionSchema, httpsURLSchema, jsonPathSchema } from './common'; import { contextParamSchema } from './context'; import { returnValueTestSchema } from './return-value-test'; @@ -8,7 +8,7 @@ export const JsonApiConditionType = 'json-api'; export const jsonApiConditionSchema = baseConditionSchema.extend({ conditionType: z.literal(JsonApiConditionType).default(JsonApiConditionType), - endpoint: z.string().url(), + endpoint: httpsURLSchema, parameters: z.record(z.string(), z.unknown()).optional(), query: jsonPathSchema.optional(), authorizationToken: contextParamSchema.optional(), diff --git a/packages/taco/test/conditions/base/json.test.ts b/packages/taco/test/conditions/base/json.test.ts index cc04fe31b..de2ad685a 100644 --- a/packages/taco/test/conditions/base/json.test.ts +++ b/packages/taco/test/conditions/base/json.test.ts @@ -20,25 +20,27 @@ describe('JsonApiCondition', () => { expect(result.data).toEqual(testJsonApiConditionObj); }); - it('rejects an invalid schema', () => { - const badJsonApiObj = { - ...testJsonApiConditionObj, - endpoint: 'not-a-url', - }; + it.each(['unsafe-url', 'http://http-url.com'])( + 'rejects an invalid schema', + () => { + const badJsonApiObj = { + ...testJsonApiConditionObj, + endpoint: 'unsafe-url', + }; - const result = JsonApiCondition.validate( - jsonApiConditionSchema, - badJsonApiObj, - ); + const result = JsonApiCondition.validate( + jsonApiConditionSchema, + badJsonApiObj, + ); - expect(result.error).toBeDefined(); - expect(result.data).toBeUndefined(); - expect(result.error?.format()).toMatchObject({ - endpoint: { - _errors: ['Invalid url'], - }, - }); - }); + expect(result.error).toBeDefined(); + expect(result.data).toBeUndefined(); + const errorMessages = result.error?.errors.map((err) => err.message); + expect( + errorMessages?.includes('Invalid url - must start with https://'), + ).toBeTruthy(); + }, + ); describe('authorizationToken', () => { it('accepts context variable', () => { From fcbac07f09b288f16b115ed6d67cc0b73fe96d07 Mon Sep 17 00:00:00 2001 From: derekpierre Date: Tue, 17 Dec 2024 20:47:26 -0500 Subject: [PATCH 64/75] Add initial code for JsonRpcCondition. --- packages/taco/src/conditions/base/index.ts | 1 + packages/taco/src/conditions/base/json-rpc.ts | 22 +++++++++++ .../taco/src/conditions/condition-factory.ts | 7 ++++ .../taco/src/conditions/context/context.ts | 39 ++++++++++++++++++- .../taco/src/conditions/schemas/json-rpc.ts | 22 +++++++++++ packages/taco/src/conditions/schemas/utils.ts | 2 + 6 files changed, 91 insertions(+), 2 deletions(-) create mode 100644 packages/taco/src/conditions/base/json-rpc.ts create mode 100644 packages/taco/src/conditions/schemas/json-rpc.ts diff --git a/packages/taco/src/conditions/base/index.ts b/packages/taco/src/conditions/base/index.ts index 4214fa7d2..f116a5987 100644 --- a/packages/taco/src/conditions/base/index.ts +++ b/packages/taco/src/conditions/base/index.ts @@ -3,5 +3,6 @@ export * as contract from './contract'; export * as jsonApi from './json-api'; +export * as jsonRpc from './json-rpc'; export * as rpc from './rpc'; export * as time from './time'; diff --git a/packages/taco/src/conditions/base/json-rpc.ts b/packages/taco/src/conditions/base/json-rpc.ts new file mode 100644 index 000000000..756e651c0 --- /dev/null +++ b/packages/taco/src/conditions/base/json-rpc.ts @@ -0,0 +1,22 @@ +import { Condition } from '../condition'; +import { + JsonRpcConditionProps, + jsonRpcConditionSchema, + JsonRpcConditionType, +} from '../schemas/json-rpc'; +import { OmitConditionType } from '../shared'; + +export { + JsonRpcConditionProps, + jsonRpcConditionSchema, + JsonRpcConditionType, +} from '../schemas/json-rpc'; + +export class JsonRpcCondition extends Condition { + constructor(value: OmitConditionType) { + super(jsonRpcConditionSchema, { + conditionType: JsonRpcConditionType, + ...value, + }); + } +} diff --git a/packages/taco/src/conditions/condition-factory.ts b/packages/taco/src/conditions/condition-factory.ts index 806dc9816..3295c2438 100644 --- a/packages/taco/src/conditions/condition-factory.ts +++ b/packages/taco/src/conditions/condition-factory.ts @@ -8,6 +8,11 @@ import { JsonApiConditionProps, JsonApiConditionType, } from './base/json-api'; +import { + JsonRpcCondition, + JsonRpcConditionProps, + JsonRpcConditionType, +} from './base/json-rpc'; import { RpcCondition, RpcConditionProps, RpcConditionType } from './base/rpc'; import { TimeCondition, @@ -46,6 +51,8 @@ export class ConditionFactory { return new ContractCondition(props as ContractConditionProps); case JsonApiConditionType: return new JsonApiCondition(props as JsonApiConditionProps); + case JsonRpcConditionType: + return new JsonRpcCondition(props as JsonRpcConditionProps); // Logical Conditions case CompoundConditionType: return new CompoundCondition(props as CompoundConditionProps); diff --git a/packages/taco/src/conditions/context/context.ts b/packages/taco/src/conditions/context/context.ts index 2636a2b84..3fd351435 100644 --- a/packages/taco/src/conditions/context/context.ts +++ b/packages/taco/src/conditions/context/context.ts @@ -20,6 +20,7 @@ import { USER_ADDRESS_PARAMS, } from '../const'; import { JsonApiConditionType } from '../schemas/json-api'; +import { JsonRpcConditionType } from '../schemas/json-rpc'; export type CustomContextParam = string | number | boolean; export type ContextParam = CustomContextParam | AuthSignature; @@ -180,8 +181,11 @@ export class ConditionContext { } } } - // If it's a JSON API condition, check url and query - if (condition.conditionType === JsonApiConditionType) { + // If it's a JSON API/RPC condition, check url and query + if ( + condition.conditionType === JsonApiConditionType || + condition.conditionType == JsonRpcConditionType + ) { const urlComponents = condition.endpoint .replace('https://', '') .split('/'); @@ -204,6 +208,37 @@ export class ConditionContext { } } + if (condition.conditionType == JsonRpcConditionType) { + const methodMatches = condition.method.match(CONTEXT_PARAM_REGEXP); + if (methodMatches) { + for (const match of methodMatches) { + requestedParameters.add(match); + } + } + + if (Array.isArray(condition.params)) { + // params is a dictionary (Record) + condition.params.forEach((paramsEntry: unknown) => { + if ( + typeof paramsEntry === 'string' && + ConditionContext.isContextParameter(paramsEntry) + ) { + requestedParameters.add(paramsEntry); + } + }); + } else { + // params is a dictionary (Record) + for (const [, value] of Object.entries(condition.params)) { + if ( + typeof value === 'string' && + ConditionContext.isContextParameter(value) + ) { + requestedParameters.add(value); + } + } + } + } + return requestedParameters; } diff --git a/packages/taco/src/conditions/schemas/json-rpc.ts b/packages/taco/src/conditions/schemas/json-rpc.ts new file mode 100644 index 000000000..d945f2f5b --- /dev/null +++ b/packages/taco/src/conditions/schemas/json-rpc.ts @@ -0,0 +1,22 @@ +import { z } from 'zod'; + +import { baseConditionSchema, httpsURLSchema, jsonPathSchema } from './common'; +import { contextParamSchema } from './context'; +import { returnValueTestSchema } from './return-value-test'; + +export const JsonRpcConditionType = 'json-rpc'; + +export const jsonRpcConditionSchema = baseConditionSchema.extend({ + conditionType: z.literal(JsonRpcConditionType).default(JsonRpcConditionType), + endpoint: httpsURLSchema, + method: z.string(), + // list or dictionary + params: z + .union([z.array(z.unknown()), z.record(z.string(), z.unknown())]) + .optional(), + query: jsonPathSchema.optional(), + authorizationToken: contextParamSchema.optional(), + returnValueTest: returnValueTestSchema, +}); + +export type JsonRpcConditionProps = z.infer; diff --git a/packages/taco/src/conditions/schemas/utils.ts b/packages/taco/src/conditions/schemas/utils.ts index 4f5e919e5..e9f8f28c2 100644 --- a/packages/taco/src/conditions/schemas/utils.ts +++ b/packages/taco/src/conditions/schemas/utils.ts @@ -5,6 +5,7 @@ import { compoundConditionSchema } from '../compound-condition'; import { contractConditionSchema } from './contract'; import { ifThenElseConditionSchema } from './if-then-else'; import { jsonApiConditionSchema } from './json-api'; +import { jsonRpcConditionSchema } from './json-rpc'; import { rpcConditionSchema } from './rpc'; import { sequentialConditionSchema } from './sequential'; import { timeConditionSchema } from './time'; @@ -16,6 +17,7 @@ export const anyConditionSchema: z.ZodSchema = z.lazy(() => contractConditionSchema, compoundConditionSchema, jsonApiConditionSchema, + jsonRpcConditionSchema, sequentialConditionSchema, ifThenElseConditionSchema, ]), From 4b1744393fc84a2629f9cf65b6e931b63977978a Mon Sep 17 00:00:00 2001 From: derekpierre Date: Wed, 18 Dec 2024 08:30:00 -0500 Subject: [PATCH 65/75] Add tests for JsonRpcCondition. --- .../base/{json.test.ts => json-api.test.ts} | 6 +- .../test/conditions/base/json-rpc.test.ts | 147 ++++++++++++++++++ .../test/conditions/condition-expr.test.ts | 25 +++ packages/taco/test/conditions/lingo.test.ts | 14 +- packages/taco/test/taco.test.ts | 47 ++++++ packages/taco/test/test-utils.ts | 20 ++- 6 files changed, 254 insertions(+), 5 deletions(-) rename packages/taco/test/conditions/base/{json.test.ts => json-api.test.ts} (96%) create mode 100644 packages/taco/test/conditions/base/json-rpc.test.ts diff --git a/packages/taco/test/conditions/base/json.test.ts b/packages/taco/test/conditions/base/json-api.test.ts similarity index 96% rename from packages/taco/test/conditions/base/json.test.ts rename to packages/taco/test/conditions/base/json-api.test.ts index de2ad685a..e388a4947 100644 --- a/packages/taco/test/conditions/base/json.test.ts +++ b/packages/taco/test/conditions/base/json-api.test.ts @@ -3,6 +3,7 @@ import { describe, expect, it } from 'vitest'; import { JsonApiCondition, + JsonApiConditionProps, jsonApiConditionSchema, JsonApiConditionType, } from '../../../src/conditions/base/json-api'; @@ -88,7 +89,7 @@ describe('JsonApiCondition', () => { }); it('accepts conditions without parameters', () => { - const { query, ...noParamsObj } = testJsonApiConditionObj; + const { parameters, ...noParamsObj } = testJsonApiConditionObj; const result = JsonApiCondition.validate( jsonApiConditionSchema, noParamsObj, @@ -101,7 +102,7 @@ describe('JsonApiCondition', () => { describe('context variables', () => { it('allow context variables for various values including as substring', () => { - const jsonApiConditionObj = { + const jsonApiConditionObj: JsonApiConditionProps = { conditionType: JsonApiConditionType, endpoint: 'https://api.coingecko.com/api/:version/simple/:endpointPath', @@ -116,6 +117,7 @@ describe('JsonApiCondition', () => { value: ':expectedPrice', }, }; + const result = JsonApiCondition.validate( jsonApiConditionSchema, jsonApiConditionObj, diff --git a/packages/taco/test/conditions/base/json-rpc.test.ts b/packages/taco/test/conditions/base/json-rpc.test.ts new file mode 100644 index 000000000..3cb9b5525 --- /dev/null +++ b/packages/taco/test/conditions/base/json-rpc.test.ts @@ -0,0 +1,147 @@ +/* eslint-disable @typescript-eslint/no-unused-vars */ +import { describe, expect, it } from 'vitest'; + +import { + JsonRpcCondition, + JsonRpcConditionProps, + jsonRpcConditionSchema, + JsonRpcConditionType, +} from '../../../src/conditions/base/json-rpc'; +import { testJsonRpcConditionObj } from '../../test-utils'; + +describe('JsonRpcCondition', () => { + describe('validation', () => { + it('accepts a valid schema', () => { + const result = JsonRpcCondition.validate( + jsonRpcConditionSchema, + testJsonRpcConditionObj, + ); + expect(result.error).toBeUndefined(); + expect(result.data).toEqual(testJsonRpcConditionObj); + }); + + it.each(['unsafe-url', 'http://http-url.com'])( + 'rejects an invalid schema', + () => { + const badJsonRpcObj = { + ...testJsonRpcConditionObj, + endpoint: 'unsafe-url', + }; + + const result = JsonRpcCondition.validate( + jsonRpcConditionSchema, + badJsonRpcObj, + ); + + expect(result.error).toBeDefined(); + expect(result.data).toBeUndefined(); + const errorMessages = result.error?.errors.map((err) => err.message); + expect( + errorMessages?.includes('Invalid url - must start with https://'), + ).toBeTruthy(); + }, + ); + + describe('authorizationToken', () => { + it('accepts context variable', () => { + const result = JsonRpcCondition.validate(jsonRpcConditionSchema, { + ...testJsonRpcConditionObj, + authorizationToken: ':authToken', + }); + expect(result.error).toBeUndefined(); + expect(result.data).toEqual({ + ...testJsonRpcConditionObj, + authorizationToken: ':authToken', + }); + }); + it.each([ + 'authToken', + 'ABCDEF1234567890', + ':authToken?', + '$:authToken', + ':auth-Token', + ])('rejects invalid context variable', (contextVar) => { + const result = JsonRpcCondition.validate(jsonRpcConditionSchema, { + ...testJsonRpcConditionObj, + authorizationToken: `${contextVar}`, + }); + expect(result.error).toBeDefined(); + expect(result.data).toBeUndefined(); + expect(result.error?.format()).toMatchObject({ + authorizationToken: { + _errors: ['Invalid'], + }, + }); + }); + }); + + describe('properties', () => { + it('accepts conditions without query path', () => { + const { query, ...noQueryObj } = testJsonRpcConditionObj; + const result = JsonRpcCondition.validate( + jsonRpcConditionSchema, + noQueryObj, + ); + + expect(result.error).toBeUndefined(); + expect(result.data).toEqual(noQueryObj); + }); + + it('accepts conditions without params', () => { + const { params, ...noParamsObj } = testJsonRpcConditionObj; + const result = JsonRpcCondition.validate( + jsonRpcConditionSchema, + noParamsObj, + ); + + expect(result.error).toBeUndefined(); + expect(result.data).toEqual(noParamsObj); + }); + + it('accepts conditions with params as dictionary', () => { + const result = JsonRpcCondition.validate(jsonRpcConditionSchema, { + ...testJsonRpcConditionObj, + params: { + value1: 42, + value2: 23, + }, + }); + expect(result.error).toBeUndefined(); + expect(result.data).toEqual({ + ...testJsonRpcConditionObj, + params: { + value1: 42, + value2: 23, + }, + }); + }); + }); + + describe('context variables', () => { + it('allow context variables for various values including as substring', () => { + const testJsonRpcConditionObjWithContextVars: JsonRpcConditionProps = { + conditionType: JsonRpcConditionType, + endpoint: 'https://math.example.com/:version/simple', + method: ':methodContextVar', + params: { + value1: 42, + value2: ':value2', + }, + query: '$.:queryKey', + authorizationToken: ':authToken', + returnValueTest: { + comparator: '==', + value: ':expectedResult', + }, + }; + + const result = JsonRpcCondition.validate( + jsonRpcConditionSchema, + testJsonRpcConditionObjWithContextVars, + ); + expect(result.error).toBeUndefined(); + expect(result.data).toEqual(testJsonRpcConditionObjWithContextVars); + }); + }); + }); +}); diff --git a/packages/taco/test/conditions/condition-expr.test.ts b/packages/taco/test/conditions/condition-expr.test.ts index 66bf48299..3ba7a93f0 100644 --- a/packages/taco/test/conditions/condition-expr.test.ts +++ b/packages/taco/test/conditions/condition-expr.test.ts @@ -10,6 +10,7 @@ import { ContractConditionProps, } from '../../src/conditions/base/contract'; import { JsonApiCondition } from '../../src/conditions/base/json-api'; +import { JsonRpcCondition } from '../../src/conditions/base/json-rpc'; import { RpcCondition, RpcConditionType } from '../../src/conditions/base/rpc'; import { TimeCondition, @@ -22,6 +23,7 @@ import { testContractConditionObj, testFunctionAbi, testJsonApiConditionObj, + testJsonRpcConditionObj, testReturnValueTest, testRpcConditionObj, testTimeConditionObj, @@ -59,6 +61,7 @@ describe('condition set', () => { const rpcCondition = new RpcCondition(testRpcConditionObj); const timeCondition = new TimeCondition(testTimeConditionObj); const jsonApiCondition = new JsonApiCondition(testJsonApiConditionObj); + const jsonRpcCondition = new JsonRpcCondition(testJsonRpcConditionObj); const compoundCondition = new CompoundCondition({ operator: 'and', operands: [ @@ -424,6 +427,28 @@ describe('condition set', () => { expect(conditionExprFromJson.condition).toBeInstanceOf(JsonApiCondition); }); + it('json rpc condition serialization', () => { + const conditionExpr = new ConditionExpression(jsonRpcCondition); + + const conditionExprJson = conditionExpr.toJson(); + expect(conditionExprJson).toBeDefined(); + expect(conditionExprJson).toContain('endpoint'); + expect(conditionExprJson).toContain('https://math.example.com/'); + expect(conditionExprJson).toContain('method'); + expect(conditionExprJson).toContain('subtract'); + expect(conditionExprJson).toContain('params'); + expect(conditionExprJson).toContain('[42,23]'); + + expect(conditionExprJson).toContain('query'); + expect(conditionExprJson).toContain('$.mathresult'); + expect(conditionExprJson).toContain('returnValueTest'); + + const conditionExprFromJson = + ConditionExpression.fromJSON(conditionExprJson); + expect(conditionExprFromJson).toBeDefined(); + expect(conditionExprFromJson.condition).toBeInstanceOf(JsonRpcCondition); + }); + it('compound condition serialization', () => { const conditionExpr = new ConditionExpression(compoundCondition); const compoundConditionObj = compoundCondition.toObj(); diff --git a/packages/taco/test/conditions/lingo.test.ts b/packages/taco/test/conditions/lingo.test.ts index bf3c37713..8b85a06eb 100644 --- a/packages/taco/test/conditions/lingo.test.ts +++ b/packages/taco/test/conditions/lingo.test.ts @@ -58,6 +58,17 @@ describe('check that valid lingo in python is valid in typescript', () => { value: 2, }, }; + const jsonRpcConditionProps = { + conditionType: 'json-rpc', + endpoint: 'https://math.example.com/', + method: 'subtract', + params: [42, 23], + query: '$.value', + returnValueTest: { + comparator: '==', + value: 2, + }, + }; const sequentialConditionProps = { conditionType: 'sequential', conditionVariables: [ @@ -81,7 +92,7 @@ describe('check that valid lingo in python is valid in typescript', () => { }; const ifThenElseConditionProps = { conditionType: 'if-then-else', - ifCondition: rpcConditionProps, + ifCondition: jsonRpcConditionProps, thenCondition: jsonApiConditionProps, elseCondition: timeConditionProps, }; @@ -107,6 +118,7 @@ describe('check that valid lingo in python is valid in typescript', () => { timeConditionProps, contractConditionProps, jsonApiConditionProps, + jsonRpcConditionProps, compoundConditionProps, sequentialConditionProps, ifThenElseConditionProps, diff --git a/packages/taco/test/taco.test.ts b/packages/taco/test/taco.test.ts index 1906c34a2..e35272646 100644 --- a/packages/taco/test/taco.test.ts +++ b/packages/taco/test/taco.test.ts @@ -172,4 +172,51 @@ describe('taco', () => { new Set([':userId', ':userAddress', ':authToken']), ); }); + // test json api condition exposes requested parameters + it('jsonrpc condition exposes requested parameters', async () => { + const mockedDkg = fakeDkgFlow(FerveoVariant.precomputed, 0, 4, 4); + const mockedDkgRitual = fakeDkgRitual(mockedDkg); + const provider = fakeProvider(aliceSecretKeyBytes); + const signer = provider.getSigner(); + const getFinalizedRitualSpy = mockGetActiveRitual(mockedDkgRitual); + + const jsonRpcCondition = new conditions.base.jsonRpc.JsonRpcCondition({ + endpoint: 'https://math.example.com/:version/simple', + method: ':methodContextVar', + params: { + value1: 42, + value2: ':value2', + }, + query: '$.:queryKey', + authorizationToken: ':authToken', + returnValueTest: { + comparator: '==', + value: ':expectedResult', + }, + }); + const messageKit = await taco.encrypt( + provider, + domains.DEVNET, + message, + jsonRpcCondition, + mockedDkg.ritualId, + signer, + ); + expect(getFinalizedRitualSpy).toHaveBeenCalled(); + + const conditionContext = ConditionContext.fromMessageKit(messageKit); + const requestedParameters = conditionContext.requestedContextParameters; + + // Verify all context parameters are detected + expect(requestedParameters).toEqual( + new Set([ + ':version', + ':methodContextVar', + ':value2', + ':queryKey', + ':authToken', + ':expectedResult', + ]), + ); + }); }); diff --git a/packages/taco/test/test-utils.ts b/packages/taco/test/test-utils.ts index 5622d27dc..f1c7b19b1 100644 --- a/packages/taco/test/test-utils.ts +++ b/packages/taco/test/test-utils.ts @@ -39,7 +39,10 @@ import { ContractConditionType, FunctionAbiProps, } from '../src/conditions/base/contract'; -import { JsonApiConditionType } from '../src/conditions/base/json-api'; +import { + JsonApiConditionProps, + JsonApiConditionType, +} from '../src/conditions/base/json-api'; import { RpcConditionProps, RpcConditionType, @@ -55,6 +58,10 @@ import { } from '../src/conditions/compound-condition'; import { ConditionExpression } from '../src/conditions/condition-expr'; import { ERC721Balance } from '../src/conditions/predefined/erc721'; +import { + JsonRpcConditionProps, + JsonRpcConditionType, +} from '../src/conditions/schemas/json-rpc'; import { SequentialConditionProps, SequentialConditionType, @@ -232,7 +239,7 @@ export const testTimeConditionObj: TimeConditionProps = { chain: TEST_CHAIN_ID, }; -export const testJsonApiConditionObj = { +export const testJsonApiConditionObj: JsonApiConditionProps = { conditionType: JsonApiConditionType, endpoint: 'https://_this_would_totally_fail.com', parameters: { @@ -243,6 +250,15 @@ export const testJsonApiConditionObj = { returnValueTest: testReturnValueTest, }; +export const testJsonRpcConditionObj: JsonRpcConditionProps = { + conditionType: JsonRpcConditionType, + endpoint: 'https://math.example.com/', + method: 'subtract', + params: [42, 23], + query: '$.mathresult', + returnValueTest: testReturnValueTest, +}; + export const testRpcConditionObj: RpcConditionProps = { conditionType: RpcConditionType, chain: TEST_CHAIN_ID, From b49a7c1e547cfd97d4e5f4f1764dae2f27bd29b4 Mon Sep 17 00:00:00 2001 From: derekpierre Date: Wed, 18 Dec 2024 16:56:27 -0500 Subject: [PATCH 66/75] Simplify the code to check for context variables within condition properties. The logic is now more general and doesn't require adding special cases every time a new condition is created. --- .../taco/src/conditions/context/context.ts | 133 ++++++------------ 1 file changed, 46 insertions(+), 87 deletions(-) diff --git a/packages/taco/src/conditions/context/context.ts b/packages/taco/src/conditions/context/context.ts index 3fd351435..0dd302d02 100644 --- a/packages/taco/src/conditions/context/context.ts +++ b/packages/taco/src/conditions/context/context.ts @@ -10,7 +10,6 @@ import { } from '@nucypher/taco-auth'; import { CoreConditions, CoreContext } from '../../types'; -import { CompoundConditionType } from '../compound-condition'; import { Condition, ConditionProps } from '../condition'; import { ConditionExpression } from '../condition-expr'; import { @@ -19,8 +18,6 @@ import { CONTEXT_PARAM_REGEXP, USER_ADDRESS_PARAMS, } from '../const'; -import { JsonApiConditionType } from '../schemas/json-api'; -import { JsonRpcConditionType } from '../schemas/json-rpc'; export type CustomContextParam = string | number | boolean; export type ContextParam = CustomContextParam | AuthSignature; @@ -144,100 +141,62 @@ export class ConditionContext { return !!String(param).match(CONTEXT_PARAM_FULL_MATCH_REGEXP); } - private static findContextParameters(condition: ConditionProps) { - // First, we want to find all the parameters we need to add - const requestedParameters = new Set(); - - // Check return value test - if (condition.returnValueTest) { - const rvt = condition.returnValueTest.value; - // Return value test can be a single parameter or an array of parameters - if (Array.isArray(rvt)) { - rvt.forEach((value) => { - if (ConditionContext.isContextParameter(value)) { - requestedParameters.add(value); - } - }); - } else if (ConditionContext.isContextParameter(rvt)) { - requestedParameters.add(rvt); - } else { - // Not a context parameter, we can skip - } - } + private static findContextParameter(value: unknown): Set { + const includedContextVars = new Set(); - // Check condition parameters - for (const param of condition.parameters ?? []) { - if (this.isContextParameter(param)) { - requestedParameters.add(param); - } + // value not set + if (!value) { + return includedContextVars; } - // If it's a compound condition, check operands - if (condition.conditionType === CompoundConditionType) { - for (const key in condition.operands) { - const innerParams = this.findContextParameters(condition.operands[key]); - for (const param of innerParams) { - requestedParameters.add(param); - } - } - } - // If it's a JSON API/RPC condition, check url and query - if ( - condition.conditionType === JsonApiConditionType || - condition.conditionType == JsonRpcConditionType - ) { - const urlComponents = condition.endpoint - .replace('https://', '') - .split('/'); - for (const param of urlComponents ?? []) { - if (this.isContextParameter(param)) { - requestedParameters.add(param); - } - } - if (condition.query) { - const queryParams = condition.query.match(CONTEXT_PARAM_REGEXP); - if (queryParams) { - for (const param of queryParams) { - requestedParameters.add(param); + if (typeof value === 'string') { + if (this.isContextParameter(value)) { + // entire string is context parameter + includedContextVars.add(String(value)); + } else { + // context var could be substring; find all matches + const contextVarMatches = value.match( + // RegExp with 'g' is stateful, so new instance needed every time + new RegExp(CONTEXT_PARAM_REGEXP.source, 'g'), + ); + if (contextVarMatches) { + for (const match of contextVarMatches) { + includedContextVars.add(match); } } } - // always a context variable, so simply check whether defined - if (condition.authorizationToken) { - requestedParameters.add(condition.authorizationToken); + } else if (Array.isArray(value)) { + // array + value.forEach((subValue) => { + const contextVarsForValue = this.findContextParameter(subValue); + contextVarsForValue.forEach((contextVar) => { + includedContextVars.add(contextVar); + }); + }); + } else if (typeof value === 'object') { + // dictionary (Record - complex object eg. Condition, ConditionVariable, ReturnValueTest etc.) + for (const [, entry] of Object.entries(value)) { + const contextVarsForValue = this.findContextParameter(entry); + contextVarsForValue.forEach((contextVar) => { + includedContextVars.add(contextVar); + }); } } - if (condition.conditionType == JsonRpcConditionType) { - const methodMatches = condition.method.match(CONTEXT_PARAM_REGEXP); - if (methodMatches) { - for (const match of methodMatches) { - requestedParameters.add(match); - } - } + return includedContextVars; + } - if (Array.isArray(condition.params)) { - // params is a dictionary (Record) - condition.params.forEach((paramsEntry: unknown) => { - if ( - typeof paramsEntry === 'string' && - ConditionContext.isContextParameter(paramsEntry) - ) { - requestedParameters.add(paramsEntry); - } - }); - } else { - // params is a dictionary (Record) - for (const [, value] of Object.entries(condition.params)) { - if ( - typeof value === 'string' && - ConditionContext.isContextParameter(value) - ) { - requestedParameters.add(value); - } - } - } - } + private static findContextParameters(condition: ConditionProps) { + // find all the context variables we need + const requestedParameters = new Set(); + + // iterate through all properties in ConditionProps + const properties = Object.keys(condition) as (keyof typeof condition)[]; + properties.forEach((prop) => { + this.findContextParameter(condition[prop]).forEach((contextVar) => { + requestedParameters.add(contextVar); + }); + }); return requestedParameters; } From 2280e93e173788856a63fe480a46f0962733f464 Mon Sep 17 00:00:00 2001 From: derekpierre Date: Thu, 19 Dec 2024 10:06:11 -0500 Subject: [PATCH 67/75] Add specific tests for checking whether context variables are extracted appropriately from conditions. --- packages/taco/test/conditions/context.test.ts | 241 ++++++++++++++++++ packages/taco/test/taco.test.ts | 83 ------ 2 files changed, 241 insertions(+), 83 deletions(-) diff --git a/packages/taco/test/conditions/context.test.ts b/packages/taco/test/conditions/context.test.ts index e893766a1..baa0f6bb8 100644 --- a/packages/taco/test/conditions/context.test.ts +++ b/packages/taco/test/conditions/context.test.ts @@ -12,16 +12,21 @@ import { ethers } from 'ethers'; import { beforeAll, describe, expect, it, vi } from 'vitest'; import { toBytes, toHexString } from '../../src'; +import { ConditionFactory } from '../../src/conditions'; import { ContractCondition, ContractConditionProps, + ContractConditionType, } from '../../src/conditions/base/contract'; import { RpcCondition } from '../../src/conditions/base/rpc'; +import { CompoundConditionType } from '../../src/conditions/compound-condition'; import { ConditionContext, CustomContextParam, } from '../../src/conditions/context'; import { RESERVED_CONTEXT_PARAMS } from '../../src/conditions/context/context'; +import { IfThenElseConditionType } from '../../src/conditions/if-then-else-condition'; +import { SequentialConditionType } from '../../src/conditions/sequential'; import { paramOrContextParamSchema, ReturnValueTestProps, @@ -29,8 +34,11 @@ import { import { testContractConditionObj, testFunctionAbi, + testJsonApiConditionObj, + testJsonRpcConditionObj, testReturnValueTest, testRpcConditionObj, + testTimeConditionObj, } from '../test-utils'; describe('context', () => { @@ -621,3 +629,236 @@ describe('param or context param schema', () => { expect(paramOrContextParamSchema.safeParse(() => {}).success).toBe(false); }); }); + +describe('recognition of context variables in conditions', () => { + const rvt = { + comparator: '>=', + value: ':expectedResult', + }; + + const rpcCondition = { + ...testRpcConditionObj, + parameters: [':userAddress', ':blockNumber'], + returnValueTest: rvt, + }; + + const timeCondition = { + ...testTimeConditionObj, + returnValueTest: rvt, + }; + + const contractCondition = { + conditionType: ContractConditionType, + contractAddress: '0x0000000000000000000000000000000000000000', + chain: 1, + method: 'balanceOf', + functionAbi: testFunctionAbi, + parameters: [':userAddress'], + returnValueTest: rvt, + }; + + const jsonApiCondition = { + ...testJsonApiConditionObj, + endpoint: 'https://api.example.com/:userId/:endpoint', + parameters: { + value1: ':value1', + value2: 2, + }, + query: '$.data[?(@.owner == :query)].value', + authorizationToken: ':authToken', + returnValueTest: rvt, + }; + + const jsonRpcConditionParamsDict = { + ...testJsonRpcConditionObj, + endpoint: 'https://math.example.com/:version/simple', + method: 'subtract', + params: { + value1: 42, + value2: ':value2', + }, + query: '$.:queryKey', + authorizationToken: ':authToken', + returnValueTest: rvt, + }; + + const jsonRpcConditionParamsArray = { + ...testJsonRpcConditionObj, + endpoint: 'https://math.example.com/:version/simple', + method: 'subtract', + params: [':value1', ':value2'], + query: '$.:queryKey', + authorizationToken: ':authToken', + returnValueTest: rvt, + }; + + it('handles context params for rpc condition', () => { + const condition = ConditionFactory.conditionFromProps(rpcCondition); + const conditionContext = new ConditionContext(condition); + + // Verify all context parameters are detected + expect(conditionContext.requestedContextParameters).toEqual( + new Set([':userAddress', ':blockNumber', ':expectedResult']), + ); + }); + it('handles context params for time condition', () => { + const condition = ConditionFactory.conditionFromProps(timeCondition); + const conditionContext = new ConditionContext(condition); + + // Verify all context parameters are detected + expect(conditionContext.requestedContextParameters).toEqual( + new Set([':expectedResult']), + ); + }); + it('handles context params for contract condition', () => { + const condition = ConditionFactory.conditionFromProps(contractCondition); + const conditionContext = new ConditionContext(condition); + + // Verify all context parameters are detected + expect(conditionContext.requestedContextParameters).toEqual( + new Set([':userAddress', ':expectedResult']), + ); + }); + it('handles context params for json api condition', () => { + const condition = ConditionFactory.conditionFromProps(jsonApiCondition); + const conditionContext = new ConditionContext(condition); + + // Verify all context parameters are detected + expect(conditionContext.requestedContextParameters).toEqual( + new Set([ + ':userId', + ':endpoint', + ':value1', + ':query', + ':authToken', + ':expectedResult', + ]), + ); + }); + it('handles context params for json rpc condition (params dict)', () => { + const condition = ConditionFactory.conditionFromProps( + jsonRpcConditionParamsDict, + ); + const conditionContext = new ConditionContext(condition); + + // Verify all context parameters are detected + expect(conditionContext.requestedContextParameters).toEqual( + new Set([ + ':version', + ':value2', + ':queryKey', + ':authToken', + ':expectedResult', + ]), + ); + }); + it('handles context params for json rpc condition (params array)', () => { + const condition = ConditionFactory.conditionFromProps( + jsonRpcConditionParamsArray, + ); + const conditionContext = new ConditionContext(condition); + + // Verify all context parameters are detected + expect(conditionContext.requestedContextParameters).toEqual( + new Set([ + ':version', + ':value1', + ':value2', + ':queryKey', + ':authToken', + ':expectedResult', + ]), + ); + }); + it.each([ + { + conditionType: SequentialConditionType, + conditionVariables: [ + { + varName: 'rpc', + condition: rpcCondition, + }, + { + varName: 'time', + condition: timeCondition, + }, + { + varName: 'contract', + condition: contractCondition, + }, + { + varName: 'jsonApi', + condition: jsonApiCondition, + }, + { + varName: 'sequential', + condition: { + conditionType: SequentialConditionType, + conditionVariables: [ + { + varName: 'jsonRpcParamsDict', + condition: jsonRpcConditionParamsDict, + }, + { + varName: 'jsonRpcParamsArray', + condition: jsonRpcConditionParamsArray, + }, + ], + }, + }, + ], + }, + { + conditionType: CompoundConditionType, + operator: 'or', + operands: [ + jsonApiCondition, + jsonRpcConditionParamsDict, + { + conditionType: CompoundConditionType, + operator: 'and', + operands: [jsonRpcConditionParamsArray, rpcCondition, timeCondition], + }, + { + conditionType: CompoundConditionType, + operator: 'not', + operands: [contractCondition], + }, + ], + }, + { + conditionType: IfThenElseConditionType, + ifCondition: rpcCondition, + thenCondition: jsonRpcConditionParamsArray, + elseCondition: { + conditionType: CompoundConditionType, + operator: 'and', + operands: [ + timeCondition, + contractCondition, + jsonApiCondition, + jsonRpcConditionParamsDict, + ], + }, + }, + ])('handles context params for logical conditions', (logicalCondition) => { + const condition = ConditionFactory.conditionFromProps(logicalCondition); + const conditionContext = new ConditionContext(condition); + // Verify all context parameters are detected + expect(conditionContext.requestedContextParameters).toEqual( + new Set([ + ':version', + ':userAddress', + ':blockNumber', + ':userId', + ':endpoint', + ':value1', + ':value2', + ':query', + ':queryKey', + ':authToken', + ':expectedResult', + ]), + ); + }); +}); diff --git a/packages/taco/test/taco.test.ts b/packages/taco/test/taco.test.ts index e35272646..5f8b59f3d 100644 --- a/packages/taco/test/taco.test.ts +++ b/packages/taco/test/taco.test.ts @@ -136,87 +136,4 @@ describe('taco', () => { new Set([customParamKey, USER_ADDRESS_PARAM_DEFAULT]), ); }); - // test json api condition exposes requested parameters - it('jsonapi condition exposes requested parameters', async () => { - const mockedDkg = fakeDkgFlow(FerveoVariant.precomputed, 0, 4, 4); - const mockedDkgRitual = fakeDkgRitual(mockedDkg); - const provider = fakeProvider(aliceSecretKeyBytes); - const signer = provider.getSigner(); - const getFinalizedRitualSpy = mockGetActiveRitual(mockedDkgRitual); - - const jsonApiCondition = new conditions.base.jsonApi.JsonApiCondition({ - endpoint: 'https://api.example.com/:userId/data', - query: '$.data[?(@.owner == :userAddress)].value', - authorizationToken: ':authToken', - returnValueTest: { - comparator: '==', - value: true, - }, - }); - - const messageKit = await taco.encrypt( - provider, - domains.DEVNET, - message, - jsonApiCondition, - mockedDkg.ritualId, - signer, - ); - expect(getFinalizedRitualSpy).toHaveBeenCalled(); - - const conditionContext = ConditionContext.fromMessageKit(messageKit); - const requestedParameters = conditionContext.requestedContextParameters; - - // Verify all context parameters from endpoint, query and authToken are detected - expect(requestedParameters).toEqual( - new Set([':userId', ':userAddress', ':authToken']), - ); - }); - // test json api condition exposes requested parameters - it('jsonrpc condition exposes requested parameters', async () => { - const mockedDkg = fakeDkgFlow(FerveoVariant.precomputed, 0, 4, 4); - const mockedDkgRitual = fakeDkgRitual(mockedDkg); - const provider = fakeProvider(aliceSecretKeyBytes); - const signer = provider.getSigner(); - const getFinalizedRitualSpy = mockGetActiveRitual(mockedDkgRitual); - - const jsonRpcCondition = new conditions.base.jsonRpc.JsonRpcCondition({ - endpoint: 'https://math.example.com/:version/simple', - method: ':methodContextVar', - params: { - value1: 42, - value2: ':value2', - }, - query: '$.:queryKey', - authorizationToken: ':authToken', - returnValueTest: { - comparator: '==', - value: ':expectedResult', - }, - }); - const messageKit = await taco.encrypt( - provider, - domains.DEVNET, - message, - jsonRpcCondition, - mockedDkg.ritualId, - signer, - ); - expect(getFinalizedRitualSpy).toHaveBeenCalled(); - - const conditionContext = ConditionContext.fromMessageKit(messageKit); - const requestedParameters = conditionContext.requestedContextParameters; - - // Verify all context parameters are detected - expect(requestedParameters).toEqual( - new Set([ - ':version', - ':methodContextVar', - ':value2', - ':queryKey', - ':authToken', - ':expectedResult', - ]), - ); - }); }); From cfd8c7317c544536e8d09213224e7c7e26dd493d Mon Sep 17 00:00:00 2001 From: derekpierre Date: Thu, 19 Dec 2024 12:00:20 -0500 Subject: [PATCH 68/75] Fix linting. --- packages/taco/src/conditions/schemas/rpc.ts | 5 +---- packages/taco/test/conditions/base/condition.test.ts | 4 +--- packages/taco/test/conditions/compound-condition.test.ts | 7 ++++++- packages/taco/test/conditions/sequential.test.ts | 7 ++++++- 4 files changed, 14 insertions(+), 9 deletions(-) diff --git a/packages/taco/src/conditions/schemas/rpc.ts b/packages/taco/src/conditions/schemas/rpc.ts index e409c9990..caac5c781 100644 --- a/packages/taco/src/conditions/schemas/rpc.ts +++ b/packages/taco/src/conditions/schemas/rpc.ts @@ -1,10 +1,7 @@ import { BlockIdentifierSchema, EthAddressSchema } from '@nucypher/shared'; import { z } from 'zod'; -import { - baseConditionSchema, - UserAddressSchema, -} from './common'; +import { baseConditionSchema, UserAddressSchema } from './common'; import { contextParamSchema } from './context'; import { returnValueTestSchema } from './return-value-test'; diff --git a/packages/taco/test/conditions/base/condition.test.ts b/packages/taco/test/conditions/base/condition.test.ts index e1473308c..a45bac133 100644 --- a/packages/taco/test/conditions/base/condition.test.ts +++ b/packages/taco/test/conditions/base/condition.test.ts @@ -40,9 +40,7 @@ describe('validation', () => { expect(result.data).toBeUndefined(); expect(result.error?.format()).toMatchObject({ chain: { - _errors: [ - "Expected number, received string", - ], + _errors: ['Expected number, received string'], }, }); }); diff --git a/packages/taco/test/conditions/compound-condition.test.ts b/packages/taco/test/conditions/compound-condition.test.ts index 96a6448ef..dabd1e3f2 100644 --- a/packages/taco/test/conditions/compound-condition.test.ts +++ b/packages/taco/test/conditions/compound-condition.test.ts @@ -247,7 +247,12 @@ describe('validation', () => { const multichainCondition: CompoundConditionProps = { conditionType: CompoundConditionType, operator: 'and', - operands: [ChainId.ETHEREUM_MAINNET, ChainId.POLYGON, ChainId.SEPOLIA, ChainId.AMOY].map((chain) => ({ + operands: [ + ChainId.ETHEREUM_MAINNET, + ChainId.POLYGON, + ChainId.SEPOLIA, + ChainId.AMOY, + ].map((chain) => ({ ...testRpcConditionObj, chain, })), diff --git a/packages/taco/test/conditions/sequential.test.ts b/packages/taco/test/conditions/sequential.test.ts index 85c1cdd34..0c08b2b95 100644 --- a/packages/taco/test/conditions/sequential.test.ts +++ b/packages/taco/test/conditions/sequential.test.ts @@ -223,7 +223,12 @@ describe('validation', () => { it('accepts on a valid multichain condition schema', () => { const multichainCondition: SequentialConditionProps = { conditionType: SequentialConditionType, - conditionVariables: [ChainId.AMOY, ChainId.POLYGON, ChainId.ETHEREUM_MAINNET, ChainId.SEPOLIA].map((chain) => ({ + conditionVariables: [ + ChainId.AMOY, + ChainId.POLYGON, + ChainId.ETHEREUM_MAINNET, + ChainId.SEPOLIA, + ].map((chain) => ({ varName: `chain_${chain}`, condition: { ...testRpcConditionObj, From 75e1a5087ce528c9a9f9baabaceb943602c88ee3 Mon Sep 17 00:00:00 2001 From: derekpierre Date: Mon, 6 Jan 2025 09:12:20 -0500 Subject: [PATCH 69/75] Perform our own https URL schema refinement check due to bug in zod. Update tests to validate refinement. --- .../taco/src/conditions/schemas/common.ts | 9 ++++- .../test/conditions/base/json-api.test.ts | 38 +++++++++---------- .../test/conditions/base/json-rpc.test.ts | 38 +++++++++---------- 3 files changed, 45 insertions(+), 40 deletions(-) diff --git a/packages/taco/src/conditions/schemas/common.ts b/packages/taco/src/conditions/schemas/common.ts index 21d33841d..182148a86 100644 --- a/packages/taco/src/conditions/schemas/common.ts +++ b/packages/taco/src/conditions/schemas/common.ts @@ -78,9 +78,14 @@ export const jsonPathSchema = z message: 'Invalid JSONPath expression', }); +const validateHttpsURL = (url: string): boolean => { + return URL.canParse(url) && url.startsWith('https://'); +}; + +// Use our own URL refinement check due to https://github.com/colinhacks/zod/issues/2236 export const httpsURLSchema = z .string() .url() - .refine((url) => url.startsWith('https://'), { - message: 'Invalid url - must start with https://', + .refine((url) => validateHttpsURL(url), { + message: 'Invalid URL', }); diff --git a/packages/taco/test/conditions/base/json-api.test.ts b/packages/taco/test/conditions/base/json-api.test.ts index e388a4947..a7fc9165b 100644 --- a/packages/taco/test/conditions/base/json-api.test.ts +++ b/packages/taco/test/conditions/base/json-api.test.ts @@ -21,27 +21,27 @@ describe('JsonApiCondition', () => { expect(result.data).toEqual(testJsonApiConditionObj); }); - it.each(['unsafe-url', 'http://http-url.com'])( - 'rejects an invalid schema', - () => { - const badJsonApiObj = { - ...testJsonApiConditionObj, - endpoint: 'unsafe-url', - }; + it.each([ + 'unsafe-url', + 'http://http-url.com', + 'mailto://mail@mailserver.org', + 'https://', + ])('rejects an invalid schema', (badUrl) => { + const badJsonApiObj = { + ...testJsonApiConditionObj, + endpoint: badUrl, + }; - const result = JsonApiCondition.validate( - jsonApiConditionSchema, - badJsonApiObj, - ); + const result = JsonApiCondition.validate( + jsonApiConditionSchema, + badJsonApiObj, + ); - expect(result.error).toBeDefined(); - expect(result.data).toBeUndefined(); - const errorMessages = result.error?.errors.map((err) => err.message); - expect( - errorMessages?.includes('Invalid url - must start with https://'), - ).toBeTruthy(); - }, - ); + expect(result.error).toBeDefined(); + expect(result.data).toBeUndefined(); + const errorMessages = result.error?.errors.map((err) => err.message); + expect(errorMessages?.includes('Invalid URL')).toBeTruthy(); + }); describe('authorizationToken', () => { it('accepts context variable', () => { diff --git a/packages/taco/test/conditions/base/json-rpc.test.ts b/packages/taco/test/conditions/base/json-rpc.test.ts index 3cb9b5525..fb1656545 100644 --- a/packages/taco/test/conditions/base/json-rpc.test.ts +++ b/packages/taco/test/conditions/base/json-rpc.test.ts @@ -20,27 +20,27 @@ describe('JsonRpcCondition', () => { expect(result.data).toEqual(testJsonRpcConditionObj); }); - it.each(['unsafe-url', 'http://http-url.com'])( - 'rejects an invalid schema', - () => { - const badJsonRpcObj = { - ...testJsonRpcConditionObj, - endpoint: 'unsafe-url', - }; + it.each([ + 'unsafe-url', + 'http://http-url.com', + 'mailto://mail@freecodecamp.org', + 'https://', + ])('rejects an invalid schema', (badUrl) => { + const badJsonRpcObj = { + ...testJsonRpcConditionObj, + endpoint: badUrl, + }; - const result = JsonRpcCondition.validate( - jsonRpcConditionSchema, - badJsonRpcObj, - ); + const result = JsonRpcCondition.validate( + jsonRpcConditionSchema, + badJsonRpcObj, + ); - expect(result.error).toBeDefined(); - expect(result.data).toBeUndefined(); - const errorMessages = result.error?.errors.map((err) => err.message); - expect( - errorMessages?.includes('Invalid url - must start with https://'), - ).toBeTruthy(); - }, - ); + expect(result.error).toBeDefined(); + expect(result.data).toBeUndefined(); + const errorMessages = result.error?.errors.map((err) => err.message); + expect(errorMessages?.includes('Invalid URL')).toBeTruthy(); + }); describe('authorizationToken', () => { it('accepts context variable', () => { From b795b3c2b53755eb50b35a3beebf23d2d67fecd1 Mon Sep 17 00:00:00 2001 From: Derek Pierre Date: Tue, 7 Jan 2025 10:55:00 -0500 Subject: [PATCH 70/75] Update test URL string --- packages/taco/test/conditions/base/json-rpc.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/taco/test/conditions/base/json-rpc.test.ts b/packages/taco/test/conditions/base/json-rpc.test.ts index fb1656545..b54244160 100644 --- a/packages/taco/test/conditions/base/json-rpc.test.ts +++ b/packages/taco/test/conditions/base/json-rpc.test.ts @@ -23,7 +23,7 @@ describe('JsonRpcCondition', () => { it.each([ 'unsafe-url', 'http://http-url.com', - 'mailto://mail@freecodecamp.org', + 'mailto://mail@mailserver.org', 'https://', ])('rejects an invalid schema', (badUrl) => { const badJsonRpcObj = { From fb055796d3c7107e737f16fec23601d3ad646418 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20N=C3=BA=C3=B1ez?= Date: Fri, 10 Jan 2025 13:14:46 +0100 Subject: [PATCH 71/75] Update links to docs.taco.build --- README.md | 4 ++-- demos/taco-demo/README.md | 2 +- demos/taco-demo/src/App.tsx | 2 +- demos/taco-nft-demo/README.md | 2 +- demos/taco-nft-demo/src/App.tsx | 2 +- examples/taco/nextjs/README.md | 2 +- examples/taco/nodejs/README.md | 2 +- examples/taco/react/README.md | 2 +- examples/taco/webpack-5/README.md | 2 +- packages/taco/README.md | 4 ++-- 10 files changed, 12 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index 87a34a5f5..25123ac53 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ A TypeScript library for interacting with access control functionality in the browser. -Full documentation can be found [here](https://docs.threshold.network/app-development/threshold-access-control-tac). +Full documentation can be found [here](https://docs.taco.build/). > **Warning** > @@ -19,7 +19,7 @@ pnpm add @nucypher/taco ## Tutorial To learn more, follow the tutorial at Threshold -Network's [docs](https://docs.threshold.network/app-development/threshold-access-control-tac/get-started-with-tac). +Network's [docs](https://docs.taco.build/taco-integration/). ## Examples diff --git a/demos/taco-demo/README.md b/demos/taco-demo/README.md index b60d76a57..0765b3d7d 100644 --- a/demos/taco-demo/README.md +++ b/demos/taco-demo/README.md @@ -19,7 +19,7 @@ pnpm start ## References Please find developer documentation -[here](https://docs.threshold.network/app-development/threshold-access-control-tac). +[here](https://docs.taco.build/). This dApp is based on [`useDapp` example](https://github.com/EthWorks/useDapp/tree/master/packages/example). diff --git a/demos/taco-demo/src/App.tsx b/demos/taco-demo/src/App.tsx index 649f46ced..a21aa1491 100644 --- a/demos/taco-demo/src/App.tsx +++ b/demos/taco-demo/src/App.tsx @@ -139,7 +139,7 @@ export default function App() {

In production (mainnet domain), your wallet address (encryptor) will also have to be allow-listed for this specific ritual. However, we have - publicly available testnet rituals + publicly available testnet rituals for use when developing your apps.

diff --git a/demos/taco-nft-demo/README.md b/demos/taco-nft-demo/README.md index a95b19ac1..f2242d7d7 100644 --- a/demos/taco-nft-demo/README.md +++ b/demos/taco-nft-demo/README.md @@ -19,7 +19,7 @@ pnpm start ## References Please find developer documentation -[here](https://docs.threshold.network/app-development/threshold-access-control-tac). +[here](https://docs.taco.build/). This dApp is based on [`useDapp` example](https://github.com/EthWorks/useDapp/tree/master/packages/example). diff --git a/demos/taco-nft-demo/src/App.tsx b/demos/taco-nft-demo/src/App.tsx index 331466306..80900fae3 100644 --- a/demos/taco-nft-demo/src/App.tsx +++ b/demos/taco-nft-demo/src/App.tsx @@ -129,7 +129,7 @@ export default function App() {

In production (mainnet domain), your wallet address (encryptor) will also have to be allow-listed for this specific ritual. However, we have - publicly available testnet rituals + publicly available testnet rituals for use when developing your apps.

diff --git a/examples/taco/nextjs/README.md b/examples/taco/nextjs/README.md index 7be041f25..34b138f03 100644 --- a/examples/taco/nextjs/README.md +++ b/examples/taco/nextjs/README.md @@ -28,7 +28,7 @@ automatically optimize and load Inter, a custom Google Font. ## Learn More Please find developer documentation for -TACo [here](https://docs.threshold.network/app-development/threshold-access-control-tac). +TACo [here](https://docs.taco.build/). To learn more about Next.js, take a look at the following resources: diff --git a/examples/taco/nodejs/README.md b/examples/taco/nodejs/README.md index bccebb083..e2147f762 100644 --- a/examples/taco/nodejs/README.md +++ b/examples/taco/nodejs/README.md @@ -30,4 +30,4 @@ pnpm start ## Learn more Please find developer documentation for -TACo [here](https://docs.threshold.network/app-development/threshold-access-control-tac). +TACo [here](https://docs.taco.build/). diff --git a/examples/taco/react/README.md b/examples/taco/react/README.md index 38929abcf..e7c7f3de0 100644 --- a/examples/taco/react/README.md +++ b/examples/taco/react/README.md @@ -14,4 +14,4 @@ inspect the UI and the JS console. ## Learn more Please find developer documentation for -TACo [here](https://docs.threshold.network/app-development/threshold-access-control-tac). +TACo [here](https://docs.taco.build/). diff --git a/examples/taco/webpack-5/README.md b/examples/taco/webpack-5/README.md index 70e66e7db..23bc7645d 100644 --- a/examples/taco/webpack-5/README.md +++ b/examples/taco/webpack-5/README.md @@ -14,4 +14,4 @@ Go to [localhost:8080](http://localhost:8080/) in your browser and look in the J ## Learn more Please find developer documentation for -TACo [here](https://docs.threshold.network/app-development/threshold-access-control-tac). +TACo [here](https://docs.taco.build/). diff --git a/packages/taco/README.md b/packages/taco/README.md index 4ec9b2724..0fd7afc6f 100644 --- a/packages/taco/README.md +++ b/packages/taco/README.md @@ -6,7 +6,7 @@ To use `taco`, you need to connect with a proper network: `mainnet`, `testnet`, or `devnet`. You can find a proper version for each network in the [npmjs.com package tags](https://www.npmjs.com/package/@nucypher/taco?activeTab=versions). -Visit [our documentation](https://docs.threshold.network/app-development/threshold-access-control-tac/integration-guide#id-0.-pick-an-appropriate-taco-version) to learn more. +Visit [our documentation](https://docs.taco.build/taco-integration/) to learn more. ## Usage @@ -67,4 +67,4 @@ const decryptedMessage = await decrypt( ## Learn more Please find developer documentation for -TACo [here](https://docs.threshold.network/app-development/threshold-access-control-tac). +TACo [here](https://docs.taco.build/). From 8dbe7ca6c07714d5b7af40fb389f8211ae58fc5b Mon Sep 17 00:00:00 2001 From: derekpierre Date: Fri, 17 Jan 2025 15:05:40 -0500 Subject: [PATCH 72/75] Fix linting error. --- examples/pre/nextjs/next-env.d.ts | 2 +- examples/taco/nextjs/next-env.d.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/pre/nextjs/next-env.d.ts b/examples/pre/nextjs/next-env.d.ts index 4f11a03dc..40c3d6809 100644 --- a/examples/pre/nextjs/next-env.d.ts +++ b/examples/pre/nextjs/next-env.d.ts @@ -2,4 +2,4 @@ /// // NOTE: This file should not be edited -// see https://nextjs.org/docs/basic-features/typescript for more information. +// see https://nextjs.org/docs/app/building-your-application/configuring/typescript for more information. diff --git a/examples/taco/nextjs/next-env.d.ts b/examples/taco/nextjs/next-env.d.ts index 4f11a03dc..40c3d6809 100644 --- a/examples/taco/nextjs/next-env.d.ts +++ b/examples/taco/nextjs/next-env.d.ts @@ -2,4 +2,4 @@ /// // NOTE: This file should not be edited -// see https://nextjs.org/docs/basic-features/typescript for more information. +// see https://nextjs.org/docs/app/building-your-application/configuring/typescript for more information. From 3132952b42a774d55110dae3c1ab120ab124fddc Mon Sep 17 00:00:00 2001 From: derekpierre Date: Fri, 17 Jan 2025 15:28:38 -0500 Subject: [PATCH 73/75] chore(release): release @nucypher/shared:0.5.0-alpha.2 --- packages/shared/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/shared/package.json b/packages/shared/package.json index d7498aa6c..da78bd903 100644 --- a/packages/shared/package.json +++ b/packages/shared/package.json @@ -1,6 +1,6 @@ { "name": "@nucypher/shared", - "version": "0.5.0-alpha.1", + "version": "0.5.0-alpha.2", "keywords": [ "pre", "taco", From 3e35d9682a8ac69dce08d4eb1bd209488afd5793 Mon Sep 17 00:00:00 2001 From: derekpierre Date: Fri, 17 Jan 2025 15:31:38 -0500 Subject: [PATCH 74/75] chore(release): release @nucypher/taco-auth:0.3.0-alpha.2 --- packages/taco-auth/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/taco-auth/package.json b/packages/taco-auth/package.json index 20e8d195d..0fdb5ec6b 100644 --- a/packages/taco-auth/package.json +++ b/packages/taco-auth/package.json @@ -1,6 +1,6 @@ { "name": "@nucypher/taco-auth", - "version": "0.3.0-alpha.1", + "version": "0.3.0-alpha.2", "keywords": [ "pre", "threshold", From c95e69d39456bb5334780f2a5a4fa8207186c307 Mon Sep 17 00:00:00 2001 From: derekpierre Date: Fri, 17 Jan 2025 15:33:07 -0500 Subject: [PATCH 75/75] chore(release): release @nucypher/taco:0.6.0-alpha.2 --- packages/taco/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/taco/package.json b/packages/taco/package.json index a3ee5f618..b0c5c1576 100644 --- a/packages/taco/package.json +++ b/packages/taco/package.json @@ -1,6 +1,6 @@ { "name": "@nucypher/taco", - "version": "0.6.0-alpha.1", + "version": "0.6.0-alpha.2", "keywords": [ "taco", "threshold",