0. 목차
1. 개요
2. truffle 없이 contract 배포
3. truffle 활용하여 contract 배포
1. 개요
저번 포스팅에서는 geth console에서 컨트랙트를 배포해 보았다.
오늘은 javascript 코드 작성을 통해 컨트랙트를 배포 해보며 전체적인 흐름을 이해하고,
truffle 을 활요한 컨트랙트 배포를 해보려 한다.
2. truffle 없이 contract 배포
2-1. UTC 파일 복호화 하여 개인키 가져오기
이전의 포스팅에서 javascript에서 web3 라이브러리를 통해 트랜잭션을 구현한 적이 있는데,
그때 ehtereumjs-tx
라이브러리를 이용해서 서명을 만들었었는데
위 라이브러리를 이용해서 서명을 만들기 위해서는 개인키가 필요하다.
사실 오늘 포스팅에서 사용은 하지 않지만, 다음 포스팅에 사용할 예정임
geth네트워크를 구현할 때 만들었던 node 디렉토리 안에 keystrore라는 디렉토리가 있고,
그 디렉토리 안에는 계정정보가 담겨 있는 UTC 파일이 들어있다.
UTC파일은 privateKey를 양방향 암호화가 되어있는 파일이다.
즉, 해당 파일을 복호화를 하면 개인키를 구할 수 있다는 뜻이다.
양방향 암호화지만 salt가 존재하는데 그게 처음 계정을 생성할 때 지정했던 비밀번호이다.
UTC 파일을 복호화 하는 라이브러리는 많은 방법이 있겠지만
난 keythereum을 통해 복호화를 진행하려고 한다.
npm init -y
npm i keythereum web3
설치를하고
const keythereum = require('keythereum');
const path = require('path');
const address = 'eb657c069c9b51cac4d587e7dc4cc26713912c5a'; //UTC 파일에 적혀있는 address
const dir = path.join(__dirname);
const keyObject = keythereum.importFromFile(address, dir);
const privateKey = keythereum.recover('1234', keyObject).toString('hex'); //recover first parameter -> salt
console.log(privateKey); //private Key 출력
테스트를 돌려보니 프라이빗 키가 잘나온다.
..다음에 쓸거니까 잘 기억해두자...^^
2-2. contract 작성
이번에는 contract파일을 새로 만들어서 solidity 코드를 작성해보자.
pragma solidity ^0.8.15;
contract HelloWorld{
string public value;
constructor(){
value = "Hello World";
}
function getValue()public view returns(string memory){
return value;
}
function setValue(string memory _v)public{
value = _v
}
}
저번포스팅이랑 똑같은 내용의 contract이다.
solidity에도 js의 getter, setter 함수와 똑같은 내용이 존재한다.
현재 getValue가 getter, setValue가 setter의 역할을 하는데,
solidity에서는 프로퍼틔의 접근제한자를 public으로 설정하면, getter함수는 자동으로 생성된다.
즉,
pragma solidity ^0.8.15;
contract HelloWorld{
string public value;
constructor(){
value = "Hello World";
}
function setValue(string memory _v)public{
value = _v
}
}
getValue는 없어도 된다.
이제 작성한 contract를 compile을 진행해야한다.
저번포스팅에서는 solc 라이브러리를 cli로 실행시켜서 컴파일된 파일을 만들었는데,
이번엔 cli가 아닌 js 코드를 이용해 컴파일 해서 파일을 만들어 보려한다.
const solc = require('solc');
const fs = require('fs');
const path = require('path');
class Contract {
static compile(_fileName) {
const contractPath = path.join(__dirname, '../contracts/', _fileName);
const content = fs.readFileSync(contractPath, 'utf-8');
const data = JSON.stringify({
language: 'Solidity',
sources: {
[_fileName]: {
content,
},
},
settings: {
outputSelection: {
'*': {
'*': ['*'],
},
},
},
});
const compiled = JSON.parse(solc.compile(data));
console.log(compiled)
}
}
Contract.compile('helloWorld.sol'); // 객체 출력됨
이렇게 작성해주면 된다.
위코드를 실행했을때 에러없이 잘 돌아간다면 성공이다.
data 객체의 스펠링에 주의해야함....
저번 포스팅에서 컴파일을 하면 abi파일과, bin 파일이 아웃풋 되는 걸 확인 할 수 있었는데,
이 둘을 확인하는 메서드를 추가로 작성해보자.
const solc = require('solc');
const fs = require('fs');
const path = require('path');
class Contract {
static compile(_fileName) {
const contractPath = path.join(__dirname, '../contracts/', _fileName);
const content = fs.readFileSync(contractPath, 'utf-8');
const data = JSON.stringify({
language: 'Solidity',
sources: {
[_fileName]: {
content,
},
},
settings: {
outputSelection: {
'*': {
'*': ['*'],
},
},
},
});
const compiled = JSON.parse(solc.compile(data));
this.writeOutput(compiled);
}
static writeOutput(_compiled) {
// console.log(_compiled.contracts['helloWorld.sol'].HelloWorld.abi); abi 확인용
for (const contractFileName in _compiled.contracts) {
const [contractName] = contractFileName.split('.');
console.log(_compiled.contracts[contractFileName][contractName].abi);
console.log(
_compiled.contracts[contractFileName][contractName].evm.bytecode.object
);
}
}
}
Contract.compile('HelloWorld.sol');
출력하려는 값이 depth가 깊어서 조금 난잡하긴하지만 이렇게 하면 잘 찍힌다.
주의 할점은 solidity 코드가 작성된 파일의 첫글자는 무조건 대문자로 작성해야 위의 코드가 적용된다!!
음 콘솔로그가 잘 찍힌다.
그럼 이제 이 내용들을 JSON 형식으로 만들어서 파일로 생성을 해주자
루트디렉토리에 build 디렉토리를 하나 생성해주고,
코드를 수정해주자.
const solc = require('solc');
const fs = require('fs-extra');
const path = require('path');
class Contract {
static compile(_fileName) {
const contractPath = path.join(__dirname, '../contracts/', _fileName);
const content = fs.readFileSync(contractPath, 'utf-8');
const data = JSON.stringify({
language: 'Solidity',
sources: {
[_fileName]: {
content,
},
},
settings: {
outputSelection: {
'*': {
'*': ['*'],
},
},
},
});
const compiled = JSON.parse(solc.compile(data));
return this.writeOutput(compiled); // 수정함.
}
static writeOutput(_compiled) {
// console.log(_compiled.contracts['helloWorld.sol'].HelloWorld.abi); abi 확인용
for (const contractFileName in _compiled.contracts) {
const [contractName] = contractFileName.split('.');
const contract = _compiled.contracts[contractFileName][contractName];
const { abi } = contract;
const {
evm: {
bytecode: { object: bytecode },
},
} = contract;
const obj = {
abi,
bytecode,
};
const buildPath = path.join(
__dirname,
'../build',
`${contractName}.json`
);
fs.outputJSONSync(buildPath, obj);
return [abi, bytecode];
}
}
}
module.exports = { Contract }
2-3. web3 연결 (싱글톤)
이번에는 싱글톤 class를 작성해서 web3를 연결할 수 있는 코드를 작성해보려고 한다.
const Web3 = require('web3');
let instance;
class Client {
constructor(_url) {
if (instance) return instance;
this.web3 = new Web3(
new Web3.providers.WebsocketProvider('ws://127.0.0.1:9005')
);
}
}
module.exports = { Client };
위와 같이 작성하면 여러개의 인스턴스를 생성해도 같은 메모리주소를 참조하게끔 할 수 있다.
준비가 드디어 다 됐다.
이제 컨트랙트를 배포 해보자..
2-4. contract 배포
루트에 index.js 하나 만들어서 작성해보자.
const { Contract } = require('./compile/compile.js');
const { Client } = require('./compile/client.js');
const [abi, bytecode] = Contract.compile('HelloWorld.sol');
const client = new Client('ws://127.0.0.1:9005');
우선 모듈 가져오고 실행시켜주자.
const { Contract } = require('./compile/compile.js');
const { Client } = require('./compile/client.js');
const [abi, bytecode] = Contract.compile('HelloWorld.sol');
const client = new Client('ws://127.0.0.1:9005');
const txObject = {
data: bytecode,
};
const contract = new client.web3.eth.Contract(abi);
contract
.deploy(txObject)
.send({ from: '0xeb657c069c9b51cac4d587e7dc4cc26713912c5a', gas: 3000000 })
.then((instance) => {
console.log(instance.options.address); // CA
});
then / catch 구문 개극혐 이니까.. async / await 으로 변경해주자.
const { Contract } = require('./compile/compile.js');
const { Client } = require('./compile/client.js');
const [abi, bytecode] = Contract.compile('HelloWorld.sol');
const client = new Client('ws://127.0.0.1:9005');
const txObject = {
data: bytecode,
};
const contract = new client.web3.eth.Contract(abi);
async function init() {
const instance = await contract
.deploy(txObject)
.send({ from: '0xeb657c069c9b51cac4d587e7dc4cc26713912c5a', gas: 3000000 });
console.log(instance.options.address);
}
init();
요렇게 변경해주면 된다.
해당 코드를 실행시키면 트랜잭션이 발생하고, 발생된 트랜잭션은 txpool 에 담긴다.
그리고 바로 await이 실행 되는 것이 아니라,
현재 txpool에 있는 transaction의 내용이 블록에 담겼을때(-> 블록 마이닝 됐을때) 실행된다.
블록이 마이닝되고 트랜잭션이 블록에 잘 담기면,
console.log가 동작하는데 출력되는 내용은 배포된 컨트랙트의 CA이다.
해당 CA를 이용해서 자동 생성 되는 getter 함수를 실행해보자.
async function init() {
const instance = await contract
.deploy(txObject)
.send({ from: '0xeb657c069c9b51cac4d587e7dc4cc26713912c5a', gas: 3000000 });
const ca = instance.options.address;
const deployed = new client.web3.eth.Contract(abi, ca);
const data = await deployed.methods.value().call();
console.log('before', data); // before Hello World
}
init();
코드를 실행시키고 마이닝을 했을때
콘솔로그에 Hello World가 찍힌다면 코드가 잘 작성된것이다.
이번엔 setter 함수를 실행 시키는 코드를 추가로 작성해보자.
async function init() {
const instance = await contract
.deploy(txObject)
.send({ from: '0xeb657c069c9b51cac4d587e7dc4cc26713912c5a', gas: 3000000 });
const ca = instance.options.address;
const deployed = new client.web3.eth.Contract(abi, ca);
const data = await deployed.methods.value().call();
console.log('before', data);
await deployed.methods.setValue('kongkong').send({
from: '0xeb657c069c9b51cac4d587e7dc4cc26713912c5a',
});
const changed = await deployed.methods.value().call();
console.log('after', changed);
}
init();
마찬가지로 코드를 실행시키고,
마이닝을 했을 때 처음엔 Hello World가 두번째는 kongkong가 찍힌다면 잘 된것이다.
3. Truffle
위의 모든 과정을 cli 명령어로 손쉽게 처리해주는 라이브러리가 있다.
저번 포스팅에 언급했던 truffle 이다.
우선은 설치부터 진행해보자
npm install -g truffle
mkdir truffle_sample && cd ./truffle_sample
truffle init
truffle init을 실행시키면
3개의 디렉토리와 하나의 파일이 생성되는데 각자 어떤 역할을 하는 지 알아보자.
- contracts : solidity 코드 작성하는 공간
- migrations : deploy
- test : 배포된 애들을 실행시켜 보는 공간
- build: (init 당시에는 생성되지 않지만 빌드 진행하면 생성됨) 컴파일 된 solidity 코드 내용이 담기는 공간
- truffle-config.js : 설정 파일
이제 명령어들을 실행해보면서
이제껏 했던 모든 것들이 명령어 한줄로 해결되는 것들을 확인해보자...^^
3-0. config 설정
truffle을 사용하기전에 세팅을 조금 만져줘야 된다.
init으로 튀어나온 파일 중 config 파일에 들어가서 cmd+F를 검색기능을 켜고 networks
를 검색해보자.
development 객체가 주석 처리가 되어있는데 주석 처리를 해제하고,
각자 네트워크 설정에 맞게 값을 지정해주자.
3-1. compile
기존에 작성했던 solidity 문서를 그대로 contracts 디렉토리에 복붙 해준뒤에 아래 명령어를 실행해주면
truffle compile
build 디렉토리가 생성되고 그안에 컴파일된 파일이 생성된다.
..ㅎ 뭔가 허무함.
3-2. contract 배포
이번엔 contract를 배포 해보자.
truffle migration
# 기존에 배포를 진행했다면 아래의 명령어를 실행하면 된다.
truffle migration --reset
migration을 하면 자동으로 트랜잭션을 발생시킨다.
트랜잭션을 발생시킨 주체는(계정 = EOA) 는 truffle-config.js 파일에서 설정 할 수 있다.
default 는 첫번째 계정임
트랜잭션이 발생했다면 txpool에 해당 내용이 담겼을 것이다.
마찬가지로 블록을 마이닝해서 contract를 배포해보자
3-3. contract 배포 확인
배포가 잘 되었는지 확인해보자.
아래의 명령어 truffle console
을 실행해보면,
터미널 모양이 살짝 바뀐다.
truffle console
truffle(development)>
방금 배포된 contract의 CA를 저장해두었다가 console 기능으로 해당 컨트랙트를 가져온다.
solidity 코드를 컴파일 한 파일의 이름으로 확인 할 수 있다.
truffle(development)> HelloWorld
# 객체 출력
HelloWorld를 입력했을 때 나오는 객체는 web3 라이브러리 + 가장 마지막에 배포된 contract의 내용 + truffle 기능 이 합쳐진 객체인듯...하다..
뇌피셜임..
truffle(development)> HelloWorld.address #CA 출력
위 명령어를 실행시키면 가장 최근에 배포된 contract 의 ca가 출력된다.
truffle(development)> HelloWorld.deployed().then((instance) => hello=instance)
hello #instance 출력
위의 명령어를 실행시키면 가장 최근에 배포된 contract 객체를 hello라는 변수에 담아주게 된다.
truffle(development)> hello.value()
# 'Hello World'
hello.value 를 호출 했을때 Hello World가 출력되는걸 토대로 확인할 수 있다.
이번에는 setValue를 실행해보자.
truffle(development)> hello.setValue('kongkong')
이게 스마트 컨트랙트를 실행시킨거다. 놀랍다.
스마트 컨트랙트가 실행되면,
실행 내용을 담은 트랜잭션이 발생된다.
당연히 마이닝을 해줘야 한다.
마이닝을 해주면
트랜잭션의 내용이 출력된다.
다시, value의 내용을 확인해보자.
truffle(development)> hello.value()
# kongkong
변경된 값이 출력되는 걸 확인 할 수 있다.
댓글