- Солидность, контракты и IPFS
- Оглавление
- Напоминание о смарт-контрактах
- Solidity OOP для смарт-контрактов
- Среда разработки
- Ganache-CLI
- VSCode/Remix
- Трюфель
- Шутка/Чай
- Метамаска
- Наш первый смарт-контракт ERC721
- Токен-контракт
- Контрактная основа и стандарт ERC721
- Реализация контракта НФТ с IPFS
- Ценовая лента для получения цены в режиме реального времени
- Изготовление минтера с ограничениями
- Связывание шахты с ценовой лентой
- Создание фабрики для развертывания
- Развертывание testnet и тестирование с помощью Remix
- Управление ресурсами с помощью IPFS
- Что такое IPFS?
- Как это работает?
- Роль IPFS при НФТ
- Загрузка метаданных в IPFS
- Цель главы
Солидность, контракты и IPFS
В этой главе мы рассмотрим язык Solidity, позволяющий писать смарт-контракты на блокчейне ETH и, в более широком смысле, на всех блокчейнах, совместимых с EVM.
Мы также увидим реализацию dev-среды, затем реализацию контракта с Remix, затем мы подойдем к использованию IPFS для децентрализации ресурсов, на которые направлены наши токены.
Оглавление
- Напоминание о смарт-контрактах
- Solidity OOP для смарт-контракта
- Среда разработки
- Ganache-CLI
- VSCode/Remix
- Трюфель
- Шутка/Чай
- Метамаска
- Среда разработки
- Наш первый смарт-контракт ERC721
- Токен-контракт
- Контрактная основа и стандарт ERC721
- Выполнение контракта НФТ с IPFS
- Ценовая лента Chainlink для ценообразования в режиме реального времени
- Создание минтера с ограничениями
- Связывание шахты с ценовой лентой
- Создание фабрики для развертывания
- Развертывание testnet и тестирование с помощью Remix
- Токен-контракт
- Управление ресурсами с помощью IPFS
- Что такое IPFS?
- Как это работает?
- Роль IPFS при НФТ
- Загрузка метаданных в IPFS
- Цель главы
Напоминание о смарт-контрактах
Смарт-контракт — это программа, которая управляет цифровыми активами. Смарт-контракт преобразует соглашение между двумя сторонами в компьютерный код
Он замораживает правила этого соглашения между несколькими сторонами в блокчейне, обеспечивая при этом передачу актива, подобно традиционным юридическим контрактам, при выполнении условий, определенных контрактом
Этот тип контракта может применяться в нескольких областях:
- Страхование
- Недвижимость
- Цепочки поставок (UPS, почта…)
- Финансы
Умные контракты не только автоматизируют соглашения, но и ограничивают их в действиях.
Этот вид контрактов развивается в тех областях, где требуется соблюдение норм.
С помощью смарт-контракта можно «создать все», как на таком языке, как C#, но при использовании функции кода учитывается стоимость исполнения.
Таким образом, необходимо будет максимально оптимизировать его код, чтобы свести к минимуму транзакционные издержки, когда пользователь будет обращаться к вашему контракту.
Solidity OOP для смарт-контрактов
Solidity — это высокоуровневый объектно-ориентированный язык программирования, используемый для реализации смарт-контрактов на различных блокчейнах, в частности на Ethereum.
Это язык статических типов, предназначенный для компиляции кода Solidity в байткод, чтобы контракты выполнялись на EVM.
Solidity доступна на блокчейнах:
- Ethereum
- Мята
- Tron
- Интеллектуальная цепочка Binance
Среда разработки
Блокчейн может показаться сложным и требует очень полной среды разработки, в нашем случае нам понадобится несколько программ/библиотек/IDE:
- Ganache -> Позволяет локально моделировать узел ethereum
- VSCode/Remix -> Локальная IDE и онлайн IDE
- Truffle -> Среда разработки, тестовая среда и конвейер активов (js, css…).
- Jest/Chai -> Библиотека тестов и утверждений для тестирования нашего контракта или DAPP
- Metamask -> Криптовалютный кошелек для тестирования нашего локального блокчейна
Ganache-CLI
Ганаш, или крем-ганаш, … (ладно, я шучу)
Ganache — это инструмент, используемый для имитации локального блокчейна для быстрой разработки контрактов на блокчейне Ethereum:
- Github Ganache
- Документация по ганашу
Он используется на протяжении всего цикла разработки, позволяет разрабатывать, развертывать и тестировать приложение в безопасной и детерминированной среде.
Мы будем использовать его в третьей части этой статьи для развертывания нашего контракта на testnet без Remix.
VSCode/Remix
Для того чтобы легко разработать контракт, рекомендуется сначала сделать это в Remix, а затем добавить его в окончательный проект в VScode.
Для использования VSCode я советую вам установить расширение с синтаксисом Solidity в файлы .sol
-> Solidity extension.
Remix — это онлайн IDE, которая позволяет разрабатывать, развертывать и администрировать смарт-контракт для блокчейн Ethereum.
Remix включает компилятор сценариев Solidity и тестовую сеть для развертывания контракта.
Remix также предоставляет доступ к интерфейсу, раскрывающему функции контракта, для тестирования этих функций.
Его можно использовать как платформу для обучения, нужно только найти на блокчейне контракт, который вы хотите понять, например:
Контракт, используемый для генерации этого NFT Fade, легко найти по его адресу
используя Blockscan, вы можете легко найти контракт, используемый для Fade
(Ссылка на контракт).
Таким образом, скопировав Fade.sol
в Remix, я смогу получить доступ ко всем функциям контракта локально.
Таким образом, мы можем понять, что происходит за функцией tokenURI
, которая каждый раз возвращает другой градиент.
Трюфель
Truffle — это среда разработки для Ethereum, цель которой — сделать обычно сложную разработку блокчейна доступной для всех.
Truffle позволит нам создавать миграции для развертывания наших контрактов, а также проводить тесты для подтверждения их безопасности и качества.
В централизованном мире, когда я выполняю JS-скрипт, именно хост передает стоимость. В децентрализованном мире, когда я выполняю транзакцию с умным контрактом (т.е. выполняю скрипт или часть скрипта этого умного контракта), я плачу комиссионные, связанные со стоимостью функций, которые будут выполнены.
Шутка/Чай
Jest — это фреймворк для тестирования JS, который позволит нам протестировать наш контракт и наш Dapp
Chai — это библиотека JS, которая позволит нам делать утверждения, то есть, следуя тесту, проверять возвращаемые значения.
Помните, что Jest и Chai рекомендуются при использовании Truffle и Ganache для выполнения автоматизированных тестов, а также для автоматического развертывания во время тестирования.
Метамаска
Metamask — это цифровой кошелек, он позволит нам хранить наши NFT или другие криптовалюты.
Metamask позволит управлять биржами на блокчейне, отображать свой баланс.
Он позволяет подключиться к локальной сети, созданной Ganache, чтобы протестировать свой Dapp перед тем, как предоставить его миру.
Наш первый смарт-контракт ERC721
Для того чтобы реализовать наш первый смарт-контракт и упростить часть разработки, мы будем использовать только Remix в этой части.
Токен-контракт
Мы собираемся создать первый NFT Юнупа, для этого в Remix создайте новый файл с именем YounupNFT.sol
.
Контрактная основа и стандарт ERC721
Мы начинаем с импорта стандартов, портированных OpenZeppelin, и полезных указаний компилятору:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.12;
// Import of ERC721 Enumerable standard
import "@openzeppelin/contracts/token/ERC721/extensions/ERC721Enumerable.sol";
// Import of Ownable standard
import "@openzeppelin/contracts/access/Ownable.sol";
// Import of Strings standard
import "@openzeppelin/contracts/utils/Strings.sol";
Реализация контракта НФТ с IPFS
Перед началом работы, если вы хотите узнать больше об IPFS и загрузить свое первое изображение децентрализованным способом, перейдите к главе IPFS.
Затем мы определим условия контракта:
contract YounupNFT is ERC721Enumerable, Ownable {
//uint256 variable could use Strings library, example: value.toString()
using Strings for uint256;
//Declare metadata of token
// ipfsURI => Internet Protocol File Storage URI
string public ipfsURI;
// ipfsExt => Extension like .json
string public ipfsExt;
constructor(string memory _name, string memory _symbol, string memory _ipfsURI) ERC721(_name, _symbol) {
//Require is use to check the variable value, it will throw "No IPFS URI provided" if you don't pass it
require(byte(_ipfsURI).length > 0, "No IPFS URI provided");
// We allocate ipfsURI to the ipfsURI passed on contract deployment
ipfsURI = _ipfsURI;
}
// This function return the metadata of a given token, here it will return our ipfsURI (json metadata)
function tokenURI(uint256 tokenId) public view virtual override returns (string memory) {
require(_exists(tokenId), "ERC721URIStorage: URI query for nonexistent token");
return string(ipfsURI);
}
// This function is use to mint (obtain, buy...) a token, it will use _safeMint from ERC721 standard
function mint(address recipient) public onlyOwner returns(uint256) {
uint256 tokenId = this.totalSupply();
_safeMint(recipient, tokenId);
return tokenId;
}
}
Ценовая лента для получения цены в режиме реального времени
Мы хотим продавать наш NFT по фиксированной цене в $, поэтому мы будем использовать принцип цепной ценовой подачи
Принцип прост, в момент, когда мы хотим майнить NFT, смарт-контракт будет обращаться к фиду Price, чтобы обеспечить нам конвертацию $ в криптовалюту, здесь мы будем использовать токен Matic от Polygon за его минимальные комиссии.
YounupNFT-priceFeed.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.12;
//Import Chainlink aggregator to call datafeed with price
import "@chainlink/contracts/src/v0.8/interfaces/AggregatorV3Interface.sol";
contract YounupNFTPriceFeed {
// Private and immutable aggregator for security concern
AggregatorV3Interface private immutable _aggregator;
// We use feedAddress (address of the token) in the aggregator
constructor(address feedAddress) {
_aggregator = AggregatorV3Interface(feedAddress);
}
// Return last price of the token
function getLatestPrice() external view returns (int) {
// Only price not commented, we need to pass 5 arg, but we don't need the others datas
(
/*uint80 roundID*/,
int price,
/*uint startedAt*/,
/*uint timeStamp*/,
/*uint80 answeredInRound*/
) = _aggregator.latestRoundData();
return price;
}
// Return the price decimals (since not all token have the same decimal we need to know it)
function decimals() external view returns (uint8) {
return _aggregator.decimals();
}
}
// We add another contract so we could just deploy this one and don't need to send the token adress in the constructor since it's fixed here
// Here we pass the data from the MATIC testnet or mainnet because our NFT will be on polygon (MATIC)
contract PriceFeedMaticUSD is YounupNFTPriceFeed {
/**
* Network: Polygon
* Aggregator: MATIC/USD
* Address (mainnet): 0xAB594600376Ec9fD91F8e885dADF0CE036862dE0
* Address (testnet): 0xd0D5e3DB44DE05E9F294BB0a3bEEaF030DE24Ada
* From: https://docs.chain.link/docs/matic-addresses/
*/
constructor () YounupNFTPriceFeed(0xd0D5e3DB44DE05E9F294BB0a3bEEaF030DE24Ada) {}
}
Изготовление минтера с ограничениями
Для того чтобы применить ограничения на майнинг к нашему НФТ, например, ограничить его эмиссию или количество токенов, разрешенных для одного пользователя, мы создадим так называемый майнер.
Именно этот контракт будет единственным, который будет иметь право вызывать монетную функцию нашего контракта NFT через использование принципа Ownable
, фактически наш контракт Minter будет развертывать контракт NFT и, следовательно, будет Владельцем.
Сначала мы определим структуру входных параметров контракта.
YounupNFT-minter.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.12;
import "./YounupNFT-priceFeed.sol"
import "./YounupNFT.sol"
struct Parameters {
uint8 chainCurrencyDecimals; // Decimal of the token use
uint presalePriceUSD; // First sale price in USD
uint publicsalePriceUSD; // Seconde sale price in USD
uint maxSupply; // Max token mintable
uint maxMint; // Max mint by user
Token token; // Token is the YounupNFT token
YouNFTupPriceFeed priceFeed; // Our created pricefeed contract
Periods periods; // We will use periods to know when it's first or second sales
address payable teamAddress; // This is the team wallet that will receive all funds
}
struct Token {
string name; // Token name ex: Ethereum
string symbol; // Token symbol ex: ETH
string ipfsURI; // Token IPFS Uri
}
struct Periods {
// timestamp use for start and end private sale
uint startPresalePeriod;
uint endPresalePeriod;
// timestamp use for start and end of public sale
uint startPublicsalePeriod;
uint endPublicsalePeriod;
}
Теперь, когда мы определили структуры, которые позволят нам использовать и защитить наш контракт, мы можем написать все остальное:
contract YounupNFTMinter {
// We use it so we could now at wich state we are
enum State
{
waitingPeriod, // no mint possible
presalePeriod, // Mint with the private sale price
publicsalePeriod, // Mint with the public sale price
complete // All items are minted no more mint possible or you have outdated the endPublicSalePeriod
}
// Chain info and team wallet
uint8 public immutable chainTokenDecimals;
address payable public immutable teamAddress;
YounupNFT public immutable token;
//NFT max supply
uint public immutable maxSupply;
//NFT max mint per wallet
uint public immutable maxMint;
// NFT price in USD
uint public immutable presalePriceUSD;
uint public immutable publicsalePriceUSD;
uint constant ticketPriceDecimals = 2;
//Periods
Periods public periods;
//Chainlink Price feed
YounupNFTPriceFeed public immutable priceFeed;
constructor(Parameters memory params) {
// Many test use to ensure that the periode are logics
require(params.periods.startPresalePeriod > block.timestamp, "Invalid timestamp: startPresalePeriod");
require(params.periods.endPresalePeriod > params.periods.startPresalePeriod, "Invalid timestamp: endPresalePeriod");
require(params.periods.startPublicsalePeriod > params.periods.endPresalePeriod, "Invalid timestamp: startPublicsalePeriod");
require(params.periods.endPublicsalePeriod > params.periods.startPublicsalePeriod, "Invalid timestamp: endPlubicsalePeriod");
// We create the contract for the Token here
token = new YounupNFT(this, params.token.name, params.token.symbol, params.token.ipfsURI);
chainTokenDecimals = params.chainCurrencyDecimals;
presalePriceUSD = params.presalePriceUSD;
publicsalePriceUSD = params.publicsalePriceUSD;
maxSupply = params.maxSupply;
maxMint = params.maxMint;
priceFeed = params.priceFeed;
periods = params.periods;
teamAddress = params.teamAddress;
}
}
Связывание шахты с ценовой лентой
Наш контракт теперь инициализирован, но нам еще предстоит установить возможность чеканки по желаемой цене:
function mintNFT() external payable {
// Check if contract is in state where you could mint
require(getState() == State.presalePeriod || getState() == State.publicsalePeriod, "You can't mint it right now");
// Get the mint price from priceFeed and ensure everything is ok with all limits and price passed
uint entryPrice = getMintPrice();
require(msg.value >= entryPrice, "Not enough funds to mint YouNFTup. See getMintPrice()");
require(token.totalSupply() < maxSupply, "Too much NFT minted");
require(token.balanceOf(msg.sender) < maxMint, "You could mint only 1 YouNFTup");
// Mint the token using the YounupNFT mint function
token.mint(msg.sender);
// We also transfer all the fund sent to the contract to the team wallet
teamAddress.transfer(address(this).balance);
}
// Function use to get the current state of the contract
function getState() public view returns(State) {
if (block.timestamp < periods.startPresalePeriod) {
return State.waitingPeriod;
}
if (block.timestamp < periods.endPresalePeriod) {
return State.presalePeriod;
}
if (block.timestamp < periods.startPublicsalePeriod) {
return State.waitingPeriod;
}
if (block.timestamp < periods.endPublicsalePeriod) {
return State.publicsalePeriod;
}
return State.complete;
}
// Function use to get the mint price using priceFeed.getLatestPrice()
function getMintPrice() public view returns(uint) {
int latestPrice = priceFeed.getLatestPrice();
uint256 ticketPriceUSD = publicsalePriceUSD;
if (getState() == State.presalePeriod) {
ticketPriceUSD = presalePriceUSD;
}
uint latestPriceAdjusted = uint(latestPrice) * 10 ** (chainTokenDecimals - priceFeed.decimals());
uint entryPrice = 10 ** (chainTokenDecimals * 2) * ticketPriceUSD / latestPriceAdjusted / (10 ** ticketPriceDecimals);
return entryPrice;
}
// Function to check if the contract is complete or no
function isComplete() public view returns(bool) {
return getState() == State.complete;
}
Нам также необходимо обновить токен, поскольку теперь мы используем майнер, как показано в следующей строке:
это эквивалентно YounupNFTMinter
YounupNFT.sol
import "./YounupNFTMinter.sol"
...
constructor(YouNFTupMinter _minter, ...) {
// We keep info about the minter contract address
minter = _minter;
}
Функция майнинга является public onlyOwner
только контракт майнера сможет майнить токен YounupNFT
Создание фабрики для развертывания
Теперь нам осталось развернуть наш контракт, чтобы упростить эту часть, я создаю фабрику, которая создает конструктор, а не объявляю его при развертывании как Tupple.
YounupNFT-factory.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.12;
import "./YouNFTup-minter.sol";
contract YouNFTupFactory is YouNFTupMinter {
constructor() YouNFTupMinter(Parameters({
chainCurrencyDecimals: 18,
presalePriceUSD: 100,
publicsalePriceUSD: 200,
maxSupply: 42,
maxMint: 1,
token: Token({
name: "YouNFTup",
symbol: "YNP",
ipfsURI: "https://ipfs.io/ipfs/bafkreihmkaetjsqkb2cnzdkyhvpattzpj7duuqj4kfvtx3qfcitj2didbu"
}),
priceFeed: new PriceFeedMaticUSD(),
periods: Periods({
startPresalePeriod: block.timestamp + 1,
endPresalePeriod: block.timestamp + 3600 * 24 * 30,
startPublicsalePeriod: block.timestamp + 3600 * 24 * 60,
endPublicsalePeriod: block.timestamp + 3600 * 24 * 90
}),
teamAddress: payable(0xB677dd9Ae9217Fbb4E3d072b9F7F68947C2a4AA6)
})) {}
}
Развертывание testnet и тестирование с помощью Remix
Осталось только составить наш контракт и развернуть его в тестовой сети Matic.
(чтобы добавить сеть, она находится здесь)
В части remix «Solidity Compiler» нужно выбрать версию компилятора 0.8.12, затем скомпилировать контракт YounupNFT-factory.sol
, который импортирует все остальные.
Если вы установили Metamask и добавили сеть Matic Mumbai
, то теперь мы развернем наш контракт.
В части Remix «Deploy & run transactions» вы должны выбрать для Environment «Injected Web3», после чего у вас появится уведомление Metamask, не забудьте проверить, что вы находитесь в тестовой сети полигонов перед развертыванием.
Затем выберите контракт YounupNFT-factory
и нажмите на «Deploy», транзакция появится на вашем Metamask, если вы примете ее, вы заплатите за развертывание контракта (в testnet это бесплатно).
Затем вы получите доступ ко всем функциям вашего контракта, чтобы иметь возможность протестировать его, вы сможете, наконец, добыть свой первый NFT и проверить его существование через OpenSea Testnet
Управление ресурсами с помощью IPFS
Что такое IPFS?
IPFS или InterPlanetary File System — это протокол P2P (peer to peer) Web3.0.
В мире Web 2.0 мы храним наши файлы на централизованных серверах, здесь же идея состоит в том, чтобы полагаться на децентрализацию и сеть совместной работы для размещения файлов.
Как это работает?
Цель IPFS — сделать сеть :
- быстрее
- более безопасный
- более открытый
- менее дорогой
В идеале децентрализации он стремится занять значительное место перед протоколом HTTP
Роль IPFS при НФТ
Многие НФТ используют блокчейн для своих контрактов, но не используют какую-либо децентрализованную систему для ресурсов.
Действительно, важно разместить в сети метаданные и все активы, которые позволят генерировать НФТ в децентрализованной сети.
Цель заключается в том, чтобы владелец НМТ полностью владел им, а создатель не мог, например, уничтожить или переместить изображение.
Загрузка метаданных в IPFS
Для того, чтобы получить ipfs URL, который позволит нам получить информацию из нашего NFT в контракте.
Начиная с того, что мы будем развивать здесь:
token: Token({
name: "YouNFTup",
symbol: "YNP",
ipfsURI: "https://ipfs.io/ipfs/bafkreihmkaetjsqkb2cnzdkyhvpattzpj7duuqj4kfvtx3qfcitj2didbu"
}),
вся задача может быть автоматизирована с помощью сценариев
Для этого мы будем использовать nft.storage, есть также pinata :
- Создайте учетную запись или войдите в систему с помощью github
- Загрузите свое изображение, чтобы получить ссылку на IPFS
- Запишите файл
metadata.json
.
{
"description": "Official Younup NFT",
"external_url": "https://younup.fr",
"image": "ipfs://your-image-link", //example: https://ipfs.io/ipfs/bafkreigf2d4qgt6klvuxwr7d3yvkpjlrszlfhgpbphbdvwkzljivef24iu
"name": "YouNFTup",
"attributes": [] // You can add many attributes check OpenSea doc
}
- Загрузите файл метаданных
Наконец, вы получите файл metada, который вы можете связать с вашим NFT.
Пример: https://ipfs.io/ipfs/bafkreihmkaetjsqkb2cnzdkyhvpattzpj7duuqj4kfvtx3qfcitj2didbu
Цель главы
Код: https://github.com/SoxZz5/NFT-tuto/tree/master/project/contracts
Résultat: https://testnets.opensea.io/assets/mumbai/0xe839ceaa7d410c7e957a97d970b8042b13a21c28/0
Контракт с Минтером: https://mumbai.polygonscan.com/address/0xc7cdcdfa7c724bd7148efb53a5774928f56dab46
NFT contract: https://mumbai.polygonscan.com/address/0xe839ceaa7d410c7e957a97d970b8042b13a21c28