Tezos NFT Implementation
Complete documentation of the production Tezos NFT system in emprops-studio.
Overview
The Tezos implementation is the production NFT system that enables users to:
- Create NFT collections from AI-generated content
- Mint NFTs with XTZ or fiat payments (Wert)
- Manage collection settings (price, editions, status)
- Configure revenue splits and royalties
- Redeem collected funds
Contract Architecture
Two Contract Versions
The system supports two contract versions that coexist in production:
V1 - Projects Model (Legacy)
┌─────────────────────────────────────────────────────────────────┐
│ Projects Contract │
├─────────────────────────────────────────────────────────────────┤
│ Storage: │
│ projects: BigMap<string, TezProject> │
│ allowlist: BigMap<string, Spot[]> │
│ freelist: BigMap<string, Spot[]> │
│ │
│ Entry Points: │
│ - create_project(params) │
│ - mint_token(project_id, universal_id) │
│ - set_status(project_id, status) │
│ - set_price(project_id, price) │
│ - set_total_editions(project_id, editions) │
│ - split_funds(project_id) │
└─────────────────────────────────────────────────────────────────┘V1 Storage Structure (TezProject):
typescript
interface TezProject {
project_name: string
description: string
cover_uri: string
publish_date: string
generator_entrypoint: string // Code URL for generative art
creator_address: string
token_contract: string // FA2 token contract
primary_sales_split: SplitConfig
secondary_sales_split: SplitConfig
price: number // In mutez (1 XTZ = 1,000,000 mutez)
editions: number
mint_metadata: string // IPFS metadata URL
mode: MintMode // 0=ALLOWLIST, 1=FREELIST, 2=ALL
status: Status // 0=OFF, 1=ON
total_editions_minted: number
funds_collected: number
free_minter: string // Address for free mints
width: number
height: number
collection_type: TokenType
}V2 - Collections Model (Current)
┌─────────────────────────────────────────────────────────────────┐
│ Collections Contract │
├─────────────────────────────────────────────────────────────────┤
│ Storage: │
│ collections: BigMap<number, TezCollection> │
│ collectionsConfig: BigMap<number, CollectionConfig> │
│ allowlist: BigMap<number, Spot[]> │
│ freelist: BigMap<number, Spot[]> │
│ fundsClaimed: BigMap<{collId, addr}, number> │
│ platformConfig: PlatformConfig │
│ │
│ Entry Points: │
│ - createCollection(params) │
│ - mint(quantityToMint, collectionId, owner) │
│ - setStatus(collectionId, status) │
│ - setPrice(collectionId, price) │
│ - setTotalEditions(collectionId, editions) │
│ - withdrawFunds(collectionId) │
└─────────────────────────────────────────────────────────────────┘V2 Storage Structure (TezCollection):
typescript
interface TezCollection {
author: string
collectionMetadata: string // IPFS metadata URL
editions: number
freeMinter: string
fundsCollected: number
mintMode: number // 0=PUBLIC, 1=ALLOW_LIST, 2=FREE_LIST
price: number // In mutez
primarySales: Receiver[] // { part_account, part_value }
royalties: Receiver[] // Secondary sales
status: number // 0=OFF, 1=ON
tokenContractAddress: string // FA2 contract
totalEditionsMinted: number
}Contract Version Detection
The system automatically detects which contract version is deployed:
typescript
// utils/index.ts
async function getTezCollectionKey(
contract: ContractAbstraction<Wallet>,
): Promise<TezosCollectionKey | null> {
const storage = await contract.storage()
if (storage?.projects) return "projects" // V1
if (storage?.collections) return "collections" // V2
return null
}FA2 Token Contract
Both versions interact with an FA2 (TZIP-012) token contract:
┌─────────────────────────────────────────────────────────────────┐
│ FA2 Token Contract │
├─────────────────────────────────────────────────────────────────┤
│ Standard: TZIP-012 │
│ │
│ Entry Points: │
│ - mint(to, metadata_uri) // Called by Projects/Collections │
│ - transfer(from, to, token_id) │
│ - balance_of(owner) │
│ │
│ Storage: │
│ - ledger: BigMap<token_id, owner> │
│ - token_metadata: BigMap<token_id, metadata> │
└─────────────────────────────────────────────────────────────────┘Data Flow
Collection Creation Flow
1. User fills collection form in Studio
│
├─▶ usePublishCollection() hook triggered
│
├─▶ openApi.computeTezosCollectionMetadata()
│ └─▶ Returns: { metadata_url, mint_metadata_url, cover_image_url }
│
├─▶ Version detection: getTezCollectionKey()
│
├─▶ V1: createTezosProjectVersion()
│ └─▶ contract.methodsObject.create_project(params).send()
│
└─▶ V2: createTezosCollectionVersion()
└─▶ contract.methodsObject.createCollection(params).send()V1 Create Parameters:
typescript
{
project: {
p_project_id: string, // Metadata ID from API
p_project_name: string,
p_creator_address: string,
p_token_contract: string, // FA2 address
p_price: number * 1_000_000, // Convert XTZ to mutez
p_editions: number,
p_mint_metadata: string, // IPFS URL
p_mode: MintMode.ALL,
p_status: Status.ON,
p_free_minter: string,
p_description: string,
p_cover_uri: string,
p_publish_date: number, // Unix timestamp
p_generator_entrypoint: string,
p_width: number,
p_height: number
},
p_primary_sales_split: SplitConfig,
p_secondary_sales_split: SplitConfig,
p_allowlist: [],
p_freelist: []
}V2 Create Parameters:
typescript
{
collection: {
metadata: string, // IPFS URL
tokenContractAddress: string,
freeMinter: string,
author: string,
mintMode: CollectionMintMode.PUBLIC,
status: Status.ON,
editions: number,
price: number * 1_000_000 // Mutez
},
collectionConfig: {
enableBatchMint: boolean,
maxBatchMintAllowed: number,
startDate: number, // Unix timestamp
endDate: null
},
freelist: [],
allowlist: [],
royalties: Receiver[],
primarySalesReceivers: Receiver[]
}Minting Flow
1. User clicks Mint Button
│
├─▶ checkMintPhase() validates eligibility
│ ├─▶ Fetches on-chain storage
│ ├─▶ Checks allowlist/freelist membership
│ └─▶ Sets mint state (isAllowed, isPremint, isFreelist)
│
├─▶ useMintTezosToken() hook builds batch
│
├─▶ V1: Multiple mint_token calls
│ for (i = 0; i < quantity; i++) {
│ batch.withContractCall(
│ contract.methodsObject.mint_token({
│ p_id: project.id,
│ universal_id: uuid()
│ }),
│ { amount: price, mutez: true }
│ )
│ }
│
└─▶ V2: Single mint call
batch.withContractCall(
contract.methodsObject.mint({
quantityToMint: quantity,
collectionId: projectId,
owner: userAddress
}),
{ amount: price * quantity, mutez: true }
)Fund Redemption Flow
1. Creator clicks "Redeem Funds"
│
├─▶ getRedeemableTezFunds() calculates available amount
│ │
│ ├─▶ V1: funds_collected * (creator_split / 10000)
│ │
│ └─▶ V2: (funds_collected * part_value / 10000) - funds_claimed
│
├─▶ V1: contract.methodsObject.split_funds(project_id)
│
└─▶ V2: contract.methodsObject.withdrawFunds(collection_id)Mint Modes & Access Control
MintMode Enum
typescript
enum MintMode {
ALLOWLIST = 0, // Only whitelisted addresses
FREELIST = 1, // Only freelist addresses (free mints)
ALL = 2 // Public minting
}Allowlist/Freelist Structure
typescript
interface Spot {
addr: string // Tezos address
limit: number // Maximum mints allowed
count: number // Already minted count
}Mint Status Checking
typescript
// utils/mintStatus.ts
async function getTezMintStatus(
project: Project,
userAddress?: string
): Promise<MintState> {
const storage = await contract.storage()
const key = getTezCollectionKey(contract)
if (key === "projects") {
const projectData = await storage.projects.get(project.id)
// Check mode and lists
} else {
const collectionData = await storage.collections.get(project.projectId)
// Check mode and lists
}
return {
isLoading: false,
isAllowed: boolean,
isPremint: boolean, // In allowlist phase
isFreelist: boolean, // In freelist phase
isSoldOut: boolean,
isMintInProgress: false
}
}Revenue Splitting
Split Configuration
typescript
interface Receiver {
part_account: string // Tezos address
part_value: number // Basis points (1 = 0.01%, 10000 = 100%)
}
interface SplitConfig {
platform?: Receiver // Platform fee (optional)
creator: number // Creator's percentage in basis points
additional: Receiver[] // Additional receivers
}Primary vs Secondary Sales
- Primary Sales: Initial mint revenue split
- Secondary Sales: Royalties on resales (stored as
royalties)
Example Split
typescript
// 90% creator, 10% collaborator
{
primarySales: [
{ part_account: "tz1Creator...", part_value: 9000 },
{ part_account: "tz1Collab...", part_value: 1000 }
],
royalties: [
{ part_account: "tz1Creator...", part_value: 500 } // 5% royalty
]
}Price Handling
Mutez Conversion
All prices are stored in mutez (1 XTZ = 1,000,000 mutez):
typescript
// Converting for storage
const priceInMutez = priceInXTZ * 1_000_000
// Converting for display
const priceInXTZ = priceInMutez / 1_000_000
// Sending payment
batch.withContractCall(method, { amount: priceInMutez, mutez: true })Price Update
typescript
// contract-client.ts
async function updateTezCollectionPrice(
collection: Collection,
newPrice: number // In XTZ
) {
const key = await getTezCollectionKey(contract)
const priceInMutez = newPrice * 1_000_000
if (key === "projects") {
await contract.methodsObject.set_price(collection.id, priceInMutez).send()
} else {
await contract.methodsObject.setPrice(collection.projectId, priceInMutez).send()
}
}Network Configuration
Chain IDs
typescript
// types/wallet.ts
enum ChainId {
TezosMainnet = "NetXdQprcVkpaWU",
TezosGhostnet = "NetXnHfVqm9iesp" // Testnet
}RPC Selection
typescript
// utils/index.ts
function getRPCFromRegistry(chainId: ChainId): string {
const registry = JSON.parse(process.env.NEXT_PUBLIC_NETWORKS_REGISTRY)
const rpcs = registry[chainId]
// Return random RPC to distribute load
return rpcs[Math.floor(Math.random() * rpcs.length)]
}Environment Variables
bash
# Contract Addresses
NEXT_PUBLIC_PROJECT_CONTRACT_ADDRESS=KT1... # Projects/Collections contract
NEXT_PUBLIC_TOKEN_CONTRACT_ADDRESS=KT1... # FA2 token contract
# Network
NEXT_PUBLIC_NETWORK=mainnet # mainnet or ghostnet
NEXT_PUBLIC_RPC_URL=https://mainnet.tezos.marigold.dev
NEXT_PUBLIC_NETWORKS_REGISTRY='{...}' # JSON of RPC endpoints
# Batch Minting
NEXT_PUBLIC_TEZOS_BATCH_CONFIGURATION='{...}' # Per-project overridesTransaction Confirmation
Taquito Confirmation Handling
typescript
// All write operations wait for confirmation
const operation = await contract.methodsObject.mint(params).send()
await operation.confirmation(3) // Wait for 3 block confirmations
// Batch operations
const batch = wallet.batch()
batch.withContractCall(...)
const batchOp = await batch.send()
await batchOp.confirmation()Transaction Details Extraction
After minting, extract token details from transaction:
typescript
// transactionDetails.ts
function txToTokenIds(tx: Transaction) {
// Filter for mint operations
const mintOps = tx.filter(op =>
op.parameter?.entrypoint === "mint_token" ||
(op.parameter?.entrypoint === "mint" && op.parameter?.value?.collectionId)
)
// Extract token IDs from storage diffs
const tokens = mintOps.map(op => ({
universal_id: op.parameter.value.universal_id,
editionNumber: extractFromDiffs(op, 'total_editions_minted')
}))
return tokens
}Error Handling
Common Errors
| Error | Cause | Resolution |
|---|---|---|
NOT_ENOUGH_EDITIONS | Max supply reached | Check totalEditionsLeft |
NOT_ON_ALLOWLIST | User not whitelisted | Check mint mode |
COLLECTION_PAUSED | Status is OFF | Wait for creator to enable |
WRONG_AMOUNT | Incorrect payment | Send exact price in mutez |
MAX_BATCH_EXCEEDED | Too many in one tx | Reduce quantity |
Validation Before Mint
typescript
// checkMintPhase() validates:
// 1. Collection exists and is active
// 2. User has minting rights (allowlist/freelist check)
// 3. Editions remaining > 0
// 4. Batch size within limits
// 5. User hasn't exceeded their allowanceRelated Documentation
- Contract Client - Detailed function reference
- Wallet Integration - Beacon wallet setup
- Minting Components - UI component reference
