diff --git a/packages/taco/src/conditions/base/index.ts b/packages/taco/src/conditions/base/index.ts index 4214fa7d..f116a598 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-api.ts b/packages/taco/src/conditions/base/json-api.ts index 0203284c..c972b586 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/base/json-rpc.ts b/packages/taco/src/conditions/base/json-rpc.ts new file mode 100644 index 00000000..756e651c --- /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 806dc981..3295c243 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 2636a2b8..0dd302d0 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,7 +18,6 @@ import { CONTEXT_PARAM_REGEXP, USER_ADDRESS_PARAMS, } from '../const'; -import { JsonApiConditionType } from '../schemas/json-api'; export type CustomContextParam = string | number | boolean; export type ContextParam = CustomContextParam | AuthSignature; @@ -143,67 +141,63 @@ 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 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(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); + }); } } + return includedContextVars; + } + + 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; } diff --git a/packages/taco/src/conditions/schemas/common.ts b/packages/taco/src/conditions/schemas/common.ts index dba89783..182148a8 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,36 @@ 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', + }); + +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) => validateHttpsURL(url), { + message: 'Invalid URL', + }); diff --git a/packages/taco/src/conditions/schemas/json-api.ts b/packages/taco/src/conditions/schemas/json-api.ts index 626a4c15..8fa6e792 100644 --- a/packages/taco/src/conditions/schemas/json-api.ts +++ b/packages/taco/src/conditions/schemas/json-api.ts @@ -1,37 +1,14 @@ -import { JSONPath } from '@astronautlabs/jsonpath'; import { z } from 'zod'; -import { CONTEXT_PARAM_REGEXP } from '../const'; - +import { baseConditionSchema, httpsURLSchema, 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(), + endpoint: httpsURLSchema, parameters: z.record(z.string(), z.unknown()).optional(), query: jsonPathSchema.optional(), authorizationToken: contextParamSchema.optional(), 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 00000000..d945f2f5 --- /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/rpc.ts b/packages/taco/src/conditions/schemas/rpc.ts index e409c999..caac5c78 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/src/conditions/schemas/utils.ts b/packages/taco/src/conditions/schemas/utils.ts index 4f5e919e..e9f8f28c 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, ]), diff --git a/packages/taco/test/conditions/base/condition.test.ts b/packages/taco/test/conditions/base/condition.test.ts index e1473308..a45bac13 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/base/json.test.ts b/packages/taco/test/conditions/base/json-api.test.ts similarity index 87% rename from packages/taco/test/conditions/base/json.test.ts rename to packages/taco/test/conditions/base/json-api.test.ts index cc04fe31..a7fc9165 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'; @@ -20,10 +21,15 @@ describe('JsonApiCondition', () => { expect(result.data).toEqual(testJsonApiConditionObj); }); - it('rejects an invalid schema', () => { + it.each([ + 'unsafe-url', + 'http://http-url.com', + 'mailto://mail@mailserver.org', + 'https://', + ])('rejects an invalid schema', (badUrl) => { const badJsonApiObj = { ...testJsonApiConditionObj, - endpoint: 'not-a-url', + endpoint: badUrl, }; const result = JsonApiCondition.validate( @@ -33,11 +39,8 @@ describe('JsonApiCondition', () => { expect(result.error).toBeDefined(); expect(result.data).toBeUndefined(); - expect(result.error?.format()).toMatchObject({ - endpoint: { - _errors: ['Invalid url'], - }, - }); + const errorMessages = result.error?.errors.map((err) => err.message); + expect(errorMessages?.includes('Invalid URL')).toBeTruthy(); }); describe('authorizationToken', () => { @@ -86,7 +89,7 @@ describe('JsonApiCondition', () => { }); it('accepts conditions without parameters', () => { - const { query, ...noParamsObj } = testJsonApiConditionObj; + const { parameters, ...noParamsObj } = testJsonApiConditionObj; const result = JsonApiCondition.validate( jsonApiConditionSchema, noParamsObj, @@ -99,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', @@ -114,6 +117,7 @@ describe('JsonApiCondition', () => { value: ':expectedPrice', }, }; + const result = JsonApiCondition.validate( jsonApiConditionSchema, jsonApiConditionObj, diff --git a/packages/taco/test/conditions/base/json-path.test.ts b/packages/taco/test/conditions/base/json-path.test.ts index affaa1c8..deac6351 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', () => { 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 00000000..fb165654 --- /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', + 'mailto://mail@freecodecamp.org', + 'https://', + ])('rejects an invalid schema', (badUrl) => { + const badJsonRpcObj = { + ...testJsonRpcConditionObj, + endpoint: badUrl, + }; + + 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')).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/compound-condition.test.ts b/packages/taco/test/conditions/compound-condition.test.ts index 96a6448e..dabd1e3f 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/condition-expr.test.ts b/packages/taco/test/conditions/condition-expr.test.ts index 66bf4829..3ba7a93f 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/context.test.ts b/packages/taco/test/conditions/context.test.ts index e893766a..baa0f6bb 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/conditions/lingo.test.ts b/packages/taco/test/conditions/lingo.test.ts index bf3c3771..8b85a06e 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/conditions/sequential.test.ts b/packages/taco/test/conditions/sequential.test.ts index 85c1cdd3..0c08b2b9 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, diff --git a/packages/taco/test/taco.test.ts b/packages/taco/test/taco.test.ts index 1906c34a..5f8b59f3 100644 --- a/packages/taco/test/taco.test.ts +++ b/packages/taco/test/taco.test.ts @@ -136,40 +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']), - ); - }); }); diff --git a/packages/taco/test/test-utils.ts b/packages/taco/test/test-utils.ts index 5622d27d..f1c7b19b 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,