Testing
The @totems/evm package includes test helpers that deploy a complete local Totems environment, making it easy to test your mods without forking mainnet.
Setup
The test helpers use Hardhat with viem. Install the required dependencies:
npm install --save-dev hardhat @nomicfoundation/hardhat-viem viem @totems/evm Run tests with the Node.js test runner:
npx hardhat test Test Helpers
Import the helpers from @totems/evm/test/helpers:
import {
// Setup
setupTotemsTest,
ZERO_ADDRESS,
Hook,
// Creation helpers
publishMod,
createTotem,
modDetails,
totemDetails,
// Operation helpers
transfer,
mint,
burn,
// Proxy mod helpers
addMod,
removeMod,
// Totem query helpers
getBalance,
getTotem,
getTotems,
getStats,
isLicensed,
getRelays,
// Market query helpers
getMod,
getMods,
getModFee,
getModsFee,
getSupportedHooks,
isUnlimitedMinter,
} from "@totems/evm/test/helpers"; setupTotemsTest
Deploys a complete local Totems environment including all core contracts.
const {
viem,
publicClient,
totems, // ITotems contract instance
market, // IMarket contract instance
proxyMod, // ProxyMod contract instance
accounts, // Array of test wallet addresses
} = await setupTotemsTest(); publishMod
Publishes a mod to the local market.
await publishMod(
market,
sellerAddress,
modContractAddress,
[Hook.Created, Hook.Transfer], // Hooks to enable
modDetails(), // Optional: custom mod details (includes isMinter, needsUnlimited)
[], // Optional: requiredActions
ZERO_ADDRESS, // Optional: referrer
1_000_000n // Optional: price in wei
); The modDetails() helper returns default values. Override specific fields:
await publishMod(
market,
sellerAddress,
modContractAddress,
[Hook.Mint],
modDetails({ isMinter: true, needsUnlimited: false }),
[]
); createTotem
Creates a Totem with allocations and mods.
await createTotem(
totems,
market,
creatorAddress,
"TEST", // Ticker
18, // Decimals
[ // Allocations
{ recipient: userAddress, amount: 1000n * 10n ** 18n },
{ recipient: minterModAddress, amount: 500n * 10n ** 18n, isMinter: true },
],
{ // Mods per hook
transfer: [transferModAddress],
mint: [mintModAddress],
}
); Transfer, Mint, Burn
Helper functions for common operations:
await transfer(totems, "TEST", fromAddress, toAddress, 100n);
await mint(totems, minterModAddress, minterAddress, "TEST", 50n, "", 0n);
await burn(totems, "TEST", ownerAddress, 25n); Proxy Mod Helpers
Add or remove mods from a totem after creation via the Proxy Mod.
Note
The totem must have the Proxy Mod added at creation time for these helpers to work.
// Add a mod to an existing totem
await addMod(
proxyMod,
totems,
market,
"TEST", // Ticker
[Hook.Transfer, Hook.Mint], // Hooks to enable
modAddress, // Mod contract address
creatorAddress, // Must be totem creator
ZERO_ADDRESS // Optional: referrer
);
// Remove a mod from a totem
await removeMod(
proxyMod,
"TEST", // Ticker
modAddress, // Mod to remove
creatorAddress // Must be totem creator
); Query Helpers
// Totem queries
const balance = await getBalance(totems, "TEST", userAddress);
const totem = await getTotem(totems, "TEST");
const multiple = await getTotems(totems, ["TEST", "OTHER"]);
const stats = await getStats(totems, "TEST");
const licensed = await isLicensed(totems, "TEST", modAddress);
const relays = await getRelays(totems, "TEST");
// Market queries
const mod = await getMod(market, modAddress);
const mods = await getMods(market, [modAddress, otherModAddress]);
const fee = await getModFee(market, modAddress);
const totalFee = await getModsFee(market, [modAddress, otherModAddress]);
const hooks = await getSupportedHooks(market, modAddress);
const unlimited = await isUnlimitedMinter(market, modAddress); Example Test
Here’s a complete example testing a transfer hook mod:
import assert from "node:assert/strict";
import { describe, it } from "node:test";
import {
setupTotemsTest,
publishMod,
createTotem,
transfer,
getBalance,
Hook,
} from "@totems/evm/test/helpers";
describe("FreezerMod", async () => {
const { viem, totems, market, accounts } = await setupTotemsTest();
const [creator, holder, recipient] = accounts;
// Deploy and publish the mod
const freezerMod = await viem.deployContract("FreezerMod", [
totems.address,
creator
]);
await publishMod(market, creator, freezerMod.address, [Hook.Transfer]);
// Create a totem using the mod
await createTotem(
totems,
market,
creator,
"FREEZE",
18,
[{ recipient: holder, amount: 1000n * 10n ** 18n }],
{ transfer: [freezerMod.address] }
);
it("should allow transfers when not frozen", async () => {
await transfer(totems, "FREEZE", holder, recipient, 100n * 10n ** 18n);
const balance = await getBalance(totems, "FREEZE", recipient);
assert.equal(balance, 100n * 10n ** 18n);
});
it("should block transfers when frozen", async () => {
// Freeze transfers (called by creator)
await freezerMod.write.toggle(["FREEZE"], { account: creator });
await assert.rejects(async () => {
await transfer(totems, "FREEZE", holder, recipient, 100n * 10n ** 18n);
}, /Transfers are frozen/);
});
}); Testing Minter Mods
import assert from "node:assert/strict";
import { describe, it } from "node:test";
import {
setupTotemsTest,
publishMod,
createTotem,
mint,
getBalance,
modDetails,
Hook,
} from "@totems/evm/test/helpers";
describe("MyMinter", async () => {
const { viem, totems, market, accounts } = await setupTotemsTest();
const [creator, minterUser] = accounts;
// Deploy and publish the minter mod
const minter = await viem.deployContract("MyMinter", [
totems.address,
creator
]);
await publishMod(market, creator, minter.address, [Hook.Mint], modDetails({ isMinter: true }));
// Create totem with minter allocation
await createTotem(
totems,
market,
creator,
"MINT",
18,
[{ recipient: minter.address, amount: 10000n * 10n ** 18n, isMinter: true }],
{ mint: [minter.address] }
);
it("should mint tokens", async () => {
await mint(totems, minter.address, minterUser, "MINT", 100n * 10n ** 18n);
const balance = await getBalance(totems, "MINT", minterUser);
assert.equal(balance, 100n * 10n ** 18n);
});
}); Hook Enum
The Hook enum is exported from the test helpers:
enum Hook {
Created = 0,
Mint = 1,
Burn = 2,
Transfer = 3,
} Use it when publishing mods:
await publishMod(market, seller, modAddress, [Hook.Transfer, Hook.Mint]); Tips
- Use
async describe()to run setup code before tests - Use
assert.rejects()to test that transactions fail as expected - Test both success and failure cases for your hooks
- Verify license checks work correctly by testing unlicensed calls
- Test edge cases like zero amounts, self-transfers, etc.