Skip to Content
DevelopersNTSToken System

Token System Guide

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

A comprehensive guide to N1’s native token system for creating, managing, and transferring tokens.

Overview

The N1 network features a sophisticated token system that enables:

  • Native Token Creation: Mint new tokens with custom properties using nmint()
  • Secure Transfers: Transfer tokens between addresses with comprehensive validation
  • Admin Controls: Flexible permission system for token management
  • Automatic Balance Tracking: Built-in balance management with BigInt precision

All token amounts in N1 use BigInt for arbitrary precision arithmetic. Always convert string amounts to BigInt before token operations.

Token Architecture

Token Identification

Each token is identified by a unique mint ID (UUID) generated during creation:

nmint(totalSupply, admin, metadata); // Note: nmint doesn't return a value in transaction actions // The mint ID is generated by the system and can be queried after the transaction

Core Token Properties

Every token has three fundamental properties:

  1. Balance Distribution: Tracks who owns how many tokens
  2. Admin Address: Controls who can modify the token
  3. Metadata: Stores additional information about the token

Data Organization

Token information is automatically organized and stored by the system. Each token maintains:

  • Balance tracking for each address
  • Admin permissions for token management
  • Metadata for token information

Creating Tokens

Basic Token Creation

Import Required Functions

src/index.ts
import { NApp, createExecutableFunctions, nmint, ntransfer, nmintEdit } from '@n1xyz/nts-compiler'; // Note: Only nread, nwrite, and log are globally available

Create Token Class

src/index.ts
class TokenCreator extends NApp { createToken(): void { // Transaction actions cannot return values nmint( BigInt('1000000'), // Total supply (1M tokens) - BigInt required this.signer(), // Admin (current user) 'My Custom Token' // Metadata ); this.log('Token creation initiated'); // Note: To get the mintId, query the chain state after transaction } }

Export Functions

src/index.ts
export const { createToken } = createExecutableFunctions(TokenCreator);

Advanced Token Creation

For tokens with structured metadata:

src/index.ts
class AdvancedTokenCreator extends NApp { createGameToken(): void { const tokenMetadata = JSON.stringify({ name: 'GameCoin', symbol: 'GAME', decimals: 18, description: 'In-game currency for MyGame', website: 'https://mygame.com', image: 'https://mygame.com/token-icon.png', social: { twitter: '@mygame', telegram: 'https://t.me/mygame' } }); nmint( BigInt('10000000000000000000000000'), // 10M tokens with 18 decimals this.appId(), // App is admin tokenMetadata ); this.log('Game token creation initiated'); } }
⚠️

When creating tokens with decimal places, multiply by the appropriate power of 10. For 18 decimals, multiply by 10^18.

Token Transfers

Basic Transfers

Transfer tokens between addresses:

src/index.ts
// ntransfer must be imported from @n1xyz/nts-compiler class TokenTransfer extends NApp { sendTokens( recipient: string, mintId: string, amount: string ): void { // Validate inputs if (!recipient || !mintId || !amount) { throw new Error('Missing required parameters'); } if (BigInt(amount) <= 0) { throw new Error('Amount must be positive'); } ntransfer( this.signer(), // From (sender) recipient, // To (recipient) mintId, // Token mint ID BigInt(amount) // Amount as BigInt ); this.log('Tokens sent:', { recipient, mintId, amount }); } sendFromApp( recipient: string, mintId: string, amount: string ): void { // App can send its own tokens ntransfer( this.appId(), // From app recipient, // To recipient mintId, // Token mint ID BigInt(amount) // Amount ); } }

Transfer Validation

The system performs extensive validation automatically:

  1. Permission Check: Verifies sender has authority to transfer tokens
  2. Balance Check: Ensures sufficient token balance exists
  3. Amount Validation: Confirms positive transfer amount
  4. Balance Updates: Atomically updates sender and recipient balances

Transfer Permissions

  • Self-Transfer: Users can always transfer their own tokens
  • Admin-Transfer: Token admin can transfer anyone’s tokens of that type
  • App-Transfer: Apps can transfer tokens they own or admin

Admin Operations

Token Editing

Token admins can modify token properties using nmintEdit():

src/index.ts
// nmintEdit must be imported from @n1xyz/nts-compiler // nread is globally available class TokenAdmin extends NApp { updateToken( mintId: string, newSupply?: string, newAdmin?: string, newMetadata?: string ): void { // Note: In transaction actions, you cannot read state directly // Admin verification should be handled by the system or in validation logic nmintEdit( mintId, newSupply ? BigInt(newSupply) : BigInt(0), newAdmin || '', newMetadata || '' ); this.log('Token update initiated:', { mintId, newSupply, newAdmin, newMetadata }); } transferAdminship(mintId: string, newAdmin: string): void { nmintEdit( mintId, BigInt(0), // No supply change newAdmin, // New admin '' // Keep same metadata ); this.log('Admin transfer initiated:', { mintId, newAdmin }); } }
⚠️

Only the current token admin can call nmintEdit(). Admin transfers are permanent and cannot be undone.

Reading Token Data

State Queries (Not Transaction Actions)

⚠️

Important: The methods below are NOT transaction actions. They are helper functions for internal use within your app logic. To read token data from outside your app, use the client SDK to query blockchain state directly.

src/internal-helpers.ts
// These are internal helper functions, NOT exported as transaction actions class TokenHelpers extends NApp { protected async getTokenBalance(mintId: string, address: string): Promise<string> { // Internal helper - can read state during execution const balance = await nread(`_balance_${mintId}_${address}`); return balance || '0'; } protected async getTokenAdmin(mintId: string): Promise<string> { // Internal helper - can read state during execution return await nread(`_mint_admin_${mintId}`); } protected async getTokenMetadata(mintId: string): Promise<string> { // Internal helper - can read state during execution return await nread(`_mint_meta_${mintId}`); } }

Reading Data from Client Side

client-example.ts
// Use client SDK to read token data import { NAppClient } from '@n1xyz/nts-sdk'; // Read token balance const balance = await client.readField(`_balance_${mintId}_${address}`); // Read token admin const admin = await client.readField(`_mint_admin_${mintId}`); // Read token metadata const metadata = await client.readField(`_mint_meta_${mintId}`);

Best Practices

Amount Handling

// ✅ Always use BigInt for amounts const amount = BigInt('1000000000000000000'); // 1 token with 18 decimals // ✅ Convert strings to BigInt safely function 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); } // ❌ Don't use regular numbers for large amounts const amount = 1000000000000000000; // May lose precision

Error Handling

src/index.ts
class SafeTokenApp extends NApp { safeTransfer(to: string, mintId: string, amount: string): void { // Transaction actions cannot return values try { // Validate inputs if (!to || !mintId || !amount) { throw new Error('Missing required parameters'); } if (BigInt(amount) <= 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, BigInt(amount)); this.log('Transfer successful:', { to, mintId, amount }); } catch (error) { this.log('Transfer failed:', error.message); throw error; // Re-throw to fail the transaction } } }

Metadata Standards

Use consistent metadata structure for better interoperability:

interface TokenMetadata { name: string; // "My Token" symbol: string; // "MTK" decimals: number; // 18 description?: string; // Optional description image?: string; // Optional image URL website?: string; // Optional website social?: { // Optional social links twitter?: string; telegram?: string; discord?: string; }; } const metadata = JSON.stringify({ name: 'GameCoin', symbol: 'GAME', decimals: 18, description: 'Official currency for MyGame', image: 'https://mygame.com/token.png', website: 'https://mygame.com', social: { twitter: '@mygame', telegram: 'https://t.me/mygame' } } as TokenMetadata);

Advanced Example: Token Vesting

A complete token vesting contract example:

src/vesting.ts
import { NApp, NMap, createExecutableFunctions, ntransfer } from '@n1xyz/nts-compiler'; interface VestingSchedule { beneficiary: string; mintId: string; totalAmount: string; startTime: number; duration: number; claimed: string; } class TokenVesting extends NApp { vestingSchedules: NMap<VestingSchedule> = new NMap<VestingSchedule>(this, 'vesting'); scheduleCount: number; init(): void { this.scheduleCount = 0; } createVestingSchedule( beneficiary: string, mintId: string, totalAmount: string, durationMonths: number ): void { const scheduleId = `vest_${++this.scheduleCount}`; const startTime = this.time(); const duration = durationMonths * 30 * 24 * 60 * 60; // Convert to seconds // Transfer tokens to vesting contract ntransfer( this.signer(), this.appId(), mintId, BigInt(totalAmount) ); const schedule: VestingSchedule = { beneficiary, mintId, totalAmount, startTime, duration, claimed: '0' }; this.vestingSchedules.set(scheduleId, schedule, 'vesting_created', beneficiary); this.log('Vesting schedule created:', { scheduleId, beneficiary, totalAmount }); } claimVested(scheduleId: string): void { const schedule = this.vestingSchedules.get(scheduleId); if (!schedule) { throw new Error('Vesting schedule not found'); } if (this.signer() !== schedule.beneficiary) { throw new Error('Only beneficiary can claim'); } const now = this.time(); const elapsed = now - schedule.startTime; const vestedAmount = this.calculateVestedAmount(schedule, elapsed); const claimableAmount = BigInt(vestedAmount) - BigInt(schedule.claimed); if (claimableAmount <= 0) { throw new Error('No tokens available to claim'); } // Transfer vested tokens to beneficiary ntransfer( this.appId(), schedule.beneficiary, schedule.mintId, claimableAmount ); // Update claimed amount schedule.claimed = (BigInt(schedule.claimed) + claimableAmount).toString(); this.vestingSchedules.set(scheduleId, schedule, 'vesting_claimed', schedule.beneficiary); this.log('Vested tokens claimed:', { scheduleId, amount: claimableAmount.toString() }); } private calculateVestedAmount(schedule: VestingSchedule, elapsed: number): string { if (elapsed >= schedule.duration) { return schedule.totalAmount; // Fully vested } const vestedPercentage = elapsed / schedule.duration; const vestedAmount = BigInt(schedule.totalAmount) * BigInt(Math.floor(vestedPercentage * 1000000)) / BigInt(1000000); return vestedAmount.toString(); } } export const { createVestingSchedule, claimVested } = createExecutableFunctions(TokenVesting);

This comprehensive token system provides the foundation for building sophisticated DeFi applications, games, and other token-based systems on N1.

Last updated on