Repository: defi-wonderland/smock (ts)

Issue:
feat request: add reading calls msg data and value
It'd be great to know when a mock is called, what was the raw shape of the `msg` structure.
In our case, we need to check that a `payable` function was called with the correct amount of funds.

Pull request description:
feat: added support for call value
**Description**
Adds full support for call `msg.value`, see changed documentation in order to see new implemented methods

**Metadata**

- Fixes #114 


Patch:
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;
 }
 


Test patch:
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<Receiver>;
   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<Receiver>;
   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));
+  });
 });