Lottery.addressLottery.address/build/contracts/Lottery.json/ 파일 내 604번째 줄의 address와 일치함
Lottery.deployed().then(function(instance){lt=instance}); // async 함수이기 때문에 callback을 달아줌 lt 변수 안에 instance를 담아줌 Lottery.deployed().then(function(instance){lt=instance});
ltltlt. 까지 입력하고 탭 두번 누르면 사용 할 수 있는 변수를 확인 가능eth. [Tab]*2
lt.abilt.abi // lt에서 사용 할 수 있는 함수를 abi 형태로 확인 가능※ ABI란? 인터페이스! 외부에서 접근 시 해당 스마트 컨트랙트에서 어떤 함수를 사용 할 수 있고, 어떤 파라미터가 있는지 리턴값은 뭔지 확인 가능
lt.owner();lt.owner();가나슈 0번째 인덱스의 address 값과 일치함
lt.getSomeValue(); lt.getSomeValue(); // BN = Big Number, 이더리움은 다루는 숫자가 크기 때문에 이렇게 사용됨
// 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
// 배팅 정보 확인
// 로그 확인
});
});
});
truffle test test/lottery.test.js // value에 숫자를 일부러 작게 작성하였기에 테스트가 실패하며 .sol 파일에 작성했던 "Not enough ETH" 가 확인 된 모습truffle test test/lottery.test.js // 테스트 코드 await lottery.bet() 안의 value값을 5000000000000000으로 변경하여 재테스트 시 테스트에 성공하는 모습
/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`);
}
}
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에는 다양한 정보가 들어가있음을 확인 가능
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,
}
테스트 truffle test test/lottery.test.js // 테스트 성공이벤트명 변경 후 다시 테스트truffle test test/lottery.test.js // 존재하지 않는 이벤트이므로 테스트에 실패하는 모습
여기까지 /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');
});
});
});
Windows PowerShell에서 프로젝트의 root directory까지 이동 trurffle migrate --resettrurffle migrate --reset // 배포 중..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: 89245lt.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 테스트
// 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 // 컴파일 완료
/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 // 테스트 성공
/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);
});
});
});
web3.eth.getAccounts(); let bettor = '0x5761AE4134726785b2E3Aa67b80b75bc8d034708'; // 0번째 인덱스
lt.betAndDistribute('0xab', {from: bettor, value: 5000000000000000, gas: 300000}); 10번 정도 실행하며 팟머니에 돈이 쌓이게 동작(event: 'FAIL'이 확인 되어야 함)console.log(pot, 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);
}