> For the complete documentation index, see [llms.txt](https://tigermint.gitbook.io/tigermint-docs/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://tigermint.gitbook.io/tigermint-docs/transparency/contracts.md).

# Smart Contracts

Everything that touches money or ownership on Tigermint is enforced by two small, purpose-built smart contracts on TON, written in Tolk. The full source is below, unedited. What you read here is exactly what gets deployed: every collection launched on Tigermint compiles to the code hashes listed in this page, and you can verify any collection's code hash yourself in a TON explorer.

## The architecture

* **NftCollection**, one per collection, deployed when the creator launches. It holds the supply counter, the mint rules, and settles every payment in the same transaction as the mint.
* **NftItem**, one per card, deployed by the collection at mint time. A standard TEP-62 NFT: it holds the owner and the content pointer, and transfers on any TON marketplace.

There is no platform treasury contract and no escrow. Funds never pool anywhere: each mint splits the payment on the spot.

## What the contract guarantees

* **Signed mints.** Every mint carries an Ed25519 authorization from the platform binding the collection address, the buyer, the pull count, the exact phase price, a strictly increasing per-wallet nonce, and an expiry. A voucher can't be replayed, reused, repriced, or moved to another collection.
* **Exact pricing.** The contract settles at the signed price. The buyer is charged that price plus a flat network fee per card, which is what the wallet shows before signing. Anything above that is refunded.
* **Hard caps.** Total supply and the per-wallet limit are enforced on-chain, not just in the UI.
* **Instant creator payout.** The creator's net lands in their wallet inside the mint transaction itself, so funds never pool on the platform.

## Where every nanoGRAM goes

For a mint at price P with N cards, the buyer sends `(P + 0.07) x N`. The 0.07 per card is a flat network fee charged **on top** of the creator's price, never carved out of it:

| Piece                 | Amount               | Destination                                                         |
| --------------------- | -------------------- | ------------------------------------------------------------------- |
| Card deployment       | 0.05 per card        | Travels with each NftItem; funds its on-chain storage rent          |
| Network fee remainder | \~0.02 per card      | Stays in the collection as gas and storage dust; admin-withdrawable |
| Platform fee          | 5% of P x N          | Platform wallet                                                     |
| Creator net           | 95% of P x N         | Creator wallet, same transaction                                    |
| Overpayment           | above (P + 0.07) x N | Refunded to the buyer                                               |

Because the network fee is separate, the platform's 5% is the only deduction from the creator's price.

## Verify the code

Compiled with acton (Tolk). Current code hashes:

| Contract      | Code hash                                                          |
| ------------- | ------------------------------------------------------------------ |
| NftCollection | `2A2A6F7004B3BD12D1FA1E91D5C07BDE49CB988B6D8B556D5AFCFD0D5369C604` |
| NftItem       | `A984D24A5D3942A48AE49C96C467043C2F00CE84C61447B8B83012B52F6FB38E` |

Open any Tigermint collection in [Tonviewer](https://tonviewer.com), check the code hash, and compare. If it matches, the contract below is what's running.

## Source

### NftCollection

{% code title="onchain/contracts/NftCollection.tolk" lineNumbers="true" %}

```
import "@stdlib/strings"
import "errors"
import "fees-management"
import "messages"
import "types"

contract NftCollection {
    author: "TigerMint"
    version: "1.0.0"
    description: "TigerMint blind-mint NFT collection. TEP-62/64/66 with signed-authorization minting."
    storage: NftCollectionStorage
    incomingMessages: AllowedMessageToNftCollection
}

const COLLECTION_STORAGE_RESERVE = ton("0.1");
const DEPLOY_VALUE_PER_ITEM      = ton("0.05");
// Flat per-item network fee (deploy + gas/forward-fee headroom), paid by the
// buyer ON TOP of the creator's listed price — not carved out of it. Real cost
// is ~0.055 (0.05 item deploy + gas). MUST match NETWORK_FEE_PER_ITEM_NANO in
// lib/ton/fees.ts, and whenever this changes, rebuild and regenerate
// lib/contracts/code.ts so the deployed BOC matches.
const NETWORK_FEE_PER_ITEM       = ton("0.07");
const MAX_PULL_COUNT             = 10;
const MIN_MINT_PRICE             = ton("0.2");
// Below this, an overpayment refund isn't worth its own forward fee — the
// dust is absorbed as part of the contract's gas/storage reserve instead.
const REFUND_DUST_THRESHOLD      = ton("0.05");
// How far below requiredTotal the arriving value may fall (the message import
// fee) while still minting. Lets the buyer attach exactly (price + fee).
const IMPORT_FEE_SLACK           = ton("0.02");

fun deployNftItem(
    itemIndex: int,
    nftItemCode: cell,
    attachTonAmount: coins,
    initParams: Cell<NftItemInitAtDeployment>,
) {
    val deployMsg = createMessage({
        bounce: BounceMode.Only256BitsOfBody,
        dest: calcDeployedNftItem(itemIndex, contract.getAddress(), nftItemCode),
        value: attachTonAmount,
        body: initParams,
    });
    deployMsg.send(SEND_MODE_PAY_FEES_SEPARATELY);
}

// Verify the Ed25519 platform signature over the mint payload (must match
// lib/voucher.ts). Binding the collection and unit price stops a voucher being
// replayed on another drop or at a different phase price.
fun verifyAuthorization(
    collectionAddress: address,
    sender: address,
    pullCount: int,
    unitPrice: coins,
    nonce: int,
    expiryTime: int,
    authorization: bits512,
    platformPublicKey: int,
) {
    val payload = beginCell()
        .storeAddress(collectionAddress)
        .storeAddress(sender)
        .storeUint(pullCount, 8)
        .storeCoins(unitPrice)
        .storeUint(nonce, 64)
        .storeUint(expiryTime, 32)
        .endCell();
    val hash = payload.hash();
    val sig = authorization as slice;
    assert (isSignatureValid(hash, sig, platformPublicKey)) throw Errors.BadSignature;
}

type AllowedMessageToNftCollection =
    | Mint
    | Pause
    | Unpause
    | SetMintPrice
    | SetMaxPerWallet
    | Withdraw
    | RequestRoyaltyParams
    | DeployNft
    | BatchDeployNfts
    | ChangeCollectionAdmin

fun onInternalMessage(in: InMessage) {
    val msg = lazy AllowedMessageToNftCollection.fromSlice(in.body);

    match (msg) {
        Mint => {
            var storage = lazy NftCollectionStorage.load();
            var mintConfig = storage.mintConfig.load();

            assert (!mintConfig.isPaused) throw Errors.Paused;

            val pullCount = msg.pullCount as int;
            assert (pullCount >= 1) throw Errors.InvalidPullCount;
            assert (pullCount <= MAX_PULL_COUNT) throw Errors.InvalidPullCount;
            assert (storage.nextItemIndex + pullCount <= mintConfig.totalSupply) throw Errors.ExceedsSupply;

            // Buyer pays the signed per-item price, not the stored default. This is
            // what lets each phase set its own price and have it enforced here.
            val unitPrice = msg.unitPrice;
            assert (unitPrice >= MIN_MINT_PRICE) throw Errors.InvalidPrice;

            val sender = in.senderAddress;
            assert (sender.getWorkchain() == BASECHAIN) throw Errors.InvalidWorkchain;

            val walletKey = addressKey(sender);
            val walletState = mintConfig.getWalletState(sender);
            val currentCount = walletState >> 64;
            assert (currentCount + pullCount <= mintConfig.maxPerWallet) throw Errors.ExceedsWallet;

            // Buyer pays the creator's price plus a flat per-item network fee on
            // top (deploy cost + gas); the platform fee below is taken only from
            // the creator's price, never from the network fee.
            val requiredTotal = (unitPrice + NETWORK_FEE_PER_ITEM) * pullCount;
            // Tolerate the message import fee so the buyer can attach exactly
            // (price + fee) — what the UI shows — and still clear. The fee has
            // headroom over the deploy cost to absorb the shortfall, so no
            // attach buffer or refund is needed.
            assert (in.valueCoins + IMPORT_FEE_SLACK >= requiredTotal) throw Errors.InsufficientTon;

            assert (blockchain.now() <= msg.expiryTime as int) throw Errors.AuthExpired;
            val lastNonce = walletState & WALLET_NONCE_MASK;
            assert (msg.nonce as int > lastNonce) throw Errors.NonceReplay;

            verifyAuthorization(
                contract.getAddress(),
                sender,
                pullCount,
                unitPrice,
                msg.nonce as int,
                msg.expiryTime as int,
                msg.authorization,
                mintConfig.platformPublicKey,
            );

            // One item per pull, owned by the minter.
            val startIndex = storage.nextItemIndex;
            var i = 0;
            while (i < pullCount) {
                val itemIndex = startIndex + i;
                val initParams = NftItemInitAtDeployment {
                    ownerAddress: sender,
                    content: itemContent(itemIndex),
                }.toCell();
                deployNftItem(itemIndex, storage.nftItemCode, DEPLOY_VALUE_PER_ITEM, initParams);
                i += 1;
            }

            storage.nextItemIndex = startIndex + pullCount;
            mintConfig.walletState.set(walletKey, (((currentCount + pullCount) << 64) | (msg.nonce as int)) as uint128);
            storage.mintConfig = mintConfig.toCell();
            storage.save();

            // Settle in this same tx so funds never pool in the contract. The
            // platform fee is a percentage of the creator's price alone — the
            // network fee collected above is never platform or creator revenue.
            val platformFee = (unitPrice * pullCount * mintConfig.platformFeeBps) / 10000;

            if (platformFee > 0) {
                val feeMsg = createMessage({
                    bounce: BounceMode.NoBounce,
                    dest: mintConfig.platformWallet,
                    value: platformFee,
                    body: ReturnExcessesBack { queryId: msg.queryId },
                });
                feeMsg.send(SEND_MODE_PAY_FEES_SEPARATELY);
            }

            // Creator net to admin: their listed price minus the platform fee.
            // Deploy cost comes out of the network fee collected above, not the
            // creator's price; any leftover network fee stays in the contract as
            // gas/storage dust, Withdraw-able by the admin.
            val creatorNet = (unitPrice * pullCount) - platformFee;
            if (creatorNet > 0) {
                val payoutMsg = createMessage({
                    bounce: BounceMode.NoBounce,
                    dest: storage.adminAddress,
                    value: creatorNet,
                    body: ReturnExcessesBack { queryId: msg.queryId },
                });
                payoutMsg.send(SEND_MODE_PAY_FEES_SEPARATELY);
            }

            // Refund overpayment above the listed price. Only when the arriving
            // value actually exceeds requiredTotal — with the import-fee slack it
            // can land a hair under, which is absorbed from the fee's headroom.
            if (in.valueCoins > requiredTotal) {
                val excess = in.valueCoins - requiredTotal;
                if (excess > REFUND_DUST_THRESHOLD) {
                    val refundMsg = createMessage({
                        bounce: BounceMode.NoBounce,
                        dest: sender,
                        value: excess - REFUND_DUST_THRESHOLD,
                        body: ReturnExcessesBack { queryId: msg.queryId },
                    });
                    refundMsg.send(SEND_MODE_PAY_FEES_SEPARATELY | SEND_MODE_IGNORE_ERRORS);
                }
            }
        }

        Pause => {
            var storage = lazy NftCollectionStorage.load();
            assert (in.senderAddress == storage.adminAddress) throw Errors.NotFromAdmin;
            var mintConfig = storage.mintConfig.load();
            assert (!mintConfig.isPaused) throw Errors.AlreadyPaused;
            mintConfig.isPaused = true;
            storage.mintConfig = mintConfig.toCell();
            storage.save();
        }

        Unpause => {
            var storage = lazy NftCollectionStorage.load();
            assert (in.senderAddress == storage.adminAddress) throw Errors.NotFromAdmin;
            var mintConfig = storage.mintConfig.load();
            assert (mintConfig.isPaused) throw Errors.NotPaused;
            mintConfig.isPaused = false;
            storage.mintConfig = mintConfig.toCell();
            storage.save();
        }

        SetMintPrice => {
            var storage = lazy NftCollectionStorage.load();
            assert (in.senderAddress == storage.adminAddress) throw Errors.NotFromAdmin;
            // Price is locked once the drop is live, so creators can't bait-and-switch
            // whitelist buyers. It can still be corrected before the first mint.
            assert (storage.nextItemIndex == 0) throw Errors.PriceLocked;
            assert (msg.price >= MIN_MINT_PRICE) throw Errors.InvalidPrice;
            var mintConfig = storage.mintConfig.load();
            mintConfig.mintPrice = msg.price;
            storage.mintConfig = mintConfig.toCell();
            storage.save();
        }

        SetMaxPerWallet => {
            var storage = lazy NftCollectionStorage.load();
            assert (in.senderAddress == storage.adminAddress) throw Errors.NotFromAdmin;
            assert (msg.maxPerWallet > 0) throw Errors.InvalidCap;
            var mintConfig = storage.mintConfig.load();
            mintConfig.maxPerWallet = msg.maxPerWallet;
            storage.mintConfig = mintConfig.toCell();
            storage.save();
        }

        Withdraw => {
            // Creator revenue auto-settles inside each Mint, so this is only a safety
            // valve for the admin to sweep the small gas-reserve dust that accumulates.
            val storage = lazy NftCollectionStorage.load();
            assert (in.senderAddress == storage.adminAddress) throw Errors.NotFromAdmin;
            val available = contract.getOriginalBalance() - COLLECTION_STORAGE_RESERVE;
            assert (available > 0) throw Errors.NoWithdraw;
            assert (msg.amount <= available) throw Errors.WithdrawTooMuch;
            val withdrawMsg = createMessage({
                bounce: BounceMode.NoBounce,
                dest: storage.adminAddress,
                value: msg.amount,
                body: ReturnExcessesBack { queryId: msg.queryId },
            });
            withdrawMsg.send(SEND_MODE_PAY_FEES_SEPARATELY);
        }

        DeployNft => {
            var storage = lazy NftCollectionStorage.load();
            assert (in.senderAddress == storage.adminAddress) throw Errors.NotFromAdmin;
            assert (msg.itemIndex <= storage.nextItemIndex) throw Errors.InvalidItemIndex;

            val isLast = msg.itemIndex == storage.nextItemIndex;
            deployNftItem(msg.itemIndex, storage.nftItemCode, msg.attachTonAmount, msg.initParams);
            if (isLast) {
                storage.nextItemIndex += 1;
                storage.save();
            }
        }

        RequestRoyaltyParams => {
            val storage = lazy NftCollectionStorage.load();
            val respondMsg = createMessage({
                bounce: BounceMode.NoBounce,
                dest: in.senderAddress,
                value: 0,
                body: ResponseRoyaltyParams {
                    queryId: msg.queryId,
                    royaltyParams: storage.royaltyParams.load(),
                },
            });
            respondMsg.send(SEND_MODE_CARRY_ALL_REMAINING_MESSAGE_VALUE);
        }

        BatchDeployNfts => {
            var storage = lazy NftCollectionStorage.load();
            assert (in.senderAddress == storage.adminAddress) throw Errors.NotFromAdmin;

            var counter = 0;
            var r = msg.deployList.findFirst();
            while (r.isFound) {
                counter += 1;
                assert (counter < BATCH_SIZE_LIMIT) throw Errors.BatchLimitExceeded;

                val itemIndex = r.getKey();
                assert (itemIndex <= storage.nextItemIndex) throw Errors.InvalidItemIndex;

                val dictItem = r.loadValue();
                deployNftItem(
                    itemIndex,
                    storage.nftItemCode,
                    dictItem.attachTonAmount,
                    dictItem.initParams,
                );
                if (itemIndex == storage.nextItemIndex) {
                    storage.nextItemIndex += 1;
                }

                r = msg.deployList.iterateNext(r);
            }
            storage.save();
        }

        ChangeCollectionAdmin => {
            var storage = lazy NftCollectionStorage.load();
            assert (in.senderAddress == storage.adminAddress) throw Errors.NotFromAdmin;
            storage.adminAddress = msg.newAdminAddress;
            storage.save();
        }

        else => {
            // ignore empty messages, "wrong opcode" for others
            assert (in.body.isEmpty()) throw 0xFFFF;
        }
    }
}

fun onBouncedMessage(_in: InMessageBounced) {
    // For receiving funds from possibly bounced deployment of NFT
}

get fun get_collection_data(): CollectionDataReply {
    val storage = lazy NftCollectionStorage.load();
    val content = lazy storage.content.load();

    return {
        nextItemIndex: storage.nextItemIndex,
        collectionMetadata: content.collectionMetadata,
        adminAddress: storage.adminAddress,
    };
}

get fun get_nft_address_by_index(itemIndex: int): address {
    val storage = lazy NftCollectionStorage.load();
    val nftDeployed = calcDeployedNftItem(itemIndex, contract.getAddress(), storage.nftItemCode);
    return nftDeployed.calculateAddress();
}

get fun royalty_params(): RoyaltyParams {
    val storage = lazy NftCollectionStorage.load();
    return storage.royaltyParams.load();
}

get fun get_nft_content(
    _itemIndex: int,
    individualNftContent: string,
): Cell<OffchainMetadataReply> {
    val storage = lazy NftCollectionStorage.load();
    val content = lazy storage.content.load();

    // commonContent + per-item content, e.g. "https://site/coll/" + "1".
    var b = StringBuilder.create();
    b.append(content.commonContent);
    b.append(individualNftContent);
    return OffchainMetadataReply { string: b.build() }.toCell();
}

// Mint getters

get fun get_total_supply(): int {
    val storage = lazy NftCollectionStorage.load();
    return storage.mintConfig.load().totalSupply;
}

get fun get_mint_price(): int {
    val storage = lazy NftCollectionStorage.load();
    return storage.mintConfig.load().mintPrice;
}

get fun get_is_paused(): bool {
    val storage = lazy NftCollectionStorage.load();
    return storage.mintConfig.load().isPaused;
}

get fun get_max_per_wallet(): int {
    val storage = lazy NftCollectionStorage.load();
    return storage.mintConfig.load().maxPerWallet;
}

get fun get_wallet_mint_count(wallet: address): int {
    val storage = lazy NftCollectionStorage.load();
    return storage.mintConfig.load().getWalletCount(wallet);
}

get fun get_platform_public_key(): int {
    val storage = lazy NftCollectionStorage.load();
    return storage.mintConfig.load().platformPublicKey;
}

get fun get_platform_fee_bps(): int {
    val storage = lazy NftCollectionStorage.load();
    return storage.mintConfig.load().platformFeeBps;
}

get fun get_platform_wallet(): address {
    val storage = lazy NftCollectionStorage.load();
    return storage.mintConfig.load().platformWallet;
}
```

{% endcode %}

### NftItem

{% code title="onchain/contracts/NftItem.tolk" lineNumbers="true" %}

```
import "errors"
import "fees-management"
import "messages"
import "types"

contract NftItem {
    author: "TigerMint"
    version: "1.0.0"
    description: "Standard NFT item. TEP-62 and TEP-64 compliant."
    storage: NftItemStorage
    storageAtDeployment: NftItemStorageNotInitialized
    incomingMessages: AllowedMessageToNftItem
}

type AllowedMessageToNftItem =
    | AskToChangeOwnership
    | RequestStaticData

fun onInternalMessage(in: InMessage) {
    var loadingStorage = NftItemStorage.startLoading();
    if (loadingStorage.isNotInitialized()) {
        assert (
            in.senderAddress == loadingStorage.collectionAddress
        ) throw Errors.NotFromCollection;

        // First message from the collection initializes the item.
        val initParams = NftItemInitAtDeployment.fromSlice(in.body);
        val storage: NftItemStorage = {
            itemIndex: loadingStorage.itemIndex,
            collectionAddress: loadingStorage.collectionAddress,
            ownerAddress: initParams.ownerAddress,
            content: initParams.content,
        };
        storage.save();
        return;
    }

    var storage = loadingStorage.endLoading();

    val msg = lazy AllowedMessageToNftItem.fromSlice(in.body);

    match (msg) {
        AskToChangeOwnership => {
            assert (in.senderAddress == storage.ownerAddress) throw Errors.NotFromOwner;

            // Either format: bit 0 = inline payload, bit 1 = ref (1 bit + 1 ref).
            val (fwdBits, fwdRefs) = msg.forwardPayload.remainingBitsAndRefsCount();
            assert (fwdBits != 0) throw Errors.IncorrectForwardPayload;
            if (msg.forwardPayload.preloadUint(1) != 0) {
                assert (fwdBits == 1 && fwdRefs == 1) throw Errors.IncorrectForwardPayload;
            }
            assert (msg.newOwnerAddress.getWorkchain() == BASECHAIN) throw Errors.InvalidWorkchain;

            val fwdFee = in.originalForwardFee;
            var restAmount = contract.getOriginalBalance() - MIN_TONS_FOR_STORAGE;
            if (msg.forwardTonAmount != 0) {
                restAmount -= (msg.forwardTonAmount + fwdFee);
            }
            if (msg.sendExcessesTo != null) {
                assert (
                    msg.sendExcessesTo.getWorkchain() == BASECHAIN
                ) throw Errors.InvalidWorkchain;
                restAmount -= fwdFee;
            }

            assert (restAmount >= 0) throw Errors.TooSmallRestAmount;

            if (msg.forwardTonAmount != 0) {
                val ownershipMsg = createMessage({
                    bounce: BounceMode.NoBounce,
                    dest: msg.newOwnerAddress,
                    value: msg.forwardTonAmount,
                    body: NotificationForNewOwner {
                        queryId: msg.queryId,
                        oldOwnerAddress: storage.ownerAddress,
                        payload: msg.forwardPayload,
                    },
                });
                ownershipMsg.send(SEND_MODE_PAY_FEES_SEPARATELY);
            }
            if (msg.sendExcessesTo != null) {
                val excessesMsg = createMessage({
                    bounce: BounceMode.NoBounce,
                    dest: msg.sendExcessesTo,
                    value: restAmount,
                    body: ReturnExcessesBack { queryId: msg.queryId },
                });
                excessesMsg.send(SEND_MODE_PAY_FEES_SEPARATELY);
            }

            storage.ownerAddress = msg.newOwnerAddress;
            storage.save();
        }

        RequestStaticData => {
            val respondMsg = createMessage({
                bounce: BounceMode.NoBounce,
                dest: in.senderAddress,
                value: 0,
                // TEP-62 encodes itemIndex as 256-bit. Force the body inline (no
                // ref): at value = 0 it fits in the message cell directly.
                body: UnsafeBodyNoRef {
                    forceInline: ResponseStaticData {
                        queryId: msg.queryId,
                        itemIndex: storage.itemIndex as uint256,
                        collectionAddress: storage.collectionAddress,
                    },
                },
            });
            respondMsg.send(SEND_MODE_CARRY_ALL_REMAINING_MESSAGE_VALUE);
        }

        else => {
            // ignore empty messages, "wrong opcode" for others
            assert (in.body.isEmpty()) throw 0xFFFF;
        }
    }
}

get fun get_nft_data(): NftDataReply {
    var loadingStorage = NftItemStorage.startLoading();
    if (loadingStorage.isNotInitialized()) {
        return {
            isInitialized: false,
            itemIndex: loadingStorage.itemIndex,
            collectionAddress: loadingStorage.collectionAddress,
        };
    }

    val storage = loadingStorage.endLoading();
    return {
        isInitialized: true,
        itemIndex: storage.itemIndex,
        collectionAddress: storage.collectionAddress,
        ownerAddress: storage.ownerAddress,
        content: storage.content,
    };
}
```

{% endcode %}

### Shared types and messages

{% code title="onchain/contracts/types.tolk" lineNumbers="true" %}

```
import "@stdlib/strings"

struct RoyaltyParams {
    numerator: uint16
    denominator: uint16
    royaltyAddress: address
}

struct NftCollectionStorage {
    adminAddress: address
    nextItemIndex: uint64
    content: Cell<CollectionContent>
    nftItemCode: cell
    royaltyParams: Cell<RoyaltyParams>
    // In a ref cell to keep the root within the 4-ref budget.
    mintConfig: Cell<MintConfig>
}

// walletState packs each minter's bookkeeping into one dict entry as
// (mintCount << 64 | lastNonce), so each wallet costs one slot and one dict op.
struct MintConfig {
    totalSupply: uint64
    platformPublicKey: uint256
    mintPrice: coins
    isPaused: bool
    maxPerWallet: uint32
    platformWallet: address
    platformFeeBps: uint16
    walletState: map<uint256, uint128>
}

struct CollectionContent {
    collectionMetadata: cell
    commonContent: string
}

struct NftItemStorage {
    itemIndex: uint64
    collectionAddress: address
    ownerAddress: address
    content: string
}

struct NftItemStorageNotInitialized {
    itemIndex: uint64
    collectionAddress: address
}

struct NftDataReply {
    isInitialized: bool
    itemIndex: int
    collectionAddress: address
    ownerAddress: address? = null
    content: string? = null
}

fun NftCollectionStorage.load(): NftCollectionStorage {
    return NftCollectionStorage.fromCell(contract.getData());
}

fun NftCollectionStorage.save(self) {
    contract.setData(self.toCell());
}

fun NftItemStorage.startLoading(): NftItemStorageLoader {
    return NftItemStorageLoader.fromCell(contract.getData());
}

fun NftItemStorage.save(self) {
    contract.setData(self.toCell());
}

// An item's storage gains its owner/content fields only after deployment, so it
// loads in two steps: startLoading reads the always-present prefix, endLoading
// reads the rest once isNotInitialized() confirms it's there.
struct NftItemStorageLoader {
    itemIndex: uint64
    collectionAddress: address
    private rest: RemainingBitsAndRefs
}

fun NftItemStorageLoader.isNotInitialized(self): bool {
    return self.rest.isEmpty();
}

fun NftItemStorageLoader.endLoading(mutate self): NftItemStorage {
    return {
        itemIndex: self.itemIndex,
        collectionAddress: self.collectionAddress,
        ownerAddress: self.rest.loadAny(),
        content: self.rest.loadAny(),
    };
}

fun calcDeployedNftItem(
    itemIndex: uint64,
    collectionAddress: address,
    nftItemCode: cell,
): AutoDeployAddress {
    val emptyNftItemStorage: NftItemStorageNotInitialized = { itemIndex, collectionAddress };

    return { stateInit: { code: nftItemCode, data: emptyNftItemStorage.toCell() } };
}

struct CollectionDataReply {
    nextItemIndex: int
    collectionMetadata: cell
    adminAddress: address
}

struct (0x01) OffchainMetadataReply {
    string: string
}

const WALLET_NONCE_MASK = 0xFFFFFFFFFFFFFFFF;

fun addressKey(addr: address): uint256 {
    val (_, hash) = addr.getWorkchainAndHash();
    return hash;
}

// Packed (mintCount << 64 | lastNonce), or 0 if the wallet never minted.
fun MintConfig.getWalletState(self, wallet: address): int {
    val r = self.walletState.get(addressKey(wallet));
    return r.isFound ? r.loadValue() : 0;
}

fun MintConfig.getWalletCount(self, wallet: address): int {
    return self.getWalletState(wallet) >> 64;
}

// The item's index as a decimal string; the indexer resolves it to
// commonContent + index + ".json".
fun itemContent(itemIndex: int): string {
    return itemIndex.toDecimalString();
}
```

{% endcode %}

{% code title="onchain/contracts/messages.tolk" lineNumbers="true" %}

```
import "types"

struct NftItemInitAtDeployment {
    ownerAddress: address
    content: string
}

struct (0x693d3950) RequestRoyaltyParams {
    queryId: uint64
}

struct (0xa8cb00ad) ResponseRoyaltyParams {
    queryId: uint64
    royaltyParams: RoyaltyParams
}

struct (0x00000001) DeployNft {
    queryId: uint64
    itemIndex: uint64
    attachTonAmount: coins
    initParams: Cell<NftItemInitAtDeployment>
}

struct (0x00000002) BatchDeployNfts {
    queryId: uint64
    deployList: map<uint64, BatchDeployDictItem>
}

struct BatchDeployDictItem {
    attachTonAmount: coins
    initParams: Cell<NftItemInitAtDeployment>
}

struct (0x00000003) ChangeCollectionAdmin {
    queryId: uint64
    newAdminAddress: address
}

// Public mint. `authorization` is the platform's Ed25519 signature over
// cell(collection || sender || pullCount:8 || unitPrice:coins || nonce:64 || expiry:32),
// matching lib/voucher.ts.
struct (0x4d494e54) Mint {
    queryId: uint64
    pullCount: uint8
    unitPrice: coins
    nonce: uint64
    expiryTime: uint32
    authorization: bits512
}

struct (0x50415553) Pause {
    queryId: uint64
}

struct (0x554e5041) Unpause {
    queryId: uint64
}

struct (0x50524943) SetMintPrice {
    queryId: uint64
    price: coins
}

struct (0x4d415857) SetMaxPerWallet {
    queryId: uint64
    maxPerWallet: uint32
}

struct (0x57495448) Withdraw {
    queryId: uint64
    amount: coins
}

struct (0x2fcb26a2) RequestStaticData {
    queryId: uint64
}

struct (0x8b771735) ResponseStaticData {
    queryId: uint64
    itemIndex: uint256
    collectionAddress: address
}

struct (0b0) PayloadInline {
    value: RemainingBitsAndRefs
}

struct (0b1) PayloadInRef {
    value: Cell<RemainingBitsAndRefs>
}

struct (0x05138d91) NotificationForNewOwner {
    queryId: uint64
    oldOwnerAddress: address
    @abi.clientType(PayloadInline | PayloadInRef)
    payload: RemainingBitsAndRefs
}

struct (0xd53276db) ReturnExcessesBack {
    queryId: uint64
}

struct (0x5fcc3d14) AskToChangeOwnership {
    queryId: uint64
    newOwnerAddress: address
    sendExcessesTo: address?
    customPayload: cell?
    forwardTonAmount: coins
    @abi.clientType(PayloadInline | PayloadInRef)
    forwardPayload: RemainingBitsAndRefs
}

// NftSale (fixed-price escrow)

// Buyer purchase. Attach at least price + SALE_GAS_RESERVE.
struct (0x42555953) BuySale {
    queryId: uint64
}

// Seller-only: return the escrowed NFT and close the sale.
struct (0x43414e43) CancelSale {
    queryId: uint64
}
```

{% endcode %}

{% code title="onchain/contracts/errors.tolk" lineNumbers="true" %}

```
// Exit codes. The TEP-62 surface keeps the reference FunC implementation's codes
// (so existing tooling recognizes them); mint errors use the 41x range.

enum Errors {
    BatchLimitExceeded = 399
    InvalidWorkchain = 333
    NotFromAdmin = 401
    NotFromOwner = 401
    NotFromCollection = 405
    InvalidItemIndex = 402
    TooSmallRestAmount = 402
    IncorrectForwardPayload = 708

    // Mint errors
    Paused = 410
    InvalidPullCount = 411
    ExceedsSupply = 412
    ExceedsWallet = 413
    InsufficientTon = 414
    BadSignature = 415
    AlreadyPaused = 416
    NotPaused = 417
    InvalidPrice = 418
    InvalidCap = 419
    NoWithdraw = 420
    WithdrawTooMuch = 421
    AuthExpired = 422
    NonceReplay = 423
    PriceLocked = 424

    // Sale (NftSale escrow) errors — 43x range. Underpayment reuses
    // InsufficientTon (414) to match the mint path.
    SaleNotActive = 430
    SaleCompleted = 431
    SaleExpired = 432
    WrongNft = 433
    NotFromSeller = 434
    FeesExceedPrice = 436
}
```

{% endcode %}

{% code title="onchain/contracts/fees-management.tolk" lineNumbers="true" %}

```
const MIN_TONS_FOR_STORAGE = ton("0.05")

/// Maximum NFTs per batch deploy, bounded by the 255-action TVM limit.
const BATCH_SIZE_LIMIT = 250
```

{% endcode %}
