dukDukz

[블록체인] 투표앱 만들기 본문

웹 개발/블록체인

[블록체인] 투표앱 만들기

헤일리_HJ 2021. 10. 12. 15:46

# 투표앱 만들기

 

* 솔리디티

1. 후보자를 초기화

2. 후보자 투표 기능

3. 후보자 정보 가져오기

 

구현하고 빌드해서 블럭에 배포(실행 시킨다는것)를 한다.

처음 배포할 때 이외에는 쓰지 않음

코드를 작성한 것.

컴파일 하면 두개의 파일이 나오는데 이것들을 블럭에 저장한다. (안에 내용들이 다 담겨 있음)


* 블럭

abi 에 기능들이 다 저장이 됨

주소값도 영수증처럼 생김.


* web3

이 저장한 내용을 웹에다가 출력 해야한다.

이때 쓰는게 바로 web3 이다.

사실 web3 는 따로 빠져있는게 아니라 html 페이지 안에 존재하는거임

얘가 가나쉬에 접근해서 접속한다. 

 

블럭에서 주소값에 대한 내용도 가져와야 한다.

 

살짝 어려운 부분은 web3 랑 가나쉬랑 연결하는 부분..?

 




# 투표앱

1. 솔리디티 코드 작성
2. 솔리디티 코드 컴파일
3. 컴파일 결과를 배포   = 블럭에 내용을 추가한다.
4. 컴파일 결과를 js 코드로 내용을 불러올 수 있는지 테스트
5. html 에서 js web3 를 활용해서 내용을 불러온다.

터미널 켜고 (가나쉬 / 작업공간 총 두 개 열어줌)

 

 

# 가나쉬 터미널

$ ganache-cli --host 0.0.0.0

 

# 솔리디티 코드 작성

Voting.sol 생성

mapping(string => uint) public voteReceived;

voteReceieved 는 mapping 으로 선언해주었다. 

형태는 이렇게 될 것이다.

let voteReceived = {
    ingoo1 : 0,
    ingoo2 : 0,
    ingoo3 : 0,
   [string]  [uint]
}

 

접근하는 방법

voteReceived.ingoo1

voteReceieved[ingoo1]

그래서 투표를 한번 하면

여기서 ingoo1 라는 값에 숫자를 +1 해주면 됨

voteReceieved[ingoo1] = voteReceieved[ingoo1] + 1;


이렇게 선언하고 사용할때는 함수에서

function voteForCandidate(string memory _candidate) public{
    voteReceived[_candidate] += 1;
}

 

memory 는 파일에 저장하지 않겠다

storage 파일에 저장하겠다.

 


 

# 예외처리 ; 문자열 비교

 

1. js ver

let arr = ['ingoo1','ingoo2','ingoo3']
let searchText = 'ingoo4'

function check(searchText){
    // 완전탐색 - 모든 배열을 다 검사한다는.
    let result = arr.map((v,k)=>{
        if(searchText == v){
            return true;
        }
        return false;
    })
    return result;

    혹은

    for(let i=0; i<arr.length; i++){
        if(arr[i] == searchText){
            return true;
        }
    }
    return false;
}

 

2. sol ver

솔리디티는 문자열 비교가 안되니까..

객체 지향적 사고... 생각이 완료되어야 작성 할 수 있음...

let arr = ['ingoo1','ingoo2','ingoo3']
let searchText = 'ingoo4'


function validCandidate(string memory _candidate) view public returns(bool){
    // string 끼리 비교가 안되니까
    // string to byte 로 바꿔주고
    // keccake256() 매서드 안에 byte 값 넣기

    for(uint i=0; i < candidateList.length; i++){
        if(keccak256(bytes(candidateList[i])) == keccak25(bytes(_candidate))){
            return true;
        }
    }
    return false;
}

 


# 솔리디티 컴파일

abi, bin 파일 두개가 나옴

 

solcjs --abi --bin [파일명]

안되면 앞에 npx

 

터미널 작업공간에서

$ solcjs --abi --bin Voting.sol

하면 파일 두개가 생성됨



# 스마트 컨트랙트 배포하기

web3 라이브러리 활용해서 

deploy 사용했던 부분

 

deploy.js 파일 만들기

- web3 라이브러리 가져오고

- 현재 사용하고 있는 블록체인 서버에 연결 = 가나쉬

- 배포 작업 위해 Contract 매서드를 사용해서 블럭 생성

- 결과 값을 배포하기(deploy) 매서드

 

 

배포할 때는 string 값을 못넣음 그래서 이거를 16진수 값으로 바꿔줄거임

['ingoo1','ingoo2','ingoo3'].map(name=>{
    return web3.utils.asciiToHex(name)
})
[['ingoo1','ingoo2','ingoo3'].map(name=>web3.utils.asciiToHex(name))]

대괄호 없으면 return 생략 가능 (문법)

 

이 deploy.js 가 솔리디티코드를 블럭으로 생성하는..

 

const Web3 = require('web3')
const fs = require('fs')

const ABI = JSON.parse(fs.readFileSync('./Voting_sol_Voting.abi').toString())
const BYTECODE = fs.readFileSync('./Voting_sol_Voting.bin').toString()

const web3 = new Web3('http://localhost:8545')


// 블럭 생성할 때 솔리디티 컴파일한 abi 파일을 인자값에 넣음
const deployContract = new web3.eth.Contract(ABI)   // ABI 는 객체가 되어야함 그래서 JSON parse

deployContract.deploy({
    //배포를 할때 byte 값을 넣음
    data : BYTECODE,
    arguments : [['ingoo1','ingoo2','ingoo3'].map(name=>web3.utils.asciiToHex(name))]
    // 배포할 때는 string 값을 못넣음 그래서 이거를 16진수 값으로 바꿔줄거임
    
    // 배포를 할때 인자값을 넣어주는거임.
})
.send({
    from : '0xc0130920344A4f84b20AD8F458274b00D577BBc1',     // 공개키 - 얘가 주체 Voting.js 코드 전체를 올린다.
    gas : 6721975,
})
.then(newContract=>{
    console.log(newContract.options.address)        // Voting 코드 올린 곳의 주소
})

 

$ npm init

$ npm i web3

$ node .\deploy.js

결과 : 0xc5f4EeA08888836D8947B3f724510c3A73E86151

 

가나쉬 터미널에서 이렇게 확인할 수 있다.

 

잠깐 주석처리 하고

컴파일 결과 배포까지는 된거고

 

js 로 불러오는거 해봐야함



const Web3 = require('web3')
const fs = require('fs')

const ABI = JSON.parse(fs.readFileSync('./Voting_sol_Voting.abi').toString())
const BYTECODE = fs.readFileSync('./Voting_sol_Voting.bin').toString()

const web3 = new Web3('http://localhost:8545')


// 주소는 Contract created: 여기로
// 해당 블럭 (Voting 코드 올린 곳)의 주소에 접속해야 한다.
const contract = new web3.eth.Contract(ABI,'0xc5f4eea08888836d8947b3f724510c3a73e86151')    

// send : 누군가 투표한다
// 주소 10개에 해당하는 녀석들만 투표가 가능하게한다. 투표자가 누구인가 (2번 공개주소로함)
contract.methods.voteForCandidate('ingoo1').send({from:'0x36eB10bAa364188DD13968a547260f96824E142F'})

contract.methods.totalVotesFor('ingoo1').call().then(data=>{
// ingoo1 에 대한 총 투표수를 반환하는 녀석
    console.log(data)
})
$ node deploy

 계속 해보면 숫자 카운팅이 늘어난다.

 


# 만약 가나쉬를 껐으면?

더보기

어케해야할까??

다시 세팅을 해줘야 할 것들이 있다!

 

가나쉬는 변수에 저장 해놓은거기 때문에

다시 켜면 리셋된 상태에서 시작해야 한다.

 

배포부터 시작해야함

deploy.js

에서 deployContract.delploy 부분을 다시 해주되 

주소값이 달라졌으니까 그 부분을 바꿔서 해주면 된다.

 

그러면 

Contract created 가 새로 나옴

 

그럼 저 주소를 index.js 에서 

deployAddress 를 이 값으로 변경해준다.

 

그리고 await send 에서도 투표자의 주소를 새로운것으로 변경해주면 된다.


index.js 에서

//가나쉬 연결을 해야함
const web3 = new Web3(new Web3.providers.HttpProvider('http://localhost:8545'))

// 브라우저에서 쓰는 js 에서는 fs 가 안되기때문에 그냥 텍스트로 작성
const ABI = JSON.parse(`[{"inputs":[{"internalType":"string[]","name":"_candidateNames","type":"string[]"}],"stateMutability":"nonpayable","type":"constructor"},{"inputs":[{"internalType":"uint256","name":"","type":"uint256"}],"name":"candidateList","outputs":[{"internalType":"string","name":"","type":"string"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"string","name":"_candidate","type":"string"}],"name":"totalVotesFor","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"string","name":"_candidate","type":"string"}],"name":"validCandidate","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"string","name":"_candidate","type":"string"}],"name":"voteForCandidate","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"string","name":"","type":"string"}],"name":"voteReceived","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"}]`)

// deployAddress : 배포했던 주소값
const deployAddress = `0xc5f4eea08888836d8947b3f724510c3a73e86151`

let VotingContract = new web3.eth.Contract(ABI, deployAddress)

let candidates = { "ingoo1": "candidate1", "ingoo2": "candidate2", "ingoo3": "candidate3" }

// page load 되면 init 함수 실행
window.addEventListener('DOMContentLoaded', init)
async function init() {
	// key 값만 빼와서 배열에 담아준다 => ['ingoo1','ingoo2','ingoo3']
    let candidateNames = Object.keys(candidates)
    
    for (let i = 0; i < candidateNames.length; i++) {   // 3
        let name = candidateNames[i]    // ingoo1

        /* 
          candidate1 값을 가져오고 싶다면?
          candidates[name]  
          -> candidates.ingoo1 과 같은 결과 값 
          -> 뽑아온 이유는 element 에서 id 값 선택 위해서
        */

        const nameElement = document.querySelector(`#${candidates[name]}`)
        nameElement.innerHTML = name
        // 여기까지 작성하고 브라우져로 확인

	// 이제 html 에서 cadidateCount 부분에 숫자를 채워줄거임
        const countElement = document.querySelector(`#candidateCount${i + 1}`)
        countElement.innerHTML = await VotingContract.methods.totalVotesFor(name).call()
    }
}

 

input box에 ingoo1 하면 투표 +1 되고 다시 값 받아와서 화면에 뿌려주는

// 페이지 로드됐을때 하는게 아니니까 init 함수 밖에다 써주고 
let btn = document.querySelector(`#btn`)
btn.addEventListener('click',btnEvent)

async function btnEvent(){
    let candidateName = document.querySelector('#candidateName').value
    await VotingContract.methods.voteForCandidate(candidateName).send({ from: '0x36eB10bAa364188DD13968a547260f96824E142F' })
    
    let candidateCount = await VotingContract.methods.totalVotesFor(candidateName).call()
    
    let number = candidateName.charAt(candidateName.length-1)
    let countElement = document.querySelector(`#candidateCount${number}`)
    countElement.innerHTML = candidateCount
}

이 부분 추가