(Project) SideProject EtherScan (blockChain)
블록체인을 배우고 약 한 달 반 정도 지났을 때
오랜만에 개인 프로젝트가 잡혔다
첫 개인 프로젝트 이후에 4달 만에 돌아온 개인 프로젝트
팀 프로젝트를 하기 전에는 몰랐지만, 요번 개인 프로젝트를 시작한다고 했을 때 무언가 긴장이 덜 됐었다
그러면서 느낀거는
아,, 확실히 개인 프로젝트가 부담이 덜 하는구나...
라고 생각이 들었다. 거기
블록, 트랜잭션, 지갑 주소 정보 출력
토큰의 코인에 대한 가격은.... 적당히 각자의 판단에
라는 말에 정말 저거만 하면 되나?ㅋㅋㅋㅋ라는 나쁜 마음이 엄청 들곤 했다ㅋㅋㅋ
어느 프로젝트를 하든 항상 하던 거처럼 환경설정 하고 EtherScan이라는 블록체인 익스플로러를 만들면 된다고 하셨으니
그 페이지를 프로젝트 요구사항에 맞춰 ClonePaging 한다고 생각하고
메인 페이지 뼈대를 잡아가기 시작했다
아무 Link나 routing 설정도 없이 정말 메인 페이지만 뼈대부터 잡기로 했다. 임시 dummy 데이터로 채워 넣고 규격을 잡았다
근데.. 아.. 만들면서 과정 좀 캡처 좀 따 놓을걸 그랬다.. 남아있는 게 insight 뿐이라니.......ㅠㅠ
하루 만에 일단은 메인 페이지 전체 뼈대를 다 잡았다, 빠르게 잡고 web3 통신 쪽에서 여유롭게 시간을 쓰고 싶었기에
두 번째 날부터는 메인 페이지에서 DB에 있는 블록에서 마지막 6개와 트랜잭션에서도 마지막 6개를 뽑아서
출력해 주는 기능을 하려고 마음을 먹었다
위처럼 블록과 트랜잭션 테이블의 model 설정을 하고, 두 테이블의 관계도 맺어 주었다
(cf. http://blockchaindev.kr/models/content/86 )
그러고 마이닝 후 express 쪽에서 web3통신을 통해 필요한 블록들의 정보를 담아 Block 테이블에 create 하고 높이 기준은
로 내림 차순 정렬 후 6개만 찾아서 리액트 쪽으로 응답을 보내 주었다
const tempLatestBlock = await web3.eth.getBlock("latest");
for (let i = 0; i <= tempLatestBlock.number; i++) {
web3.eth.getBlock(i, false, (error, block) => {
db.Block.create(
{
hash: block.hash,
nonce: block.nonce,
number: block.number,
parentHash: block.parentHash,
receiptsRoot: block.receiptsRoot,
size: block.size,
time: block.timestamp,
difficulty: block.difficulty,
miner: block.miner,
txs: block.transactions.length,
transactionsRoot: block.transactionsRoot,
gasUsed: block.gasUsed,
gasLimit: block.gasLimit,
},
{ ignoreDuplicates: true }
);
});
}
const listUp = await db.Block.findAll({
order: [["number", "desc"]],
limit: 6,
});
res.send({ list: listUp });
- axios.post("/api/block/latestBlocks")에서 코드의 일부이다
이와 같이 세 번째 날은 geth를 통해 트랜잭션을 발생시켜 또한 블록의 높이를 기준으로 6개를 리액트 쪽으로 응답해 주었다
web3.eth.getBlockNumber((err, number) => {
for (let i = 1; i <= number; i++) {
web3.eth.getBlockTransactionCount(i, true, (err, count) => {
if (count > 0) {
for (let j = 0; j < count; j++) {
web3.eth.getTransactionFromBlock(i, j, async (err, tx) => {
const tempBlock = await db.Block.findOne({
where: { number: tx.blockNumber },
});
if (tempBlock) {
const checkTx = await db.Transaction.findOne({
where: { blockHeight: tempBlock.number },
});
if (!checkTx) {
const txAdd = await db.Transaction.create({
blockHash: tx.blockHash,
blockNumber: tx.blockNumber,
from: tx.from,
to: tx.to,
hash: tx.hash,
nonce: tx.nonce,
transactionIndex: tx.transactionIndex,
r: tx.r,
s: tx.s,
v: tx.v,
value: tx.value,
});
tempBlock.addTransaction(txAdd);
}
}
});
}
}
});
}
});
const checkList = await db.Transaction.findAll({
order: [["blockHeight", "desc"]],
limit: 6,
include: [
{
model: db.Block,
attributes: ["time", "parentHash"],
},
],
});
res.send({ list: checkList });
- 제네시스 블록을 제외한 블록들을 전부 확인하면서 트랜잭션이 있는 블록에 대해서만 Transaction 테이블에 추가해 주었다.
- 추가해주고 6개를 응답해 줄 때 블록 테이블에서 시간과 이전 해쉬값을 포함해서 응답해 주었다
이렇게 메인에 6개씩 출력하는 걸 이용해서
전체 블록들과 트랜잭션들 보여주는 페이지도 재활용하여 틀 정도만 만들어서 주말을 마무리했다
월요일에는 블록과 트랜잭션의 상세페이지 작업을 시작하였다
일단은 틀을 잡고, EtherScan 페이지를 참고하여 필요한 정보들을 받아 출력해 주었다
다른 페이지에서 블록의 높이를 클릭해서 이동할 수 있도록 링크설정을 해주었다
<Link to={`/blocks/${item.number}`}>{item.number}</Link>
그리곤 상세페이지 Container에서는 useParams Hook을 통해 높이에 맞는 블록마다 정보들 띄워주게 설정했다
// detailBlock Container.jsx
axios.post("http://localhost:8083/api/block/detail", {number: params.blockInfo})
// routes/block.js
router.post("/detail", async (req, res) => {
const { number } = req.body;
const detailBlock = await db.Block.findOne({ where: { number: number } });
res.send({ block: detailBlock });
});
이렇게 블록 상세페이지를 만들었다. 이를 활용하여 트랜잭션도 완성하기 수월했다
그러곤 이 전까지 했던 내용들로
block에서는 receipt 계정주소값, tx에서는 from, to에 있는 계정주소를 눌렀을 때, 계정 상세페이지로 이동할 계획이라
계정 상세 페이지를 만들었다
계정 주소 값과 잔액을 보여주고, 트랜잭션이 있다면 마지막 트랜잭션과, 처음 보낸 트랜잭션을 보여준다
그 밑에는 그 계정이 보내고, 받은 트랜잭션 리스트를 출력해주었다
마지막으로는 검색 기능을 구현하면서 개인 프로젝트를 마무리했다
<input
type="text"
value={inputData}
onInput={(e) => {
setInput(e.target.value);
}}
placeholder="Search by Addr / Tx hash / Block "
/>
<button
onClick={() => {
checkInput(inputData);
setInput("");
}}
>
<img
src="https://media.giphy.com/media/wp2rA9gXbKXo0KzTjD/giphy.gif"
alt=""
/>
</button>
const checkInput = (_input) => {
if (_input.length == 66 && _input.includes("0x"))
navigate(`/txs/${_input}`);
else if (_input.length == 42 && _input.includes("0x"))
navigate(`/address/${_input}`);
else if (/[0-9]/g.test(_input)) {
if (_input.length < 42) navigate(`/blocks/${_input}`);
} else navigate(`/${_input}`);
};
- 매개변수로 전달받은 값을 조건으로 판단하여서 해당 경로로 useNavigate Hook을 사용하여 전환시켰다
다만, 이렇게 하니까 없는 블록들도 상세페이지로 넘어가고
그 내용물들이 비어진 상태로 이동되는 아주 난감한.... 상황이 나타났다
이에 생각한 예외처리는
<Route path="/*" element={<NotFoundContainer />} />
이렇게 하여서 axios로 요청으로 block테이블에 없는 값으로 findOne 했을 때 아무것도 찾지 못하고
catch에 걸렸을 때 해당 params의 정보를 notFount로 넘겨서 예외처리를 해결했다
그렇게 마무리를 지으려고 했지만...
전체 블록과 전체 트랜잭션들을 보여주는 페이지에서 페이징네이션을 해야겠다는 생각이 들었다
그래서 react-js-pagination 라이브러리를 설치하였다
// Paging/paging.js
import Pagination from "react-js-pagination";
import "./Paging.css";
export const Paging = ({ page, count, setPage }) => {
return (
<Pagination
activePage={page} // 현재 페이지
itemsCountPerPage={7} // 한페이지 당 보여줄 아이템의 개수
totalItemsCount={count} // 아이템의 총 개수
pageRangeDisplayed={7} // 페이징네이션 목록 보여줄 범위
prevPageText={"◀"} // '이전'을 나타내는 텍스트(모양)
nextPageText={"▶"} // '다음'을 나타내는 텍스트(모양)
onChange={setPage} // 페이지가 바뀌는 걸 핸들링할 함수
/>
);
};
- 기본적인 CSS가 아예 없는 상태이다
- 코드 기준 한 페이지당 7개의 목록을 확인할 수 있도록 설정
- count 변수는 페이징네이션할 전체 수를 담고 있다
/*
Paging/paging.css
*/
.pagination {
display: flex;
justify-content: center;
margin-top: 15px;
}
ul {
list-style: none;
padding: 0;
}
ul.pagination li {
display: inline-block;
width: 30px;
height: 30px;
border: 1px solid #e2e2e2;
display: flex;
justify-content: center;
align-items: center;
font-size: 1rem;
}
ul.pagination li:first-child {
border-radius: 5px 0 0 5px;
}
ul.pagination li:last-child {
border-radius: 0 5px 5px 0;
}
ul.pagination li a {
text-decoration: none;
color: #337ab7;
font-size: 1rem;
}
ul.pagination li.active a {
color: white;
}
ul.pagination li.active {
background-color: #337ab7;
}
ul.pagination li a:hover,
ul.pagination li a.active {
color: blue;
}
.page-selection {
width: 48px;
height: 30px;
color: #337ab7;
}
- 위의 CSS 내에서 원하는 색상으로 변경해서 사용했다
검색을 ㅋㅋㅋ 통한 pagination 라이브러리 사용법과 CSS 효과들을 가져와서 내가 원하는 색상과 보여줄 아이템을 변경해서 사용했다
아쉬운 점은 EtherScan처럼 보이는 목록의 개수를 변경할 수 없다는 점인데
이를 select의 option value로 바뀌는 state값으로 조절해서 10개, 25개, 50개, 100개 선택해서 바뀌도록 설정했다
<select
onChange={(e) => {
setPageNumber(+e.target.value);
}}
>
<option value={10}>10</option>
<option value={25}>25</option>
<option value={50}>50</option>
<option value={100}>100</option>
</select>
라이브러리 쓰는 김에 블록높이에 따른 즉, 블록마다의 트랜잭션의 수를 그래프로 나타내보고자
그래프 라이브러리도 사용했다
트랜잭션을 내림차순 정렬 후 50개만 가져와서 트랜잭션의 수의 변화량을 그래프로 확인할 수 있었다
return (
<ChartBox>
<div>Transaction In Chart</div>
<LineChart
width={1300}
height={400}
data={txs}
margin={{
top: 5,
right: 30,
left: 20,
bottom: 5,
}}
>
<XAxis dataKey="number" />
{/* 가로축 숫자 */}
<YAxis dataKey="txs" />
<Tooltip />
<Legend />
<Line
type="monotone"
name="Transactions"
dataKey="txs"
stroke="#8884d8"
activeDot={{ r: 8 }}
/>
<Line
type={"monotone"}
name="Block Height"
dataKey="number"
stroke="#82ca9d"
/>
</LineChart>
</ChartBox>
);
처음엔 언제 다할까.... 어떻게 만들어야 보기 편할까
라는 많은 생각을 했었지만, 막상 프로젝트가 시작이 되고 하니까 조금씩 조금씩 점층적으로 나아갔던 거 같다
아직도 아쉬운 점도 많고 추가해야 되는 기능들도 있겠지만, 어느 정도의 기능들은 잘 실행되고
블록과 트랜잭션의 web3 통신 eth 메서드들을 많이 해서 쓰다 보니 사용법 또한 많이 익혀서 너무 좋은 경험이었다