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
- Input Validation - Always validate inputs
- Error Handling - Provide clear error messages
- Event Emission - Emit events for all actions
- Security - Implement proper access controls
- Testing - Thoroughly test all functions
Next Steps
- Explore DAO Application example
- Learn about NFT Marketplace implementation
- Check out DeFi Protocol example
Table of Contents