dApp | Магазин токенов ERC 20
Всем привет. Сегодня снова пойдет речь об web 3.0. Создадим свой marketplace токенов, где будет реализована продажа покупка и перевод токенов.
Наш магазин будет построен на смарт-контракте прошлого урока. Так что если вы не знакомы с функциями approve или transfer, то вам туда.
Я не силен в css и фантазии у меня мало. Так что как есть.
Есть пару изменений в deploy.js из прошлых статей:
Внутри функции main в конце добавляем строчки для создания и записи в файл адреса токена. Он нам понадобиться.
async function main() { if (network.name === "hardhat") { console.warn( "You are trying to deploy a contract to the Hardhat Network, which" + "gets automatically created and destroyed every time. Use the Hardhat" + " option '--network localhost'" ); } const [deployer] = await ethers.getSigners() console.log("Deploying with", await deployer.getAddress()) const ERC721 = await ethers.getContractFactory("DDWorldShop", deployer) const Token = await ERC721.deploy() await Token.deployed() console.log("Token: ", await Token.token()) const tokenAddr = await Token.token() saveFrontendFiles({ DDWorldShop: Token }) //Обновление main fs.writeFileSync( path.join("./front/contracts", '/', 'token-address' + '.json'), JSON.stringify({['tokenAddress']: tokenAddr}, undefined, 2) ) }
Теперь открываем консоль, заходим в нашу папку с проектом. Вводим команду
npx hardhat node
Открываем новую консоль и вводим команду:
npx hardhat run scripts\deploy.js --network localhost
теперь создаем проект next.js, водим одну из команд на выбор:
npx create-next-app@latest yarn create next-app p npm create next-app
Дальше на все отвечаем нет и вводим название файла
полное объяснение создания next.js проекта находится
в статье про connect wallet
Заходим в папку которую создали (у меня front)
npm run dev
Наш проект запущен, можно начать создавать наш магазин
Удаляем все из файлика index.js и пишем свой скрипт
Функции frontend
Функции для connect wallet и initialize я не буду показывать. Просто их добавим. И не забываем про компоненты. Их тоже добавляем. Все это рассказывалось в предыдущих статьях про fontend. Поэтому не буду повторяться.
В import нужно добавить три новых импорта:
import token from '../contracts/token-address.json' import tokenJSON from 'D:/solidity/ERC 20/artifacts/contracts/Erc.sol/DDWorldToken.json' import styles from '../styles/Home.module.css'
import token - это адрес нашего токена который мы создали при развертывании контракта
tokenJSON - это json структура всех наших функций и переменных, которые есть в контракте DDWorldToken, который в свою очередь наследует все функции из контракта ERC20. Это нам нужно для доступа к функциям стандарта ERC20, таким как approve. Ведь мы развернули контракт магазина, у которого нет таких функций, но нам они нужны.
Но наш контракт магазина внутри себя развернул контракт DDWorldToken, который в свою очередь унаследовал все функции контракта ERC20. И поэтому мы получим доступ к ним. Надеюсь это не взрывает мозг. На самом деле все очень просто и дальше мы это увидим.
Для начала поговорим о функции render и что мы хотим выводить и вводить на нашем сайте.
Как видно из первого скрина, мы хотим, чтоб у нас был input, куда мы будем вводить количество токенов для покупки, продажи или перевода. И разумеется кнопки, которые будут отвечать за вывоз функций.
render() { if(!this.state.selectedAccount) { return <ConnectWallet connectWallet={this._connectWallet} networkError={this.state.networkError} dismiss={this._dismissNetworkError} /> }else{ return( <> <div className={styles.container}> <p className={styles.name}>DDW Token Marketplace </p> <div className={styles.containerBalance}> <div style={{display:"flex"}}> <div className={styles.tokenAmount} style={{margin:"auto",display:"flex"}}> token amount: {this.state.amount} </div> </div> <p className={styles.tokenBalance}> {this.state.tokenBalance} DDW </p> {this.state.balance && <p className={styles.balance}> {ethers.utils.formatEther(this.state.balance).slice(0,10)} ETH </p> } </div> <div className={styles.containerBalance} style={{marginTop:"30px"}}> <div className={styles.balance} style={{marginTop:"10px"}}>token to buy <br></br> <input ref={this.tokenToBuy}></input> </div> <div className={styles.balance} style={{marginTop:"10px"}}> <button className={styles.button} onClick = {this._buy}> buy </button> </div> <div className={styles.balance} style={{marginTop:"10px"}}> amount to sell <br></br> <input ref={this.amountToSell}></input> </div> <div className={styles.balance} style={{marginTop:"10px"}}> <button className={styles.button} onClick = {this._sell}> sell </button> </div> <div className={styles.balance} style={{marginTop:"10px"}}> amount to transfer <br></br> <input ref={this.amountToTransfer}></input> <br></br> address to transfer <br></br> <input ref={this.tokenTransferTo}></input> </div> <div className={styles.balance} style={{marginTop:"10px"}}> <button className={styles.button} onClick = {this._transfer}> transfer token </button> </div> </div> </div> </> )} }
Тут нет ни чего особенного обычный html, где в button мы указали функции, которые будем писать и в input ссылки, о которых чуть позже.
Так же можно видеть два вида записи стилей css.
Первый вариант, это через styel ={{...}}, это удобно, когда нам не нужен отдельный класс для описания стилей блока.
Второй вариант, это через классы, className={styles.container}. Этот вариант работает, когда мы подключили файлик со стилями, поэтому его нужно подключить.
import styles from '../styles/Home.module.css'
То есть чтоб показать, какой класс использовать, нужно написать styles.Name, где styles, это переменная которая обладает всеми данными из файлика home.module.css и Name, название нашего класса. Подробнее о стилях и вариантах их подключения расскажу в другой статье.
Если вы знаете как писать на css, то все ваши классы должны быть написаны в файле hame.module.css, который уже создан и в котором уже есть код.
Храниться он в папке styles.
constructor(props) { super(props) this.initialState = { selectedAccount: null, networkError: null, balance: null, amount: null, tokenBalance: null } this.state = this.initialState this.tokenToBuy = React.createRef(); this.amountToSell = React.createRef(); this.amountToTransfer = React.createRef(); this.tokenTransferTo = React.createRef(); }
У нас будет 2 новых глобальных переменных, отвечающих за разные балансы.
amount - общий баланс токенов
tokenBalance - баланс токенов кошелька
Так же новая переменные, которые будут получать значения input и сохранять их как ссылку, чтоб мы могли получить их значение и передать в ту или иную переменную. Для этого нужно указать React.createRef().
Наш input должен понимать куда сохранять значения переменных которые мы ввели. Поэтому это нужно указать как ref (ссылка на объект).
Как пример: У нас есть переменная this.tokenToBuy и при помощи ref мы передаем всю информацию об этом импуте.
<input ref={this.tokenToBuy}></input>
Первую новую функцию, которую мы напишем, это функция покупки токенов из магазина.
_buy = async () => { let buyToken = this.tokenToBuy.current.value console.log(buyToken); const txData = { value: buyToken, gasLimit: ethers.utils.hexlify(100000) } const tx = await this._DDWShop.buy(txData) await tx.wait() this._balanceOff() const tx1 = await this._DDWShop._tokenbalance(this.state.selectedAccount) this.setState({ tokenBalance: Number(tx1)/1000000000 }) }
Первая переменная принимает значение из input. Которое мы обозначили в конструкторе и указали в input, берем текущее значение (current.value). Также важная переменная txData, которая вмещает в себя количество токенов, которое мы хотим купить и gasLimit.
Так как по дефолту при вызове функции мы берем 21000 газа, а тут мы еще вводим переменные и делаем расчеты внутри, без установки своего начального лимита будет вылетать ошибка нехватки газа.
Количество газа можно менять и подгонять под минимальное допустимое значение, которое не будет выдавать ошибку. Я брал с запасом для тестов
Дальше мы вызываем функцию buy из смарт-контракта с нашими значениями, ждем завершения транзакции при помощи tx.wait(). И обновляем баланс кошелька. После мы вызываем функцию баланса токенов на нашем кошельке и записываем ее в глобальную переменную на самом сайте, чтоб вывести ее на экран.
Вторая функция это transfer, она переводит с одного кошелька токены на другой
_transfer = async () => { let amount = this.amountToTransfer.current.value; let address = this.tokenTransferTo.current.value const txData = { gasLimit: ethers.utils.hexlify(100000) } const tx1 = await this.erc20.approve(address.toString(),amount,txData) await tx1.wait() const tx = await this._DDWShop.transferTo(address.toString(),amount,txData) this._balanceOff() const tx2 = await this._DDWShop._tokenbalance(this.state.selectedAccount) this.setState({ tokenBalance: Number(tx2)/1000000000 }) }
Первая переменная принимает число токенов для перевода
Вторая переменная адрес куда переводить токены
После мы снова говорим про gasLimit.
Первая транзакция, которую нам нужно провести, это через контракт erc20 вызвать функцию approve, которая даст нам разрешение переводить токены на другой аккаунт.
Вторя транзакция это сам перевод токенов.
Ну и последняя транзакция для обновления баланса токенов
Эта функция похожа на transfer, только тут мы будем не переводить токены а продавать нашему магазину.
_sell = async () =>{ let amount = this.amountToSell.current.value console.log(amount); const txData = { gasLimit: ethers.utils.hexlify(100000) } const tx1 = await this.erc20.approve(this._MShop.address, amount, txData) await tx1.wait() const tx = await this._DDWShop.sell(amount, txData) this._balanceOff() const tx2 = await this._DDWShop._tokenbalance(this.state.selectedAccount) this.setState({ tokenBalance: Number(tx2)/1000000000 }) }
Все так же, но мы не принимает ни какой адрес, a функция transferTo меняется на sell.
Функция обновления баланса токенов на смарт-контракте
_balanceOf = async () => { const balanceAddress = await this._DDWShop.tokenBalance() this.setState({ amount: Number(balanceAddress) }) }
Тут ни чего особенного. Просто перезапись баланса.
Так же в функцию _initialize я вызываю функцию _balanceOf и _tokenbalance, чтоб при запуске нашей страницы мы сразу видели наш баланс токенов.
async _initialize(selectedAddress) { this._provider = new ethers.providers.Web3Provider(window.ethereum) this._MShop = new ethers.Contract( shopAddress.DDWorldShop, shopArtifacts.abi, this._provider.getSigner(0) ) this.erc20 = new ethers.Contract( token.tokenAddress, tokenJSON.abi, this._provider.getSigner(0) ) this.setState({ selectedAccount: selectedAddress }, async () => await this.updateBalance() ) this._balanceOf() const tx = await this._DDWShop._tokenbalance(selectedAddress) this.setState({ tokenBalance: Number(tx)/1000000000 }) }
Еще важный момент. В этой функции мы разворачиваем смарт контракт магазина, но нам еще нужен смарт контракт erc20 и именно поэтому мы импортировали JSON контракта erc20 и адрес токена который мы создали при деплое смарт-контракта в наш frontend (еще в начале)
this._provider = new ethers.providers.Web3Provider(window.ethereum) this._DDWShop = new ethers.Contract( shopAddress.DDWorldShop, shopArtifacts.abi, this._provider.getSigner(0) ) this.erc20 = new ethers.Contract( token.tokenAddress, tokenJSON.abi, this._provider.getSigner(0) )
Рассмотрим по подробнее. Вот мы разворачиваем наш контракт магазина _DDWShop. Передаем адрес и abi контракта, который задеплоили и пользователя который это все разворачивает.
Теперь наш контракт erc20. Тут, как я говорил в начале мы должны развернуть новый контракт нашего токена, не магазина. Для этого адрес контракта это наш токен и abi нашего erc20, но провайдер такой же.
То есть где нам нужны были функции, такие как approve мы обращаемся не _DDWShop, а к erc20. Все просто.
Если вы сделали все правильно, то можно тестить наши функции.
Если есть ошибки, то в конце будет репозиторий этого проекта. Сможете сравнить и найти ошибки.
Вообще мы можем импортировать свой токен в ММ, но почему то в последнее время это не работает, а ошибку я не нашел как пофиксить. Только видел людей, которые тоже сталкиваются с этим. И все пришли к выводу, что в localhost перестала работать возможность импорта своих токенов, но если у вас работает или это починили, то я покажу как это делать.
Находим кнопку импорта токена.
После вставляем свой адрес токена и все остальные параметры сами подтянуться.
После если нажмем на кнопку то нам покажет информацию о токене и наш баланс.
После импотра токена он должен отобразиться в нашем ММ, но его почему то там нет. Вот такая проблема. НО для нас это не проблема, потому что все данные мы выводим на самом сайте.
И вот у нас получилось купить, так как изменился баланс.
Так как мы вводили wei, наш токен равен gwei, то мы получили 0.000001 токен. Можно ввод улучшить и вводить токены 1 к 1.
Теперь давайте продадим наши токены.
Когда вы будете подтверждать, то у вас будет метамаск просить подтвердить перевод того количества токенов, которые вы выбрали , это выглядит примерно так:
После approve будет вызываться функция перевода токенов и по завершении этой транзакции с вас спишут токены и вы получите деньги за токены.
Вот так будет все выглядит после продажи всех токенов. Потратили эфир только на газ
Теперь купим побольше токенов и попробуем перевести их на другой кошелек.
Вводим количество токенов для продажи и адрес куда переводим.
Так же подтверждаем перевод, как при продаже и ждем завершение транзакции.
Теперь проверим пришли ли токены на другой кошелек.
Как видно на этом аккаунте не было ни одной транзакции (так как 10000 eth по дефолту), но у нас появилось 0.00001 DDW токена. Все работает ура!
Так же если у нас есть токены, то они отображайся при импотре но, сам токен не отображается в ММ. (Так можно проверять если что)
На этом думаю закончу. Написали свой простой магазин токенов. Дальше больше. Оставлю репозиторий на этот проект, чтоб могли сами потыкать и посмотреть. Объяснять фронтенд очень сложно, поэтому если будут вопросы то пишите.
tg: мой телеграмчик)