Найповніший практичний вебінар: від написання Solidity коду до деплою ERC-20 токенів та ERC-721 NFT у мережі Conflux. Створимо токен ConfluxUkraine та NFT колекцію разом, розберемо всі нюанси та підводні камені розробки!
Початок та налаштування
Знайомимося з учасниками, перевіряємо готовність інструментів та озвучуємо план на 3 години.
Основи токенів на блокчейні
Розбираємо стандарт ERC-20, його функції та особливості. Чому саме цей стандарт використовують для токенів.
Створення ERC-20 токена з нуля
Пишемо повноцінний смарт-контракт токена з розширеними функціями: mint, burn, pause та роль-система.
Перевірка функціональності
Тестуємо всі функції токена: transfer, mint, burn. Додаємо токен у гаманці та перевіряємо на ConfluxScan.
Відпочинок та питання
Короткий перерва для відпочинку. Відповідаємо на питання по токенам та підготовлюємося до NFT частини.
Світ унікальних токенів
Вивчаємо стандарт ERC-721, відмінності від ERC-20. Розбираємо метадані, IPFS та екосистему NFT.
Створення NFT колекції
Розробляємо повноцінний NFT контракт з можливістю mint, управління метаданими та інтеграцією з маркетплейсами.
Публікація вихідного коду
Верифікуємо обидва контракти на ConfluxScan для прозорості та довіри. Розбираємо різні способи верифікації.
Завершення вебінару
Підводимо підсумки, відповідаємо на питання та обговорюємо наступні кроки у Web3 розробці.
Це дозволить нам відразу перейти до практики та максимально ефективно використати час
Офіційний гаманець Conflux з підтримкою eSpace та Core Space - найкращий вибір для роботи з Conflux
Встановлюємо та налаштовуємо MetaMask для роботи з Conflux eSpace Testnet
Онлайн IDE для розробки смарт-контрактів - головний інструмент вебінару
🌐 Відкрити Remix IDEАвтоматична перевірка налаштувань та отримання тестових CFX для участі у вебінарі
Детальні інструкції для кожного етапу створення ERC-20 токена та ERC-721 NFT на Conflux
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import "@openzeppelin/contracts/token/ERC20/extensions/ERC20Burnable.sol";
import "@openzeppelin/contracts/access/Ownable.sol";
import "@openzeppelin/contracts/security/Pausable.sol";
/**
* @title ConfluxUkraine Token
* @dev ERC20 token for Ukrainian Conflux community
* @author Conflux Ukraine Team
*/
contract ConfluxUkraineToken is ERC20, ERC20Burnable, Ownable, Pausable {
uint256 public constant MAX_SUPPLY = 1_000_000_000 * 10**18; // 1 billion tokens
uint256 public totalMinted;
mapping(address => bool) public minters;
event TokensMinted(address indexed to, uint256 amount, address indexed minter);
event TokensBurned(address indexed from, uint256 amount);
event MinterAdded(address indexed minter);
event MinterRemoved(address indexed minter);
modifier onlyMinter() {
require(minters[msg.sender] || msg.sender == owner(), "ConfluxUkraine: not a minter");
_;
}
modifier validAddress(address _address) {
require(_address != address(0), "ConfluxUkraine: zero address");
require(_address != address(this), "ConfluxUkraine: contract address");
_;
}
constructor(uint256 initialSupply) ERC20("ConfluxUkraine", "CFUKR") Ownable(msg.sender) {
require(initialSupply <= MAX_SUPPLY, "ConfluxUkraine: exceeds max supply");
if (initialSupply > 0) {
_mint(msg.sender, initialSupply);
totalMinted = initialSupply;
}
minters[msg.sender] = true;
emit MinterAdded(msg.sender);
}
function mint(address to, uint256 amount)
external
onlyMinter
validAddress(to)
whenNotPaused
{
require(amount > 0, "ConfluxUkraine: amount must be greater than 0");
require(totalMinted + amount <= MAX_SUPPLY, "ConfluxUkraine: exceeds max supply");
_mint(to, amount);
totalMinted += amount;
emit TokensMinted(to, amount, msg.sender);
}
function batchMint(address[] calldata recipients, uint256[] calldata amounts)
external
onlyMinter
whenNotPaused
{
require(recipients.length == amounts.length, "ConfluxUkraine: arrays length mismatch");
require(recipients.length > 0, "ConfluxUkraine: empty arrays");
require(recipients.length <= 100, "ConfluxUkraine: too many recipients");
uint256 totalAmount = 0;
for (uint256 i = 0; i < amounts.length; i++) {
require(amounts[i] > 0, "ConfluxUkraine: amount must be greater than 0");
totalAmount += amounts[i];
}
require(totalMinted + totalAmount <= MAX_SUPPLY, "ConfluxUkraine: exceeds max supply");
for (uint256 i = 0; i < recipients.length; i++) {
require(recipients[i] != address(0), "ConfluxUkraine: zero address");
require(recipients[i] != address(this), "ConfluxUkraine: contract address");
_mint(recipients[i], amounts[i]);
emit TokensMinted(recipients[i], amounts[i], msg.sender);
}
totalMinted += totalAmount;
}
function burn(uint256 amount) public override whenNotPaused {
require(amount > 0, "ConfluxUkraine: amount must be greater than 0");
require(balanceOf(msg.sender) >= amount, "ConfluxUkraine: insufficient balance");
super.burn(amount);
emit TokensBurned(msg.sender, amount);
}
function addMinter(address minter) external onlyOwner validAddress(minter) {
require(!minters[minter], "ConfluxUkraine: already a minter");
minters[minter] = true;
emit MinterAdded(minter);
}
function removeMinter(address minter) external onlyOwner {
require(minter != owner(), "ConfluxUkraine: cannot remove owner");
require(minters[minter], "ConfluxUkraine: not a minter");
minters[minter] = false;
emit MinterRemoved(minter);
}
function pause() external onlyOwner {
_pause();
}
function unpause() external onlyOwner {
_unpause();
}
function _update(address from, address to, uint256 value) internal override whenNotPaused {
super._update(from, to, value);
}
function isMinter(address account) external view returns (bool) {
return minters[account] || account == owner();
}
function remainingSupply() external view returns (uint256) {
return MAX_SUPPLY - totalMinted;
}
function canMint(uint256 amount) external view returns (bool) {
return totalMinted + amount <= MAX_SUPPLY;
}
receive() external payable {}
}
⚠️ Важливо: Працює тільки в Chrome/Edge. У Firefox використовуйте "Custom - External Http Provider"
Ваш токен контракт з'явиться в розділі "Deployed Contracts" з адресою схожою на:
Перевірити можно створений конракт на https://evmtestnet.confluxscan.org/. Збережіть цю адресу - вона знадобиться пізніше!
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
import "@openzeppelin/contracts/token/ERC721/ERC721.sol";
import "@openzeppelin/contracts/token/ERC721/extensions/ERC721URIStorage.sol";
import "@openzeppelin/contracts/token/ERC721/extensions/ERC721Burnable.sol";
import "@openzeppelin/contracts/access/Ownable.sol";
import "@openzeppelin/contracts/security/Pausable.sol";
/**
* @title ConfluxUkraine NFT
* @dev ERC721 NFT collection for Ukrainian Conflux community
* @author Conflux Ukraine Team
*/
contract ConfluxUkraineNFT is ERC721, ERC721URIStorage, ERC721Burnable, Ownable, Pausable {
uint256 private _tokenIdCounter;
uint256 public constant MAX_SUPPLY = 10000;
uint256 public mintPrice = 0.01 ether; // 0.01 CFX per NFT
mapping(address => bool) public minters;
event NFTMinted(address indexed to, uint256 indexed tokenId, string uri);
event MintPriceUpdated(uint256 oldPrice, uint256 newPrice);
event MinterAdded(address indexed minter);
event MinterRemoved(address indexed minter);
modifier onlyMinter() {
require(minters[msg.sender] || msg.sender == owner(), "ConfluxUkraine: not a minter");
_;
}
modifier validAddress(address _address) {
require(_address != address(0), "ConfluxUkraine: zero address");
_;
}
constructor() ERC721("ConfluxUkraine NFT", "CFUKRNFT") Ownable(msg.sender) {
minters[msg.sender] = true;
emit MinterAdded(msg.sender);
}
function mint(address to, string memory uri)
public
onlyMinter
validAddress(to)
whenNotPaused
returns (uint256)
{
require(_tokenIdCounter < MAX_SUPPLY, "ConfluxUkraine: max supply reached");
uint256 tokenId = _tokenIdCounter;
_tokenIdCounter++;
_safeMint(to, tokenId);
_setTokenURI(tokenId, uri);
emit NFTMinted(to, tokenId, uri);
return tokenId;
}
function publicMint(string memory uri)
external
payable
whenNotPaused
returns (uint256)
{
require(msg.value >= mintPrice, "ConfluxUkraine: insufficient payment");
require(_tokenIdCounter < MAX_SUPPLY, "ConfluxUkraine: max supply reached");
uint256 tokenId = _tokenIdCounter;
_tokenIdCounter++;
_safeMint(msg.sender, tokenId);
_setTokenURI(tokenId, uri);
// Refund excess payment
if (msg.value > mintPrice) {
payable(msg.sender).transfer(msg.value - mintPrice);
}
emit NFTMinted(msg.sender, tokenId, uri);
return tokenId;
}
function batchMint(address[] calldata recipients, string[] calldata uris)
external
onlyMinter
whenNotPaused
{
require(recipients.length == uris.length, "ConfluxUkraine: arrays length mismatch");
require(recipients.length > 0, "ConfluxUkraine: empty arrays");
require(recipients.length <= 50, "ConfluxUkraine: too many recipients");
require(_tokenIdCounter + recipients.length <= MAX_SUPPLY, "ConfluxUkraine: would exceed max supply");
for (uint256 i = 0; i < recipients.length; i++) {
require(recipients[i] != address(0), "ConfluxUkraine: zero address");
uint256 tokenId = _tokenIdCounter;
_tokenIdCounter++;
_safeMint(recipients[i], tokenId);
_setTokenURI(tokenId, uris[i]);
emit NFTMinted(recipients[i], tokenId, uris[i]);
}
}
function addMinter(address minter) external onlyOwner validAddress(minter) {
require(!minters[minter], "ConfluxUkraine: already a minter");
minters[minter] = true;
emit MinterAdded(minter);
}
function removeMinter(address minter) external onlyOwner {
require(minter != owner(), "ConfluxUkraine: cannot remove owner");
require(minters[minter], "ConfluxUkraine: not a minter");
minters[minter] = false;
emit MinterRemoved(minter);
}
function setMintPrice(uint256 _mintPrice) external onlyOwner {
uint256 oldPrice = mintPrice;
mintPrice = _mintPrice;
emit MintPriceUpdated(oldPrice, _mintPrice);
}
function pause() external onlyOwner {
_pause();
}
function unpause() external onlyOwner {
_unpause();
}
function withdraw() external onlyOwner {
uint256 balance = address(this).balance;
require(balance > 0, "ConfluxUkraine: no funds to withdraw");
payable(owner()).transfer(balance);
}
function totalSupply() external view returns (uint256) {
return _tokenIdCounter;
}
function remainingSupply() external view returns (uint256) {
return MAX_SUPPLY - _tokenIdCounter;
}
// Required overrides
function tokenURI(uint256 tokenId)
public
view
override(ERC721, ERC721URIStorage)
returns (string memory)
{
return super.tokenURI(tokenId);
}
function supportsInterface(bytes4 interfaceId)
public
view
override(ERC721, ERC721URIStorage)
returns (bool)
{
return super.supportsInterface(interfaceId);
}
function _update(address to, uint256 tokenId, address auth)
internal
override
whenNotPaused
returns (address)
{
return super._update(to, tokenId, auth);
}
}
Ваш NFT контракт з'явиться в "Deployed Contracts" поруч з токен контрактом
Тепер у вас є обидва контракти готові до тестування!
bafkreibuel3en6nl7pzrmjm5t4azvuwswyayeh4ekrqknbjmjvu2abhfoi){
"name": "ConfluxUkraine #1",
"description": "Перший NFT української Conflux спільноти. Символізує єдність та інновації в Web3 просторі України.",
"image": "https://teal-genetic-skink-879.mypinata.cloud/ipfs/bafkreibuel3en6nl7pzrmjm5t4azvuwswyayeh4ekrqknbjmjvu2abhfoi",
"external_url": "https://confluxukraine.org",
"attributes": [
{
"trait_type": "Edition",
"value": "Genesis"
},
{
"trait_type": "Community",
"value": "Ukraine"
},
{
"trait_type": "Network",
"value": "Conflux"
},
{
"trait_type": "Rarity",
"value": "Legendary"
}
]
}
Ви можете завантажити готовий файл метаданих натиснувши кнопку "💾 Завантажити файл" вище, або створити власний, скопіювавши та відредагувавши JSON.
Після завантаження файлу, просто завантажте його на Pinata як описано в наступному кроці.
Використовуйте HTTPS посилання замість ipfs:// для кращої сумісності з ConfluxScan!
bafkreifrahfthgogod6ryaaj7vnjuweutfz3mzndvsjbazz2iwjrvw2yiq)1. Підготовка в Remix
2. Заповнення параметрів mint
https://teal-genetic-skink-879.mypinata.cloud/ipfs/bafkreifrahfthgogod6ryaaj7vnjuweutfz3mzndvsjbazz2iwjrvw2yiqВикористовуйте HTTPS посилання замість ipfs:// для правильного відображення в ConfluxScan!
В Remix:
В ConfluxScan:
Додавання в кошелек:
image та uri✅ Результат: Повністю функціональний NFT з коректним відображенням в ConfluxScan!
Верифікація робить ваші контракти прозорими та довіреними. Будемо використовувати офіційний ConfluxScan.
Ви побачите по середині екрана повідомлення "Verification Succeed", після чого:
Переглянути приклади з попередніх вебінарів:
1030https://evm.confluxrpc.comhttps://evm.confluxscan.orgВи успішно створили, розгорнули та верифікували ERC-20 токен та ERC-721 NFT на Conflux blockchain
Поділіться результатом в соціальних мережах з хештегом #ConfluxUkraine
Рекомендуємо переглянути ці відео перед вебінаром для кращого розуміння процесу розробки на Conflux
Детальний гайд по верифікації смарт-контрактів через ConfluxScan. Дізнаєтеся, як правильно публікувати вихідний код ваших контрактів.
Покрокове налаштування фреймворку Hardhat для розробки на Conflux eSpace. Ідеально для професійної розробки.
Вивчаємо розробку для Core Space - унікальної частини екосистеми Conflux з власними особливостями та можливостями.
Швидкий курс по основах мови Solidity. Якщо ви новачок у смарт-контрактах - цей відео обов'язковий до перегляду.
Основні вразливості смарт-контрактів та способи їх уникнення. Важливі концепції для безпечної розробки.
Розбираємо архітектуру Conflux та відмінності між двома просторами. Коли використовувати кожен з них.
Заповніть форму нижче, щоб зареєструватися на безкоштовний практичний вебінар. Посилання та всі матеріали надішлемо на email за день до початку.