Skip to Content
DevelopersNTSBest Practices

Best Practices

️⚠️
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.

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:

src/organized-app.ts
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:

src/tagging-example.ts
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:

src/safe-amounts.ts
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:

src/balance-validation.ts
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:

src/access-control.ts
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:

src/input-validation.ts
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:

src/error-handling.ts
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:

src/performance.ts
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:

src/efficient-app.ts
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:

tests/app.test.ts
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:

tests/integration.test.ts
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:

src/mixins/ownable.ts
// 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.

Last updated on