Розробка токена FA2 (Tezos)
FA2 (TZIP-12) — мультиассетний стандарт токенів у екосистемі Tezos. На відміну від ERC-20/ERC-721, один FA2 контракт може управляти fungible та non-fungible токенами одночасно з довільним token_id. Це архітектурне рішення: замість розгортання окремого контракту для кожного токена — один контракт для цілого портфеля активів.
Смарт-контракти Tezos пишуться переважно на SmartPy або LIGO. Рантайм — Michelson. Розуміння Michelson потрібно для відладки та газової оптимізації, але писати на ньому напрямку не обов'язково.
Специфікація FA2: обов'язковий інтерфейс
Transfer entrypoint
Ключовий entrypoint — transfer. Приймає список transfer_batch: кожен batch описує відправника, список отримувачів та token_id/amount.
# SmartPy: FA2 transfer тип
# transfer_params: list of {from_: address, txs: list of {to_: address, token_id: nat, amount: nat}}
@sp.entrypoint
def transfer(self, batch):
for transfer in batch:
from_ = transfer.from_
# Перевіряємо авторизацію: відправник сам або approved operator
sp.verify(
(from_ == sp.sender) |
self.data.operators.contains(sp.record(
owner=from_,
operator=sp.sender,
token_id=transfer.txs[0].token_id # per-token operator
)),
"FA2_NOT_OPERATOR"
)
for tx in transfer.txs:
self._transfer_tokens(from_, tx.to_, tx.token_id, tx.amount)
Operators у FA2 — це треті сторони, яким власник дозволяє управляти своїми токенами. Аналог setApprovalForAll в ERC-721, але більш гранулярний: можна дати дозвіл на конкретний token_id.
Balance_of view
@sp.onchain_view()
def balance_of(self, requests):
# requests: list of {owner: address, token_id: nat}
# returns: list of {request: ..., balance: nat}
results = []
for req in requests:
balance = self.data.ledger.get(
sp.record(owner=req.owner, token_id=req.token_id),
default=0
)
results.append(sp.record(request=req, balance=balance))
return results
Це on-chain view — не змінює стан, викликається з інших контрактів або off-chain. Tezos відрізняється від EVM тим, що views явно об'являються як onchain_view.
Update_operators
@sp.entrypoint
def update_operators(self, updates):
for update in updates:
update.match_cases({
"add_operator": lambda op: self._add_operator(op),
"remove_operator": lambda op: self._remove_operator(op),
})
def _add_operator(self, op):
# Тільки власник може додавати операторів для своїх токенів
sp.verify(op.owner == sp.sender, "FA2_NOT_OWNER")
self.data.operators.add(sp.record(
owner=op.owner,
operator=op.operator,
token_id=op.token_id
))
Повна реалізація FA2 на SmartPy
import smartpy as sp
FA2_ERRORS = sp.utils.import_script_from_url(
"https://smartpy.io/templates/fa2_lib.py"
)
@sp.module
def main():
class FA2Token(
FA2_ERRORS.Fungible, # fungible token миксин
FA2_ERRORS.Admin, # admin контроль
FA2_ERRORS.MintFungible,
FA2_ERRORS.BurnFungible,
sp.Contract
):
def __init__(self, admin, metadata, token_metadata):
FA2_ERRORS.Admin.__init__(self, admin)
FA2_ERRORS.Fungible.__init__(self, {
"ledger": sp.big_map(), # (owner, token_id) → баланс
"operators": sp.big_map(), # (owner, operator, token_id) → unit
})
self.init_metadata("metadata", metadata)
# Ініціалізуємо токен 0
self.data.token_metadata = sp.big_map({
0: sp.record(
token_id=0,
token_info=token_metadata
)
})
self.data.supply = sp.big_map({0: 0})
@sp.entrypoint
def mint(self, to_, token_id, amount):
sp.verify(sp.sender == self.data.admin, "NOT_ADMIN")
# Оновлюємо баланс отримувача
key = sp.record(owner=to_, token_id=token_id)
current = self.data.ledger.get(key, default=0)
self.data.ledger[key] = current + amount
# Оновлюємо total supply
self.data.supply[token_id] = self.data.supply.get(token_id, default=0) + amount
Storage: big_map vs map
У Tezos важливо розрізняти map та big_map:
-
map— зберігається повністю в пам'яті при виконанні. Дорого по газу для великих колекцій. -
big_map— lazy loaded. При доступі до ключа платите тільки за читання цього ключа. Для ledger з тисячами адрес —big_mapобов'язковий.
# Правильно: big_map для ledger
self.data.ledger = sp.big_map(
tkey=sp.TRecord(owner=sp.TAddress, token_id=sp.TNat),
tvalue=sp.TNat
)
# Неправильно для продакшну: map перераховується повністю при кожному доступі
# self.data.ledger = sp.map(...) — тільки для малих колекцій
Tzip-16: метаданні токена
TZIP-16 — стандарт метаданих для Tezos контрактів. Метаданні зберігаються в storage контракту або посилаються на IPFS/HTTPS.
# Метаданні контракту (TZIP-16)
contract_metadata = {
"name": "My FA2 Token",
"description": "Utility token for platform",
"interfaces": ["TZIP-012", "TZIP-016"],
"homepage": "https://example.com"
}
# Метаданні токена (TZIP-21 для NFT)
token_metadata = {
"name": sp.utils.bytes_of_string("MyToken"),
"symbol": sp.utils.bytes_of_string("MTK"),
"decimals": sp.utils.bytes_of_string("6"),
"description": sp.utils.bytes_of_string("Utility token"),
"thumbnailUri": sp.utils.bytes_of_string("ipfs://Qm...")
}
Стандарт вимагає зберігати значення як bytes (UTF-8 кодування). Інструменти (кошельки, explorers) декодують назад у рядки. Створює незручність у коді, але забезпечує сумісність.
Multi-asset: кілька токенів в одному контракті
Сила FA2 — token_id. Один контракт, три токени:
# Токен 0: governance токен (fungible)
# Токен 1: utility токен (fungible)
# Токен 2-10000: NFT колекція (non-fungible, amount=1 завжди)
# При мінті NFT
@sp.entrypoint
def mint_nft(self, to_, token_id, metadata):
sp.verify(sp.sender == self.data.admin, "NOT_ADMIN")
sp.verify(~self.data.ledger.contains(
sp.record(owner=to_, token_id=token_id)), "NFT_EXISTS")
self.data.ledger[sp.record(owner=to_, token_id=token_id)] = 1
self.data.token_metadata[token_id] = sp.record(
token_id=token_id,
token_info=metadata
)
Для NFT: ніколи не мінтити amount > 1 для одного token_id. Це конвенція, не enforced протоколом.
Тестування з SmartPy
# SmartPy unit тест
@sp.add_test(name="FA2 Transfer Test")
def test():
sc = sp.test_scenario(main)
admin = sp.test_account("Admin")
alice = sp.test_account("Alice")
bob = sp.test_account("Bob")
token = main.FA2Token(
admin=admin.address,
metadata={"": sp.utils.bytes_of_string("ipfs://...")},
token_metadata={"symbol": sp.utils.bytes_of_string("MTK")}
)
sc += token
# Мінтимо 1000 токенів Alice
token.mint(to_=alice.address, token_id=0, amount=1000).run(sender=admin)
# Alice переводить 100 токенів Bob
token.transfer([sp.record(
from_=alice.address,
txs=[sp.record(to_=bob.address, token_id=0, amount=100)]
)]).run(sender=alice)
# Верифікуємо баланси
sc.verify(token.balance_of([
sp.record(owner=alice.address, token_id=0)
])[0].balance == 900)
sc.verify(token.balance_of([
sp.record(owner=bob.address, token_id=0)
])[0].balance == 100)
Розгортання та інструменти
SmartPy IDE (smartpy.io) — браузерний IDE з симулятором. Taquito — JavaScript бібліотека для взаємодії з Tezos (аналог ethers.js). Ghostnet — основний Tezos testnet. Temple Wallet / Kukai — кошельки для тестування.
// Taquito: взаємодія з FA2 контрактом
import { TezosToolkit } from "@taquito/taquito"
const Tezos = new TezosToolkit("https://ghostnet.ecadinfra.com")
const contract = await Tezos.contract.at("KT1...")
// Transfer токенів
const op = await contract.methods.transfer([{
from_: "tz1Alice...",
txs: [{ to_: "tz1Bob...", token_id: 0, amount: 100 }]
}]).send()
await op.confirmation(3)
Процес
Аналіз вимог (2-3 дні). Скільки token_id, fungible або NFT або mixed, кастомні entrypoints (whitelist, vesting), вимоги TZIP-16/21.
Розробка (1-3 тижні). FA2 базовий контракт → кастомна логіка → тести → розгортання на Ghostnet → верифікація через explorer (tzkt.io).
Інтеграція (1-2 тижні). Taquito frontend або backend інтеграція, wallet connection, transaction tracking.
Типовий FA2 токен зі стандартним функціоналом — 1-2 тижні. З кастомною токеномікою (vesting, bonding curve, governance) — 3-6 тижнів.







