블록체인 Block-Chain - 이더리움 DApp - Smart Contract Event 등록( Ganache ) (2)

dev_swan·2022년 7월 14일
0

블록체인

목록 보기
23/36
post-thumbnail

-> 지난 블록체인 Block-Chain - 이더리움 DApp - Counter 만들기 ( Feat. Ganache ) (1) 포스트에서 이어집니다.

지난 작업 내용 수정

  • Ganache network를 재실행하게 되면 배포한 Smart Contract가 초기화되기 때문에 자동으로 json 파일의 network에 배포된 CA값을 가져오도록 세팅하였습니다.
  • migration --reset으로 컴파일 내용을 재배포 한뒤 FrontServer의 기존의 json 파일을 제거하고 재배포한 json 파일을 가져와주었습니다.
  • web3.eth.net.getId() : 현재 연결된 네트워크의 ID를 가져옵니다.

  • 가져온 네트워크 ID를 가지고 Counter.json 파일에서 Contract Address값을 가져옵니다.

Event 등록하기

  • SmartContract EventSolidityCode로 작성한 Contract의 함수들이 호출될 때 배포하고 난뒤 TransactionReceipt에 해당 기록을 남김으로써 후에 기록을 찾아볼 수 있도록 log를 삽입하는 것입니다.
  • Event를 등록하는 이유는 현재 코드에서 +버튼을 누르는 사람이 A라고 했을때 A의 화면에는 바뀐 count 값을 가져와서 화면에 띄워주겠지만 B라는 사람의 화면에는 상태변수가 변했을때 다시 바뀐 count 값을 가져와서 화면에 띄워주는 코드가 없습니다.
    즉 본인이 + 버튼을 눌렀을 경우에는 화면에 바로 바뀐 값이 렌더되겠지만 본인이 + 버튼을 누르지 않았을때에는 network 상에서는 count값이 변했겠지만 이 변한 값을 받아오는 부분이 빠져있기 때문에 Event를 추가하여 본인이 + 버튼을 누르지 않아도 log를 추적하여 새로운 log가 발생되면 즉시 해당 log를 읽어 값을 변경하여 내가 버튼을 누르지 않아도 화면에 바뀐 상태변수를 보여주도록 할 것입니다.
  • Solidity Code 수정
  contract Counter {
  	uint256 private _count;
  
  	// 이벤트 선언 => 이벤트명은 Count이고 uint256에 대한 내용으로 log를 찍을것입니다.
  	event Count(uint256 count);
  
  	// public이 아닌 private기 때문에 _count 상태변수를 가져오는 함수를 만들어주었습니다.
  	function current() public view returns (uint256) {
  		return _count;
  	}
  
  	function increment() public {
  		_count += 1;
  
  		// 이벤트를 실행 => log를 찍음
  		emit Count(_count);
  	}
  
  	function decrement() public {
  		_count -= 1;
  
  		// 이벤트를 실행 => log를 찍음
  		emit Count(_count);
  	}
  }
  • Count Event 추가 및 함수 실행시 emit을 사용하여 Count Event를 실행하도록 하였습니다.
  • Solidity Code를 수정했으니 재배포 하고 Counter.json파일을 다시 FrontServer로 가져와야 합니다.
  • truffle migration --reset 후 배포한 json 파일 FrontServer로 가져옵시다.

Smart Contract의 log 추적하기 ( Front Server)

  useEffect(() => {
  		(async () => {
  			if (deployed) return;
  
  			// netwokrId 가져오기
  			const networkId = await web3.eth.net.getId();
  			// Counter.json 파일에서 network의 CA 가져오기
  			const ContractAddress = CounterContract.networks[networkId].address;
  
  			// 2가지 인자값 abi ,CA
  			const deploy = await new web3.eth.Contract(CounterContract.abi, ContractAddress); 
  
  			const current = await deploy.methods.current().call();
  
  			// logs의 두번째 인자값의 내용은 어디 Contract의 log를 가져올 것인가 CA값을 적습니다.
  			web3.eth
  				.subscribe('logs', { address: ContractAddress }) //
  				.on('data', (log) => {
  					// on 메서드로 연결한 CA에서 data를 받을때 log를 console.log로 찍을것입니다.
  					console.log(log);
  				});
  
  			setDeployed(deploy);
  			setCounte(current);
  		})();
  	}, []);
  • Front에서 useEffect부분에 log를 읽는 코드를 추가합니다.
  • +혹은 -버튼을 누르고 console.log(log)를 확인해보면 아래와 같은 값이 출력됩니다.
  • data 값에 해당 숫자가 찍힌것을 확인할 수 있는데 0x0000...2 이런식으로 나오는 이유는 Contract를 작성할 때 countuint256으로 타입을 선언해주었기 때문에 메모리 공간을 uint256만큼 공간을 만들어주었기 때문입니다.


  • web3.eth.subscribe() 코드 수정
  web3.eth
      .subscribe('logs', { address: ContractAddress }) //
      .on('data', (log) => {
      // on 메서드로 연결한 CA에서 data를 받을때 log를 console.log로 찍을것입니다.
      console.log(log.data);

      // 첫번째 인자값은 16진수로 받아온 log를 어떤 type으로 파싱을 할것인가와 파싱한값을 어떤 name으로 받을것인가를 적습니다.
      // 두번째 인자값은 파싱할 데이터가 들어갑니다.
      const value = web3.eth.abi.decodeLog([{ type: 'uint256', name: '_count' }], log.data);
	  // return Obj 값이 객체로 나오는 이유는 Event에서 여러개의 data를 넣는다면 여러개의 data값이 출력되기 때문에 객체 형태로 return 됩니다.
      console.log(value);
  });
  • 위와 같이 log.datadecodeLog 메서드를 사용하여 16진수에서 사람이 읽을 수 있도록 파싱을 하면 Object로 아래와 같은 값이 나옵니다.

  • 객체의 0data값의 index를 의미하고 length는 길이 _count는 파싱한 data값을 의미합니다.

web3.eth
    .subscribe('logs', { address: ContractAddress }) //
    .on('data', (log) => {
    // on 메서드로 연결한 CA에서 data를 받을때 log를 console.log로 찍을것입니다.
    console.log(log.data);

    // 첫번째 인자값은 16진수로 받아온 log를 어떤 type으로 파싱을 할것인가와 파싱한값을 어떤 name으로 받을것인가를 적습니다.
    // 두번째 인자값은 파싱할 데이터가 들어갑니다.
    const value = web3.eth.abi.decodeLog([{ type: 'uint256', name: '_count' }], log.data); // return Obj 값이 객체로 나오는 이유는 Event에서 여러개의 data를 넣을수도 있기 때문에
    console.log(value);
    setCounte(value._count);
});

--- 중략 ---
    
const increment = async () => {
   await deployed.methods.increment().send({ from: account });
};

const decrement = async () => {
    await deployed.methods.decrement().send({ from: account });
};
  • 마지막으로 위와 같이 이벤트 log를 추적하여 받은 data값을 파싱하고 이 파싱한 count값을 setCounte로 변경하면 내가 아닌 다른 누군가가 + 버튼을 눌러도 내 화면에서도 eventlog를 받기 때문에 내 화면에도 count가 변경될 것입니다.
  • 내가 + 혹은 - 버튼을 누른다고 하더라도 subscribe 메서드로 log를 추적할 것이니 내 화면에도 count가 변경될 것입니다.

  • 이런식으로 Transaction이 발생되었을때 블록이 mining이 되며 Smart Contract가 실행되고 이때 Event가 발동하여 log를 남길 경우 TransactionReceiptlogs부분에 해당 log의 내용들을 확인할 수 있습니다.

서명을 제외한 Transaction 생성하기 ( Back Server )

  • 지금까지는 Front에서 MetaMask와 연결하여 SmartContract를 실행시켰다면 이제는 Back 서버에서 서명을 제외한 Transaction을 생성하여 Front로 전달한후 Front에서 해당 Transaction을 가지고 MetaMaskTransaction을 보내도록 처리할 것입니다.
  • 이렇게 작업해야하는 이유는 지금과 같이 Counter 같이 간단한 작업들은 Front에서 처리해도 되지만, 예를 들어 쇼핑몰 같은 경우에 DataBase에 연동하여 작업을 해야할 경우가 있기 때문에 미리 연습해 보는것입니다.
  • 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')); // Ganache 네트워크 연결

app.use(
	cors({
		origin: true,
		credential: true,
	}),
);

app.post('/counter', (req, res) => {
	res.json('Hello World!');
});

app.listen(4000, () => {
	console.log('Back Server On');
});
  • Back 서버에서 서명을 제외한 Transaction을 만들어서 front로 보내주면 front에서 MetaMask로 연결하여 MetaMask에서 서명을 만들어 Transaction을 발생시킬것 입니다.
  • 배포한 Contract에 대한 내용이 필요할 것이니 front에서 사용중이던 Counter.json 파일을 Back서버에도 그대로 가져와서 작업을 할 것입니다.

  • app.post(counter) 코드 작성
/* server.js */
  
app.post('/counter', async (req, res) => {
	const { from, type } = req.body; // 0xb81cc9fa0ccd058f5a1669f719bf67e4b97bac56

	const transaction = await new Txobj().init(from, type);

	console.log(transaction);

	res.json(transaction);
});

  • 위와 같이 req.bodyFront에서 요청한 내용의 body 내용을 가져와 Transactionfrom내용, 즉 address값과 count+ 할것인지 - 할것인지에 대한 type을 받아오도록 하겠습니다.
/* txobj.js */
  
const Web3 = require('web3');
const web3 = new Web3(new Web3.providers.HttpProvider('http://127.0.0.1:8545')); // Ganache 네트워크 연결
const CounterContract = require('./contracts/Counter.json');

class Txobj {
	async init(_from, _type) {
		this.nonce = await Txobj.getNonce(_from);
		this.from = _from;
		this.to = await Txobj.getCA(_from); // Truffle 디렉토리의 Counter.json 파일을 복사해서 가져오고 CA 계정을 구합니다.
		this.data = await Txobj.getData(this.to, _type); // 함수를 실행시킬수 있는 bytecode가 들어갑니다.

		return this;
	}

	//nonce 구해오기
	static getNonce = async (_from) => {
		return await web3.eth.getTransactionCount(_from);
	};

	// CA 계정 구해오기
	static getCA = async (_from) => {
		const networkId = await web3.eth.net.getId();

		return CounterContract.networks[networkId].address;
	};

	// Data 구해오기
	static getData = async (_ca, _type) => {
		const abi = CounterContract.abi;
		const deployed = new web3.eth.Contract(abi, _ca);

		switch (_type) {
			case 'increment':
				return await deployed.methods.increment().encodeABI(); // increment 메서드 자체를 코드를 bytecode로 변환합니다.
			case 'decrement':
				return await deployed.methods.decrement().encodeABI();
		}
	};

	/*
		{
			"nonce": 16,
			"from": "0xb81cc9fa0ccd058f5a1669f719bf67e4b97bac56",
			"to": "0x1a305591E65a493A042Fa445cE44F237DdAC2550",
			"data": "0xd09de08a"
		}
	*/
}

module.exports = { Txobj };
  • Transaction이란 객체이니 class Txobj를 생성하여 인자값으로 req.body로 받아온 fromtype을 넣고 new 키워드로 새로운 Txobj를 생성하려고 하면 자동으로 nonce, from, to, data를 가진 인스턴스를 생성하도록 해주었습니다.
  • nonceweb3.ethgetTransactionCount 메서드를 사용하여 해당 계정으로 몇번의 Transaction이 발동되었는지에 대한 nonce을 가져왔습니다.
  • from은 말그대로 인자값으로 받은 address값을 넣어줍니다.
  • to에는 Contract AddressContract에 대한 주소 값이 들어가는데 먼저 networkIdweb3.ethgetId 메서드를 사용하여 가져온후 networkId를 가지고 배포한 ContractCounter.json파일에서 해당 네트워크의 배포된 Contractaddress을 찾아와서 넣어주었습니다.
  • 마지막으로 data에는 미리 구한 CA값과 인자값으로 받은 type을 사용할 것인데, 배포한 Contractabi 값을 가져온후 가져온 abi값과 인자값으로 받은 CA를 사용하여 새로운 deployed인스턴스를 생성해주고, 인자값으로 받은type에 따라 increment일경우 + decrement일 경우 -를 하도록 deployed인스턴스안에methods에서 increment메서드 혹은derement메서드 자체를 컴퓨터가 읽을 수 있도록bytecode`로 변환하여 넣어주었습니다.

  • 이런식으로 class를 활용하여 increment 혹은 decrement를 처리하는 Transaction을 만들어서 front로 보내줄 것입니다.

Back Server API에서 Transaction 받아오기 및 MetaMask에 전송하기 ( Front Server )

const increment = async () => {
		const response = await axios.post('http://localhost:4000/counter', { from: account, type: 'increment' });

		// MetaMask에 Transaction send
		await web3.eth.sendTransaction(response.data);
	};

	const decrement = async () => {
		const response = await axios.post('http://localhost:4000/counter', { from: account, type: 'decrement' });

		// MetaMask에 Transaction send
		await web3.eth.sendTransaction(response.data);
	};
  • 마지막으로 Frontincrement, decrement 함수를 수정하여 모두 같은 Back 서버의 API로 요청을 보내고 type 값만 다르게 주어 Back 서버에서 increment 혹은 decrement Contract 내용이 포함된 Transaction을 받고 해당 Transaction을 연결된 MetaMask에게 보내 서명을 추가한 뒤 해당 Transaction을 블록에 넣어 Smart contract를 실행할 것입니다.

Counter+ 혹은 - 버튼을 누를때마다 Smart Contract를 실행하여 상태변수를 바꿔줄것이니 가스비를 지불하여 Transactiontxpool에 담기고 블록이 mining되면 transactioncontract가 실행되며 상태변수를 바꿔줍니다.

0개의 댓글