Создание токена стандарта FA 1.2
На прошлых уроках мы изучили синтаксис языка программирования LIGO и опубликовали простейший контракт в тестовой сети Tezos.
Теперь мы изучим механизм работы цифровых активов, разберемся со стандартом FA 1.2, и выпустим токен в тестовой сети.
Виды и стандарты токенов
Для начала определимся с терминами. Токенами называют два типа единиц учета: нативные токены и токены, которые выпускают смарт-контракты. Логика работы первых описана в коде протокола, а вторых — в коде смарт-контрактов (СК).
Нативные токены также называют криптовалютами. К ним относятся биткоин, Ether (ETH), Tezos (XTZ, tez), Litecoin и другие.
Токены смарт-контрактов — цифровые активы, созданные пользователями блокчейна, например USDT и DOGE на блокчейне Ethereum, kUSD — на Tezos.
В этом и следующем уроках мы будем говорить о токенах смарт-контрактов, но для простоты называть их токенами.
Мы выяснили, что логика работы токенов не описана в протоколе. Разработчики создают ее в коде смарт-контрактов: добавляют базовые функции вроде хранения балансов и передачи монет.
Так, 100 kUSD на кошельке пользователя — это запись в хранилище СК вида «Адрес TZ1 может отправить 100 токенов этого контракта». Другими словами, токены — это реестры внутри блокчейна.
Чтобы разработчики выпускали рабочие и устойчивые к атакам смарт-контракты, сообщество принимает стандарты токенов. Они содержат списки функций, необходимых для взаимодействия смарт-контракта с кошельками и блокчейн-приложениями.
Разработчики могут прописывать в смарт-контрактах дополнительные функции, например сжигание токенов. Но базовые функции стандарта обязательны.
Базовые функции стандарта FA 1.2
Стандарты Tezos называются Financial Application (FA). Два самых популярных из них:
- FA 1.2 для взаимозаменяемых токенов;
- FA 2 для взаимозаменяемых токенов и NFT.
На этом уроке мы рассмотрим более простой стандарт FA 1.2, а на следующем — разберемся с FA 2.
Стандарт FA 1.2 включает функции:
- передача (transfer) — проведение транзакций между пользователями;
- одобрение (approve) — разрешение Алисе отправить токены с адреса Боба. Позволяет контракту отправлять пользователем запросы на транзакции;
- одобренное количество (getAllowance) — просмотр количества токенов, которые Алиса может отправить с адреса контракта;
- баланс пользователя (getBallance) — просмотр баланса пользователя;
- свободная эмиссия (getTotalSupply) — просмотр количества токенов на адресах всех пользователей.
Кроме того, контракт токена должен хранить в storage информацию о полной эмиссии и балансах пользователей, а в коде — сообщения о стандартных ошибках вроде нехватки средств.
Разработчик может включить в хранилище метаданные с названием контракта и токена, а также ссылку на его логотип.
Что такое имплементация FA 1.2
Имплементация — это рабочий продукт на основе какой-либо идеи. В случае FA 1.2 — шаблон смарт-контракта для операций с токенами.
Разработчик может скопировать имплементацию, изменить ее под свои задачи, опубликовать в блокчейне и получить работоспособный токен.
Имплементация токена FA 1.2 состоит примерно из сотни строк. Код можно разделить на восемь частей:
- псевдонимы, типы данных и псевдо-точки входа;
- функция getAccount, которая получает данные пользователя;
- функция getAllowance, которая получает количество токенов, доступных для транзакции;
- функция transfer для перевода токенов из одного адреса на другой;
- функция approve, которая подтверждает право пользователя на перевод токенов;
- функция getBalance, которая возвращает баланс пользователя;
- функция getTotalSupply, которая возвращает количество свободных токенов;
- функция main, которая принимает входящие параметры и передает их одной из предыдущих функций.
Ниже подробнее расскажем о каждой из этих частей.
Объявление псевдонимов и типов
В начале контракта нужно объявить все типы и псевдонимы, которые он будет использовать в функциях:
//объявляем псевдоним trusted типа address. Мы будем использовать его для обозначения адресов, у которых есть право отправлять токены
type trusted is address;
//объявляем псевдоним amt (amount) типа nat для хранения балансов
type amt is nat;
(* объявляем псевдоним account типа record. В нем будем хранить данные пользователей, которым можно передавать токены.
*)
type account is
record [
balance : amt;
allowances : map (trusted, amt);
]
(* объявляем тип хранилища смарт-контракта. В нем хранится общее количество токенов, а также структура данных big_map, которая связывает публичные адреса и балансы пользователей *)
type storage is
record [
totalSupply : amt;
ledger : big_map (address, account);
]
(* объявляем псевдоним для метода return, который будем использовать для возвращения операций. В коротких контрактах можно обойтись без него, но в контрактах с несколькими псевдо-точками входа проще один раз прописать тип возврата и использовать его в каждой функции *)
type return is list (operation) * storage
(* объявляем пустой список noOperations. Его будут возвращать функции transfer и approve *)
const noOperations : list (operation) = nil;
Компилятор Michelson автоматически сортирует содержимое продвинутых структур данных в алфавитном порядке. Сортировка может случайно сломать контракт. Например, если первый параметр — адрес отправителя — начинается с TZ19, а второй — адрес получателя — начинается с TZ11, компилятор поменяет их местами. В таком случае контракт попытается отправить токены не с того аккаунта.
Чтобы сохранить нужный порядок параметров, разработчики записывают важные структуры данных в типе michelson_pair. Такая структура включает два аргумента и их названия, например, два адреса: sender и receiver. Компилятор переносит в код на Michelson значения michelson_pair без сортировки.
(* объявляем псевдонимы входящих параметров для каждой базовой функции FA 1.2. *)
// функция transfer получает на вход адрес отправителя, адрес получателя и сумму транзакции
type transferParams is michelson_pair(address, "from", michelson_pair(address, "to", amt, "value"), "")
// approve получает адрес пользователя и количество токенов, которые он может отправить с баланса смарт-контракта
type approveParams is michelson_pair(trusted, "spender", amt, "value")
// getBallance получает адрес пользователя и прокси-контракта, которому она отправит данные о балансе
type balanceParams is michelson_pair(address, "owner", contract(amt), "")
// getAllowance получает адрес пользователя, данные его аккаунта в смарт-контракте и прокси-контракт
type allowanceParams is michelson_pair(michelson_pair(address, "owner", trusted, "spender"), "", contract(amt), "")
// totalSupply не использует michelson_pair, потому что первый входящий параметр — пустое значение unit — и так окажется первым после сортировки компилятора Michelson
type totalSupplyParams is (unit * contract(amt))
(* объявляем псевдо-точки входа: даем название и присваиваем им тип параметров, которые описали выше*)
type entryAction is
| Transfer of transferParams
| Approve of approveParams
| GetBalance of balanceParams
| GetAllowance of allowanceParams
| GetTotalSupply of totalSupplyParams
Функция getAccount получает входящий параметр типа address и значение storage из смарт-контракта:
function getAccount (const addr : address; const s : storage) : account is
block {
// присваиваем переменной acct значение типа account: нулевой баланс и пустую запись allowances
var acct : account :=
record [
balance = 0n;
allowances = (map [] : map (address, amt));
];
(* проверяем, есть ли в хранилище аккаунт пользователя. Если нет — оставляем в acct пустое значение из предыдущего блока. Если есть — присваиваем переменной acct значение из хранилища. Функция возвращает значение acct *)
case s.ledger[addr] of
None -> skip
| Some(instance) -> acct := instance
end;
} with acct
Функция getAllowance спрашивает у пользователя, сколько токенов он разрешает перевести на другой адрес. Она получает адрес пользователя, адрес контракта (spender) и состояние хранилища, а возвращает аргумент amt — количество токенов разрешенных для расходования:
function getAllowance (const ownerAccount : account; const spender : address; const s : storage) : amt is
(* если пользователь разрешил отправить некоторое количество токенов, функция присваивает это количество переменной amt. Если не разрешил — количество токенов равняется нулю *)
case ownerAccount.allowances[spender] of
Some (amt) -> amt
| None -> 0n
end;
Функция transfer получает от пользователя адреса отправителя и получателя, количество токенов для перевода и состояние хранилища:
function transfer (const from_ : address; const to_ : address; const value : amt; var s : storage) : return is
block {
(* вызываем функцию getAccount, чтобы присвоить переменной senderAccount данные аккаунта пользователя. Затем мы используем senderAccount, чтобы считывать баланс пользователя и разрешения *)
var senderAccount : account := getAccount(from_, s);
(* проверяем, достаточно ли у пользователя средств для перевода. Если нет — виртуальная машина прерывает исполнение контракта, если достаточно — продолжает исполнять контракт *)
if senderAccount.balance < value then
failwith("NotEnoughBalance")
else skip;
(* проверяем, может ли адрес-инициатор транзакции отправить токены. Если он запрашивает перевод из чужого адреса, функция запрашивает разрешение у настоящего владельца. Если инициатор и отправитель — один адрес, виртуальная машина продолжает исполнять контракт *)
if from_ =/= Tezos.sender then block {
(* вызываем функцию getAllowance, чтобы владелец адреса-отправителя указал, сколько токенов он разрешает отправить. Присваиваем это значение константе spenderAllowance *)
const spenderAllowance : amt = getAllowance(senderAccount, Tezos.sender, s);
(* если владелец разрешил отправить меньше токенов, чем указано во входящем параметре, виртуальная машина прекратит исполнять контракт *)
if spenderAllowance < value then
failwith("NotEnoughAllowance")
else skip;
(* отнимаем от разрешенного для отправки количества токенов сумму транзакции *)
senderAccount.allowances[Tezos.sender] := abs(spenderAllowance - value);
} else skip;
(* отнимаем от баланса адреса-отправителя количество отправленных токенов *)
senderAccount.balance := abs(senderAccount.balance - value);
(* обновляем запись о балансе отправителя в storage *)
s.ledger[from_] := senderAccount;
(* еще раз вызываем функцию getAccount, чтобы получить или создать запись аккаунта для адреса-получателя *)
var destAccount : account := getAccount(to_, s);
(* добавляем к балансу получателя количество отправленных токенов *)
destAccount.balance := destAccount.balance + value;
(* обновляем запись о балансе получателя в storage *)
s.ledger[to_] := destAccount;
}
// возвращаем пустой список операций и состояние storage после исполнения функции
with (noOperations, s)
Функция approve запрашивает подтверждение количества токенов, которое spender-адрес может отправить с sender-адреса. Пример использования: блокчейн-приложение (spender) запрашивает у пользователя (sender) разрешение на отправку токенов.
Эта функция подвержена атаке опережающего расходования. Например, sender снижает количество разрешенных для отправки токенов с 20 до 10. Spender узнает об этом и создает транзакцию с расходованием 20 токенов. Он платит повышенную комиссию, чтобы транзакция с тратой попала в блок раньше, чем транзакция со сменой разрешений.
Spender получает 20 токенов и дожидается смены разрешения. После этого он создает еще одну транзакцию — на отправку 10 токенов. Функция разрешает отправку. В результате spender отправляет с sender-адреса 30 токенов вместо разрешенных 10.
Чтобы избежать подобной ситуации, разработчики реализуют в approve задержку смены разрешения. Если разрешенное количество токенов больше нуля, его можно изменить только на нуль, а если равно нулю — его можно изменить на натуральное число. В таком случае spender-адрес не может потратить токены дважды.
function approve (const spender : address; const value : amt; var s : storage) : return is
block {
(* получаем данные аккаунта пользователя *)
var senderAccount : account := getAccount(Tezos.sender, s);
(* получаем текущее количество токенов, которое пользователь разрешил отправить *)
const spenderAllowance : amt = getAllowance(senderAccount, spender, s);
if spenderAllowance > 0n and value > 0n then
failwith("UnsafeAllowanceChange")
else skip;
(* вносим в данные аккаунта новое разрешенное количество токенов для расходования *)
senderAccount.allowances[spender] := value;
(* обновляем хранилище смарт-контракта *)
s.ledger[Tezos.sender] := senderAccount;
} with (noOperations, s)
Функции getBalance, getAllowance и getTotalSupply относятся к обзорным (view). Они возвращают запрашиваемое значение не пользователю, а специальному промежуточному контракту (proxy-contract). Последний позволяет приложениям получать данные от пользовательских контрактов и отображать их в интерфейсе.
Функция getBalance возвращает значение баланса заданного адреса:
function getBalance (const owner : address; const contr : contract(amt); var s : storage) : return is
block {
//присваиваем константе ownerAccount данные аккаунта
const ownerAccount : account = getAccount(owner, s);
}
//возвращаем промежуточному контракту баланс аккаунта
with (list [transaction(ownerAccount.balance, 0tz, contr)], s)
Функция getAllowance возвращает количество разрешенных для расходования токенов запрашиваемого аккаунта:
function getAllowance (const owner : address; const spender : address; const contr : contract(amt); var s : storage) : return is
block {
//получаем данные аккаунта, а из них — количество разрешенных для расходования токенов
const ownerAccount : account = getAccount(owner, s);
const spenderAllowance : amt = getAllowance(ownerAccount, spender, s);
} with (list [transaction(spenderAllowance, 0tz, contr)], s)
Функция getTotalSupply возвращает количество токенов на балансах всех пользователей:
function getTotalSupply (const contr : contract(amt); var s : storage) : return is
block {
skip
} with (list [transaction(s.totalSupply, 0tz, contr)], s)
Главная функция принимает название псевдо-точки входа и ее параметры:
function main (const action : entryAction; var s : storage) : return is
block {
skip
} with case action of
| Transfer(params) -> transfer(params.0, params.1.0, params.1.1, s)
| Approve(params) -> approve(params.0, params.1, s)
| GetBalance(params) -> getBalance(params.0, params.1, s)
| GetAllowance(params) -> getAllowance(params.0.0, params.0.1, params.1, s)
| GetTotalSupply(params) -> getTotalSupply(params.1, s)
end;
Готовим смарт-контракт токена к публикации
Чтобы не тратить время на добавление Taquito и создание файлов проекта, воспользуемся папкой taq-test из прошлого урока.
Запустите редактор VS Code. Создайте в папке taq-test папку token, а в ней — файл token.ligo. Скопируйте в файл код токена.
//объявляем псевдоним trusted типа address. Мы будем использовать его для обозначения адресов, которые могут пересылать токены с контракта
type trusted is address;
//объявляем псевдоним amt (amount) типа nat для хранения балансов
type amt is nat;
(* объявляем псевдоним account типа record. В нем будем хранить данные пользователей, которым можно передавать токены.
*)
type account is
record [
balance : amt;
allowances : map (trusted, amt);
]
(* объявляем тип хранилища смарт-контракта. Он хранит общее количество токенов, а также big_map,
который связывает публичные адреса и данные account пользователей *)
type storage is
record [
totalSupply : amt;
ledger : big_map (address, account);
]
(* объявляем псевдоним для метода return, который будем использовать для возвращения операций. В коротких контрактах можно обойтись без него.
Но в контрактах с несколькими псевдо-точками входа проще один раз прописать тип возврата и использовать его для каждой функции *)
type return is list (operation) * storage
(* объявляем пустой список noOperations. Его будет возвращать метод return *)
const noOperations : list (operation) = nil;
(* объявляем псевдонимы входящих параметров для каждой базовой функции FA1.2 *)
type transferParams is michelson_pair(address, "from", michelson_pair(address, "to", amt, "value"), "")
type approveParams is michelson_pair(trusted, "spender", amt, "value")
type balanceParams is michelson_pair(address, "owner", contract(amt), "")
type allowanceParams is michelson_pair(michelson_pair(address, "owner", trusted, "spender"), "", contract(amt), "")
type totalSupplyParams is (unit * contract(amt))
(* псевдо-точки входа *)
type entryAction is
| Transfer of transferParams
| Approve of approveParams
| GetBalance of balanceParams
| GetAllowance of allowanceParams
| GetTotalSupply of totalSupplyParams
(* функция getAccount получает входящий параметр типа address и значение storage из смарт-контракта.
Это функция-помощник: ее вызывают другие функции контракта для получения данных о пользователе, а пользователь не может вызвать ее напрямую *)
function getAccount (const addr : address; const s : storage) : account is
block {
// присваиваем переменной acct значение типа account: нулевой баланс и пустую запись allowances
var acct : account :=
record [
balance = 0n;
allowances = (map [] : map (address, amt));
];
(* проверяем, есть ли в хранилище аккаунт пользователя. Если нет — оставляем в acct пустое значение из предыдущего блоа.
Если есть — присваиваем его переменной acct. Функция возвращает запись acct *)
case s.ledger[addr] of
None -> skip
| Some(instance) -> acct := instance
end;
} with acct
(* getAllowance запрашивает у пользователя разрешение отрпавить токены из его адреса.
Она получает адрес пользователя, адрес контракта spender и состояние хранилища *)
function getAllowance (const ownerAccount : account; const spender : address; const _s : storage) : amt is
(* если пользователь разрешил отправить некоторое количество токенов, функция присваивает это количество переменной amt.
Если не разрешил — количество токенов равняется нулю *)
case ownerAccount.allowances[spender] of
Some (amt) -> amt
| None -> 0n
end;
(* Функция Transfer получает от пользователя адреса отправителя и получателя, количество токенов для перевода и состояние хранилища *)
function transfer (const from_ : address; const to_ : address; const value : amt; var s : storage) : return is
block {
(* вызываем фукнцию getAccount, чтобы присвоить ей данные аккаунта пользователя.
Затем мы используем senderAccount чтобы считывать баланс пользователя и разрешения *)
var senderAccount : account := getAccount(from_, s);
(* проверяем, есть ли у пользователя достаточно средств для перевода.
Если нет — виртуальная машина прерывает исполнение контракта, если есть — продолжает исполнять контракт *)
if senderAccount.balance < value then
failwith("NotEnoughBalance")
else skip;
(* проверяем, может ли адрес-инициатор транзакции отправить токены.
Если адрес-инициатор запрашивает перевод из чужого адреса, функция запрашивает разрешение у настоящего владельца. Если инциатор и отправитель — один адрес, виртуальная машина продолжает исполнять контракт *)
if from_ =/= Tezos.sender then block {
(* вызываем функцию getAllowance, чтобы владелец адреса-отправителя указал, сколько токенов он разрешает отправить.
Присваиваем это значение константе spenderAllowance *)
const spenderAllowance : amt = getAllowance(senderAccount, Tezos.sender, s);
(* если владелец разрешил отправить меньше токенов, чем указано во входящем параметре, виртуальная машина прекратит исполнять контракт *)
if spenderAllowance < value then
failwith("NotEnoughAllowance")
else skip;
(* отнимаем от разрешенного для отправки количества токенов сумму транзакции *)
senderAccount.allowances[Tezos.sender] := abs(spenderAllowance - value);
} else skip;
(* отнимаем от баланса адреса-отправителя количество отправленных токенов *)
senderAccount.balance := abs(senderAccount.balance - value);
(* обновляем запись о балансе отправителя в storage *)
s.ledger[from_] := senderAccount;
(* еще раз вызываем функцию getAccount, чтобы получить или создать запись аккаунта для адреса-получателя *)
var destAccount : account := getAccount(to_, s);
(* добавляем к балансу получателя количество отправленных токенов *)
destAccount.balance := destAccount.balance + value;
(* обновляем запись о балансе получателя в storage *)
s.ledger[to_] := destAccount;
}
// возвращаем пустой список операций и состояние storage после исполнения функции
with (noOperations, s)
(* функция Approve запрашивает подверждение на количество токенов, которое адрес-ицинатор может отправить из адреса пользователей *)
function approve (const spender : address; const value : amt; var s : storage) : return is
block {
(* получаем данные аккаунта-иницатора *)
var senderAccount : account := getAccount(Tezos.sender, s);
(* получаем текущее количество токенов, которое пользователь разрешил отправить *)
const spenderAllowance : amt = getAllowance(senderAccount, spender, s);
(* защищаем контракт от атаки с опережающим расходованием. Допустим, пользователь изменяет разрешенное количество токенов для отправки с 20 до 10.
Владелец адреса-инициатора может об этом узнать и создать транзакцию с расходованием 20 токенов.
Если он оплатит повышеннуюю комиссию, то эта транзакция попадет в блок раньше, чем пользователь изменит разрешенное количество токенов.
По обновлении разрешения владелец адреса-инициатора создаст еще одну транзакцию на 10 токенов.
В результате он отправит из адреса пользователя 30 токенов вместо 10. Чтобы такого не случилось, разработчики добавляют эту защиту *)
(* если старое разрешенное количество токенов для расходования больше нуля, его можно изменить только на ноль.
Если оно равно нулю, его можно изменить на другое натуральное число *)
if spenderAllowance > 0n and value > 0n then
failwith("UnsafeAllowanceChange")
else skip;
(* вносим в данные аккаунта новое разрешенное количество токенов для расходывания *)
senderAccount.allowances[spender] := value;
(* обновляем хранилище смарт-контракта *)
s.ledger[Tezos.sender] := senderAccount;
} with (noOperations, s)
(* Функции getBalance, getAllowance и getTotalSupply относятся к обзорным (view).
Они возвращают запрашиваемое значение не пользователю, а специальному промежуточному контракту (proxy-contract).
Во входящих параметрах для вызова этих функций пользователь должен указать адрес промежуточного контракта *)
(* Функция getBallance возвращает значение баланса заданного адреса *)
function getBalance (const owner : address; const contr : contract(amt); var s : storage) : return is
block {
//присваиваем константе ownerAccaunt данные аккаунта
const ownerAccount : account = getAccount(owner, s);
}
//возвращаем промежуточному контракту баланс аккаунта
with (list [transaction(ownerAccount.balance, 0tz, contr)], s)
(* Функция getAllowance возвращает количество разрешенных для расходования токенов запрашиваемого аккаунта *)
function getAllowance (const owner : address; const spender : address; const contr : contract(amt); var s : storage) : return is
block {
//получаем данные аккаунта, а из них — количество разрешенных для расходования токенов
const ownerAccount : account = getAccount(owner, s);
const spenderAllowance : amt = getAllowance(ownerAccount, spender, s);
} with (list [transaction(spenderAllowance, 0tz, contr)], s)
(* Функция getTotalSupply возвращает количество токенов на балансах всех пользователей *)
function getTotalSupply (const contr : contract(amt); var s : storage) : return is
block {
skip
} with (list [transaction(s.totalSupply, 0tz, contr)], s)
(* Главная функция принимает название псевдо-точки входа и ее параметры *)
function main (const action : entryAction; var s : storage) : return is
block {
skip
} with case action of
| Transfer(params) -> transfer(params.0, params.1.0, params.1.1, s)
| Approve(params) -> approve(params.0, params.1, s)
| GetBalance(params) -> getBalance(params.0, params.1, s)
| GetAllowance(params) -> getAllowance(params.0.0, params.0.1, params.1, s)
| GetTotalSupply(params) -> getTotalSupply(params.1, s)
end;
Код на LIGO нужно скомпилировать в Michelson, чтобы виртуальная машина Tezos могла его исполнить. На прошлом уроке мы вставили код контракта из двух строчек напрямую в скрипт развертывания. Код токена включает 200 строк, поэтому лучше сохранить его отдельным файлом и импортировать в скрипт с помощью команды import.
Откройте онлайн-среду LIGO и вставьте код токена в поле редактора. В выпадающем списке выберите пункт Compile Contract и поставьте галочку в поле Output Michelson in JSON format. Нажмите кнопку Run. Компилятор выдаст готовый код под полем редактора.
Создайте в папке token файл token.json и вставьте в него JSON-код.
На прошлом уроке мы вписали в скрипт deploy.ts данные тестового аккаунта и настроили RPC-ссылку публичного узла тестовой сети Tezos. Мы можем использовать этот код для публикации токена.
Создайте в папке taq-test файл token-deploy.ts и вставьте в него код из deploy.ts. Затем его нужно доработать: добавить метод file sync для чтения других файлов, импортировать код контракта и указать начальное состояние хранилища.
После методов import добавьте метод file sync и константу Tezos для вызова методов Taquito:
const fs = require('fs')
const { Tezos } = require('@taquito/taquito')
Замените метод try. После этого задайте исходное состояние хранилища — общее количество токенов и баланс пользователя:
try {
const op = await tezos.contract.originate({
// считываем код из файла token.json
code: JSON.parse(fs.readFileSync("./token.json").toString()),
// задаем состояние хранилища на языке Michelson. Замените оба адреса на адрес своего аккаунта в тестовой сети,
// а числа — на количество токенов, которое вы хотите выпустить
init:
'(Pair { Elt "tz1imn4fjJFwmNaiEWnAGdRrRHxzxrBdKafZ" (Pair { Elt "tz1imn4fjJFwmNaiEWnAGdRrRHxzxrBdKafZ" 1000 } 1000) } 1000)',
})
Готовый код выглядит так:
import { TezosToolkit } from '@taquito/taquito'
import { importKey } from '@taquito/signer'
const { Tezos } = require('@taquito/taquito')
const fs = require('fs')
const provider = 'https://florencenet.api.tez.ie'
async function deploy() {
const tezos = new TezosToolkit(provider)
await importKey(
tezos,
'hoqfgsoy.qyisbhtk@tezos.example.org', //почта
'ZnnZLS0v6O', //пароль
[
'able', //мнемоника
'public',
'usual',
'hello',
'october',
'owner',
'essence',
'old',
'author',
'original',
'various',
'gossip',
'core',
'high',
'hire',
].join(' '),
'2bed8dc244ee43a1e737096c4723263c269049d8' //приватный ключ
)
try {
const op = await tezos.contract.originate({
// считываем код из файла token.json
code: JSON.parse(fs.readFileSync('./token.json').toString()),
// задаем состояние хранилища на языке Michelson. Замените оба адреса на адрес своего аккаунта в тестовой сети,
// а числа — на количество токенов, которое вы хотите выпустить
init: '(Pair { Elt "tz1imn4fjJFwmNaiEWnAGdRrRHxzxrBdKafZ" (Pair { Elt "tz1imn4fjJFwmNaiEWnAGdRrRHxzxrBdKafZ" 1000 } 1000) } 1000)',
})
//начало развертывания
console.log('Awaiting confirmation...')
const contract = await op.contract()
//отчет о развертывании: количество использованного газа, значение хранилища
console.log('Gas Used', op.consumedGas)
console.log('Storage', await contract.storage())
//хеш операции, по которому можно найти контракт в блокчейн-обозревателе
console.log('Operation hash:', op.hash)
} catch (ex) {
console.error(ex)
}
}
deploy()
Откройте терминал и выполните команду npx ts-node token-deploy.ts. Через несколько минут Taquito опубликует контракт токена в тестовой сети и выдаст хеш операции.
Найдите ее в florence.tzstats и проверьте состояние хранилища контракта. У нас в нем записана тысяча токенов, у вас — ваше количество.
В поле Bigmap находятся записи об адресах владельцев токенов. При публикации контракта мы выпустили все токены на тестовый адрес.
Отправляем токены с помощью Taquito
Помните, как мы добавили число в хранилище контракта с помощью Taquito? Теперь усложним задачу: вызовем функцию transfer и передадим токены другому пользователю.
Установите кошелек Tezos и создайте аккаунт, если не сделали это на прошлом уроке. Мы рекомендуем Temple Wallet, потому что он из коробки поддерживает тестовые сети Tezos.
Откройте VS Code и создайте файл token-transfer.ts. Вставьте в него код:
//импортируем методы Taquito и файл с данными тестового аккаунта acc.json
import { TezosToolkit } from '@taquito/taquito'
import { InMemorySigner } from '@taquito/signer'
const acc = require('./acc.json')
export class token_transfer {
// настраиваем ссылку на публичный узел тестовой сети
private tezos: TezosToolkit
rpcUrl: string
constructor(rpcUrl: string) {
this.tezos = new TezosToolkit(rpcUrl)
this.rpcUrl = rpcUrl
//считываем почту, пароль и мнемоническую фразу, из которой можно получить приватный ключ
this.tezos.setSignerProvider(InMemorySigner.fromFundraiser(acc.email, acc.password, acc.mnemonic.join(' ')))
}
// объявляем метод transfer, который принимает параметры:
//
// 1) contract — адрес контракта;
// 2) sender — адрес отправителя;
// 3) receiver — адрес получателя;
// 4) amount — количество токенов для отправки.
public transfer(contract: string, sender: string, receiver: string, amount: number) {
this.tezos.contract
.at(contract) //обращаемся к контракту по адресу
.then((contract) => {
console.log(`Sending ${amount} from ${sender} to ${receiver}...`)
//обращаемся к точке входа transfer, передаем ей адреса отправителя и получателя, а также количество токенов для отправки.
return contract.methods.transfer(sender, receiver, amount).send()
})
.then((op) => {
console.log(`Awaiting for ${op.hash} to be confirmed...`)
return op.confirmation(1).then(() => op.hash) //ждем одно подтверждение сети
})
.then((hash) => console.log(`Hash: https://florence.tzstats.com/${hash}`)) //получаем хеш операции
.catch((error) => console.log(`Error: ${JSON.stringify(error, null, 2)}`))
}
}
Теперь создайте файл transfer.ts и вставьте в него код:
import { token_transfer } from './token-transfer'
const RPC_URL = 'https://florencenet.api.tez.ie'
const CONTRACT = 'KT1DUdLarKFG9tmA4uZCgbiHv5SJA9oUBw8G' //адрес опубликованного контракта
const SENDER = 'tz1imn4fjJFwmNaiEWnAGdRrRHxzxrBdKafZ' //публичный адрес отправителя — возьмите его из acc.json
const RECEIVER = 'tz1UEQzJbuaGJgwvkekk6HwGwaKvjZ7rr9v4' //публичный адрес получателя — возьмите его из кошелька Tezos, который вы создали
const AMOUNT = 3 //количество токенов для отправки. Можете ввести другое число
new token_transfer(RPC_URL).transfer(CONTRACT, SENDER, RECEIVER, AMOUNT)
Откройте консоль и выполните команду npx ts-node transfer.ts. Подождите, пока консоль вернет хеш операции.
Теперь откройте кошелек Temple Wallet. Нажмите на кнопку Tezos Mainnet в верхней части окна и выберите пункт Florence Testnet в выпадающем списке.
Кошелек выглядит пустым, но на самом деле токены уже пришли на ваш адрес. Temple не видит токены, потому что в коде контракта нет метаданных с названием и другими параметрами. Метаданные — тема следующего урока. Но сейчас мы можем добавить их вручную и увидеть токены на кошельке.
Скопируйте адрес контракта токена из файла transfer.ts. Откройте Temple Wallet и нажмите кнопку Manage справа от поля Search Assets, а затем — кнопку Add Token.
Temple откроет браузер с вкладкой, на которой можно добавить токен вручную. Выберите стандарт FA 1.2 в выпадающем списке Token type, а затем вставьте адрес смарт-контракта в поле Address.
Заполните данные о токене: тикер, описание, число знаков после запятой и ссылку на логотип. Пишите, что нравится, но оставьте значение «0» в поле Decimals. Нажмите кнопку Add Token, чтобы добавить в кошелек токен с заданными настройками.
Готово! Теперь Temple Wallet отображает ваш токен. Он подхватил точки входа FA 1.2, а значит вы можете просматривать баланс и отправлять токены из интерфейса кошелька. Однако получателю придется вручную настраивать метаданные, чтобы увидеть перевод.
На следующем уроке мы разберемся с метаданными и опубликуем полноценный токен, который кошельки будут видеть сразу. А еще выпустим NFT стандарта FA 2.
Подбиваем итоги
Токены — это записи в хранилище смарт-контрактов вроде «Адресу TZ1 принадлежит 1000 монет». Передача токенов происходит так:
- Пользователь или приложение вызывает смарт-контракт.
- Смарт-контракт проверяет, может ли пользователь отправить токены.
- Смарт-контракт обновляет записи о балансах пользователей: уменьшает количество токенов отправителя на сумму перевода, а затем добавляет ее к балансу получателя.
Разработчики протокола создают стандарты, которые описывают основные функции токенов: хранение записей о пользователях, проверку балансов и проведение транзакций. В Tezos два популярных стандарта: FA 1.2 для взаимозаменяемых активов и FA 2 для NFT.
Разработчики могут писать смарт-контракты токенов с нуля или использовать имплементации — шаблоны с базовыми функциями. Продвинутые механизмы вроде выпуска и сжигания токенов нужно создавать самостоятельно.