Створення токена стандарту 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. Компілятор видасть готовий код під полем редактора.
Створіть в папці 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 належить тисяча монет». Переказ токенів відбувається так:
- Користувач або додаток викликає смарт-контракт.
- Смарт-контракт перевіряє, чи може користувач відправити токени.
- Смарт-контракт оновлює записи про баланси користувачів: зменшує кількість токенів відправника на суму переказу, а потім додає її до балансу одержувача.
Розробники протоколу створюють стандарти, які описують основні функції токенів: зберігання записів про користувачів, перевірку балансів і проведення транзакцій. У Tezos два популярних стандарта: FA 1.2 для взаємозамінних активів і FA 2 для NFT.
Розробники можуть писати смарт-контракти токенів з нуля або використовувати імплементації — шаблони з базовими функціями. Просунуті механізми на кшталт випуску і спалювання токенів потрібно створювати самостійно.