Реализация невзаимозаменяемого токена, написанного с помощью Ink

ink !: язык смарт-контрактов Substrate

Блокчейн-фреймворк Parity Substrate, на котором построен Polkadot, находится в активной разработке и быстро продвигается к финальной версии. Ink (или ink!, как это название обычно называют в документации) - это решение Parity для написания смарт-контрактов для блокчейна на основе Substrate.

Как и Substrate, Ink построен на основе Rust и, следовательно, соответствует правилам и синтаксису языка Rust. В этом выступлении будет рассмотрен пример смарт-контракта, воспроизводящего невзаимозаменяемый токен, обычно называемый токенами ERC721 в блокчейне Ethereum. Сам контракт можно найти на Github здесь.

Это первая часть серии, которая будет охватывать процесс создания и развертывания этого смарт-контракта Ink, в частности, мы рассмотрим:

  • Как установить субстрат и чернила с их зависимостями
  • Написание незаменимого контракта с невзаимозаменяемыми токенами, который будет поддерживать 3 основные функции: чеканка токенов, передача токенов и утверждение другой учетной записи для отправки токенов от вашего имени.
  • Как создать и развернуть смарт-контракт на блокчейне Substrate, а также протестировать функции в цепочке с помощью приложения Polkadot JS

Примечание. Приложение Polkadot JS было разработано для управления не только самим Polkadot, но и любой цепочкой субстратов. Интерфейс является динамическим в том смысле, что доступные параметры управления зависят от который поддерживает цепочку подложек.

В этой статье мы рассмотрим процесс установки Ink и его необходимые зависимости, а также представим пример контракта с невзаимозаменяемым токеном, сопровождающий эту серию, поговорим о структуре контракта Ink и его отличиях от контракта на основе Solidity. как сходство между ними.

Примечание о невзаимозаменяемых токенах

Невзаимозаменяемые токены, или NFT, отличаются от токенов в стиле ERC20, при этом каждый токен уникален. В нашем контракте каждый токен будет иметь уникальный идентификатор, используемый для представления токена. Отсюда каждый токен может иметь собственное значение, собственные метаданные или иметь конкретное использование в приложении или системе ценностей.

Утверждение учетных записей, не являющихся владельцами, для передачи или управления токеном также отличается и должно выполняться для каждого токена с использованием невзаимозаменяемых токенов. Cryptokitties - самый известный пример реализации невзаимозаменяемых токенов - каждый токен представляет котенка на платформе.

NFT представляют различные проблемы для стандартного токена и, следовательно, дают нам больше возможностей для изучения синтаксиса и возможностей Ink. Теперь давайте установим Substrate, Ink и необходимые пакеты для написания нашего смарт-контракта.

Установка

Чтобы начать писать контракты Ink, необходимо несколько пакетов. После их установки мы можем запустить цепочку субстратов в окне терминала - мы будем использовать эту цепочку как средство развертывания нашего смарт-контракта.

Ржавчина

Субстрат и Чернила основаны на Ржавчине. Установите Rust с помощью следующих команд или перейдите на страницу установки. Две другие команды включены ниже, чтобы убедиться, что ваша nightly версия Rust (последняя доступная версия Rust) актуальна, а также поддержка сборки WebAssembly для nightly Rust: контракты Ink компилируются в файлы .wasm и, следовательно, развертываются в цепочки субстратов как WebAssembly .

# install rust and update environment
curl https://sh.rustup.rs -sSf | sh
source ~/.cargo/env

# run rust updates and add WebAssembly target support
rustup update nightly
rustup target add wasm32-unknown-unknown --toolchain nightly

Примечание. Cargo - это менеджер пакетов Rust, пакеты которого называются ящиками. Общедоступный реестр грузов можно просмотреть по адресу https://crates.io.

Примечание 2: в последней команде цель относится к целевой папке в каталоге вашего проекта Rust, которая содержит сборки ваших программ. Здесь будут создаваться файлы наших смарт-контрактов.

Субстрат

Установите Substrate с помощью следующей команды:

curl https://getsubstrate.io -sSf | bash

Утилиты wasm

Смарт-контракты Ink компилируются в WebAssembly перед загрузкой в ​​цепочку Substrate. Для этого нам потребуется установить некоторые утилиты:

# We will install:
#
# Binaryen
# Compiler infrastructure and toolchain library for WebAssembly
#
# Wabt
# The WebAssembly Binary Toolkit
#
# Parity Wasm Utils
# Parity specific WebAssemply utilities */

# MacOS
brew install binaryen
brew install wabt
cargo install pwasm-utils-cli --bin wasm-prune
# Ubuntu
apt install binaryen
apt install wabt
cargo install pwasm-utils-cli --bin wasm-prune

Чернила

Отсюда мы можем установить Ink через ящик. Для этого выполните следующее:

cargo install --force --git https://github.com/paritytech/ink cargo-contract

Запуск локальной цепочки субстратов

Наш смарт-контракт будет протестирован на локальной цепочке блоков Substrate, запущенной на вашем компьютере. После установки Substrate выполните следующее для инициализации цепочки:

substrate --dev

Флаг —-dev инициализирует для вас блокчейн разработки, локальный только для вашей машины, и включает некоторые учетные записи пользователей с самого начала.

Эта установка предназначена исключительно для целей тестирования; вы можете думать об этом как об эквиваленте программы Truffle’s Ganache, которая запускает локальную цепочку на основе Ethereum, настроенную для целей тестирования.

Теперь вы заметите, что узел работает с проверяемыми новыми блоками. Нажатие CTRL+C или закрытие окна терминала остановит выполнение узла, хотя состояние цепочки сохраняется до следующего запуска цепочки. В этой заметке, если вы хотите запустить новую цепочку, выполните следующую команду, чтобы очистить цепочку:

substrate purge-chain --dev

Когда наша цепочка запущена и чернила готовы к использованию, мы можем приступить к кодированию контракта. Прежде чем сделать это, также стоит упомянуть о поддержке редактора. Я лично считаю Visual Studio Code лучшим редактором для работы не только на Ink, но и на Rust в целом. Чтобы быстро настроить:

  • Установите VS Code отсюда, если у вас еще не установлен редактор
  • Установите расширение RLS (Rust Language Support) и расширения WebAssembly. Это можно сделать прямо в редакторе на вкладке Расширения или нажать Shift+CMD+X, чтобы перейти прямо к нему.

Настройка Ink Project

Установив все зависимости, приступим к настройке проекта.

Самый простой способ сделать это в настоящее время - установить контракт Ink Hello World под названием Flipper. Установив Flipper, мы можем опираться на то, что уже включено, и не беспокоиться о настройке и компиляции скриптов - они предусмотрены в Flipper.

Примечание. Как субстрат, так и чернила находятся в быстрой разработке и еще не завершены, поэтому среда смарт-контракта и сам код смарт-контракта, скорее всего, изменятся по мере приближения Parity к финальный выпуск фреймворка.

Чтобы быстро запустить наш проект Ink, загрузите Flipper с помощью Cargo:

# fetch the Flipper Ink contract
cargo contract new flipper

Flipper предоставляет нам шаблон проекта, необходимый для начала написания смарт-контракта. Включено:

  • Структура папок и метаданные конфигурации проекта
  • Простой контракт Flipper в src/lib.rs, который просто «переворачивает» логическое значение между true и false с помощью метода flip() и получает это значение в цепочке с помощью метода get(). Мы заменим этот файл контрактом NFT.
  • Специальный Cargo.toml файл Rust, описывающий зависимости проекта и метаданные модуля, .gitignore файл и build.sh файл. Файл build.sh - это то, что мы запускаем для компиляции нашего смарт-контракта, в результате чего получается скомпилированный файл .wasm контракта, абстракция контракта JSON и многое другое. Мы рассмотрим построенный контракт ниже.

Примечание. Сейчас самое время проверить src/lib.rs, чтобы почувствовать синтаксис контракта.

Давайте изменим имя flipper на более подходящее: nftoken. Измените следующее:

  • /flipper имя папки на /nftoken
  • Cargo.toml: изменить [package] name и [lib] name на nftoken
  • build.sh: изменить PROJNAME=nftoken

Также убедитесь, что у нас есть разрешения на запуск nftoken/build.sh:

cd nftoken
chmod +x build.sh

Наконец, добавьте папку /nftoken в рабочую область VS Code, и мы готовы начать писать.

О чернилах

Ink имеет несколько уровней абстракции, где более высокие уровни абстрагируются над более низкими уровнями. Мы будем использовать самый высокий уровень, который называется уровнем языка, или lang уровнем. Эти уровни также были разделены на модули, которые можно изучить здесь.

Под модулем lang находятся модули model и core, которые ориентированы на абстракции среднего уровня и основные утилиты соответственно. Под модулем core мы также можем ожидать интерфейс командной строки, специально предназначенный для создания контрактов Ink и управления ими.

Хотя на момент написания статьи о том, как использовать эти модули, мало что известно, у нас действительно есть необработанные документы API для просмотра, как для модуля core, так и для модуля model. Если вы читаете эту статью, эти документы можно просмотреть сейчас, хотя в нашем контракте ниже будут использоваться некоторые из этих API, предназначенных для демонстрации того, как они используются в контексте уровня lang через контракт невзаимозаменяемых токенов.

Имея это в виду, давайте теперь посмотрим, как выглядит структура нашего lang производного контракта, и сравним ее с тем, что мы ожидаем от смарт-контракта на основе Solidity.

Напоминание: заполненный контракт можно найти здесь на Github.

Структура контракта

Структурирование контракта Ink аналогично контракту Solidity, где основные компоненты, которые мы ожидаем от Solidity, также согласованы в Ink: переменные контракта, события, публичные функции и частные функции, а также переменные среды для захвата вызывающего. адрес и многое другое.

Ниже представлена ​​абстракция структуры NFToken контракта:

// declare modules
use parity::<module>
...
//wrap entire contract inside the contract! macro
contract! {
   // contract variables as a struct
   struct NFToken {
      owner: storage::Value<AccountId>,
      ...
   }
   // compulsory deploy method that is run upon the initial contract instantiation
   impl Deploy for NFToken {
      fn deploy(&mut self, init_value: u64){}
   }
   // define events
   event EventMint { owner: AccountId, value: u64 } 
   ...
   // public contract methods in an impl{} block
   impl NFToken {
      pub(external) fn total_minted(&self) -> u64 {}
      ...
   }
   // private contract methods in a separate impl{} block
   imp NFToken {
      fn is_token_owner(
         &self, 
         of: &AccountId, 
         token_id: u64) -> bool {}
       ...
   }
}
// test functions
mod tests {
   fn it_works() {}
   ...
}

Давайте кратко рассмотрим эти разделы и узнаем, чем они отличаются от того, что мы ожидаем от контракта на Solidity. Ink построен на Rust, поэтому весь синтаксис здесь является допустимым синтаксисом Rust.

  • В разделе объявления модуля мы привносим в контракт внешние функции, и он аналогичен по своему характеру объявлениям using в Solidity.
// Ink
use ink_core::{
    env::{self, AccountId},
    storage,
};
use ink_lang::contract;

// Solidity
interface ContractName {
   using SafeMath for uint256;
   using AddressUtils for address;
}
  • События объявляются внутри макроса !contract, тогда как с Solidity мы определяем наши события в интерфейсе контракта, вводя каждое как event:
// Ink
event Transfer { from: AccountId, to: AccountId, token_id: u64 }
// Solidity
event Transfer(
   address indexed from,
   address indexed to,
   uint256 indexed _tokenId
);
  • Если контракт Solidity встроен в блок interface, контракт Ink встроен в макрос contract!. Наши события объявляются внутри этого макроса, тогда как события объявляются в интерфейсе Solidity. Это описано ниже.

Примечание. Макрос в Rust - это объявление, представляющее блок синтаксиса, которым будут окружены обернутые выражения. Макросы абстрактны на синтаксическом уровне, поэтому макрос contract! добавляет в свое содержимое дополнительный синтаксис.

// Ink
contract! {
   // contract
}
// Solidity
interface ContractName {
   // contract
}
  • В Ink переменные контракта записываются в структуру имени контракта. Карты хеширования, производные от типа HashMap в Rust, заменяют тип mapping в Solidity, обеспечивая key => value списков.

Как Substrate хранит ценности

Любая часть данных, хранящаяся в цепочке субстратов, называется внешними, и Ink предоставляет нам средства для хранения внешних данных в цепочке через модуль storage, который находится в модуле core языка. Другими словами, все переменные контракта, которые вы планируете сохранить в цепочке, будут использовать тип из storage. И наоборот, модуль memory также доступен для структур данных для работы с памятью.

Solidity, с другой стороны, использует другой подход к этому. Начиная с версии Solidity 0.5 ссылочные типы storage и memory были введены в аргументы функции или переменные функции, поэтому контракт знает, где ссылаться на эти переменные. Однако это не обязательно для переменных контракта в Solidity.

Примитивные типы также доступны и согласованы на обоих языках; где Rust использует u64, Solidity принимает более подробное объявление типа uint64. В целом довольно просто добиться одинаковой структуры переменных контракта между двумя языками.

// Ink
struct NFToken {
   owner: storage::Value<AccountId>,
   approvals: storage::HashMap<u64, AccountId>,
}

// Solidity
address owner;
mapping (uint64 => address) approvals;

В приведенном выше примере тип значений, обрабатываемых storage объектами, передается через угловые скобки вместо общих типов.

  • Концепция функции инициализации присутствует как в Ink, так и в Solidity, хотя и реализована по-разному. Используя Ink, мы явно определяем метод deploy() в блоке реализации Deploy{}. Параметры, которые мы определяем для этого метода, представляют, какие данные мы отправляем при инициализации контракта. Например. для нашего невзаимозаменяемого токена мы предоставим начальное количество токенов для чеканки:
// Inks initialisation function, deploy()
impl Deploy for NFToken {
   fn deploy(&mut self, init_value: u64) {
      ...
   }
 }
  • Общедоступные и частные методы также определены в блоках impl, где открытые методы явно определены с помощью pub(external). Опять же, при сравнении этого синтаксиса с синтаксисом Solidity, internal и external используются для определения частного или общедоступного ресурса.

Примечание. В Rust функции, модули, трейты и структуры по умолчанию являются закрытыми и должны быть определены с помощью ключевого слова pub, чтобы они были доступны извне. Расширение (external) для pub зависит от рукописного ввода и обязательно для включения в общедоступные функции рукописного ввода.

// public functions
impl NFToken {
   pub(external) fn total_minted(&self) -> u64 {}
}
// private functions
impl NFToken {
   fn mint_impl(
      &mut self, 
      receiver: AccountId, 
      value: u64) -> bool { 
    }
}

Опять же, мы разделили наши частные и общедоступные функции на отдельные impl блоки и включили pub(external) для определенных общедоступных функций.

  • В качестве последнего строительного блока нашего контракта определен модуль tests, который устанавливает различные условия при тестировании наших функций. В модуле tests мы можем протестировать логику нашего контракта без необходимости компилировать и развертывать ее в цепочке субстратов, что позволяет быстро устранять ошибки и проверять, работает ли контракт должным образом.

Следующий

Теперь мы обсудили строительные блоки смарт-контракта Ink. В следующей части этой серии мы более подробно рассмотрим функции контракта невзаимозаменяемых токенов, разберемся с используемыми шаблонами проектирования Rust и как они связаны с логикой нашего контракта. Затем мы рассмотрим процесс создания и развертывания смарт-контракта в локальной цепочке субстратов и протестируем его функции с помощью Polkadot JS.

Читайте следующую часть этой серии здесь: