트러플을 사용해 빌드한 프로젝트 구조는 다음과 같이 된다.
├── 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
: 트랜잭션 영수증을 포함하는 object
result.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
: 다음 블럭의 시간을 increase
evm_mine
: 새로운 블록을 mine
evm_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;