Skip to content

Commit

Permalink
⭐️ Support events not defined in a contract (#822)
Browse files Browse the repository at this point in the history
* ⭐️ Support events not defined in a contract
  • Loading branch information
pawelpolak2 authored Feb 6, 2023
1 parent a1d89d0 commit 4d83cde
Show file tree
Hide file tree
Showing 6 changed files with 188 additions and 35 deletions.
6 changes: 6 additions & 0 deletions .changeset/hip-adults-guess.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"@ethereum-waffle/mock-contract": patch
"@ethereum-waffle/chai": patch
---

Emit matcher improvement
84 changes: 51 additions & 33 deletions waffle-chai/src/matchers/emit.ts
Original file line number Diff line number Diff line change
@@ -1,70 +1,88 @@
import {Contract, providers, utils} from 'ethers';
import {keccak256, toUtf8Bytes} from 'ethers/lib/utils';
import {callPromise} from '../call-promise';
import {waitForPendingTransaction} from './misc/transaction';
import {supportWithArgs} from './withArgs';
import {supportWithNamedArgs} from './withNamedArgs';

export function supportEmit(Assertion: Chai.AssertionStatic) {
const filterLogsWithTopics = (logs: providers.Log[], topic: any, contractAddress: string) =>
const filterLogsWithTopics = (logs: providers.Log[], topic: any, contractAddress?: string) =>
logs.filter((log) => log.topics.includes(topic))
.filter((log) => log.address && log.address.toLowerCase() === contractAddress.toLowerCase());
.filter((log) =>
log.address &&
(contractAddress === undefined || log.address.toLowerCase() === contractAddress.toLowerCase()
));

Assertion.addMethod('emit', function (this: any, contract: Contract, eventName: string) {
const assertEmit = (assertion: any, frag: utils.EventFragment, isNegated: boolean, from?: string) => {
const topic = keccak256(toUtf8Bytes(frag.format()));
const receipt: providers.TransactionReceipt = assertion.txReceipt;
assertion.args = filterLogsWithTopics(receipt.logs, topic, from);
const isCurrentlyNegated = assertion.__flags.negate === true;
assertion.__flags.negate = isNegated;
assertion.assert(assertion.args.length > 0,
`Expected event "${frag.name}" to be emitted, but it wasn't`,
`Expected event "${frag.name}" NOT to be emitted, but it was`
);
assertion.__flags.negate = isCurrentlyNegated;
};

Assertion.addMethod('emit', function (this: any, contractOrEventSig: Contract|string, eventName?: string) {
if (typeof this._obj === 'string') {
if (typeof contractOrEventSig === 'string') {
throw new Error('The emit by event signature matcher must be called on a transaction');
}
// Handle specific case of using transaction hash to specify transaction. Done for backwards compatibility.
this.callPromise = waitForPendingTransaction(this._obj, contract.provider)
this.callPromise = waitForPendingTransaction(this._obj, contractOrEventSig.provider)
.then(txReceipt => {
this.txReceipt = txReceipt;
});
} else {
callPromise(this);
}
const isNegated = this.__flags.negate === true;
this.callPromise = this.callPromise
.then(() => {
if (!('txReceipt' in this)) {
throw new Error('The emit matcher must be called on a transaction');
this.callPromise = this.callPromise.then(() => {
if (!('txReceipt' in this)) {
throw new Error('The emit matcher must be called on a transaction');
}
let eventFragment: utils.EventFragment | undefined;
if (typeof contractOrEventSig === 'string') {
try {
eventFragment = utils.EventFragment.from(contractOrEventSig);
} catch (e) {
throw new Error(`Invalid event signature: "${contractOrEventSig}"`);
}
const receipt: providers.TransactionReceipt = this.txReceipt;
let eventFragment: utils.EventFragment | undefined;
assertEmit(this, eventFragment, isNegated);
} else if (eventName) {
try {
eventFragment = contract.interface.getEvent(eventName);
eventFragment = contractOrEventSig.interface.getEvent(eventName);
} catch (e) {
// ignore error
// ignore error
}
if (eventFragment === undefined) {
this.assert(
isNegated,
this.__flags.negate,
`Expected event "${eventName}" to be emitted, but it doesn't` +
' exist in the contract. Please make sure you\'ve compiled' +
' its latest version before running the test.',
' exist in the contract. Please make sure you\'ve compiled' +
' its latest version before running the test.',
`WARNING: Expected event "${eventName}" NOT to be emitted.` +
' The event wasn\'t emitted because it doesn\'t' +
' exist in the contract. Please make sure you\'ve compiled' +
' its latest version before running the test.',
' The event wasn\'t emitted because it doesn\'t' +
' exist in the contract. Please make sure you\'ve compiled' +
' its latest version before running the test.',
eventName,
''
);
return;
}
assertEmit(this, eventFragment, isNegated, contractOrEventSig.address);

this.contract = contractOrEventSig;
} else {
throw new Error('The emit matcher must be called with a contract and an event name or an event signature');
}
});

const topic = contract.interface.getEventTopic(eventFragment);
this.args = filterLogsWithTopics(receipt.logs, topic, contract.address);
// As this callback will be resolved after the chain of matchers is finished, we need to
// know if the matcher has been negated or not. To simulate chai behaviour, we keep track of whether
// the matcher has been negated or not and set the internal chai flag __flags.negate to the same value.
// After the assertion is finished, we set the flag back to original value to not affect other assertions.
const isCurrentlyNegated = this.__flags.negate === true;
this.__flags.negate = isNegated;
this.assert(this.args.length > 0,
`Expected event "${eventName}" to be emitted, but it wasn't`,
`Expected event "${eventName}" NOT to be emitted, but it was`
);
this.__flags.negate = isCurrentlyNegated;
});
this.then = this.callPromise.then.bind(this.callPromise);
this.catch = this.callPromise.catch.bind(this.callPromise);
this.contract = contract;
this.eventName = eventName;
this.txMatcher = 'emit';
return this;
Expand Down
2 changes: 1 addition & 1 deletion waffle-chai/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ declare namespace Chai {
interface Assertion extends LanguageChains, NumericComparison, TypeComparison {
reverted: AsyncAssertion;
revertedWith(reason: string | RegExp): RevertedWithAssertion;
emit(contract: any, eventName: string): EmitAssertion;
emit(contractOrEventSig: any, eventName?: string): EmitAssertion;
properHex(length: number): void;
hexEqual(other: string): void;
properPrivateKey: void;
Expand Down
68 changes: 68 additions & 0 deletions waffle-chai/test/contracts/EventsProxy.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
export const EVENTSPROXY_SOURCE = `
pragma solidity ^0.8.3;
import {Events} from "./Events.sol";
contract EventsProxy {
Events public events;
constructor(Events _events) {
events = _events;
}
function emitTwoDelegate() public {
// emit Two with a delegatecall to the events contract
(bool success, ) = address(events).delegatecall(
abi.encodeWithSignature("emitTwo()")
);
require(success, "delegatecall failed");
}
function emitOne() public {
events.emitOne();
}
}`;

export const EVENTSPROXY_ABI = [
{
inputs: [
{
internalType: 'contract Events',
name: '_events',
type: 'address'
}
],
stateMutability: 'nonpayable',
type: 'constructor'
},
{
inputs: [],
name: 'emitOne',
outputs: [],
stateMutability: 'nonpayable',
type: 'function'
},
{
inputs: [],
name: 'emitTwoDelegate',
outputs: [],
stateMutability: 'nonpayable',
type: 'function'
},
{
inputs: [],
name: 'events',
outputs: [
{
internalType: 'contract Events',
name: '',
type: 'address'
}
],
stateMutability: 'view',
type: 'function'
}
];

// eslint-disable-next-line max-len
export const EVENTSPROXY_BYTECODE = '608060405234801561001057600080fd5b506040516105413803806105418339818101604052810190610032919061008d565b806000806101000a81548173ffffffffffffffffffffffffffffffffffffffff021916908373ffffffffffffffffffffffffffffffffffffffff16021790555050610111565b600081519050610087816100fa565b92915050565b60006020828403121561009f57600080fd5b60006100ad84828501610078565b91505092915050565b60006100c1826100da565b9050919050565b60006100d3826100b6565b9050919050565b600073ffffffffffffffffffffffffffffffffffffffff82169050919050565b610103816100c8565b811461010e57600080fd5b50565b610421806101206000396000f3fe608060405234801561001057600080fd5b50600436106100415760003560e01c8063272b33af146100465780633f0e64ba14610050578063b5f8558c1461005a575b600080fd5b61004e610078565b005b6100586101c9565b005b61006261024b565b60405161006f91906102e9565b60405180910390f35b60008060009054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff166040516024016040516020818303038152906040527f34c10115000000000000000000000000000000000000000000000000000000007bffffffffffffffffffffffffffffffffffffffffffffffffffffffff19166020820180517bffffffffffffffffffffffffffffffffffffffffffffffffffffffff838183161783525050505060405161014291906102d2565b600060405180830381855af49150503d806000811461017d576040519150601f19603f3d011682016040523d82523d6000602084013e610182565b606091505b50509050806101c6576040517f08c379a00000000000000000000000000000000000000000000000000000000081526004016101bd90610304565b60405180910390fd5b50565b60008054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16633f0e64ba6040518163ffffffff1660e01b8152600401600060405180830381600087803b15801561023157600080fd5b505af1158015610245573d6000803e3d6000fd5b50505050565b60008054906101000a900473ffffffffffffffffffffffffffffffffffffffff1681565b600061027a82610324565b610284818561032f565b935061029481856020860161038f565b80840191505092915050565b6102a98161036b565b82525050565b60006102bc60138361033a565b91506102c7826103c2565b602082019050919050565b60006102de828461026f565b915081905092915050565b60006020820190506102fe60008301846102a0565b92915050565b6000602082019050818103600083015261031d816102af565b9050919050565b600081519050919050565b600081905092915050565b600082825260208201905092915050565b600073ffffffffffffffffffffffffffffffffffffffff82169050919050565b60006103768261037d565b9050919050565b60006103888261034b565b9050919050565b60005b838110156103ad578082015181840152602081019050610392565b838111156103bc576000848401525b50505050565b7f64656c656761746563616c6c206661696c65640000000000000000000000000060008201525056fea26469706673582212200ce5c4d7353a3aed66c47b692095e41e9a8c9f4b6412620eabf464a32caff56a64736f6c63430008030033';
47 changes: 46 additions & 1 deletion waffle-chai/test/matchers/eventsTest.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import {expect, AssertionError} from 'chai';
import {Wallet, Contract, ContractFactory, BigNumber, ethers} from 'ethers';
import {EVENTS_ABI, EVENTS_BYTECODE} from '../contracts/Events';
import {EVENTSPROXY_ABI, EVENTSPROXY_BYTECODE} from '../contracts/EventsProxy';

import type {TestProvider} from '@ethereum-waffle/provider';

/**
* Struct emitted in the Events contract, emitStruct method
*/
Expand Down Expand Up @@ -698,5 +698,50 @@ export const eventsWithNamedArgs = (provider: TestProvider) => {
'{ Object (hash, value, ...) } to deeply equal { Object (hash, value, ...) }'
);
});

it('Signature only - delegatecall', async () => {
const proxyFactory = new ContractFactory(EVENTSPROXY_ABI, EVENTSPROXY_BYTECODE, wallet);
const proxy = await proxyFactory.deploy(events.address);

await expect(proxy.emitTwoDelegate()).to.emit('Two(uint256,string)');
});

it('Signature only - regular event', async () => {
await expect(events.emitTwo()).to.emit('Two(uint256,string)');
});

it('Signature only - negative', async () => {
await expect(
expect(events.emitTwo()).to.emit('One(uint256,string,bytes32)')
).to.be.eventually.rejectedWith(
AssertionError,
'Expected event "One" to be emitted, but it wasn\'t'
);
});

it('Signature only - invalid event signature', async () => {
await expect(
expect(events.emitTwo()).to.emit('One')
).to.be.eventually.rejectedWith(
Error,
'Invalid event signature: "One"'
);
});

it('Signature only - invalid args', async () => {
await expect(
expect(events.emitTwo()).to.emit(events)
).to.be.eventually.rejectedWith(
Error,
'The emit matcher must be called with a contract and an event name or an event signature'
);
});

it('Signature only - Other contract event', async () => {
const proxyFactory = new ContractFactory(EVENTSPROXY_ABI, EVENTSPROXY_BYTECODE, wallet);
const proxy = await proxyFactory.deploy(events.address);

await expect(proxy.emitOne()).to.emit('One(uint256,string,bytes32)');
});
});
};
16 changes: 16 additions & 0 deletions waffle-mock-contract/test/proxiedTest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,5 +57,21 @@ export function mockContractProxiedTest(provider: MockProvider) {
await mockContract.mock.read.returns(1);
expect(await proxy.readCapped()).to.eq(1);
});

it('calledOnContract with mock contract', async () => {
const {capContract, mockCounter} = await deploy();

await mockCounter.mock.read.returns(1);
await capContract.readCapped();
expect('read').to.be.calledOnContract(mockCounter);
});

it('calledOnContractWith with mock contract', async () => {
const {capContract, mockCounter} = await deploy();

await mockCounter.mock.add.returns(1);
await capContract.addCapped(1);
expect('add').to.be.calledOnContractWith(mockCounter, [1]);
});
});
}

0 comments on commit 4d83cde

Please sign in to comment.