Skip to content

Instantly share code, notes, and snippets.

@mesquka
Last active March 18, 2024 11:06
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save mesquka/529627ab5482580df8fb1e7f559b9322 to your computer and use it in GitHub Desktop.
Save mesquka/529627ab5482580df8fb1e7f559b9322 to your computer and use it in GitHub Desktop.
Chai ethersjs v5 struct/return value matcher
import { Result } from '@ethersproject/abi';
import chaiLocal from 'chai';
import { ethers } from 'ethers';
declare global {
// eslint-disable-next-line @typescript-eslint/no-namespace
export namespace Chai {
interface Assertion {
matchStruct(expected: unknown): void;
}
}
}
/**
* Recursively check ethers return object to ensure values match
* @param chai - chai object
* @param ethersValue - ethers value
* @param expectedValue - expected value
* @returns matches
*/
function matchStruct(chai: Chai.ChaiStatic, ethersValue: unknown, expectedValue: unknown) {
const { expect } = chai;
// 'Magic' match any set value
if (expectedValue === 'any' && ethersValue) {
return true;
}
// TODO: ethers v6 update, change to using Result.toObject()
// This works fine for the current contracts as no keys are numbers in any structs
if (Array.isArray(ethersValue) && ethersValue.length !== Object.keys(ethersValue).length) {
// If array, and array length is not equal to keys length, this is an object
// Assert expected value is also an object
expect(expectedValue).to.be.an('object');
// Cast expected value to object
const expectedValueAsObject = expectedValue as Record<string, unknown>;
// Cast ethersValue to object
const ethersValueAsObject = ethersValue as unknown as Record<string, unknown>;
// Test all expected value keys exist on ethers value
Object.keys(expectedValueAsObject).forEach((key) => {
expect(ethersValueAsObject).to.have.property(key);
});
// Filter out numerical values (array position keys), recursively test remaining
Object.keys(ethersValueAsObject)
.filter((key) => !/^\d+$/.test(key))
.forEach((key) => {
// Assert expected value also has key
expect(expectedValue).to.have.property(key);
// Recursively test sub-objects
matchStruct(chai, ethersValueAsObject[key] as Result, expectedValueAsObject[key]);
});
return true;
}
// Is regular array
if (Array.isArray(ethersValue)) {
// Assert expected value is also an array
expect(expectedValue).to.be.an('array');
// Cast expected value to array
const expectedValueAsArray = expectedValue as unknown[];
// Assert array lengths are the same
expect(ethersValue.length).to.equal(expectedValueAsArray.length);
// Check each array value recursively
ethersValue.forEach((value, i) => matchStruct(chai, value, expectedValueAsArray[i]));
return true;
}
// Check if string
if (typeof ethersValue === 'string') {
// If expected value is strictly equal (is string and same value), no further checks needed
if (ethersValue === expectedValue) return true;
// If string test doesn't match, check if ethers value is bytes (address is treated as bytes)
// eslint-disable-next-line @typescript-eslint/no-unused-expressions
expect(ethersValue.startsWith('0x'), "String doesn't match, and not bytes").to.be.true;
// Check bytes string matches
if (typeof expectedValue === 'string') {
// Ensure 0x prefixed
const expectedValuePrefixed = expectedValue.startsWith('0x')
? expectedValue
: `0x${expectedValue}`;
// Check value matches
expect(ethersValue.toLowerCase()).to.equal(expectedValuePrefixed.toLowerCase());
return true;
}
// Check bytes array matches
if (expectedValue instanceof Uint8Array) {
// Convert to 0x prefixed hex string
const expectedValuePrefixed = ethers.hexlify(expectedValue);
// Check value matches
expect(ethersValue.toLowerCase()).to.equal(expectedValuePrefixed.toLowerCase());
return true;
}
expect.fail(
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions
`${ethersValue} (${typeof ethersValue}) isn't bytes, but ${expectedValue} (${typeof expectedValue}) is not`,
);
}
// Numerical value
if (typeof ethersValue === 'number' || typeof ethersValue === 'bigint') {
// Check numerical values match
expect(ethersValue).to.equal(expectedValue);
return true;
}
// Boolean value
if (typeof ethersValue === 'boolean') {
// Check numerical values match
expect(ethersValue).to.equal(expectedValue);
return true;
}
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions
expect.fail(`${ethersValue} (${typeof ethersValue}) isn't a known type`);
return false;
}
/**
* Generates withArgs matcher
* @param expectedValue - expected value
* @returns withArgs matcher
*/
function argStruct(expectedValue: unknown) {
return function (ethersValue: unknown) {
return matchStruct(chaiLocal, ethersValue, expectedValue);
};
}
chaiLocal.use((chai) => {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
chai.Assertion.addMethod('matchStruct', function (expectedValue: Result) {
matchStruct(chai, this._obj, expectedValue);
});
});
export { argStruct };
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment