@tevm/test-matchers
Version:
Vite test matchers for Tevm or EVM-related testing in TypeScript.
1,102 lines (1,084 loc) • 47.8 kB
JavaScript
;
var vitest = require('vitest');
var node = require('@tevm/node');
var viem = require('viem');
var actions = require('@tevm/actions');
var actions$1 = require('viem/actions');
var contract = require('@tevm/contract');
var Hex = require('ox/Hex');
var ox = require('ox');
var errors = require('@tevm/errors');
var utils = require('@tevm/utils');
// src/index.ts
var chaiUtils;
var setupPromise = (assertion, utils) => {
const existingPromise = utils.flag(assertion, "callPromise");
if (existingPromise) return;
const obj = utils.flag(assertion, "object");
if (obj && typeof obj.then === "function") {
utils.flag(assertion, "callPromise", obj);
} else {
utils.flag(assertion, "callPromise", Promise.resolve());
}
};
var isAsyncMatcher = (matcher) => {
return matcher.constructor.name === "AsyncFunction";
};
var buildChainState = (assertion, utils) => {
const chainHistory = utils.flag(assertion, "chainHistory") || [];
if (chainHistory.length === 0) {
return {
chainedFrom: void 0,
previousPassed: void 0,
previousValue: void 0,
previousState: void 0,
previousArgs: void 0
};
}
const matcherName = chainHistory[chainHistory.length - 1];
let previousValue = utils.flag(assertion, `${matcherName}.value`);
const currentObj = utils.flag(assertion, "object");
if (previousValue && typeof previousValue.then === "function" && currentObj !== previousValue)
previousValue = currentObj;
const state = {
chainedFrom: matcherName,
previousPassed: utils.flag(assertion, `${matcherName}.passed`),
previousValue,
previousState: utils.flag(assertion, `${matcherName}.state`),
previousArgs: utils.flag(assertion, `${matcherName}.args`)
};
return state;
};
var createInternalResultHandler = () => {
return {
__expectResult: function(result, isNegated) {
vitest.assert(chaiUtils !== void 0, "ChaiUtils not initialized");
const shouldFail = isNegated ? result.pass : !result.pass;
if (shouldFail) {
return {
pass: false,
message: () => result.message(),
actual: result.actual,
expected: result.expected
};
}
return { pass: true, message: () => result.message() };
}
};
};
var expectResult = (result, isNegated) => {
vitest.expect(result).__expectResult(isNegated);
};
var storeChainState = (assertion, utils, name, obj, args, result) => {
utils.flag(assertion, `${name}.passed`, result.pass);
utils.flag(assertion, `${name}.value`, obj);
utils.flag(assertion, `${name}.state`, result.state);
utils.flag(assertion, `${name}.args`, args);
const chainHistory = utils.flag(assertion, "chainHistory") ?? [];
chainHistory.push(name);
utils.flag(assertion, "chainHistory", chainHistory);
};
var parseChainArgs = (args) => {
const argsWithoutChainState = args.slice(0, -1);
const chainState = args[args.length - 1];
vitest.assert(
chainState && typeof chainState === "object" && "chainedFrom" in chainState,
"Internal error: no chain state found"
);
return { args: argsWithoutChainState, chainState };
};
function makeVitestSyncChainable(name, vitestMatcher, args) {
vitest.assert(chaiUtils !== void 0, "ChaiUtils not initialized");
const callPromise = chaiUtils.flag(this, "callPromise");
if (callPromise && typeof callPromise.then === "function") {
return makeVitestAsyncChainable.call(this, name, vitestMatcher, args);
}
const obj = chaiUtils.flag(this, "object");
const chainState = buildChainState(this, chaiUtils);
const isNegated = chaiUtils.flag(this, "negate") === true;
chaiUtils.flag(this, "negate", false);
const result = vitestMatcher(obj, ...args, chainState);
expectResult(result, isNegated);
storeChainState(this, chaiUtils, name, obj, args, result);
return this;
}
var executeMatcherLogic = async (context, name, vitestMatcher, args, actualObj, isNegated, chaiUtils2) => {
chaiUtils2.flag(context, "object", actualObj);
const chainState = buildChainState(context, chaiUtils2);
const currentNegated = chaiUtils2.flag(context, "negate") === true;
chaiUtils2.flag(context, "negate", isNegated);
const result = await vitestMatcher(actualObj, ...args, chainState);
expectResult(result, isNegated);
chaiUtils2.flag(context, "negate", currentNegated);
storeChainState(context, chaiUtils2, name, actualObj, args, result);
return actualObj;
};
function makeVitestAsyncChainable(name, vitestMatcher, args) {
vitest.assert(chaiUtils !== void 0, "ChaiUtils not initialized");
setupPromise(this, chaiUtils);
const isNegated = chaiUtils.flag(this, "negate") === true;
const obj = chaiUtils.flag(this, "object");
const callPromiseValue = chaiUtils.flag(this, "callPromise");
const derivedPromise = callPromiseValue.then(
// Success handler - for normal resolved promises
async (resolvedValue) => {
vitest.assert(chaiUtils !== void 0, "ChaiUtils not initialized");
const actualObj = resolvedValue !== void 0 ? resolvedValue : await obj;
return await executeMatcherLogic(
this,
name,
vitestMatcher,
args,
actualObj,
isNegated,
chaiUtils
);
},
// Error handler - for rejected promises (like contract reverts)
async (error) => {
vitest.assert(chaiUtils !== void 0, "ChaiUtils not initialized");
const errorPromise = Promise.reject(error);
try {
await executeMatcherLogic(
this,
name,
vitestMatcher,
args,
errorPromise,
isNegated,
chaiUtils
);
return error;
} catch (executeError) {
if (executeError === error) return error;
throw executeError;
}
}
);
this.then = derivedPromise.then.bind(derivedPromise);
this.catch = derivedPromise.catch.bind(derivedPromise);
chaiUtils.flag(this, "callPromise", derivedPromise);
return this;
}
var createChainableFromVitest = (config) => {
const { name, vitestMatcher } = config;
const isAsync = isAsyncMatcher(vitestMatcher);
return {
name,
isAsync,
methodFunction: function(...args) {
if (isAsync) {
return makeVitestAsyncChainable.call(
this,
name,
vitestMatcher,
args
);
}
return makeVitestSyncChainable.call(
this,
name,
vitestMatcher,
args
);
},
chainFunction: function() {
vitest.assert(chaiUtils !== void 0, "ChaiUtils not initialized");
chaiUtils.flag(this, `${name}.chained`, true);
return this;
}
};
};
var createChainablePlugin = (matchers) => {
return (_chai, utils) => {
chaiUtils = utils;
Object.entries(matchers).forEach(([_, matcher]) => {
utils.addChainableMethod(_chai.Assertion.prototype, matcher.name, matcher.methodFunction, matcher.chainFunction);
});
};
};
var registerChainableMatchers = (matchers) => {
vitest.expect.extend(createInternalResultHandler());
vitest.chai.use(createChainablePlugin(matchers));
};
var handleTransaction = async (client, tx) => {
const res = tx instanceof Promise ? await tx : tx;
const node$1 = "request" in client ? node.createTevmNode({ fork: { transport: client } }) : client;
const txHash = (
// If it's a transaction receipt
typeof res === "object" && "transactionHash" in res ? res.transactionHash : (
// If it's a call result
typeof res === "object" && "txHash" in res ? res.txHash : (
// If it's already a transaction hash
typeof res === "string" && viem.isHex(res) ? res : void 0
)
)
);
if (txHash === void 0) {
throw new Error(
"Transaction hash is undefined, you need to pass a transaction hash, receipt or call result, or a promise that resolves to one of those"
);
}
if ("request" in client) {
await actions$1.waitForTransactionReceipt(client, { hash: txHash });
}
return { node: node$1, txHash };
};
// src/matchers/balance/getBalanceChange.ts
var getBalanceChange = (prestateTrace, address) => {
const pre = prestateTrace.pre[address];
const post = prestateTrace.post[address];
if (!pre || !post) return 0n;
return BigInt(post.balance ?? 0n) - BigInt(pre.balance ?? 0n);
};
var getTokenBalanceChange = async (node, tokenAddress, address, prestateTrace) => {
const currentBalanceResponse = await actions.contractHandler(node)({
...contract.ERC20.withAddress(tokenAddress).read.balanceOf(address),
throwOnFail: false
});
if (currentBalanceResponse.errors || currentBalanceResponse.data === void 0) {
throw new Error(
`Failed to get initial balance for token ${tokenAddress} and address ${address} when trying to find the balanceOf slot. Could not retrieve the balance change.
${JSON.stringify(currentBalanceResponse, null, 2)}`
);
}
const currentBalance = currentBalanceResponse.data;
for (const [contractAddress, traceData] of Object.entries(prestateTrace.post)) {
if (!traceData.storage) continue;
for (const [slot, currentValue] of Object.entries(traceData.storage)) {
if (!Hex.isEqual(currentValue, viem.numberToHex(currentBalance))) continue;
try {
const nextValue = viem.numberToHex(currentBalance + 1n, { size: 32 });
await actions.anvilSetStorageAtJsonRpcProcedure(node)({
method: "anvil_setStorageAt",
params: [contractAddress, slot, nextValue],
id: 1,
jsonrpc: "2.0"
});
const nextBalanceResponse = await actions.contractHandler(node)(contract.ERC20.withAddress(tokenAddress).read.balanceOf(address));
if (nextBalanceResponse.data === void 0 || nextBalanceResponse.data === currentBalance) continue;
const preSlotValue = prestateTrace.pre[contractAddress]?.storage?.[slot];
if (preSlotValue === void 0) throw new Error("preSlotValue is undefined");
const previousBalance = viem.hexToBigInt(preSlotValue);
return currentBalance - previousBalance;
} catch (_) {
} finally {
await actions.anvilSetStorageAtJsonRpcProcedure(node)({
method: "anvil_setStorageAt",
params: [contractAddress, slot, currentValue],
id: 1,
jsonrpc: "2.0"
});
}
}
}
return 0n;
};
// src/matchers/balance/getDiffMethodsFromPrestateTrace.ts
var getDiffMethodsFromPrestateTrace = async (client, tx) => {
const { node, txHash } = await handleTransaction(client, tx);
const res = await actions.debugTraceTransactionJsonRpcProcedure(node)({
jsonrpc: "2.0",
method: "debug_traceTransaction",
params: [{ transactionHash: txHash, tracer: "prestateTracer", tracerConfig: { diffMode: true } }],
id: 1
});
const prestateTrace = res.result;
return {
getBalanceChange: (address) => getBalanceChange(prestateTrace, address.toLowerCase()),
getTokenBalanceChange: (tokenAddress, address) => getTokenBalanceChange(
node,
tokenAddress.toLowerCase(),
address.toLowerCase(),
prestateTrace
)
};
};
// src/matchers/balance/toChangeBalance.ts
var toChangeBalance = async (received, client, account, expectedChange) => {
const address = typeof account === "string" ? account : account.address;
if (!viem.isAddress(address)) throw new Error(`Invalid address: ${address}`);
const expectedChangeBigInt = typeof expectedChange === "bigint" ? expectedChange : BigInt(expectedChange);
const { getBalanceChange: getBalanceChange2 } = await getDiffMethodsFromPrestateTrace(client, received);
const balanceChange = getBalanceChange2(address);
const pass = balanceChange === expectedChangeBigInt;
return {
pass,
message: () => pass ? `Expected account ${address} not to change balance by ${expectedChangeBigInt.toString()}` : `Expected account ${address} to change balance by ${expectedChangeBigInt.toString()}`,
actual: balanceChange,
expected: expectedChangeBigInt
};
};
var toChangeBalances = async (received, client, balanceChanges) => {
const { getBalanceChange: getBalanceChange2 } = await getDiffMethodsFromPrestateTrace(client, received);
const normalizedBalanceChanges = balanceChanges.map((change) => {
const address = typeof change.account === "string" ? change.account : change.account.address;
const amount = typeof change.amount === "bigint" ? change.amount : BigInt(change.amount);
if (!viem.isAddress(address)) throw new Error(`Invalid address: ${address}`);
return {
address,
amount,
actualAmount: getBalanceChange2(address)
};
});
const failedIndexes = normalizedBalanceChanges.filter((change) => change.actualAmount !== change.amount).map((change) => normalizedBalanceChanges.indexOf(change));
const pass = failedIndexes.length === 0;
return {
pass,
message: () => pass ? "Expected transaction not to change balances by the specified amounts, but all of them passed" : failedIndexes.length === normalizedBalanceChanges.length ? "Expected transaction to change balances by the specified amounts, but none of them passed" : `Expected transaction to change balances by the specified amounts, but some of them didn't pass (at indexes [${failedIndexes.join(", ")}])`,
actual: balanceChanges.map((change, i) => ({
account: change.account,
amount: normalizedBalanceChanges[i]?.actualAmount
})),
expected: balanceChanges.map((change, i) => ({
account: change.account,
amount: normalizedBalanceChanges[i]?.amount
}))
};
};
var toChangeTokenBalance = async (received, client, tokenContract, account, expectedChange) => {
const tokenAddress = typeof tokenContract === "string" ? tokenContract : tokenContract.address;
if (!viem.isAddress(tokenAddress)) throw new Error(`Invalid token address: ${tokenAddress}`);
const address = typeof account === "string" ? account : account.address;
if (!viem.isAddress(address)) throw new Error(`Invalid address: ${address}`);
const expectedChangeBigInt = typeof expectedChange === "bigint" ? expectedChange : BigInt(expectedChange);
const { getTokenBalanceChange: getTokenBalanceChange2 } = await getDiffMethodsFromPrestateTrace(client, received);
const tokenBalanceChange = await getTokenBalanceChange2(tokenAddress, address);
const pass = tokenBalanceChange === expectedChangeBigInt;
return {
pass,
message: () => pass ? `Expected account ${address} not to change token balance by ${expectedChangeBigInt.toString()}` : `Expected account ${address} to change token balance by ${expectedChangeBigInt.toString()}`,
actual: tokenBalanceChange,
expected: expectedChangeBigInt
};
};
var toChangeTokenBalances = async (received, client, tokenContract, balanceChanges) => {
const tokenAddress = typeof tokenContract === "string" ? tokenContract : tokenContract.address;
if (!viem.isAddress(tokenAddress)) throw new Error(`Invalid token address: ${tokenAddress}`);
const { getTokenBalanceChange: getTokenBalanceChange2 } = await getDiffMethodsFromPrestateTrace(client, received);
const normalizedBalanceChanges = await Promise.all(
balanceChanges.map(async (change) => {
const address = typeof change.account === "string" ? change.account : change.account.address;
const amount = typeof change.amount === "bigint" ? change.amount : BigInt(change.amount);
if (!viem.isAddress(address)) throw new Error(`Invalid address: ${address}`);
const balanceChange = await getTokenBalanceChange2(tokenAddress, address);
return {
address,
amount,
actualAmount: balanceChange
};
})
);
const failedIndexes = normalizedBalanceChanges.filter((change) => change.actualAmount !== change.amount).map((change) => normalizedBalanceChanges.indexOf(change));
const pass = failedIndexes.length === 0;
return {
pass,
message: () => pass ? "Expected transaction not to change token balances by the specified amounts, but all of them passed" : failedIndexes.length === normalizedBalanceChanges.length ? "Expected transaction to change token balances by the specified amounts, but none of them passed" : `Expected transaction to change token balances by the specified amounts, but some of them didn't pass (at indexes [${failedIndexes.join(", ")}])`,
actual: balanceChanges.map((change, i) => ({
account: change.account,
amount: normalizedBalanceChanges[i]?.actualAmount
})),
expected: balanceChanges.map((change, i) => ({
account: change.account,
amount: normalizedBalanceChanges[i]?.amount
}))
};
};
var getSelectorToCalldataMap = async (client, contractAddress, tx) => {
const { node, txHash } = await handleTransaction(client, tx);
const { result, error } = await actions.debugTraceTransactionJsonRpcProcedure(node)({
jsonrpc: "2.0",
method: "debug_traceTransaction",
params: [{ transactionHash: txHash, tracer: "4byteTracer" }],
id: 1
});
if (error) throw new Error("Error tracing transaction to retrieve function calls", { cause: error });
const trace = result;
const contractTrace = trace[viem.getAddress(contractAddress)] ?? {};
const calldataMap = new Map(Object.entries(contractTrace).filter(([selector]) => viem.isHex(selector)));
return calldataMap;
};
// src/matchers/contract/toCallContractFunction.ts
var toCallContractFunction = async (received, client, contract, functionIdentifier) => {
if (!contract.address) throw new Error("You need to provide a contract address");
const calldataMap = await getSelectorToCalldataMap(client, contract.address, received);
if (viem.isHex(functionIdentifier) || functionIdentifier.includes("function") || functionIdentifier.includes("(")) {
const functionSelector = viem.isHex(functionIdentifier) ? functionIdentifier : ox.AbiItem.getSelector(functionIdentifier);
const pass2 = calldataMap.has(functionSelector);
return {
pass: pass2,
actual: Array.from(calldataMap.keys()),
expected: `transaction calling function with selector ${functionSelector}`,
message: () => pass2 ? `Expected transaction not to call function ${functionIdentifier}` : `Expected transaction to call function ${functionIdentifier}`
};
}
if (!contract.abi) throw new Error("You need to provide a contract ABI if you want to match a function name");
const abiFunction = ox.AbiFunction.fromAbi(contract.abi, functionIdentifier);
const selector = ox.AbiFunction.getSelector(abiFunction);
const pass = calldataMap.has(selector);
return {
pass,
actual: Array.from(calldataMap.keys()),
expected: `transaction calling function with selector ${selector}`,
message: () => pass ? `Expected transaction not to call function ${functionIdentifier}` : `Expected transaction to call function ${functionIdentifier}`,
state: {
abiFunction,
selector,
calldataMap
}
};
};
var withFunctionArgs = (_received, ...argsAndChainState) => {
const {
args,
chainState: { previousState }
} = parseChainArgs(argsAndChainState);
if (!previousState || !("abiFunction" in previousState))
throw new Error("withFunctionArgs() requires a contract with abi and function name");
const { abiFunction, selector, calldataMap } = previousState;
const calldata = calldataMap.get(selector);
const actualDecodedArgs = calldata ? calldata.map((calldata2) => viem.decodeAbiParameters(abiFunction.inputs, calldata2)) : void 0;
const argsMatched = actualDecodedArgs ? actualDecodedArgs.some((decodedArgs) => {
return Array.isArray(decodedArgs) && args.every((arg, i) => decodedArgs[i] === arg);
}) : false;
return {
pass: argsMatched,
actual: actualDecodedArgs ? Object.fromEntries(actualDecodedArgs.map((decodedArgs, i) => [i, decodedArgs])) : {},
expected: args,
message: () => argsMatched ? "Expected transaction not to call function with the specified arguments" : "Expected transaction to call function with the specified arguments"
};
};
var withFunctionNamedArgs = (_received, expectedArgs, chainState) => {
if (!chainState?.previousState || !("abiFunction" in chainState.previousState))
throw new Error("withFunctionNamedArgs() requires a contract with abi and function name");
const { abiFunction, selector, calldataMap } = chainState.previousState;
const calldata = calldataMap.get(selector);
const actualDecodedArgs = calldata ? calldata.map((calldata2) => viem.decodeAbiParameters(abiFunction.inputs, calldata2)) : void 0;
const actualNamedArgs = actualDecodedArgs ? actualDecodedArgs.map(
(decodedArgs) => abiFunction.inputs.reduce(
(acc, input, index) => {
acc[input.name ?? ""] = decodedArgs[index];
return acc;
},
{}
)
) : void 0;
const argsMatched = actualNamedArgs ? actualNamedArgs.some((namedArgs) => {
return Object.entries(expectedArgs).every(
([key, value]) => key in namedArgs && namedArgs[key] === value
);
}) : false;
return {
pass: argsMatched,
actual: actualNamedArgs ? Object.fromEntries(actualNamedArgs.map((namedArgs, i) => [i, namedArgs])) : {},
expected: expectedArgs,
message: () => argsMatched ? "Expected transaction not to call function with the specified arguments" : "Expected transaction to call function with the specified arguments"
};
};
// src/matchers/contract/index.ts
var toCallContractFunctionChainable = createChainableFromVitest({
name: "toCallContractFunction",
vitestMatcher: toCallContractFunction
});
var withFunctionArgsChainable = createChainableFromVitest({
name: "withFunctionArgs",
vitestMatcher: withFunctionArgs
});
var withFunctionNamedArgsChainable = createChainableFromVitest({
name: "withFunctionNamedArgs",
vitestMatcher: withFunctionNamedArgs
});
var chainableContractMatchers = {
toCallContractFunction: toCallContractFunctionChainable,
withFunctionArgs: withFunctionArgsChainable,
withFunctionNamedArgs: withFunctionNamedArgsChainable
};
var handleTransaction2 = async (tx, opts) => {
const { client } = opts;
try {
const res = tx instanceof Promise ? await tx : tx;
if (typeof res === "object" && "errors" in res) throw res.errors[0];
if (!client)
throw new Error(
"You need to pass a client if the result of the promise is a transaction hash, receipt or call result that didn't throw"
);
const txReceipt = typeof res === "object" && "status" in res ? res : typeof res === "string" && viem.isHex(res) ? await actions$1.getTransactionReceipt(client, { hash: res }) : void 0;
if (txReceipt) await maybeThrowErrorFromTxReceipt(client, txReceipt);
return {
isRevert: false,
isError: false,
revertReason: void 0,
error: void 0
};
} catch (error) {
if (error instanceof Error && error.message.includes("You need to pass a client")) throw error;
return {
...parseError(error),
error
};
}
};
var parseError = (error) => {
const isRevert = (error instanceof errors.BaseError || error instanceof viem.BaseError) && error.message.includes("revert");
if (!isRevert) {
return {
isRevert: false,
isError: true,
revertReason: void 0
};
}
let decodedRevertData;
let rawRevertData;
const revertData = extractRevertData(error);
if (viem.isHex(revertData)) {
rawRevertData = revertData;
} else {
decodedRevertData = revertData;
}
const { revertString, revertReason } = extractRevertStringAndReason(rawRevertData, decodedRevertData);
return {
isRevert: true,
isError: false,
rawRevertData,
decodedRevertData,
revertString,
revertReason
};
};
var maybeThrowErrorFromTxReceipt = async (client, txReceipt) => {
if (txReceipt.status === "success") return;
const tx = await actions$1.getTransaction(client, { hash: txReceipt.transactionHash });
await actions$1.estimateGas(client, { account: tx.from, to: tx.to, data: tx.input, value: tx.value });
};
var extractRevertData = (error) => {
if (!error) return void 0;
const isRevertError = (err) => {
return (err instanceof viem.BaseError || err instanceof errors.BaseError) && err.message.includes("execution reverted") && ("data" in err || "raw" in err);
};
const revertError = isRevertError(error) ? error : error instanceof viem.BaseError || error instanceof errors.BaseError ? error.walk?.(isRevertError) || error.walk?.() : {};
if (!revertError) return void 0;
const revertData = "data" in revertError && !!revertError.data ? revertError.data : revertError;
return ("data" in revertData ? revertData.data : revertData) ?? ("raw" in revertError ? revertError.raw : void 0);
};
var REVERT_STRING_SELECTOR = "0x08c379a0";
var extractRevertStringAndReason = (rawData, decodedData) => {
const revertString = decodeRevertString(rawData, decodedData);
if (revertString) return { revertString, revertReason: `revert: ${revertString}` };
const revertReason = formatNonStringRevertReason(rawData, decodedData);
if (revertReason) return { revertString: void 0, revertReason };
return { revertString: void 0, revertReason: void 0 };
};
var decodeRevertString = (rawData, decodedData) => {
if (decodedData && decodedData.errorName === "Error") return decodedData.args?.[0];
if (rawData && rawData !== "0x" && rawData.startsWith(REVERT_STRING_SELECTOR)) {
const rawDataString = `0x${rawData.slice(REVERT_STRING_SELECTOR.length)}`;
return viem.decodeAbiParameters([{ type: "string" }], rawDataString)[0];
}
return void 0;
};
var formatNonStringRevertReason = (rawData, decodedData) => {
if (decodedData) {
const prefix = `custom error: ${decodedData.errorName}`;
const argTypes = decodedData.abiItem.inputs.map((i) => i.type).join(", ");
if (!decodedData.args) return `${prefix}(${argTypes})`;
const argValues = Array.isArray(decodedData.args) ? decodedData.args.map((v) => typeof v === "bigint" ? v.toString() : JSON.stringify(v)).join(", ") : String(decodedData.args);
return `${prefix}(${argTypes})
${Array.from({ length: prefix.length }).map(() => " ").join("")}(${argValues})`;
}
if (!rawData || rawData === "0x") return void 0;
return `custom error: ${rawData.slice(0, 10)}`;
};
// src/matchers/errors/toBeReverted.ts
var toBeReverted = async (received, client) => {
const { error, isRevert, isError, revertReason } = await handleTransaction2(received, { client });
if (isError) {
throw new Error("Expected transaction to be or not be reverted, but a different error was thrown", { cause: error });
}
return {
pass: isRevert,
message: () => isRevert ? `Expected transaction not to be reverted, but it reverted${revertReason ? ` with:
${revertReason}` : " without reason"}` : `Expected transaction to be reverted, but it didn't`
};
};
var toBeRevertedWithError = async (received, client, contractOrErrorIdentifier, errorName) => {
const { error, isRevert, isError, revertReason, decodedRevertData, rawRevertData } = await handleTransaction2(
received,
{ client }
);
if (isError) {
throw new Error("Expected transaction to be or not be reverted, but a different error was thrown", { cause: error });
}
const actualSelector = decodedRevertData ? ox.AbiItem.getSelector(decodedRevertData.abiItem) : rawRevertData?.slice(0, 10);
if (actualSelector === void 0) throw new Error("Could not get selector from revert data");
if (typeof contractOrErrorIdentifier === "string") {
const errorSelector = viem.isHex(contractOrErrorIdentifier) ? contractOrErrorIdentifier : ox.AbiItem.getSelector(
contractOrErrorIdentifier.startsWith("error") ? contractOrErrorIdentifier : `error ${contractOrErrorIdentifier}`
);
const pass2 = isRevert && actualSelector === errorSelector;
return {
pass: pass2,
actual: actualSelector,
expected: errorSelector,
message: () => pass2 ? `Expected transaction not to be reverted with ${contractOrErrorIdentifier.startsWith("0x") ? "selector" : "signature"} ${contractOrErrorIdentifier}, but it reverted ${revertReason ? `with:
${revertReason}` : "without reason"}` : `Expected transaction to be reverted with ${contractOrErrorIdentifier.startsWith("0x") ? "selector" : "signature"} ${contractOrErrorIdentifier}`,
state: {
decodedRevertData,
rawRevertData
}
};
}
if (typeof errorName !== "string") throw new Error("You need to provide an error name as a second argument");
const contract = contractOrErrorIdentifier;
const errorAbi = contract.abi.find((item) => item.type === "error" && item.name === errorName);
if (!errorAbi)
throw new Error(
`Error ${errorName} not found in contract ABI. Please make sure you've compiled the latest version before running the test.`
);
const pass = isRevert && actualSelector === ox.AbiItem.getSelector(errorAbi);
return {
pass,
actual: `error ${decodedRevertData?.errorName ?? rawRevertData?.slice(0, 10)}`,
expected: `error ${errorName}`,
message: () => pass ? `Expected transaction not to be reverted with error ${errorName}, but it reverted ${revertReason ? `with:
${revertReason}` : "without reason"}` : `Expected transaction to be reverted with error ${errorName}`,
state: {
decodedRevertData,
rawRevertData,
contract
}
};
};
// src/matchers/errors/toBeRevertedWithString.ts
var toBeRevertedWithString = async (received, client, expectedRevertString) => {
const { error, isRevert, isError, revertString, revertReason } = await handleTransaction2(received, { client });
if (isError) {
throw new Error("Expected transaction to be or not be reverted, but a different error was thrown", { cause: error });
}
const pass = isRevert && revertString !== void 0 && revertString === expectedRevertString;
return {
pass,
actual: revertString,
expected: expectedRevertString,
message: () => pass ? `Expected transaction not to be reverted with revert string, but it reverted ${revertReason ? `with:
${revertReason}` : "without reason"}` : `Expected transaction to be reverted with revert string ${expectedRevertString}`
};
};
var withErrorArgs = (_received, ...argsAndChainState) => {
const {
args,
chainState: { previousState }
} = parseChainArgs(argsAndChainState);
if (!previousState || !("contract" in previousState))
throw new Error("withErrorArgs() requires a contract with abi and error name");
const { contract, decodedRevertData, rawRevertData } = previousState;
const decodedRevert = decodedRevertData ?? (rawRevertData ? viem.decodeErrorResult({ abi: contract?.abi, data: rawRevertData }) : void 0);
const decodedArgs = decodedRevert?.args;
if (!decodedArgs) throw new Error("Could not decode revert data");
const argsMatched = args.length <= decodedArgs.length && args.every((arg, i) => decodedArgs[i] === arg);
return {
pass: argsMatched,
actual: decodedArgs,
expected: args,
message: () => argsMatched ? "Expected transaction not to revert with the specified arguments" : "Expected transaction to revert with the specified arguments"
};
};
var withErrorNamedArgs = (_received, expectedArgs, chainState) => {
vitest.assert(chainState, "Internal error: no chain state found");
const { previousState } = chainState;
if (!previousState || !("contract" in previousState))
throw new Error("withErrorNamedArgs() requires a contract with abi and error name");
const { contract, decodedRevertData, rawRevertData } = previousState;
const decodedRevert = decodedRevertData ?? (rawRevertData ? viem.decodeErrorResult({ abi: contract?.abi, data: rawRevertData }) : void 0);
const decodedArgs = decodedRevert?.args;
if (!decodedArgs) throw new Error("Could not decode revert data");
const namedArgs = decodedRevert.abiItem.inputs.reduce(
(acc, input, index) => {
acc[input.name ?? ""] = decodedArgs[index];
return acc;
},
{}
);
const argsMatched = Object.keys(expectedArgs).every(
(key) => key in namedArgs && namedArgs[key] === expectedArgs[key]
);
return {
pass: argsMatched,
actual: namedArgs,
expected: expectedArgs,
message: () => argsMatched ? "Expected transaction not to revert with the specified arguments" : "Expected transaction to revert with the specified arguments"
};
};
// src/matchers/errors/index.ts
var toBeRevertedWithErrorChainable = createChainableFromVitest({
name: "toBeRevertedWithError",
vitestMatcher: toBeRevertedWithError
});
var withErrorArgsChainable = createChainableFromVitest({
name: "withErrorArgs",
vitestMatcher: withErrorArgs
});
var withErrorNamedArgsChainable = createChainableFromVitest({
name: "withErrorNamedArgs",
vitestMatcher: withErrorNamedArgs
});
var chainableErrorMatchers = {
toBeRevertedWithError: toBeRevertedWithErrorChainable,
withErrorArgs: withErrorArgsChainable,
withErrorNamedArgs: withErrorNamedArgsChainable
};
var toEmit = async (received, contractOrEventIdentifier, eventName) => {
const logs = (await received).logs;
if (typeof contractOrEventIdentifier === "string") {
const eventSelector = viem.isHex(contractOrEventIdentifier) ? contractOrEventIdentifier : ox.AbiItem.getSelector(contractOrEventIdentifier);
const matchedLogs2 = logs.filter((log) => log.topics[0]?.startsWith(eventSelector));
const pass2 = matchedLogs2.length > 0;
return {
pass: pass2,
actual: logs.map((log) => log.topics[0]).filter(Boolean),
expected: `logs containing event selector ${eventSelector}`,
message: () => pass2 ? `Expected event ${contractOrEventIdentifier} not to be emitted` : `Expected event ${contractOrEventIdentifier} to be emitted`,
state: {
matchedLogs: matchedLogs2,
eventIdentifier: contractOrEventIdentifier
}
};
}
if (!eventName) throw new Error("You need to provide an event name as a third argument");
const contract = contractOrEventIdentifier;
const eventAbi = contract.abi.find((item) => item.type === "event" && item.name === eventName);
if (!eventAbi)
throw new Error(
`Event ${eventName} not found in contract ABI. Please make sure you've compiled the latest version before running the test.`
);
const eventTopic = viem.encodeEventTopics({
abi: contract.abi,
eventName
})[0];
const matchedLogs = logs.filter((log) => {
const topicMatches = log.topics[0] === eventTopic;
const addressMatches = contract?.address ? viem.getAddress(log.address) === viem.getAddress(contract.address) : true;
return topicMatches && addressMatches;
});
const pass = matchedLogs.length > 0;
const actualEvents = logs.filter((log) => contract?.address ? viem.getAddress(log.address) === viem.getAddress(contract.address) : true).map((log) => {
try {
const decoded = viem.decodeEventLog({
abi: contract.abi,
data: log.data,
topics: log.topics
});
return `${decoded.eventName}(${decoded.args ? Object.values(decoded.args).join(", ") : ""})`;
} catch {
return `UnknownEvent(${log.topics[0]})`;
}
});
return {
pass,
actual: actualEvents,
expected: `event "${eventName}" to be emitted`,
message: () => pass ? `Expected event ${eventName} not to be emitted` : `Expected event ${eventName} to be emitted`,
state: {
matchedLogs,
contract,
eventName,
eventAbi
}
};
};
var withEventArgs = (_received, ...argsAndChainState) => {
const {
args,
chainState: { previousState }
} = parseChainArgs(argsAndChainState);
if (!previousState || !("contract" in previousState))
throw new Error("withEventArgs() requires a contract with abi and event name");
const { matchedLogs, contract, eventName } = previousState;
let argsMatched = false;
const actualArgsFromLogs = [];
for (const log of matchedLogs) {
const decodedLog = viem.decodeEventLog({
abi: contract.abi,
data: log.data,
topics: log.topics,
eventName
});
const decodedArgs = Object.values(decodedLog.args);
if (!decodedArgs.length) continue;
actualArgsFromLogs.push(decodedArgs);
if (decodedArgs.length === args.length) {
const allArgsMatch = decodedArgs.every((actual, i) => actual === args[i]);
if (allArgsMatch) {
argsMatched = true;
break;
}
}
}
return {
pass: argsMatched,
actual: actualArgsFromLogs.flat(),
expected: args,
message: () => {
if (argsMatched) return `Expected event ${eventName} not to be emitted with the specified arguments`;
return `Expected event ${eventName} to be emitted with the specified arguments, but it wasn't found in any of the ${matchedLogs?.length ?? 0} emitted events`;
}
};
};
var withEventNamedArgs = (_received, expectedArgs, chainState) => {
vitest.assert(chainState, "Internal error: no chain state found");
const { previousState } = chainState;
if (!previousState || !("contract" in previousState))
throw new Error("withEventNamedArgs() requires a contract with abi and event name");
const { matchedLogs, contract, eventName } = previousState;
let argsMatched = false;
const actualNamedArgsFromLogs = [];
for (const log of matchedLogs) {
const decodedLog = viem.decodeEventLog({
abi: contract.abi,
data: log.data,
topics: log.topics,
eventName
});
const decodedArgs = decodedLog.args;
actualNamedArgsFromLogs.push(decodedArgs);
const allArgsMatch = Object.entries(expectedArgs).every(
([argName, expectedValue]) => decodedArgs[argName] === expectedValue
);
if (allArgsMatch) {
argsMatched = true;
break;
}
}
return {
pass: argsMatched,
actual: actualNamedArgsFromLogs,
expected: expectedArgs,
message: () => {
if (argsMatched) return `Expected event ${eventName} not to be emitted with the specified named arguments`;
return `Expected event ${eventName} to be emitted with the specified named arguments, but it wasn't found in any of the ${matchedLogs?.length ?? 0} emitted events`;
}
};
};
// src/matchers/events/index.ts
var toEmitChainable = createChainableFromVitest({
name: "toEmit",
vitestMatcher: toEmit
});
var withEventArgsChainable = createChainableFromVitest({
name: "withEventArgs",
vitestMatcher: withEventArgs
});
var withEventNamedArgsChainable = createChainableFromVitest({
name: "withEventNamedArgs",
vitestMatcher: withEventNamedArgs
});
var chainableEventMatchers = {
toEmit: toEmitChainable,
withEventArgs: withEventArgsChainable,
withEventNamedArgs: withEventNamedArgsChainable
};
var toBeInitializedAccount = async (received, client) => {
const address = typeof received === "string" ? received : received.address;
if (!viem.isAddress(address)) throw new Error(`Invalid address: ${address}`);
const node$1 = "request" in client ? node.createTevmNode({ fork: { transport: client } }) : client;
const account = await actions.getAccountHandler(node$1, { throwOnFail: false })({ address });
const pass = account.errors === void 0;
return {
pass,
message: () => pass ? `Expected account ${address} not to be initialized` : `Expected account ${address} to be initialized but received:
${account.errors?.[0]?.message}`,
actual: account
};
};
var toHaveState = async (received, client, expectedState) => {
const address = typeof received === "string" ? received : received.address;
if (!viem.isAddress(address)) throw new Error(`Invalid address: ${address}`);
const node$1 = "request" in client ? node.createTevmNode({ fork: { transport: client } }) : client;
const account = await actions.getAccountHandler(node$1, { throwOnFail: false })({ address, returnStorage: true });
if (account.errors) throw new Error(account.errors[0]?.message ?? "Could not retrieve account");
const { storage = {}, ...state } = expectedState;
const mismatchedKeys = [];
Object.entries(state).forEach(([_key, expectedValue]) => {
const key = _key;
const actualValue = account[key];
if (actualValue !== expectedValue) mismatchedKeys.push(key);
});
for (const [_slot, expectedValue] of Object.entries(storage)) {
const slot = _slot;
const actualValue = account.storage?.[slot];
if (actualValue !== expectedValue) {
mismatchedKeys.push("storage");
break;
}
}
const pass = mismatchedKeys.length === 0;
return {
pass,
message: () => pass ? `Expected account ${address} not to have state.` : `Expected account ${address} to have state but received mismatched state at keys: ${mismatchedKeys.join(", ")}`,
// Only show provided keys with their actual values so it's not confusing
actual: pass ? void 0 : {
...Object.fromEntries(
Object.entries(expectedState).filter(([key]) => key !== "storage").map(([key]) => [key, account[key] ?? void 0])
),
...Object.keys(expectedState).includes("storage") && expectedState.storage ? {
storage: Object.fromEntries(
Object.entries(expectedState.storage).map(([slot]) => [
slot,
account.storage?.[slot] ?? void 0
])
)
} : {}
},
expected: pass ? void 0 : expectedState
};
};
var toHaveStorageAt = async (received, client, expectedStorage) => {
const address = typeof received === "string" ? received : received.address;
if (!viem.isAddress(address)) throw new Error(`Invalid address: ${address}`);
const node$1 = "request" in client ? node.createTevmNode({ fork: { transport: client } }) : client;
const account = await actions.getAccountHandler(node$1, { throwOnFail: false })({ address, returnStorage: true });
if (account.errors) throw new Error(account.errors[0]?.message ?? "Could not retrieve account");
const storageEntries = Array.isArray(expectedStorage) ? expectedStorage : [expectedStorage];
const mismatchedSlots = [];
for (const { slot, value: expectedValue } of storageEntries) {
const actualValue = account.storage?.[slot];
if (actualValue !== expectedValue) mismatchedSlots.push(slot);
}
const pass = mismatchedSlots.length === 0;
return {
pass,
message: () => pass ? `Expected account ${address} not to have storage values at the specified slots.` : `Expected account ${address} to have storage values at slots: ${mismatchedSlots.join(", ")}`,
actual: pass ? void 0 : storageEntries.map(({ slot }) => ({ slot, value: account.storage?.[slot] ?? void 0 })),
expected: pass ? void 0 : storageEntries.map(({ slot, value }) => ({ slot, value }))
};
};
function toBeAddress(received, opts) {
const pass = typeof received === "string" && viem.isAddress(received, opts);
return {
pass,
actual: received,
message: () => {
if (pass)
return `Expected ${received} not to be ${opts?.strict !== false ? "a valid Ethereum address (checksummed)" : "a valid Ethereum address"}`;
return `Expected ${received} to be a ${opts?.strict !== false ? "valid Ethereum address (checksummed)" : "valid Ethereum address"}`;
}
};
}
function toBeHex(received, opts) {
const isStringReceived = typeof received === "string";
const isValidHex = isStringReceived && viem.isHex(received, opts);
const receivedSize = isStringReceived ? (received.length - 2) / 2 : 0;
const isValidSize = opts?.size === void 0 || receivedSize === opts.size;
const pass = isValidHex && isValidSize;
const expectedDescription = pass ? "not a valid hex string" : `a valid hex string${opts?.size ? ` with size ${opts.size} bytes` : ""}`;
return {
pass,
actual: received,
message: () => {
if (pass) return `Expected ${received} not to be ${expectedDescription}`;
if (!isStringReceived) return `Expected ${typeof received} to be a hex string`;
if (!received.startsWith("0x")) return `Expected ${received} to start with "0x"`;
if (!isValidHex) return `Expected ${received} to contain only hex characters (0-9, a-f, A-F) after "0x"`;
if (!isValidSize) return `Expected ${received} to have ${opts?.size} bytes, but got ${receivedSize} bytes`;
return `Expected ${received} to be ${expectedDescription}`;
}
};
}
function toEqualAddress(received, expected) {
const isStringReceived = typeof received === "string";
const isStringExpected = typeof expected === "string";
const isAddressReceived = isStringReceived && viem.isAddress(received, { strict: false });
const isAddressExpected = isStringExpected && viem.isAddress(expected, { strict: false });
let pass = false;
let normalizedReceived;
let normalizedExpected;
if (isAddressReceived && isAddressExpected) {
try {
normalizedReceived = viem.getAddress(received);
normalizedExpected = viem.getAddress(expected);
pass = viem.isAddressEqual(received, expected);
} catch {
pass = false;
}
}
return {
pass,
actual: normalizedReceived ?? received,
expected: normalizedExpected ?? expected,
message: () => {
if (pass) return "Expected addresses not to be equal";
if (!isStringReceived) return `Expected ${received} to be a string, but got ${typeof received}`;
if (!isStringExpected) return `Expected ${expected} to be a string, but got ${typeof expected}`;
if (!isAddressReceived) return `Expected ${received} to be a valid address`;
if (!isAddressExpected) return `Expected ${expected} to be a valid address`;
return "Expected addresses to be equal";
}
};
}
function toEqualHex(received, expected, opts) {
const isStringReceived = typeof received === "string";
const isStringExpected = typeof expected === "string";
const isHexReceived = isStringReceived && viem.isHex(received, { strict: true });
const isHexExpected = isStringExpected && viem.isHex(expected, { strict: true });
if (!isHexReceived || !isHexExpected) {
return {
pass: false,
actual: received,
expected,
message: () => {
if (!isStringReceived) return `Expected ${received} to be a string, but got ${typeof received}`;
if (!isStringExpected) return `Expected ${expected} to be a string, but got ${typeof expected}`;
if (!isHexReceived) return `Expected ${received} to be a valid hex string`;
if (!isHexExpected) return `Expected ${expected} to be a valid hex string`;
return "Expected hex strings to be equal";
}
};
}
let pass;
let normalizedReceived;
let normalizedExpected;
if (opts?.exact) {
normalizedReceived = received.toLowerCase();
normalizedExpected = expected.toLowerCase();
pass = normalizedReceived === normalizedExpected;
} else {
normalizedReceived = viem.trim(received);
normalizedExpected = viem.trim(expected);
try {
const receivedBytes = viem.hexToBytes(normalizedReceived);
const expectedBytes = viem.hexToBytes(normalizedExpected);
pass = utils.equalsBytes(receivedBytes, expectedBytes);
} catch {
pass = false;
}
}
return {
pass,
actual: normalizedReceived,
expected: normalizedExpected,
message: () => {
if (pass) return "Expected hex strings not to be equal";
return `Expected hex strings to be equal${opts?.exact ? " (exact match)" : " (normalized comparison)"}`;
}
};
}
// src/index.ts
vitest.expect.extend({
toBeAddress,
toBeHex,
toEqualAddress,
toEqualHex,
toBeReverted,
toBeRevertedWithString,
toBeInitializedAccount,
toHaveState,
toHaveStorageAt,
toChangeBalance,
toChangeBalances,
toChangeTokenBalance,
toChangeTokenBalances
});
registerChainableMatchers(chainableEventMatchers);
registerChainableMatchers(chainableErrorMatchers);
registerChainableMatchers(chainableContractMatchers);
//# sourceMappingURL=index.cjs.map
//# sourceMappingURL=index.cjs.map