Dapps
March 17, 2023

dApp | Магазин токенов ERC 20

Всем привет. Сегодня снова пойдет речь об web 3.0. Создадим свой marketplace токенов, где будет реализована продажа покупка и перевод токенов.

Наш магазин будет построен на смарт-контракте прошлого урока. Так что если вы не знакомы с функциями approve или transfer, то вам туда.

Наш результат на сегодня:

Я не силен в css и фантазии у меня мало. Так что как есть.

запуск

Есть пару изменений в deploy.js из прошлых статей:

Внутри функции main в конце добавляем строчки для создания и записи в файл адреса токена. Он нам понадобиться.

Обновлённая функция 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

Для начала поговорим о функции 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

Первую новую функцию, которую мы напишем, это функция покупки токенов из магазина.

_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, она переводит с одного кошелька токены на другой

_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, которая даст нам разрешение переводить токены на другой аккаунт.

Вторя транзакция это сам перевод токенов.

И обновляем баланс

Ну и последняя транзакция для обновления баланса токенов

Функция sell

Эта функция похожа на 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

Функция обновления баланса токенов на смарт-контракте

_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 перестала работать возможность импорта своих токенов, но если у вас работает или это починили, то я покажу как это делать.

Находим кнопку импорта токена.

После вставляем свой адрес токена и все остальные параметры сами подтянуться.

После если нажмем на кнопку то нам покажет информацию о токене и наш баланс.

После импотра токена он должен отобразиться в нашем ММ, но его почему то там нет. Вот такая проблема. НО для нас это не проблема, потому что все данные мы выводим на самом сайте.

Пробуем купить 1000 токенов.

И вот у нас получилось купить, так как изменился баланс.

Так как мы вводили wei, наш токен равен gwei, то мы получили 0.000001 токен. Можно ввод улучшить и вводить токены 1 к 1.

Теперь давайте продадим наши токены.

Когда вы будете подтверждать, то у вас будет метамаск просить подтвердить перевод того количества токенов, которые вы выбрали , это выглядит примерно так:

После approve будет вызываться функция перевода токенов и по завершении этой транзакции с вас спишут токены и вы получите деньги за токены.

Вот так будет все выглядит после продажи всех токенов. Потратили эфир только на газ

Теперь купим побольше токенов и попробуем перевести их на другой кошелек.

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

Так же подтверждаем перевод, как при продаже и ждем завершение транзакции.

С нас списали токены

Теперь проверим пришли ли токены на другой кошелек.

Как видно на этом аккаунте не было ни одной транзакции (так как 10000 eth по дефолту), но у нас появилось 0.00001 DDW токена. Все работает ура!

Так же если у нас есть токены, то они отображайся при импотре но, сам токен не отображается в ММ. (Так можно проверять если что)

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

Полезные ссылки

Песочница ERC20

Документация ERC20

Ether js

Hardhat и деплой

GitHub этого проекта:

ERC 20 контракт

Frontend файлы маркета

tg: мой телеграмчик)