Найповніший практичний вебінар: від написання 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", після чого:
Переглянути приклади з попередніх вебінарів:
1030
https://evm.confluxrpc.com
https://evm.confluxscan.org
Ви успішно створили, розгорнули та верифікували ERC-20 токен та ERC-721 NFT на Conflux blockchain
Поділіться результатом в соціальних мережах з хештегом #ConfluxUkraine
Рекомендуємо переглянути ці відео перед вебінаром для кращого розуміння процесу розробки на Conflux
Детальний гайд по верифікації смарт-контрактів через ConfluxScan. Дізнаєтеся, як правильно публікувати вихідний код ваших контрактів.
Покрокове налаштування фреймворку Hardhat для розробки на Conflux eSpace. Ідеально для професійної розробки.
Вивчаємо розробку для Core Space - унікальної частини екосистеми Conflux з власними особливостями та можливостями.
Швидкий курс по основах мови Solidity. Якщо ви новачок у смарт-контрактах - цей відео обов'язковий до перегляду.
Основні вразливості смарт-контрактів та способи їх уникнення. Важливі концепції для безпечної розробки.
Розбираємо архітектуру Conflux та відмінності між двома просторами. Коли використовувати кожен з них.
Заповніть форму нижче, щоб зареєструватися на безкоштовний практичний вебінар. Посилання та всі матеріали надішлемо на email за день до початку.