Day 6

AEX-9 代币标准

中级阶段 · 预计学习时间 90 分钟

学习目标
  • 理解 AEX-9 代币标准
  • 部署 AEX-9 代币合约
  • 执行代币转账和授权
  • 查询代币信息和余额
AEX-9 标准概述
什么是 AEX-9

AEX-9 是 Aeternity 的同质化代币标准,类似于以太坊的 ERC-20。

特性说明
同质化每个代币完全相同
可分割支持小数点(通常 18 位)
可转账支持点对点转账
可授权支持第三方转账
标准接口
// 必须实现的接口
entrypoint meta_info() : meta_info
entrypoint total_supply() : int
entrypoint balance(account : address) : int
entrypoint allowance(owner : address, spender : address) : int

stateful entrypoint transfer(to : address, value : int)
stateful entrypoint approve(spender : address, value : int)
stateful entrypoint transfer_from(from : address, to : address, value : int)

// 扩展接口(可选)
stateful entrypoint create_allowance(spender : address, value : int)
stateful entrypoint change_allowance(spender : address, value : int)
stateful entrypoint reset_allowance(spender : address)

// 可选功能
stateful entrypoint mint(account : address, value : int)
stateful entrypoint burn(value : int)
标准事件
datatype event =
    Transfer(address, address, int)      // from, to, value
    | Approval(address, address, int)    // owner, spender, value
    | Mint(address, int)                 // account, value
    | Burn(address, int)                 // account, value
AEX-9 合约实现
完整合约代码
@compiler >= 6

include "Option.aes"

contract AEX9Token =
    
    record meta_info = {
        name     : string,
        symbol   : string,
        decimals : int
    }

    record state = {
        owner        : address,
        total_supply : int,
        balances     : map(address, int),
        allowances   : map(address, map(address, int)),
        meta_info    : meta_info
    }

    datatype event =
        Transfer(address, address, int)
        | Approval(address, address, int)
        | Mint(address, int)
        | Burn(address, int)

    entrypoint init(name : string, symbol : string, decimals : int, initial_supply : int) = {
        owner = Call.caller,
        total_supply = initial_supply,
        balances = { [Call.caller] = initial_supply },
        allowances = {},
        meta_info = { name = name, symbol = symbol, decimals = decimals }
    }

    // ===== 查询接口 =====
    entrypoint meta_info() : meta_info = state.meta_info
    entrypoint total_supply() : int = state.total_supply
    entrypoint balance(account : address) : int =
        Map.lookup_default(account, state.balances, 0)
    
    // ===== 转账接口 =====
    stateful entrypoint transfer(to : address, value : int) =
        internal_transfer(Call.caller, to, value)

    // ... 更多实现详见完整合约
实践任务
任务 6.1: 部署 AEX-9 代币
from aeternity.node import NodeClient, Config
from aeternity.signing import Account
from aeternity.contract_native import ContractManager

client = NodeClient(Config(external_url='https://testnet.aeternity.io'))

# 加载账户
# account = Account.from_keystore("wallet.json", "password")

# AEX-9 合约源码(简化版)
aex9_source = '''
@compiler >= 6

contract AEX9Token =
    record meta_info = { name : string, symbol : string, decimals : int }
    record state = {
        meta_info : meta_info,
        total_supply : int,
        balances : map(address, int),
        allowances : map(address, map(address, int))
    }
    
    entrypoint init(name : string, symbol : string, decimals : int, supply : int) = {
        meta_info = { name = name, symbol = symbol, decimals = decimals },
        total_supply = supply,
        balances = { [Call.caller] = supply },
        allowances = {}
    }
'''

# 创建 ContractManager
manager = ContractManager(
    client=client,
    account=account,
    source=aex9_source
)

# 部署代币
tx = manager.deploy(
    '"MyToken"',      # 代币名称
    '"MTK"',          # 代币符号
    '18',             # 小数位数
    '1000000000000000000000000'  # 100万代币 (18位小数)
)

print(f"代币合约地址: {manager.contract_id}")
print(f"部署交易: {tx.hash}")
任务 6.2: 代币转账
# 假设已部署代币合约
contract_id = "ct_xxx..."
recipient = "ak_yyy..."

# 使用 ContractManager
manager = ContractManager(
    client=client,
    account=account,
    contract_id=contract_id,
    source=aex9_source
)

# 转账 100 个代币(18位小数)
amount = "100000000000000000000"  # 100 * 10^18

tx = manager.call("transfer", f'"{recipient}"', amount)
print(f"转账交易: {tx.hash}")

# 查询余额
sender_balance = manager.call_static("balance", f'"{account.get_address()}"')
recipient_balance = manager.call_static("balance", f'"{recipient}"')
print(f"发送者余额: {sender_balance}")
print(f"接收者余额: {recipient_balance}")
任务 6.3: 授权和转账 (Approve + TransferFrom)
# 场景:允许第三方(如交易所)代替你转账

# 步骤 1: 持有者授权
owner_manager = ContractManager(
    client=client,
    account=owner_account,
    contract_id=contract_id,
    source=aex9_source
)

# 授权 spender 可以转账 1000 个代币
spender_address = "ak_spender..."
amount = "1000000000000000000000"  # 1000 * 10^18

tx = owner_manager.call("approve", f'"{spender_address}"', amount)
print(f"授权交易: {tx.hash}")

# 步骤 2: 被授权者执行转账
spender_manager = ContractManager(
    client=client,
    account=spender_account,  # 被授权者的账户
    contract_id=contract_id,
    source=aex9_source
)

# 从 owner 转账到 recipient
recipient = "ak_recipient..."
transfer_amount = "500000000000000000000"  # 500 * 10^18

tx = spender_manager.call(
    "transfer_from",
    f'"{owner_account.get_address()}"',  # from
    f'"{recipient}"',                     # to
    transfer_amount                        # amount
)
print(f"转账交易: {tx.hash}")
任务 6.4: 代币信息格式化
def format_token_amount(amount: int, decimals: int, symbol: str = "") -> str:
    """格式化代币金额"""
    value = amount / (10 ** decimals)
    if value == int(value):
        formatted = f"{int(value):,}"
    else:
        formatted = f"{value:,.{decimals}f}".rstrip('0').rstrip('.')
    return f"{formatted} {symbol}".strip()

# 示例
decimals = 18
symbol = "MTK"

test_amounts = [
    1000000000000000000,      # 1 MTK
    1500000000000000000,      # 1.5 MTK
    100000000000000000000,    # 100 MTK
]

for amount in test_amounts:
    formatted = format_token_amount(amount, decimals, symbol)
    print(f"{amount:>30} → {formatted}")
练习题

目标:编写脚本,向多个地址空投代币。

def airdrop(manager, recipients, amount_each):
    """向多个地址空投代币"""
    results = []
    for recipient in recipients:
        try:
            tx = manager.call("transfer", f'"{recipient}"', str(amount_each))
            results.append({'address': recipient, 'status': 'success', 'tx': tx.hash})
        except Exception as e:
            results.append({'address': recipient, 'status': 'failed', 'error': str(e)})
    return results

# 使用示例
# recipients = ["ak_1...", "ak_2...", "ak_3..."]
# amount = 1000000000000000000  # 1 token
# results = airdrop(manager, recipients, amount)

目标:扩展 AEX-9 合约,添加锁仓功能。

// 在 state 中添加
record lock_info = { amount : int, unlock_height : int }
// locks : map(address, lock_info)

// 添加锁仓函数
stateful entrypoint lock(amount : int, duration : int) =
    let sender = Call.caller
    require(balance(sender) >= amount, "INSUFFICIENT_BALANCE")
    let unlock_height = Chain.block_height + duration
    put(state{
        balances = state.balances{ [sender] = balance(sender) - amount },
        locks = state.locks{ [sender] = { amount = amount, unlock_height = unlock_height }}
    })

// 添加解锁函数
stateful entrypoint unlock() =
    let sender = Call.caller
    switch(Map.lookup(sender, state.locks))
        None => abort("NO_LOCKED_TOKENS")
        Some(info) =>
            require(Chain.block_height >= info.unlock_height, "STILL_LOCKED")
            put(state{
                balances = state.balances{ [sender] = balance(sender) + info.amount },
                locks = Map.delete(sender, state.locks)
            })
常见问题

错误: BALANCE_NOT_ENOUGHINSUFFICIENT_BALANCE

解决: 检查发送者余额是否足够

错误: ALLOWANCE_NOT_ENOUGH

解决: 确保 approve 的额度足够

问题: 代币金额显示不正确

解决: 注意 decimals,通常是 18 位

# 1 代币 = 10^18 最小单位
amount_in_tokens = 100
amount_in_smallest = amount_in_tokens * (10 ** 18)
知识检查点

完成 Day 6 后,你应该能够: