UNPKG

@tevm/test-matchers

Version:

Vite test matchers for Tevm or EVM-related testing in TypeScript.

1,102 lines (1,084 loc) 47.8 kB
'use strict'; 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