N1TS Examples
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.
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.
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.
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.
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:
State Management
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
Direct Links to Source Files
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.