diff --git a/src/conditions/base/contract.ts b/src/conditions/base/contract.ts index 38b048e5e..e205e22db 100644 --- a/src/conditions/base/contract.ts +++ b/src/conditions/base/contract.ts @@ -6,6 +6,39 @@ import { RpcCondition, rpcConditionRecord } from './rpc'; export const STANDARD_CONTRACT_TYPES = ['ERC20', 'ERC721']; +const functionAbiVariable = Joi.object({ + internalType: Joi.string().required(), + name: Joi.string().required(), + type: Joi.string().required(), +}); + +const functionAbiSchema = Joi.object({ + name: Joi.string().required(), + type: Joi.string().valid('function').required(), + inputs: Joi.array().items(functionAbiVariable), + outputs: Joi.array().items(functionAbiVariable), + // TODO: Should we restrict this to 'view'? + // stateMutability: Joi.string().valid('view').required(), +}).custom((functionAbi, helper) => { + // Validate method name + const method = helper.state.ancestors[0].method; + if (functionAbi.name !== method) { + return helper.message({ + custom: '"method" must be the same as "functionAbi.name"', + }); + } + + // Validate nr of parameters + const parameters = helper.state.ancestors[0].parameters; + if (functionAbi.inputs?.length !== parameters.length) { + return helper.message({ + custom: '"parameters" must have the same length as "functionAbi.inputs"', + }); + } + + return functionAbi; +}); + export const contractConditionRecord = { ...rpcConditionRecord, contractAddress: Joi.string().pattern(ETH_ADDRESS_REGEXP).required(), @@ -13,7 +46,7 @@ export const contractConditionRecord = { .valid(...STANDARD_CONTRACT_TYPES) .optional(), method: Joi.string().required(), - functionAbi: Joi.object().optional(), + functionAbi: functionAbiSchema.optional(), parameters: Joi.array().required(), }; diff --git a/test/unit/conditions/base/condition.test.ts b/test/unit/conditions/base/condition.test.ts index 2a567434c..7759e02d1 100644 --- a/test/unit/conditions/base/condition.test.ts +++ b/test/unit/conditions/base/condition.test.ts @@ -8,6 +8,7 @@ import { TEST_CONTRACT_ADDR, TEST_CONTRACT_ADDR_2, testContractConditionObj, + testFunctionAbi, } from '../../testVariables'; describe('validation', () => { @@ -61,3 +62,58 @@ describe('serialization', () => { }); }); }); +describe('accepts either standardContractType or functionAbi but not both or none', () => { + const standardContractType = 'ERC20'; + + it('accepts standardContractType', () => { + const conditionObj = { + ...testContractConditionObj, + standardContractType, + functionAbi: undefined, + }; + const contractCondition = new ContractCondition(conditionObj); + expect(contractCondition.toObj()).toEqual({ + ...conditionObj, + }); + }); + + it('accepts functionAbi', () => { + const conditionObj = { + ...testContractConditionObj, + functionAbi: testFunctionAbi, + method: testFunctionAbi.name, + parameters: [1, 2], + standardContractType: undefined, + }; + const contractCondition = new ContractCondition(conditionObj); + expect(contractCondition.toObj()).toEqual({ + ...conditionObj, + }); + }); + + it('rejects both', () => { + const conditionObj = { + ...testContractConditionObj, + standardContractType, + functionAbi: testFunctionAbi, + method: testFunctionAbi.name, + parameters: [1, 2], + }; + const contractCondition = new ContractCondition(conditionObj); + expect(() => contractCondition.toObj()).toThrow( + '"value" contains a conflict between exclusive peers [standardContractType, functionAbi]' + ); + }); + + it('rejects none', () => { + const conditionObj = { + ...testContractConditionObj, + standardContractType: undefined, + functionAbi: undefined, + }; + const contractCondition = new ContractCondition(conditionObj); + expect(() => contractCondition.toObj()).toThrow( + '"value" must contain at least one of [standardContractType, functionAbi]' + ); + }); +}); diff --git a/test/unit/conditions/base/evm.test.ts b/test/unit/conditions/base/evm.test.ts deleted file mode 100644 index 66f03d172..000000000 --- a/test/unit/conditions/base/evm.test.ts +++ /dev/null @@ -1,138 +0,0 @@ -import { ContractCondition } from '../../../../src/conditions/base'; -import { testContractConditionObj } from '../../testVariables'; - -describe('validation', () => { - it('accepts on a valid schema', () => { - const contract = new ContractCondition(testContractConditionObj); - expect(contract.toObj()).toEqual({ - ...testContractConditionObj, - }); - }); - - it('rejects an invalid schema', () => { - const badContractCondition = { - ...testContractConditionObj, - // Intentionally removing `contractAddress` - contractAddress: undefined, - }; - const badEvm = new ContractCondition(badContractCondition); - expect(() => badEvm.toObj()).toThrow( - 'Invalid condition: "contractAddress" is required' - ); - - const { error } = badEvm.validate(badContractCondition); - expect(error?.message).toEqual('"contractAddress" is required'); - }); -}); - -describe('accepts either standardContractType or functionAbi but not both or none', () => { - const standardContractType = 'ERC20'; - const functionAbi = { fake_function_abi: true }; - - it('accepts standardContractType', () => { - const conditionObj = { - ...testContractConditionObj, - standardContractType, - functionAbi: undefined, - }; - const contractCondition = new ContractCondition(conditionObj); - expect(contractCondition.toObj()).toEqual({ - ...conditionObj, - }); - }); - - it('accepts functionAbi', () => { - const conditionObj = { - ...testContractConditionObj, - functionAbi, - standardContractType: undefined, - }; - const contractCondition = new ContractCondition(conditionObj); - expect(contractCondition.toObj()).toEqual({ - ...conditionObj, - }); - }); - - it('rejects both', () => { - const conditionObj = { - ...testContractConditionObj, - standardContractType, - functionAbi, - }; - const contractCondition = new ContractCondition(conditionObj); - expect(() => contractCondition.toObj()).toThrow( - '"value" contains a conflict between exclusive peers [standardContractType, functionAbi]' - ); - }); - - it('rejects none', () => { - const conditionObj = { - ...testContractConditionObj, - standardContractType: undefined, - functionAbi: undefined, - }; - const contractCondition = new ContractCondition(conditionObj); - expect(() => contractCondition.toObj()).toThrow( - '"value" must contain at least one of [standardContractType, functionAbi]' - ); - }); -}); - -// TODO(#124) -// it('accepts custom parameters in function abi methods', async () => { -// throw new Error('Not implemented'); -// }); - -// TODO(#124) -// describe('supports custom function abi', () => { -// const fakeFunctionAbi = { -// name: 'myFunction', -// type: 'function', -// inputs: [ -// { -// name: 'account', -// type: 'address', -// }, -// { -// name: 'myCustomParam', -// type: 'uint256', -// }, -// ], -// outputs: [ -// { -// name: 'someValue', -// type: 'uint256', -// }, -// ], -// }; -// const contractConditionObj = { -// ...testContractConditionObj, -// functionAbi: fakeFunctionAbi, -// method: 'myFunction', -// parameters: [USER_ADDRESS_PARAM, ':customParam'], -// returnValueTest: { -// index: 0, -// comparator: '==', -// value: USER_ADDRESS_PARAM, -// }, -// }; -// const contractCondition = new ContractCondition(contractConditionObj); -// const web3Provider = fakeWeb3Provider(SecretKey.random().toBEBytes()); -// const conditionSet = new ConditionSet([contractCondition]); -// const conditionContext = new ConditionContext( -// conditionSet.toWASMConditions(), -// web3Provider -// ); -// const myCustomParam = ':customParam'; -// const customParams: Record = {}; -// customParams[myCustomParam] = 1234; -// -// it('accepts custom function abi', async () => { -// const asJson = await conditionContext -// .withCustomParams(customParams) -// .toJson(); -// expect(asJson).toBeDefined(); -// expect(asJson).toContain(USER_ADDRESS_PARAM); -// expect(asJson).toContain(myCustomParam); -// }); -// }); diff --git a/test/unit/conditions/context.test.ts b/test/unit/conditions/context.test.ts index ac0d1ecb3..958454cd1 100644 --- a/test/unit/conditions/context.test.ts +++ b/test/unit/conditions/context.test.ts @@ -82,6 +82,7 @@ describe('context parameters', () => { ...testContractConditionObj, standardContractType: undefined, // We're going to use a custom function ABI functionAbi: testFunctionAbi, + method: testFunctionAbi.name, parameters: [USER_ADDRESS_PARAM, customParamKey], // We're going to use a custom parameter returnValueTest: { ...testReturnValueTest, diff --git a/test/unit/testVariables.ts b/test/unit/testVariables.ts index db2bf9ef5..a95d99be0 100644 --- a/test/unit/testVariables.ts +++ b/test/unit/testVariables.ts @@ -50,16 +50,19 @@ export const testFunctionAbi = { type: 'function', inputs: [ { + internalType: 'address', name: 'account', type: 'address', }, { + internalType: 'uint256', name: 'myCustomParam', type: 'uint256', }, ], outputs: [ { + internalType: 'uint256', name: 'someValue', type: 'uint256', },