По нашим предыдущим статьям может сложиться впечатление, что писать и деплоить смарт-контракты для асинхронных сетей на Threaded Virtual Machine (TVM), таких как Everscale и Venom, сложно и долго. Код смарт-контракта необходимо преобразовывать в файл с расширением .boc, в котором будет лежать код, приведенный к типу древа ячеек, с которым работает TVM. Кроме того, компилятор создает .abi файл, описывающий интерфейс контракта, его переменные, функции их параметры и возвращаемые ими типы. Этот файл используется для дальнейшей типизации для Typescript. Однако, благодаря инструментам, созданным в помощь разработчикам, процесс теста и деплоя смарт-контрактов по большей части автоматизирован.

В этой статье мы начнем новый проект – напишем коллекционную карточную игру полностью на смарт-контрактах Everscale. В первой статье мы опишем процесс подготовки нового проекта, напишем о минимально необходимых зависимостях, об особенностях процесса деплоя контракта в сеть и задеплоим нашу NFT коллекцию с игровыми картами.

Подготовка

Для написания и деплоя смарт-контрактов нам пригодятся следующие инструменты:

  • node.js – все взаимодействие с блокчейнами Everscale и Venom можно осуществлять через библиотеки, SDK и фреймворки на Javascript и Typescript;

  • Опционально: IntelliJ Tsol Plugin или расширение для VS Code Everscale (TON) Solidity (tsol) – подсветка синтаксиса при написании смарт-контрактов на языке Threaded Solidity в файлах с расширением .tsol. Threaded Solidity – адаптация Solidity для работы с TVM: язык предполагает написание асинхронно взаимодействующих смарт-контрактов с вызовами инструкций TVM;

  • Фреймворк для тестирования и деплоя смарт-контрактов – Locklift. Этот фреймворк может эмулировать сеть Everscale в вашем проекте, что дает возможность опробовать процесс деплоя без поднятия локальной ноды;

    • Конфигурирование Locklift осуществляется в отдельном файле – locklift.config.ts, куда подтягиваются ключи от контрактов-гиверов и ключи от адреса, инициирующего деплой контракта.

    • Рекомендуем обращаться к документации Locklift по ходу статьи.

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

    • Установите расширение Ever Wallet для Chrome;

    • Далее необходимо зарегистрироваться и создать проект на https://evercloud.dev/ – после создания проекта вас перенаправит в дэшборд со списком эндпоинтов и краном для получения тестнет токенов;

    • Укажите адрес своего кошелька для получения токенов тестнета из крана. В UI расширении переключитесь на тестнет, на вашем балансе должно быть 100 EVER. Для активации адреса в сети, отправьте какое-то количество EVER на свой же адрес. 

Мы зачастую будем описывать вещи, свойственные именно языку Threaded Solidity, поэтому прилагаем для вас ссылки на документацию API компилятора и нашу статью о написании .tsol контрактов.

Теперь мы готовы приступать к созданию проекта и его конфигурированию.

Подготовка проекта

Проект лежит в открытом репозитории на Github:

git clone https://github.com/FairyFromAlfeya/ever-gwent-contracts.git

Вы можете идти по статье и сверяться с тем, что лежит в репозитории или создать проект с нуля.

Переходим к созданию проекта.

Устанавливаем Locklift:

npm i -g locklift

Создаем проект Locklift:

mkdir gwent-contracts
cd gwent-contracts
npx locklift init -f

Для удобства пропишем в package.json пару нужных скриптов:

"scripts": {
    "format": "prettier --write \"{scripts,test,deploy}/**/*.ts\"",
    "lint": "eslint \"{scripts,test,deploy}/**/*.ts\" --fix",

    "build": "locklift build",
    "test": "locklift test --disable-build -n locklift",
    "deploy": "locklift deploy --disable-build -n locklift",
    “verify”: “locklift verify”,

    "set-card-code": "locklift run --disable-build -s scripts/1-set-card-code.ts -n",
    "mint": "locklift run --disable-build -s scripts/0-mint-card.ts -n"
  },

Вызов команд format и lint прогонят скрипты для проверки кода линтером и исправления ошибок. build – билдит .tsol файлы в директории contracts; test – прогоняет все mocha тесты в директории test. deploy – погоняет все деплой скрипты в директории deploy. verify – верификация контрактов в эксплорере Ever Scan.

Зависимости:

"@broxus/ever-contracts": "git://github.com/broxus/ever-contracts"

Подготовленные базовые контракты; аналогичны openzeppelin/contracts. В нашем проекте мы будем наследоваться от абстрактного контракта Ownable, чтобы наш смарт-контракт имел необходимые атрибуты.

"@broxus/tip4": "git://github.com/broxus/tip4"

Готовые контракты с имплементацией NFT стандарта TIP4. Понадобятся нам как база для наших собственных NFT-карт. Мы дополним базовые контракты возможностями обновления через платформу, а также добавим характеристики персонажей в наши карты.

"@broxus/locklift-deploy": "1.1.1"

Плагин для Locklift, который по функционалу похож на плагин hardhat-deploy. С данным плагином вы сможете прописывать скрипты для деплоя контрактов и гибко конфигурировать то, какой скрипт будет взят при проставлении той или иной сети для деплоя.

"@broxus/locklift-verifier": "1.0.5"

Плагин для Locklift для упрощения процесса верификации смарт-контракта. Верифицированный смарт-контракт – контракт с открытым исходным кодом, который может быть скомпилирован любым желающим для проверки исходного кода путем его хеширования.

Зависимости с d.ts файлами – указание типов для Typescript:

"@types/chai": "4.3.6",
"@types/mocha": "10.0.1",
"@types/node": "20.7.0",

Линтеры для Typescript:

"@typescript-eslint/eslint-plugin": "6.7.3",
"@typescript-eslint/parser": "6.7.3"
"eslint": "8.50.0",
"eslint-config-prettier": "9.0.0",
"eslint-plugin-prettier": "5.0.0",
"prettier": "3.0.3"

Библиотека для подгрузки .env файлов с переменными окружения:

"dotenv": "16.3.1"

Библиотека для работы с большими числами bigNumber и prompts для создания CLI в скриптах. Последнее нам понадобится в минте NFT:

"@types/prompts": "2.4.5",
"bignumber.js": "9.1.2"

Версия Locklift и самого Typescript:

"locklift": "^2.8.3",
"typescript": "5.2.2"

Для конфигурации собственной сети Locklift для теста смарт-контрактов, в packaje.json необходимо прописать следующее:

"resolutions": {
    "nekoton-wasm": "npm:nekoton-wasm-locklift@1.20.2"
},

В файлах .eslintrc.yml и .prettierrc лежат конфиги для линтинга и форматирования, а в tsconfig.json – указания для работы Typescript компилятора. 

В .env.template – шаблон для .env файла, который будет подтягиваться Locklift. Необходимо удалить «.template» из названия файла перед тестами/деплоями.

Конфигурационный файл Locklift

Переходим к файлу locklift.config.ts. При инициализации проекта файл создается автоматически. Нам лишь нужно внести небольшие корректировки. Этим и удобен фреймворк Locklift – управление всеми параметрами происходит из одного конфиг-файла.

Давайте пройдемся по изменениям, которые мы внесли в автоматически генерируемый файл locklift.config.ts.

Подключили плагин для деплоев и верификации контрактов:

import { Deployments } from '@broxus/locklift-deploy';
import '@broxus/locklift-verifier';

Импортируем ключи из файла .env и делаем возможным вызов ключей через process.env.CONST_NAME:

import * as dotenv from 'dotenv';

dotenv.config();

Подгружаем плагин Locklift в Chai, чтобы чтобы можно было писать expect для traceTree из locklift.tracing.trace:

import * as chai from 'chai';

chai.use(lockliftChai);

Конфигурируем BigNumber, чтобы экспонента срабатывала с 9 знака:

BigNumber.config({ EXPONENTIAL_AT: 1e9 });

Для корректной работы Typescript импортируем объявленные в ./build/factorySource.ts типы нашего смарт-контракта:

import { FactorySource } from './build/factorySource';

Объявляем глобальную переменную locklift – интерфейс Locklift, в который мы добавим наш контракт в виде свойства deployments:

declare global {
  const locklift: import('locklift').Locklift<FactorySource>;
}

declare module 'locklift' {
  // eslint-disable-next-line @typescript-eslint/ban-ts-comment
  // @ts-ignore
  export interface Locklift {
    deployments: Deployments<FactorySource>;
  }
}

Далее мы будем использовать эту глобальную переменную в скриптах для деплоя контрактов.

Переходим к самому конфигу:

const config: LockliftConfig = {
  compiler: {
    version: '0.62.0',
    externalContractsArtifacts: {
      'node_modules/@broxus/tip4/build': ['Index', 'IndexBasis'],
    },
  },
  linker: { version: '0.15.48' },
  verifier: {
    verifierVersion: 'latest',
    apiKey: process.env.EVERSCAN_API_KEY!,
    secretKey: process.env.EVERSCAN_SECRET_KEY!,
  },

Указываем версию компилятора, а также внешние контракты, которые будут использоваться Locklift в фабрике. Меняем версию линкера. Добавляем информацию о плагине verifier.

Теперь нам нужно настроить сеть для тестирования нашего смарт-контракта. Раньше для тестирования смарт-контрактов поднималась локальная нода в докер-контейнере. Locklift может эмулировать сеть с пустым стейтом непосредственно в оперативной памяти, однако для этого необходимо изменить параметры локального подключения. Мы удалили лишние объекты подключений, пока что они нам не потребуются. Добавляем:

locklift: {
      connection: {
        id: 2,
        group: 'local',
        type: 'proxy',
        data: {} as never,
      },
      giver: {
        address: process.env.LOCAL_GIVER_ADDRESS!,
        key: process.env.LOCAL_GIVER_KEY!,
      },
      keys: {
        phrase: process.env.LOCAL_PHRASE,
        amount: 20,
      },
    },

В этом объекте подключения type: proxy как раз сообщает фреймворку, что мы хотим использовать его собственную сеть для тестирования смарт-контракта.

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

Для этого добавим в наш .env файл эндпоинт тестнет ноды, публичный и приватный ключи из нашего кошелька Ever Wallet, а также сид фразу:

TESTNET_NETWORK_ENDPOINT='https://devnet.evercloud.dev/your-hashed-project/graphql'
TESTNET_GIVER_ADDRESS='your-wallet-address'
TESTNET_GIVER_KEY=your='wallet-private-key'
TESTNET_PHRASE='seed phrase from your wallet'

А также добавим новое подключение в locklift.config.ts:

    testnet: {
      connection: {
        id: 3,
        group: 'dev',
        type: 'graphql',
        data: {
          endpoints: [process.env.DEVNET_NETWORK_ENDPOINT!],
          latencyDetectionInterval: 1000,
          local: false,
        },
      },
      giver: {
        address: process.env.DEVNET_GIVER_ADDRESS!,
        key: process.env.DEVNET_GIVER_KEY!,
      },
      keys: {
        phrase: process.env.DEVNET_PHRASE!,
        amount: 20,
      },
    },

Теперь мы можем переключаться между сетями. Переходим к описанию смарт-контрактов.

Смарт-контракты коллекции

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

Основной контракт GwentCollection.tsol наследуется от абстрактного контракта GwentCollectionBase.tsol, в который импортируются стандартные контракты имплементации TIP4 NFT с возможностью упрощенного апгрейда.

Интерфейс коллекции:

pragma ever-solidity ^0.62.0;

import "@broxus/tip4/contracts/TIP4_1/interfaces/ITIP4_1Collection.tsol";
import "@broxus/tip4/contracts/TIP4_2/interfaces/ITIP4_2JSON_Metadata.tsol";
import "@broxus/tip4/contracts/TIP4_3/interfaces/ITIP4_3Collection.tsol";

import "../structures/IGwentCardParams.tsol";

interface IGwentCollection is
    ITIP4_1Collection,
    ITIP4_2JSON_Metadata,
    ITIP4_3Collection
{
    event CardCodeChanged(uint256 codeHash, uint32 newVersion);

    event PlatformCodeSet(uint256 codeHash);

    function platformCode() external view responsible returns (TvmCell);

    function platformCodeHash() external view responsible returns (uint256);

    function nftCodeVersion() external view responsible returns (uint32);

    function setCardCode(TvmCell _newCardCode, address _remainingGasTo) external;

    function mintCard(
        address _recipient,
        string _cardJson,
        IGwentCardParams.GwentCardParams _cardParams,
        address _remainingGasTo
    ) external;
}

Наша коллекция имплементирует все методы стандарта NFT TIP-4 (4_1, 4_2, 4_3), а также добавляет собственные.

У коллекции, помимо конструктора, прописаны следующие публичные методы:

  • setCardCode() – обновление кода NFT адресом-владельцем коллекции;

  • mintCard() – минт игровой карты (NFT);

  • upgrade() – апгрейд кода коллекции;

  • Для упрощенного обновления кода коллекции, описывается структура со стейтом контракта коллекции в IGwentCollectionUpgradeData.tsol, в который импортируется интерфейс со структурой версий коллекции (IGwentVersions.tsol);

  • onCodeUpgrade() – функция срабатывающая после апгрейда кода контракта. Может принимать любые аргументы. В нашем случае удаляет старый код контракта.

Давайте рассмотрим функцию upgrade() нашей коллекции подробнее:

function upgrade(
        TvmCell _code,
        optional(uint32) _version,
        optional(address) _remainingGasTo
    )
        external
        override
        reserve(_getTargetBalanceInternal())
        onlyOwner
        validTvmCell(_code, GwentErrors.COLLECTION_CODE_IS_EMPTY)
    {
        uint32 currentVersion = _version.hasValue() ? _version.get() : _getCurrentVersionInternal() + 1;
        address remainingGasTo = _remainingGasTo.hasValue() ? _remainingGasTo.get() : msg.sender;

        TvmCell data = abi.encode(
            IGwentCollectionUpgradeData.GwentCollectionUpgradeData({
                versions: IGwentVersions.GwentVersions(currentVersion, _getCurrentVersionInternal()),
                remainingGasTo: remainingGasTo,

                nonce: nonce,
                owner: _getOwnerInternal(),
                totalCardSupply: _totalCardSupply,
                json: _json,
                supportedInterfaces: _supportedInterfaces,
                platformCode: _platformCode,

                cardCode: _cardCode,
                cardCodeVersion: _cardCodeVersion,

                indexCode: _indexCode,
                indexBasisCode: _indexBasisCode
            })
        );

        tvm.setcode(_code);

        tvm.setCurrentCode(_code);

        onCodeUpgrade(data);
    }

Необходимо обязательно передать новый код коллекции в виде ячейки TVM. Внутри функции идет проверка, передали ли мы ячейку с кодом. Опционально передаем версию коллекции и адрес, на который будет отправлен не потраченный газ. Если мы не передаем версию, тогда к текущей версии платформы добавится единица. Если мы не передаем адрес, на который хотим отправить оставшийся от транзакции газ, он уйдет на отправителя сообщения об апгрейде коллекции.

Переданный в ячейке код упаковывается в соответствии со структурой GwentCollectionUpgradeData. Далее новый код записывается в смарт-контракт для следующих вызовов, а затем и для текущего вызова (setCode() и setCurrentCode()). После этого обязательно вызывается функция onCodeUpgrade(), которую мы переписали, чтобы она удаляла предыдущую версию кода.

В конструкторе мы проверяем передаваемые аргументы и возвращаем написанные нами коды ошибок, импортируемые из GwentErrors.tsol.

Абстрактная контракт GwentCollectionBase.tsol описывает свойства и методы, которые будут присутствовать в дочернем контракте основной коллекции.

Начнем с родительских контрактов, от которых наследуется абстрактная коллекция:

  • Ownable – у контракта должен быть определен адрес владельца;

  • Upgradable – код контракта может быть обновлен владельцем;

  • Upgrader – контракт может инициировать обновление других контрактов, в нашем случае контрактов карт (NFT из коллекции);

  • TIP6 – стандарт, описывающий набор интерфейсов контракта; используется для web3 приложений. При наследовании контракта TIP6, приложения сами понимают, могут ли они взаимодействовать с TIP-6 контрактом или нет;

  • IGwentCollection – интерфейс, который наследуется от стандартных интерфейсов NFT: TIP4_1Collection, TIP4_2JSON_Metadata (описывает конструктор NFT, в который передаются метаданные NFT в виде строки с json), TIP4_3Collection. Интерфейс описывает следующие методы:

    • Read only методы platformCode(), platformCodeHash(), nftCodeVersion(), возвращающие код GwentPlatform.tsol в виде ячейки TVM, хеш кода платформы, версию кода карты (NFT) соответственно;

    • setCardCode() – обновление кода NFT;

    • mintCard() – минт NFT. Сюда мы передаем импортируемую структуру с параметрами карты для минта уникального NFT.

Функционал read only методов абстрактной коллекции понятен из названий методов. Рассмотрим один из них подробнее:

    function getJson()
        external
        view
        override
        responsible
        returns (string json)
    {
        return {
            value: 0,
            flag: MsgFlag.REMAINING_GAS,
            bounce: false
        } _json;
    }

external – функция может быть вызвана внешним сообщением;

view  – функция для чтения из контракта;

responsible – исполнение функции обязательно приведет к созданию исходящего сообщения (ответа);

В ответе прописывается заголовок исходящего сообщения, где:

value – количество EVER, приложенных к исходящему сообщению (указываем 0, поскольку далее прописываем флаг);

flag – флаг для автоматического расчета суммы, прикрепляемой к исходящему сообщению в зависимости от условий, заданным во флаге. MsgFlag.REMAINING_GAS является флагом 64, что означает, что к ответному исходящему сообщению будет приложено 0 EVER с баланса отвечающего контракта и весь EVER, который остался не потраченным на доставку сообщения с запросом на исполнение этой функции;

bounce – флаг, который, будучи проставленным как true, приведет к отправке bounce сообщения от принимающего контракта в случае, если исполнение логики, вызванной отправленным сообщением, провалится. Поскольку наша функция только на чтение и отправляет данные вызывающему контракту, что не может стриггерить никакое исполнение логики, мы проставляем этот флаг как false.

Давайте посмотрим, почему у нас есть два стандарта TIP4_1Collection и TIP4_3Collection. TIP-4_3 – стандарт, описывающий создание собственных индексов для NFT коллекций, чтобы облегчить web3 приложениям поиск коллекций или отдельных NFT по адресу владельца. В нашей абстрактной коллекции, методы _deployIndexBasis(), _buildIndexBasisCode(), _buildIndexBasisState() написаны для создания индексов исходного кода и состояния коллекции, а также их деплоя в сеть.

Смарт-контракты карт (NFT)

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

У карт есть менеджер и владелец. Только владелец коллекции может провести деплой карт. Обновление карты может быть вызвано кем угодно, поскольку код карт берется из коллекции, код которой может обновлять только владелец. Менеджер управляет NFT. После передачи функции менеджера другому адресу, владелец не сможет вернуть себе его функционал. Обычно менеджером назначается адрес, осуществивший покупку NFT. В случае игры менеджер нужен для интеграции с другими контрактами: в будущем менеджером будет назначаться контракт колоды, чтобы у игроков не было возможности добавлять карты в колоды, передавать их на другие адреса или продавать.

В абстрактном контракте карты описан метод для чтения для получения характеристик, фракции, способностей карт, описанных в контрактах, лежащих в ./libraries/characteristics. Эти же методы частично определяются в интерфейсе IGwentCard.tsol.

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

Характеристики карты Cirilla Fiona Elen Riannon:

attributes: {
      strength: 15,
      strengthBoosted: 0,
      abilities: ABILITY_TYPE.HERO,
      effects: EFFECT_TYPE.NO_EFFECT,
      rows: ROW_TYPE.MELEE,
      faction: FACTION_TYPE.NEUTRAL,
    },

Возьмем способности (abilities). В GwentAbilityType.tsol лежит библиотека с возможными вариантами способностей:

library GwentAbilityType {
    uint8 constant GWENT_ABILITY_TYPE_HERO = 1;
    uint8 constant GWENT_ABILITY_TYPE_MEDIC = 2;
    uint8 constant GWENT_ABILITY_TYPE_MORALE_BOOST = 4;
    uint8 constant GWENT_ABILITY_TYPE_MASTER = 8;
    uint8 constant GWENT_ABILITY_TYPE_SPY = 16;
    uint8 constant GWENT_ABILITY_TYPE_TIGHT_BOND = 32;
}

В абстрактном контракте карты есть функция для получения кортежа с булевыми значениями: true – карта обладает данной способностью, false – не обладает.

function getAbilities()
        external
        view
        override
        responsible
        returns (
            bool hero,
            bool medic,
            bool moraleBoost,
            bool master,
            bool spy,
            bool tightBond
        )
    {
        bool isHero = (_params.abilities & GwentAbilityType.GWENT_ABILITY_TYPE_HERO) > 0;
        bool isMedic = (_params.abilities & GwentAbilityType.GWENT_ABILITY_TYPE_MEDIC) > 0;
        bool isMoraleBoost = (_params.abilities & GwentAbilityType.GWENT_ABILITY_TYPE_MORALE_BOOST) > 0;
        bool isMaster = (_params.abilities & GwentAbilityType.GWENT_ABILITY_TYPE_MASTER) > 0;
        bool isSpy = (_params.abilities & GwentAbilityType.GWENT_ABILITY_TYPE_SPY) > 0;
        bool isTightBond = (_params.abilities & GwentAbilityType.GWENT_ABILITY_TYPE_TIGHT_BOND) > 0;

        return {
            value: 0,
            flag: MsgFlag.REMAINING_GAS,
            bounce: false
        } (isHero, isMedic, isMoraleBoost, isMaster, isSpy, isTightBond);
    }

Давайте посмотрим, как происходит вычисление:

GWENT_ABILITY_TYPE_HERO = 1, то есть 00000001

abilities: ABILITY_TYPE.HERO = 00000001

Применяем побитовое И (если оба бита равны 1 в ответе на месте этого бита будет единица, иначе 0):

00000001 & 00000001 = 00000001

00000001 = 1

1 > 0

Следовательно в кортеже под нулевым индексом будет true.

Характеристики карты описываются в структуре IGwentCardParams.tsol, которые передаются в IGwentCardExtra.tsol. В этой структуре также лежат уникальные свойства, которые никак не должны влиять на адрес контракта при передачи ее в виде ячейки TVM в конструктор карты.

Смарт-контракт платформы

Обратите внимание на GwentPlatform.tsol. Если платформу попытается использовать смарт-контракт с адресом, отличным от адреса деплоера, контракт платформы уничтожится, а средства, приложенные к сообщению об апгрейде кода платформы вернуться на адрес, переданный в качестве address _remainingGasTo.  В случае если платформа используется адресом деплоера, она обновит свой код на переданный в параметры конструктора и будет готова к передаче в другие функции в качестве аргумента.

Платформа инициализируется внутри функции mintCard() для расчета адреса новой карты:

address card = new GwentPlatform{
            stateInit: state,
            value: GwentGas.MINT_CARD_VALUE,
            flag: MsgFlag.SENDER_PAYS_FEES,
            bounce: false
        }(
            _buildCardCode(),
            _cardCodeVersion,
            extra,
            _remainingGasTo
        );

В конструктор платформы передаем код карты с солью, который рассчитывается функцией _buildCardCode() абстрактной коллекции:

    function _buildCardCode() internal view returns (TvmCell) {
        TvmBuilder salt;

        salt.store(address(this));

        return tvm.setCodeSalt(_cardCode, salt.toCell());
    }

Далее, платформа упаковывает переданные в конструктор данные и обновляет свой код, превращая его в код новой карты:

function _upgrade(
        TvmCell _code,
        uint32 _version,
        TvmCell _extra,
        address _remainingGasTo
    ) internal {
        tvm.rawReserve(GwentGas.GWENT_TARGET_BALANCE, 0);

        TvmCell data = abi.encode(
            IGwentPlatformUpgradeData.GwentPlatformUpgradeData({
                versions: IGwentVersions.GwentVersions(_version, uint32(0)),
                deployer: _deployer,
                platformCode: tvm.code(),
                params: _params,
                extra: _extra,
                remainingGasTo: _remainingGasTo
            })
        );
        // в рамках одной транзакции мы обновляем код контракта для следующих вызовов, затем меняем код контракта для текущего вызова, что вызывает срабатывание функции onCodeUpgrade().
        tvm.setcode(_code);
        tvm.setCurrentCode(_code);

        onCodeUpgrade(data);
    }

Также через платформу мы можем обновлять код карт. При вызове функции upgrade() у контракта карты срабатывает функция onCodeUpgrade(), которая приводит к выполнению условия:

if (versions.previous == 0) {
            _upgradeFromPlatform(_data);
        }

Поскольку только у платформы предыдущая версия – 0. Соответственно вызывается _upgradeFromPlatform(), в которую передается data, переданная в функцию upgrade().

Скрипты для деплоя

Нам необходимо задеплоить в сеть несколько смарт-контрактов. В директории deploy лежат скрипты, которые будут исполнены фреймворком Locklift по команде test или deploy.

Первый – аккаунт, который будет владельцем нашей NFT коллекции – 0-deploy-owner-wallet.ts:

    type: WalletTypes.WalletV3,
    value: toNano(5),

Деплоим кошелек третьей версии (самая простая реализация аккаунта с балансом) и пересылаем на него 5 EVER.

Вместо того, чтобы прописывать все руками, просто воспользуемся функцией из плагина locklift-deploy:

await locklift.deployments.deployAccounts()

Переходим к скрипту деплоя контракта самой коллекции – 2-deploy-gwent.ts:

Объявляем владельца коллекции, взяв deploymentName: 'OwnerWallet', из деплоя аккаунта владельца:

const owner = locklift.deployments.getAccount('OwnerWallet');

Для удобства воспользуемся функцией из плагина:

await locklift.deployments.deploy()

В нее передаем публичный ключ объявленного владельца коллекции; адрес владельца с указанием того, что на адрес владельца будут отправлены средства, оставшиеся от приложенных к сообщению для инициирования деплоя; сумма EVER, которую владелец оставит на балансе смарт-контракта.

А вот эта строчка запрещает деплой коллекции, пока в сеть не задеплоен аккаунт владельца:

export const dependencies = ['owner-wallet'];

После деплоя коллекции, деплоятся карты.

Проверка наличия коллекции:

export const dependencies = ['gwent-collection'];

Свойства карт берутся из файла ./assets/neutral.cards.ts. 

В скрипте 1-deploy-user-wallet.ts описывается деплой обычного аккаунта, на который может быть переведено право владения картами.

Скрипты в директории scripts

0-mint-card.ts в отличие от  3-deploy-gwent-card.ts используется в качестве скрипта для владельца коллекции, которым он может сминтить карту на любой адрес. 3-deploy-gwent-card.ts сминтит карту на адрес, полученный после деплоя кошелька скриптом 1-deploy-user-wallet.ts.

1-set-card-code.ts отдельный скрипт для обновления кода NFT. Этот скрипт запускается командой ‘yarn mint’.

Отлично! Осталось написать тесты и можно пробовать деплоить контракты во внутреннюю сеть Locklift.

Тесты

В gwent-collection.spec.ts описаны следующие тесты коллекции:

  • Базовые тесты:

    • Проверяем, что на балансе адреса коллекции остается 1 Эвер;

    • Проверяем адрес владельца коллекции;

    • Проверяем, что нигде не был потрачен лишний газ;

    • Проверяем перевод права владения на кошелек, который мы задеплоили скриптом 1-deploy-user-wallet.ts;

    • Проверяем перевод права владения на кошелек владельца коллекции;

  • Тест деплоя карты;

  • Тест обновления версии коллекции с 1 до 2;

  • Проверка обновленного кода;

  • Тест метода чтения величины коллекции;

  • Проверка соответствия хеша коллекции с солью;

  • Проверка хеша индекса с солью;

  • Проверка хеша индекса;

  • Проверка хеша платформы с солью;

  • Проверка версии NFT;

  • Проверка, что IndexBasis для коллекции задеплоен, и в нем верно указан адрес коллекции. Если индекс не был задеплоен в сеть, Ever Scan и Ever Wallet не смогут корректно отобразить коллекцию;

  • Тест метода расчета адреса NFT.

В gwent-card.spec.ts описаны тесты NFT:

  • Тест обновления кода карт до второй версии;

  • Тест метода смены владельца;

  • Тест метода смены менеджера;

  • Тесты метода передачи NFT (transfer());

    • Отмена передачи права на владение NFT владельцем;

    • Передача права на владение NFT адресом пользователя адресу владельца;

    • Проверка изменения индекса контракта;

    • Тест передачи права на владение NFT пользователю;

  • Тесты метода upgrade():

    • Тест обновления кода – версия должна стать равной 2;

  • Проверка обновленных данных;

    • Тест метода getInfo();

    • Тест метода getJson();

    • Проверка хеша кода индекса;

    • Проверка валидности контракта-индекса;

    • Проверка апгрейдера на валидность;

    • Проверка новых характеристик карт на валидность;

Всё, осталось только прогнать тесты и убедиться, что все работает:

npm i -g yarn
yarn
yarn build
yarn test

Мы же уже задеплоили нашу коллекцию в виде коллекции NFT в мейннет, вы можете увидеть ее в эксплорере, карточки будут добавляться со временем:

https://everscan.io/accounts/0:7538ba2a9d957510d5843f03cc0f3b8b35985ba9dd47adcbe3d6458cf945edd4/transactions

Заключение

Итак, сегодня мы сделали первый шаг по созданию игры в сети Everscale. В следующих статьях мы будем описывать саму логику игрового процесса, которая будет записана в смарт-контрактах, и фронтенд приложении для взаимодействия с контрактами игры.

Комментарии (2)


  1. pnaydanovgoo
    06.10.2023 05:42

    Давайте посмотрим, почему у нас есть два стандарта TIP4_1Collection и TIP4_3Collection.

    Ох, тяжело и непривычно ориентироваться по стандартам после опыта с OpenZeppelin))


    1. FairyFromAlfeya
      06.10.2023 05:42

      Основных всего 2: TIP-3, TIP-4

      ознакомиться с ними можно здесь:
      https://docs.everscale.network/standard/TIP-4