Token Contract

Build a simple token contract with transfer, approve, and balance functionality.

Overview

This example demonstrates how to create a basic token contract using ao-forge, including:

  • Token Minting - Create and mint new tokens
  • Transfer System - Transfer tokens between addresses
  • Approval System - Approve spending allowances
  • Balance Management - Track token balances
  • Supply Management - Manage total token supply
  • Events - Emit events for important actions

Project Setup

Create the Project

# Create new token contract project
ao-forge init my-token --template token

# Or create manually
ao-forge init my-token --framework nextjs
cd my-token

Install Dependencies

# Install additional dependencies
npm install @solana/web3.js @solana/wallet-adapter-react
npm install @headlessui/react @heroicons/react
npm install date-fns

AO Process Implementation

Token Contract (token.lua)

-- Simple Token Contract with ERC-20 like functionality
local token = {}

-- State management
local function getBalance(address)
    return tonumber(AO.load("balance:" .. address) or "0")
end

local function setBalance(address, amount)
    AO.store("balance:" .. address, tostring(amount))
end

local function getAllowance(owner, spender)
    return tonumber(AO.load("allowance:" .. owner .. ":" .. spender) or "0")
end

local function setAllowance(owner, spender, amount)
    AO.store("allowance:" .. owner .. ":" .. spender, tostring(amount))
end

local function getTotalSupply()
    return tonumber(AO.load("total_supply") or "0")
end

local function setTotalSupply(amount)
    AO.store("total_supply", tostring(amount))
end

-- Mint tokens
Handlers.add("mint", function(msg)
    local to = msg.To
    local amount = tonumber(msg.Amount)
    local minter = msg.From
    
    -- Check if minter is authorized (in a real contract, you'd have proper authorization)
    if not minter then
        return { error = "Unauthorized minter" }
    end
    
    if not to or not amount or amount <= 0 then
        return { error = "Invalid mint parameters" }
    end
    
    -- Update balances
    local currentBalance = getBalance(to)
    setBalance(to, currentBalance + amount)
    
    -- Update total supply
    local currentSupply = getTotalSupply()
    setTotalSupply(currentSupply + amount)
    
    -- Emit event
    AO.emit("Transfer", {
        from = "0x0000000000000000000000000000000000000000",
        to = to,
        amount = amount
    })
    
    return { 
        success = true, 
        balance = getBalance(to),
        totalSupply = getTotalSupply()
    }
end)

-- Transfer tokens
Handlers.add("transfer", function(msg)
    local from = msg.From
    local to = msg.To
    local amount = tonumber(msg.Amount)
    
    -- Validate inputs
    if not to or not amount or amount <= 0 then
        return { error = "Invalid transfer parameters" }
    end
    
    -- Check balance
    local balance = getBalance(from)
    if balance < amount then
        return { error = "Insufficient balance" }
    end
    
    -- Update balances
    setBalance(from, balance - amount)
    setBalance(to, getBalance(to) + amount)
    
    -- Emit event
    AO.emit("Transfer", {
        from = from,
        to = to,
        amount = amount
    })
    
    return { 
        success = true, 
        fromBalance = getBalance(from),
        toBalance = getBalance(to)
    }
end)

-- Approve spending
Handlers.add("approve", function(msg)
    local owner = msg.From
    local spender = msg.Spender
    local amount = tonumber(msg.Amount)
    
    -- Validate inputs
    if not spender or not amount or amount < 0 then
        return { error = "Invalid approve parameters" }
    end
    
    -- Set allowance
    setAllowance(owner, spender, amount)
    
    -- Emit event
    AO.emit("Approval", {
        owner = owner,
        spender = spender,
        amount = amount
    })
    
    return { 
        success = true, 
        allowance = getAllowance(owner, spender)
    }
end)

-- Transfer from (spender transfer)
Handlers.add("transfer_from", function(msg)
    local spender = msg.From
    local from = msg.FromAddress
    local to = msg.To
    local amount = tonumber(msg.Amount)
    
    -- Validate inputs
    if not from or not to or not amount or amount <= 0 then
        return { error = "Invalid transfer_from parameters" }
    end
    
    -- Check allowance
    local allowance = getAllowance(from, spender)
    if allowance < amount then
        return { error = "Insufficient allowance" }
    end
    
    -- Check balance
    local balance = getBalance(from)
    if balance < amount then
        return { error = "Insufficient balance" }
    end
    
    -- Update balances
    setBalance(from, balance - amount)
    setBalance(to, getBalance(to) + amount)
    
    -- Update allowance
    setAllowance(from, spender, allowance - amount)
    
    -- Emit event
    AO.emit("Transfer", {
        from = from,
        to = to,
        amount = amount
    })
    
    return { 
        success = true, 
        fromBalance = getBalance(from),
        toBalance = getBalance(to),
        allowance = getAllowance(from, spender)
    }
end)

-- Get balance
Handlers.add("balance_of", function(msg)
    local address = msg.Address
    
    if not address then
        return { error = "Address required" }
    end
    
    local balance = getBalance(address)
    
    return { 
        success = true, 
        address = address,
        balance = balance
    }
end)

-- Get allowance
Handlers.add("allowance", function(msg)
    local owner = msg.Owner
    local spender = msg.Spender
    
    if not owner or not spender then
        return { error = "Owner and spender required" }
    end
    
    local allowance = getAllowance(owner, spender)
    
    return { 
        success = true, 
        owner = owner,
        spender = spender,
        allowance = allowance
    }
end)

-- Get total supply
Handlers.add("total_supply", function(msg)
    local supply = getTotalSupply()
    
    return { 
        success = true, 
        totalSupply = supply
    }
end)

-- Burn tokens
Handlers.add("burn", function(msg)
    local from = msg.From
    local amount = tonumber(msg.Amount)
    
    if not amount or amount <= 0 then
        return { error = "Invalid burn amount" }
    end
    
    -- Check balance
    local balance = getBalance(from)
    if balance < amount then
        return { error = "Insufficient balance" }
    end
    
    -- Update balance and supply
    setBalance(from, balance - amount)
    setTotalSupply(getTotalSupply() - amount)
    
    -- Emit event
    AO.emit("Transfer", {
        from = from,
        to = "0x0000000000000000000000000000000000000000",
        amount = amount
    })
    
    return { 
        success = true, 
        balance = getBalance(from),
        totalSupply = getTotalSupply()
    }
end)

Frontend Implementation

Token Dashboard Component

// components/TokenDashboard.tsx
import React, { useState, useEffect } from 'react'
import { useWallet } from '@solana/wallet-adapter-react'

interface TokenInfo {
  balance: number
  totalSupply: number
  allowance: number
}

export default function TokenDashboard() {
  const { publicKey, connected } = useWallet()
  const [tokenInfo, setTokenInfo] = useState<TokenInfo>({
    balance: 0,
    totalSupply: 0,
    allowance: 0
  })
  const [loading, setLoading] = useState(true)
  const [transferAmount, setTransferAmount] = useState('')
  const [transferTo, setTransferTo] = useState('')
  const [approveAmount, setApproveAmount] = useState('')
  const [approveSpender, setApproveSpender] = useState('')

  useEffect(() => {
    if (connected && publicKey) {
      loadTokenInfo()
    }
  }, [connected, publicKey])

  const loadTokenInfo = async () => {
    if (!publicKey) return

    try {
      // Load balance
      const balanceResponse = await fetch('/api/token/balance', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ address: publicKey.toString() })
      })
      const balanceData = await balanceResponse.json()

      // Load total supply
      const supplyResponse = await fetch('/api/token/total-supply')
      const supplyData = await supplyResponse.json()

      setTokenInfo({
        balance: balanceData.balance || 0,
        totalSupply: supplyData.totalSupply || 0,
        allowance: 0 // This would be loaded for a specific spender
      })
    } catch (error) {
      console.error('Error loading token info:', error)
    } finally {
      setLoading(false)
    }
  }

  const transfer = async () => {
    if (!connected || !publicKey || !transferAmount || !transferTo) return

    try {
      const response = await fetch('/api/token/transfer', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({
          from: publicKey.toString(),
          to: transferTo,
          amount: parseFloat(transferAmount)
        })
      })

      if (response.ok) {
        loadTokenInfo()
        setTransferAmount('')
        setTransferTo('')
      }
    } catch (error) {
      console.error('Error transferring tokens:', error)
    }
  }

  const approve = async () => {
    if (!connected || !publicKey || !approveAmount || !approveSpender) return

    try {
      const response = await fetch('/api/token/approve', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({
          owner: publicKey.toString(),
          spender: approveSpender,
          amount: parseFloat(approveAmount)
        })
      })

      if (response.ok) {
        loadTokenInfo()
        setApproveAmount('')
        setApproveSpender('')
      }
    } catch (error) {
      console.error('Error approving tokens:', error)
    }
  }

  const mint = async (amount: number) => {
    if (!connected || !publicKey) return

    try {
      const response = await fetch('/api/token/mint', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({
          to: publicKey.toString(),
          amount: amount
        })
      })

      if (response.ok) {
        loadTokenInfo()
      }
    } catch (error) {
      console.error('Error minting tokens:', error)
    }
  }

  if (loading) {
    return <div>Loading token dashboard...</div>
  }

  return (
    <div className="max-w-4xl mx-auto p-6">
      <h1 className="text-3xl font-bold mb-8">Token Dashboard</h1>
      
      {/* Token Info */}
      <div className="bg-white rounded-lg shadow p-6 mb-8">
        <h2 className="text-xl font-semibold mb-4">Token Information</h2>
        <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
          <div>
            <p className="text-sm text-gray-600">Your Balance</p>
            <p className="text-2xl font-bold">{tokenInfo.balance.toLocaleString()}</p>
          </div>
          <div>
            <p className="text-sm text-gray-600">Total Supply</p>
            <p className="text-2xl font-bold">{tokenInfo.totalSupply.toLocaleString()}</p>
          </div>
        </div>
      </div>

      {/* Actions */}
      <div className="grid grid-cols-1 md:grid-cols-2 gap-6">
        {/* Transfer */}
        <div className="bg-white rounded-lg shadow p-6">
          <h3 className="text-lg font-semibold mb-4">Transfer Tokens</h3>
          <div className="space-y-4">
            <div>
              <label className="block text-sm font-medium text-gray-700 mb-1">
                To Address
              </label>
              <input
                type="text"
                value={transferTo}
                onChange={(e) => setTransferTo(e.target.value)}
                placeholder="Enter recipient address"
                className="w-full border rounded px-3 py-2"
              />
            </div>
            <div>
              <label className="block text-sm font-medium text-gray-700 mb-1">
                Amount
              </label>
              <input
                type="number"
                value={transferAmount}
                onChange={(e) => setTransferAmount(e.target.value)}
                placeholder="Enter amount"
                className="w-full border rounded px-3 py-2"
              />
            </div>
            <button
              onClick={transfer}
              disabled={!transferAmount || !transferTo}
              className="w-full bg-blue-500 text-white py-2 px-4 rounded hover:bg-blue-600 disabled:opacity-50"
            >
              Transfer
            </button>
          </div>
        </div>

        {/* Approve */}
        <div className="bg-white rounded-lg shadow p-6">
          <h3 className="text-lg font-semibold mb-4">Approve Spending</h3>
          <div className="space-y-4">
            <div>
              <label className="block text-sm font-medium text-gray-700 mb-1">
                Spender Address
              </label>
              <input
                type="text"
                value={approveSpender}
                onChange={(e) => setApproveSpender(e.target.value)}
                placeholder="Enter spender address"
                className="w-full border rounded px-3 py-2"
              />
            </div>
            <div>
              <label className="block text-sm font-medium text-gray-700 mb-1">
                Amount
              </label>
              <input
                type="number"
                value={approveAmount}
                onChange={(e) => setApproveAmount(e.target.value)}
                placeholder="Enter amount"
                className="w-full border rounded px-3 py-2"
              />
            </div>
            <button
              onClick={approve}
              disabled={!approveAmount || !approveSpender}
              className="w-full bg-green-500 text-white py-2 px-4 rounded hover:bg-green-600 disabled:opacity-50"
            >
              Approve
            </button>
          </div>
        </div>
      </div>

      {/* Mint Tokens */}
      <div className="bg-white rounded-lg shadow p-6 mt-6">
        <h3 className="text-lg font-semibold mb-4">Mint Tokens</h3>
        <div className="flex space-x-4">
          <button
            onClick={() => mint(100)}
            className="bg-purple-500 text-white py-2 px-4 rounded hover:bg-purple-600"
          >
            Mint 100
          </button>
          <button
            onClick={() => mint(1000)}
            className="bg-purple-500 text-white py-2 px-4 rounded hover:bg-purple-600"
          >
            Mint 1000
          </button>
          <button
            onClick={() => mint(10000)}
            className="bg-purple-500 text-white py-2 px-4 rounded hover:bg-purple-600"
          >
            Mint 10000
          </button>
        </div>
      </div>
    </div>
  )
}

API Routes

Token API

// pages/api/token/transfer.ts
import { NextApiRequest, NextApiResponse } from 'next'

export default async function handler(req: NextApiRequest, res: NextApiResponse) {
  if (req.method === 'POST') {
    try {
      const { from, to, amount } = req.body
      
      const response = await fetch(`${process.env.AO_PROCESS_URL}/transfer`, {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({
          From: from,
          To: to,
          Amount: amount
        })
      })
      
      const data = await response.json()
      res.json(data)
    } catch (error) {
      res.status(500).json({ error: 'Failed to transfer tokens' })
    }
  } else {
    res.setHeader('Allow', ['POST'])
    res.status(405).end(`Method ${req.method} Not Allowed`)
  }
}

Balance API

// pages/api/token/balance.ts
import { NextApiRequest, NextApiResponse } from 'next'

export default async function handler(req: NextApiRequest, res: NextApiResponse) {
  if (req.method === 'POST') {
    try {
      const { address } = req.body
      
      const response = await fetch(`${process.env.AO_PROCESS_URL}/balance_of`, {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({
          Address: address
        })
      })
      
      const data = await response.json()
      res.json(data)
    } catch (error) {
      res.status(500).json({ error: 'Failed to get balance' })
    }
  } else {
    res.setHeader('Allow', ['POST'])
    res.status(405).end(`Method ${req.method} Not Allowed`)
  }
}

Configuration

ao.config.yml

name: 'token-contract'
framework: 'nextjs'
packageManager: 'pnpm'
luaFiles: ['token.lua']
autoStart: true
processName: 'token-process'
ports:
  dev: 3000
  ao: 8080

Features

Token Operations

  • Minting - Create new tokens
  • Transferring - Send tokens to other addresses
  • Approving - Allow others to spend your tokens
  • Burning - Destroy tokens to reduce supply
  • Balance Checking - View token balances

Security Features

  • Input Validation - Validate all inputs
  • Balance Checks - Ensure sufficient balance
  • Allowance Checks - Verify spending permissions
  • Event Emission - Log all important actions

User Interface

  • Balance Display - Show current token balance
  • Transfer Form - Easy token transfers
  • Approval Form - Approve spending allowances
  • Mint Buttons - Quick token minting

Best Practices

  1. Input Validation - Always validate inputs
  2. Error Handling - Provide clear error messages
  3. Event Emission - Emit events for all actions
  4. Security - Implement proper access controls
  5. Testing - Thoroughly test all functions

Next Steps