로메오의 블로그

[DApp] Watch Event Listener 본문

Backend/Python & Blockchain

[DApp] Watch Event Listener

romeoh 2019. 7. 8. 02:51
반응형

마지막으로 Watch Event Listener를 사용해보겠습니다.

[DAPP] 투표하기 화면 구현

투표하기에 성공하면 location.reload()로 화면을 강제로 새로고침하였습니다.

Election back-end 코드에서 이벤트가 발생했을때 화면으로 이벤트를 전달해 보도록 하겠습니다.

 

/contracts/Election.js

pragma solidity ^0.5.0;

contract Election {
    
    ....

    // 투표하기 Watch Event
    event votedEvent (
        uint indexed _candidateId
    );

    // 투표하기
    function vote(uint _candidateId) public {
        
        ....

        // 이벤트 트리거
        emit votedEvent(_candidateId);
    }
}

votedEvent를 등록하고 이벤트 리스너를 등록합니다.

전체 코드는 아래와 같습니다.

pragma solidity ^0.5.0;

contract Election {
    
    // 후보자 모델
    struct Candidate {
        uint id;
        string name;
        uint voteCount;
    }

    // 후보자 기호 변수
    uint public candidatesCount;

    // 후보자 반환하기
    mapping(uint => Candidate) public candidates;

    // 투표에 참여한 ID 기록
    mapping(address => bool) public voters;
 
    // 후보자 등록하기
    function addCandidate (string memory _name) public {
        candidatesCount++;
        candidates[candidatesCount] = Candidate(candidatesCount, _name, 0);
    }

    // constructor
    constructor() public {
        // addCandidate('romeoh');
        // addCandidate('doraemong');
        // addCandidate('pororo');
    }

    // 투표하기 Watch Event
    event votedEvent (
        uint indexed _candidateId
    );

    // 투표하기
    function vote(uint _candidateId) public {
        
        // 중복투표를 하면 오류를 발생 시킨다.
        require(!voters[msg.sender]);

        // 목록에 없는 후보자에게 투표하면 오류를 발생시킨다.
        require(_candidateId > 0 && _candidateId <= candidatesCount);

        // 투표에 참여한 ID를 기록해서 두번 투표하지 못하도록 한다.
        voters[msg.sender] = true;

        // 득표수를 +1 한다.
        candidates[_candidateId].voteCount ++;

        // 이벤트 트리거
        emit votedEvent(_candidateId);
    }
}

테스트를 위해 constructor에서 후보자 3명 미리 추가하는 코드는 주석처리 했습니다.

 

Build

$ truffle migrate --reset

테스트 코드 작성하기

/test/election.spec.js

const Election = artifacts.require('./Election.sol')

contract('Election', accounts => {

    ....

    it('투표하기', () => {
        return Election.deployed()
            .then(instance => {
                electionInstance = instance
                candidateId = 1
                
                // 1번 후보자에게 투표함
                return electionInstance.vote(candidateId, {from: accounts[0]})
            })
            .then(receipt => {
                
                // watch event listener를 검증한다.
                assert.equal(receipt.logs.length, 1, '이벤트가 트리거됨')
                assert.equal(receipt.logs[0].event, 'votedEvent', '트리거된 이벤트는 votedEvent다.')
                assert.equal(receipt.logs[0].args._candidateId.toNumber(), candidateId, '트리거된 이벤트의 candidateId를 검증한다.')
                
                // account[0] 계정 주소를 반환함
                return electionInstance.voters(accounts[0])
            })
            .then(voted => {
                assert(voted, 'account[0]의 계정 주소가 투표한 것을 확인한다.')
                
                // 1번 후보자를 반환함
                return electionInstance.candidates(candidateId)
            })
            .then(candidate => {
                const voteCount = candidate.voteCount.toNumber()
                assert(voteCount, 1, '1번 후보자의 득표는 1 이다.')
            })
    })
    
    ....
})

투표하기 부분에 Watch event listener를 검증한다. 부분에 이벤트 발생 테스트 코드를 추가합니다.

전체 코드는 아래와 같습니다.

const Election = artifacts.require('./Election.sol')

contract('Election', accounts => {

    it('후보자 두 명 등록하면 candidatesCount는 2명 이다.', () => {
        return Election.deployed()
            .then(instance => {
                // 두명의 후보자를 등록합니다.
                instance.addCandidate('romeoh')
                instance.addCandidate('doraemong')
                
                // candidatesCount를 반환한다.
                return instance.candidatesCount()
            })
            .then(count => {
                // count는 2 다.
                assert.equal(count, 2)
            })
    })

    it('후보자 정보를 검증한다.', () => {
        return Election.deployed()
            .then(instance => {
                electionInstance = instance
                
                // 첫번째 후보자를 반환한다.
                // 위 테스트 코드에서 이미 두명의 후보자가 등록되어 있음
                return electionInstance.candidates(1)
            })
            .then(candidate => {
                assert.equal(candidate[0], 1, '첫번째 후보자 기호는 1번이다.')
                assert.equal(candidate[1], 'romeoh', '첫번째 후보자 이름은 romeoh다.')
                assert.equal(candidate[2], 0, '첫번째 후보자 득표는 0 이다.')
                
                // 두번째 후보자를 반환한다.
                return electionInstance.candidates(2)
            })
            .then(candidate => {
                assert.equal(candidate[0], 2, '두번째 후보자 기호는 2번이다.')
                assert.equal(candidate[1], 'doraemong', '두번째 후보자 이름은 doraemong이다.')
                assert.equal(candidate[2], 0, '두번째 후보자 득표는 0 이다.')
            })
    })

    it('투표하기', () => {
        return Election.deployed()
            .then(instance => {
                electionInstance = instance
                candidateId = 1
                
                // 1번 후보자에게 투표함
                return electionInstance.vote(candidateId, {from: accounts[0]})
            })
            .then(receipt => {
                
                // watch event listener를 검증한다.
                assert.equal(receipt.logs.length, 1, '이벤트가 트리거됨')
                assert.equal(receipt.logs[0].event, 'votedEvent', '트리거된 이벤트는 votedEvent다.')
                assert.equal(receipt.logs[0].args._candidateId.toNumber(), candidateId, '트리거된 이벤트의 candidateId를 검증한다.')
                
                // account[0] 계정 주소를 반환함
                return electionInstance.voters(accounts[0])
            })
            .then(voted => {
                assert(voted, 'account[0]의 계정 주소가 투표한 것을 확인한다.')
                
                // 1번 후보자를 반환함
                return electionInstance.candidates(candidateId)
            })
            .then(candidate => {
                const voteCount = candidate.voteCount.toNumber()
                assert(voteCount, 1, '1번 후보자의 득표는 1 이다.')
            })
    })

    it('투표 유효성 검사', () => {
        return Election.deployed()
            .then(instance => {
                electionInstance = instance

                // 유효하지 않은 후보자에게 투표한다.
                return electionInstance.vote(99, {from: accounts[0]})
            })
            .then(assert.fail)
            .catch(error => {
                assert(error.message.indexOf('revert') >= 0, '유효하지 않은 후보자에게 투표하면 exception이 발생해야 한다.')
                
                // 1번 후보자를 반환한다.
                return electionInstance.candidates(1)
            })
            .then(candidate1 => {
                const voteCount = candidate1.voteCount.toNumber()
                assert.equal(voteCount, 1, 'exception 발생 이후 1번 후보자의 득표는 여전히 1 이어야 한다.')
                
                // 1번 후보자를 반환한다.
                return electionInstance.candidates(2)
            })
            .then(candidate1 => {
                const voteCount = candidate1.voteCount.toNumber()
                assert.equal(voteCount, 0, '2번 후보자의 득표수는 0이다.')
            })
    })

    it('중복 투표를 방지한다.', () => {
        return Election.deployed()
            .then(instance => {
                electionInstance = instance

                // 2번 후보자에게 2번째 계정으로 투표한다.
                candidateId = 2
                electionInstance.vote(candidateId, {from: accounts[1]})

                // 2번 후보자를 반환한다.
                return electionInstance.candidates(candidateId)
            })
            .then(candidate2 => {
                const voteCount = candidate2.voteCount.toNumber()
                assert.equal(voteCount, 1, '첫 번째 투표는 정상적으로 작동해야한다.')

                // 같은 후보자에게 같은 계정으로 다시 투표한다.
                return electionInstance.vote(candidateId, {from: accounts[1]})
            })
            .then(assert.fail)
            .catch(error => {
                assert(error.message.indexOf('revert') >= 0, '같은 게정으로 투표할 수 없어야 한다.')
                
                // 1번 후보자를 반환한다.
                return electionInstance.candidates(1)
            })
            .then(candidate1 => {
                const voteCount = candidate1.voteCount.toNumber()
                assert.equal(voteCount, 1, '1번 후보자는 득표는 1 이 유지되고 있음')
                
                // 2번 후보자를 반환한다.
                return electionInstance.candidates(2)
            })
            .then(candidate2 => {
                const voteCount = candidate2.voteCount.toNumber()
                assert.equal(voteCount, 1, '2번 후보자의 득표는 1이다.')
            })
    })
})

테스트 실행

$ truffle test

5개의 테스트가 성공했습니다.

 

Front-end 화면개발

/src/js/app.js

var App = {
    web3Provider: null,
    contracts: {}
}

$(window).load(function () {

    ....

    // Election.json을 가지고 온다.
    $.getJSON('Election.json', function (election) {

        ....
        
        // 투표하기 이벤트 리스너 생성
        voteEventListener();

        render();
    });

    // 거래가 발생하면 event를 받는다.
    function voteEventListener() {
        App.contracts.Election.deployed()
            .then(function(instance) {
                instance.votedEvent({}, {
                    fromBlock: 0,
                    toBlock: 'lastest'
                }).watch(function(error, event) {
                    console.log('이벤트 트리거됨', event)
                    render()
                })
            })
    }

    // 화면구현
    function render() {

        ....

        // 계약 정보를 읽어온다.
        App.contracts.Election.deployed().then(function (instance) {
            electionInstance = instance;
            return electionInstance.candidatesCount();
        }).then(function (candidatesCount) {
            
            // 후보자 목록을 reset 한다.
            $('#candidatesResults').empty();
            $('#candidateSelect').empty();
            
            for (var i = 1; i <= candidatesCount; i++) {
                ....
            }

            ....
            
        }).catch(function (error) {
            console.warn(error);
        });
    }
    
    ....
    
});

Election.json을 읽어왔을때 render() 하는 부분에 voteEventListener()를 추가합니다.

voteEventListener 함수를 만들어 watch promise를 추가합니다.

render 함수에서 목록을 표시하기 전에 후보자 table과 select를 empty 해줍니다.

마지막으로 투표에 성공했을때 location.reload() 하는 코드를 제거합니다.

전체 코드는 아래와 같습니다.

var App = {
    web3Provider: null,
    contracts: {}
}

$(window).load(function () {

    // web3Provider 생성하기
    if (typeof web3 !== 'undefined') {
        // MetaMask가 설치되어 있어서 web3 인스턴스가 이미 생성되어 있음
        App.web3Provider = web3.currentProvider;
        web3 = new Web3(web3.currentProvider);
    } else {
        // MetaMask가 설치되지 않았을 경우 기본 인스턴스를 지정함
        App.web3Provider = new Web3.providers.HttpProvider('http://localhost:7545');
        web3 = new Web3(App.web3Provider);
    }

    // Election.json을 가지고 온다.
    $.getJSON('Election.json', function (election) {
        // Truffle 계약을 초기화 함
        App.contracts.Election = TruffleContract(election);
        // 인스턴스에 접속함
        App.contracts.Election.setProvider(App.web3Provider);

        // 투표하기 이벤트 리스너 생성
        voteEventListener();

        render();
    });

    // 거래가 발생하면 event를 받는다.
    function voteEventListener() {
        App.contracts.Election.deployed()
            .then(function(instance) {
                instance.votedEvent({}, {
                    fromBlock: 0,
                    toBlock: 'lastest'
                }).watch(function(error, event) {
                    console.log('이벤트 트리거됨', event)
                    render()
                })
            })
    }

    // 화면구현
    function render() {

        // 계정 정보 읽어오기
        web3.eth.getCoinbase(function (err, account) {
            if (err === null) {
                App.account = account;
                $('#accountAddress').html('나의 계정: ' + account);
            }
        });

        // 계약 정보를 읽어온다.
        App.contracts.Election.deployed().then(function (instance) {
            electionInstance = instance;
            return electionInstance.candidatesCount();
        }).then(function (candidatesCount) {
            
            // 후보자 목록을 reset 한다.
            $('#candidatesResults').empty();
            $('#candidateSelect').empty();

            for (var i = 1; i <= candidatesCount; i++) {
                electionInstance.candidates(i).then(function (candidate) {
                    var id = candidate[0];
                    var name = candidate[1];
                    var voteCount = candidate[2];

                    // 투표결과 html 파싱
                    var candidateTemplate = '<tr><th>' + id + '</th><td>' + name + '</td><td>' + voteCount + '</td></tr>'
                    $('#candidatesResults').append(candidateTemplate);

                    // 후보자 목록 표시
                    var candidateOption = '<option value="' + id + '">' + name + '</option>'
                    $('#candidateSelect').append(candidateOption);
                });
            }

            // 후보자 화면 표시
            $('#loader').hide();
            $('#content').show();
        }).catch(function (error) {
            console.warn(error);
        });
    }

    // 투표하기
    $('#btnVote').on('click', function() {
        var candidateId = $('#candidateSelect').val()
        if (!candidateId) {
            return alert('후보자를 선택하세요.')
        }
        App.contracts.Election.deployed()
            .then(function(instance) {
                return instance.vote(candidateId, {from: App.account})
            })
            .then(function(result) {
                if (result.receipt) {
                    alert('성공적으로 투표했습니다.')
                    // location.reload();
                }
            })
            .catch(function(error) {
                alert(error.message)
            })

    })
});

 

페이지를 새로고침합니다.

truffle migrate --reset을 했기 때문에 이전 데이터는 모두 초기화 되어 있습니다.

후보자를 선택하고 투표하기를 누릅니다.

 

투표가 성공했을때 페이지가 새로고침 되지 않고 이벤트가 트리거 되면서 화면을 새로 그리게 됩니다.

 

 

반응형
Comments