Створення токена стандарту 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, яка пов'язує публічні адреси і баланси користувачів *)
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;


(* Оголошуємо псевдоніми вхідних параметрів для кожної базової функції 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

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

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;

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)

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)

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)


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)


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. Компілятор видасть готовий код під полем редактора.

1

Створіть в папці token файл token.json і вставте в нього JSON-код.

2

На минулому уроці ми вписали в скрипт 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 і перевірте стан сховища контракту. У нас в ньому записана тисяча токенів, у вас — ваша кількість.

3

В полі 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 в випадаючому списку.

5

Гаманець виглядає порожнім, але насправді токени вже прийшли на вашу адресу. Temple не бачить токени, тому що в коді контракту немає метаданих з назвою та іншими параметрами. Метадані — тема наступного уроку. Але зараз ми можемо додати їх вручну і побачити токени на гаманці.

Скопіюйте адресу контракту токена з файлу transfer.ts. Відкрийте Temple Wallet і натисніть кнопку Manage праворуч від поля Search Assets, а потім — кнопку Add Token.

6

Temple відкриє браузер з вкладкою, на якій можна додати токен вручну. Виберіть стандарт FA 1.2 в випадаючому списку Token type, а потім вставте адресу смарт-контракту в поле Address.

7

Заповніть дані про токені: тікер, опис, число знаків після коми і посилання на логотип. Пишіть, що подобається, але залиште значення «0» в полі Decimals. Натисніть кнопку Add Token, щоб додати в гаманець токен з заданими настройками.

8

Готово! Тепер Temple Wallet відображає ваш токен. Він підхопив точки входу FA 1.2, а значить ви можете переглядати баланс і відправляти токени з інтерфейсу гаманця. Однак одержувачу доведеться вручну налаштовувати метадані, щоб побачити переказ.

9

На наступному уроці ми розберемося з метаданими і опублікуємо повноцінний токен, який гаманці будуть бачити відразу. А ще випустимо NFT стандарту FA 2.

Підбиваємо підсумки

Токени — це записи в сховищі смарт-контрактів на кшталт «Адресу TZ1 належить тисяча монет». Переказ токенів відбувається так:

  1. Користувач або додаток викликає смарт-контракт.
  2. Смарт-контракт перевіряє, чи може користувач відправити токени.
  3. Смарт-контракт оновлює записи про баланси користувачів: зменшує кількість токенів відправника на суму переказу, а потім додає її до балансу одержувача.

Розробники протоколу створюють стандарти, які описують основні функції токенів: зберігання записів про користувачів, перевірку балансів і проведення транзакцій. У Tezos два популярних стандарта: FA 1.2 для взаємозамінних активів і FA 2 для NFT.

Розробники можуть писати смарт-контракти токенів з нуля або використовувати імплементації — шаблони з базовими функціями. Просунуті механізми на кшталт випуску і спалювання токенів потрібно створювати самостійно.

  • Автор — Павло Скоропляс
  • Продюсер — Світлана Коваль
  • Стилі — Дмитро Бойко
  • Ілюстрації — Кшиштоф Шпак
  • Верстка — Зара Аракелян
  • Розробка — Олександр Пупко
  • Керівник — Влад Ліхута