본문 바로가기

블록체인_9기/⛓ BlockChain

64강_230925_Blockchain(스마트 컨트랙트, 간단한 카운터 제작, react와 metamask를 사용한 트랜잭션 생성)

728x90

 

 

 


스마트 컨트랙트(Smart contract)

 

출처 : https://upbitcare.com/academy/education/blockchain/70

스마트 컨트랙트는 블록체인 기술을 이용해 제 3의 인증기관 없이 개인 간 계약이 이루어질 수 있도록 하는 기술이다. 자세하게 설명하자면, 계약상의 급부와 반대급부를 프로토콜화해 소프트웨어 및 하드웨어에 미리 저장하고, 해당 계약을 이행하는 과정에서 조건 충족 여부에 대한 판간을 인간이 아닌 컴퓨터 등의 기계가 대신 실행함으로써 제 3의 인증기관이 필요없도록 하는 개념이다.

 

이더리움에서 스마트 컨트랙트가 구현되기 전까진 디지털 자산이 화폐나 통화의 기능에 집중되어 있었다. 예를 들어, 비트코인은 단순한 비트코인의 입출금내역 정도만을 블록체인에 기록할 수 있었고, 디지털 자신의 거래 이상의 동작을 블록체인 상에서 구현하는 것은 어려웠다. 하지만, 스마트 컨트랙트가 나오고 디지털 자산은 결제 수단의 기능을 넘어, 블록체인의 생태계가 확장되는 기반이 되었다. 스마트 컨트랙트는 블록테인을 활용했기 때문에 뛰어난 보안성을 가지면서 제 3의 기관이 필요하지 않다는 점에서 블록체인이 추구하는 탈중앙화를 구현했다.

 

스마트 컨트랙트의 기본 성질

스마트 컨트랙트 == 계약 코드를 통해 실행되는 전산화 계약

  • 관측가능성 (observability) : 스마트 컨트랙트는 서로의 계약 이행 가능성을 관찰하거나 성과를 입증할 수 있어야 한다.
  • 검증가능성 (verifiability) : 계약을 이행하거나 위반할 경우 계약 당사자들이 이를 알 수 있어야 한다.
  • 프라이버시 (privity) : 계약 내용은 계약에 필요한 당사자만 알 수 있어야 한다.
  • 강제 가능성 (enforceability) : 계약이 이루어질 수 있도록 구속력이 있어야 한다.

 

컨트랙트 실행

  • 데이터 영구 저장 가능
    • 스마트 컨트랙트는 데이터를 블록체인에 영구적으로 저장할 수 있다. 저장된 데이터는 변경할 수 없다.
    • contract storage에 데이터를 저장, 유지할 수 있다.
  • 클래스 문법과 유사
    • solidity에서 스마트 컨트랙트 구조는 Javascript의 클래스 문법과 유사하다.
    • new 키워드를 통해 생성된 JS의 인스턴스들은 다른 메모리 주소를 참조하기 때문에 동일한 객체가 아니다.
    • solidity에서 컨트랙트는 컴파일된 코드의 내용이 EVM을 통해 실행되고 CA가 생성될때 solidity 코드의 내용으로 인스턴스가 한 번 생성된다.
// Javascript
class Counter {
  value: number;
  constractor() {}
  setValue() {}
  getValue() {}
}
const _counter = new Counter();
{ value: 1; }
const _counter2 = new Counter();
{ value: 1; }

_counter.setVlaue(2);

_counter.value == 2;
_counter2.value == 1;

// solidity
// 솔리디티 버전
pragma solidity ^0.8.0;

// 컨트랙트 코드
contract Counter{
    uint256 value;

    constractor(){}

    function setValue(uint256 _value) public {
        value = _value;
    }

    function getValue() public view returns (uint256) {
        // 상태변수를 변경하지 않고 조회하기 위해 view를 쓴다.
        return value;
    }
}
  • 싱글톤 패턴
    • 인스턴스 객체를 하나 생성해서 어디서든 생성한 인스턴스만 탐조하는 디자인 패턴이다. 이후에 생성된 인스턴스는 CA로 참조해 컨트랙트에 접근해서 사용하는 데이터는 같은 데이터를 참조하게 된다.

 

스마트 컨트랙트 배포과정

  1. 계약 코드를 작성 : 스마트 컨트랙트 코드를 작성한다.
  2. 계약 코드 컴파일 : Solidity 컴파일러를 사용하여 코드를 컴파일한다.
  3. 스마트 컨트랙트 배포 : 컨트랙트를 블록체인에 배포한다. 컨트랙트의 바이트 코드를 배포하는 트랜잭션을 생성한다.
  4. node에 전송 : 트랜잭션을 블록체인 노드에 전송한다.
  5. 블록 생성 : 마이너들이 트랜잭션을 검증하고, 수락되면 블록에 포함시킨다.
  6. 계정(CA) 생성 : 배포 시 컨트랙트 주소(CA)가 생성된다. 이 주소는 컨트랙트와 상호작용 하기 위해 사용된다.
  7. 인스턴스 생성 : 컨트랙트 주소와 상호 작용하여 컨트랙트의 인스턴스를 생성한다. 이 인스턴스는 블록체인에 배포된 컨트랙트를 참조한다.
  8. 데이터 저장 : 컨트랙트 함수를 사용하여 데이터를 저장하고 검색한다.

 

스마트 컨트랙트 가스비

  • 스마트 컨트랙트의 코드가 실행될 때 EVM에서 연산을 얼마나 할지와 네트워크의 환경을 기준으로 수수료 가스가 측정된다.
  • 네트워크 상황과 코드의 복잡성에 따라서 연산한다.
    • 이것을 우리가 직접하기는 어렵다. 가스비 추정을 위한 메소드가 존재해 추정 정도는 가능하다.
  • 상태 변수의 값을 조회하는 함수는 연산을 하는 과정이 없기 때문에 가스비를 필요로 하지 않는다.
  • 상태 변수의 값을 변경하는 경우는 연산으 포함되어 연산에 따른 가스비를 지불해야 한다.
    • 한정된 네트워크 자원을 사용한다.
  • 연산을 하는 과정에서 코드의 무한루프를 연산하게 되면, 과도한 가스비 발생을 방지하기 위해 gasLimit가 초과되면 트랜잭션이 블록에 담기지 않는다.

 

개발 환경 구축

# solc 설치
npm i solc@0.8.13

# ganache-cli 설치
npm i ganache-cli
# ganache-cli 열기
npx ganache-cli

 

 

 

 


이더리움 카운터 컨트랙트 제작

HTML로 Ethereum 스마트 컨트랙트를 배포하고 상호작용해 카운터를 구현하는 웹 어플리케이션을 제작하고자 한다. 먼저, 사용가능한 계정의 목록을 표시하고, 스마트 컨트랙트를 배포한 뒤, 컨트랙트의 함수를 호출해 카운터 기능을 구현하고자 한다.

개발 환경 구축

# solc 설치
npm i solc@0.8.13

# ganache-cli 설치
npm i ganache-cli
# ganache-cli 열기
npx ganache-cli

 

카운트 기능을 할 스마트 컨트랙트 Counter 정의

// SPDX-License-Identifier: MIT

// Solidity 컴파일러 버전을 지정. Solidity 0.8.13 버전에서 작성
pragma solidity ^0.8.13;

// Counter 라는 스마트 컨트랙트를 정의
contract Counter{
    // value : 256비트, 부호 없는 정수형 상태 변수를 선언.
    // 초기값은 0으로 지정된다.
    uint256 value;

    // 컨트랙트의 생성자
    constructor(){}

    // setValue : _256비트, 부호 없는 정수형 매개변수 _value를 받는다.
    // public : 이 함수가 누구에게나 호출이 가능함을 명시한다.
    function setValue(uint256 _value) public{
        // 상태 변수 변경
        value = _value;
    }
    
    // getValue : 상태변수 value의 값을 읽는 함수이다.
    // public view : 이 함수는 상태를 읽기만 하고 변경하지 않는다.
    // returns(uint256) : 이 함수는 부호 없는 정수형 값을 반환한다.
    function getValue()public view returns(uint256){
        return value;
    }
}

 

이더리움의 트랜잭션을 배포하기 위한 바이트 코드를 생성한다.

solc를 이용하여 solidity 컨트랙트를 사용한다.

# bin 파일과 abi파일을 생성한다.
# npx solc --bin --abi 파일의 경로

npx solc --bin --abi Counter.sol

 

Web3 라이브러리를 사용하기 위해 body태그 상단에 스크립트를 넣는다.

<script src="https://cdn.jsdelivr.net/npm/web3@1.10.0/dist/web3.min.js"></script>

 

사용가능한 이더리움 계정을 조회한다.

    // 이더리움 네트워크 연결. 현재는 로컬 가나슈 테스트 네트워크에 연결
    // ganache == http://127.0.0.1:8545
    const web3 = new Web3("http://127.0.0.1:8545");

    // getAccount == 네트워크의 계정들 조회
    web3.eth.getAccounts().then((data) => {
      let items = "";
      data.forEach(async (i) => {
        // getBalance : 계정의 잔액 조회. Wei단위
        const balance = await web3.eth.getBalance(i);

        // ETH단위로 단위변경.
        // fromWei : wei 단위를 ether 단위로 변경
        const eth_balance = await web3.utils.fromWei(balance);
        items += `<li>${i} : ${eth_balance}ETH</li>`;
        Accounts.innerHTML = items;
      });
    });

 

입력한 계정의 트랜잭션을 생성(배포)한다.

    // 컨트랙트를 배포할 때 수수료를 지불할 컨트랙트 배포자 계정
    // bin 컴파일된 컨트랙트 코드 내용
    // 트랜잭션 생성
    sendTransactionBtn.onclick = () => {
      web3.eth
        .sendTransaction({
          // 컨트랙트 배포자 계정
          from: useAccount.value,
          // 거래 gas 제한량. 해당 값보다 더 많은 가스를 소비해야 한다면 트랜잭션은 실패한다.
          gas: "3000000",
          // 컴파일된 컨트랙트 바이트 코드
          data: contract.value,
        })
        .then(console.log);
      // 컨트랙트 배포 후
      // 트랜잭션 처리가 되면 응답으로 컨트랙트 주소를 주는데 CA
      // 컨트랙트 참조에 사용하는 주소 CA
      // 결과 : 0xa814d3153aAf6efd5C8237EF48104A4bD0A92e49
    };

 

스마트 컨트랙트 상호작용을 위해 abi를 정의한다.

    // 배포한 컨트랙트 실행
    // abi를 사용해서 컨트랙트 코드를 정의하고 실행
    // interface
    // 코드를 활성화 시켜서 사용할 때 정의한 구조대로 사용하기 위해서
    const abi = [
      // 생성자 함수를 의미. 스마트 컨트랙트를 배포할 때 한 번만 실행된다.
      //// inputs : 매개변수. 전달할 매개변수가 없으므로 빈 배열
      //// stateMutability === nonpayable : 이더리움을 받지 않는 상태 전환 함수
      //// stateMutability === payable : 이더를 전달 받을 수 있는 상태 전환 함수
      //// type == constructor : 생성자 함수의 타입
      { inputs: [], stateMutability: "nonpayable", type: "constructor" },

      // getValue 함수
      {
        //// inputs : 매개변수를 받지 않으므로 빈 문자열
        inputs: [],
        //// name : 함수의 이름. 지금은 getValue
        name: "getValue",
        //// outputs : 함수의 출력 내용. 반환하는 값
        ////// internalType : 상태 변수의 함수 값에 대한 타입. uint256로 반환하는 값은 부호없는 256비트 정수형이다.
        ////// name : 사용하는 매개변수의 이름
        outputs: [{ internalType: "uint256", name: "", type: "uint256" }],
        //// stateMutability == view : 상태 변경을 하지 않고 view 속성. 조회만 한다.
        stateMutability: "view",
        //// type == function 함수 타입
        type: "function",
      },
      // setValue 함수
      {
        ////// internalType : 함수의 값에 대한 타입
        ////// name : 사용하는 매개변수 이름. _value
        inputs: [{ internalType: "uint256", name: "_value", type: "uint256" }],
        //// name : 함수의 이름. setValue
        name: "setValue",
        //// outputs : 함수의 출력. 없으니까 빈배열
        outputs: [],
        //// stateMutability === nonpayable : 이더리움을 받지 않는 상태 전환 함수
        stateMutability: "nonpayable",
        //// type == function 함수 타입
        type: "function",
      },
    ];

 

카운트의 값을 조회하는 함수 getValue

web.eth.abi.encodeFunctionCall() 함수로, 스마트 컨트랙트의 함수 내용을 호출한다. 이 함수는 호출한 함수와 전달할 매개변수의 내용의 결과를 16진수의 문자열로 인코딩하여 반환한다. 첫 번째 매개변수로, 스마트 컨트랙트의 abi 인터페이스의 내용을 받아온다. 두 번째 매개변수로, 함수에 전달할 매개변수를 전달한다. 해당 함수는 카운트의 값을 조회하는 함수를 만들 예정으로, abi 인터페이스의 두 번째 인터페이스를 불러온다. 해당 인터페이스는 매개변수를 받지 않으므로, 두 번째 매개변수는 빈 배열을 전달한다. 따라서 web.eth.abi.encodeFunctionCall(abi[1], [])를 입력한다.

web.eth.call() 함수로 이더리움 스마트 계약과 상호작용하기 위한 기능이다. 스마트 계약과 상호작용하며 블록체인 상태를 변경하지 않고 스마트 계약에서 데이터를 검색하는 기능을 수행한다. 이 함수의 속성인 'to'는 호출하려는 스마트 계약의 주소를 지정한다. CA를 넣는다. 'data'속성은 스마트 계약에서 수행하려는 함수 호출을 나타내는 인코딩된 데이터를 지정한다. 

web.utils.toBN(data).toString(10)으로 16진수의 결과값을 10진수로 변환한다. 

    // 카운트의 값을 조회하는 함수
    const getValue = async () => {
      // encodeFunctionCall
      // 첫 번째 매개변수 : abi의 내용. 실행시킬 함수의 interface
      // 두 번째 매개변수 : 함수에 전달할 매개변수 값

      // encodeFunctionCall : 16진수 문자열을 반환
      // 컨트랙트 함수의 내용과 우리가 전달할 매개변수를 전달해서 해시코드로 변환
      // EVM애서 실행을 시킨다.
      const getCodeHash = await web3.eth.abi.encodeFunctionCall(abi[1], []);
      console.log("~~~",getCodeHash);

      // call : 읽기 전용. 원격 프로시저 호출. 값을 조회한다.
      const data = await web3.eth.call({
        // CA 넣기
        to: "0x56b7639290C4421fBDbc53c063DD94058e25a12f",
        data: getCodeHash,
      });
      console.log(data);
      // data에는 16진수로 변환된 값이 넘어오는데 10진수로 바꿔서 사용해보자
      const result = await web3.utils.toBN(data).toString(10);
      console.log(result);
      counterValue.innerHTML = result;
      return parseInt(result);
    };
    // 첫 로딩 시 상태 변수를 조회했고
    getValue();

	// 조회 버튼을 클릭하면 조회
    callBtn.onclick = getValue;

 

카운터 동작을 하는 setValue 함수 (증가, 감소)

getValue와 마찬가지로 web.eth.abi.encodeFunctionCall() 함수를 이용하여 상태 변수를 변경한다. abi의 세 번째 객체인 setValue를 사용하고, 해당 함수에 매개변수로 _getValue값을 추가(setValue 함수) 또는 감소(setValue2 함수)하는 동작을 수행한다. 

tx 트랜잭션 객체를 생성하여, 트랜잭션을 생성하는 계정, 스마트 컨트랙트 주소 등의 컨트랙트의 값을 변경하기 위한 속성들을 입력한다.

web.eth.sendTransaction() 을 tx를 매개변수로 담아 보내, 새로운 트랜잭션을 생성한다.

    // setValue 상태변수 변경
    const setValue = async () => {
      // 현재 상태 값을 가져온다.
      const _getValue = await getValue();
      const setCodeHash = await web3.eth.abi.encodeFunctionCall(abi[2], [
        _getValue + 1,
      ]);
      console.log(setCodeHash);
      // 0x552410770000000000000000000000000000000000000000000000000000000000000002
      //// '000000000000000000000000000000000000000000000000000000000000000' 사이의 0 값은 의미 없는 값이다. 구분을 짓기 위해 사용한다.
      
      // 사용자가 계정을 입력하지 않았으면 경고 메세지를 띄워준다.
      if (!useAccount.value) return alert("Account 입력하세요");
      const tx = {
        from: useAccount.value, // 트랜잭션을 발생시키는 계정
        to: "0x56b7639290C4421fBDbc53c063DD94058e25a12f", // CA 계정주소
        data: setCodeHash,
        gas: 500000,
        gasPrice: 200000000,
      };
      const data = await web3.eth.sendTransaction(tx);
      console.log(data);
      getValue();
    };

    const setValue2 = async () => {
      const _getValue = await getValue();
      const setCodeHash = await web3.eth.abi.encodeFunctionCall(abi[2], [
        _getValue - 1,
      ]);
      console.log(setCodeHash);
      // 0x552410770000000000000000000000000000000000000000000000000000000000000002
      //// '000000000000000000000000000000000000000000000000000000000000000' 사이의 0 값은 의미 없는 값이다. 구분을 짓기 위해 사용한다.
      if (!useAccount.value) return alert("Account 입력하세요");
      const tx = {
        from: useAccount.value, // 트랜잭션을 발생시키는 계정
        to: "0x56b7639290C4421fBDbc53c063DD94058e25a12f", // CA 계정주소
        data: setCodeHash,
        gas: 500000,
        gasPrice: 200000000,
      };
      const data = await web3.eth.sendTransaction(tx);
      console.log(data);
      getValue();
    };

    sendBtn.onclick = setValue;
    Btn.onclick = setValue2;

 

 

 


react와 metamask를 사용한 트랜잭션 생성

리엑트 앱을 다운받는다.

# 리엑트 라이브러리를 설치 받는다.
# npx create-react-app '라이브러리 폴더명'

npx create-react-app test

 

Chrome의 Metamask에서 네트워크와 계정을 추가한다.

1. 크롬 상단의 확장프로그램에서 메타마스크를 선택한다. 팝업의 좌측 상단의 현재 설정되어있는 네트워크를 클릭하고 하단의 '네트워크 추가'를 선택한다.

 

2. 이동한 페이지 하단의 '네트워크 수동 추가'를 클릭한다.

 

3. 네트워크 이름, 새 RPC URL, 체인 ID, 통화 기호 를 입력하여 저장한다.

  • 새 RPC URL에는 ganache 인스턴스가 열려있는 URL을 입력한다.
  • 체인 ID는 ganache의 관례인 1337을 입력한다. 

 

4. 현재 열려있는 ganache의 계정 중 하나의 비밀키를 입력해 계정을 가져온다.

  • 터미널에 'npx ganache-cli'를 입력하여 생성된 계정 목록에서 Private Key를 메타마스크에 입력한다.
  • Chrome의 Metamask에서 상단의 계정을 클릭한 후 열린 팝업의 하단의 계정 가져오기를 클릭한다.

 

test/src/App.js_사용자의 계정과 잔고를 확인하는 애플리케이션

Ethereum 블록체인과 상호작용하고 메타마스크에 연결한 계정과 잔액을 확인하는 애플리케이션을 제작한다.

 

작업에 필요한 상태를 담을 useState()를 선언한다.

  • account : 현재 메타마스크에 연결한 지갑의 주소를 담는다.
  • web3 : web3 인스턴스를 담는다.
  • balance : 현재 계정의 잔액을 담는다.
  const [account, setAccount] = useState(null);
  const [web3, setWeb3] = useState(null);
  const [balance, setBalance] = useState(0);

 

useEffect로 현재 연결한 지갑의 주소를 받고, web3 네트워크를 연결한다.

useEffect를 사용해 페이지가 처음 렌더링될 때 연결한 지갑의 주소를 띄워주는 해당 함수를 실행할 수 있도록 한다.

data 객체에 현재 메타마스크에 연결한 지갑의 주소를 반환받는다. 'window.ethereum'은 Metamask 브라우저 확장 프로그램이 설치되어 실행될 때 제공되는 전역 Javascript 개체이다. 웹 어플리케이션과 ethereum 블록체인간의 인터페이스 역할을 한다. 'window.ethereum.request({ method: "eth_requestAccounts" })'는 실질적인 계정의 정보를 엑세스해오는 코드이다. 

setWeb3 함수를 사용해 web3 객체를 이더리움 네트워크와 연결한다.

setAccount 함수를 사용해 data 객체로 받은 지갑의 주소를 account 객체에 저장한다.

  useEffect(() => {
    (async () => {
      // 배열의 구조분해 할당
      // window
      const [data] = await window.ethereum.request({
        method: "eth_requestAccounts",
      });
      console.log(data);
      // 결과 : 0x925d8db2812e38c27c09a8762c1290d59f0e4756
      //// 현재 연결한 지갑의 주소

      // 네크워크 web3 연결
      setWeb3(new Web3(window.ethereum));
      setAccount(data);
    })();
  }, []);

 

잔액조회 버튼 생성

balanceBtn 함수로 연결된 계정의 잔액을 조회하는 버튼을 생성한다.

'web3.eth.getBalance(account)' 코드로, account 지갑 주소의 잔액을 getBalance() 메소드로 불러온다. 이 때 잔액은 Wei단위로 표시된다. 반환받은 값을 balance 객체에 저장한다.

Wei단위로 저장된 잔액의 값을 fromWei() 메소드를 통해 ether 단위로 반환한다. 이 값은 _balance 객체에 저장한다.

setBalance 함수를 통해 이더 단위의 잔액 값을 가진 _balance 값을 balance 상태 변수에 저장한다.

  const balanceBtn = async () => {
    const balance = await web3.eth.getBalance(account);
    const _balance = await web3.utils.fromWei(balance, "ether");
    setBalance(_balance);
  };

 

페이지에 return 되는 코드

  return (
    <div className="App">
      {account || "로그인하셈"} <br></br>
      {balance} ETH <br></br>
      <button onClick={balanceBtn}>잔액조회</button>
    </div>
  );

 

 

👩‍🏫 직접 코딩하기_송금 트랜잭션 제작 

위의 코드들을 바탕으로 잔액을 송금하는 트랜잭션을 제작하였다. 트랜잭션 제작에 앞서, 보내는 계정을 입력할 곳과, 받는 계정, 잔액을 입력할 영역, 송금 버튼이 노출되도록 return문을 아래와 같이 수정하였다.

  return (
    <>
    <div className="App">
      {account || "로그인하셈"} <br></br>
      {balance} ETH <br></br>
      <button onClick={balanceBtn}>잔액조회</button>
    </div>

{/* 추가된 부분 ---------------------- */}
    <div>
      <input type="text" placeholder="보내는 계정" id="fromAdress"></input>
      <input type="text" placeholder="받는 계정" id="toAddress"></input>
      <input type="text" placeholder="금액" id="money"></input>
      <button onClick={sendTransaction}>송금하기</button>
    </div>
    </>
  );

 

송금을 위한 트랜잭션은 다음과 같이 작성하였다.

sendTransaction 메소드를 사용해, from속성으로 잔액을 보낼 계정의 주소를 입력받고, to 속성으로 잔액을 받을 계정의 주소를 입력받고, value 속성으로 입력받은 이더 값을 toWei 메소드를 사용해 wei 값으로 반환해 입력해주었다.

  const sendTransaction = ()=>{
    const fromAdress = document.getElementById("fromAdress").value
    const toAddress = document.getElementById("toAddress").value
    const money = document.getElementById("money").value

    web3.eth.sendTransaction({
      from : fromAdress,
      to: toAddress,
      value : web3.utils.toWei(money, 'ether')
    })
  }

 

 

 

 

 

 

728x90