
트러플을 사용해 빌드한 프로젝트 구조는 다음과 같이 된다.
├── build
├── contracts
├── Migrations.json
├── CryptoZombies.json
├── erc721.json
├── ownable.json
├── safemath.json
├── zombieattack.json
├── zombiefactory.json
├── zombiefeeding.json
├── zombiehelper.json
├── zombieownership.json
├── contracts
├── Migrations.sol
├── CryptoZombies.sol
├── erc721.sol
├── ownable.sol
├── safemath.sol
├── zombieattack.sol
├── zombiefactory.sol
├── zombiefeeding.sol
├── zombiehelper.sol
├── zombieownership.sol
├── migrations
└── test
. package-lock.json
. truffle-config.js
. truffle.js
test 폴더에 주목!javascript와 solidity를 지원한다.javascript로 코드를 짤 예정JSON 형식의 파일을 생성한다.binary representation이 들어있다.JSON 파일은 build/contracts 폴더에 저장된다.const myAwesomeContract = artifacts.require(“myAwesomeContract”);
컨트랙트 abstraction을 리턴한다.abstraction 이라서 뒤에서 자바스크립트 객체를 생성해서 그거로 컨트랙트와 통신해야한다.테스트를 단순하게 만들기 위해 Truffle은 Mocha안에 얇은 wrapper를 만든다.
contract()함수를 불러서 실행하고, 이건 mocha의 describe함수에 테스팅 어카운트 리스트를 제공하고 정리 좀 해주고 extends 한다.contract()함수는 두개의 인자를 받는다. 어떤 것을 테스트할지 보여주는 string과 테스트를 write하는 장소를 보여주는 callback()it()이라는 함수를 불러서 테스트를 실행한다. string과 테스트 장소를 나타내는callback()으로 이루어져있다.contract("MyAwesomeContract", (accounts) => {
it("should be able to receive Ethers", await () => {
})
})
contract의 callback()에서 it()을 부르는 형태await써서 it()함수 사용Ganache라는 툴은 로컬 이더리움 네트워크를 셋업해준다.Ganache는 시작할 때마다 10개의 테스트 어카운트를 만들고 각각 100이더를 지급한다.Ganache와 Truffle은 잘 연동되어 있어서 우리는 accounts라는 배열로 어카운트에 접근할 수 있다.account[0], account[1]같은 것은 테스트 시 읽기가 매우 불편하다. 그래서 contract() 함수 안에서 아래처럼 initialize 해서 쓴다.let [alice, bob] = accounts;
initial state와 input을 define한다.const contractInstance = await myAwesomeContract.new();
abstraction인 myAwesomeContract을 이용하여 실제 컨트랙트 통신에 이용할 수 있는 자바스크립트 인스턴스를 만든다.//좀비 array를 글로벌로 선언하고
const zombieNames = ["Zombie #1", "Zombie #2"];
//아래처럼 좀비를 이용해 컨트랙트의 메소드를 실행할 수 있다.
//createRandomZombie는 좀비 객체를 받는 함수였을 것..
contractInstance.createRandomZombie(zombieNames[0]);
const result = await contractInstance.createRandomZombie(zombieNames[0], {from: alice});
Setup 부분 코드의 문제점은 좀비를 보내는 주소를 지정하지 못한다는 것이었다.createRandomZombie 함수에 {from: alice}를 넣어 msg.sender가 address를 set할 수 있도록 한다.artifacts.require을 사용하면 result.logs[0].args.name 같이 이용할 수 있다.result.tx: 트랜잭션 해시값result.receipt: 트랜잭션 영수증을 포함하는 objectresult.receipt.status: true일 경우 트랜잭션 성공, 아닐경우 false를 리턴equal(), deepEqual() 같이 빌트인된 assertion 을 이용한다.assert.equal() 이런 식으로 사용! it("should be able to create a new zombie", async () => {
//앞에 만든 abstraction으로 instance를 생성해서 사용
const contractInstance = await CryptoZombies.new();
//await으로 result에 반드시 값이 들어오도록 설정
const result = await contractInstance.createRandomZombie(zombieNames[0], {from: alice});
//영수증 받은거 맞는지 확인
assert.equal(result.receipt.status, true);
//좀비 이름이 같은지 확인
assert.equal(result.logs[0].args.name,zombieNames[0]);
})
beforeEach() 함수로 자동으로 설정해준다.beforeEach(async () => {
// let's put here the code that creates a new contract instance
});
contract.new() 대신에 beforeEach() 한 번이면 뚝딱 해결//컨트랙트 삭제하는 kill 함수 정의
function kill() public onlyOwner {
selfdestruct(owner());
}
//매번 실행이 끝날 때마다 컨트랙트를 kill
afterEach(async () => {
await contractInstance.kill();
});
destruct한다!try {
//try to create the second zombie
await contractInstance.createRandomZombie(zombieNames[1], {from: alice});
assert(true);
}
catch (err) {
return;
}
assert(false, "The contract did not throw.");
try-catch 사용해서 에러가 났을 때 에러 잡음!create 했기 때문이다.helpers/utils에 적고 import 해온다.owner 가 transferFrom()을 불러 receiver에게 토큰 전달function transferFrom(address _from, address _to, uint256 _tokenId) external payable;
owner 가 approve를 부르고 owner나 receiver가 transferForm()을 보내는 형식//approve 부르기
function approve(address _approved, uint256 _tokenId) external payable;
//transferForm 부르기
function transferFrom(address _from, address _to, uint256 _tokenId) external payable;
approve를 부르면 컨트랙트가 트랜잭션 정보(address)를 저장한다.transferForm을 부르면 자동으로 msg.sender가 owner나 receiver의 주소와 일치하는지를 검사한다. context("with the single-step transfer scenario", async () => {
it("should transfer a zombie", async () => {
// TODO: Test the single-step transfer scenario.
//랜덤 좀비 생성 코드
const result = await contractInstance.createRandomZombie(zombieNames[0], {from: alice});
//좀비 아이디가 로그에 찍힌 좀비 아이디와 같은지 확인
const zombieId = result.logs[0].args.zombieId.toNumber();
await contractInstance.transferFrom(alice, bob, zombieId, {from: alice});
//새로운 주인이 bob이 되었는지 확인
const newOwner = await contractInstance.ownerOf(zombieId);
assert.equal(newOwner, bob);
})
})
context("with the two-step transfer scenario", async () => {
//alice가 transferFrom을 부름
it("should approve and then transfer a zombie when the approved address calls transferFrom", async () => {
const result = await contractInstance.createRandomZombie(zombieNames[0], {from: alice});
const zombieId = result.logs[0].args.zombieId.toNumber();
await contractInstance.approve(bob, zombieId, {from: alice});
await contractInstance.transferFrom(alice, bob, zombieId, {from: bob});
const newOwner = await contractInstance.ownerOf(zombieId);
assert.equal(newOwner,bob);
})
//bob이 transferForm을 부름
it("should approve and then transfer a zombie when the owner calls transferFrom", async () => {
const result = await contractInstance.createRandomZombie(zombieNames[0], {from: alice});
const zombieId = result.logs[0].args.zombieId.toNumber();
await contractInstance.approve(bob, zombieId, {from: alice});
await contractInstance.transferFrom(alice, bob, zombieId, {from: alice});
const newOwner = await contractInstance.ownerOf(zombieId);
assert.equal(newOwner,bob);
})
})
x를 붙여 트러플이 검사를 스킵할 수 있도록 조정//context 앞에 x
xcontext("with the single-step transfer scenario", async () => {
it("should transfer a zombie", async () => {
// TODO: Test the single-step transfer scenario.
})
})
//it 앞에 x
context("with the single-step transfer scenario", async () => {
xit("should transfer a zombie", async () => {
// TODO: Test the single-step transfer scenario.
})
})
Contract: CryptoZombies
✓ should be able to create a new zombie (199ms)
✓ should not allow two zombies (175ms)
with the single-step transfer scenario
- should transfer a zombie
with the two-step transfer scenario
- should approve and then transfer a zombie when the owner calls transferFrom
- should approve and then transfer a zombie when the approved address calls transferFrom
2 passing (827ms)
3 pending
function _createZombie(string _name, uint _dna) internal {
uint id = zombies.push(Zombie(_name, _dna, 1, uint32(now + cooldownTime), 0, 0)) - 1;
zombieToOwner[id] = msg.sender;
ownerZombieCount[msg.sender] = ownerZombieCount[msg.sender].add(1);
emit NewZombie(id, _name, _dna);
}
coolDown 시간 설정 때문에 한 번 좀비를 생성하면 하루동안 생성이 안됨.. 그래서 테스트 코드를 여러번 돌려볼 수 없음evm_increaseTime: 다음 블럭의 시간을 increaseevm_mine: 새로운 블록을 mineevm_increaseTime을 작동시킨다. 블록체인은 이미 기록된 블록은 변할 수가 없으니 당장 시간에는 아무 변화가 없고, 대신 다음 채굴되는 블록의 시간을 변화시킨다.evm_mine을 통해 블록을 채굴하면 시간이 앞으로 당겨져있는 것을 볼 수 있다.await web3.currentProvider.sendAsync({
jsonrpc: "2.0",
method: "evm_increaseTime",
params: [86400], // there are 86400 seconds in a day
id: new Date().getTime()
}, () => { });
web3.currentProvider.send({
jsonrpc: '2.0',
method: 'evm_mine',
params: [],
id: new Date().getTime()
});
helper 쪽으로 빼서 작성해놓는다.await time.increase(time.duration.days(1))
time 임포트해와서 가져다가 사용!Contract: CryptoZombies
✓ should be able to create a new zombie (119ms)
✓ should not allow two zombies (112ms)
✓ should return the correct owner (109ms)
✓ zombies should be able to attack another zombie (475ms)
with the single-step transfer scenario
✓ should transfer a zombie (235ms)
with the two-step transfer scenario
✓ should approve and then transfer a zombie when the owner calls transferFrom (181ms)
✓ should approve and then transfer a zombie when the approved address calls transferFrom (152ms)
위의 assertion은 읽기가 힘들어서 chai의 assertion 모듈을 많이 따다가 쓴다!
That said, let's take a look at the three kinds of assertion styles bundled into Chai:
expect: 체인의 natural language assertions 을 다음과 같이 제공한다.let lessonTitle = "Testing Smart Contracts with Truffle";
expect(lessonTitle).to.be.a("string");
should: should 로 시작하고 인터페이스와 비슷한 assertion을 제공한다.let lessonTitle = "Testing Smart Contracts with Truffle";
lessonTitle.should.be.a("string");
assert: notation 을 제공하고 브라우저에서 작동할 수 있는 추가적인 테스트를 제공한다. let lessonTitle = "Testing Smart Contracts with Truffle";
assert.typeOf(lessonTitle, "string");
let zombieName = 'My Awesome Zombie';
expect(zombieName).to.equal('My Awesome Zombie');
assertion들을 위 코드처럼 chai 문법으로 바꾸어 사용한다! loom_testnet: {
provider: function() {
const privateKey = 'YOUR_PRIVATE_KEY';
const chainId = 'extdev-plasma-us1';
const writeUrl = 'wss://extdev-basechain-us1.dappchains.com/websocket';
const readUrl = 'wss://extdev-basechain-us1.dappchains.com/queryws';
return new LoomTruffleProvider(chainId, writeUrl, readUrl, privateKey);
},
network_id: 'extdev'
}
const HDWalletProvider = require("truffle-hdwallet-provider");
const LoomTruffleProvider = require('loom-truffle-provider');
const mnemonic = "YOUR MNEMONIC HERE";
module.exports = {
// Object with configuration for each network
networks: {
//development
development: {
host: "127.0.0.1",
port: 7545,
network_id: "*",
gas: 9500000
},
// Configuration for Ethereum Mainnet
mainnet: {
provider: function() {
return new HDWalletProvider(mnemonic, "https://mainnet.infura.io/v3/<YOUR_INFURA_API_KEY>")
},
network_id: "1" // Match any network id
},
// Configuration for Rinkeby Metwork
rinkeby: {
provider: function() {
// Setting the provider with the Infura Rinkeby address and Token
return new HDWalletProvider(mnemonic, "https://rinkeby.infura.io/v3/<YOUR_INFURA_API_KEY>")
},
network_id: 4
},
// Configuration for Loom Testnet
loom_testnet: {
provider: function() {
const privateKey = 'YOUR_PRIVATE_KEY';
const chainId = 'extdev-plasma-us1';
const writeUrl = 'wss://extdev-basechain-us1.dappchains.com/websocket';
const readUrl = 'wss://extdev-basechain-us1.dappchains.com/queryws';
// TODO: Replace the line below
return new LoomTruffleProvider(chainId, writeUrl, readUrl, privateKey);
},
network_id: '9545242630824'
}
},
compilers: {
solc: {
version: "0.4.25"
}
}
};
HDWallet대신 LoomProvider를 이용하고, 프로바이더로 account 정보를 보내야 한다.return new LoomTruffleProvider(chainId, writeUrl, readUrl, privateKey)
const loomTruffleProvider = new LoomTruffleProvider(chainId, writeUrl, readUrl, privateKey);
loomTruffleProvider.createExtraAccountsFromMnemonic(mnemonic, 10);
return loomTruffleProvider;