일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | 4 | |||
5 | 6 | 7 | 8 | 9 | 10 | 11 |
12 | 13 | 14 | 15 | 16 | 17 | 18 |
19 | 20 | 21 | 22 | 23 | 24 | 25 |
26 | 27 | 28 | 29 | 30 | 31 |
- javascript기초
- useContext
- JWT 로그인 기능 구현
- 시퀄라이즈 기본설정
- JWT 하드코딩
- JWT 만들어보기
- 라우터와 미들웨어
- 아이디 중복체크기능
- 비동기파일업로드
- 블록 만들기
- css기초
- mysql wsl
- 세션으로 로그인 구현
- express router
- FormData()
- next 매개변수
- express.static
- nodejs파일업로드
- OAuth 카카오
- express실행
- node.js path
- 라우트 매개변수
- 라우터 분리
- buffer.from
- useEffect clean up
- ws 라이브러리
- cookie-parser 만들어보기
- express session
- Uncaught Error: could not find react-redux context value; please ensure the component is wrapped in a <Provider>
- 라우터미들웨어 분리
- Today
- Total
즐코
스마트 컨트랙트 이벤트 등록 / 백엔드에서 트랜잭션 생성 본문
1. 스마트 컨트랙트 - 이벤트 등록
저번 포스팅에서 화면이 렌더되자마자 배포된 컨트랙트를 가져와서 최신 상태변수값을 조회하여 화면에 렌더해주었다. 다만, 우리는 가나쉬 네트워크를 사용하고 있기 때문에 가나쉬를 실행하고 있는 터미널을 종료하면 가나쉬 네트워크와 연결된 노드는 사라지고 다시 실행 시 새로운 노드와 그에 따른 계정들도 재생성해준다. 이 때 이 이더리움 네트워크 상에서 각 노드들을 구분할 수 있는게 network id 이다.
자세히 설명하자면,
네트워크 chain id 가 같은 노드들은 같은 네트워크 상에 묶이게 된다. 이때 한 네트워크 내의 각 노드들은 저마다의 식별자를 가지고 있는데 그게 바로 네트워크 아이디이다. truffle migration으로 배포 진행 후에 build/contracts/Counter.json 파일의 networks 부분을 보면 각 네트워크 아이디와 그에 해당하는 CA 값이 확인 가능하다.
하기 스크린샷에 두 개의 네트워크 아이디가 들어간 이유는 가나쉬 서버를 실행하고 있기 때문에 가나쉬 서버를 껐다 키면 새로운 노드가 생성되고, 네트워크와 연결된 새로운 노드가 배포를 다시 진행하면 truffle 상에서 자동으로 다른 노드로 인식하여 Counter.json 상에 이 새로운 노드의 네트워크 아이디와 그에 해당하는 CA를 업데이트 해주기 때문이다.
다만 현재 디렉토리 구조상 front와 truffle이 나눠져 있기에 truffle에서 배포 후에 만들어진 이 json 파일을 복사해서 front 쪽 디렉토리 상에 넣어주는 방식에다가, address(CA) 를 일일이 복사해서 front-Counter.jsx 코드 상에 넣어주었다. (아래 참고)
// src/components/Counter.jsx
import React, { useEffect, useState } from "react";
import CounterContract from "../contracts/Counter.json";
const Counter = ({ web3, account }) => {
const [count, setCount] = useState(0);
const [deployed, setDeployed] = useState();
const increase = async () => {
const result = await deployed.methods.increment().send({
from: account,
});
if(!result) return;
const current = await deployed.methods.current().call();
setCount(current);
};
const decrease = async () => {
const result = await deployed.methods.decrement().send({
from: account,
});
if(!result) return;
const current = await deployed.methods.current().call();
setCount(current);
};
// 코드 수정이 필요한 부분!
useEffect(() => {
(async () => {
if (deployed) return;
const deployedCont = new web3.eth.Contract(
CounterContract.abi,
"0xAF152c774673D45F37adf53792BBCB00D9F2F76A"
);
const count = await deployedCont.methods.current().call();
setCount(count);
setDeployed(deployedCont);
})();
},[]);
return(
<div>
<h2>Counter: {count} </h2>
<button onClick={increase}>증가</button>
<button onClick={decrease}>감소</button>
</div>
);
};
하지만, 이렇게 가나쉬 네트워크를 껐다 새로 킬때마다 배포 후 새로 생성되는 이 CA를 넣어준다면 꽤 귀찮은 일이다..
그렇기에 살짝 코드 수정이 필요하다!
코드 수정 전에, 한가지 web3 메소드를 알아야한다.
web3.eth.net.getId()
truffle console 상에 아래 명령어를 쳐보면 현재 이더리움 네트워크에 연결된 노드의 네트워크 아이디를 확인해볼 수 있다.
truffle(development)> web3.eth.net.getId()
이 web3 메서드를 사용하여, 코드를 수정하자면 아래와 같다.
// front/src/components/Counter.jsx
import React, { useEffect, useState } from "react";
import CounterContract from "../contracts/Counter.json";
// props.web props.account
const Counter = ({ web3, account }) => {
const [count, setCount] = useState(0);
const [deployed, setDeployed] = useState();
// new web3.eth.Contract() ===> abi, address.. 등 나옴
const increase = async () => {
const result = await deployed.methods.increment().send({
from: account,
});
console.log(result);
if (!result) return;
const current = await deployed.methods.current().call();
setCount(current);
};
const decrease = async () => {
const result = await deployed.methods.decrement().send({
from: account,
});
if (!result) return;
const current = await deployed.methods.current().call();
setCount(current);
};
useEffect(() => {
(async () => {
if (deployed) return;
// network id를 가져올 수 있어야함
const networkId = await web3.eth.net.getId();
const ca = CounterContract.networks[networkId].address;
const abi = CounterContract.abi;
const deployedCont = new web3.eth.Contract(abi, ca);
const count = await deployedCont.methods.current().call();
setCount(count);
setDeployed(deployedCont);
})();
}, []);
return (
<div>
<h2>Counter: {count}</h2>
<button onClick={increase}>증가</button>
<button onClick={decrease}>감소</button>
</div>
);
};
export default Counter;
이렇게 하면 가나쉬를 껐다 켜도 작업이 수월해진다.
다만, 현재 코드엔 저번 포스팅에서 언급했던대로 문제가 있다.
컨트랙트 실행 시, 실행시킨 클라이언트만 이 상태변수의 업데이트를 확인할 수 있고, 같은 네트워크 내 존재하는 다른 노드들은 이를 인지하지 못한다. 상태변수 업데이트 즉, 컨트랙트의 실행 공유를 위해 이번 포스팅에선 '이벤트' 라는 걸 다뤄본다.
이벤트하면 생각나는 건 바로 웹소켓이다.
웹소켓에서도 특정 이벤트를 emit 하여 다른 노드가 이 특정 이벤트를 감지하여 on 하여 특정 코드를 실행했듯이 이 스마트 컨트랙트에서의 이벤트도 비슷한 흐름으로 간다. 다만 웹소켓과 살짝 다른 점은, '로그를 남긴다' 라는 표현이 더 어울린다는 것이다.
솔리디티 코드 안에서 이벤트 작성
1/ 이벤트 등록
event [이벤트명]([기록할 내용의 데이터 타입], [로그에 남길 내용])
Counter.sol : uint256 타입의 count라는 내용을 로그로 남기는 Count라는 이벤트를 등록하겠다! 란 뜻이다.
2/ 이벤트 시점 작성
emit [로그할 이벤트명]([로그할 내용])
어느 시점에 이벤트를 찍어줄건지, 즉 이더리움 클라이언트에 로그를 어느 시점에 찍어줄건지 작성해줘야한다.
Counter.sol : increment, decrement 실행 시 해당 이벤트를 찍어줄 것이다.
// truffle/contracts/Counter.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.15;
contract Counter{
uint256 private _count;
event Count(uint256 count); // 이벤트 등록
function current() public view returns(uint256){
return _count;
}
function increment() public{
_count += 1;
emit Count(_count); // 이벤트 시점 지정
}
function decrement() public{
_count -= 1;
emit Count(_count); // 이벤트 시점 지정
}
}
솔리디티 코드를 수정했기때문에 다시 migration을 해준다. truffle migration --reset
업데이트된 json 파일 프론트쪽으로 옮겨주고 이벤트 감지를 위한 프론트 코드 수정
프론트 : 이벤트 감지 코드 작성
- subscribe('이벤트', [각 이벤트에 따른 인자])
우리는 로그를 찍는 걸 할 것이므로, logs라는 이벤트를 구독(subscribe)할 것이다.
logs subscribe 의 2번째 인자에는 어떤 컨트랙트 안의 로그를 가져올지를 명시해주는 부분이다. 즉, 컨트랙트에 접근할 수 있는 고유값인 CA를 address 값으로 넣어줘야한다. 이로서, 특정 컨트랙트 안의 로그만 추적이 가능해진다.
- on
여기서 on이란 이 이벤트를 감지하면 해당 인자로 들어가는 콜백함수 부분을 실행하겠다란 뜻이다.
web3.eth.subscribe('logs', { address: [contract address] })
.on("data", (log) => { [코드 작성] })
// front/src/components/Counter.jsx
// useEffect 부분만 쓴다.
useEffect(() => {
(async () => {
if (deployed) return;
const networkId = await web3.eth.net.getId();
const ca = CounterContract.networks[networkId].address;
const abi = CounterContract.abi;
const deployedCont = new web3.eth.Contract(abi, ca);
const count = await deployedCont.methods.current().call();
// logs라는 이벤트를 subscribe 할 것이다.
web3.eth.subscribe("logs", { address: ca }).on("data", (log) => {
console.log("logs 이벤트 감지", log);
});
setCount(count);
setDeployed(deployedCont);
})();
}, []);
증가버튼을 눌러서 메타마스크를 통해 컨트랙트를 실행시키고 나면 상태변수가 바뀜과 동시에 logs 이벤트를 감지하여 아래 콘솔을 찍어준다. 이때 찍히는 log를 확인해보면 객체가 하나 출력되는데, 여기서 주목해야할 곳이 data이다. data는 업데이트된 상태변수값을 나타낸다. 이때, 그냥 3이 아니라 0x00...03 의 16진수로 표현된 건 컨트랙트 이벤트 작성 시 count값을 uint256으로 메모리값을 잡아놨기때문에 이를 채워줘야하기 때문이다. 따라서, 우린 정확히 정수 3을 가져오고 싶으므로, 이 data를 디코딩을 해줘야한다.
logs 로 받아온 data 값 디코딩
web3.eth.abi.decodeLog('파싱디테일', '파싱할 값');
web3.eth.abi.decodeLog({ type: "uint256", name: "_count" }, logs.data);
1. 첫번째 인자: [ { type : '파싱할 내용의 데이터 타입', name: '파싱된 데이터를 받을 변수명'} ]
하나의 이벤트에 한가지 이상의 값을 받을 수도 있으므로, 배열 안에 여러 개의 객체를 열거해줄 수 있다.
2. 두번째 인자 : 파싱할 데이터를 넣어준다. log.data
디코딩 내용을 출력해보면 아래와 같다.
프론트 쪽 코드를 정리해보면 아래와 같다.
특정 컨트랙트의 logs 이벤트 감지 시 count 상태를 업데이트 해주는 코드로 바꾼 것이다.
따라서, 이전엔 increase, decrease 함수에서 일일이 setCount로 상태 업데이트를 해주었으나, 이젠 해당 이벤트 감지 시에 count를 업데이트 해주기 때문에 각 증가,감소 함수 상에서 setCount 코드를 다 지워버려도 된다.
// front/src/components/Counter.jsx
import React, { useEffect, useState } from "react";
import CounterContract from "../contracts/Counter.json";
const Counter = ({ web3, account }) => {
const [count, setCount] = useState(0);
const [deployed, setDeployed] = useState();
const increase = async () => {
await deployed.methods.increment().send({
from: account,
});
};
const decrease = async () => {
await deployed.methods.decrement().send({
from: account,
});
};
useEffect(() => {
(async () => {
if (deployed) return;
const networkId = await web3.eth.net.getId();
const ca = CounterContract.networks[networkId].address;
const abi = CounterContract.abi;
const deployedCont = new web3.eth.Contract(abi, ca);
const count = await deployedCont.methods.current().call();
// logs라는 이벤트를 subscribe 할 것이다.
web3.eth.subscribe("logs", { address: ca }).on("data", (log) => {
console.log("logs 이벤트 감지", log.data);
const params = [{ type: "uint256", name: "_count" }];
const value = web3.eth.abi.decodeLog(params, log.data);
console.log(value);
setCount(value._count);
// 여기서 count 상태를 업데이트 해줌으로써
// 각 increase, decrease 함수에서 일일이 setCount로 상태 업데이트를 안해줘도 된다!
});
setCount(count); // 첫페이지 렌더 시
setDeployed(deployedCont);
})();
}, []);
return (
<div>
<h2>Counter: {count}</h2>
<button onClick={increase}>증가</button>
<button onClick={decrease}>감소</button>
</div>
);
};
export default Counter;
또한, web3.eth.getTransactionReceipt('txHash값') 으로도 해당 logs 가 기록됨을 확인할 수 있다!
2. 백엔드 쪽에서 트랜잭션 생성하기
여태까지의 코드는 백엔드 없이 프론트 쪽에서 바로 메타마스크로 컨트랙트 실행을 요청 하는 구조였다.
이번엔 백엔드 쪽에서 트랜잭션 객체를 만들어서 그 객체를 프론트쪽에서 받아 메타마스크에게 트랜잭션을 요청하는 구조로 바꿔줄 것이다.
자세히 말하자면, 프론트 쪽에서 컨트랙트를 실행할 계정만 백엔드 쪽에 보내면서 트랜잭션 객체를 만들어달라고 요청을 하면, 백엔드에서 그에 맞게 트랜잭션 객체를 만들어서 프론트로 응답을 보내주고, 프론트가 이걸 가지고 메타마스크에게 트랜잭션 요청, 즉 컨트랙트 실행 요청을 보내는 구조이다.
여기서 백엔드가 만드는 트랜잭션 객체는 이전에 만들었던 코인을 송금하는 트랜잭션 객체와는 살짝 다르다. 현재 프론트는 컨트랙트를 실행시키는 요청을 하고싶으므로, 백엔드가 컨트랙트 실행과 관련된 트랙잭션 객체를 만드려면, 이더리움 네트워크 상에 배포된 컨트랙트에 접근할 수 있어야한다. 따라서, 현재 배포된 컨트랙트에 접근하려면 abi와 ca 정보가 필요하므로 백엔드 쪽에도 Counter.json 파일이 필요하다. 복사해서 가져온다.
그리고 이전에 작성했던 트랜잭션 객체의 to는 수취인의 지갑주소 EOA 였다.
이번엔 컨트랙트 실행과 관련된 트랜잭션이므로 to는 실행시킬 컨트랙트의 주소 CA임을 인지하고 코드를 작성해준다.
우선 아래 백엔드 쪽 코드는 increment, 즉 증가 버튼을 눌렀을 때의 경우만 작성해주었다.
// back/server.js
const express = require("express");
const app = express();
const cors = require("cors");
const Web3 = require("web3");
const web3 = new Web3(new Web3.providers.HttpProvider("http://127.0.0.1:8545"));
const CounterContract = require("./contracts/Counter.json");
app.use(express.json());
app.use(
cors({
origin: true,
credentials: true,
})
);
app.post("/api/increment", async (req, res) => {
const { from } = req.body;
const nonce = await web3.eth.getTransactionCount(from);
// 트랜잭션 객체에서의 nonce는 인자로 들어간 계정의 트랜잭션 횟수이다.
const networkId = await web3.eth.net.getId();
const ca = CounterContract.networks[networkId].address;
const abi = CounterContract.abi;
const deployedCont = new web3.eth.Contract(abi, ca);
const data = await deployedCont.methods.increment().encodeABI();
// encodeABI() : increment 호출시킬수있는 abi 내용을 바이트 코드로 변환해준다.
let txObj = {
nonce,
from,
to: ca,
data,
};
res.json(txObj);
});
app.listen(3005, () => {
console.log("server starts 🚀");
});
여기서 주목할 점은 data 를 만들어주는 부분이다. 이 data 부분에 컨트랙트 함수를 실행시킬 내용이 들어가야한다.
그래서 그냥 deployedCont.methods.increment()하면 될 거 같지만, 이더리움 네트워크 상에 배포된 스마트 컨트랙트는 바이트 코드 형태로 존재하기 때문에 어떤 함수를 실행시킬지에 대한 내용도 바이트코드 형태로 넣어줘야한다. 그 때 쓰는 메소드가 encodeABI() 이다.
const data = await deployedCont.methods.increment().encodeABI();
프론트 쪽 코드는 아래와 같다.
현재 메타마스크에 연결된 계정 정보를 백엔드 쪽에 post 요청으로 넘겨주고 (컨트랙트 실행 주체 계정), 백엔드 쪽에서 받은 트랜잭션 객체를 메타마스크 쪽에 넘겨주는 것이다.
// front/src/components/Counter.jsx
const increase = async () => {
// 프론트에서 직접 날릴 경우
// await deployed.methods.increment().send({
// from: account,
// });
// 백엔드에서 트랜잭션 객체 만들어 보내줄 경우
const response = await axios.post("http://127.0.0.1:3005/api/increment", {
from: account,
});
await web3.eth.sendTransaction(response.data);
};
아래 스크린샷에서 data로 넣은 increment 함수의 인코딩값과 increment 실행을 한 어떤 다른 트랜잭션 객체의 input값이 동일함을 확인할 수 있다.
트랜잭션은 현재까지 배우기론 아래와 같이 크게 3가지 정도이다.
- 단순한 코인 송금의 트랜잭션
- 스마트컨트랙트 배포 관련 트랜잭션
- 스마트컨트랙트 실행 관련 트랜잭션
송금개념의 트랜잭션과 스마트 컨트랙트 배포 트랜잭션의 구분은 transactionReceipt 객체의 contractAddress 속성 즉, CA 값의 존재유무임을 알고있다. 이제 스마트 컨트랙트 배포와 실행을 구분해주는 것도 알 수 있다. 바로 transaction 객체의 input 속성에 컨트랙트 함수의 인코딩값이 있냐없냐로 구분하면 된다!
'BlockChain' 카테고리의 다른 글
Crypto Zombie / lesson2 (0) | 2022.07.19 |
---|---|
Crypto Zombie / lesson1 - Overview (0) | 2022.07.18 |
메타마스크 통해서 스마트 컨트랙트 실행해보기 (0) | 2022.07.13 |
truffle - 스마트 컨트랙트 개발 프레임워크 (0) | 2022.07.12 |
JS로 스마트 컨트랙트 컴파일, 배포 및 실행 (0) | 2022.07.12 |