Blockchain

#26 컨트랙트를 통한 거래 기능 구현

Sila 2022. 7. 20. 17:58

오늘은 컨트랙트를 통해 이더리움을 지불하고 무언가 ( 토큰 등)를 사고, 혹은 그것을 다시 환불하는 기능을 구현해보도록 하자.

 

solidity의 고유한 특성 중 하나로 payable 속성이 있다. 이름을 보면 알 수 있듯 돈 (보통 이더리움)을 지불하는 기능과 연관된 속성이다.

 

사용자가 버튼을 누른다던가 해서 구매를 하면 컨트랙트가 발동을해 사용자에게서 이더리움을 받고, 물건을 준다.

 

반대로 환불을 할때도 컨트랙트가 해당 account의 물건을 제거하고 돈을 돌려주면 된다.

 

이 떄 컨트랙트가 이더리움을 주고 받을 수 있는 상태여야하는데, 이런 것들을 허용해주는 것이 payable 속성이라고 보면 된다.

 

이 속성이 있다면 tx object를 만들 때

 

tx {
    from : account,
    to : ca,
    data : 0x...,
    value : 1 ETH
}

 

이런 식으로 얼마를 보낼 건지 그 value를 tx 객체 안에 넣어줄 수 있게 된다.

 

payable 속성은 account, function에 넣어줄 수 있고, 이를 통해 이더리움을 한 곳에서 다른 곳으로 이동시키는 것이 가능하다.

 

이제 본격적으로 컨트랙트를 구현해보자. truffle에서 sol 파일로 컨트랙트를 작성해 ganache 네트워크에 배포한 후,

 

이를 리액트의 ui로 사용자에게 렌더링해주면 된다.

 

루트 디렉토리에 truffle 폴더를 만들고 그 폴더 내부에서

 

truffle init

 

을 입력해 truffle 을 셋업한다.

 

truffle.config.js에서 네트워크와 solc 버전을 잘 맞춰준다.

 

 ganache 네트워크도 구동 해준다. (키와 지갑 주소는 기록해 둘 것)

 

ganache-cli

 

1.  컨트랙트 작성

ETH를 지불해 사과 (사과 토큰이라고 생각해도 된다)를 살 수 있는 컨트랙트를 만들어보자.

 

/*  truffle/contracts/AppleShop.sol */

// SPDX-License-Identifier:MIT
pragma solidity ^0.8.15;

contract Appleshop {
    mapping (adress => uint) myApple;
    // 지갑 주소를 주면 그 지갑 주소 명의로 된 사과 수를 알려주는 매핑 myApple 선언
    
    function buyApple() public payable {
        myApple[msg.sender] +=1;
    }
    
    function refundApple(uint _applePrice) public payable {
        uint256 refund = myApple[msg.sender] * _applePrice;
        myApple[msg.sender] = 0;
        payable(msg.sender).transfer(refund);
        // account에 payable 속성이 추가되어 돈을 보내는 transfer 메소드가 사용 가능해진다.
    }
    
    function getApple() view public returns (uint) {
        return myApple[msg.sender];
    }
}

 

함수는 각각 사과(토큰이라고 생각해도 문제 없다)를 하나 주는 함수, 사과를 환불하는 함수,

 

msg.sender가 가진 사과의 갯수를 보여주는 함수가 있다.

 

사과를 하나 주는 함수 buyApple은 payable 함수가 붙어있기 때문에 추가적인 코드 없이도

 

사용자가 컨트랙트를 실행할 때 eth를 컨트랙트(ca)로 전송하는 것이 가능하다.

 

( 정확한 교환비 등에 대한 코드는 추후 프론트서버 쪽에서 구현해준다)

 

작성한 컨트랙트를 truffle을 이용해 ganache 네트워크에 배포하고 생성된 ca를 기록해둔다.

 

2.  컨트랙트  호출

 

루트 디렉토리로 돌아와 리액트 앱을 만들 셋업을 한다.

 

npx create-react-app front

 

지난 번 counter를 만들었던 것 처럼 (#23) 커스텀 훅을 이용해 지갑 주소와 web3, 컨트랙트를 가져올 것이다.

 

그 전에 truffle로 솔리디티 파일을 컴파일할 때 생성된 json파일을 프론트에서도 활용할 수 있도록 복사해오자.

 

truffle/build/contracts/AppleShop.json 파일을 front/src 폴더 안에 contracts 폴더를 생성하고 그 안에 복사하면 된다.

 

(이 안에 있는 bytecode, abi 를 사용할 수 있도록)

 

web3 라이브러리를 설치한 후, 다음과 같이 메타마스크와 상호작용할 수 있도록 커스텀 훅 useWeb3를 만들어준다.(#23 참조)

 

/*  front/src/hooks/useWeb3.js. */

import { useEffect, useState } from 'react';
import Web3 from 'web3/dist/web3.min.js'

const useWeb3 = () => {
    const [ account, setAccount ] = useState();
    const [ web3, setWeb3 ] = useState();
    
    const getRequestAccount = async () => {
        const [account] = await window.ethereum.request({
            method : 'eth_requestAccounts'
        })
        return account
    }
    
    useEffect(() => {
         (async () => {
             const account = await getRequestAccount()
             const web3 = new Web3(window.ethereum)
             
             setAccount(account)
             setWeb3(web3)
         })()
    },[])
    
    return [ web3, account ]
}

export default useWeb3;

 

그리고 ui를 렌더링해줄 AppleShop 컴포넌트를 만들어준다.

 

/*  front/src/somponents/AppleShop.js. */

import React, { useEffect, useState } from 'react';
import { Web3 } from 'web3/dist/web3.min';
import AppleShopContract from '../components/AppleShop.json'

const Appleshop = ({ web3, account }) => {
    const [ apple, setApple ] = useState()
    const [ deployed, setDeployed ] = useState()
    
    useEffect(() => {
        (async () => {
        if(!web3) return
        
        const instance = await new Web3.eth.COntract(
            AppleShopContract.abi,
            '0x664469c1073f5d75507e3b173bdfffee3da59f26' // ca
        )
        
        const currentApple = await instance.methods.getApple().call()
        // 블록체인 네트워크에서 가져온 instance의 함수 사용
        
        setApple(currentApple)
        setDeployed(instance)
        })()
    },[])
    
    return (
        <div>
            <div> 사과 가격 : 1ETH </div>
        
            <h2> 내 사과 : { apple } </h2>
            <button> buy </button>
        
            <button> refund </button>
        </div>
    )
}

export default AppleShop;

 

이 둘을 App.js에서 가져올 것이다.

 

/*  front/src/App.js. */

import React from 'react';
import useWeb3 from './hooks/uweWeb3.js'
import AppleShop from './components/AppleShop'

const App = () => {
    const [ web3, account ] = useWeb3()
    
    if(!account) return <> 메타마스크를 연결해주세요 </>
    
    return (
        <div>
            <h2> Apple Shop! </h2>
            <AppleShop web3={web3} acount={account}/>
        </div>
    )
}

export default App;

 

렌더링이 잘 된다면 이제 버튼에 각각 컨트랙트의 함수를 실행해주는 함수를 추가해보자.

 

buy button과 refund버튼에 각각 컨트랙트의 buyApple, refundApple 함수를 호출하도록 연결해주면 된다.

 

/* front/src/components/AppleShop.js. */

// ...중략

const AppleShop = ({}) => {
    // ...중략
    
    const buy = async () => {
        await deployed.methods.buyApple().send({
            from : account,
            to : // ca 입력
            value : web3.utils.toWei('1','ether')
            // 1 ether를 ca에 지불하고 apple 하나를 얻는다.
        })
    }
    
    const refund = async () => {
        const eth = web3.utils.toWei('1','ether')
        await deployed.methods.refundApple(eth).send({
            from : account,
            to : // ca 입력
        })
    }
    
    // ...중략
    
    return(
        <div>
            // ...중략
            <button onClick={buy}> buy </button>
            <button onClick={refund}> refund </button>
        </div>
    )
}

export default AppleShop;

 

이제 다시 리액트 ui를 켜서 구입과 환불 버튼을 누르고 truffle console에서 ca의 이더리움 잔액을 조회하면

 

그 액수가 변하는 것을 확인할 수 있다.