Lottery Dapp 개발 실습

2021. 10. 26. 16:13Blockchain/Project - Coin Swap

준비물

 

  1. Node.js
  2. Visual Studio Code
  3. Visual Studio Code - Solidity Extension
  4. Truffle
  5. Ganache-cli
  6. MetaMask

실습 진행

 

- Truffle 을 활용한 스마트 컨트랙트 상호작용

더보기
  1. Truffle init
  2. 가나슈와 메타마스크 연결
  3. /contracs/Lottery.sol 파일 생성 후 코드 작성
    pragma solidity >=0.4.22 <0.9.0;
    
    contract Lottery {
    
    }
  4. truffle compile // 컴파일
    파일 생성 확인
  5. /migrations/1_initial_migration.js 파일 복사 후 2_deploy_smart_contracts.js로 파일명 변경 후 코드 작성
    const Lottery = artifacts.require("Lottery");
    
    module.exports = function (deployer) {
      deployer.deploy(Lottery);
    };
    /migrations/
  6. truffle-config.js 파일 코드 수정
    44~48번째 줄 주석 해제
    82~84번째 줄 주석 해제 후 버젼 수정
  7. truffle migrate // 배포
    truffle migrate
  8. /contracs/Lottery.sol 파일 코드 작성
    // pragma solidity >=0.4.22 <0.9.0;
    pragma solidity ^0.6.0;
    
    contract Lottery {
        address public owner;
    
        constructor() public {
            owner = msg.sender;
        }
    
        function getSomeValue() public pure returns(uint256 value) {
            return 5;
        }
    }
  9. truffle migrate --reset --all // 재배포
    truffle migrate --reset --all // 코드를 더 쓴 만큼 더 많은 내용을 블록체인에 저장해야하기때문에 더 많은 수수료(gas)가 사용(used)되었다는 것을 알 수 있음
  10. Windows PowerShell에서 프로젝트의 root directory까지 이동 후
    truffle console
    truffle console
  11. web3
    web3 // 사용 가능한 여러 함수나 오브젝트들이 출력 됨
     
  12. eth = web3.eth // 편의를 위해 web3.eth를 eth라는 새로운 변수에 담아 사용
    eth
    eth == we3.eth
    eth. 까지 입력하고 탭 두번 누르면 사용 할 수 있는 변수를 확인 가능
    eth. [Tab]*2
  13. eth.getAccounts();
    eth.getAccounts();
    가나슈의 address값과 개수와 내용이 일치함
  14. eth.getBalance('0x2209B8d48f767F0d33D3f3C921CF9189e3d519FA');
    eth.getBalance(' 0x2209B8d48f767F0d33D3f3C921CF9189e3d519FA' );
    입력한 address값과 가나슈의 balance가 일치함
  15. Lottery.address
    Lottery.address
    /build/contracts/Lottery.json/ 파일 내 604번째 줄의 address와 일치함
  16. Lottery.deployed().then(function(instance){lt=instance}); // async 함수이기 때문에 callback을 달아줌
    lt 변수 안에 instance를 담아줌
    Lottery.deployed().then(function(instance){lt=instance});
  17. lt
    lt
    lt. 까지 입력하고 탭 두번 누르면 사용 할 수 있는 변수를 확인 가능
    eth. [Tab]*2
  18. lt.abi
    lt.abi // lt에서 사용 할 수 있는 함수를 abi 형태로 확인 가능
    ※ ABI란? 인터페이스!
    외부에서 접근 시 해당 스마트 컨트랙트에서 어떤 함수를 사용 할 수 있고, 어떤 파라미터가 있는지 리턴값은 뭔지 확인 가능 
  19. lt.owner();
    lt.owner();
    가나슈 0번째 인덱스의 address 값과 일치함
  20. lt.getSomeValue();
    lt.getSomeValue(); // BN = Big Number, 이더리움은 다루는 숫자가 크기 때문에 이렇게 사용됨

 

- Truffle을 사용한 테스트

더보기
  1. /test/lottery.test.js 파일 생성 후 코드 작성
    const Lottery = artifacts.require("Lottery");
    
    contract('Lottery', function([deployer, user1, user2]){
    
    });
    
    // deployer : 가나슈 0번 인덱스의 address
    // user1 : 가나슈 1번 인덱스의 address
    // user2 : 가나슈 2번 인덱스의 address
    ※ 가장 기본적인 모카 테스트의 구조 (위) / 테스트 코드 작성 완료 (아래)
  2. const Lottery = artifacts.require("Lottery"); contract('Lottery', function ([deployer, user1, user2]) { beforEach(async () => { console.log('Basic each'); }); it('Basic test', async () => { console.log('Basic test'); }); });​
  3. truffle test test/lottery.test.js // 테스트
    ※ truffle test만 입력 할 경우 test 폴더 내 전체 파일 테스트가 진행 됨
    truffle test test/lottery.test.js
  4. /test/lottery.test.js 파일 코드 수정
    const Lottery = artifacts.require("Lottery");
    
    contract('Lottery', function ([deployer, user1, user2]) {
        let lottery;
    
        beforeEach(async () => {
            console.log('Basic each');
            lottery = await Lottery.new(); // 배포, 이렇게 테스트용 배포 코드를 작성해서 사용하는게 좋음
        });
    
        it('Basic test', async () => {
            console.log('Basic test');
            let owner = await lottery.owner();
            let value = await lottery.getSomeValue();
    
            console.log(`owner : ${owner}`);
            console.log(`value : ${value}`);
    
            // 값을 확인 할 때 차이를 확인하기위해 assert 구문을 사용, truffle 내장
            assert.equal(value, 5);
        });
    });​
  5. truffle test test/lottery.test.js // 재테스트
    truffle test test/lottery.test.js

 

- Dapp 설계 방법 및 Lottery 규칙

더보기
  1. 사전 설명
    Dapp 서비스 설계
    Lottery 규칙
    ※ 보낸 돈 : 0.005 ETH (10 ** 15 wei)

 

- Lottery Domain 및 Queue 설계, Lottery Bet 함수 구현, Lottery Bet 테스트

더보기
  1. /contracts/Lottery.sol 코드 수정
    // pragma solidity >=0.4.22 <0.9.0;
    pragma solidity ^0.6.0;
    
    contract Lottery {
        struct BetInfo {
            uint256 answerBlockNumber;  // 정답 블록 넘버
            address payable better;     // 정답 시 여기로 돈을 보냄
            byte challenges;           // 문제, 0xab....
        }
    
        address public owner;
    
        uint256 private _pot; // 팟머니 저장소
    
        constructor() public {
            owner = msg.sender;
        }
    
        function getSomeValue() public pure returns(uint256 value) {
            return 5;
        }
    
        function getPot() public view returns(uint256 pot) {
            return _pot;
        }
    }
  2. 테스트 코드 작성
    const Lottery = artifacts.require("Lottery");
    
    contract('Lottery', function ([deployer, user1, user2]) {
        let lottery;
    
        beforeEach(async () => {
            console.log('Before each');
            lottery = await Lottery.new(); // 배포, 이렇게 테스트용 배포 코드를 작성해서 사용하는게 좋음
        });
    
        it('Basic test', async () => {
            console.log('Basic test');
            let owner = await lottery.owner();
            let value = await lottery.getSomeValue();
    
            console.log(`owner : ${owner}`);
            console.log(`value : ${value}`);
    
            assert.equal(value, 5);
        });
    
        // .only() : 테스트 시 특정 테스트 케이스만 테스트
        it.only('getPot sholud return current pot', async () => {
            let pot = await lottery.getPot();
    
            assert.equal(pot, 0);
        });
    });
  3. truffle test test/lottery.test.js
    truffle test test/lottery.test.js // .ony() 사용으로 인해 console.log('Basic test')가 출력 되지 않는 모습
  4. /contracts/Lottery.sol 코드 수정
    전역으로 사용 할 수 있는 변수 확인 가능(링크 : https://docs.soliditylang.org/en/v0.8.9/units-and-global-variables.html)
    // pragma solidity >=0.4.22 <0.9.0;
    pragma solidity ^0.6.0;
    
    contract Lottery {
        struct BetInfo {
            uint256 answerBlockNumber;  // 정답 블록 넘버
            address payable bettor;     // 정답 시 여기로 돈을 보냄
            byte challenges;            // 문제, 0xab....
        }
    
        // 맵을 이용하여 선형 큐 설계 (다이나믹 리스트 or 큐로 가능)
        uint256 private _tail;
        uint256 private _head;
        mapping (uint256 => BetInfo) private _bets; // 여기로 값이 들어오면 tail이 증가하고, 검증은 head부터 시작
        
        address public owner;
    
        // 상수 정의
        uint256 constant internal BLOCK_LIMIT = 256; // 블록 해쉬 제한
        uint256 constant internal BET_BLOCK_INTERVAL = 3; // +3번째 규칙 추가, 유저가 던진 트랜잭션이 들어가는 블록 +3의 블록해쉬
        uint256 constant internal BET_AMOUNT = 5 * 10 ** 15; // 배팅 금액을 0.005 ETH로 고정
    
        uint256 private _pot; // 팟머니 저장소
    
        event BET(uint256 index, address bettor, uint256 amount, byte challenges, uint256 answerBlockNumber);
    
        constructor() public {
            owner = msg.sender;
        }
    
        // function getSomeValue() public pure returns(uint256 value) {
        //     return 5;
        // }
    
        function getPot() public view returns(uint256 pot) {
            return _pot;
        }
    
        /**
         * @dev 배팅 시 유저는 0.005 ETH와 1 byte 크기의 배팅용 글자를 보내야 함
         * @param challenges 배팅 시 유저가 보내는 글자
         * return 함수가 잘 수행되었는지 확인하는 bool값
         * 큐에 저장 된 배팅 정보는 이후 distribute 함수에서 해결 됨
         */
        // Bet(배팅)
        function bet(byte challenges) public payable returns (bool result) {
            // 돈이 제대로 들어오는지 확인
            require(msg.value == BET_AMOUNT, "Not enough ETH");
    
            // 배팅 정보를 큐에 저장
            require(pushBet(challenges), "Fail to add a new Bet Info");
    
            // 이벤트 로그 출력
            emit BET(_tail - 1, msg.sender, msg.value, challenges, block.number + BET_BLOCK_INTERVAL);
    
            return true;
        }
    
        // Distribute(검증), 값이 틀리면 팟머니에 저장, 맞으면 돌리는 연산
        
    
        function getBetInfo(uint256 index) public view returns(uint256 answerBlockNumber, address bettor, byte challenges) {
            BetInfo memory b = _bets[index]; // 인덱스가 3번까지만 저장되어있더라도 5번에 있는 값을 다 불러 올 수 있고, 다만 그 값들은 0으로 초기화 되어있음
            answerBlockNumber = b.answerBlockNumber;
            bettor = b.bettor;
            challenges = b.challenges;
        }
        function pushBet(byte challenges) internal returns (bool) {
            BetInfo memory b;
            b.bettor = msg.sender;
            b.answerBlockNumber = block.number + BET_BLOCK_INTERVAL; // block.number : 현재 이 트랜잭션에 들어가는 블록의 값
            b.challenges = challenges;
    
            _bets[_tail] = b;
            _tail++; // safemath? integerOverflow?
    
            return true;
        }
        function popBet(uint256 index) internal returns (bool) {
            // map에 있는 값을 삭제 = 상태 데이터베이스의 값을 삭제
            // 삭제 시 가스를 돌려받음
            delete _bets[index];// 필요하지 않은 값에 대해서는 삭제를 해주는게 좋음
            return true;
        }
    }
  5. /test/lottery.test.js 테스트 코드 수정 후 테스트
    const Lottery = artifacts.require("Lottery");
    
    contract('Lottery', function ([deployer, user1, user2]) {
        let lottery;
    
        beforeEach(async () => {
            console.log('Before each');
            lottery = await Lottery.new(); // 배포, 이렇게 테스트용 배포 코드를 작성해서 사용하는게 좋음
        });
    
        // it('Basic test', async () => {
        //     console.log('Basic test');
        //     let owner = await lottery.owner();
        //     let value = await lottery.getSomeValue();
    
        //     console.log(`owner : ${owner}`);
        //     console.log(`value : ${value}`);
    
        //     assert.equal(value, 5);
        // });
    
        // .only() = 모카 테스트 시 특정 테스트 케이스만 테스트
        it('getPot sholud return current pot', async () => {
            let pot = await lottery.getPot();
    
            assert.equal(pot, 0);
        });
    
        describe('Bet', function () {
            it.only('should fail when the bet money is not 0.005 ETH', async () => {
                // Fail transaction
                await lottery.bet('0xab', { from: user1, value: 4000000000000000 });
                
            });
            it('should put the bet to the bet queue with 1 bet', async () => {
                // 배팅
    
                // 컨트랙트 발생 시 밸런스 체크 == 0.005 ETH
                
                // 배팅 정보 확인
    
                // 로그 확인
            });
        });
    });
    truffle test test/lottery.test.js // value에 숫자를 일부러 작게 작성하였기에 테스트가 실패하며 .sol 파일에 작성했던 "Not enough ETH" 가 확인 된 모습
    truffle test test/lottery.test.js // 테스트 코드 await lottery.bet() 안의 value값을 5000000000000000으로 변경하여 재테스트 시 테스트에 성공하는 모습
  6. /test/assertRevert.js 파일 생성 후 코드 작성
    module.exports = async (promise) => {
        try {
            await promise;
            assert.fail('Expected revert not received');
        } catch (error) { // error를 e로 축약하여 작성하면 테스트 시 error를 찾을 수 없다고 오류 발생
            const revertFound = error.message.search('revert') >= 0;
            assert(revertFound, `Expected "revert", got ${error} instead`);
        }
    }
  7. /test/lottery.test.js 파일 코드 수정 후 테스트
    // 발생한 에러를 assertRevert()에서 try/catch문으로 받음
    // await lottery.bet('0xab', { from: user1, value: 4000000000000000 });
    await assertRevert('0xab', { from: user1, value: 4000000000000000 });
    실패한 트랜잭션이 잘 catch 된 모습
  8. /test/lottery.test.js 파일 코드 수정 후 테스트
    it.only('should put the bet to the bet queue with 1 bet', async () => {
        // 배팅
        let receipt = await lottery.bet('0xab', { from: user1, value: 5000000000000000});
        // console.log(receipt);
    
        // 컨트랙트 발생 시 밸런스 체크 == 0.005 ETH
        
        // 배팅 정보 확인
    
        // 로그 확인
    });​
    truffle test test/lottery.test.js // console.log(receipt) -> receipt에는 다양한 정보가 들어가있음을 확인 가능
  9. npm install chai // 필요한 패키지 인스톨
    test/expectEvent.js 파일 생성 후 코드 작성
    const assert = require('chai').assert;
    
    // console.log(receipt) 했을 때 나왔던 logs를 inLogs에 넣어줌
    // 찾고자하는 문자열을 넣어줬을 때 logs 안에 있는 배열에서 찾고, 있으면 실행
    const inLogs = async (logs, eventName) => {
        const event = logs.find(e => e.event === eventName);
        assert.exists(event);
    }
    
    module.exports = {
        inLogs,
    }
  10. 테스트
    truffle test test/lottery.test.js // 테스트 성공
    이벤트명 변경 후 다시 테스트
    truffle test test/lottery.test.js // 존재하지 않는 이벤트이므로 테스트에 실패하는 모습
  11. 여기까지 /test/lottery.test.js 파일의 전체 코드
    const Lottery = artifacts.require("Lottery");
    const assertRevert = require("./assertRevert");
    const expectEvent = require("./expectEvent");
    
    contract('Lottery', function ([deployer, user1, user2]) {
        let lottery;
        let betAmount = 5 * 10 ** 15; // 5000000000000000
        let bet_block_interval = 3;
    
        beforeEach(async () => {
            // console.log('Before each');
            lottery = await Lottery.new(); // 배포, 이렇게 테스트용 배포 코드를 작성해서 사용하는게 좋음
        });
    
        // it('Basic test', async () => {
        //     console.log('Basic test');
        //     let owner = await lottery.owner();
        //     let value = await lottery.getSomeValue();
    
        //     console.log(`owner : ${owner}`);
        //     console.log(`value : ${value}`);
    
        //     assert.equal(value, 5);
        // });
    
        // .only() = 모카 테스트 시 특정 테스트 케이스만 테스트
        it('getPot sholud return current pot', async () => {
            let pot = await lottery.getPot();
    
            assert.equal(pot, 0);
        });
    
        describe.only('Bet', function () {
            it('should fail when the bet money is not 0.005 ETH', async () => {
                // Fail transaction
                // 발생한 에러를 assertRevert()에서 try/catch문으로 받음
                await assertRevert('0xab', { from: user1, value: 4000000000000000 });
                // transaction object {chainId, value, to, form, gas(Limit), gasPrice}
            });
    
            it('should put the bet to the bet queue with 1 bet', async () => {
                // 배팅
                let receipt = await lottery.bet('0xab', { from: user1, value: betAmount});
                // console.log(receipt);
                let pot = await lottery.getPot();
                assert.equal(pot, 0);
    
                // 컨트랙트 발생 시 밸런스 체크 == 0.005 ETH
                let contractBalance = await web3.eth.getBalance(lottery.address);
                assert.equal(contractBalance, betAmount);
                
                // 배팅 정보 확인
                let currentBlockNumber = await web3.eth.getBlockNumber();
                let bet = await lottery.getBetInfo(0);
            
                assert.equal(bet.answerBlockNumber, currentBlockNumber + bet_block_interval);
                assert.equal(bet.bettor, user1);
                assert.equal(bet.challenges, '0xab');
    
                // 로그 확인
                await expectEvent.inLogs(receipt.logs, 'BET');
            });
        });
    });
  12.  

 

- 이더리움 GAS 계산

더보기
  • 이더리움의 수수료
  • GAS 계산

 

  1. Windows PowerShell에서 프로젝트의 root directory까지 이동
    trurffle migrate --reset
    trurffle migrate --reset // 배포 중..
    trurffle migrate --reset // 배포 완료
  2. truffle console // 콘솔과 상호작용
    Lottery.deployed().then(function(instance){lt=instance}); // lt 변수 안에 instance를 담아줌
    완료 화면
  3. web3.eth.getAccounts();
    let bettor = '0x5761AE4134726785b2E3Aa67b80b75bc8d034708'; // 사용 할 account를 변수에 담음
  4. lt.bet("0xab", { from: bettor, value: 5000000000000000, gas: 300000 });
    첫 번째 transaction의 gasUsed: 89245
    lt.bet... 한번 더 실행 // 두 번째 transaction의 gasUsed: 74245
    * 첫 번째 transaction에 사용 된 gasUsed : 89245
    첫 실행하여 새로 저장 약 20000 GAS + bet() 함수 실행 시 코드의 양이나 연산에 따라 소비되는 GAS의 양 약 60000 GAS + 이벤트 약 5000 GAS = 약 85000 GAS
    * 두 번째 transaction에 사용 된 gasUsed : 74245
    (새로 저장이 아닌)기존 변수에 있는 값을 바꿀 때 5000 GAS + 나머지 = 약 70000 GAS
    첫 실행하여 새로 저장 약 20000 GAS - 기존 변수에 있는 값을 바꿀 때 5000 GAS = 15000 GAS // 첫 번째와 두 번째 transaction에 들어가는 GAS가 딱 15000만큼 차이나는 것을 알 수 있음

 

- Lottery Distribute 함수 설계, Lottery isMatch 함수 구현 및 테스트, Lottery Distribute 테스트

더보기
  1. /contracts/Lottery.sol 파일 코드 수정 후 컴파일하여 정상 작동 확인
    // pragma solidity >=0.4.22 <0.9.0;
    pragma solidity ^0.6.0;
    
    contract Lottery {
        struct BetInfo {
            uint256 answerBlockNumber;  // 정답 블록 넘버
            address payable bettor;     // 정답 시 여기로 돈을 보냄
            byte challenges;            // 문제, 0xab....
        }
    
        // 맵을 이용하여 선형 큐 설계 (다이나믹 리스트 or 큐로 가능)
        uint256 private _tail;
        uint256 private _head;
        mapping (uint256 => BetInfo) private _bets; // 여기로 값이 들어오면 tail이 증가하고, 검증은 head부터 시작
        
        address public owner;
    
        // 상수 정의
        uint256 constant internal BLOCK_LIMIT = 256; // 블록 해쉬 제한
        uint256 constant internal BET_BLOCK_INTERVAL = 3; // +3번째 규칙 추가, 유저가 던진 트랜잭션이 들어가는 블록 +3의 블록해쉬
        uint256 constant internal BET_AMOUNT = 5 * 10 ** 15; // 배팅 금액을 0.005 ETH로 고정
    
        uint256 private _pot; // 팟머니 저장소
    
        enum BlockStatus { Checkable, NotRevealed, BlockLimitPassed }
        enum BettingResult { Fail, Win, Draw }
    
        event BET(uint256 index, address bettor, uint256 amount, byte challenges, uint256 answerBlockNumber);
    
        constructor() public {
            owner = msg.sender;
        }
    
        // function getSomeValue() public pure returns(uint256 value) {
        //     return 5;
        // }
    
        function getPot() public view returns(uint256 pot) {
            return _pot;
        }
    
        /**
         * @dev 배팅 시 유저는 0.005 ETH와 1 byte 크기의 배팅용 글자를 보내야 함
         * @param challenges 배팅 시 유저가 보내는 글자
         * return : 함수가 잘 수행되었는지 확인하는 bool값
         * 큐에 저장 된 배팅 정보는 이후 distribute 함수에서 해결 됨
         */
        // Bet(배팅)
        function bet(byte challenges) public payable returns (bool result) {
            // 돈이 제대로 들어오는지 확인
            require(msg.value == BET_AMOUNT, "Not enough ETH");
    
            // 배팅 정보를 큐에 저장
            require(pushBet(challenges), "Fail to add a new Bet Info");
    
            // 이벤트 로그 출력
            emit BET(_tail - 1, msg.sender, msg.value, challenges, block.number + BET_BLOCK_INTERVAL);
    
            return true;
        }
    
        // Distribute(검증), 값이 틀리면 팟머니에 저장, 맞으면 돌리는 연산
        function distribute() public {
            // Queue에 저장 된 배팅 정보 -> head 3 4 5 6 7 8 9 10 (새로운 정보는 여기서부터)11 22 tail
            // 언제 멈추는지? 더 이상 정답을 확인 할 수 없을 때(정답 배팅을 한 블록이 아직 채굴되지 않았을 때)
            uint256 cur;
            BetInfo memory b;
            BlockStatus currentBlockStatus;
    
            for (cur = _head; cur < _tail; cur++) {
                b = _bets[cur];
                currentBlockStatus = getBlockStatus(b.answerBlockNumber); // 현재 블록의 상태
                
                // Checkable, 확인 가능 할 때
                // block.number > answerBlockNumber && block.number < BlOCK_LIMIT + answerBlockNumber
                if (currentBlockStatus == BlockStatus.Checkable) { 
                    // if win : bettor가 팟머니를 가져감
                    
                    // if fail : bettor의 돈이 팟으로 감
    
                    // if darw(한글자만 맞췄을 때) : bettor의 돈이 환불이 됨
                }
    
                // NotRevealed, 블록 체크가 불가능 할 때(아직 채굴되지 않았을 때)
                // block.number <= answerBlockNumber
                if (currentBlockStatus == BlockStatus.NotRevealed) { 
                    break;
                }
    
                // BlockLimitPassed, 블록 제한이 지났을 때
                // block.number >= answerBlockNumber + BLOCK_LIMIT
                if (currentBlockStatus == BlockStatus.BlockLimitPassed) { 
                    // 환불
                    // emit refund
                }
    
                // 정답 체크
                popBet(cur);
            }
        }
    
        /**
         * @dev 배팅 글자와 정답을 확인
         * @param challenges 배팅 글자
         * @param answer 블록해쉬
         * return : 정답 결과
         */
        function isMatch(byte challenges, bytes32 answer) public pure returns (BettingResult) {
            // challenges 0xab      // 1 byte
            // answer 0xab....ff    // 32 bytes
            
            // 순서대로 글자를 뽑아서 비교
            byte c1 = challenges;
            byte c2 = challenges;
            
            byte a1 = answer[0];
            byte a2 = answer[0];
    
            // 첫 번째 숫자 가져오기(시프트 연산)
            c1 = c1 >> 4; // 오른쪽으로 시프팅, 0xab -> 0x0a
            c1 = c1 << 4; // 왼쪽으로 시프팅, 0x0a -> 0xa0
    
            a1 = a1 >> 4;
            a1 = a1 << 4;
    
            // 두 번째 숫자 가져오기
            c2 = c2 << 4; // 왼쪽으로 시프팅, 0xab -> 0xb0
            c2 = c2 >> 4; // 오른쪽으로 시프팅, 0xb0 -> 0x0b
    
            a2 = a2 << 4;
            a2 = a2 >> 4;
    
            if (a1 == c1 && a2 == c2) {
                return BettingResult.Win;
            }
            if (a1 == c1 || a2 == c2) {
                return BettingResult.Draw;
            }
            
            return BettingResult.Fail;
        }
    
        function getBlockStatus(uint256 answerBlockNumber) internal view returns (BlockStatus) {
            if (block.number > answerBlockNumber && block.number < BLOCK_LIMIT + answerBlockNumber) {
                return BlockStatus.Checkable;
            }
            if (block.number <= answerBlockNumber) {
                return BlockStatus.NotRevealed;
            }
            if (block.number >= answerBlockNumber + BLOCK_LIMIT) {
                return BlockStatus.BlockLimitPassed;
            }
            
            return BlockStatus.BlockLimitPassed; // default
        }
        
    
        function getBetInfo(uint256 index) public view returns(uint256 answerBlockNumber, address bettor, byte challenges) {
            BetInfo memory b = _bets[index]; // 인덱스가 3번까지만 저장되어있더라도 5번에 있는 값을 다 불러 올 수 있고, 다만 그 값들은 0으로 초기화 되어있음
            answerBlockNumber = b.answerBlockNumber;
            bettor = b.bettor;
            challenges = b.challenges;
        }
        function pushBet(byte challenges) internal returns (bool) {
            BetInfo memory b;
            b.bettor = msg.sender;
            b.answerBlockNumber = block.number + BET_BLOCK_INTERVAL; // block.number : 현재 이 트랜잭션에 들어가는 블록의 값
            b.challenges = challenges;
    
            _bets[_tail] = b;
            _tail++; // safemath? integerOverflow?
    
            return true;
        }
        function popBet(uint256 index) internal returns (bool) {
            // map에 있는 값을 삭제 = 상태 데이터베이스의 값을 삭제
            // 삭제 시 가스를 돌려받음
            delete _bets[index];// 필요하지 않은 값에 대해서는 삭제를 해주는게 좋음
            return true;
        }
    }​
    truffle compile // 컴파일 완료
  2. /test/lottery.test.js 파일 코드 수정(describe 추가) 후 테스트
    describe.only('isMatch', function () {
        // 32 bytes짜리 아무 해쉬값 가져와서 테스트하기 쉽게 3번째 글자 a로 변경
        let blockHash = '0xabec17438e4f0afb9cc8b77ce84bb7fd501497cfa9a1695095247daa5b4b7bcc';
    
        // Win
        it('should be BettingResult.Win when two characters match', async () => {    
            let matchingResult = await lottery.isMatch('0xab', blockHash);
            assert.equal(matchingResult, 1);
        });
    
        // Fail
        it('should be BettingResult.Fail when two characters match', async () => {    
            let matchingResult = await lottery.isMatch('0xcd', blockHash);
            assert.equal(matchingResult, 0);
        });
    
        // Draw
        it('should be BettingResult.Draw when two characters match', async () => {    
            let matchingResult = await lottery.isMatch('0xaf', blockHash);
            assert.equal(matchingResult, 2);
    
            matchingResult = await lottery.isMatch('0xfb', blockHash);
            assert.equal(matchingResult, 2);
        });
    });​
    truffle test test/lottery.test.js // 테스트 성공
  3. /contracts/Lottery.sol 코드 수정 후 컴파일
    // pragma solidity >=0.4.22 <0.9.0;
    pragma solidity ^0.6.0;
    
    contract Lottery {
        struct BetInfo {
            uint256 answerBlockNumber;  // 정답 블록 넘버
            address payable bettor;     // 정답 시 여기로 돈을 보냄
            byte challenges;            // 문제, 0xab....
        }
    
        // 맵을 이용하여 선형 큐 설계 (다이나믹 리스트 or 큐로 가능)
        uint256 private _tail;
        uint256 private _head;
        mapping (uint256 => BetInfo) private _bets; // 여기로 값이 들어오면 tail이 증가하고, 검증은 head부터 시작
        
        address payable public owner;
    
        // 상수 정의
        uint256 constant internal BLOCK_LIMIT = 256; // 블록 해쉬 제한
        uint256 constant internal BET_BLOCK_INTERVAL = 3; // +3번째 규칙 추가, 유저가 던진 트랜잭션이 들어가는 블록 +3의 블록해쉬
        uint256 constant internal BET_AMOUNT = 5 * 10 ** 15; // 배팅 금액을 0.005 ETH로 고정
    
        uint256 private _pot; // 팟머니 저장소
    
        // blockhash()는 랜덤값이기 때문에 테스트에 별로 좋지 않음
        // 그래서 간단한 모드를 만들어 바꿔가면서 테스트 진행
        bool private mode = false; // false: use answer for test
        bytes32 public answerForTest; // true: use real block hash
    
        // enum
        enum BlockStatus { Checkable, NotRevealed, BlockLimitPassed }
        enum BettingResult { Fail, Win, Draw }
    
        // event
        event BET(uint256 index, address bettor, uint256 amount, byte challenges, uint256 answerBlockNumber);
        event WIN(uint256 index, address bettor, uint256 amount, byte challenges, byte answer, uint256 answerBlockNumber);
        event FAIL(uint256 index, address bettor, uint256 amount, byte challenges, byte answer, uint256 answerBlockNumber);
        event DRAW(uint256 index, address bettor, uint256 amount, byte challenges, byte answer, uint256 answerBlockNumber);
        event REFUND(uint256 index, address bettor, uint256 amount, byte challenges, uint256 answerBlockNumber);
    
        constructor() public {
            owner = msg.sender;
        }
    
        // function getSomeValue() public pure returns(uint256 value) {
        //     return 5;
        // }
    
        function getPot() public view returns(uint256 pot) {
            return _pot;
        }
    
        /**
         * @dev 배팅과 정답 체크를 함
         * @param challenges 배팅 시 유저가 보내는 글자
         * return : 함수가 잘 수행되었는지 확인하는 bool값
         */
        function betAndDistribute(byte challenges) public payable returns (bool result) {
            bet(challenges);
            distribute();
    
            return true;
        }
    
        /**
         * @dev 배팅 시 유저는 0.005 ETH와 1 byte 크기의 배팅용 글자를 보내야 함
         * @param challenges 배팅 시 유저가 보내는 글자
         * return : 함수가 잘 수행되었는지 확인하는 bool값
         * 큐에 저장 된 배팅 정보는 이후 distribute 함수에서 해결 됨
         */
        // Bet(배팅)
        function bet(byte challenges) public payable returns (bool result) {
            // 돈이 제대로 들어오는지 확인
            require(msg.value == BET_AMOUNT, "Not enough ETH");
    
            // 배팅 정보를 큐에 저장
            require(pushBet(challenges), "Fail to add a new Bet Info");
    
            // 이벤트 로그 출력
            emit BET(_tail - 1, msg.sender, msg.value, challenges, block.number + BET_BLOCK_INTERVAL);
    
            return true;
        }
    
        /**
         * @dev 배팅 결과값을 확인하고 팟머니를 분배
         * 정답 실패 : 팟머니 축적, 정답 맞춤 : 팟머니 획득, 한글자 맞춤 or 정답 확인 불가 : 배팅 금액만 환불
         */
        // Distribute(검증), 값이 틀리면 팟머니에 저장, 맞으면 돌리는 연산
        function distribute() public {
            // Queue에 저장 된 배팅 정보 -> head 3 4 5 6 7 8 9 10 (새로운 정보는 여기서부터)11 22 tail
            // 언제 멈추는지? 더 이상 정답을 확인 할 수 없을 때(정답 배팅을 한 블록이 아직 채굴되지 않았을 때)
            uint256 cur;
            uint256 transferAmount;
            BetInfo memory b;
            BlockStatus currentBlockStatus;
            BettingResult currentBettingResult;
    
            for (cur = _head; cur < _tail; cur++) {
                b = _bets[cur];
                currentBlockStatus = getBlockStatus(b.answerBlockNumber); // 현재 블록의 상태
                
                // Checkable, 확인 가능 할 때
                // block.number > answerBlockNumber && block.number < BlOCK_LIMIT + answerBlockNumber
                if (currentBlockStatus == BlockStatus.Checkable) {
                    bytes32 answerBlockHash = getAnswerBlockHash(b.answerBlockNumber);
                    currentBettingResult = isMatch(b.challenges, answerBlockHash);
    
                    // if win : bettor가 팟머니를 가져감
                    if (currentBettingResult == BettingResult.Win) {
                        // 팟머니 이동 후 0으로 초기화
                        transferAmount = transferAfterPayingFee(b.bettor, _pot + BET_AMOUNT);
                        _pot = 0; // transfer가 아닌 call이나 send 사용 시 순서를 위로
    
                        emit WIN(cur, b.bettor, transferAmount, b.challenges, answerBlockHash[0], b.answerBlockNumber);
                    }
                    
                    // if fail : bettor의 돈이 팟으로 감
                    if (currentBettingResult == BettingResult.Fail) {
                        // 팟머니 + 배팅 금액
                        _pot += BET_AMOUNT;
    
                        emit FAIL(cur, b.bettor, 0, b.challenges, answerBlockHash[0], b.answerBlockNumber);
                    }
    
                    // if darw(한글자만 맞췄을 때) : bettor의 돈이 환불이 됨
                    if (currentBettingResult == BettingResult.Draw) {
                        // 배팅한 돈만큼 환불
                        transferAmount = transferAfterPayingFee(b.bettor, BET_AMOUNT);
    
                        emit DRAW(cur, b.bettor, transferAmount, b.challenges, answerBlockHash[0], b.answerBlockNumber);
                    }
                }
    
                // NotRevealed, 블록 체크가 불가능 할 때(아직 채굴되지 않았을 때)
                // block.number <= answerBlockNumber
                if (currentBlockStatus == BlockStatus.NotRevealed) { 
                    break;
                }
    
                // BlockLimitPassed, 블록 제한이 지났을 때
                // block.number >= answerBlockNumber + BLOCK_LIMIT
                if (currentBlockStatus == BlockStatus.BlockLimitPassed) { 
                    // 환불
                    transferAmount = transferAfterPayingFee(b.bettor, BET_AMOUNT);
    
                    emit REFUND(cur, b.bettor, transferAmount, b.challenges, b.answerBlockNumber);
                }
    
                // 정답 체크
                popBet(cur);
            }
            _head = cur; // 헤드 업데이트
        }
    
        function transferAfterPayingFee(address payable addr, uint256 amount) internal returns (uint256) {
            // uint256 fee = amount / 100; // 수수료
            uint256 fee = 0;
            uint256 amountWithoutFee = amount - fee;
    
            // transfer to addr
            addr.transfer(amountWithoutFee);
    
            // transfer to owner
            owner.transfer(fee);
    
            // 스마트 컨트랙트에서 이더를 전송하는 방법
            // 1: call, 2: send, **3: transfer
    
            return amountWithoutFee;
        }
    
        function setAnswerForTest(bytes32 answer) public returns (bool result) {
            require(msg.sender == owner, "Only owner can set the answer for test mode");
            answerForTest = answer;
            return true;
        }
        function getAnswerBlockHash(uint256 answerBlockNumber) internal view returns (bytes32 answer) {
            return mode ? blockhash(answerBlockNumber) : answerForTest;
        }
    
        /**
         * @dev 배팅 글자와 정답을 확인
         * @param challenges 배팅 글자
         * @param answer 블록해쉬
         * return : 정답 결과
         */
        function isMatch(byte challenges, bytes32 answer) public pure returns (BettingResult) {
            // challenges 0xab      // 1 byte
            // answer 0xab....ff    // 32 bytes
            
            // 순서대로 글자를 뽑아서 비교
            byte c1 = challenges;
            byte c2 = challenges;
            
            byte a1 = answer[0];
            byte a2 = answer[0];
    
            // 첫 번째 숫자 가져오기(시프트 연산)
            c1 = c1 >> 4; // 오른쪽으로 시프팅, 0xab -> 0x0a
            c1 = c1 << 4; // 왼쪽으로 시프팅, 0x0a -> 0xa0
    
            a1 = a1 >> 4;
            a1 = a1 << 4;
    
            // 두 번째 숫자 가져오기
            c2 = c2 << 4; // 왼쪽으로 시프팅, 0xab -> 0xb0
            c2 = c2 >> 4; // 오른쪽으로 시프팅, 0xb0 -> 0x0b
    
            a2 = a2 << 4;
            a2 = a2 >> 4;
    
            if (a1 == c1 && a2 == c2) {
                return BettingResult.Win;
            }
            if (a1 == c1 || a2 == c2) {
                return BettingResult.Draw;
            }
    
            return BettingResult.Fail;
        }
    
        function getBlockStatus(uint256 answerBlockNumber) internal view returns (BlockStatus) {
            if (block.number > answerBlockNumber && block.number < BLOCK_LIMIT + answerBlockNumber) {
                return BlockStatus.Checkable;
            }
            if (block.number <= answerBlockNumber) {
                return BlockStatus.NotRevealed;
            }
            if (block.number >= answerBlockNumber + BLOCK_LIMIT) {
                return BlockStatus.BlockLimitPassed;
            }
            
            return BlockStatus.BlockLimitPassed; // default
        }
        
    
        function getBetInfo(uint256 index) public view returns(uint256 answerBlockNumber, address bettor, byte challenges) {
            BetInfo memory b = _bets[index]; // 인덱스가 3번까지만 저장되어있더라도 5번에 있는 값을 다 불러 올 수 있고, 다만 그 값들은 0으로 초기화 되어있음
            answerBlockNumber = b.answerBlockNumber;
            bettor = b.bettor;
            challenges = b.challenges;
        }
        function pushBet(byte challenges) internal returns (bool) {
            BetInfo memory b;
            b.bettor = msg.sender;
            b.answerBlockNumber = block.number + BET_BLOCK_INTERVAL; // block.number : 현재 이 트랜잭션에 들어가는 블록의 값
            b.challenges = challenges;
    
            _bets[_tail] = b;
            _tail++; // safemath? integerOverflow?
    
            return true;
        }
        function popBet(uint256 index) internal returns (bool) {
            // map에 있는 값을 삭제 = 상태 데이터베이스의 값을 삭제
            // 삭제 시 가스를 돌려받음
            delete _bets[index];// 필요하지 않은 값에 대해서는 삭제를 해주는게 좋음
            return true;
        }
    }
    컴파일 완료
  4. /test/lottery.test.js 코드 수정 후 테스트
    const Lottery = artifacts.require("Lottery");
    const assertRevert = require("./assertRevert");
    const expectEvent = require("./expectEvent");
    
    contract('Lottery', function ([deployer, user1, user2]) {
        let lottery;
        let betAmount = 5 * 10 ** 15; // 5000000000000000
        let betAmountBN = new web3.utils.BN('5000000000000000');
        let bet_block_interval = 3;
    
        beforeEach(async () => {
            // console.log('Before each');
            lottery = await Lottery.new(); // 배포, 이렇게 테스트용 배포 코드를 작성해서 사용하는게 좋음
        });
    
        // it('Basic test', async () => {
        //     console.log('Basic test');
        //     let owner = await lottery.owner();
        //     let value = await lottery.getSomeValue();
    
        //     console.log(`owner : ${owner}`);
        //     console.log(`value : ${value}`);
    
        //     assert.equal(value, 5);
        // });
    
        // .only() = 모카 테스트 시 특정 테스트 케이스만 테스트
        it('getPot sholud return current pot', async () => {
            let pot = await lottery.getPot();
    
            assert.equal(pot, 0);
        });
    
        describe('Bet', function () {
            it('should fail when the bet money is not 0.005 ETH', async () => {
                // Fail transaction
                // 발생한 에러를 assertRevert()에서 try/catch문으로 받음
                await assertRevert('0xab', { from: user1, value: 4000000000000000 });
                // transaction object {chainId, value, to, form, gas(Limit), gasPrice}
            });
    
            it('should put the bet to the bet queue with 1 bet', async () => {
                // 배팅
                let receipt = await lottery.bet('0xab', { from: user1, value: betAmount});
                // console.log(receipt);
                let pot = await lottery.getPot();
                assert.equal(pot, 0);
    
                // 컨트랙트 발생 시 밸런스 체크 == 0.005 ETH
                let contractBalance = await web3.eth.getBalance(lottery.address);
                assert.equal(contractBalance, betAmount);
                
                // 배팅 정보 확인
                let currentBlockNumber = await web3.eth.getBlockNumber();
                let bet = await lottery.getBetInfo(0);
            
                assert.equal(bet.answerBlockNumber, currentBlockNumber + bet_block_interval);
                assert.equal(bet.bettor, user1);
                assert.equal(bet.challenges, '0xab');
    
                // 로그 확인
                await expectEvent.inLogs(receipt.logs, 'BET');
            });
        });
    
        describe('Distribute', function () {
            describe.only('When the answer is checkable', function () {
                it('should give the user the pot when the answer matches', async () => {
                    // 두 글자 다 맞았을 때
                    await lottery.setAnswerForTest('0xabec17438e4f0afb9cc8b77ce84bb7fd501497cfa9a1695095247daa5b4b7bcc', { from: deployer });
    
                    await lottery.betAndDistribute('0xef', { from: user2, value: betAmount }); // 1 -> 4
                    await lottery.betAndDistribute('0xef', { from: user2, value: betAmount }); // 2 -> 5
                    await lottery.betAndDistribute('0xab', { from: user1, value: betAmount }); // 3 -> 6
                    await lottery.betAndDistribute('0xef', { from: user2, value: betAmount }); // 4 -> 7
                    await lottery.betAndDistribute('0xef', { from: user2, value: betAmount }); // 5 -> 8
                    await lottery.betAndDistribute('0xef', { from: user2, value: betAmount }); // 6 -> 9
    
                    let potBefore = await lottery.getPot(); // 0.01 ETH
                    let user1BalanceBefore = await web3.eth.getBalance(user1);
                    
                    // user1이 팟머니 획득
                    let receipt7 = await lottery.betAndDistribute('0xef', { from: user2, value: betAmount }); // 7 -> 10
                    
                    let potAfter = await lottery.getPot(); // 0 ETH
                    let user1BalanceAfter = await web3.eth.getBalance(user1); // before + 0.015 ETH
    
                    // 팟머니의 변화량 확인
                    assert.equal(potBefore.toString(), new web3.utils.BN('10000000000000000').toString());
                    assert.equal(potAfter.toString(), new web3.utils.BN('0').toString());
    
                    // 유저(승자)의 밸런스 확인
                    user1BalanceBefore = new web3.utils.BN(user1BalanceBefore);
                    assert.equal(user1BalanceBefore.add(potBefore).add(betAmountBN).toString(), new web3.utils.BN(user1BalanceAfter).toString());
                });
    
                it('should give the user the amount he or she bet when a single character matches', async () => {
                    // 한 글자만 맞았을 때
                    await lottery.setAnswerForTest('0xabec17438e4f0afb9cc8b77ce84bb7fd501497cfa9a1695095247daa5b4b7bcc', { from: deployer });
    
                    await lottery.betAndDistribute('0xef', { from: user2, value: betAmount }); // 1 -> 4
                    await lottery.betAndDistribute('0xef', { from: user2, value: betAmount }); // 2 -> 5
                    await lottery.betAndDistribute('0xaf', { from: user1, value: betAmount }); // 3 -> 6
                    await lottery.betAndDistribute('0xef', { from: user2, value: betAmount }); // 4 -> 7
                    await lottery.betAndDistribute('0xef', { from: user2, value: betAmount }); // 5 -> 8
                    await lottery.betAndDistribute('0xef', { from: user2, value: betAmount }); // 6 -> 9
    
                    let potBefore = await lottery.getPot(); // 0.01 ETH
                    let user1BalanceBefore = await web3.eth.getBalance(user1);
                    
                    // user1이 팟머니 획득
                    let receipt7 = await lottery.betAndDistribute('0xef', { from: user2, value: betAmount }); // 7 -> 10
                    
                    let potAfter = await lottery.getPot(); // 0.01 ETH
                    let user1BalanceAfter = await web3.eth.getBalance(user1); // before + 0.005 ETH
    
                    // 팟머니의 변화량 확인
                    assert.equal(potBefore.toString(), potAfter.toString());
    
                    // 유저(승자)의 밸런스 확인
                    user1BalanceBefore = new web3.utils.BN(user1BalanceBefore);
                    assert.equal(user1BalanceBefore.add(betAmountBN).toString(), new web3.utils.BN(user1BalanceAfter).toString());
                });
    
                it('should get the eth of user when the answer does not match at all', async () => {
                    // 다 틀렸을 때
                    await lottery.setAnswerForTest('0xabec17438e4f0afb9cc8b77ce84bb7fd501497cfa9a1695095247daa5b4b7bcc', { from: deployer });
    
                    await lottery.betAndDistribute('0xef', { from: user2, value: betAmount }); // 1 -> 4
                    await lottery.betAndDistribute('0xef', { from: user2, value: betAmount }); // 2 -> 5
                    await lottery.betAndDistribute('0xef', { from: user1, value: betAmount }); // 3 -> 6
                    await lottery.betAndDistribute('0xef', { from: user2, value: betAmount }); // 4 -> 7
                    await lottery.betAndDistribute('0xef', { from: user2, value: betAmount }); // 5 -> 8
                    await lottery.betAndDistribute('0xef', { from: user2, value: betAmount }); // 6 -> 9
    
                    let potBefore = await lottery.getPot(); // 0.01 ETH
                    let user1BalanceBefore = await web3.eth.getBalance(user1);
                    
                    // user1이 팟머니 획득
                    let receipt7 = await lottery.betAndDistribute('0xef', { from: user2, value: betAmount }); // 7 -> 10
                    
                    let potAfter = await lottery.getPot(); // 0.015 ETH
                    let user1BalanceAfter = await web3.eth.getBalance(user1); // before
    
                    // 팟머니의 변화량 확인
                    assert.equal(potBefore.add(betAmountBN).toString(), potAfter.toString());
    
                    // 유저(승자)의 밸런스 확인
                    user1BalanceBefore = new web3.utils.BN(user1BalanceBefore);
                    assert.equal(user1BalanceBefore.toString(), new web3.utils.BN(user1BalanceAfter).toString());
                });
            });
            describe('When the answer is not revealed(Not Mined)', function () {
                // 아무것도 일어나지 않은 것을 확인
                // 배팅 전으로 스마트 컨트랙트의 밸런스, 팟머니의 밸런스, 유저의 밸런스 체크 필요
            });
            describe('When the answer is not revealed(Block limit is passed)', function () {
                // 블록을 계속 증가 시키기
            });
        });
    
        describe('isMatch', function () {
            // 32 bytes짜리 아무 해쉬값 가져와서 테스트하기 쉽게 3번째 글자 a로 변경
            let blockHash = '0xabec17438e4f0afb9cc8b77ce84bb7fd501497cfa9a1695095247daa5b4b7bcc';
    
            // Win
            it('should be BettingResult.Win when two characters match', async () => {    
                let matchingResult = await lottery.isMatch('0xab', blockHash);
                assert.equal(matchingResult, 1);
            });
    
            // Fail
            it('should be BettingResult.Fail when two characters match', async () => {    
                let matchingResult = await lottery.isMatch('0xcd', blockHash);
                assert.equal(matchingResult, 0);
            });
    
            // Draw
            it('should be BettingResult.Draw when two characters match', async () => {    
                let matchingResult = await lottery.isMatch('0xaf', blockHash);
                assert.equal(matchingResult, 2);
    
                matchingResult = await lottery.isMatch('0xfb', blockHash);
                assert.equal(matchingResult, 2);
            });
        });
    });​
    truffle test test/lottery.test.js // 테스트 완료

 

- Lottery 컨트랙트 리뷰

더보기
  • Smart Contract를 만들 때는 기존의 코드 중 보안에 문제가 없는 검증 된 코드를 최대한 재활용하는게 좋음
    Ex) OpenZeppelin
  • 직접 짜는 경우 테스트 시나리오를 미리 짜놓고 테스트 코드까지 다 통과시켜보는게 검증을 위해 좋음
    (다른 사람들의 신뢰를 얻기 위함이고, Smart Contract는 기본적으로 오픈소스!)
  • 아무리 잘 짠 코드여도 문제는 발생 할 수 있기에 그에 대한 해결책을 미리 생각해보는 것도 좋음

 

- React js 기본 설정

더보기
  1. truffle unbox react
  2. cd client
    npm run start
    오류 발생
    /client/.env 파일 생성 후 SKIP_PREFLIGHT_CHECK=true 내용 입력하여 저장한 다음 실행하면 오류 해결
  3. /client/App.js 코드 수정 // web3로 메타마스크 연결
    import React, { useEffect } from "react";
    import getWeb3 from "./getWeb3";
    
    import "./App.css";
    
    const App = () => {
      const getweb = async () => {
        let web3 = await getWeb3();
        let accounts = await web3.eth.getAccounts();
        let balance = await web3.eth.getBalance(accounts[0]);
        console.log(web3);
        console.log(balance);
      }
    
      useEffect(() => {
        getweb();
      }, []);
    
      return (
        <div>
          Hi React
        </div>
      );
    }
    
    export default App;
    console.log(web3);
    console.log(balance);

 

- web3.js - send & call

더보기
  1. 작업했던 스마트 컨트랙트를 새로 컴파일 후 배포한 다음 contract address 가져오기
    truffle compile
    truffle migrate
    contract address : 0xd4BF9196Eb5e6cdf539Ed7A8B29118de3A3f104d
  2. /client/App.js 코드 수정
    import React, { useEffect } from "react";
    import getWeb3 from "./getWeb3";
    
    import "./App.css";
    
    // migrate -> contract address
    let lotteryAddress = '0xd4BF9196Eb5e6cdf539Ed7A8B29118de3A3f104d';
    // LotteryDapp -> /build/contracts/Lottery.json
    let lotteryABI = [ { "inputs": [], "stateMutability": "nonpayable", "type": "constructor" }, { "anonymous": false, "inputs": [ { "indexed": false, "internalType": "uint256", "name": "index", "type": "uint256" }, { "indexed": false, "internalType": "address", "name": "bettor", "type": "address" }, { "indexed": false, "internalType": "uint256", "name": "amount", "type": "uint256" }, { "indexed": false, "internalType": "bytes1", "name": "challenges", "type": "bytes1" }, { "indexed": false, "internalType": "uint256", "name": "answerBlockNumber", "type": "uint256" } ], "name": "BET", "type": "event" }, { "anonymous": false, "inputs": [ { "indexed": false, "internalType": "uint256", "name": "index", "type": "uint256" }, { "indexed": false, "internalType": "address", "name": "bettor", "type": "address" }, { "indexed": false, "internalType": "uint256", "name": "amount", "type": "uint256" }, { "indexed": false, "internalType": "bytes1", "name": "challenges", "type": "bytes1" }, { "indexed": false, "internalType": "bytes1", "name": "answer", "type": "bytes1" }, { "indexed": false, "internalType": "uint256", "name": "answerBlockNumber", "type": "uint256" } ], "name": "DRAW", "type": "event" }, { "anonymous": false, "inputs": [ { "indexed": false, "internalType": "uint256", "name": "index", "type": "uint256" }, { "indexed": false, "internalType": "address", "name": "bettor", "type": "address" }, { "indexed": false, "internalType": "uint256", "name": "amount", "type": "uint256" }, { "indexed": false, "internalType": "bytes1", "name": "challenges", "type": "bytes1" }, { "indexed": false, "internalType": "bytes1", "name": "answer", "type": "bytes1" }, { "indexed": false, "internalType": "uint256", "name": "answerBlockNumber", "type": "uint256" } ], "name": "FAIL", "type": "event" }, { "anonymous": false, "inputs": [ { "indexed": false, "internalType": "uint256", "name": "index", "type": "uint256" }, { "indexed": false, "internalType": "address", "name": "bettor", "type": "address" }, { "indexed": false, "internalType": "uint256", "name": "amount", "type": "uint256" }, { "indexed": false, "internalType": "bytes1", "name": "challenges", "type": "bytes1" }, { "indexed": false, "internalType": "uint256", "name": "answerBlockNumber", "type": "uint256" } ], "name": "REFUND", "type": "event" }, { "anonymous": false, "inputs": [ { "indexed": false, "internalType": "uint256", "name": "index", "type": "uint256" }, { "indexed": false, "internalType": "address", "name": "bettor", "type": "address" }, { "indexed": false, "internalType": "uint256", "name": "amount", "type": "uint256" }, { "indexed": false, "internalType": "bytes1", "name": "challenges", "type": "bytes1" }, { "indexed": false, "internalType": "bytes1", "name": "answer", "type": "bytes1" }, { "indexed": false, "internalType": "uint256", "name": "answerBlockNumber", "type": "uint256" } ], "name": "WIN", "type": "event" }, { "inputs": [], "name": "answerForTest", "outputs": [ { "internalType": "bytes32", "name": "", "type": "bytes32" } ], "stateMutability": "view", "type": "function", "constant": true }, { "inputs": [], "name": "owner", "outputs": [ { "internalType": "address payable", "name": "", "type": "address" } ], "stateMutability": "view", "type": "function", "constant": true }, { "inputs": [], "name": "getPot", "outputs": [ { "internalType": "uint256", "name": "pot", "type": "uint256" } ], "stateMutability": "view", "type": "function", "constant": true }, { "inputs": [ { "internalType": "bytes1", "name": "challenges", "type": "bytes1" } ], "name": "betAndDistribute", "outputs": [ { "internalType": "bool", "name": "result", "type": "bool" } ], "stateMutability": "payable", "type": "function", "payable": true }, { "inputs": [ { "internalType": "bytes1", "name": "challenges", "type": "bytes1" } ], "name": "bet", "outputs": [ { "internalType": "bool", "name": "result", "type": "bool" } ], "stateMutability": "payable", "type": "function", "payable": true }, { "inputs": [], "name": "distribute", "outputs": [], "stateMutability": "nonpayable", "type": "function" }, { "inputs": [ { "internalType": "bytes32", "name": "answer", "type": "bytes32" } ], "name": "setAnswerForTest", "outputs": [ { "internalType": "bool", "name": "result", "type": "bool" } ], "stateMutability": "nonpayable", "type": "function" }, { "inputs": [ { "internalType": "bytes1", "name": "challenges", "type": "bytes1" }, { "internalType": "bytes32", "name": "answer", "type": "bytes32" } ], "name": "isMatch", "outputs": [ { "internalType": "enum Lottery.BettingResult", "name": "", "type": "uint8" } ], "stateMutability": "pure", "type": "function", "constant": true }, { "inputs": [ { "internalType": "uint256", "name": "index", "type": "uint256" } ], "name": "getBetInfo", "outputs": [ { "internalType": "uint256", "name": "answerBlockNumber", "type": "uint256" }, { "internalType": "address", "name": "bettor", "type": "address" }, { "internalType": "bytes1", "name": "challenges", "type": "bytes1" } ], "stateMutability": "view", "type": "function", "constant": true } ];
    
    const App = () => {
      const getweb = async () => {
        let web3 = await getWeb3();
        let accounts = await web3.eth.getAccounts();
        // console.log(web3);
        // console.log(balance);
    
        let account = accounts[0];
        let lotteryContract = new web3.eth.Contract(lotteryABI, lotteryAddress);
        
        let pot = await lotteryContract.methods.getPot().call();
        let owner = await lotteryContract.methods.owner().call();
    
        console.log(pot);
        console.log(owner);
      }
    
      useEffect(() => {
        getweb();
      }, []);
    
      return (
        <div>
          Hi React
        </div>
      );
    }
    
    export default App;
    console.log(pot, owner);
  3.  truffle console
    Lottery.deployed().then(function(instance){lt=instance});
  4. web3.eth.getAccounts();
    let bettor = '0x5761AE4134726785b2E3Aa67b80b75bc8d034708'; // 0번째 인덱스
  5. lt.betAndDistribute('0xab', {from: bettor, value: 5000000000000000, gas: 300000});
    10번 정도 실행하며 팟머니에 돈이 쌓이게 동작(event: 'FAIL'이 확인 되어야 함)
    console.log(pot, owner);
  6. 코드 작성하던 부분 밑에 아래 코드 추가 후 저장하면 트랜잭션이 일어나며 메타마스크 팝업창이 열림
    확인 후 트러플 콘솔에서 트랜잭션 확인
    // 트랜잭션 발생
    lotteryContract.methods.betAndDistribute('0xcd')
    .send({ from: account, value: 5000000000000000, gas: 300000 });
    lt.getBetInfo('숫자'); // 0부터 숫자를 입력해보며 challenges: '0xcd'를 찾아보기
  7.  배팅을 함수로 따로 뺀 /client/App.js 전체 코드
    import React, { useEffect } from "react";
    import getWeb3 from "./getWeb3";
    
    import "./App.css";
    
    // migrate -> contract address
    let lotteryAddress = '0xd4BF9196Eb5e6cdf539Ed7A8B29118de3A3f104d';
    // LotteryDapp -> /build/contracts/Lottery.json
    let lotteryABI = [ { "inputs": [], "stateMutability": "nonpayable", "type": "constructor" }, { "anonymous": false, "inputs": [ { "indexed": false, "internalType": "uint256", "name": "index", "type": "uint256" }, { "indexed": false, "internalType": "address", "name": "bettor", "type": "address" }, { "indexed": false, "internalType": "uint256", "name": "amount", "type": "uint256" }, { "indexed": false, "internalType": "bytes1", "name": "challenges", "type": "bytes1" }, { "indexed": false, "internalType": "uint256", "name": "answerBlockNumber", "type": "uint256" } ], "name": "BET", "type": "event" }, { "anonymous": false, "inputs": [ { "indexed": false, "internalType": "uint256", "name": "index", "type": "uint256" }, { "indexed": false, "internalType": "address", "name": "bettor", "type": "address" }, { "indexed": false, "internalType": "uint256", "name": "amount", "type": "uint256" }, { "indexed": false, "internalType": "bytes1", "name": "challenges", "type": "bytes1" }, { "indexed": false, "internalType": "bytes1", "name": "answer", "type": "bytes1" }, { "indexed": false, "internalType": "uint256", "name": "answerBlockNumber", "type": "uint256" } ], "name": "DRAW", "type": "event" }, { "anonymous": false, "inputs": [ { "indexed": false, "internalType": "uint256", "name": "index", "type": "uint256" }, { "indexed": false, "internalType": "address", "name": "bettor", "type": "address" }, { "indexed": false, "internalType": "uint256", "name": "amount", "type": "uint256" }, { "indexed": false, "internalType": "bytes1", "name": "challenges", "type": "bytes1" }, { "indexed": false, "internalType": "bytes1", "name": "answer", "type": "bytes1" }, { "indexed": false, "internalType": "uint256", "name": "answerBlockNumber", "type": "uint256" } ], "name": "FAIL", "type": "event" }, { "anonymous": false, "inputs": [ { "indexed": false, "internalType": "uint256", "name": "index", "type": "uint256" }, { "indexed": false, "internalType": "address", "name": "bettor", "type": "address" }, { "indexed": false, "internalType": "uint256", "name": "amount", "type": "uint256" }, { "indexed": false, "internalType": "bytes1", "name": "challenges", "type": "bytes1" }, { "indexed": false, "internalType": "uint256", "name": "answerBlockNumber", "type": "uint256" } ], "name": "REFUND", "type": "event" }, { "anonymous": false, "inputs": [ { "indexed": false, "internalType": "uint256", "name": "index", "type": "uint256" }, { "indexed": false, "internalType": "address", "name": "bettor", "type": "address" }, { "indexed": false, "internalType": "uint256", "name": "amount", "type": "uint256" }, { "indexed": false, "internalType": "bytes1", "name": "challenges", "type": "bytes1" }, { "indexed": false, "internalType": "bytes1", "name": "answer", "type": "bytes1" }, { "indexed": false, "internalType": "uint256", "name": "answerBlockNumber", "type": "uint256" } ], "name": "WIN", "type": "event" }, { "inputs": [], "name": "answerForTest", "outputs": [ { "internalType": "bytes32", "name": "", "type": "bytes32" } ], "stateMutability": "view", "type": "function", "constant": true }, { "inputs": [], "name": "owner", "outputs": [ { "internalType": "address payable", "name": "", "type": "address" } ], "stateMutability": "view", "type": "function", "constant": true }, { "inputs": [], "name": "getPot", "outputs": [ { "internalType": "uint256", "name": "pot", "type": "uint256" } ], "stateMutability": "view", "type": "function", "constant": true }, { "inputs": [ { "internalType": "bytes1", "name": "challenges", "type": "bytes1" } ], "name": "betAndDistribute", "outputs": [ { "internalType": "bool", "name": "result", "type": "bool" } ], "stateMutability": "payable", "type": "function", "payable": true }, { "inputs": [ { "internalType": "bytes1", "name": "challenges", "type": "bytes1" } ], "name": "bet", "outputs": [ { "internalType": "bool", "name": "result", "type": "bool" } ], "stateMutability": "payable", "type": "function", "payable": true }, { "inputs": [], "name": "distribute", "outputs": [], "stateMutability": "nonpayable", "type": "function" }, { "inputs": [ { "internalType": "bytes32", "name": "answer", "type": "bytes32" } ], "name": "setAnswerForTest", "outputs": [ { "internalType": "bool", "name": "result", "type": "bool" } ], "stateMutability": "nonpayable", "type": "function" }, { "inputs": [ { "internalType": "bytes1", "name": "challenges", "type": "bytes1" }, { "internalType": "bytes32", "name": "answer", "type": "bytes32" } ], "name": "isMatch", "outputs": [ { "internalType": "enum Lottery.BettingResult", "name": "", "type": "uint8" } ], "stateMutability": "pure", "type": "function", "constant": true }, { "inputs": [ { "internalType": "uint256", "name": "index", "type": "uint256" } ], "name": "getBetInfo", "outputs": [ { "internalType": "uint256", "name": "answerBlockNumber", "type": "uint256" }, { "internalType": "address", "name": "bettor", "type": "address" }, { "internalType": "bytes1", "name": "challenges", "type": "bytes1" } ], "stateMutability": "view", "type": "function", "constant": true } ];
    
    const App = () => {
      const getweb = async () => {
        let web3 = await getWeb3();
        let lotteryContract = new web3.eth.Contract(lotteryABI, lotteryAddress);
        
        let pot = await lotteryContract.methods.getPot().call();
        let owner = await lotteryContract.methods.owner().call();
    
        console.log(pot);
        console.log(owner);
      }
    
      const bet = async () => {
        let web3 = await getWeb3();
        let accounts = await web3.eth.getAccounts();
        let account = accounts[0];
        let lotteryContract = new web3.eth.Contract(lotteryABI, lotteryAddress);
        let nonce = await web3.eth.getTransactionCount(account);
        
        lotteryContract.methods.betAndDistribute('0xcd').send({ from: account, value: 5000000000000000, gas: 300000, nonce: nonce });
      }
    
      useEffect(() => {
        getweb();
      }, []);
    
      return (
        <div>
          Hi React
        </div>
      );
    }
    
    export default App;
  8. ㅇㅇ

 

- web3.js - filter

더보기
  1. /client/App.js 코드 수정 // 함수 추가, useEffect에서 함수 실행
    const getBetEvents = async () => {
      const records = []; // 이벤트 관련 레코드를 넣을 배열
    
      let web3 = await getWeb3();
      let lotteryContract = new web3.eth.Contract(lotteryABI, lotteryAddress);
      // getPastEvents() : 해당 이벤트의 지난 내역을 가져옴
      let events = await lotteryContract.getPastEvents('BET', { fromBlock: 0, toBlock: 'latest' });
    
      console.log(events);
    }
    console.log(events);

 

- Dapp 데이터 관리

 

- Lottery UI 개발

더보기
  1. npm install bootstrap // 필요한 패키지 인스톨
    client/App.js 코드 수정 후 서버 실행
    return (
      <div className="App">
        <div className="container">
          <div className="jumbotron">
            Lottery
          </div>
        </div>
      </div>
    );
    bootstrap을 사용하여 가운데 정렬이 된 모습
  2. 화면을 그리는 작업의 전체 코드
    import React, { useEffect, useState } from "react";
    import getWeb3 from "./getWeb3";
    // npm install bootstrap
    import 'bootstrap/dist/css/bootstrap.css';
    import "./App.css";
    
    // migrate -> contract address
    let lotteryAddress = '0xd4BF9196Eb5e6cdf539Ed7A8B29118de3A3f104d';
    // LotteryDapp -> /build/contracts/Lottery.json
    let lotteryABI = [ { "inputs": [], "stateMutability": "nonpayable", "type": "constructor" }, { "anonymous": false, "inputs": [ { "indexed": false, "internalType": "uint256", "name": "index", "type": "uint256" }, { "indexed": false, "internalType": "address", "name": "bettor", "type": "address" }, { "indexed": false, "internalType": "uint256", "name": "amount", "type": "uint256" }, { "indexed": false, "internalType": "bytes1", "name": "challenges", "type": "bytes1" }, { "indexed": false, "internalType": "uint256", "name": "answerBlockNumber", "type": "uint256" } ], "name": "BET", "type": "event" }, { "anonymous": false, "inputs": [ { "indexed": false, "internalType": "uint256", "name": "index", "type": "uint256" }, { "indexed": false, "internalType": "address", "name": "bettor", "type": "address" }, { "indexed": false, "internalType": "uint256", "name": "amount", "type": "uint256" }, { "indexed": false, "internalType": "bytes1", "name": "challenges", "type": "bytes1" }, { "indexed": false, "internalType": "bytes1", "name": "answer", "type": "bytes1" }, { "indexed": false, "internalType": "uint256", "name": "answerBlockNumber", "type": "uint256" } ], "name": "DRAW", "type": "event" }, { "anonymous": false, "inputs": [ { "indexed": false, "internalType": "uint256", "name": "index", "type": "uint256" }, { "indexed": false, "internalType": "address", "name": "bettor", "type": "address" }, { "indexed": false, "internalType": "uint256", "name": "amount", "type": "uint256" }, { "indexed": false, "internalType": "bytes1", "name": "challenges", "type": "bytes1" }, { "indexed": false, "internalType": "bytes1", "name": "answer", "type": "bytes1" }, { "indexed": false, "internalType": "uint256", "name": "answerBlockNumber", "type": "uint256" } ], "name": "FAIL", "type": "event" }, { "anonymous": false, "inputs": [ { "indexed": false, "internalType": "uint256", "name": "index", "type": "uint256" }, { "indexed": false, "internalType": "address", "name": "bettor", "type": "address" }, { "indexed": false, "internalType": "uint256", "name": "amount", "type": "uint256" }, { "indexed": false, "internalType": "bytes1", "name": "challenges", "type": "bytes1" }, { "indexed": false, "internalType": "uint256", "name": "answerBlockNumber", "type": "uint256" } ], "name": "REFUND", "type": "event" }, { "anonymous": false, "inputs": [ { "indexed": false, "internalType": "uint256", "name": "index", "type": "uint256" }, { "indexed": false, "internalType": "address", "name": "bettor", "type": "address" }, { "indexed": false, "internalType": "uint256", "name": "amount", "type": "uint256" }, { "indexed": false, "internalType": "bytes1", "name": "challenges", "type": "bytes1" }, { "indexed": false, "internalType": "bytes1", "name": "answer", "type": "bytes1" }, { "indexed": false, "internalType": "uint256", "name": "answerBlockNumber", "type": "uint256" } ], "name": "WIN", "type": "event" }, { "inputs": [], "name": "answerForTest", "outputs": [ { "internalType": "bytes32", "name": "", "type": "bytes32" } ], "stateMutability": "view", "type": "function", "constant": true }, { "inputs": [], "name": "owner", "outputs": [ { "internalType": "address payable", "name": "", "type": "address" } ], "stateMutability": "view", "type": "function", "constant": true }, { "inputs": [], "name": "getPot", "outputs": [ { "internalType": "uint256", "name": "pot", "type": "uint256" } ], "stateMutability": "view", "type": "function", "constant": true }, { "inputs": [ { "internalType": "bytes1", "name": "challenges", "type": "bytes1" } ], "name": "betAndDistribute", "outputs": [ { "internalType": "bool", "name": "result", "type": "bool" } ], "stateMutability": "payable", "type": "function", "payable": true }, { "inputs": [ { "internalType": "bytes1", "name": "challenges", "type": "bytes1" } ], "name": "bet", "outputs": [ { "internalType": "bool", "name": "result", "type": "bool" } ], "stateMutability": "payable", "type": "function", "payable": true }, { "inputs": [], "name": "distribute", "outputs": [], "stateMutability": "nonpayable", "type": "function" }, { "inputs": [ { "internalType": "bytes32", "name": "answer", "type": "bytes32" } ], "name": "setAnswerForTest", "outputs": [ { "internalType": "bool", "name": "result", "type": "bool" } ], "stateMutability": "nonpayable", "type": "function" }, { "inputs": [ { "internalType": "bytes1", "name": "challenges", "type": "bytes1" }, { "internalType": "bytes32", "name": "answer", "type": "bytes32" } ], "name": "isMatch", "outputs": [ { "internalType": "enum Lottery.BettingResult", "name": "", "type": "uint8" } ], "stateMutability": "pure", "type": "function", "constant": true }, { "inputs": [ { "internalType": "uint256", "name": "index", "type": "uint256" } ], "name": "getBetInfo", "outputs": [ { "internalType": "uint256", "name": "answerBlockNumber", "type": "uint256" }, { "internalType": "address", "name": "bettor", "type": "address" }, { "internalType": "bytes1", "name": "challenges", "type": "bytes1" } ], "stateMutability": "view", "type": "function", "constant": true } ];
    
    const App = () => {
      const [state, setState] = useState({
        betRecords: [],
        winRecords: [],
        failRecords: [],
        pot: '0',
        challenges: ['A', 'B'],
        finalRecords: [{
          bettor: '0xabcd...',
          index: '0',
          challenges: 'ab',
          answer: 'ab',
          targetBlockNumber: '10',
          pot: '0',
        }],
      });
    
      const getweb = async () => {
        let web3 = await getWeb3();
        let lotteryContract = new web3.eth.Contract(lotteryABI, lotteryAddress);
        
        let pot = await lotteryContract.methods.getPot().call();
        let owner = await lotteryContract.methods.owner().call();
    
        console.log(pot);
        console.log(owner);
      }
    
      const getBetEvents = async () => {
        const records = []; // 이벤트 관련 레코드를 넣을 배열
    
        let web3 = await getWeb3();
        let lotteryContract = new web3.eth.Contract(lotteryABI, lotteryAddress);
        // getPastEvents() : 해당 이벤트의 지난 내역을 가져옴
        let events = await lotteryContract.getPastEvents('BET', { fromBlock: 0, toBlock: 'latest' });
    
        console.log(events);
      }
    
      const bet = async () => {
        let web3 = await getWeb3();
        let accounts = await web3.eth.getAccounts();
        let account = accounts[0];
        let lotteryContract = new web3.eth.Contract(lotteryABI, lotteryAddress);
        let nonce = await web3.eth.getTransactionCount(account);
    
        lotteryContract.methods.betAndDistribute('0xcd').send({ from: account, value: 5000000000000000, gas: 300000, nonce: nonce });
      }
    
      useEffect(() => {
        getweb();
        getBetEvents();
      }, []);
    
      const getCard = (_character, _cardStyle) => {
        let _card = '';
    
        if (_character === 'A') {
          _card = '🂡';
        }
        if (_character === 'B') {
          _card = '🂱';
        }
        if (_character === 'C') {
          _card = '🃁';
        }
        if (_character === 'D') {
          _card = '🃑';
        }
    
        return (
          <button className={_cardStyle}>
            <div className="card-body text-center">
              <p className="card-text"></p>
              <p className="card-text text-center" style={{fontSize:300}}>{_card}</p>
              <p className="card-text"></p>
            </div>
          </button>
        );
      }
    
      return (
        <div className="App">
          {/* Heaer - Pot, Betting characters */}
          <div className="container">
            <div className="jumbotron">
              <h1>Current Pot : {state.pot}</h1>
              <p>Lottery tutorial</p>
              <p>Your Bet</p>
              <p>{state.challenges[0]} {state.challenges[1]}</p>
            </div>
          </div>
    
          {/* Card section */}
          <div className="container">
            <div className="card-group">
              {getCard('A', 'card bg-primary')}
              {getCard('B', 'card bg-warning')}
              {getCard('C', 'card bg-danger')}
              {getCard('D', 'card bg-success')}
            </div>
          </div>
          <br />
    
          <div className="container">
            <button className="btn btn-danger btn-lg">BET!</button>
          </div>
          <br/>
    
          <div className="container">
            <table className="table table-dark table-striped">
              <thead>
                <tr>
                  <th>Index</th>
                  <th>Address</th>
                  <th>Challenge</th>
                  <th>Answer</th>
                  <th>Pot</th>
                  <th>Status</th>
                  <th>AnswerBlockNumber</th>
                </tr>
              </thead>
              <tbody>
                {
                  state.finalRecords.map((record, k) => {
                    return (
                      <tr key={k}>
                        <td>0</td>
                        <td>0</td>
                        <td>0</td>
                        <td>0</td>
                        <td>0</td>
                        <td>0</td>
                        <td>0</td>
                      </tr>
                    );
                  })
                }
              </tbody>
            </table>
          </div>
        </div>
      );
    }
    
    export default App;
  3.  

 

 

-ㅁㅁㅁㅁㅁㅁ