Skip to content

Commit ede638e

Browse files
authoredMar 24, 2022
🪲 Replace ganache-core with ganache (#652)
1 parent d7715fa commit ede638e

File tree

33 files changed

+783
-3298
lines changed

33 files changed

+783
-3298
lines changed
 

‎.changeset/eleven-beds-sort.md

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@ethereum-waffle/provider": patch
3+
---
4+
5+
Replaced deprecated `ganache-core` package with `ganache`.

‎docs/source/migration-guides.rst

+47
Original file line numberDiff line numberDiff line change
@@ -264,6 +264,53 @@ We updated the following dependencies:
264264
- :code:`typechain` - bumped version from ^2.0.0 to ^9.0.0. Now every Waffle package uses the same version of the package. Also the package was moved to the :code:`peerDependencies` section - you now need to install :code:`typechain` manually when using Waffle.
265265
- :code:`ethers` - bumped version from to ^5.5.4. Now every Waffle package uses the same version of the package. Also the package was moved to the :code:`peerDependencies` section - you now need to install :code:`ethers` manually when using Waffle.
266266
- :code:`solc` - the package is used by :code:`waffle-compiler` package to provide the default option for compiling Soldity code. Was moved to the :code:`peerDependencies` section and has no version restrictions - you now have to install :code:`solc` manually when using Waffle.
267+
- Deprecated :code:`ganache-core` package has been replaced with :code:`ganache` version ^7.0.3. It causes slight differences in the parameters of :code:`MockProvider` from :code:`@ethereum-waffle/provider`. Now the :code:`MockProvider` uses :code:`berlin` hardfork by default.
268+
269+
Changes to :code:`MockProvider` parameters
270+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
271+
272+
Previous (optional) parameters of :code:`MockProvider` included override options for the Ganache provider:
273+
274+
.. code-block:: ts
275+
276+
interface MockProviderOptions {
277+
ganacheOptions: {
278+
account_keys_path?: string;
279+
accounts?: object[];
280+
allowUnlimitedContractSize?: boolean;
281+
blockTime?: number;
282+
db_path?: string;
283+
debug?: boolean;
284+
default_balance_ether?: number;
285+
fork?: string | object;
286+
fork_block_number?: string | number;
287+
forkCacheSize?: number;
288+
gasLimit?: string | number;
289+
gasPrice?: string;
290+
hardfork?: "byzantium" | "constantinople" | "petersburg" | "istanbul" | "muirGlacier";
291+
hd_path?: string;
292+
locked?: boolean;
293+
logger?: {
294+
log(msg: string): void;
295+
};
296+
mnemonic?: string;
297+
network_id?: number;
298+
networkId?: number;
299+
port?: number;
300+
seed?: any;
301+
time?: Date;
302+
total_accounts?: number;
303+
unlocked_accounts?: string[];
304+
verbose?: boolean;
305+
vmErrorsOnRPCResponse?: boolean;
306+
ws?: boolean;
307+
}
308+
}
309+
310+
Current :code:`ganacheOptions` parameter are documented `here <https://github.com/trufflesuite/ganache/blob/386771d84a9985f6d4b61b262f2be3cda896162e/src/chains/ethereum/options/src/index.ts#L22-L29>`_.
311+
312+
Typechain changes
313+
~~~~~~~~~~~~~~~~~
267314

268315
If you used type generation (:code:`typechainEnabled` option set to :code:`true` in :code:`waffle.json`), you need to update your code to conform to the new naming convention used by :code:`typechain`. Contract factories now have postfix :code:`__factory` instead of :code:`Factory`. For example, :code:`MyContractFactory` becomes :code:`MyContract__factory`. Example refactoring:
269316

‎examples/basic/package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818
"eslint": "^6.8.0",
1919
"eslint-plugin-import": "^2.20.2",
2020
"ethereum-waffle": "workspace:*",
21-
"ethers": "4.0.47",
21+
"ethers": "^5.6.1",
2222
"mocha": "^7.1.2",
2323
"ts-node": "^8.9.1",
2424
"typescript": "^4.6.2",

‎examples/called-on-contract/package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818
"eslint": "^6.8.0",
1919
"eslint-plugin-import": "^2.20.2",
2020
"ethereum-waffle": "workspace:*",
21-
"ethers": "4.0.47",
21+
"ethers": "^5.6.1",
2222
"mocha": "^7.1.2",
2323
"ts-node": "^8.9.1",
2424
"typescript": "^4.6.2",

‎examples/change-balance/package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818
"eslint": "^6.8.0",
1919
"eslint-plugin-import": "^2.20.2",
2020
"ethereum-waffle": "workspace:*",
21-
"ethers": "4.0.47",
21+
"ethers": "^5.6.1",
2222
"mocha": "^7.1.2",
2323
"ts-node": "^8.9.1",
2424
"typescript": "^4.6.2"

‎examples/dynamic-mocking-and-testing-calls/package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818
"eslint": "^6.8.0",
1919
"eslint-plugin-import": "^2.20.2",
2020
"ethereum-waffle": "workspace:*",
21-
"ethers": "^5.0.17",
21+
"ethers": "^5.6.1",
2222
"mocha": "^7.1.2",
2323
"ts-node": "^8.9.1",
2424
"typescript": "^4.6.2"

‎examples/ens/package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
"lint:fix": "eslint --fix '{src,test}/**/*.ts'"
1111
},
1212
"devDependencies": {
13-
"ethers": "4.0.47",
13+
"ethers": "^5.6.1",
1414
"@types/chai": "^4.2.3",
1515
"@types/mocha": "^5.2.7",
1616
"@typescript-eslint/eslint-plugin": "^5.15.0",

‎examples/mock-contracts/package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
"lint:fix": "eslint --fix '{src,test}/**/*.ts'"
1111
},
1212
"devDependencies": {
13-
"ethers": "4.0.47",
13+
"ethers": "^5.6.1",
1414
"@types/chai": "^4.2.3",
1515
"@types/mocha": "^5.2.7",
1616
"@typescript-eslint/eslint-plugin": "^5.15.0",

‎patches/ganache-core+2.13.2.patch

-44
This file was deleted.

‎pnpm-lock.yaml

+360-3,184
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎waffle-chai/package.json

+2-2
Original file line numberDiff line numberDiff line change
@@ -39,8 +39,8 @@
3939
"node": ">=10.0"
4040
},
4141
"dependencies": {
42-
"@ethereum-waffle/provider": "workspace:*",
43-
"ethers": "^5.5.4"
42+
"ethers": "^5.6.1",
43+
"@ethereum-waffle/provider": "workspace:*"
4444
},
4545
"devDependencies": {}
4646
}

‎waffle-chai/src/matchers/reverted.ts

+14-10
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,6 @@
11
export function supportReverted(Assertion: Chai.AssertionStatic) {
22
Assertion.addProperty('reverted', function (this: any) {
33
const promise = this._obj;
4-
const onSuccess = (value: any) => {
5-
this.assert(
6-
false,
7-
'Expected transaction to be reverted',
8-
'Expected transaction NOT to be reverted',
9-
'Transaction reverted.',
10-
'Transaction NOT reverted.'
11-
);
12-
return value;
13-
};
144
const onError = (error: any) => {
155
const message = (error instanceof Object && 'message' in error) ? error.message : JSON.stringify(error);
166
const isReverted = message.search('revert') >= 0;
@@ -25,6 +15,20 @@ export function supportReverted(Assertion: Chai.AssertionStatic) {
2515
);
2616
return error;
2717
};
18+
const onSuccess = (value: any) => {
19+
if ('wait' in value) {
20+
// Sending the transaction succeeded, but we wait to see if it will revert on-chain.
21+
return value.wait().then((newValue: any) => newValue, onError);
22+
}
23+
this.assert(
24+
false,
25+
'Expected transaction to be reverted',
26+
'Expected transaction NOT to be reverted',
27+
'Transaction reverted.',
28+
'Transaction NOT reverted.'
29+
);
30+
return value;
31+
};
2832
const derivedPromise = promise.then(onSuccess, onError);
2933
this.then = derivedPromise.then.bind(derivedPromise);
3034
this.catch = derivedPromise.catch.bind(derivedPromise);

‎waffle-chai/src/matchers/revertedWith.ts

+18
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,14 @@
1+
import {decodeRevertString} from '@ethereum-waffle/provider';
2+
13
export function supportRevertedWith(Assertion: Chai.AssertionStatic) {
24
Assertion.addMethod('revertedWith', function (this: any, revertReason: string) {
35
const promise = this._obj;
46

57
const onSuccess = (value: any) => {
8+
if ('wait' in value) {
9+
// Sending the transaction succeeded, but we wait to see if it will revert on-chain.
10+
return value.wait().then((newValue: any) => newValue, onError);
11+
}
612
this.assert(
713
false,
814
'Expected transaction to be reverted',
@@ -14,6 +20,18 @@ export function supportRevertedWith(Assertion: Chai.AssertionStatic) {
1420
};
1521

1622
const onError = (error: any) => {
23+
const revertString = error?.receipt?.revertString ?? decodeRevertString(error);
24+
if (revertString) {
25+
this.assert(
26+
revertString === revertReason,
27+
`Expected transaction to be reverted with ${revertReason}, but other reason was found: ${revertString}`,
28+
`Expected transaction NOT to be reverted with ${revertReason}`,
29+
`Transaction reverted with ${revertReason}.`,
30+
error
31+
);
32+
return error;
33+
}
34+
1735
// See https://github.com/ethers-io/ethers.js/issues/829
1836
const isEstimateGasError =
1937
error instanceof Object &&

‎waffle-cli/package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@
5050
"@ethereum-waffle/compiler": "workspace:*",
5151
"@ethereum-waffle/mock-contract": "workspace:*",
5252
"@ethereum-waffle/provider": "workspace:*",
53-
"ethers": "^5.5.4",
53+
"ethers": "^5.6.1",
5454
"solc": "^0.6.3",
5555
"typechain": "^7.0.0"
5656
},

‎waffle-compiler/package.json

+5-5
Original file line numberDiff line numberDiff line change
@@ -39,9 +39,9 @@
3939
"node": ">=10.0"
4040
},
4141
"dependencies": {
42-
"@ethersproject/abi": "^5.0.0",
43-
"@ethersproject/bytes": "^5.0.0",
44-
"@ethersproject/providers": "^5.0.0",
42+
"@ethersproject/abi": "^5.6.0",
43+
"@ethersproject/bytes": "^5.6.0",
44+
"@ethersproject/providers": "^5.6.1",
4545
"@resolver-engine/imports": "^0.3.3",
4646
"@resolver-engine/imports-fs": "^0.3.3",
4747
"@typechain/ethers-v5": "^9.0.0",
@@ -55,14 +55,14 @@
5555
"@ethereum-waffle/provider": "workspace:*",
5656
"@openzeppelin/contracts": "3.0.1",
5757
"@types/fs-extra": "^9.0.4",
58-
"ethers": "^5.5.4",
58+
"ethers": "^5.6.1",
5959
"fs-extra": "^9.0.1",
6060
"openzeppelin-solidity": "2.3.0",
6161
"solc": "^0.6.3",
6262
"typechain": "^7.0.0"
6363
},
6464
"peerDependencies": {
65-
"ethers": "^5.5.4",
65+
"ethers": "^5.6.1",
6666
"solc": "*",
6767
"typechain": "^7.0.0"
6868
}

‎waffle-e2e/package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
"@ethereum-waffle/chai": "workspace:*",
1616
"@ethereum-waffle/compiler": "workspace:*",
1717
"@ethereum-waffle/provider": "workspace:*",
18-
"ethers": "^5.5.4",
18+
"ethers": "^5.6.1",
1919
"solc": "^0.6.3",
2020
"typechain": "^7.0.0"
2121
}

‎waffle-ens/package.json

+2-2
Original file line numberDiff line numberDiff line change
@@ -42,9 +42,9 @@
4242
"dependencies": {
4343
"@ensdomains/ens": "^0.4.4",
4444
"@ensdomains/resolver": "^0.2.4",
45-
"ethers": "^5.5.4"
45+
"ethers": "^5.6.1"
4646
},
4747
"devDependencies": {
48-
"ganache-core": "^2.13.2"
48+
"ganache": "^7.0.3"
4949
}
5050
}

‎waffle-ens/test/utils.ts

+4-3
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,13 @@
11
import {providers, Wallet} from 'ethers';
2-
import Ganache from 'ganache-core';
2+
import Ganache from 'ganache';
33

44
export const getWallet = (): Wallet => {
5-
const balance = '10000000000000000000000000000000000';
5+
const balance = '0x1ED09BEAD87C0378D8E6400000000'; // 10^34
66
const secretKey = '0x03c909455dcef4e1e981a21ffb14c1c51214906ce19e8e7541921b758221b5ae';
77

88
const defaultAccount = [{balance, secretKey}];
99

10-
const provider = new providers.Web3Provider(Ganache.provider({accounts: defaultAccount}) as any);
10+
const ganacheProvider = Ganache.provider({accounts: defaultAccount, logging: {quiet: true}});
11+
const provider = new providers.Web3Provider(ganacheProvider as any);
1112
return new Wallet(defaultAccount[0].secretKey, provider);
1213
};

‎waffle-jest/package.json

+2-2
Original file line numberDiff line numberDiff line change
@@ -39,8 +39,8 @@
3939
"node": ">=10.0"
4040
},
4141
"dependencies": {
42-
"@ethereum-waffle/provider": "^3.4.0",
43-
"ethers": "^5.5.4",
42+
"@ethereum-waffle/provider": "workspace:*",
43+
"ethers": "^5.6.1",
4444
"jest-diff": "^26.0.1"
4545
},
4646
"devDependencies": {

‎waffle-jest/src/matchers/toBeReverted.ts

+13-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,18 @@
11
export async function toBeReverted(promise: Promise<any>) {
22
try {
3-
await promise;
3+
const tx = await promise;
4+
if ('wait' in tx) {
5+
// Sending the transaction succeeded, but we wait to see if it will revert on-chain.
6+
try {
7+
await tx.wait();
8+
} catch (e) {
9+
return {
10+
pass: true,
11+
message: () => 'Expected transaction to be reverted'
12+
};
13+
}
14+
}
15+
416
return {
517
pass: false,
618
message: () => 'Expected transaction to be reverted'

‎waffle-jest/src/matchers/toBeRevertedWith.ts

+14-2
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,26 @@
1+
import {decodeRevertString} from '@ethereum-waffle/provider';
2+
13
export async function toBeRevertedWith(
24
promise: Promise<any>,
35
revertReason: string
46
) {
57
try {
6-
await promise;
8+
const tx = await promise;
9+
await tx.wait();
710
return {
811
pass: false,
912
message: () => 'Expected transaction to be reverted'
1013
};
11-
} catch (error) {
14+
} catch (error: any) {
15+
const revertString = error?.receipt?.revertString ?? decodeRevertString(error);
16+
if (revertString) {
17+
return {
18+
pass: revertString === revertReason,
19+
message: () =>
20+
`Expected transaction to be reverted with ${revertReason}, but other reason was found: ${revertString}`
21+
};
22+
}
23+
1224
const message = error instanceof Object && 'message' in error ? (error as any).message : JSON.stringify(error);
1325

1426
const isReverted = message.search('revert') >= 0 && message.search(revertReason) >= 0;

‎waffle-jest/test/matchers/reverted.test.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -98,7 +98,7 @@ describe('INTEGRATION: Matchers: revertedWith', () => {
9898
expect(matchers.doRevert()).toBeRevertedWith('different message')
9999
).rejects.toThrowError(
100100
'Expected transaction to be reverted with different message, ' +
101-
'but other exception was thrown: RuntimeError: VM Exception while processing transaction: revert Revert cause'
101+
'but other reason was found: Revert cause'
102102
);
103103
});
104104

@@ -123,7 +123,7 @@ describe('INTEGRATION: Matchers: revertedWith', () => {
123123
expect(matchers.doRequireFail()).toBeRevertedWith('Different message')
124124
).rejects.toThrowError(
125125
'Expected transaction to be reverted with Different message, ' +
126-
'but other exception was thrown: RuntimeError: VM Exception while processing transaction: revert Require cause'
126+
'but other reason was found: Require cause'
127127
);
128128
});
129129

‎waffle-mock-contract/package.json

+2-2
Original file line numberDiff line numberDiff line change
@@ -44,8 +44,8 @@
4444
"node": ">=10.0"
4545
},
4646
"dependencies": {
47-
"@ethersproject/abi": "^5.5.0",
48-
"ethers": "^5.5.4"
47+
"@ethersproject/abi": "^5.6.0",
48+
"ethers": "^5.6.1"
4949
},
5050
"devDependencies": {
5151
"@ethereum-waffle/chai": "workspace:*",

‎waffle-provider/package.json

+4-4
Original file line numberDiff line numberDiff line change
@@ -41,10 +41,10 @@
4141
},
4242
"dependencies": {
4343
"@ethereum-waffle/ens": "workspace:*",
44-
"ethers": "^5.5.4",
45-
"ganache-core": "^2.13.2",
46-
"patch-package": "^6.2.2",
47-
"postinstall-postinstall": "^2.1.0"
44+
"ethers": "^5.6.1",
45+
"ganache": "^7.0.3",
46+
"postinstall-postinstall": "^2.1.0",
47+
"patch-package": "^6.2.2"
4848
},
4949
"resolutions": {
5050
"web3": "1.2.4"
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
diff --git a/node_modules/ganache/dist/ganache.d.ts b/node_modules/ganache/dist/ganache.d.ts
2+
index d5884c2..56b7392 100644
3+
--- a/node_modules/ganache/dist/ganache.d.ts
4+
+++ b/node_modules/ganache/dist/ganache.d.ts
5+
@@ -7061,7 +7061,7 @@ declare class EthereumProvider_2 extends Emittery<{
6+
disconnect: () => Promise<void>;
7+
}
8+
9+
-declare type EthereumProviderOptions = Partial<{
10+
+export declare type EthereumProviderOptions = Partial<{
11+
[K in keyof EthereumConfig]: ExternalConfig<EthereumConfig[K]>;
12+
}>;
13+
+106-18
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,17 @@
1-
import {providers, utils} from 'ethers';
1+
import {utils} from 'ethers';
2+
import {parseTransaction} from 'ethers/lib/utils';
3+
import type {Provider} from 'ganache';
24

35
export interface RecordedCall {
46
readonly address: string | undefined;
57
readonly data: string;
68
}
79

10+
/**
11+
* CallHistory gathers a log of queries and transactions
12+
* sent to a blockchain provider.
13+
* It is used by the `calledOnContract` matcher.
14+
*/
815
export class CallHistory {
916
private recordedCalls: RecordedCall[] = []
1017

@@ -16,25 +23,74 @@ export class CallHistory {
1623
return this.recordedCalls;
1724
}
1825

19-
record(provider: providers.Web3Provider) {
20-
addVmListener(provider, 'beforeMessage', (message) => {
21-
this.recordedCalls.push(toRecordedCall(message));
26+
record(provider: Provider): Provider {
27+
// Required for the Proxy object.
28+
// eslint-disable-next-line @typescript-eslint/no-this-alias
29+
const callHistory = this;
30+
31+
/**
32+
* One needs to register any `ganache:*` event handler
33+
* after a `connect` event is emitted.
34+
* After that we can consider the provider to be initialized.
35+
* Otherwise some internal object might not have been created yet,
36+
* and there is a silently ignored error deep in ganache / ethereum VM.
37+
*/
38+
(provider as any).on('connect', () => {
39+
/**
40+
* A single step over a single opcode inside the EVM.
41+
* We use it to intercept `CALL` and `STATICCALL` opcodes,
42+
* and track a history of internal calls between smart contracts.
43+
*/
44+
(provider as any).on('ganache:vm:tx:step', (args: any) => {
45+
if (['CALL', 'STATICCALL'].includes(args.data.opcode.name)) {
46+
try {
47+
callHistory.recordedCalls.push(toRecordedCall(decodeCallData(args.data)));
48+
} catch (err) {
49+
console.log(err);
50+
}
51+
}
52+
});
2253
});
23-
}
24-
}
2554

26-
function addVmListener(
27-
provider: providers.Web3Provider,
28-
event: string,
29-
handler: (value: any) => void
30-
) {
31-
const {blockchain} = (provider.provider as any).engine.manager.state;
32-
const createVMFromStateTrie = blockchain.createVMFromStateTrie;
33-
blockchain.createVMFromStateTrie = function (...args: any[]) {
34-
const vm = createVMFromStateTrie.apply(this, args);
35-
vm.on(event, handler);
36-
return vm;
37-
};
55+
/**
56+
* We override the ganache provider with a proxy,
57+
* that hooks into a `provider.request()` method.
58+
*
59+
* All other methods and properties are left intact.
60+
*/
61+
return new Proxy(provider, {
62+
get(target, prop, receiver) {
63+
const original = (target as any)[prop as any];
64+
if (typeof original !== 'function') {
65+
// Some non-method property - returned as-is.
66+
return original;
67+
}
68+
// Return a function override.
69+
return function (...args: any[]) {
70+
// Get a function result from the original provider.
71+
const originalResult = original.apply(target, args);
72+
73+
// Every method other than `provider.request()` left intact.
74+
if (prop !== 'request') return originalResult;
75+
76+
const method = args[0]?.method;
77+
/**
78+
* A method can be:
79+
* - `eth_call` - a query to the node,
80+
* - `eth_sendRawTransaction` - a transaction,
81+
* - `eth_estimateGas` - gas estimation, typically precedes `eth_sendRawTransaction`.
82+
*/
83+
if (method === 'eth_call') { // Record a query.
84+
callHistory.recordedCalls.push(toRecordedCall(args[0]?.params?.[0]));
85+
} else if (method === 'eth_sendRawTransaction') { // Record a transaction.
86+
const parsedTx = parseTransaction(args[0]?.params?.[0]);
87+
callHistory.recordedCalls.push(toRecordedCall(parsedTx));
88+
}
89+
return originalResult;
90+
};
91+
}
92+
});
93+
}
3894
}
3995

4096
function toRecordedCall(message: any): RecordedCall {
@@ -43,3 +99,35 @@ function toRecordedCall(message: any): RecordedCall {
4399
data: message.data ? utils.hexlify(message.data) : '0x'
44100
};
45101
}
102+
103+
/**
104+
* Decodes the arguments of CALLs and STATICCALLs taken from a traced step in EVM execution.
105+
* Source of the arguments: ethervm.io
106+
*/
107+
function decodeCallData(callData: any) {
108+
let addr: Buffer, argsOffset: Buffer, argsLength: Buffer;
109+
if (callData.opcode.name === 'CALL') {
110+
[, addr, , argsOffset, argsLength] = [...callData.stack].reverse();
111+
} else if (callData.opcode.name === 'STATICCALL') {
112+
[, addr, argsOffset, argsLength] = [...callData.stack].reverse();
113+
} else {
114+
throw new Error(`Unsupported call type for decoding call data: ${callData.opcode.name}`);
115+
}
116+
117+
const decodedCallData = callData.memory
118+
.slice(decodeNumber(argsOffset), decodeNumber(argsOffset) + decodeNumber(argsLength));
119+
120+
return {
121+
to: addr,
122+
data: decodedCallData
123+
};
124+
}
125+
126+
/**
127+
* Decodes a number taken from EVM execution step
128+
* into a JS number.
129+
*/
130+
function decodeNumber(data: Buffer): number {
131+
const newData = Buffer.concat([data, Buffer.alloc(32, 0)]);
132+
return newData.readUInt32LE();
133+
}

‎waffle-provider/src/MockProvider.ts

+38-6
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,59 @@
11
import {providers, Wallet} from 'ethers';
22
import {CallHistory, RecordedCall} from './CallHistory';
33
import {defaultAccounts} from './defaultAccounts';
4-
import type Ganache from 'ganache-core';
4+
import type {EthereumProviderOptions, Provider} from 'ganache';
5+
56
import {deployENS, ENS} from '@ethereum-waffle/ens';
7+
import {injectRevertString} from './revertString';
68

79
export {RecordedCall};
810

911
interface MockProviderOptions {
10-
ganacheOptions: Ganache.IProviderOptions;
12+
ganacheOptions: EthereumProviderOptions;
1113
}
1214

1315
export class MockProvider extends providers.Web3Provider {
1416
private _callHistory: CallHistory
1517
private _ens?: ENS;
1618

1719
constructor(private options?: MockProviderOptions) {
18-
super(require('ganache-core').provider({accounts: defaultAccounts, ...options?.ganacheOptions}) as any);
19-
this._callHistory = new CallHistory();
20-
this._callHistory.record(this);
20+
const mergedOptions: EthereumProviderOptions = {
21+
wallet: {
22+
accounts: defaultAccounts
23+
},
24+
logging: {quiet: true},
25+
chain: {
26+
hardfork: 'berlin'
27+
},
28+
...options?.ganacheOptions
29+
};
30+
const provider: Provider = require('ganache').provider(mergedOptions);
31+
const callHistory = new CallHistory();
32+
const patchedProvider = injectRevertString(callHistory.record(provider));
33+
34+
super(patchedProvider as any);
35+
this._callHistory = callHistory;
36+
37+
/**
38+
* The override to the provider's formatter allows us to inject
39+
* additional values to a transaction receipt.
40+
* We inject a `revertString` in overriden `eth_getTransactionReceipt` handler.
41+
* Ethers do not bubble up a revert error message when a transaction reverts,
42+
* but it does bubble it up when a call (query) reverts.
43+
* In order to make the revert string accessible for matchers like `revertedWith`,
44+
* we need to simulate transactions as queries and add the revert string to the receipt.
45+
*/
46+
(this.formatter as any).formats = {
47+
...this.formatter.formats,
48+
receipt: {
49+
...this.formatter.formats.receipt,
50+
revertString: (val: any) => val
51+
}
52+
};
2153
}
2254

2355
getWallets() {
24-
const items = this.options?.ganacheOptions.accounts ?? defaultAccounts;
56+
const items = this.options?.ganacheOptions.wallet?.accounts ?? defaultAccounts;
2557
return items.map((x: any) => new Wallet(x.secretKey, this));
2658
}
2759

‎waffle-provider/src/defaultAccounts.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
const balance = '10000000000000000000000000000000000';
1+
const balance = '0x1ED09BEAD87C0378D8E6400000000'; // 10^34
22

33
const privateKeys = [
44
'0x29f3edee0ad3abf8e2699402e0e28cd6492c9be7eaab00d732a791c33552f797',

‎waffle-provider/src/index.ts

+1
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
export * from './MockProvider';
22
export * from './fixtures';
33
export * from './defaultAccounts';
4+
export * from './revertString';
+89
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
import {providers} from 'ethers';
2+
import {toUtf8String} from 'ethers/lib/utils';
3+
import {Provider} from 'ganache';
4+
5+
/* eslint-disable no-control-regex */
6+
7+
/**
8+
* Decodes a revert string from a failed call/query that reverts on chain.
9+
* @param callRevertError The error catched from performing a reverting call (query)
10+
*/
11+
export const decodeRevertString = (callRevertError: any): string => {
12+
/**
13+
* https://ethereum.stackexchange.com/a/66173
14+
* Numeric.toHexString(Hash.sha3("Error(string)".getBytes())).substring(0, 10)
15+
*/
16+
const errorMethodId = '0x08c379a0';
17+
18+
if (!callRevertError.data?.startsWith(errorMethodId)) return '';
19+
return toUtf8String('0x' + callRevertError.data.substring(138))
20+
.replace(/\x00/g, ''); // Trim null characters.
21+
};
22+
23+
/**
24+
* Ethers executes a gas estimation before sending the transaction to the blockchain.
25+
* This poses a problem for Waffle - we cannot track sent transactions which eventually revert.
26+
* This is a common use case for testing, but such transaction never gets sent.
27+
* A failed gas estimation prevents it from being sent.
28+
*
29+
* In test environment, we replace the gas estimation with an always-succeeding method.
30+
* If a transaction is meant to be reverted, it will do so after it is actually send and mined.
31+
*
32+
* Additionally, we patch the method for getting transaction receipt.
33+
* Ethers does not provide the error code in the receipt that we can use to
34+
* read a revert string, so we patch it and include it using a query to the blockchain.
35+
*/
36+
export const injectRevertString = (provider: Provider): Provider => {
37+
return new Proxy(provider, {
38+
get(target, prop, receiver) {
39+
const original = (target as any)[prop as any];
40+
if (typeof original !== 'function') {
41+
// Some non-method property - returned as-is.
42+
return original;
43+
}
44+
// Return a function override.
45+
return function (...args: any[]) {
46+
// Get a function result from the original provider.
47+
const originalResult = original.apply(target, args);
48+
49+
// Every method other than `provider.request()` left intact.
50+
if (prop !== 'request') return originalResult;
51+
52+
const method = args[0]?.method;
53+
/**
54+
* A method can be:
55+
* - `eth_estimateGas` - gas estimation, typically precedes `eth_sendRawTransaction`.
56+
* - `eth_getTransactionReceipt` - getting receipt of sent transaction,
57+
* typically supersedes `eth_sendRawTransaction`.
58+
* Other methods left intact.
59+
*/
60+
if (method === 'eth_estimateGas') {
61+
return (async () => {
62+
try {
63+
return await originalResult;
64+
} catch (e) {
65+
return '0xE4E1C0'; // 15_000_000
66+
}
67+
})();
68+
} else if (method === 'eth_getTransactionReceipt') {
69+
return (async () => {
70+
const receipt = await originalResult;
71+
if (parseInt(receipt.status) === 0) {
72+
// A reverted transaction. We try to add a revert string to the receipt.
73+
try {
74+
const etherProvider = new providers.Web3Provider(provider as any);
75+
const tx = await etherProvider.getTransaction(receipt.transactionHash);
76+
// Run the transaction as a query. It works differently in Ethers, a revert code is included.
77+
await etherProvider.call(tx as any, tx.blockNumber);
78+
} catch (error: any) {
79+
receipt.revertString = decodeRevertString(error);
80+
}
81+
}
82+
return receipt;
83+
})();
84+
}
85+
return originalResult; // Fallback for any other method.
86+
};
87+
}
88+
});
89+
};

‎waffle-provider/test/MockProvider.test.ts

+3-1
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,9 @@ describe('INTEGRATION: MockProvider', () => {
2020
const original = Wallet.createRandom();
2121
const provider = new MockProvider({
2222
ganacheOptions: {
23-
accounts: [{balance: '100', secretKey: original.privateKey}]
23+
wallet: {
24+
accounts: [{balance: '0x64', secretKey: original.privateKey}]
25+
}
2426
}
2527
});
2628
const wallets = provider.getWallets();

‎waffle-provider/test/callHistory.test.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,8 @@ describe('INTEGRATION: MockProvider.callHistory', () => {
8080
const token = await deployToken(wallet, 10);
8181

8282
provider.clearCallHistory();
83-
await expect(token.transfer(wallet.address, 20)).to.be.rejected;
83+
const transferTx = await token.transfer(wallet.address, 20);
84+
await expect(transferTx.wait()).to.be.eventually.rejected;
8485

8586
expect(provider.callHistory).to.deep.include({
8687
address: token.address,
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import {expect} from 'chai';
2+
import {constants} from 'ethers';
3+
import {MockProvider} from '../src/MockProvider';
4+
import {decodeRevertString} from '../src/revertString';
5+
import {deployToken} from './BasicToken';
6+
7+
describe('INTEGRATION: MockProvider.callHistory', () => {
8+
it('decodes revert strings from calls', async () => {
9+
const provider = new MockProvider();
10+
const [wallet] = provider.getWallets();
11+
12+
const token = await deployToken(wallet, 10);
13+
14+
const transferTx = await token.transfer(constants.AddressZero, 1);
15+
try {
16+
await transferTx.wait();
17+
} catch (transactionError: any) {
18+
const receipt = transactionError.receipt;
19+
const revertedTx = await provider.getTransaction(receipt.transactionHash);
20+
try {
21+
await provider.call(revertedTx as any, revertedTx.blockNumber);
22+
} catch (callError: any) {
23+
const revertString = decodeRevertString(callError);
24+
expect(revertString).to.be.equal('Invalid address');
25+
}
26+
}
27+
});
28+
});

0 commit comments

Comments
 (0)
Please sign in to comment.