diff --git a/src/conditions/base/contract.ts b/src/conditions/base/contract.ts new file mode 100644 index 000000000..8485227bc --- /dev/null +++ b/src/conditions/base/contract.ts @@ -0,0 +1,24 @@ +import Joi from 'joi'; + +import { ETH_ADDRESS_REGEXP } from '../const'; + +import { RpcCondition, rpcConditionSchema } from './rpc'; + +export const STANDARD_CONTRACT_TYPES = ['ERC20', 'ERC721']; + +const contractMethodSchemas: Record = { + ...rpcConditionSchema, + contractAddress: Joi.string().pattern(ETH_ADDRESS_REGEXP).required(), + standardContractType: Joi.string() + .valid(...STANDARD_CONTRACT_TYPES) + .optional(), + method: Joi.string().required(), + functionAbi: Joi.object().optional(), + parameters: Joi.array().required(), +}; + +export class ContractCondition extends RpcCondition { + public readonly schema = Joi.object(contractMethodSchemas) + // At most one of these keys needs to be present + .xor('standardContractType', 'functionAbi'); +} diff --git a/src/conditions/base/evm.ts b/src/conditions/base/evm.ts deleted file mode 100644 index 604817948..000000000 --- a/src/conditions/base/evm.ts +++ /dev/null @@ -1,26 +0,0 @@ -import Joi from 'joi'; - -import { ETH_ADDRESS_REGEXP, SUPPORTED_CHAINS } from '../const'; - -import { Condition } from './condition'; -import { returnValueTestSchema } from './schema'; - -export const STANDARD_CONTRACT_TYPES = ['ERC20', 'ERC721']; - -export class EvmCondition extends Condition { - public readonly schema = Joi.object({ - contractAddress: Joi.string().pattern(ETH_ADDRESS_REGEXP).required(), - chain: Joi.number() - .valid(...SUPPORTED_CHAINS) - .required(), - standardContractType: Joi.string() - .valid(...STANDARD_CONTRACT_TYPES) - .optional(), - functionAbi: Joi.object().optional(), - method: Joi.string().required(), - parameters: Joi.array().required(), - returnValueTest: returnValueTestSchema, - }) - // At most one of these keys needs to be present - .xor('standardContractType', 'functionAbi'); -} diff --git a/src/conditions/base/index.ts b/src/conditions/base/index.ts index dc2fcf522..805717e01 100644 --- a/src/conditions/base/index.ts +++ b/src/conditions/base/index.ts @@ -1,4 +1,4 @@ export { Condition } from './condition'; -export { EvmCondition } from './evm'; +export { ContractCondition } from './contract'; export { RpcCondition } from './rpc'; export { TimeCondition } from './time'; diff --git a/src/conditions/base/rpc.ts b/src/conditions/base/rpc.ts index 5144c251f..4a088817d 100644 --- a/src/conditions/base/rpc.ts +++ b/src/conditions/base/rpc.ts @@ -18,15 +18,17 @@ const makeParameters = () => })), }); +export const rpcConditionSchema = { + chain: Joi.number() + .valid(...SUPPORTED_CHAINS) + .required(), + method: Joi.string() + .valid(...Object.keys(rpcMethodSchemas)) + .required(), + parameters: makeParameters(), + returnValueTest: returnValueTestSchema.required(), +}; + export class RpcCondition extends Condition { - public readonly schema = Joi.object({ - chain: Joi.number() - .valid(...SUPPORTED_CHAINS) - .required(), - method: Joi.string() - .valid(...Object.keys(rpcMethodSchemas)) - .required(), - parameters: makeParameters(), - returnValueTest: returnValueTestSchema.required(), - }); + public readonly schema = Joi.object(rpcConditionSchema); } diff --git a/src/conditions/base/time.ts b/src/conditions/base/time.ts index 78fa28795..8b136e4a7 100644 --- a/src/conditions/base/time.ts +++ b/src/conditions/base/time.ts @@ -1,24 +1,21 @@ import Joi from 'joi'; -import { SUPPORTED_CHAINS } from '../const'; +import { omit } from '../../utils'; -import { Condition } from './condition'; -import { returnValueTestSchema } from './schema'; +import { RpcCondition, rpcConditionSchema } from './rpc'; -export class TimeCondition extends Condition { - // TODO: This is the only condition that uses defaults, and also the only condition that uses `method` in order - // to determine the schema. I.e. the only method that used `METHOD = 'blocktime'` in `nucypher/nucypher`. - // TODO: Consider introducing a different field for this, e.g. `conditionType` or `type`. Use this field in a - // condition factory. +const BLOCKTIME_METHOD = 'blocktime'; + +const timeConditionSchema = { + // TimeCondition is an RpcCondition with the method set to 'blocktime' and no parameters + ...omit(rpcConditionSchema, ['parameters']), + method: Joi.string().valid(BLOCKTIME_METHOD).required(), +}; + +export class TimeCondition extends RpcCondition { public readonly defaults: Record = { - method: 'blocktime', + method: BLOCKTIME_METHOD, }; - public readonly schema = Joi.object({ - method: Joi.string().valid(this.defaults.method).required(), - returnValueTest: returnValueTestSchema.required(), - chain: Joi.number() - .valid(...SUPPORTED_CHAINS) - .required(), - }); + public readonly schema = Joi.object(timeConditionSchema); } diff --git a/src/conditions/predefined/erc721.ts b/src/conditions/predefined/erc721.ts index ef9f880f3..beed9a5ce 100644 --- a/src/conditions/predefined/erc721.ts +++ b/src/conditions/predefined/erc721.ts @@ -1,7 +1,7 @@ -import { EvmCondition } from '../base'; +import { ContractCondition } from '../base'; import { USER_ADDRESS_PARAM } from '../const'; -export class ERC721Ownership extends EvmCondition { +export class ERC721Ownership extends ContractCondition { public readonly defaults = { method: 'ownerOf', parameters: [], @@ -14,7 +14,7 @@ export class ERC721Ownership extends EvmCondition { }; } -export class ERC721Balance extends EvmCondition { +export class ERC721Balance extends ContractCondition { public readonly defaults = { method: 'balanceOf', parameters: [USER_ADDRESS_PARAM], diff --git a/src/utils.ts b/src/utils.ts index 8b80d5463..722e98488 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -67,3 +67,9 @@ export const bytesEquals = (first: Uint8Array, second: Uint8Array): boolean => export const objectEquals = (a: unknown, b: unknown, strict = true): boolean => deepEqual(a, b, { strict }); + +export const omit = (obj: Record, keys: string[]) => { + const copy = { ...obj }; + keys.forEach((key) => delete copy[key]); + return copy; +}; diff --git a/test/docs/cbd.test.ts b/test/docs/cbd.test.ts index 86be24fe6..5d4a4702a 100644 --- a/test/docs/cbd.test.ts +++ b/test/docs/cbd.test.ts @@ -18,7 +18,7 @@ import { const { predefined: { ERC721Ownership }, - base: { EvmCondition }, + base: { ContractCondition }, ConditionSet, } = conditions; @@ -100,7 +100,7 @@ describe('Get Started (CBD PoC)', () => { value: 3, }, }; - const NFTBalance = new EvmCondition(NFTBalanceConfig); + const NFTBalance = new ContractCondition(NFTBalanceConfig); const encrypter = newDeployed.encrypter; diff --git a/test/unit/conditions/base/condition.test.ts b/test/unit/conditions/base/condition.test.ts index a38798744..dc9b310ab 100644 --- a/test/unit/conditions/base/condition.test.ts +++ b/test/unit/conditions/base/condition.test.ts @@ -1,4 +1,4 @@ -import { EvmCondition } from '../../../../src/conditions/base'; +import { ContractCondition } from '../../../../src/conditions/base'; import { ERC721Balance, ERC721Ownership, @@ -7,7 +7,7 @@ import { TEST_CHAIN_ID, TEST_CONTRACT_ADDR, TEST_CONTRACT_ADDR_2, - testEvmConditionObj, + testContractConditionObj, } from '../../testVariables'; describe('validation', () => { @@ -47,18 +47,18 @@ describe('validation', () => { describe('serialization', () => { it('serializes to a plain object', () => { - const evm = new EvmCondition(testEvmConditionObj); - expect(evm.toObj()).toEqual({ - ...testEvmConditionObj, - _class: 'EvmCondition', + const contract = new ContractCondition(testContractConditionObj); + expect(contract.toObj()).toEqual({ + ...testContractConditionObj, + _class: 'ContractCondition', }); }); it('serializes predefined conditions', () => { - const evm = new ERC721Ownership(testEvmConditionObj); - expect(evm.toObj()).toEqual({ - ...evm.defaults, - ...testEvmConditionObj, + const contract = new ERC721Ownership(testContractConditionObj); + expect(contract.toObj()).toEqual({ + ...contract.defaults, + ...testContractConditionObj, _class: 'ERC721Ownership', }); }); diff --git a/test/unit/conditions/base/evm.test.ts b/test/unit/conditions/base/evm.test.ts index 5a35efa93..404d2d90b 100644 --- a/test/unit/conditions/base/evm.test.ts +++ b/test/unit/conditions/base/evm.test.ts @@ -1,27 +1,27 @@ -import { EvmCondition } from '../../../../src/conditions/base'; -import { testEvmConditionObj } from '../../testVariables'; +import { ContractCondition } from '../../../../src/conditions/base'; +import { testContractConditionObj } from '../../testVariables'; describe('validation', () => { it('accepts on a valid schema', () => { - const evm = new EvmCondition(testEvmConditionObj); - expect(evm.toObj()).toEqual({ - ...testEvmConditionObj, - _class: 'EvmCondition', + const contract = new ContractCondition(testContractConditionObj); + expect(contract.toObj()).toEqual({ + ...testContractConditionObj, + _class: 'ContractCondition', }); }); it('rejects an invalid schema', () => { - const badEvmCondition = { - ...testEvmConditionObj, + const badContractCondition = { + ...testContractConditionObj, // Intentionally removing `contractAddress` contractAddress: undefined, }; - const badEvm = new EvmCondition(badEvmCondition); + const badEvm = new ContractCondition(badContractCondition); expect(() => badEvm.toObj()).toThrow( 'Invalid condition: "contractAddress" is required' ); - const { error } = badEvm.validate(badEvmCondition); + const { error } = badEvm.validate(badContractCondition); expect(error?.message).toEqual('"contractAddress" is required'); }); }); @@ -32,50 +32,50 @@ describe('accepts either standardContractType or functionAbi but not both or non it('accepts standardContractType', () => { const conditionObj = { - ...testEvmConditionObj, + ...testContractConditionObj, standardContractType, functionAbi: undefined, }; - const evmCondition = new EvmCondition(conditionObj); - expect(evmCondition.toObj()).toEqual({ + const contractCondition = new ContractCondition(conditionObj); + expect(contractCondition.toObj()).toEqual({ ...conditionObj, - _class: 'EvmCondition', + _class: 'ContractCondition', }); }); it('accepts functionAbi', () => { const conditionObj = { - ...testEvmConditionObj, + ...testContractConditionObj, functionAbi, standardContractType: undefined, }; - const evmCondition = new EvmCondition(conditionObj); - expect(evmCondition.toObj()).toEqual({ + const contractCondition = new ContractCondition(conditionObj); + expect(contractCondition.toObj()).toEqual({ ...conditionObj, - _class: 'EvmCondition', + _class: 'ContractCondition', }); }); it('rejects both', () => { const conditionObj = { - ...testEvmConditionObj, + ...testContractConditionObj, standardContractType, functionAbi, }; - const evmCondition = new EvmCondition(conditionObj); - expect(() => evmCondition.toObj()).toThrow( + const contractCondition = new ContractCondition(conditionObj); + expect(() => contractCondition.toObj()).toThrow( '"value" contains a conflict between exclusive peers [standardContractType, functionAbi]' ); }); it('rejects none', () => { const conditionObj = { - ...testEvmConditionObj, + ...testContractConditionObj, standardContractType: undefined, functionAbi: undefined, }; - const evmCondition = new EvmCondition(conditionObj); - expect(() => evmCondition.toObj()).toThrow( + const contractCondition = new ContractCondition(conditionObj); + expect(() => contractCondition.toObj()).toThrow( '"value" must contain at least one of [standardContractType, functionAbi]' ); }); @@ -108,8 +108,8 @@ describe('accepts either standardContractType or functionAbi but not both or non // }, // ], // }; -// const evmConditionObj = { -// ...testEvmConditionObj, +// const contractConditionObj = { +// ...testContractConditionObj, // functionAbi: fakeFunctionAbi, // method: 'myFunction', // parameters: [USER_ADDRESS_PARAM, ':customParam'], @@ -119,9 +119,9 @@ describe('accepts either standardContractType or functionAbi but not both or non // value: USER_ADDRESS_PARAM, // }, // }; -// const evmCondition = new EvmCondition(evmConditionObj); +// const contractCondition = new ContractCondition(contractConditionObj); // const web3Provider = fakeWeb3Provider(SecretKey.random().toBEBytes()); -// const conditionSet = new ConditionSet([evmCondition]); +// const conditionSet = new ConditionSet([contractCondition]); // const conditionContext = new ConditionContext( // conditionSet.toWASMConditions(), // web3Provider diff --git a/test/unit/conditions/context.test.ts b/test/unit/conditions/context.test.ts index 9a1746919..79d048157 100644 --- a/test/unit/conditions/context.test.ts +++ b/test/unit/conditions/context.test.ts @@ -2,12 +2,12 @@ import { SecretKey } from '@nucypher/nucypher-core'; import { CustomContextParam } from '../../../src'; import { ConditionSet } from '../../../src/conditions'; -import { EvmCondition, RpcCondition } from '../../../src/conditions/base'; +import { ContractCondition, RpcCondition } from '../../../src/conditions/base'; import { USER_ADDRESS_PARAM } from '../../../src/conditions/const'; import { RESERVED_CONTEXT_PARAMS } from '../../../src/conditions/context/context'; import { fakeWeb3Provider } from '../../utils'; import { - testEvmConditionObj, + testContractConditionObj, testFunctionAbi, testReturnValueTest, testRpcConditionObj, @@ -40,15 +40,15 @@ describe('context parameters', () => { const customParams: Record = {}; customParams[customParamKey] = 1234; - const evmConditionObj = { - ...testEvmConditionObj, + const contractConditionObj = { + ...testContractConditionObj, returnValueTest: { ...testReturnValueTest, value: customParamKey, }, }; - const evmCondition = new EvmCondition(evmConditionObj); - const conditionSet = new ConditionSet([evmCondition]); + const contractCondition = new ContractCondition(contractConditionObj); + const conditionSet = new ConditionSet([contractCondition]); const conditionContext = conditionSet.buildContext(web3Provider); describe('return value test', () => { @@ -78,8 +78,8 @@ describe('context parameters', () => { }); describe('custom method parameters', () => { - const evmConditionObj = { - ...testEvmConditionObj, + const contractConditionObj = { + ...testContractConditionObj, standardContractType: undefined, // We're going to use a custom function ABI functionAbi: testFunctionAbi, parameters: [USER_ADDRESS_PARAM, customParamKey], // We're going to use a custom parameter @@ -89,12 +89,12 @@ describe('context parameters', () => { }; it('rejects on a missing parameter ', async () => { - const customEvmCondition = new EvmCondition({ - ...evmConditionObj, + const customContractCondition = new ContractCondition({ + ...contractConditionObj, parameters: [USER_ADDRESS_PARAM, customParamKey], }); const conditionContext = new ConditionSet([ - customEvmCondition, + customContractCondition, ]).buildContext(web3Provider); await expect(async () => conditionContext.toObj()).rejects.toThrow( @@ -103,12 +103,12 @@ describe('context parameters', () => { }); it('accepts on a hard-coded parameter', async () => { - const customEvmCondition = new EvmCondition({ - ...evmConditionObj, + const customContractCondition = new ContractCondition({ + ...contractConditionObj, parameters: [USER_ADDRESS_PARAM, 100], }); const conditionContext = new ConditionSet([ - customEvmCondition, + customContractCondition, ]).buildContext(web3Provider); const asObj = await conditionContext.toObj(); diff --git a/test/unit/testVariables.ts b/test/unit/testVariables.ts index 99c219be0..6ece2f977 100644 --- a/test/unit/testVariables.ts +++ b/test/unit/testVariables.ts @@ -26,7 +26,7 @@ export const testRpcConditionObj = { returnValueTest: testReturnValueTest, }; -export const testEvmConditionObj = { +export const testContractConditionObj = { contractAddress: '0x0000000000000000000000000000000000000000', chain: 5, standardContractType: 'ERC20',