Skip to Content
DevelopersNTSCode Examples

N1TS Examples

️⚠️
NTS is in active development and currently unstable. It is open to select early developers for exploration but is not yet suitable for production use. Follow developer updates for the latest changes, as performance is not at production level.

Practical, working examples of N1TS smart contracts to get you started with real-world patterns.

All examples are available in the N1 Examples Repository.

Counter App

A simple counter demonstrating basic state management with automatic persistence and comprehensive logging.

📁 View on GitHub

counter/contracts/app.ts
import { createExecutableFunctions, NApp } from "@n1xyz/nts-compiler"; class Counter extends NApp { count: number; init(): void { this.log('Initializing Counter app'); this.count = 0; this.log('Counter initialized with count:', this.count); } increment(by: number = 1): void { this.log('Increment called with value:', by); this.log('Current count before increment:', this.count); if (by < 0) { this.log('ERROR: Negative increment value provided:', by); throw new Error('Increment value must be non-negative'); } if (by === 0) { this.log('WARNING: Zero increment value provided, no change will occur'); } const oldCount = this.count; this.count += by; this.log('Count incremented successfully:', { oldValue: oldCount, incrementBy: by, newValue: this.count }); } decrement(by: number = 1): void { this.log('Decrement called with value:', by); this.log('Current count before decrement:', this.count); if (by < 0) { this.log('ERROR: Negative decrement value provided:', by); throw new Error('Decrement value must be non-negative'); } if (by === 0) { this.log('WARNING: Zero decrement value provided, no change will occur'); } if (this.count - by < 0) { this.log('WARNING: Decrement would result in negative count:', { currentCount: this.count, decrementBy: by, resultWouldBe: this.count - by }); } const oldCount = this.count; this.count -= by; this.log('Count decremented successfully:', { oldValue: oldCount, decrementBy: by, newValue: this.count }); } } export const { init, increment, decrement } = createExecutableFunctions(Counter);

Properties assigned to this are automatically persisted to blockchain state. No manual save/load required!

User Registry

User management system with profiles using typed NMap collections and comprehensive validation.

📁 View on GitHub

user-registry/contracts/app.ts
import { NApp, NMap, createExecutableFunctions } from '@n1xyz/nts-compiler'; interface UserProfile { name: string; email: string; registeredAt: number; active: boolean; } class UserRegistry extends NApp { users: NMap<UserProfile> = new NMap<UserProfile>(this, 'users'); userCount: number; init(): void { this.log('Initializing UserRegistry app'); this.userCount = 0; this.log('UserRegistry initialized successfully', { userCount: this.userCount, timestamp: this.time() }); } register(name: string, email: string): void { const userId = this.signer(); this.log('User registration attempt started', { userId, name, email, timestamp: this.time() }); // Check if user already exists this.log('Checking if user already exists for userId:', userId); const existingUser = this.users.get(userId); if (existingUser) { this.log('ERROR: User registration failed - user already exists', { userId, existingUser, attemptedName: name, attemptedEmail: email }); throw new Error('User already registered'); } this.log('User existence check passed - user does not exist yet'); // Validate input this.log('Starting input validation', { name, email }); if (!name) { this.log('ERROR: Registration failed - name is empty or null', { userId, providedName: name, email }); throw new Error('Name and email are required'); } if (!email) { this.log('ERROR: Registration failed - email is empty or null', { userId, name, providedEmail: email }); throw new Error('Name and email are required'); } if (name.trim().length === 0) { this.log('ERROR: Registration failed - name is only whitespace', { userId, name, email }); throw new Error('Name cannot be empty or only whitespace'); } if (email.trim().length === 0) { this.log('ERROR: Registration failed - email is only whitespace', { userId, name, email }); throw new Error('Email cannot be empty or only whitespace'); } if (!email.includes('@')) { this.log('ERROR: Registration failed - invalid email format', { userId, name, email, reason: 'Email must contain @ symbol' }); throw new Error('Invalid email format'); } this.log('Input validation passed successfully'); const profile: UserProfile = { name: name.trim(), email: email.trim().toLowerCase(), registeredAt: this.time(), active: true }; this.log('Creating user profile', { userId, profile, previousUserCount: this.userCount }); this.users.set(userId, profile, 'user_registration', userId); this.userCount++; this.log('User registered successfully', { userId, name: profile.name, email: profile.email, newUserCount: this.userCount, registrationTime: profile.registeredAt }); } updateProfile(name?: string, email?: string): void { const userId = this.signer(); this.log('Profile update attempt started', { userId, newName: name, newEmail: email, timestamp: this.time() }); this.log('Fetching existing user profile for userId:', userId); const profile = this.users.get(userId); if (!profile) { this.log('ERROR: Profile update failed - user not found', { userId, attemptedName: name, attemptedEmail: email }); throw new Error('User not found'); } this.log('Existing profile found', { userId, currentProfile: profile }); const originalProfile = { ...profile }; let hasChanges = false; if (name !== undefined) { this.log('Validating new name', { oldName: profile.name, newName: name }); if (!name || name.trim().length === 0) { this.log('ERROR: Profile update failed - invalid name provided', { userId, providedName: name, currentName: profile.name }); throw new Error('Name cannot be empty or only whitespace'); } const trimmedName = name.trim(); if (trimmedName !== profile.name) { profile.name = trimmedName; hasChanges = true; this.log('Name will be updated', { oldName: originalProfile.name, newName: trimmedName }); } else { this.log('Name unchanged - same as current name'); } } if (email !== undefined) { this.log('Validating new email', { oldEmail: profile.email, newEmail: email }); if (!email || email.trim().length === 0) { this.log('ERROR: Profile update failed - invalid email provided', { userId, providedEmail: email, currentEmail: profile.email }); throw new Error('Email cannot be empty or only whitespace'); } if (!email.includes('@')) { this.log('ERROR: Profile update failed - invalid email format', { userId, providedEmail: email, currentEmail: profile.email, reason: 'Email must contain @ symbol' }); throw new Error('Invalid email format'); } const trimmedEmail = email.trim().toLowerCase(); if (trimmedEmail !== profile.email) { profile.email = trimmedEmail; hasChanges = true; this.log('Email will be updated', { oldEmail: originalProfile.email, newEmail: trimmedEmail }); } else { this.log('Email unchanged - same as current email'); } } if (hasChanges) { this.users.set(userId, profile, 'profile_update', userId); this.log('Profile updated successfully', { userId, originalProfile, updatedProfile: profile, updateTime: this.time() }); } else { this.log('No profile changes detected, no update performed', { userId, providedName: name, providedEmail: email }); } } deactivate(): void { const userId = this.signer(); this.log('User deactivation attempt started', { userId, timestamp: this.time() }); this.log('Fetching user profile for deactivation:', userId); const profile = this.users.get(userId); if (!profile) { this.log('ERROR: Deactivation failed - user not found', { userId }); throw new Error('User not found'); } this.log('Profile found for deactivation', { userId, currentProfile: profile }); if (!profile.active) { this.log('WARNING: User already deactivated', { userId, profile }); throw new Error('User is already deactivated'); } profile.active = false; this.users.set(userId, profile, 'user_deactivation', userId); this.log('User deactivated successfully', { userId, deactivationTime: this.time(), updatedProfile: profile }); } } export const { init, register, updateProfile, deactivate } = createExecutableFunctions(UserRegistry);

Token Faucet

Rate-limited token distribution system with cooldowns, comprehensive validation, and detailed logging.

📁 View on GitHub

token-faucet/contracts/app.ts
import { NApp, NMap, createExecutableFunctions, nmint, ntransfer } from '@n1xyz/nts-compiler'; class TokenFaucet extends NApp { faucetToken: string; isInitialized: boolean; lastClaim: NMap<number> = new NMap<number>(this, 'last_claim'); totalClaimed: string; claimAmount: string; cooldownPeriod: number; init(): void { this.log('Initializing TokenFaucet app'); this.faucetToken = ''; this.isInitialized = false; this.totalClaimed = '0'; this.claimAmount = '1000000000000000000'; // 1 token with 18 decimals this.cooldownPeriod = 3600; // 1 hour in seconds this.log('TokenFaucet initialized successfully', { isInitialized: this.isInitialized, claimAmount: this.claimAmount, cooldownPeriod: this.cooldownPeriod, totalClaimed: this.totalClaimed, timestamp: this.time() }); } setupFaucet(totalSupply: string): void { const admin = this.signer(); this.log('Faucet setup attempt started', { admin, requestedSupply: totalSupply, currentInitStatus: this.isInitialized, timestamp: this.time() }); this.log('Verifying admin permissions for faucet setup'); if (admin !== this.appAdmin()) { this.log('ERROR: Faucet setup failed - insufficient permissions', { signer: admin, requiredAdmin: this.appAdmin(), action: 'setupFaucet' }); throw new Error('Only admin can setup faucet'); } this.log('Admin permission check passed'); this.log('Checking faucet initialization status'); if (this.isInitialized) { this.log('ERROR: Faucet setup failed - already initialized', { admin, totalSupply, currentFaucetToken: this.faucetToken, initTimestamp: this.time() }); throw new Error('Faucet already initialized'); } this.log('Initialization status check passed - faucet not yet initialized'); this.log('Validating total supply parameter', { totalSupply }); if (!totalSupply) { this.log('ERROR: Faucet setup failed - total supply is empty or null', { admin, providedSupply: totalSupply }); throw new Error('Total supply is required'); } let supplyBigInt: bigint; try { supplyBigInt = BigInt(totalSupply); this.log('Total supply successfully converted to BigInt', { originalValue: totalSupply, bigIntValue: supplyBigInt.toString() }); } catch (error) { this.log('ERROR: Faucet setup failed - invalid total supply format', { admin, providedSupply: totalSupply, error: error.message }); throw new Error('Invalid total supply format'); } if (supplyBigInt <= 0n) { this.log('ERROR: Faucet setup failed - total supply must be positive', { admin, providedSupply: totalSupply, parsedValue: supplyBigInt.toString() }); throw new Error('Total supply must be positive'); } this.log('Creating faucet token with nmint', { totalSupply: supplyBigInt.toString(), admin: this.appId(), metadata: { name: 'Faucet Token', symbol: 'FAUCET', decimals: 18, description: 'Free tokens from the faucet' } }); // Create the faucet token nmint( supplyBigInt, this.appId(), // App is the admin JSON.stringify({ name: 'Faucet Token', symbol: 'FAUCET', decimals: 18, description: 'Free tokens from the faucet' }) ); // Note: The mint ID will be generated by the system // For this example, we'll use a placeholder or query the system after transaction this.faucetToken = 'SYSTEM_GENERATED_MINT_ID'; this.isInitialized = true; this.log('Faucet setup completed successfully', { admin, totalSupply: supplyBigInt.toString(), isInitialized: this.isInitialized, appId: this.appId(), setupTime: this.time() }); } claim(): void { const user = this.signer(); const now = this.time(); this.log('Token claim attempt started', { user, timestamp: now, isInitialized: this.isInitialized, faucetToken: this.faucetToken, claimAmount: this.claimAmount }); this.log('Checking faucet initialization status'); if (!this.isInitialized) { this.log('ERROR: Claim failed - faucet not initialized', { user, requestedAt: now }); throw new Error('Faucet not initialized'); } this.log('Faucet initialization check passed'); this.log('Checking cooldown period for user:', user); const lastClaimTime = this.lastClaim.get(user) || 0; const timeSinceLastClaim = now - lastClaimTime; this.log('Cooldown validation', { user, lastClaimTime, currentTime: now, timeSinceLastClaim, requiredCooldown: this.cooldownPeriod }); if (timeSinceLastClaim < this.cooldownPeriod) { const timeLeft = this.cooldownPeriod - timeSinceLastClaim; this.log('ERROR: Claim failed - cooldown period not met', { user, timeLeft, lastClaimTime, cooldownPeriod: this.cooldownPeriod }); throw new Error(`Please wait ${timeLeft} seconds before claiming again`); } this.log('Cooldown period check passed'); this.log('Processing token transfer', { from: this.appId(), to: user, mintId: this.faucetToken, amount: this.claimAmount }); // Transfer tokens from app to user ntransfer( this.appId(), user, this.faucetToken, BigInt(this.claimAmount) ); // Update claim tracking this.lastClaim.set(user, now, 'claim_timestamp', user); // Update total claimed const newTotalClaimed = (BigInt(this.totalClaimed) + BigInt(this.claimAmount)).toString(); this.totalClaimed = newTotalClaimed; this.log('Token claim completed successfully', { user, claimAmount: this.claimAmount, claimTime: now, totalClaimedAfter: this.totalClaimed, nextClaimAvailableAt: now + this.cooldownPeriod }); } updateClaimAmount(newAmount: string): void { const admin = this.signer(); this.log('Claim amount update attempt started', { admin, currentAmount: this.claimAmount, newAmount, timestamp: this.time() }); this.log('Verifying admin permissions for claim amount update'); if (admin !== this.appAdmin()) { this.log('ERROR: Claim amount update failed - insufficient permissions', { signer: admin, requiredAdmin: this.appAdmin(), newAmount }); throw new Error('Only admin can update claim amount'); } this.log('Admin permission check passed'); this.log('Validating new claim amount', { newAmount }); if (!newAmount) { this.log('ERROR: Claim amount update failed - amount is empty or null', { admin, providedAmount: newAmount }); throw new Error('Claim amount is required'); } let amountBigInt: bigint; try { amountBigInt = BigInt(newAmount); this.log('Claim amount successfully converted to BigInt', { originalValue: newAmount, bigIntValue: amountBigInt.toString() }); } catch (error) { this.log('ERROR: Claim amount update failed - invalid amount format', { admin, providedAmount: newAmount, error: error.message }); throw new Error('Invalid amount format'); } if (amountBigInt <= 0n) { this.log('ERROR: Claim amount update failed - amount must be positive', { admin, providedAmount: newAmount, parsedValue: amountBigInt.toString() }); throw new Error('Claim amount must be positive'); } const previousAmount = this.claimAmount; this.claimAmount = newAmount; this.log('Claim amount updated successfully', { admin, previousAmount, newAmount: this.claimAmount, updateTime: this.time() }); } updateCooldown(newCooldown: number): void { const admin = this.signer(); this.log('Cooldown period update attempt started', { admin, currentCooldown: this.cooldownPeriod, newCooldown, timestamp: this.time() }); this.log('Verifying admin permissions for cooldown update'); if (admin !== this.appAdmin()) { this.log('ERROR: Cooldown update failed - insufficient permissions', { signer: admin, requiredAdmin: this.appAdmin(), newCooldown }); throw new Error('Only admin can update cooldown period'); } this.log('Admin permission check passed'); this.log('Validating new cooldown period', { newCooldown }); if (typeof newCooldown !== 'number' || newCooldown < 0) { this.log('ERROR: Cooldown update failed - invalid cooldown value', { admin, providedCooldown: newCooldown, type: typeof newCooldown }); throw new Error('Cooldown period must be a non-negative number'); } const previousCooldown = this.cooldownPeriod; this.cooldownPeriod = newCooldown; this.log('Cooldown period updated successfully', { admin, previousCooldown, newCooldown: this.cooldownPeriod, updateTime: this.time() }); } } export const { init, setupFaucet, claim, updateClaimAmount, updateCooldown } = createExecutableFunctions(TokenFaucet);
⚠️

Always use BigInt for token amounts to ensure precision. Regular JavaScript numbers can lose precision with large values.

Escrow Contract

Two-party escrow system for secure transactions with dispute resolution and comprehensive status tracking.

📁 View on GitHub

escrow-contract/contracts/app.ts
import { NApp, NMap, createExecutableFunctions, ntransfer, nmint } from '@n1xyz/nts-compiler'; enum EscrowStatus { PENDING = 'pending', COMPLETED = 'completed', DISPUTED = 'disputed', CANCELLED = 'cancelled' } interface EscrowData { buyer: string; seller: string; arbiter: string; mintId: string; amount: string; status: EscrowStatus; createdAt: number; description: string; } class EscrowContract extends NApp { escrows: NMap<EscrowData> = new NMap<EscrowData>(this, 'escrows'); escrowCount: number; feePercentage: number; testTokenMintId: string; init(): void { this.escrowCount = 0; this.feePercentage = 250; // 2.5% default fee this.testTokenMintId = ''; } createEscrow( seller: string, arbiter: string, mintId: string, amount: string, description: string ): void { if (!seller || !arbiter || !mintId || !amount) { throw new Error('All parameters are required'); } if (BigInt(amount) <= 0) { throw new Error('Amount must be positive'); } const buyer = this.signer(); if (buyer === seller) { throw new Error('Buyer and seller cannot be the same'); } const escrowId = `escrow_${++this.escrowCount}`; // Transfer tokens from buyer to escrow contract ntransfer(buyer, this.appId(), mintId, BigInt(amount)); const escrowData: EscrowData = { buyer, seller, arbiter, mintId, amount, status: EscrowStatus.PENDING, createdAt: this.time(), description }; this.escrows.set(escrowId, escrowData, 'escrow_created', buyer); this.log('Escrow created:', { escrowId, buyer, seller, amount }); } completeEscrow(escrowId: string): void { const escrow = this.escrows.get(escrowId); if (!escrow) { throw new Error('Escrow not found'); } if (escrow.status !== EscrowStatus.PENDING) { throw new Error('Escrow is not pending'); } const signer = this.signer(); if (signer !== escrow.buyer) { throw new Error('Only buyer can complete escrow'); } // Calculate fee const totalAmount = BigInt(escrow.amount); const feeAmount = (totalAmount * BigInt(this.feePercentage)) / BigInt(10000); const sellerAmount = totalAmount - feeAmount; // Transfer to seller (minus fee) ntransfer(this.appId(), escrow.seller, escrow.mintId, sellerAmount); escrow.status = EscrowStatus.COMPLETED; this.escrows.set(escrowId, escrow, 'escrow_completed', escrow.buyer); this.log('Escrow completed:', { escrowId, sellerAmount: sellerAmount.toString(), feeAmount: feeAmount.toString() }); } disputeEscrow(escrowId: string): void { const escrow = this.escrows.get(escrowId); if (!escrow) { throw new Error('Escrow not found'); } if (escrow.status !== EscrowStatus.PENDING) { throw new Error('Escrow is not pending'); } const signer = this.signer(); if (signer !== escrow.buyer && signer !== escrow.seller) { throw new Error('Only buyer or seller can dispute escrow'); } escrow.status = EscrowStatus.DISPUTED; this.escrows.set(escrowId, escrow, 'escrow_disputed', signer); this.log('Escrow disputed:', { escrowId, disputedBy: signer }); } resolveDispute(escrowId: string, refundToBuyer: boolean): void { const escrow = this.escrows.get(escrowId); if (!escrow) { throw new Error('Escrow not found'); } if (escrow.status !== EscrowStatus.DISPUTED) { throw new Error('Escrow is not disputed'); } const signer = this.signer(); if (signer !== escrow.arbiter) { throw new Error('Only arbiter can resolve dispute'); } const amount = BigInt(escrow.amount); const recipient = refundToBuyer ? escrow.buyer : escrow.seller; // Transfer full amount to resolved party (no fee on dispute resolution) ntransfer(this.appId(), recipient, escrow.mintId, amount); escrow.status = EscrowStatus.COMPLETED; this.escrows.set(escrowId, escrow, 'dispute_resolved', signer); this.log('Dispute resolved:', { escrowId, recipient, refundToBuyer, amount: amount.toString() }); } cancelEscrow(escrowId: string): void { const escrow = this.escrows.get(escrowId); if (!escrow) { throw new Error('Escrow not found'); } if (escrow.status !== EscrowStatus.PENDING) { throw new Error('Escrow is not pending'); } const signer = this.signer(); if (signer !== escrow.seller) { throw new Error('Only seller can cancel escrow'); } // Refund to buyer ntransfer(this.appId(), escrow.buyer, escrow.mintId, BigInt(escrow.amount)); escrow.status = EscrowStatus.CANCELLED; this.escrows.set(escrowId, escrow, 'escrow_cancelled', signer); this.log('Escrow cancelled:', { escrowId }); } updateFeePercentage(newFee: number): void { if (this.signer() !== this.appAdmin()) { throw new Error('Only admin can update fee'); } if (newFee < 0 || newFee > 1000) { // Max 10% throw new Error('Fee must be between 0 and 1000 basis points'); } this.feePercentage = newFee; this.log('Fee percentage updated:', { newFee }); } // For testing purposes - creates test tokens mintTestToken(totalSupply: string, metadata: string): void { if (this.signer() !== this.appAdmin()) { throw new Error('Only admin can mint test tokens'); } const mintId = nmint( BigInt(totalSupply), this.appId(), metadata ); this.testTokenMintId = mintId; this.log('Test token minted:', { mintId, totalSupply, metadata }); } // For testing purposes - distributes test tokens distributeTokens(recipient: string, mintId: string, amount: string): void { if (this.signer() !== this.appAdmin()) { throw new Error('Only admin can distribute tokens'); } ntransfer(this.appId(), recipient, mintId, BigInt(amount)); this.log('Tokens distributed:', { recipient, mintId, amount }); } } export const { init, createEscrow, completeEscrow, disputeEscrow, resolveDispute, cancelEscrow, updateFeePercentage, mintTestToken, distributeTokens } = createExecutableFunctions(EscrowContract);

Key Patterns Demonstrated

These examples showcase important N1TS patterns:

Automatic State Persistence

  • Properties assigned to this are automatically saved
  • Use NMap for typed key-value collections
  • Changes are committed atomically at transaction end

Data Organization

  • Use descriptive tags for data categorization
  • Group related data in NMap collections
  • Maintain referential integrity in your data model

Best Practices Highlighted

  • Comprehensive Input Validation: All examples validate inputs thoroughly
  • Detailed Logging: Extensive logging for debugging and monitoring
  • Proper Error Handling: Clear, descriptive error messages
  • State Management: Efficient use of NMap for complex data structures
  • Access Control: Proper permission checks for sensitive operations
  • Token Precision: Consistent use of BigInt for token amounts

For easy access, here are direct links to each example’s main contract file:

Each example includes additional files like tests, configuration, and utilities. Visit the full repository to explore the complete project structure.

Last updated on