23-06-12
기준 정상 작동하는 코드입니다.
express 프로젝트 생성
express-starter-github
git remote add starter https://github.com/lopahn2/express-starter.git
git pull starter main
npm i
BLOCK_CHAIN_HTTP_PROVIDER = https://sepolia.infura.io/v3/<APIKEY>
SEND_ACCOUNT= 0xINPUT YOUR WALLET ADDRESS IN HERE
SEND_ACCOUNT_PK = INPUT YOUR WALLET PRIVATE KEY IN HERE
컴파일 된 컨트랙트가 저장될 폴더
abi
와 bytecode
가 적힌 json 파일이 저장된다.
빌드할 컨트랙트가 저장될 폴더.
Web3 모듈을 사용할 client.js
, 스마트 컨트랙트 컴파일러 객체가 있는 compile.js
, 컨트랙트를 작성할 템플릿 파일인 contractWriter.js
가 들어있다.
import Web3 from "web3";
let instance;
class Client {
constructor(_url) {
if (instance) return instance;
this.web3 = new Web3(_url);
const account = this.web3.eth.accounts.privateKeyToAccount("0x" + process.env.SEND_ACCOUNT_PK);
this.web3.eth.accounts.wallet.add(account);
instance = this;
}
}
export default Client;
web3 모듈을 사용할 클라이언트 객체이다.
싱글톤 패턴으로 작성됐다.
_url
: 블록체인 IPFS URL을 넣어준다.
IPFS
는InterPlanetary File System
의 약자로서, 분산형 파일 시스템에 데이터를 저장하고 인터넷으로 공유하기 위한 프로토콜이다. 냅스터, 토렌트(Torrent) 등 P2P 방식으로 대용량 파일과 데이터를 공유하기 위해 사용한다.
후에 컨트랙트 서명을 위해 자신의 지갑의 개인키로 계정을 만들어서 등록시켜준다.
import solc from "solc";
import fs from 'fs-extra';
import path, { dirname } from "path";
import { fileURLToPath } from 'url';
import templateContract from './contractWriter.js';
const __fileName = fileURLToPath(import.meta.url);
const __dirname = dirname(__fileName);
class Contract {
static compile(_filename) {
const contractPath = path.join(__dirname, '../contracts', _filename + ".sol");
fs.writeFileSync(contractPath, templateContract(_filename), "utf8");
const data = JSON.stringify({
language: 'Solidity',
sources: {
[_filename]: {
content: fs.readFileSync(contractPath, 'utf8'),
},
},
settings: {
outputSelection: {
'*': {
'*': ['*'],
},
},
},
});
const compiled = JSON.parse(solc.compile(data));
console.log(compiled);
return Contract.writeOutput(compiled); // [abi, bytecode]
}
static writeOutput(_compiled) {
for (const contractFileName in _compiled.contracts) {
const [contractName] = contractFileName.split('.');
const contract = _compiled.contracts[contractFileName][contractName];
const abi = contract.abi;
const bytecode = contract.evm.bytecode.object;
const obj = {
abi,
bytecode,
};
const buildPath = path.join(__dirname, '../build', `${contractName}.json`);
fs.outputJSONSync(buildPath, obj);
return [abi, bytecode];
}
}
}
export default Contract;
생성하길 원하는 / 컴파일하기 원하는 컨트랙트의 경로를 받아온다.
컨트랙트 라이터로 컨트랙트 내용을 작성한 뒤 파일을 작성한다.
컨트랙트 생성용 DATA를 JSON으로 파싱해서 data 변수에 할당한다.
solc.compile(data)
로 컨트랙트를 컴파일한 뒤에 JS 객체로 파싱해 abi와 bytecode를 추출한다.
writeOutput에는 컴파일된 컨트랙트 데이터에서 abi와 bytecode를 추출한 뒤 JSON 파일 형태로 저장한 뒤 abi와 bytecode를 반환한다.
const templateContract = (contractTitle) => {
return `contract Content in here`
}
export default templateContract;
import express from 'express';
import dotenv from 'dotenv';
import moment from 'moment-timezone'
moment.tz.setDefault('Asia/Seoul');
import { createRequire } from "module";
import { Chain, Common, Hardfork } from '@ethereumjs/common';
import { Transaction } from '@ethereumjs/tx';
import sqlCon from '../db/sqlCon.js'
import Contract from '../controllers/compile.js';
import Client from '../controllers/client.js';
import { makeGroupHashedID } from '../lib/hashing.js';
dotenv.config({ path: '../.env' });
const require = createRequire(import.meta.url);
const client = new Client(process.env.BLOCK_CHAIN_HTTP_PROVIDER);
const conn = sqlCon();
const router = express.Router();
const privateKey = Buffer.from(process.env.SEND_ACCOUNT_PK,'hex');
const regExp = /[\{\}\[\]\/?.,;:|\)*~`!^\-_+<>@\#$%&\\\=\(\'\"]/g;
/**
* @Notice Write Smart Contract and Deploy on Chain Network
*
* @Body
* group_id : Long group_id / 1
* title : String title / not included !, @, # ... / testGorupName
*
*/
router.post('/write', async function(req, res, next) {
try {
const contractInfo = req.body;
if (regExp.test(contractInfo.title)) {
return res.status(401).json(
{
message : "그룹 텍스트에 특수문자가 있는 경우 컨트랙트를 생성할 수 없습니다",
}
);
}
const contractTitle = contractInfo.title + "Contract";
const hashedGroupInfo = await makeGroupHashedID(contractInfo.group_id, contractInfo.title);
const contractWriteResult = {};
console.log('transaction...compiling contract .....');
const [abi, bytecode] = Contract.compile(contractTitle);
const MyContract = new client.web3.eth.Contract(abi);
const deploy = MyContract.deploy({
data: "0x" + bytecode,
from: process.env.SEND_ACCOUNT
}).encodeABI();
client.web3.eth.getTransactionCount(process.env.SEND_ACCOUNT, (err, txCount) => {
const txObject = {
nonce: client.web3.utils.toHex(txCount),
gasLimit: 4000000,
gasPrice: client.web3.utils.toHex(client.web3.utils.toWei('10', 'gwei')),
data : deploy
};
const common = new Common({chain:Chain.Sepolia, hardfork: Hardfork.London});
const tx = Transaction.fromTxData(txObject, { common });
const signedTx = tx.sign(privateKey);
const serializedTx = signedTx.serialize();
const raw = '0x' + serializedTx.toString('hex');
client.web3.eth.sendSignedTransaction(raw)
.once('transactionHash', (hash) => {
console.info('transactionHash', hash);
})
.once('receipt', async (receipt) => {
const nowTime = moment().format("YYYY-M-D H:m:s");
contractWriteResult.blockHash = receipt.blockHash;
contractWriteResult.contractAddress = receipt.contractAddress;
contractWriteResult.transactionHash = receipt.transactionHash;
contractWriteResult.status = receipt.status;
await conn.execute('INSERT INTO contracts VALUES (?,?,?,?,?,?,?)', [null, hashedGroupInfo.crypt, receipt.contractAddress, contractTitle,hashedGroupInfo.salt ,nowTime, nowTime]);
await conn.execute('INSERT INTO contract_log VALUES (?,?,?,?)', [null, hashedGroupInfo.crypt, receipt.transactionHash, nowTime]);
console.info('receipt', receipt);
return res.status(201).json(
{
message : "컨트랙트 생성에 성공했습니다.",
contractWriteResult
}
);
}).on('error', (err) => {
return res.status(401).json(
{
message : "컨트랙트 생성에 실패했습니다.",
err
}
);
});
});
} catch(e) {
console.log(e);
return res.status(401).json(
{
message : "예상치 못한 에러가 발생했습니다.",
error : e
}
);
}
});
const [abi, bytecode] = Contract.compile(contractTitle);
const MyContract = new client.web3.eth.Contract(abi);
const deploy = MyContract.deploy({
data: "0x" + bytecode,
from: process.env.SEND_ACCOUNT
}).encodeABI();
const txObject = {
nonce: client.web3.utils.toHex(txCount),
gasLimit: 4000000,
gasPrice: client.web3.utils.toHex(client.web3.utils.toWei('10', 'gwei')),
data : deploy
};
Gas Limit
: 사용할 가스에 대한 예측치( 너무 높으면 거절 당할 수 있음)
Gas Price
: Gas Limit 에서 1 Gas 당 가격, 높을수록 빨리 실행된다.
총 비용
: Gas Limit 중 실제 사용량 * Gas Price
Block Gas Limit
: 한 블록에 넣을 수 있는 Gas Limit 의 총합
const common = new Common({chain:Chain.Sepolia, hardfork: Hardfork.London});
const tx = Transaction.fromTxData(txObject, { common });
const signedTx = tx.sign(privateKey);
const serializedTx = signedTx.serialize();
const raw = '0x' + serializedTx.toString('hex');
일반적인 블록체인 네트워크의 정보를 common 객체를 통해 Transaction 진행시 전달해준다.
트렌젝션에 서명은 발생자의 privateKey를 이용해 진행하며 privateKey는 버퍼로 만들어준다.
const privateKey = Buffer.from(process.env.SEND_ACCOUNT_PK,'hex');
client.web3.eth.sendSignedTransaction(raw)
.once('transactionHash', (hash) => {
console.info('transactionHash', hash);
})
.once('receipt', async (receipt) => {
const nowTime = moment().format("YYYY-M-D H:m:s");
contractWriteResult.blockHash = receipt.blockHash;
contractWriteResult.contractAddress = receipt.contractAddress;
contractWriteResult.transactionHash = receipt.transactionHash;
contractWriteResult.status = receipt.status;
await conn.execute('INSERT INTO contracts VALUES (?,?,?,?,?,?,?)', [null, hashedGroupInfo.crypt, receipt.contractAddress, contractTitle,hashedGroupInfo.salt ,nowTime, nowTime]);
await conn.execute('INSERT INTO contract_log VALUES (?,?,?,?)', [null, hashedGroupInfo.crypt, receipt.transactionHash, nowTime]);
console.info('receipt', receipt);
return res.status(201).json(
{
message : "컨트랙트 생성에 성공했습니다.",
contractWriteResult
}
);
}).on('error', (err) => {
return res.status(401).json(
{
message : "컨트랙트 생성에 실패했습니다.",
err
}
);
});
// ... 위 파일 내용과 동일 경로
/**
* @Notice setStudyGroupContracts Call Router
*
* @Param groupId : Long group_id
* @Param title : String title / Not included !,@,#, ...
*
* @body
* leaderId : String leaderId / admin
* capacity : Int capacity / 4
* groupDepositPerPerson : Int groupDepositPerPerson / 10000
* groupPeriod : Int groupPeriod / 30 (days)
*/
router.post('/:groupId/:title', async (req, res, next) => {
try {
const params = req.params;
const body = req.body;
if (regExp.test(params.title)) {
return res.status(401).json(
{
message : "그룹 텍스트에 특수문자가 있는 경우는 없습니다.",
}
);
}
const hashedGroupInfo = await makeGroupHashedID(params.groupId, params.title);
const groupCreateInfo = {
leaderId : body.leaderId,
groupId : hashedGroupInfo.crypt,
capacity : body.capacity,
groupDepositPerPerson : body.groupDepositPerPerson,
groupPeriod : body.groupPeriod
}
const [contractInfo, ] = await conn.execute('select * from contracts where hashed_group_id = ?',[hashedGroupInfo.crypt]);
const CA = contractInfo[0].contract_address;
const encodedContract = require(`../build/${params.title}Contract.json`);
const deployedContract = new client.web3.eth.Contract(encodedContract.abi, CA);
deployedContract.methods
.setStudyGroupContracts(
groupCreateInfo.leaderId,
groupCreateInfo.groupId,
groupCreateInfo.capacity,
groupCreateInfo.groupDepositPerPerson,
groupCreateInfo.groupPeriod
).send({
from : process.env.SEND_ACCOUNT,
gas: 4000000
})
.on("receipt", (receipt) => {
return res.status(201).json(
{
message : "컨트랙트 작성에 성공했습니다.",
receipt
}
);
});
} catch (error) {
console.log(error);
return res.status(400).json(
{
message : "존재하지 않는 그룹의 스마트 컨트랙트 입니다. ID는 Long type 식별자를 썼는지, Title에 오타가 없는지 확인하세요.",
error : error.message
}
);
}
});
const [contractInfo, ] = await conn.execute('select * from contracts where hashed_group_id = ?',[hashedGroupInfo.crypt]);
const CA = contractInfo[0].contract_address;
const encodedContract = require(`../build/${params.title}Contract.json`);
const deployedContract = new client.web3.eth.Contract(encodedContract.abi, CA);
DB에 저장된 컨트랙트의 주소와 build 폴더에 저장된 json 파일에서 컨트랙트의 abi를 통해서 온체인된 컨트랙트를 불러올 수 있다.
deployedContract.methods
.setStudyGroupContracts(
groupCreateInfo.leaderId,
groupCreateInfo.groupId,
groupCreateInfo.capacity,
groupCreateInfo.groupDepositPerPerson,
groupCreateInfo.groupPeriod
).send({
from : process.env.SEND_ACCOUNT,
gas: 4000000
})
.on("receipt", (receipt) => {
return res.status(201).json(
{
message : "컨트랙트 작성에 성공했습니다.",
receipt
}
);
});
블록체인에 온체인된 컨트랙트 내용을 작성할 수 있다.
여기서 client 객체에 우리 지갑의 개인키를 등록시키지 않으면 에러가 발생한다.
// ... 위 파일 내용과 동일 경로
/**
* @Notice callContractDetail Call Router
*
* @Param groupId : Long group_id
* @Param title : String title / Not included !,@,#, ...
*
*/
router.get('/callContract/:groupId/:title', async (req, res, next) => {
try {
const params = req.params;
if (regExp.test(params.title)) {
return res.status(401).json(
{
message : "그룹 텍스트에 특수문자가 있는 경우는 없습니다.",
}
);
}
const hashedGroupInfo = await makeGroupHashedID(params.groupId, params.title);
const [contractInfo, ] = await conn.execute('select * from contracts where hashed_group_id = ?',[hashedGroupInfo.crypt]);
const CA = contractInfo[0].contract_address;
const encodedContract = require(`../build/${params.title}Contract.json`);
const deployedContract = new client.web3.eth.Contract(encodedContract.abi, CA);
deployedContract.methods
.callContractDetail()
.call({
from : process.env.SEND_ACCOUNT,
gas : 4000000
})
.then(result =>console.info(result))
.catch(err => console.log(err));
return res.status(201).json(
{
message : "컨트랙트 읽기에 성공했습니다.",
hashedGroupInfo
}
);
} catch (error) {
console.log(error);
return res.status(400).json(
{
message : "존재하지 않는 그룹의 스마트 컨트랙트 입니다. ID는 Long type 식별자를 썼는지, Title에 오타가 없는지 확인하세요.",
error : error.message
}
);
}
});