Created
September 23, 2021 19:06
-
-
Save pashius/12e01c97a56005d0e1c28b0a95865a29 to your computer and use it in GitHub Desktop.
FA2 token implementation with comments in Ukrainian
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// ERRORS | |
const fa2_token_undefined = "FA2_TOKEN_UNDEFINED" | |
const fa2_insufficient_balance = "FA2_INSUFFICIENT_BALANCE" | |
const fa2_tx_denied = "FA2_TX_DENIED" | |
const fa2_not_owner = "FA2_NOT_OWNER" | |
const fa2_not_operator = "FA2_NOT_OPERATOR" | |
const fa2_operators_not_supported = "FA2_OPERATORS_UNSUPPORTED" | |
const fa2_receiver_hook_failed = "FA2_RECEIVER_HOOK_FAILED" | |
const fa2_sender_hook_failed = "FA2_SENDER_HOOK_FAILED" | |
const fa2_receiver_hook_undefined = "FA2_RECEIVER_HOOK_UNDEFINED" | |
const fa2_sender_hook_undefined = "FA2_SENDER_HOOK_UNDEFINED" | |
// INTERFACE | |
// оголошуємо тип ідентифікатора токена — натуральне число | |
type token_id is nat | |
// оголошуємо типи вхідних параметрів, які приймає функція переказу токена: адресу одержувача, id і кількість токенів. В тип transfer додаємо адресу відправника | |
type transfer_destination is | |
[@layout:comb] | |
record [ | |
to_: address; | |
token_id: token_id; | |
amount: nat; | |
] | |
type transfer is | |
[@layout:comb] | |
record [ | |
from_: address; | |
txs: list (transfer_destination); | |
] | |
// оголошуємо типи для читання балансу: адреса власника, id токена | |
type balance_of_request is | |
[@layout:comb] | |
record [ | |
owner: address; | |
token_id: token_id; | |
] | |
type balance_of_response is | |
[@layout:comb] | |
record [ | |
request: balance_of_request; | |
balance: nat; | |
] | |
type balance_of_param is | |
[@layout:comb] | |
record [ | |
requests: list (balance_of_request); | |
callback: contract (list (balance_of_response)); | |
] | |
// оголошуємо тип оператора — адреси, яка може відправляти токени | |
type operator_param is | |
[@layout:comb] | |
record [ | |
owner: address; | |
operator: address; | |
token_id: token_id; | |
] | |
// оголошуємо тип параметрів, які потрібні для оновлення списку операторів | |
type update_operator is | |
[@layout:comb] | |
| Add_operator of operator_param | |
| Remove_operator of operator_param | |
// оголошуємо тип, який містить метадані NFT: ID токена і посилання на json-файл | |
type token_info is (token_id * map (string, bytes)) | |
type token_metadata is | |
big_map (token_id, token_info) | |
// оголошуємо тип з посиланням на метадані смарт-контракту. Ці дані будуть відображатися в гаманці | |
type metadata is | |
big_map (string, bytes) | |
// оголошуємо тип, який може зберігати записи про кілька токенів і їх метадані в одному контракті | |
type token_metadata_param is | |
[@layout:comb] | |
record [ | |
token_ids: list (token_id); | |
handler: (list (token_metadata)) -> unit; | |
] | |
// оголошуємо псевдо-точки входу: передачу токенів, перевірку балансу, оновлення операторів і перевірку метаданих | |
type fa2_entry_points is | |
| Transfer of list (transfer) | |
| Balance_of of balance_of_param | |
| Update_operators of list (update_operator) | |
| Token_metadata_registry of contract (address) | |
type fa2_token_metadata is | |
| Token_metadata of token_metadata_param | |
// оголошуємо типи даних для зміни дозволів на передачу токенів. Наприклад, з їх допомогою можна зробити токен, який не можна відправити на іншу адресу | |
type operator_transfer_policy is | |
[@layout:comb] | |
| No_transfer | |
| Owner_transfer | |
| Owner_or_operator_transfer | |
type owner_hook_policy is | |
[@layout:comb] | |
| Owner_no_hook | |
| Optional_owner_hook | |
| Required_owner_hook | |
type custom_permission_policy is | |
[@layout:comb] | |
record [ | |
tag: string; | |
config_api: option (address); | |
] | |
type permissions_descriptor is | |
[@layout:comb] | |
record [ | |
operator: operator_transfer_policy; | |
receiver: owner_hook_policy; | |
sender: owner_hook_policy; | |
custom: option (custom_permission_policy); | |
] | |
type transfer_destination_descriptor is | |
[@layout:comb] | |
record [ | |
to_: option (address); | |
token_id: token_id; | |
amount: nat; | |
] | |
type transfer_descriptor is | |
[@layout:comb] | |
record [ | |
from_: option (address); | |
txs: list (transfer_destination_descriptor) | |
] | |
type transfer_descriptor_param is | |
[@layout:comb] | |
record [ | |
batch: list (transfer_descriptor); | |
operator: address; | |
] | |
// OPERATORS | |
// оголошуємо тип, який зберігає записи про операторів в одному big_map | |
type operator_storage is big_map ((address * (address * token_id)), unit) | |
// оголошуємо функцію для оновлення списку операторів | |
function update_operators (const update : update_operator; const storage : operator_storage) | |
: operator_storage is | |
case update of | |
| Add_operator (op) -> | |
Big_map.update ((op.owner, (op.operator, op.token_id)), (Some (unit)), storage) | |
| Remove_operator (op) -> | |
Big_map.remove ((op.owner, (op.operator, op.token_id)), storage) | |
end | |
// оголошуємо функцію, яка перевіряє, чи може користувач оновити список операторів | |
function validate_update_operators_by_owner (const update: update_operator; const updater: address) | |
: unit is block { | |
const op = case update of | |
| Add_operator (op) -> op | |
| Remove_operator (op) -> op | |
end; | |
if (op.owner = updater) then skip else failwith (fa2_not_owner) | |
} with unit | |
// оголошуємо функцію, яка перевіряє, чи може користувач оновити список адрес власників токенів, і тільки в цьому випадку викликає функцію оновлення | |
function fa2_update_operators (const updates: list (update_operator); const storage: operator_storage): operator_storage is block { | |
const updater = Tezos.sender; | |
function process_update (const ops: operator_storage; const update: update_operator) is block { | |
const u = validate_update_operators_by_owner (update, updater); | |
} with update_operators (update, ops) | |
} with List.fold (process_update, updates, storage) | |
type operator_validator is (address * address * token_id * operator_storage) -> unit | |
// оголошуємо функцію, яка перевіряє дозволи на передачу токенів. Якщо користувач не може передати токен, функція припиняє виконання контракту | |
function make_operator_validator (const tx_policy: operator_transfer_policy): operator_validator is block { | |
const x = case tx_policy of | |
| No_transfer -> (failwith (fa2_tx_denied): bool * bool) | |
| Owner_transfer -> (True, False) | |
| Owner_or_operator_transfer -> (True, True) | |
end; | |
const can_owner_tx = x.0; | |
const can_operator_tx = x.1; | |
const inner = function (const owner: address; const operator: address; const token_id: token_id; const ops_storage: operator_storage): unit is | |
if (can_owner_tx and owner = operator) | |
then unit | |
else if not (can_operator_tx) | |
then failwith (fa2_not_owner) | |
else if (Big_map.mem ((owner, (operator, token_id)), ops_storage)) | |
then unit | |
else failwith (fa2_not_operator) | |
} with inner | |
// оголошуємо функцію для передачі токена власником | |
function default_operator_validator (const owner: address; const operator: address; const token_id: token_id; const ops_storage: operator_storage): unit is | |
if (owner = operator) | |
then unit | |
else if Big_map.mem ((owner, (operator, token_id)), ops_storage) | |
then unit | |
else failwith (fa2_not_operator) | |
// оголошуємо функцію, яка збирає всі транзакції одного токена в пакет (batch) | |
function validate_operator (const tx_policy: operator_transfer_policy; const txs: list (transfer); const ops_storage: operator_storage): unit is block { | |
const validator = make_operator_validator (tx_policy); | |
List.iter (function (const tx: transfer) is | |
List.iter (function (const dst: transfer_destination) is | |
validator (tx.from_, Tezos.sender, dst.token_id, ops_storage), | |
tx.txs), | |
txs) | |
} with unit | |
// MAIN | |
// оголошуємо тип даних для зберігання записів про те, на якій адресі знаходяться токени з заданим id | |
type ledger is big_map (token_id, address) | |
// оголошуємо сховище контракту: метадані TZIP-16, реєстр адрес і токенів, список операторів і ончейн-метадані | |
type collection_storage is record [ | |
metadata: big_map (string, bytes); | |
ledger: ledger; | |
operators: operator_storage; | |
token_metadata: token_metadata; | |
] | |
// оголошуємо функцію передачі токена. Вона отримує id токена, адреси відправника та одержувача, а потім перевіряє, чи є у відправника право передати токен | |
function transfer ( | |
const txs: list (transfer); | |
const validate: operator_validator; | |
const ops_storage: operator_storage; | |
const ledger: ledger): ledger is block { | |
// перевірка права відправника передати токен | |
function make_transfer (const l: ledger; const tx: transfer) is | |
List.fold ( | |
function (const ll: ledger; const dst: transfer_destination) is block { | |
const u = validate (tx.from_, Tezos.sender, dst.token_id, ops_storage); | |
} with | |
// перевірка кількості переданих NFT. Маємо на увазі, що контракт випустив тільки один токен з цим id | |
// Якщо користувач хоче передати 0, 0.5, 2 або іншу кількість токенів, функція перериває виконання контракту | |
if (dst.amount = 0n) then | |
ll | |
else if (dst.amount =/= 1n) | |
then (failwith (fa2_insufficient_balance): ledger) | |
else block { | |
const owner = Big_map.find_opt (dst.token_id, ll); | |
} with | |
case owner of | |
Some (o) -> | |
// перевірка, чи є у відправника токен | |
if (o =/= tx.from_) | |
then (failwith (fa2_insufficient_balance): ledger) | |
else Big_map.update (dst.token_id, Some (dst.to_), ll) | |
| None -> (failwith (fa2_token_undefined): ledger) | |
end | |
, | |
tx.txs, | |
l | |
) | |
} with List.fold (make_transfer, txs, ledger) | |
// оголошуємо функцію, яка поверне баланс відправника | |
function get_balance (const p: balance_of_param; const ledger: ledger): operation is block { | |
function to_balance (const r: balance_of_request) is block { | |
const owner = Big_map.find_opt (r.token_id, ledger); | |
} | |
with | |
case owner of | |
None -> (failwith (fa2_token_undefined): record [balance: nat; request: record [owner: address; token_id: nat]]) | |
| Some (o) -> block { | |
const bal = if o = r.owner then 1n else 0n; | |
} with record [request = r; balance = bal] | |
end; | |
const responses = List.map (to_balance, p.requests); | |
} with Tezos.transaction (responses, 0mutez, p.callback) | |
// оголошуємо головну функцію з псевдо-точками входу. Ці псевдо-точки — основа стандарту FA2 | |
function main (const param: fa2_entry_points; const storage: collection_storage): (list (operation) * collection_storage) is | |
case param of | |
| Transfer (txs) -> block { | |
const new_ledger = transfer (txs, default_operator_validator, storage.operators, storage.ledger); | |
const new_storage = storage with record [ledger = new_ledger] | |
} with ((list []: list (operation)), new_storage) | |
| Balance_of (p) -> block { | |
const op = get_balance (p, storage.ledger); | |
} with (list [op], storage) | |
| Update_operators (updates) -> block { | |
const new_operators = fa2_update_operators (updates, storage.operators); | |
const new_storage = storage with record [operators = new_operators]; | |
} with ((list []: list (operation)), new_storage) | |
| Token_metadata_registry (callback) -> block { | |
const callback_op = Tezos.transaction (Tezos.self_address, 0mutez, callback); | |
} with (list [callback_op], storage) | |
end |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment