or run

npx @tessl/cli init
Log in

Version

Tile

Overview

Evals

Files

docs

chai-matchers.mdcompiler-utilities.mdcontract-deployment.mdens-utilities.mdindex.mdmock-contracts.mdoptimism-provider.mdprovider-testing.md
tile.json

chai-matchers.mddocs/

Chai Matchers

Comprehensive set of 17+ custom chai matchers specifically designed for smart contract testing, including transaction revert checking, event emission testing, balance change assertions, contract interaction verification, and enhanced value comparisons.

Capabilities

Chai Plugin Setup

Main plugin function that extends Chai with all Waffle-specific matchers for smart contract testing.

/**
 * Chai plugin providing custom matchers for smart contract testing
 * @param chai - Chai instance to extend
 * @param utils - Chai utilities for assertion building
 */
function solidity(chai: Chai.ChaiStatic, utils: Chai.ChaiUtils): void;

Usage Examples:

import { use, expect } from "chai";
import { solidity } from "ethereum-waffle";

// Enable Waffle matchers
use(solidity);

// Now all Waffle matchers are available on expect()
await expect(transaction).to.be.reverted;
await expect(contract.transfer(to, amount)).to.emit(contract, "Transfer");

Transaction Matchers

Matchers for testing transaction success, failure, and revert behavior with detailed reason checking.

/**
 * Assert that transaction reverted
 */
.reverted: AsyncAssertion;

/**
 * Assert that transaction reverted with specific reason
 * @param reason - Expected revert reason (string or RegExp)
 * @returns Assertion with argument matching support
 */
.revertedWith(reason: string | RegExp): RevertedWithAssertion;

/**
 * Fluent interface for revertedWith assertions
 */
interface RevertedWithAssertion extends AsyncAssertion {
  /** Match specific revert arguments */
  withArgs(...args: any[]): AsyncAssertion;
}

Usage Examples:

import { expect } from "chai";

// Test transaction reverts
await expect(contract.restrictedFunction()).to.be.reverted;

// Test specific revert reasons
await expect(contract.withdraw(1000)).to.be.revertedWith("Insufficient balance");

// Test revert with regex pattern
await expect(contract.divide(10, 0)).to.be.revertedWith(/division by zero/i);

// Test custom error with arguments (Solidity 0.8.4+)
await expect(contract.validateAmount(0))
  .to.be.revertedWith("InvalidAmount")
  .withArgs(0, "Amount must be positive");

Event Emission Matchers

Comprehensive event testing with argument matching, named parameter support, and multiple event assertions.

/**
 * Assert that transaction emitted specific event
 * @param contract - Contract that should emit the event
 * @param eventName - Name of the event to check
 * @returns Assertion with argument matching support
 */
.emit(contract: Contract, eventName: string): EmitAssertion;

/**
 * Alternative syntax using event signature
 * @param eventSig - Full event signature string
 * @returns Assertion with argument matching support  
 */
.emit(eventSig: string): EmitAssertion;

/**
 * Fluent interface for event emission assertions
 */
interface EmitAssertion extends AsyncAssertion {
  /** Match specific event arguments by position */
  withArgs(...args: any[]): AsyncAssertion;
  
  /** Match specific event arguments by name */
  withNamedArgs(args: Record<string, unknown>): AsyncAssertion;
}

Usage Examples:

import { expect } from "chai";

// Basic event emission
await expect(contract.transfer(to, amount))
  .to.emit(contract, "Transfer");

// Event with argument matching
await expect(contract.transfer(to, 100))
  .to.emit(contract, "Transfer")
  .withArgs(wallet.address, to, 100);

// Event with named arguments  
await expect(contract.mint(to, 500))
  .to.emit(contract, "Transfer")
  .withNamedArgs({
    from: ethers.constants.AddressZero,
    to: to,
    value: 500
  });

// Multiple events from same transaction
await expect(contract.complexOperation())
  .to.emit(contract, "OperationStarted")
  .withArgs(1)
  .and.to.emit(contract, "OperationCompleted")
  .withArgs(1, true);

// Event signature syntax
await expect(contract.approve(spender, amount))
  .to.emit("Approval(address,address,uint256)")
  .withArgs(wallet.address, spender, amount);

Balance Change Matchers

Comprehensive balance testing for ETH and ERC20 tokens with fee inclusion options and error margins.

/**
 * Assert ETH balance change for single account
 * @param account - Account to check balance change for
 * @param balance - Expected balance change amount
 * @param options - Additional options for balance checking
 * @returns Async assertion
 */
.changeEtherBalance(
  account: Account, 
  balance: BigNumberish, 
  options?: BalanceChangeOptions
): AsyncAssertion;

/**
 * Assert ETH balance changes for multiple accounts
 * @param accounts - Array of accounts to check
 * @param balances - Array of expected balance changes
 * @param options - Additional options for balance checking
 * @returns Async assertion
 */
.changeEtherBalances(
  accounts: Account[], 
  balances: BigNumberish[], 
  options?: BalanceChangeOptions
): AsyncAssertion;

/**
 * Assert ERC20 token balance change for single account
 * @param token - ERC20 token contract
 * @param account - Account to check balance change for
 * @param balance - Expected balance change amount
 * @param errorMargin - Acceptable error margin for balance check
 * @returns Async assertion
 */
.changeTokenBalance(
  token: Contract, 
  account: Account, 
  balance: BigNumberish, 
  errorMargin?: BigNumberish
): AsyncAssertion;

/**
 * Assert ERC20 token balance changes for multiple accounts
 * @param token - ERC20 token contract
 * @param accounts - Array of accounts to check
 * @param balances - Array of expected balance changes
 * @param errorMargin - Acceptable error margin for balance checks
 * @returns Async assertion
 */
.changeTokenBalances(
  token: Contract, 
  accounts: Account[], 
  balances: BigNumberish[], 
  errorMargin?: BigNumberish
): AsyncAssertion;

/**
 * Options for balance change assertions
 */
interface BalanceChangeOptions {
  /** Include transaction fees in balance calculation */
  includeFee?: boolean;
  /** Acceptable error margin for balance check */
  errorMargin?: BigNumberish;
}

/**
 * Account type for balance assertions
 */
type Account = Signer | Contract;

Usage Examples:

import { expect } from "chai";
import { ethers } from "ethers";

// ETH balance changes
await expect(() => 
  wallet.sendTransaction({
    to: otherWallet.address,
    value: ethers.utils.parseEther("1.0")
  })
).to.changeEtherBalance(otherWallet, ethers.utils.parseEther("1.0"));

// Multiple ETH balance changes
await expect(() => 
  contract.distribute([addr1, addr2], [100, 200])
).to.changeEtherBalances(
  [addr1, addr2], 
  [ethers.utils.parseEther("0.1"), ethers.utils.parseEther("0.2")]
);

// ETH balance with fee inclusion
await expect(() => 
  wallet.sendTransaction({to: otherWallet.address, value: 1000})
).to.changeEtherBalance(wallet, -1000, { includeFee: true });

// Token balance changes
await expect(() => 
  token.transfer(otherWallet.address, 100)
).to.changeTokenBalance(token, otherWallet, 100);

// Token balance with error margin
await expect(() => 
  stakingContract.unstake(1000)
).to.changeTokenBalance(token, wallet, 1000, 5); // Allow ±5 token difference

// Multiple token balance changes
await expect(() => 
  token.batchTransfer([addr1, addr2], [50, 75])
).to.changeTokenBalances(token, [addr1, addr2], [50, 75]);

Call History Matchers

Matchers for verifying contract function calls and their parameters during testing.

/**
 * Assert that function was called on specific contract
 * @param contract - Contract that should have been called
 */
.calledOnContract(contract: Contract): void;

/**
 * Assert that function was called on contract with specific parameters
 * @param contract - Contract that should have been called
 * @param parameters - Expected parameters for the call
 */
.calledOnContractWith(contract: Contract, parameters: any[]): void;

Usage Examples:

import { expect } from "chai";
import { MockProvider } from "ethereum-waffle";

const provider = new MockProvider();
const [wallet] = provider.getWallets();

// Test contract interactions
await contract.complexOperation();

// Verify calls were made
expect("setValue").to.be.calledOnContract(contract);
expect("setValue").to.be.calledOnContractWith(contract, [42]);

// Works with mock contracts too
const mockToken = await deployMockContract(wallet, ERC20.abi);
await contract.useToken(mockToken.address);

expect("transfer").to.be.calledOnContract(mockToken);
expect("transfer").to.be.calledOnContractWith(mockToken, [wallet.address, 100]);

Value and Format Matchers

Specialized matchers for Ethereum-specific value formats and BigNumber comparisons.

/**
 * Assert proper hexadecimal format with specific length
 * @param length - Expected hex string length (without 0x prefix)
 */
.properHex(length: number): void;

/**
 * Assert hexadecimal values are equal (case-insensitive)
 * @param other - Hex string to compare with
 */
.hexEqual(other: string): void;

/**
 * Assert value is a valid Ethereum private key
 */
.properPrivateKey: void;

/**
 * Assert value is a valid Ethereum address
 */
.properAddress: void;

// BigNumber matchers extend existing Chai number matchers
// All standard comparison operators work with BigNumber values

Usage Examples:

import { expect } from "chai";

// Hex format validation
expect("0x1234abcd").to.be.properHex(8);
expect("0x").to.be.properHex(0);

// Hex equality (case-insensitive)
expect("0xabcd").to.hexEqual("0xABCD");

// Address validation
expect("0x742d35Cc6634C0532925a3b8D2Ba4BB4A3c6b8D8").to.be.properAddress;
expect("0x123").to.not.be.properAddress; // Too short

// Private key validation
expect("0x60ddfe7f579ab6867cbe7a2dc03853dc141d7a4ab6dbefc0dda2c93d6c3e12dd").to.be.properPrivateKey;

// BigNumber comparisons
const bn1 = ethers.BigNumber.from("1000000000000000000");
const bn2 = ethers.BigNumber.from("2000000000000000000");

expect(bn2).to.be.above(bn1);
expect(bn1).to.be.below(bn2);
expect(bn1).to.equal(ethers.utils.parseEther("1"));

Deprecated Matchers

Legacy balance matchers maintained for backward compatibility. These matchers are deprecated but still functional for existing codebases.

/**
 * @deprecated Use changeEtherBalance instead
 * Assert ETH balance change for single account
 * @param account - Account to check balance change for
 * @param balance - Expected balance change amount
 * @returns Async assertion
 */
.changeBalance(account: Account, balance: BigNumberish): AsyncAssertion;

/**
 * @deprecated Use changeEtherBalances instead  
 * Assert ETH balance changes for multiple accounts
 * @param accounts - Array of accounts to check
 * @param balances - Array of expected balance changes
 * @returns Async assertion
 */
.changeBalances(accounts: Account[], balances: BigNumberish[]): AsyncAssertion;

Migration Examples:

// Old (deprecated) - still works but not recommended
await expect(() => 
  wallet.sendTransaction({
    to: otherWallet.address,
    value: ethers.utils.parseEther("1.0")
  })
).to.changeBalance(otherWallet, ethers.utils.parseEther("1.0"));

await expect(() => 
  contract.distributeTokens([addr1, addr2], [100, 200])
).to.changeBalances([addr1, addr2], [100, 200]);

// New (recommended) - preferred approach
await expect(() => 
  wallet.sendTransaction({
    to: otherWallet.address,
    value: ethers.utils.parseEther("1.0")
  })
).to.changeEtherBalance(otherWallet, ethers.utils.parseEther("1.0"));

await expect(() => 
  contract.distributeTokens([addr1, addr2], [100, 200])
).to.changeEtherBalances([addr1, addr2], [100, 200]);

Why Migration is Recommended:

  • Clarity: changeEtherBalance makes it explicit that ETH balances are being tested
  • Consistency: Matches naming pattern with changeTokenBalance
  • Features: New matchers support additional options like includeFee and errorMargin
  • Maintenance: Deprecated matchers may be removed in future versions