Token Management

The Aori protocol supports a wide range of tokens across multiple blockchain networks. This guide covers how to discover available tokens, load token metadata, and work with token information in your applications.

Overview

Token management in Aori involves:

  • Discovery: Finding available tokens across supported chains
  • Caching: Loading and storing token metadata locally for performance
  • Validation: Verifying token addresses and metadata before trading
  • Cross-Chain Mapping: Understanding token relationships across different networks

Token Information Structure

Each token in the Aori protocol includes comprehensive metadata:

interface TokenInfo {
  symbol: string;         // Token symbol (e.g., "USDC", "ETH")
  address: string;        // Token contract address
  chainId: number;        // Chain ID where token is deployed
  chainKey: string;       // Human-readable chain identifier
}

Loading All Tokens

During Initialization

Load all tokens when creating an Aori instance for immediate access:

import { Aori } from '@aori-io/aori-ts';

// Load all tokens during initialization
const aori = await Aori.create(
  'https://api.aori.io',
  'wss://api.aori.io',
  'your_api_key',
  true // Load tokens during initialization
);

// Access all cached tokens
const allTokens = aori.getAllTokens();
console.log(`Loaded ${allTokens.length} tokens across all chains`);

// Filter tokens by chain
const ethereumTokens = allTokens.filter(token => token.chainKey === 'ethereum');
const baseTokens = allTokens.filter(token => token.chainId === 8453);

Manual Loading

Load all tokens after initialization when needed:

const aori = await Aori.create(); // Don't load tokens initially

// Load all tokens manually
await aori.loadTokens();

const allTokens = aori.getAllTokens();
console.log(`Loaded ${allTokens.length} tokens`);

Loading Tokens by Chain

Using Chain Keys

Load tokens for specific chains using human-readable identifiers:

// Load tokens for specific chains
await aori.loadTokens('ethereum');
await aori.loadTokens('base');
await aori.loadTokens('arbitrum');
await aori.loadTokens('optimism');

// Get cached tokens for specific chains
const ethereumTokens = aori.getTokens('ethereum');
const baseTokens = aori.getTokens('base');

console.log(`Ethereum: ${ethereumTokens.length} tokens`);
console.log(`Base: ${baseTokens.length} tokens`);

Using Chain IDs

Load tokens using numeric chain IDs:

// Load tokens by chain ID
await aori.loadTokens(1);     // Ethereum
await aori.loadTokens(8453);  // Base
await aori.loadTokens(42161); // Arbitrum
await aori.loadTokens(10);    // Optimism

// Get cached tokens by chain ID
const ethereumTokens = aori.getTokens(1);
const baseTokens = aori.getTokens(8453);

Direct Token Fetching

Bypass Cache

Fetch tokens directly from the API without using the cache:

import { fetchAllTokens, getTokens } from '@aori-io/aori-ts';

// Fetch all tokens directly from API
const allTokens = await fetchAllTokens('https://api.aori.io', 'your_api_key');

// Fetch tokens for specific chain directly
const ethereumTokens = await getTokens('ethereum', 'https://api.aori.io', 'your_api_key');
const baseTokens = await getTokens(8453, 'https://api.aori.io', 'your_api_key');

With Instance Methods

Fetch tokens using instance methods to bypass cache:

// Fetch tokens for specific chain (bypasses cache)
const ethereumTokens = await aori.fetchTokens('ethereum');
const baseTokens = await aori.fetchTokens(8453);

// These don't update the internal cache - use loadTokens() for that

Finding Specific Tokens

By Symbol

Find tokens across chains by their symbol:

const allTokens = aori.getAllTokens();

// Find all USDC tokens across chains
const usdcTokens = allTokens.filter(token => token.symbol === 'USDC');

console.log('USDC tokens found:');
usdcTokens.forEach(token => {
  console.log(`${token.chainKey}: ${token.address}`);
});

// Find USDC on specific chain
const ethereumUSDC = allTokens.find(token => 
  token.symbol === 'USDC' && token.chainKey === 'ethereum'
);

By Address

Validate and find tokens by address:

const allTokens = aori.getAllTokens();

// Find token by address
const tokenAddress = '0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913';
const token = allTokens.find(token => 
  token.address.toLowerCase() === tokenAddress.toLowerCase()
);

if (token) {
  console.log(`Found ${token.symbol} on ${token.chainKey}`);
} else {
  console.log('Token not found or not supported');
}

Cross-Chain Token Mapping

Find the same token across different chains:

function findTokenAcrossChains(symbol: string, tokens: TokenInfo[]) {
  return tokens
    .filter(token => token.symbol === symbol)
    .reduce((acc, token) => {
      acc[token.chainKey] = token;
      return acc;
    }, {} as Record<string, TokenInfo>);
}

const allTokens = aori.getAllTokens();
const usdcMapping = findTokenAcrossChains('USDC', allTokens);

console.log('USDC addresses:');
Object.entries(usdcMapping).forEach(([chain, token]) => {
  console.log(`${chain}: ${token.address}`);
});

Native Token Handling

Native tokens (ETH) are represented with a special address across all chains:

import { NATIVE_TOKEN_ADDRESS, isNativeToken } from '@aori-io/aori-ts';

const allTokens = aori.getAllTokens();

// Find all native tokens
const nativeTokens = allTokens.filter(token => isNativeToken(token.address));

console.log('Native tokens:');
nativeTokens.forEach(token => {
  console.log(`${token.chainKey}: ${token.symbol} (${token.name})`);
});

// Check if a specific address is native
const isNative = isNativeToken('0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE'); // true
console.log(`Native token address: ${NATIVE_TOKEN_ADDRESS}`);

Validation and Verification

Token Address Validation

Validate token addresses before using them in quotes:

function validateTokenAddress(address: string, chainKey: string, tokens: TokenInfo[]): boolean {
  return tokens.some(token => 
    token.address.toLowerCase() === address.toLowerCase() && 
    token.chainKey === chainKey
  );
}

const allTokens = aori.getAllTokens();
const isValid = validateTokenAddress(
  '0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913',
  'base',
  allTokens
);

if (!isValid) {
  throw new Error('Token not supported on specified chain');
}

Token Existence Validation

Ensure tokens exist in the supported token list:

function validateTokenExists(tokenAddress: string, chainKey: string, tokens: TokenInfo[]): TokenInfo | null {
  const token = tokens.find(t => 
    t.address.toLowerCase() === tokenAddress.toLowerCase() && 
    t.chainKey === chainKey
  );
  
  return token || null;
}

Practical Examples

Building a Token Selector

Create a token selector component for your UI:

interface TokenSelectorProps {
  chainKey: string;
  onTokenSelect: (token: TokenInfo) => void;
}

function TokenSelector({ chainKey, onTokenSelect }: TokenSelectorProps) {
  const [tokens, setTokens] = useState<TokenInfo[]>([]);
  const [loading, setLoading] = useState(true);

  useEffect(() => {
    async function loadTokens() {
      try {
        const aori = await Aori.create();
        await aori.loadTokens(chainKey);
        const chainTokens = aori.getTokens(chainKey);
        setTokens(chainTokens);
      } catch (error) {
        console.error('Failed to load tokens:', error);
      } finally {
        setLoading(false);
      }
    }

    loadTokens();
  }, [chainKey]);

  if (loading) return <div>Loading tokens...</div>;

  return (
    <div className="token-selector">
      {tokens.map(token => (
        <div 
          key={`${token.chainId}-${token.address}`}
          className="token-item"
          onClick={() => onTokenSelect(token)}
        >
          <div>
            <div className="symbol">{token.symbol}</div>
            <div className="address">{token.address.slice(0, 6)}...{token.address.slice(-4)}</div>
          </div>
        </div>
      ))}
    </div>
  );
}

Cross-Chain Trading Setup

Set up tokens for cross-chain trading:

async function setupCrossChainTrade(
  inputChain: string,
  outputChain: string,
  tokenSymbol: string
) {
  const aori = await Aori.create();
  
  // Load tokens for both chains
  await Promise.all([
    aori.loadTokens(inputChain),
    aori.loadTokens(outputChain)
  ]);
  
  // Find token on both chains
  const inputTokens = aori.getTokens(inputChain);
  const outputTokens = aori.getTokens(outputChain);
  
  const inputToken = inputTokens.find(t => t.symbol === tokenSymbol);
  const outputToken = outputTokens.find(t => t.symbol === tokenSymbol);
  
  if (!inputToken) {
    throw new Error(`${tokenSymbol} not available on ${inputChain}`);
  }
  
  if (!outputToken) {
    throw new Error(`${tokenSymbol} not available on ${outputChain}`);
  }
  
  return { inputToken, outputToken };
}

// Usage
const { inputToken, outputToken } = await setupCrossChainTrade(
  'ethereum',
  'base', 
  'USDC'
);

// Now create quote with validated tokens
const quote = await aori.getQuote({
  offerer: userAddress,
  recipient: userAddress,
  inputToken: inputToken.address,
  outputToken: outputToken.address,
  inputAmount: '1000000', // 1 USDC (6 decimals)
  inputChain: 'ethereum',
  outputChain: 'base'
});

Token Metadata Cache

Implement a token metadata cache for performance:

class TokenCache {
  private cache = new Map<string, TokenInfo[]>();
  private aori: Aori;

  constructor(aori: Aori) {
    this.aori = aori;
  }

  async getTokens(chainKey: string): Promise<TokenInfo[]> {
    if (this.cache.has(chainKey)) {
      return this.cache.get(chainKey)!;
    }

    await this.aori.loadTokens(chainKey);
    const tokens = this.aori.getTokens(chainKey);
    this.cache.set(chainKey, tokens);
    
    return tokens;
  }

  findToken(address: string, chainKey: string): TokenInfo | undefined {
    const tokens = this.cache.get(chainKey);
    return tokens?.find(t => 
      t.address.toLowerCase() === address.toLowerCase()
    );
  }

  clearCache(chainKey?: string) {
    if (chainKey) {
      this.cache.delete(chainKey);
    } else {
      this.cache.clear();
    }
  }
}

Performance Considerations

Efficient Loading

Load tokens strategically based on your application needs:

// Load only what you need
const supportedChains = ['ethereum', 'base', 'arbitrum'];

async function loadRequiredTokens(aori: Aori) {
  // Load tokens for all supported chains in parallel
  await Promise.all(
    supportedChains.map(chain => aori.loadTokens(chain))
  );
}

// Or load on-demand
async function getTokensOnDemand(aori: Aori, chainKey: string) {
  const cached = aori.getTokens(chainKey);
  
  if (cached.length === 0) {
    await aori.loadTokens(chainKey);
    return aori.getTokens(chainKey);
  }
  
  return cached;
}

Filtering for UI

Filter tokens appropriately for different UI contexts:

function getPopularTokens(tokens: TokenInfo[]): TokenInfo[] {
  const popularSymbols = ['ETH', 'USDC', 'USDT', 'WETH', 'DAI'];
  
  return tokens.filter(token => 
    popularSymbols.includes(token.symbol)
  );
}

function getTestnetTokens(tokens: TokenInfo[]): TokenInfo[] {
  // Filter tokens based on testnet chain IDs
  const testnetChainIds = [5, 11155111]; // Goerli, Sepolia
  
  return tokens.filter(token => 
    testnetChainIds.includes(token.chainId)
  );
}

Error Handling

Implement robust error handling for token operations:

async function safeLoadTokens(aori: Aori, chainKey: string): Promise<TokenInfo[]> {
  try {
    await aori.loadTokens(chainKey);
    return aori.getTokens(chainKey);
  } catch (error) {
    console.error(`Failed to load tokens for ${chainKey}:`, error);
    
    // Fallback to direct fetch
    try {
      return await aori.fetchTokens(chainKey);
    } catch (fallbackError) {
      console.error(`Fallback token fetch failed:`, fallbackError);
      return [];
    }
  }
}

function validateTokenForTrading(token: TokenInfo | undefined, chainKey: string): TokenInfo {
  if (!token) {
    throw new Error(`Token not found on ${chainKey}`);
  }
  
  if (!token.address || token.address === '0x') {
    throw new Error('Invalid token address');
  }
  
  if (!token.symbol || token.symbol.trim() === '') {
    throw new Error('Invalid token symbol');
  }
  
  return token;
}