Best Practices
Essential best practices for building secure, efficient, and maintainable N1TS smart contracts.
Critical Rule #1: Transaction Actions Cannot Return Values
Transaction actions (methods called by users) are for state changes only and cannot return values. Return statements in transaction actions are ignored.
// ❌ Wrong: Transaction action with return value
createToken(): string {
const mintId = nmint(BigInt(1000), this.signer(), "Token");
return mintId; // This return value is IGNORED!
}
// ✅ Correct: Transaction action for state change only
createToken(): void {
nmint(BigInt(1000), this.signer(), "Token");
this.log("Token creation initiated");
}
// ✅ Correct: Read state via client SDK, not actions
// client.readField('field_name') - Use this for getting data
To read data: Use client SDK queries or internal helper methods during execution, never transaction actions with return values.
Critical Rule #2: Cannot Initialize Class Properties
You CANNOT initialize class properties directly in N1TS. All properties must be declared and then initialized in an init()
method.
// ❌ Wrong: Direct property initialization
class MyApp extends NApp {
counter: number = 0; // This doesn't work!
users = new NMap<User>(this, 'users'); // This doesn't work!
}
// ✅ Correct: Initialize in init() method
class MyApp extends NApp {
counter: number;
users: NMap<User>;
init(): void {
this.counter = 0;
this.users = new NMap<User>(this, 'users');
}
}
Always use init()
method: Declare all properties without values, then initialize them in the init()
method.
Exception: NMap Must Be Declared Outside Functions
Despite the general rule that variables need to be initialized inside functions, NMap must be declared and initialized outside of functions:
// ✅ This is required and must be done for NMap
class NAPP extends NApp {
users: NMap<User> = new NMap<User>(this, 'users');
user: User;
init(): void {
this.user = new User();
}
}
This is a requirement because NMap needs special handling for persistence and state management that requires it to be initialized at the class level.
State Management
Property Organization
Structure your state logically using descriptive names and appropriate data types:
class WellOrganizedApp extends NApp {
// Property declarations (cannot initialize here!)
userCount: number;
isInitialized: boolean;
// Typed collections using NMap (must initialize directly)
userProfiles: NMap<UserProfile> = new NMap<UserProfile>(this, 'profiles');
userBalances: NMap<string> = new NMap<string>(this, 'balances');
systemSettings: NMap<ConfigValue> = new NMap<ConfigValue>(this, 'settings');
// Use descriptive property names
lastProcessedTimestamp: number;
adminWhitelist: NMap<boolean> = new NMap<boolean>(this, 'admin_whitelist');
init(): void {
// Initialize regular properties in init() method
this.userCount = 0;
this.isInitialized = false;
this.lastProcessedTimestamp = 0;
}
}
Data Tagging
Use meaningful tags to organize and query your data efficiently:
class TaggedDataApp extends NApp {
userMetadata: NMap<UserData> = new NMap<UserData>(this, 'user_metadata');
transactions: NMap<TransactionData> = new NMap<TransactionData>(this, 'transactions');
init(): void {
// Initialize other properties here if needed
}
updateUserProfile(userId: string, profile: UserData): void {
this.userMetadata.set(
userId,
profile,
'profile_update', // Primary tag
`user_${userId}_profile` // Secondary tag for specific queries
);
}
recordTransaction(txId: string, data: TransactionData): void {
this.transactions.set(
txId,
data,
'transaction_record',
`user_${data.userId}_tx` // Enable user-specific transaction queries
);
}
}
Tags help with data organization and can be used by indexers to efficiently query blockchain state.
Token Handling
Amount Precision
Always use BigInt for token amounts to ensure precision:
import {
NApp,
createExecutableFunctions,
ntransfer
} from '@n1xyz/nts-compiler';
class SafeTokenApp extends NApp {
// ✅ Correct: Use BigInt for amounts
transferTokens(to: string, mintId: string, amount: string): void {
const amountBigInt = BigInt(amount);
if (amountBigInt <= 0) {
throw new Error('Amount must be positive');
}
ntransfer(this.signer(), to, mintId, amountBigInt);
}
// ✅ Handle decimal inputs correctly
parseTokenAmount(amount: string, decimals: number): BigInt {
const parts = amount.split('.');
const whole = parts[0] || '0';
const fractional = (parts[1] || '').padEnd(decimals, '0').slice(0, decimals);
return BigInt(whole + fractional);
}
// ❌ Avoid: Using regular numbers for large amounts
// const amount = 1000000000000000000; // May lose precision
}
Balance Validation
Always check balances before token operations:
import {
NApp,
createExecutableFunctions,
ntransfer
} from '@n1xyz/nts-compiler';
class SecureTokenApp extends NApp {
safeTransfer(to: string, mintId: string, amount: string): void {
try {
// Validate inputs
if (!to || !mintId || !amount) {
throw new Error('Missing required parameters');
}
const amountBigInt = BigInt(amount);
if (amountBigInt <= 0) {
throw new Error('Amount must be positive');
}
// Note: In transaction actions, you cannot read state to check balance
// Balance validation is handled by the system during ntransfer
ntransfer(this.signer(), to, mintId, amountBigInt);
this.log('Transfer successful:', { to, mintId, amount });
} catch (error) {
this.log('Transfer failed:', error.message);
throw error; // Re-throw to fail the transaction
}
}
// Internal helper for reading balance (not a transaction action)
private async getTokenBalance(mintId: string, address: string): Promise<string> {
const balance = await nread(`_balance_${mintId}_${address}`);
return balance || '0';
}
}
Security
Access Control
Implement proper permission checks for sensitive operations:
class SecureApp extends NApp {
adminSettings: NMap<string> = new NMap<string>(this, 'admin_settings');
moderators: NMap<boolean> = new NMap<boolean>(this, 'moderators');
userProfiles: NMap<UserProfile> = new NMap<UserProfile>(this, 'user_profiles');
init(): void {
// Initialize other properties here if needed
}
// Admin-only operations
setSystemSetting(key: string, value: string): void {
if (this.signer() !== this.appAdmin()) {
throw new Error('Admin access required');
}
this.adminSettings.set(key, value, 'admin_setting', key);
this.log('System setting updated:', { key, value, admin: this.signer() });
}
// Role-based access
moderateContent(contentId: string, action: 'approve' | 'reject'): void {
const isModerator = this.moderators.get(this.signer()) || false;
const isAdmin = this.signer() === this.appAdmin();
if (!isModerator && !isAdmin) {
throw new Error('Moderator or admin access required');
}
this.processModeration(contentId, action);
this.log('Content moderated:', { contentId, action, moderator: this.signer() });
}
// Owner-only operations
updateUserProfile(userId: string, profile: UserProfile): void {
if (this.signer() !== userId && this.signer() !== this.appAdmin()) {
throw new Error('Can only update your own profile');
}
this.userProfiles.set(userId, profile, 'profile_update', userId);
}
}
Input Validation
Validate all user inputs thoroughly:
class ValidatedApp extends NApp {
users: NMap<UserProfile> = new NMap<UserProfile>(this, 'users');
init(): void {
// Initialize other properties here if needed
}
createUser(name: string, email: string, age: number): void {
// String validation
if (!name || name.trim().length === 0) {
throw new Error('Name is required');
}
if (name.length > 100) {
throw new Error('Name too long (max 100 characters)');
}
// Email validation (basic)
if (!email || !email.includes('@')) {
throw new Error('Valid email required');
}
// Number validation
if (age < 0 || age > 150) {
throw new Error('Age must be between 0 and 150');
}
// Address validation
const userId = this.signer();
if (this.users.get(userId)) {
throw new Error('User already exists');
}
const user: UserProfile = {
name: name.trim(),
email: email.toLowerCase().trim(),
age,
createdAt: this.time()
};
this.users.set(userId, user, 'user_created', userId);
}
transferFunds(to: string, amount: string): void {
// Address validation
if (!to || to.length === 0) {
throw new Error('Recipient address required');
}
// Amount validation
let amountBigInt: BigInt;
try {
amountBigInt = BigInt(amount);
} catch (error) {
throw new Error('Invalid amount format');
}
if (amountBigInt <= 0) {
throw new Error('Amount must be positive');
}
// Self-transfer check
if (to === this.signer()) {
throw new Error('Cannot transfer to yourself');
}
// Process transfer...
}
}
Error Handling
Comprehensive Error Management
Provide clear error messages and handle edge cases:
class RobustApp extends NApp {
lastTransactions: NMap<number> = new NMap<number>(this, 'last_transactions');
init(): void {
// Initialize other properties here if needed
}
processPayment(
recipient: string,
mintId: string,
amount: string,
memo?: string
): void {
try {
// Input validation with specific error messages
if (!recipient) {
throw new Error('Recipient address is required');
}
if (!mintId) {
throw new Error('Token mint ID is required');
}
if (!amount) {
throw new Error('Amount is required');
}
const amountBigInt = BigInt(amount);
if (amountBigInt <= 0) {
throw new Error('Amount must be greater than zero');
}
// Note: In transaction actions, balance checks are handled by the system
// Custom validation can be added here if needed
// Rate limiting check (if implemented in your app logic)
if (!this.checkRateLimit(this.signer())) {
throw new Error('Rate limit exceeded. Please wait before making another transaction');
}
// Process the payment
ntransfer(this.signer(), recipient, mintId, amountBigInt);
// Record transaction timestamp for rate limiting
this.lastTransactions.set(this.signer(), this.time(), 'payment_timestamp', this.signer());
this.log('Payment processed successfully:', {
recipient,
mintId,
amount,
timestamp: this.time(),
sender: this.signer()
});
} catch (error) {
// Log error for debugging
this.log('Payment failed:', {
error: error.message,
recipient,
mintId,
amount,
sender: this.signer()
});
// Re-throw to fail the transaction
throw error;
}
}
private checkRateLimit(user: string): boolean {
const lastTx = this.lastTransactions.get(user) || 0;
const minInterval = 60; // 1 minute between transactions
return (this.time() - lastTx) >= minInterval;
}
}
Performance Optimization
Efficient Data Access
Minimize state reads and writes for better performance:
class EfficientApp extends NApp {
// ✅ Batch operations when possible
processBulkTransfers(transfers: Array<{to: string, amount: string}>): void {
const sender = this.signer();
let totalAmount = BigInt(0);
// Validate all transfers first
for (const transfer of transfers) {
if (!transfer.to || BigInt(transfer.amount) <= 0) {
throw new Error('Invalid transfer data');
}
totalAmount += BigInt(transfer.amount);
}
// Note: Balance validation handled by system during transfer
// Additional custom validation can be added here if needed
// Process all transfers
for (const transfer of transfers) {
this.processTransfer(sender, transfer.to, transfer.amount);
}
}
// ✅ Cache frequently accessed data
private userLevelCache: NMap<number> = new NMap<number>(this, 'user_levels');
init(): void {
// Initialize other properties here if needed
}
getUserLevel(userId: string): number {
// Check cache first
let level = this.userLevelCache.get(userId);
if (level === undefined) {
// Calculate and cache
level = this.calculateUserLevel(userId);
this.userLevelCache.set(userId, level, 'level_cache', userId);
}
return level;
}
// ✅ Use descriptive state organization
organizeDataEfficiently(): void {
// Group related operations
const userId = this.signer();
const timestamp = this.time();
// Update multiple related fields together
this.userLastSeen.set(userId, timestamp);
this.userActionCount.set(userId, (this.userActionCount.get(userId) || 0) + 1);
this.userStatus.set(userId, 'active');
}
}
Efficient Data Handling
Be mindful of data processing and resource usage:
class EfficientApp extends NApp {
// ✅ Process large datasets in manageable chunks
processLargeDataset(items: string[]): void {
const chunkSize = 100;
for (let i = 0; i < items.length; i += chunkSize) {
const chunk = items.slice(i, i + chunkSize);
this.processChunk(chunk);
}
}
// ✅ Keep data structures lean and focused
processUserData(userData: LargeUserData): void {
// Extract only needed fields
const essential = {
id: userData.id,
name: userData.name,
email: userData.email
};
this.users.set(userData.id, essential, 'user_essential', userData.id);
// Store additional data separately if needed
if (userData.profileExtras) {
this.userExtras.set(userData.id, userData.profileExtras, 'profile_extras', userData.id);
}
}
}
Testing
Mock Testing
Test your contract logic before deployment:
import { compileCodeAndIdl } from '@n1xyz/nts-compiler';
describe('MyApp Tests', () => {
const appCode = `
import { NApp, createExecutableFunctions } from '@n1xyz/nts-compiler';
class TestApp extends NApp {
counter: number = 0;
increment(): number {
this.counter++;
return this.counter;
}
getCount(): number {
return this.counter;
}
}
export const { increment, getCount } = createExecutableFunctions(TestApp);
`;
test('compiles successfully', () => {
const { code, idl } = compileCodeAndIdl(appCode);
expect(code).toBeDefined();
expect(idl).toBeDefined();
expect(idl.actions).toHaveProperty('increment');
expect(idl.actions).toHaveProperty('getCount');
});
test('validates return types', () => {
const { idl } = compileCodeAndIdl(appCode);
expect(idl.actions.increment.returnType).toBe('number');
expect(idl.actions.getCount.returnType).toBe('number');
});
});
Integration Testing
Test end-to-end functionality:
import { NAppClient, NTSInterface } from '@n1xyz/nts-sdk';
describe('Integration Tests', () => {
let client: NAppClient;
let appId: string;
beforeAll(async () => {
// Deploy app and setup client
// Implementation depends on your test environment
});
test('counter functionality', async () => {
// Test increment
const result1 = await client.callMethod('increment', []);
expect(result1.success).toBe(true);
// Test get count
const result2 = await client.callMethod('getCount', []);
expect(result2.success).toBe(true);
expect(result2.returnValue).toBe(1);
});
});
Code Organization
Project Structure
Organize your code for maintainability:
src/
├── core/
│ ├── base-app.ts # Base app with common functionality
│ ├── types.ts # Shared type definitions
│ └── constants.ts # App constants
├── features/
│ ├── user-management.ts
│ ├── token-operations.ts
│ └── governance.ts
├── utils/
│ ├── validation.ts # Input validation helpers
│ ├── math.ts # Math utilities
│ └── formatting.ts # Data formatting
└── index.ts # Main app entry point
Code Reuse
Create reusable components and mixins:
// Mixin for ownership functionality
export class OwnableMixin extends NApp {
owner: string = '';
protected initializeOwner(): void {
if (!this.owner) {
this.owner = this.signer();
}
}
protected requireOwner(): void {
if (this.signer() !== this.owner) {
throw new Error('Owner access required');
}
}
transferOwnership(newOwner: string): void {
this.requireOwner();
if (!newOwner) {
throw new Error('New owner address required');
}
const oldOwner = this.owner;
this.owner = newOwner;
this.log('Ownership transferred:', { from: oldOwner, to: newOwner });
}
}
Following these best practices will help you build secure, efficient, and maintainable N1TS applications.