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.
<-
Library
Security Checklist
->