Token System Guide
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:
- Balance Distribution: Tracks who owns how many tokens
- Admin Address: Controls who can modify the token
- 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
import {
NApp,
createExecutableFunctions,
nmint,
ntransfer,
nmintEdit
} from '@n1xyz/nts-compiler';
// Note: Only nread, nwrite, and log are globally available
Create Token Class
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
export const { createToken } = createExecutableFunctions(TokenCreator);
Advanced Token Creation
For tokens with structured metadata:
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:
// 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:
- Permission Check: Verifies sender has authority to transfer tokens
- Balance Check: Ensures sufficient token balance exists
- Amount Validation: Confirms positive transfer amount
- 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()
:
// 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.
// 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
// 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
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:
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.