Lottery Dapp 개발 실습
2021. 10. 26. 16:13ㆍBlockchain/Project - Coin Swap
준비물
- Node.js
- Visual Studio Code
- Visual Studio Code - Solidity Extension
- Truffle
- Ganache-cli
- MetaMask
실습 진행
- Truffle 을 활용한 스마트 컨트랙트 상호작용
더보기
- Truffle init
- 가나슈와 메타마스크 연결
- /contracs/Lottery.sol 파일 생성 후 코드 작성
pragma solidity >=0.4.22 <0.9.0; contract Lottery { }
truffle compile // 컴파일
- /migrations/1_initial_migration.js 파일 복사 후 2_deploy_smart_contracts.js로 파일명 변경 후 코드 작성
const Lottery = artifacts.require("Lottery"); module.exports = function (deployer) { deployer.deploy(Lottery); };
- truffle-config.js 파일 코드 수정
- truffle migrate // 배포
- /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; } }
- truffle migrate --reset --all // 재배포
- Windows PowerShell에서 프로젝트의 root directory까지 이동 후
truffle console - web3
- eth = web3.eth // 편의를 위해 web3.eth를 eth라는 새로운 변수에 담아 사용
eth eth. 까지 입력하고 탭 두번 누르면 사용 할 수 있는 변수를 확인 가능 - eth.getAccounts();
- eth.getBalance('0x2209B8d48f767F0d33D3f3C921CF9189e3d519FA');
- Lottery.address
- Lottery.deployed().then(function(instance){lt=instance}); // async 함수이기 때문에 callback을 달아줌
lt 변수 안에 instance를 담아줌
- lt lt. 까지 입력하고 탭 두번 누르면 사용 할 수 있는 변수를 확인 가능
- lt.abi
※ ABI란? 인터페이스!
외부에서 접근 시 해당 스마트 컨트랙트에서 어떤 함수를 사용 할 수 있고, 어떤 파라미터가 있는지 리턴값은 뭔지 확인 가능 - lt.owner();
- lt.getSomeValue();
- Truffle을 사용한 테스트
더보기
- /test/lottery.test.js 파일 생성 후 코드 작성
※ 가장 기본적인 모카 테스트의 구조 (위) / 테스트 코드 작성 완료 (아래)const Lottery = artifacts.require("Lottery"); contract('Lottery', function([deployer, user1, user2]){ }); // deployer : 가나슈 0번 인덱스의 address // user1 : 가나슈 1번 인덱스의 address // user2 : 가나슈 2번 인덱스의 address
- 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'); }); });
- truffle test test/lottery.test.js // 테스트
※ truffle test만 입력 할 경우 test 폴더 내 전체 파일 테스트가 진행 됨 - /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); }); });
- truffle test test/lottery.test.js // 재테스트
- Dapp 설계 방법 및 Lottery 규칙
더보기
- 사전 설명 ※ 보낸 돈 : 0.005 ETH (10 ** 15 wei)
- Lottery Domain 및 Queue 설계, Lottery Bet 함수 구현, Lottery Bet 테스트
더보기
- /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; } }
- 테스트 코드 작성
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); }); });
- truffle test test/lottery.test.js
- /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; } }
- /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 // 배팅 정보 확인 // 로그 확인 }); }); });
- /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`); } }
- /test/lottery.test.js 파일 코드 수정 후 테스트
// 발생한 에러를 assertRevert()에서 try/catch문으로 받음 // await lottery.bet('0xab', { from: user1, value: 4000000000000000 }); await assertRevert('0xab', { from: user1, value: 4000000000000000 });
- /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 // 배팅 정보 확인 // 로그 확인 });
- 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, }
- 테스트
- 여기까지 /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'); }); }); });
- 이더리움 GAS 계산
더보기
- 이더리움의 수수료
- GAS 계산
- Windows PowerShell에서 프로젝트의 root directory까지 이동
trurffle migrate --reset - truffle console // 콘솔과 상호작용
Lottery.deployed().then(function(instance){lt=instance}); // lt 변수 안에 instance를 담아줌 - web3.eth.getAccounts();
let bettor = '0x5761AE4134726785b2E3Aa67b80b75bc8d034708'; // 사용 할 account를 변수에 담음
- lt.bet("0xab", { from: bettor, value: 5000000000000000, gas: 300000 });
* 첫 번째 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 테스트
더보기
- /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; } }
- /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); }); });
- /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; } }
- /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); }); }); });
- Lottery 컨트랙트 리뷰
더보기
- Smart Contract를 만들 때는 기존의 코드 중 보안에 문제가 없는 검증 된 코드를 최대한 재활용하는게 좋음
Ex) OpenZeppelin - 직접 짜는 경우 테스트 시나리오를 미리 짜놓고 테스트 코드까지 다 통과시켜보는게 검증을 위해 좋음
(다른 사람들의 신뢰를 얻기 위함이고, Smart Contract는 기본적으로 오픈소스!) - 아무리 잘 짠 코드여도 문제는 발생 할 수 있기에 그에 대한 해결책을 미리 생각해보는 것도 좋음
- React js 기본 설정
더보기
- truffle unbox react
- cd client
npm run start - /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;
- web3.js - send & call
더보기
- 작업했던 스마트 컨트랙트를 새로 컴파일 후 배포한 다음 contract address 가져오기
truffle compile
truffle migrate
- /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;
- truffle console
Lottery.deployed().then(function(instance){lt=instance}); - web3.eth.getAccounts();
let bettor = '0x5761AE4134726785b2E3Aa67b80b75bc8d034708'; // 0번째 인덱스 - lt.betAndDistribute('0xab', {from: bettor, value: 5000000000000000, gas: 300000});
10번 정도 실행하며 팟머니에 돈이 쌓이게 동작(event: 'FAIL'이 확인 되어야 함) - 코드 작성하던 부분 밑에 아래 코드 추가 후 저장하면 트랜잭션이 일어나며 메타마스크 팝업창이 열림
확인 후 트러플 콘솔에서 트랜잭션 확인// 트랜잭션 발생 lotteryContract.methods.betAndDistribute('0xcd') .send({ from: account, value: 5000000000000000, gas: 300000 });
- 배팅을 함수로 따로 뺀 /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;
- ㅇㅇ
- web3.js - filter
더보기
- /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); }
- Dapp 데이터 관리
- Lottery UI 개발
더보기
- npm install bootstrap // 필요한 패키지 인스톨
client/App.js 코드 수정 후 서버 실행return ( <div className="App"> <div className="container"> <div className="jumbotron"> Lottery </div> </div> </div> );
- 화면을 그리는 작업의 전체 코드
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;
- ㅁ
- ㅁ
- ㅁ
- ㅁ
- ㅁ
-ㅁㅁㅁㅁㅁㅁ
'Blockchain > Project - Coin Swap' 카테고리의 다른 글
자료 조사 : 도커(Docker) 컨테이너 다루기 (0) | 2021.11.02 |
---|---|
자료 조사 : 도커(Docker), 카이트메틱(Kitematic) 설치 및 실행 (0) | 2021.11.02 |
타입스크립트(TypeScript) 컴파일러(Compiler)와 ts-node 설치 및 실행 방법 (0) | 2021.10.25 |
자료 조사 : 트렐로(Trello) (0) | 2021.10.21 |
자료 조사 : 도커(Docker)란? (0) | 2021.10.19 |