HAX
← Back

Steps for Creating a 3D NFT Marketplace on Stacks #03 - Testing Avatar Minter

2025-04-14Fabo Hax

test-it.png

Testing smart contracts is KEY when building on Bitcoin L2s like Stacks. You'll save a lot of time just by running your test before deploying to Testnet. In this post, I’ll walk through how I tested the SIP-009 compliant minter.clar contract for the 4V4 project — a 3D NFT Marketplace where you can mint and trade avatars with satoshis.

🎯 What We Test

The minter.clar contract enables:

  • Whitelist-based minting
  • Public minting
  • NFT transfers
  • Metadata URI management
  • Royalty info for marketplaces

We built tests using @hirosystems/clarinet-sdk and vitest in TypeScript.


🛠️ Test Setup

We used initSimnet() to spin up a temporary Clarinet devnet (simnet) for each test case. This gives us access to:

  • A fresh blockchain state
  • Wallet accounts
  • Simulated block mining
beforeEach(async () => {
  simnet = await initSimnet();
  accounts = simnet.getAccounts();
});

✅ Test Cases

1. Add to Whitelist & Mint

We first confirmed an admin can whitelist a user and that user can mint within allowance:

await simnet.mineBlock([
  tx.callPublicFn("minter", "add-to-whitelist", [
    Cl.standardPrincipal(wallet1), Cl.uint(2)
  ], deployer),
]);

await simnet.mineBlock([
  tx.callPublicFn("minter", "mint-whitelist", [
    Cl.standardPrincipal(wallet1)
  ], wallet1),
]);

Then verified ownership:

const read = await simnet.callReadOnlyFn("minter", "get-owner", [Cl.uint(1)], wallet1);
expect(read.result).toEqual(Cl.ok(Cl.some(Cl.standardPrincipal(wallet1))));

2. Public Mint

Ensures anyone can mint after the whitelist phase:

await simnet.mineBlock([
  tx.callPublicFn("minter", "mint-public", [], wallet2),
]);

3. Whitelist Overflow

We tested the whitelist limit to ensure a user cannot mint more than allowed:

expect(block[0].result.type).toBe(ClarityType.ResponseErr);
expect(block[0].result.value.value).toBe(403n);

4. Royalty Info

For marketplaces to calculate royalties dynamically:

const read = await simnet.callReadOnlyFn("minter", "get-royalty-info", [Cl.uint(1000)], deployer);
const result = read.result.value.data;

expect(result["amount"].value).toBe(50n); // 5% of 1000
expect(result["recipient"].value).toBe(deployer);

5. Setting & Reading Token URI

This test gave us the most headaches but was totally worth it. We validated that a user can set the token URI after minting and then retrieve it:

const uri = Cl.stringAscii("ipfs://avatar-0004");

await simnet.mineBlock([
  tx.callPublicFn("minter", "set-token-uri", [Cl.uint(1), uri], wallet4),
]);

const read = await simnet.callReadOnlyFn("minter", "get-token-uri", [Cl.uint(1)], wallet4);

const optional = read.result.value;
expect(optional.type).toBe(ClarityType.OptionalSome);

const stringVal = optional.value;
expect(stringVal.type).toBe(ClarityType.StringASCII);
expect(stringVal.data).toBe("ipfs://avatar-0004");

Read the full test suite here.

🚀 Final Thoughts

This was a hot 🔥 session — I iterated FOR DAYS until all 5 tests passed and this now ensures a:

  • Full confidence in the 3D NFT minting flow
  • Robust logic for access control and metadata
  • A reusable structure to test further functions

TIP: Ensure to check docs about clarinet-sdk and watch this video: youtube.com/watch?v=H-NKxafz7YU

Next up: we’ll deploy the contract to Testnet.

Go to Step #04

Sign Up to Stay Sync:

menu