Разработка токена стандарта FA2 (Tezos)

Проектируем и разрабатываем блокчейн-решения полного цикла: от архитектуры смарт-контрактов до запуска DeFi-протоколов, NFT-маркетплейсов и криптобирж. Аудит безопасности, токеномика, интеграция с существующей инфраструктурой.
Показано 1 из 1 услугВсе 1306 услуг
Разработка токена стандарта FA2 (Tezos)
Средняя
~2-3 рабочих дня
Часто задаваемые вопросы
Направления блокчейн-разработки
Этапы блокчейн-разработки
Последние работы
  • image_website-b2b-advance_0.png
    Разработка сайта компании B2B ADVANCE
    1221
  • image_web-applications_feedme_466_0.webp
    Разработка веб-приложения для компании FEEDME
    1163
  • image_websites_belfingroup_462_0.webp
    Разработка веб-сайта для компании БЕЛФИНГРУПП
    855
  • image_ecommerce_furnoro_435_0.webp
    Разработка интернет магазина для компании FURNORO
    1056
  • image_logo-advance_0.png
    Разработка логотипа компании B2B Advance
    561
  • image_crm_enviok_479_0.webp
    Разработка веб-приложения для компании Enviok
    828

Разработка токена стандарта 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 — это третьи стороны, которым owner разрешает управлять его токенами. Аналог 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):
    # Только owner может добавлять операторов для своих токенов
    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) → balance
                "operators": sp.big_map(),       # (owner, operator, token_id) → unit
            })
            self.init_metadata("metadata", metadata)
            
            # Инициализируем token 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-021 для 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 encoded). Инструменты (кошельки, explorers) декодируют обратно в строки. Это создаёт неудобство в коде, но обеспечивает совместимость.

Multi-asset: несколько токенов в одном контракте

Сила FA2 — token_id. Один контракт, три токена:

# Token 0: governance token (fungible)
# Token 1: utility token (fungible)
# Token 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. Это convention, не enforced протоколом.

Тестирование с Clarinet/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 недель.