From 3c2b80b72fe146634b999df5b9bb9ef5ffc27508 Mon Sep 17 00:00:00 2001 From: 0xGorilla <84932007+0xGorilla@users.noreply.github.com> Date: Sun, 17 Jul 2022 20:26:54 +0300 Subject: [PATCH] feat: added support for call value (#142) Added functionality: - `expect(myFake.myFunction).to.have.been.calledWithValue(1234);` - `expect(myFake.myFunction.getCall(0).value).to.eq(1);` --- docs/source/fakes.rst | 18 ++++++++-- src/chai-plugin/matchers.ts | 1 + src/chai-plugin/types.ts | 5 +++ src/factories/smock-contract.ts | 3 +- src/logic/watchable-function-logic.ts | 4 +++ src/types.ts | 3 +- .../watchable-function-logic/Receiver.sol | 2 +- .../call-arguments.spec.ts | 36 +++++++++++++++++++ .../type-handling.spec.ts | 15 ++++++++ 9 files changed, 82 insertions(+), 5 deletions(-) diff --git a/docs/source/fakes.rst b/docs/source/fakes.rst index 4c973a4..2791669 100644 --- a/docs/source/fakes.rst +++ b/docs/source/fakes.rst @@ -277,8 +277,8 @@ Called N times expect(myFake.myFunction).to.have.callCount(123); -Asserting call arguments -************************ +Asserting call arguments or value +********************************* Called with specific arguments ############################## @@ -313,6 +313,13 @@ Called once with specific arguments expect(myFake.myFunction).to.have.been.calledOnceWith(1234, false); +Called with an specific call value +################################### + +.. code-block:: typescript + + expect(myFake.myFunction).to.have.been.calledWithValue(1234); + Asserting call order ******************** @@ -355,6 +362,13 @@ Getting arguments at a specific call index expect(myFake.myFunction.getCall(0).args[0]).to.be.gt(50); +Getting call value at a specific call index +########################################## + +.. code-block:: typescript + + expect(myFake.myFunction.getCall(0).value).to.eq(1); + Manipulating fallback functions ******************************* diff --git a/src/chai-plugin/matchers.ts b/src/chai-plugin/matchers.ts index ee7394e..7263e34 100644 --- a/src/chai-plugin/matchers.ts +++ b/src/chai-plugin/matchers.ts @@ -136,6 +136,7 @@ export const matchers: Chai.ChaiPlugin = (chai: Chai.ChaiStatic, utils: Chai.Cha smockMethodWithWatchableContractArg('calledImmediatelyBefore', 'been called immediately before %1'); smockMethodWithWatchableContractArg('calledImmediatelyAfter', 'been called immediately after %1'); smockMethod('calledWith', 'been called with arguments %*', '%D'); + smockMethod('calledWithValue', 'been called with value %*', '%D'); smockMethod('calledOnceWith', 'been called exactly once with arguments %*', '%D'); smockMethod('delegatedFrom', 'been called via a delegated call by %*', ''); }; diff --git a/src/chai-plugin/types.ts b/src/chai-plugin/types.ts index f7e2bf4..6040fa8 100644 --- a/src/chai-plugin/types.ts +++ b/src/chai-plugin/types.ts @@ -1,3 +1,4 @@ +import { BigNumber } from 'ethers'; import { WatchableContractFunction } from '../index'; declare global { @@ -50,6 +51,10 @@ declare global { * Returns true if call received provided arguments. */ calledWith(...args: any[]): Assertion; + /** + * Returns true if call received the provided value. + */ + calledWithValue(value: BigNumber): Assertion; /** * Returns true when called at exactly once with the provided arguments. */ diff --git a/src/factories/smock-contract.ts b/src/factories/smock-contract.ts index d07588e..f6cea4e 100644 --- a/src/factories/smock-contract.ts +++ b/src/factories/smock-contract.ts @@ -1,6 +1,6 @@ import Message from '@nomiclabs/ethereumjs-vm/dist/evm/message'; import { FactoryOptions } from '@nomiclabs/hardhat-ethers/types'; -import { BaseContract, ContractFactory, ethers } from 'ethers'; +import { BaseContract, BigNumber, ContractFactory, ethers } from 'ethers'; import { Interface } from 'ethers/lib/utils'; import { ethers as hardhatEthers } from 'hardhat'; import { Observable } from 'rxjs'; @@ -205,6 +205,7 @@ function parseMessage(message: Message, contractInterface: Interface, sighash: s return { args: sighash === null ? toHexString(message.data) : getMessageArgs(message.data, contractInterface, sighash), nonce: Sandbox.getNextNonce(), + value: BigNumber.from(message.value.toString()), target: fromFancyAddress(message.delegatecall ? message.codeAddress : message.to), delegatedFrom: message.delegatecall ? fromFancyAddress(message.to) : undefined, }; diff --git a/src/logic/watchable-function-logic.ts b/src/logic/watchable-function-logic.ts index 57e2bf4..f333014 100644 --- a/src/logic/watchable-function-logic.ts +++ b/src/logic/watchable-function-logic.ts @@ -31,6 +31,10 @@ export class WatchableFunctionLogic { return !!this.callHistory.find((call) => this.isDeepEqual(call.args, expectedCallArgs)); } + calledWithValue(value: BigNumber): boolean { + return !!this.callHistory.find((call) => call.value.eq(value)); + } + alwaysCalledWith(...expectedCallArgs: unknown[]): boolean { const callWithOtherArgs = this.callHistory.find((call) => !this.isDeepEqual(call.args, expectedCallArgs)); return this.getCalled() && !callWithOtherArgs; diff --git a/src/types.ts b/src/types.ts index 873c5d9..020ff50 100644 --- a/src/types.ts +++ b/src/types.ts @@ -2,7 +2,7 @@ import { Fragment, Interface, JsonFragment } from '@ethersproject/abi'; import { Provider } from '@ethersproject/abstract-provider'; import { Signer } from '@ethersproject/abstract-signer'; -import { BaseContract, ContractFactory, ethers } from 'ethers'; +import { BaseContract, BigNumber, ContractFactory, ethers } from 'ethers'; import { EditableStorageLogic } from './logic/editable-storage-logic'; import { ReadableStorageLogic } from './logic/readable-storage-logic'; import { WatchableFunctionLogic } from './logic/watchable-function-logic'; @@ -35,6 +35,7 @@ export interface ContractCall { args: unknown[] | string; nonce: number; target: string; + value: BigNumber; delegatedFrom?: string; } diff --git a/test/contracts/watchable-function-logic/Receiver.sol b/test/contracts/watchable-function-logic/Receiver.sol index f3ac7a9..2715910 100644 --- a/test/contracts/watchable-function-logic/Receiver.sol +++ b/test/contracts/watchable-function-logic/Receiver.sol @@ -29,7 +29,7 @@ struct StructNested { contract Receiver { fallback() external {} - function receiveEmpty() public {} + function receiveEmpty() public payable {} function receiveBoolean(bool) public {} diff --git a/test/unit/watchable-function-logic/call-arguments.spec.ts b/test/unit/watchable-function-logic/call-arguments.spec.ts index c4c651b..5e31025 100644 --- a/test/unit/watchable-function-logic/call-arguments.spec.ts +++ b/test/unit/watchable-function-logic/call-arguments.spec.ts @@ -1,6 +1,8 @@ +import { SignerWithAddress } from '@nomiclabs/hardhat-ethers/signers'; import { FakeContract, smock } from '@src'; import { Caller, Caller__factory, Receiver } from '@typechained'; import chai, { AssertionError, expect } from 'chai'; +import { BigNumber } from 'ethers'; import { ethers } from 'hardhat'; chai.should(); @@ -9,8 +11,11 @@ chai.use(smock.matchers); describe('WatchableFunctionLogic: Call arguments', () => { let fake: FakeContract; let caller: Caller; + let signer: SignerWithAddress; before(async () => { + [, signer] = await ethers.getSigners(); + const callerFactory = (await ethers.getContractFactory('Caller')) as Caller__factory; caller = await callerFactory.deploy(); }); @@ -48,6 +53,37 @@ describe('WatchableFunctionLogic: Call arguments', () => { }); }); + describe('calledWithValue', async () => { + const value = BigNumber.from(123); + + it('should throw when the watchablecontract is not called', async () => { + expect(() => { + fake.receiveEmpty.should.have.been.calledWithValue(value); + }).to.throw(AssertionError); + }); + + it('should throw when the watchablecontract is called with incorrect arguments', async () => { + await fake.connect(signer).receiveEmpty({ value: value.sub(1) }); + + expect(() => { + fake.receiveEmpty.should.have.been.calledWithValue(value); + }).to.throw(AssertionError); + }); + + it('should not throw when the watchablecontract is called with the correct arguments', async () => { + await fake.connect(signer).receiveEmpty({ value }); + fake.receiveEmpty.should.have.been.calledWithValue(value); + }); + + it('should not throw when the watchablecontract is called with incorrect arguments but the correct ones as well', async () => { + await fake.connect(signer).receiveEmpty({ value: value.sub(1) }); + await fake.connect(signer).receiveEmpty({ value: value }); + await fake.connect(signer).receiveEmpty({ value: value.add(1) }); + + fake.receiveEmpty.should.have.been.calledWithValue(value); + }); + }); + describe('always.calledWith', async () => { it('should throw when the watchablecontract is not called', async () => { expect(() => { diff --git a/test/unit/watchable-function-logic/type-handling.spec.ts b/test/unit/watchable-function-logic/type-handling.spec.ts index 9dd9fda..576c16e 100644 --- a/test/unit/watchable-function-logic/type-handling.spec.ts +++ b/test/unit/watchable-function-logic/type-handling.spec.ts @@ -1,3 +1,4 @@ +import { SignerWithAddress } from '@nomiclabs/hardhat-ethers/signers'; import { FakeContract, smock } from '@src'; import { BYTES32_EXAMPLE, BYTES_EXAMPLE, STRUCT_DYNAMIC_SIZE_EXAMPLE, STRUCT_FIXED_SIZE_EXAMPLE } from '@test-utils'; import { Caller, Caller__factory, Receiver } from '@typechained'; @@ -11,8 +12,11 @@ chai.use(smock.matchers); describe('WatchableFunctionLogic: Type handling', () => { let fake: FakeContract; let caller: Caller; + let signer: SignerWithAddress; before(async () => { + [, signer] = await ethers.getSigners(); + const callerFactory = (await ethers.getContractFactory('Caller')) as Caller__factory; caller = await callerFactory.deploy(); }); @@ -123,4 +127,15 @@ describe('WatchableFunctionLogic: Type handling', () => { fake['receiveOverload(bool)'].should.have.been.calledWith(true); fake['receiveOverload(bool,bool)'].should.have.been.calledWith(true, false); }); + + it('should handle msg.value', async () => { + const value = BigNumber.from(123); + await fake.connect(signer).receiveEmpty({ value }); + fake.receiveEmpty.getCall(0).value.should.equal(value); + }); + + it('should handle empty msg.value', async () => { + await fake.connect(signer).receiveEmpty(); + fake.receiveEmpty.getCall(0).value.should.equal(BigNumber.from(0)); + }); });