Dapp의 프런트엔드 react 애플리케이션을 처음부터 구축: 부분(3/4)


이 부분은 내가 먼저 이 시리즈의 강좌를 쓰게 한 주요 원인이다.solidity에 대한 강좌는 쉽게 찾을 수 있고 충분하지만 문제는 전방을 어떻게 구축하는지, 배치된 스마트 계약과 어떻게 상호작용하는지, 그리고 Heroku 등 클라우드 플랫폼에서 어떻게 위탁 관리하는지에 관한 좋은 강좌를 찾는 것이다.

This section requires you to have some basic understanding of react such as components, useState, useEffect, etc. There are a ton of good resources on react, so please check them out if you are not familiar with these concepts.

This project is hosted here.
Link to Github Repo of the project here. This will serve as a reference for you, if at some point you are not able to follow the tutorial.


react 응용 프로그램 만들기


루트 디렉토리에서 실행
npx create-react-app frontend
이것은frontend라는 react 프로그램을 만들 것입니다.
npm install @alch/alchemy-web3
npm install react-bootstrap [email protected]
npm install dotenv
npm install react-icons --save
이것은 우리 프로젝트를 위해 필요한 모든 소프트웨어 패키지를 설치할 것이다.

Alchemy Web3 is a wrapper around Web3.js which provides some enhanced API methods.


설치 프로그램

create-react-app 우리가 벗어날 수 있을 때 샘플 코드로 시작합니다.
src 폴더에서 삭제App.test.jslogo.svgreportWebVitals.jssetupTests.js.index.jsApp.js 파일을 다음과 같이 변경합니다.

색인js


import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';

ReactDOM.render(
  <React.StrictMode>
    <App />
  </React.StrictMode>,
  document.getElementById('root')
);

응용 프로그램.js


import './App.css'

function App() {
  return (
    <>
    </>
  )
}

export default App
이제 frontend/public/index.html로 이동하여 부트 CDN을 헤드 탭에 추가합니다.
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css" integrity="sha384-BVYiiSIFeK1dGmJRAkycuHAHRg32OmUcww7on3RYdg4Va+PmSTsz/K68vbdEjh4u" crossorigin="anonymous">
프런트엔드 디렉토리에 .env 파일을 만들고 이전 섹션에서 사용한 Alchemy API 키를 추가합니다.
REACT_APP_ALCHEMY_KEY = <YOUR_API_KEY>
지금 우리는 모두 우리의 프로젝트를 건설할 준비가 되어 있다.

인공품을 얻다


우리는 그와 상호작용을 하기 위해 스마트 계약의 특정 데이터를 전방에 배치해야 한다.이러한 데이터를 얻기 위해 배포 스크립트SimpleBank/scripts/deploy.js를 수정합니다.

배치하다.js


const { ethers, artifacts } = require('hardhat')

async function main() {
  const [deployer] = await ethers.getSigners()

  console.log('Deploying contracts with the account: ', deployer.address)

  const Bank = await ethers.getContractFactory('Bank')
  const bank = await Bank.deploy()

  console.log('Bank address: ', bank.address)

  saveArtifacts(bank)
}

// save the address and artifact of the deployed contract in the frontend
const saveArtifacts = (bank) => {
  const fs = require('fs')
  const artifactDir = __dirname + '/../frontend/src/artifacts'

  if (!fs.existsSync(artifactDir)) {
    fs.mkdirSync(artifactDir)
  }

  const bankArtifact = artifacts.readArtifactSync('Bank')

  const artifact = {
    address: bank.address,
    abi: bankArtifact.abi,
  }

  console.log('Saving artifacts to: ', artifactDir)

  fs.writeFileSync(artifactDir + '/Bank.json', JSON.stringify(artifact))
}

main()
  .then(() => process.exit(0))
  .catch((error) => {
    console.error(error)
    process.exit(1)
  })

우리는 새로운 함수 saveArtifacts() 를 추가했고, main 마지막에 그것을 호출하면 계약을 배치할 때 배치된 계약 addressabifrontend/src/artifacts 에 저장할 것이다.
우리는 이 데이터를 얻기 위해 계약을 다시 배치해야 한다.
npx hardhat run .\scripts\deploy.js --network rinkeby
이것은 frontend/srcBank.json 파일이 있는 작업 폴더를 만들 것입니다.

효용 함수


이것은 우리가 코드를 작성하여 배치된 스마트 계약과 상호작용하는 부분이다.frontend/srcutils라는 폴더를 만들고 다음 파일을 추가합니다.이것은 우리가 모든 함수를 쓴 곳이다.

지갑 기능.js


export const connectWallet = async () => {
  if (window.ethereum) {
    try {
      const addresses = await window.ethereum.request({
        method: 'eth_requestAccounts',
      })
      const obj = {
        address: addresses[0],
        connected: true,
        status: '',
      }
      return obj
    } catch (error) {
      return {
        address: '',
        connected: false,
        status: error.message,
      }
    }
  } else {
    return {
      address: '',
      connected: false,
      status: (
        <a href="https://metamask.io/" target="_blank" rel="noreferrer">
          {' '}
          You need to install Metamask
        </a>
      ),
    }
  }
}

export const getWalletStatus = async () => {
  if (window.ethereum) {
    try {
      const addresses = await window.ethereum.request({
        method: 'eth_requestAccounts',
      })
      if (addresses.length > 0) {
        return {
          address: addresses[0],
          connected: true,
          status: '',
        }
      } else {
        return {
          address: '',
          connected: false,
          status: '🦊 Please connect to Metamask Wallet',
        }
      }
    } catch (error) {
      return {
        address: '',
        connected: false,
        status: error.message,
      }
    }
  } else {
    return {
      address: '',
      connected: false,
      status: (
        <a href="https://metamask.io/" target="_blank" rel="noreferrer">
          {' '}
          You need to install Metamask
        </a>
      ),
    }
  }
}
여기에 우리는 두 개의 함수가 있는데, 그것들은 모두 매우 비슷하기 때문에, 나는 이 두 함수의 관건적인 부분을 함께 해석해 보려고 한다.
if (window.ethereum) {
    //does someting
} else {
    return {
      address: '',
      connected: false,
      status: (
        <a href="https://metamask.io/" target="_blank" rel="noreferrer">
          {' '}
          You need to install Metamask
        </a>
      ),
    }
  }
여기 window.ethereum 브라우저에 지갑이 있는지 확인하십시오.만약 그렇다면, 계속하십시오. 그렇지 않으면 메타마스크 지갑을 설치하는 설명을 되돌려 드리겠습니다.
try {
      const addresses = await window.ethereum.request({
        method: 'eth_requestAccounts',
      })
      const obj = {
        address: addresses[0],
        connected: true,
        status: '',
      }
      return obj
    } catch (error) {
      return {
        address: '',
        connected: false,
        status: error.message,
      }
    }
현재 사용 가능한 지갑이 있으면, 우리는 const addresses = await window.ethereum.request({ method: 'eth_requestAccounts', }) 지갑을 사용하여 계정을 응용 프로그램에 연결할 것을 요청합니다.연결이 성공하면, 우리는 모든 연결의 주소를 얻어서 첫 번째 주소로 돌아갈 것입니다. 그렇지 않으면 오류 메시지를 되돌려줍니다.getWalletStatus 함수의 작업 원리도 기본적으로 같다.지갑에 연결된 계정을 검사합니다. 없으면 연결 요청에 응답합니다.

은행 직능.js


const alchemyKey = process.env.REACT_APP_ALCHEMY_KEY
const { createAlchemyWeb3 } = require('@alch/alchemy-web3')
const web3 = createAlchemyWeb3(alchemyKey)

const { abi, address } = require('../artifacts/Bank.json')

export const depositEth = async (amount) => {
  if (parseFloat(amount) <= 0) {
    return {
      status: 'Please enter a valid amount',
    }
  }

  window.contract = await new web3.eth.Contract(abi, address)

  const WeiAmount = web3.utils.toHex(web3.utils.toWei(amount, 'ether'))

  const txParams = {
    to: address,
    from: window.ethereum.selectedAddress,
    value: WeiAmount,
    data: window.contract.methods.deposit().encodeABI(),
  }

  try {
    await window.ethereum.request({
      method: 'eth_sendTransaction',
      params: [txParams],
    })
    return {
      status: 'Transaction Successful. Refresh in a moment.',
    }
  } catch (error) {
    return {
      status: 'Transaction Failed' + error.message,
    }
  }
}

export const withdrawEth = async (amount) => {
  window.contract = await new web3.eth.Contract(abi, address)

  const WeiAmount = web3.utils.toWei(amount, 'ether')

  const txParams = {
    to: address,
    from: window.ethereum.selectedAddress,
    data: window.contract.methods.withdraw(WeiAmount).encodeABI(),
  }

  try {
    await window.ethereum.request({
      method: 'eth_sendTransaction',
      params: [txParams],
    })
    return {
      status: 'Transaction Successful. Refresh in a moment',
    }
  } catch (error) {
    return {
      status: 'Transaction Failed' + error.message,
    }
  }
}

export const getBalance = async () => {
  window.contract = await new web3.eth.Contract(abi, address)

  const reqParams = {
    to: address,
    from: window.ethereum.selectedAddress,
    data: window.contract.methods.getBalance().encodeABI(),
  }

  try {
    const response = await window.ethereum.request({
      method: 'eth_call',
      params: [reqParams],
    })
    const exhRate = await exchangeRate()
    const balance = web3.utils.fromWei(response, 'ether')
    return {
      inr: balance * exhRate,
      eth: balance,
      exhRate: exhRate,
    }
  } catch (error) {
    return {
      status: 'Check Failed ' + error.message,
    }
  }
}

export const exchangeRate = async () => {
  const response = await fetch(
    'https://api.coingecko.com/api/v3/simple/price?ids=ethereum&vs_currencies=inr',
  )
  const data = await response.json()
  return data.ethereum.inr
}

이 기능들은 응용 프로그램의 은행 기능을 처리할 것이다.
const alchemyKey = process.env.REACT_APP_ALCHEMY_KEY
const { createAlchemyWeb3 } = require('@alch/alchemy-web3')
const web3 = createAlchemyWeb3(alchemyKey)

const { abi, address } = require('../artifacts/Bank.json')
우선, out API 키를 사용하여 웹 3 실례를 초기화하고 address 에서 abiartifacts/Bank.json 를 가져옵니다.
if (parseFloat(amount) <= 0) {
    return {
      status: 'Please enter a valid amount',
    }
}
그리고 우리는 depositEth 기능을 가지게 되었는데, 그것은 예금에 쓰일 것이다.amount 매개 변수는 문자열 형식을 사용하기 때문에float로 변환하고 0보다 크도록 합니다.
window.contract = await new web3.eth.Contract(abi, address)
여기서 우리는 abiaddress를 사용하여 계약 실례를 얻었다.
const WeiAmount = web3.utils.toHex(web3.utils.toWei(amount, 'ether'))
수신된 매개 변수는 이더리움의 최소값(1이더리움=10^18 Wei)으로 변환됩니다.그런 다음 16진수로 변환하여 트랜잭션 매개 변수로 사용합니다.
const txParams = {
    to: address,
    from: window.ethereum.selectedAddress,
    value: WeiAmount,
    data: window.contract.methods.deposit().encodeABI(),
  }
거래 매개 변수는 to: <contract_address>,from: <address_of_account_connected_to_app>,value: <amount_to_be_deposited>,data: <call_to_contract_function>,withdrawEth를 포함한다.
try {
    await window.ethereum.request({
      method: 'eth_sendTransaction',
      params: [txParams],
    })
    return {
      status: 'Transaction Successful. Refresh in a moment.',
    }
  } catch (error) {
    return {
      status: 'Transaction Failed' + error.message,
    }
  }
}
마지막으로, 우리는 사무를 발송한다.성공하면 성공 메시지를 되돌려줍니다. 그렇지 않으면 오류 메시지를 되돌려줍니다.getBalance 말 그대로 은행에서 지갑으로 금액을 돌려받는 기능이 있다.이것은 앞의 것과 거의 같습니다. 다만, 우리는 amount를 사무 매개 변수로 보내지 않고, 매개 변수로call 함수에 보냅니다.method: 'eth_call' 함수는 호출된 금액의 사용 가능한 잔액을 되돌려줍니다.이곳의 주요 차이점은 우리가 사용하는 데 있다. 왜냐하면 그것은 단지 보기 함수이기 때문이다.우리는api에 대한 간단한 exchangeRate 요청을 사용하여 fetch 에서 ETH 까지의 현재 환율을 가져오고 두 가지 형식으로 잔액을 되돌려줍니다.

구성 요소 및 CSS

INRcomponents 폴더를 만들고 구성 요소를 추가합니다.
이러한 구성 요소의 구축은 매우 기본적이고 일부 안내 요소를 가지고 있으며 CSS가 가장 작기 때문에 우리는 그것들에 대해 너무 많은 상세한 소개를 하지 않을 것이다.

탐색 모음.jsx


import { Container, Navbar } from 'react-bootstrap'

export default function NavBar() {
  return (
    <div>
      <Navbar bg="dark" variant="dark">
        <Container>
          <Navbar.Brand>SimpleBank & Co.</Navbar.Brand>
        </Container>
      </Navbar>
    </div>
  )
}
이것은 브랜드가 있는 간단한 내비게이션 표시줄로 우리는 응용 프로그램에서 그것을 사용할 것이다.

상태 상자.jsx


import { Alert, Container, Row, Col } from 'react-bootstrap'

const StatusBox = ({ status }) => {
  return (
    <Container
      className={status.length === 0 ? 'status-box-null' : 'status-box'}
    >
      <Row className="justify-content-center">
        <Col lg="6">
          <Alert variant="danger">{status}</Alert>
        </Col>
      </Row>
    </Container>
  )
}

export default StatusBox
문제가 발생하면 이 상태 표시줄은 사용자에게 알리는 데 사용됩니다.

은행 정보.jsx


import { IoIosRefresh } from 'react-icons/io'
import {
  Button,
  Container,
  FormControl,
  InputGroup,
  Col,
  Row,
  Alert,
} from 'react-bootstrap'
import { useState, useEffect } from 'react'

import { getBalance, depositEth, withdrawEth } from '../utils/bankFunctions'

const BankInfo = ({ onAccoutChange }) => {
  const [balanceINR, setBalanceINR] = useState(0)
  const [balanceETH, setBalanceETH] = useState(0)
  const [showDeposit, setShowDeposit] = useState(false)
  const [showWithdraw, setShowWithdraw] = useState(false)
  const [exhRate, setExhRate] = useState(0)
  const [inputINR, setInputINR] = useState(null)
  const [inputETH, setInputETH] = useState(null)
  const [response, setResponse] = useState(null)

  const handleShowDeposit = () => {
    setShowDeposit(true)
  }

  const handleShowWithdraw = () => {
    setShowWithdraw(true)
  }

  const handleClose = () => {
    setShowDeposit(false)
    setShowWithdraw(false)
    setInputINR(null)
    setInputETH(null)
    setResponse(null)
  }

  const checkBalance = async () => {
    const balance = await getBalance()
    setBalanceETH(balance.eth)
    setBalanceINR(balance.inr)
    setExhRate(balance.exhRate)
  }

  const handleInoutINR = (e) => {
    setInputINR(e.target.value)
    setInputETH((e.target.value / exhRate).toFixed(18))
  }

  const handleDeposit = async () => {
    setResponse(null)
    const deposit = await depositEth(inputETH.toString())
    setInputETH(null)
    setInputINR(null)
    setResponse(deposit.status)
  }

  const handleWithdraw = async () => {
    if (inputINR > balanceINR) {
      setResponse('Insufficient Balance')
    } else {
      setResponse(null)
      const withdraw = await withdrawEth(inputETH.toString())
      setInputETH(null)
      setInputINR(null)
      setResponse(withdraw.status)
    }
  }

  useEffect(() => {
    checkBalance()
  }, [onAccoutChange])

  return (
    <>
      <div className="balance-card">
        <h1>
          Your Balance
          <IoIosRefresh className="refresh-icon" onClick={checkBalance} />
        </h1>
        <h3 className="balance-inr">{parseFloat(balanceINR).toFixed(2)} INR</h3>
        <h3 className="balance-eth">{parseFloat(balanceETH).toFixed(4)} ETH</h3>
        {!showDeposit && !showWithdraw && (
          <div className="btn-grp">
            <Button
              className="deposit-btn"
              variant="success"
              onClick={handleShowDeposit}
            >
              Deposit
            </Button>
            <Button
              className="withdraw-btn"
              variant="warning"
              onClick={handleShowWithdraw}
            >
              Withdraw
            </Button>
          </div>
        )}
        {showDeposit || showWithdraw ? (
          <>
            <Container>
              <Row className="justify-content-center ">
                <Col md="6">
                  <InputGroup className="amount-input">
                    <FormControl
                      placeholder="Enter Amount in INR"
                      type="number"
                      value={inputINR > 0 ? inputINR : ''}
                      onChange={handleInoutINR}
                    />
                    <InputGroup.Text>INR</InputGroup.Text>
                  </InputGroup>
                </Col>
              </Row>
              <Row className="justify-content-center">
                <Col md="6">
                  <InputGroup className="amount-input">
                    <FormControl
                      placeholder="ETH Equivalent"
                      type="number"
                      value={inputETH > 0 ? inputETH : ''}
                      readOnly
                    />
                    <InputGroup.Text>ETH</InputGroup.Text>
                  </InputGroup>
                </Col>
              </Row>
            </Container>
            <div className="btn-grp">
              <Button
                className="deposit-btn"
                variant="success"
                onClick={showDeposit ? handleDeposit : handleWithdraw}
              >
                {showDeposit ? 'Deposit' : 'Withdraw'}
              </Button>
              <Button
                className="withdraw-btn"
                variant="info"
                onClick={handleClose}
              >
                Close
              </Button>
            </div>
            {response && (
              <Container>
                <Row className="justify-content-center">
                  <Col md="6">
                    <Alert variant="info">{response}</Alert>
                  </Col>
                </Row>
              </Container>
            )}
          </>
        ) : null}
      </div>
    </>
  )
}

export default BankInfo
이 부품은 인도 루피와 에티오피아 선령으로 계좌 잔액을 표시하고 예금과 인출을 처리한다.UI를 부드럽게 실행하는 데 사용되는 상태 변수 집합입니다.

BTN을 연결합니다.jsx


import { useState, useEffect } from 'react'
import { Button } from 'react-bootstrap'

import { connectWallet, getWalletStatus } from '../utils/walletFunctions'

export const ConnectBtn = ({ setStatus, setConnected, setWallet }) => {
  const [walletAddress, setWalletAddress] = useState('')

  const handleConnect = async () => {
    const walletResponse = await connectWallet()
    setStatus(walletResponse.status)
    setConnected(walletResponse.connected)
    setWalletAddress(walletResponse.address)
    setWallet(walletResponse.address)
  }

  useEffect(() => {
    const checkWalletStatus = async () => {
      const walletResponse = await getWalletStatus()
      setStatus(walletResponse.status)
      setConnected(walletResponse.connected)
      setWalletAddress(walletResponse.address)
      setWallet(walletResponse.address)
    }

    const walletListener = () => {
      if (window.ethereum) {
        window.ethereum.on('accountsChanged', (accounts) => {
          checkWalletStatus()
        })
      }
    }

    checkWalletStatus()
    walletListener()
  }, [setConnected, setStatus, setWallet])

  return (
    <div className="connect-btn">
      <Button variant="primary" onClick={handleConnect}>
        {walletAddress.length === 0
          ? 'Connet Wallet'
          : 'Connected: ' +
            String(walletAddress).substring(0, 6) +
            '...' +
            String(walletAddress).substring(38)}
      </Button>
    </div>
  )
}

export default ConnectBtn
frontend/src 구성 요소는 지갑과 응용 프로그램의 연결 상태를 표시하고 연결되지 않았을 때 연결을 요청하는 데 사용됩니다.

바닥글jsx


import { IoIosInformationCircleOutline } from 'react-icons/io'

const Footer = () => {
  return (
    <footer className="footer">
      <p className="footer-text">
        <IoIosInformationCircleOutline className="info-icon" /> This application
        in running on rinkeby test network. Please only use test Ethers.
      </p>
    </footer>
  )
}

export default Footer
사용자에게 테스트 이더리움만 사용하도록 경고하는 간단한 스크립트 구성 요소

색인css


body {
  margin: 0;
  font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
    'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
    sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
}

code {
  font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
    monospace;
}

.amount-input {
  margin: 10px;
}

.balance-card {
  margin-top: 20px;
  text-align: center;
}

.balance-eth {
  color: rgb(19, 202, 28);
}

.balance-inr {
  color: rgb(25, 214, 214);
}

.btn-grp {
  margin: 20px;
}

.deposit-btn {
  margin-right: 20px;
}

.footer {
  position: fixed;
  left: 0;
  bottom: 0;
  width: 100%;
  padding: 5px;
  background-color: teal;
  color: white;
  text-align: center;
}

.footer-text {
  font-size: large;
}

.info-icon {
  font-size: x-large;
}

.withdraw-btn {
  margin-left: 20px;
}

.connect-btn {
  text-align: center;
  margin: 20px;
}

.refresh-icon {
  margin-left: 10px;
  font-size: 28px;
  cursor: pointer;
}

.status-box {
  margin-top: 20px;
  text-align: center;
}

.status-box-null {
  display: none;
}

스타일 디자인은 별다른 것이 없다. 단지 모든 구성 요소를 정확하게 맞추기 위해서이다.

응용 프로그램에 모든 구성 요소를 추가합니다.js


응용 프로그램.js


import { useState } from 'react'

import NavBar from './components/NavBar'
import ConnectBtn from './components/ConnectBtn'
import StatusBox from './components/StatusBox'
import BankInfo from './components/BankInfo'
import Footer from './components/Footer'

function App() {
  const [status, setStatus] = useState('')
  const [connected, setConnected] = useState()
  const [wallet, setWallet] = useState()
  return (
    <>
      <NavBar />
      <ConnectBtn
        setStatus={setStatus}
        setConnected={setConnected}
        setWallet={setWallet}
      />
      <StatusBox status={status} />
      {connected && <BankInfo onAccoutChange={wallet} />}
      <Footer />
    </>
  )
}

export default App
응용 프로그램에 모든 구성 요소를 ConnectBtn 추가하여 보여 줍니다.
우리의 react 프로그램은 이것으로 끝납니다.다음 명령을 실행하여localhost에서 실행합니다.
npm start
만약 일이 예상대로 진행되지 않았다면github repohere를 참고하십시오.
다음 강좌에서heroku에서 이 프로그램을 위탁 관리하는 방법을 볼 수 있습니다.클릭하십시오.

좋은 웹페이지 즐겨찾기