지난주차에는 스마트 컨트랙트에 대해서 공부했다면,
이번주는 그 스마트컨트랙트를 이용해서 블록체인을 활용한 어플리케이션을 개발하는 과정을 살펴본다.
<Bapp 이란?>
일반적인 App 은 아이디,비밀번호를 이용해서 로그인을 하고 서버에 정보가 저장된다.
하지만 Bapp 서비스는 개인키를 이용해서 로그인하고 블록체인 에 정보를 저장한다.
개인키는 참말로 길고 어렵게 생겼는데, 이를 사용자들이 직접 사용하기에는 무리가 있다.
그래서 우리는 Bapp 서비스를 만들기 위해서 Caver.js, Klaytn API, Klip API 와 같은 툴들을 이용하여
유저가 편리하게 Bapp을 이용할 수 있게 할 수 있다.
<개발 환경 설정 (React, Klip)>
우리는 React를 이용해서 개발을 할건데,
React 프로젝트는 여러가지 방식으로 만들 수 있는데
여기서 우리는 create-react-app이라는 스크립트를 통해서 만들 것이다.
//create-react-app을 이용한 klay-market이라는 리액트 프로젝트를 생성한다.
npx create-react-app klay-market
//생성 완료 후 리액트 프로젝트 실행되는지 확인 (localhost:3000 에 리액트 뜸)
cd klay-market
yarn start <or> npm run start
//추가 라이브러리 설치
//axios - 외부와의 소통을 도와줌(api call을 도와줌)
yarn add axios <or> npm install axios
//caver.js
yarn add caver.js <or> npm install caver-js
//혹시나 caver.js 설치가 안되면 nodejs ver 12 이상 사용할 것
//버전 확인
node --version
//버전 바꾸기
nvm use 12
//qrcode 설치
yarn add qrcode.react <or> npm install qrcode.react
caver.js라는 라이브러리를 깔았는데, 이를 좀 잘 사용하기 위해서는
klaytn api service(KAS)를 가입하는 것이 필요하다.
https://www.klaytnapi.com/ko/landing/main
KAS를 이용하면, 우리가 노드를 따로 운용할 필요 없이
노드를 이용한 기능들을 사용할 수 있다.(노드를 직접 운용하는 것은 사실 좀 힘들고 귀찮은 일이다.)
Security에 Credential이라는 메뉴가 있는데 들어가면 AccessKey를 생성할 수 있다.
이 키들은 아무도 모르게 보안이 좋은 곳에 잘 보관해야한다.
<caver.js (Read)>
클레이튼에서 여러가지 doc들을 '한글'로 제공한다.
여기에 caver.js에 관한 것들도 있으니, 필요하다면 참고하면 된다.
이 강의에서는 몇가지 기능만 사용할 것이다.
https://ko.docs.klaytn.com/bapp/sdk/caver-js
전 시간에 설치한 klay-market의 app.js에서 코딩을 합니다.
이번시간에 해야할일을 미리 정리해보면
// 1. Smart contract 배포 주소 파악(가져오기)
// 2. caver.js 이용해서 스마트 컨트랙트 연동하기
// 3. 가져온 스마트 컨트랙트 실행 결과(데이터) 웹에 표현하기
import logo from './logo.svg';
import Caver from 'caver-js'; //받았던 caver-js를 연동시켜줍니다.
import './App.css';
//필요한 정보들을 상수로 저장해주고
const COUNT_CONTRACT_ADDRESS = '0xb38848B4b7C4f581543Da61ace6cAFDeAA1e69aF'; //사용할 컨트랙트 주소
const ACCESS_KEY_ID = 'KASKX7TPUMWXIL7U09QBJDBA'; // KAS ACCESS KEY
const SECRET_ACCESS_KEY = 't6AWl9gVKpsYnRBNtOzfhGpvZVbx0avAgmoBTOsN'; // KAS SECRET KEY
const CHAIN_ID = '1001'; // mainnet 8217 testnet 1001
const COUNT_ABI = '[{"constant": false,"inputs": [{ "name": "_count", "type": "uint256" }], "name": "setCount","outputs": [],"payable": false,"stateMutability": "nonpayable", "type": "function"},{"constant": true,"inputs": [],"name": "count","outputs": [{ "name": "", "type": "uint256" }], "payable": false,"stateMutability": "view", "type": "function"},{"constant": true,"inputs": [],"name": "getBlockNumber", "outputs": [{ "name": "", "type": "uint256" }], "payable": false,"stateMutability": "view", "type": "function"}]';
const option = {
headers : [
{
name : "Authorization",
value : "Basic " + Buffer.from(ACCESS_KEY_ID + ":" + SECRET_ACCESS_KEY).toString("base64")
},
{name : "x-chain-id", value : CHAIN_ID} //tesr or main 판단쓰
]
}
//어느 노드와 통신할건지를 알려주고, 통신 연결을 한다.
const caver = new Caver(new Caver.providers.HttpProvider("https://node-api.klaytnapi.com/v1/klaytn", option));
//컨트랙트의 어떤 컨트랙트를 실행할건지, 컨트랙트 내의 함수 설명서와 주소를 넣어준다.(여기서 caver는 위의 caver)
const CountContract = new caver.contract(JSON.parse(COUNT_ABI), COUNT_CONTRACT_ADDRESS);
//컨트랙트 실행 함수
const readCount = async() => {
//CountContract(위의 CountContract).methods.실행할 함수.call();
const _count = await CountContract.methods.count().call();
console.log(_count);
}
//
COUNT_ABI
가져온 contract를 어떻게 해석해야하는지에 대한 정보
klaytn ide에서 작성한 코드 컴파일 버튼 밑에 ABI버튼이 있다 누르면 자동 복사가 된다.
//
const option 부분은 노드와 소통하는 부분에서 정해진 형식이므로 그냥 따라서 하면 된다.(대충 KAS 가입자인지 확인하는 내용)
value 부분에 저 값들 대신 아까 AccessKey를 생성할 때 있던 Authorization값을 넣어도 된다. 편한대로 쓰면 된다.
<내가 하다가 만난 에러들과 원인 해결책>
Error: You must provide the json interface of the contract when instantiating a contract object.
json을 문자열로 넣었을 때 에러/ JSON.parse를 붙여주면 댐
Unhandled Rejection (TypeError): Cannot read property 'count' of undefined
count값을 불러올 수 없다는 뜻인데, 이것은 readCount함수에서 CountContract 호출에 오류가 있을 때 생김,
여기서는 methods를 method로 적어서 생겼었음
Unhandled Rejection (Error): Invalid JSON RPC response: {"code":1010008,"message":"The authorization header you provided is invalid."}
option의 값이 잘못되었을 경우 나타남(주고 받을때 양식이 달라서 인증과정 실패하는 에러)
여기서는 hearders를 header로 적어서 나타났었음.
<짜잔>
다 했으면 readCount();를 App() 함수 안에 살포시 넣어본다.
(위의 코드에는 없지만, 기본적으로 App.js파일 안에 있는 함수이다.)
짜잔. 위와 같이 본인이 컨트랙에 설정한 값이 뜨면 성공이다!
밑의 워닝 두개는 찾아보니까 별 상관없다해서, 그냥 뒀다.
보기 싫으면 해당 코드를 찾아서 주석처리하면 된다고 한다.
<ABI(Application Binary Interface)>
쉽게 설명하면, 사용 설명서 같은거다.
스마트컨트랙트를 외부에서 호출할 때 컨트랙트 코드 자체를 전부 알 필요가 없기 떄문에,
그 코드에 어떤 기능이 있고, 어떤 변수를 넣어주면 되는지와 같은 내용들을 설명하는 것이다.
<전체적인 로직>
컨트랙트 주소로 찾아가서 ABI를 통해서 필요한 함수를 사용 요청하고 결과값을 가져온다.
caver-js는 그 사이에서 코드를 컴퓨터가 알아들을 수 있는 언어들로 변경해준다.
이 모든 과정은 KAS가 본인의 노드를 이용해서 해준다.
라고 하셨지만, 코드를 보면 그 KAS를 이용하게 해주는 것 까지 caver-js의 역할인 것처럼 보인다.
<추가 getBalance 함수>
const getBalance = (address) => {
return caver.rpc.klay.getBalance(address).then((response) => {
const balance = caver.utils.convertFromPeb(caver.utils.hexToNumberString(response));
console.log(`BALANCE : ${balance}`);
return balance;
})
}
//블록체인에 클레이 잔고 요청하기
//caver.rpc.klay.getBalance(address)
//답변이 오면
//.then((response)
//답변이 16진수로 오는데 그걸 읽을 수 있는 숫자로 변경해주세용
//caver.utils.hexToNumberString(response)
//klay최소단위인 Peb으로 값이 넘어오는데, 그걸 klay단위로 바꿔주세요.
//caver.utils.convertFromPeb
//콘솔에 찍어서 확인해보기
//console.log(`BALANCE : ${balance}`);
<caver.js (Write)>
지난 시간엔 Count.sol의 count 변수를 불러보고, 클레이 잔고를 조회해봤다면, 이번시간엔 setCount함수를 실행해보겠다.
값을 부르는 일과 달리, 함수를 실행시키는 일은 트랜잭션을 발생시키기 때문에 수수료가 필요하고
따라서 클레이 잔고가 있는 지갑(개인키)이 필요하다.
const setCount = async(newCount) => {
try{
// 수수료를 내기 위한 account 설정과정
// privatekey 입력
const privatekey = '어쩌구저쩌구';
//caver에 privatekey를 넣어줌
const deployer = caver.wallet.keyring.createFromPrivateKey(privatekey);
caver.wallet.add(deployer);
//스마트 컨트랙트 실행 트랜잭션 날리기
//위의 readCount는 값을 읽어오기만 하기 때문에 call을 쓰지만
//setCount는 newCount라는 값을 보내줘야하기 때문에 send를 쓴다.
//어떤 지갑에서(from), 수수료를 얼마나(gas)낼 것인지
const recipt = await CountContract.methods.setCount(newCount).send({
from : deployer.address, //아까 넣어준 deployer의 address에서
gas : "0x4bfd200" //gas fee를 사용한다. 아무값이나 넣어도 알아서 필요한만큼만 나감. 너무 작지만 않으면 됨.
});
//결과 확인
console.log(recipt);
} catch(e) {
console.log(`[ERROR_SET_COUNT]${e}`);
}
}
function App()에 button을 추가해준다.
function App() {
readCount();
getBalance('0x7b9133bdd4e31765917843532fe76ddc8c0f0249');
return (
<div className="App">
<header className="App-header">
<img src={logo} className="App-logo" alt="logo" />
// 카운트 변경 버튼
// 누르면 카운트가 100으로 변경된다.
<button title={'카운트 변경'} onClick={()=>{setCount(100)}} />
<p>
Edit <code>src/App.js</code> and save to reload.
</p>
<a
className="App-link"
href="https://reactjs.org"
target="_blank"
rel="noopener noreferrer"
>
Learn React
</a>
</header>
</div>
);
}
버튼을 만들고 누르면
이런식으로 트랜잭션 결과가 Console에 뜨게 된다.
call()은 읽은 값을 리턴해서 보여줬지만, send는 이렇게 트랜잭션 결과를 리턴값으로 준다.
정말 잘 갔는지 확인하고 싶다면, 블록 해쉬를 클레이튼 스코프에 검색해서 확인해보면 된다.
(사실 이미 몇번 실행해봤기 때문에, 아까 readCount 결과가 2000이었는데 지금 100으로 바뀐 걸 볼 수 있다.)
사실 개인키(private key)를 저렇게 코드 안에 넣는다거나, 사용자가 직접 입력하게 한다거나 하는 행동은 유출의 위험이 있기 때문에, 보안상 좋지 않은 행동이다. 개인키를 넘겨주는 것은 그 지갑의 소유가 넘겨주는 것과 같기 때문에 개인키가 유출되는 것은 아주 위험한 일이다.
또한 개인키를 직접 입력하는 것은 보안상의 문제 뿐 아니라,
개인키가 길고 의미없는 글자들로 이뤄져있기 때문에, 상당히 어렵고 번거로운 일이다.
따라서 다음 챕터에서는 클레이튼에서 제공하는 Klip API를 이용해서,
개인키의 입력없이 qr을 이용해 쉽게 BAPP에 접근할 수 있도록 할 것이다.
<Klip API 사용하기 (1)>
개인키를 직접 사용하지 않기 위해서 우리는 Klip API를 사용하기로 했다.
API doc을 보면
여기서 우리가 사용할 API는 App2App API이다.
설명을 읽어보면 딱 우리가 원하는 기능이 사용가능하다고 쓰여있다.
여기서 Klip이란 카카오톡내에 있는 클레이 지갑이다.
이 Klip 지갑을 연동하는 작업을 도와주는 API가 Klip API이다.
Klip API는
Prepare -> Request -> Result 세 단계의 과정을 거치는데
Prepare은 사용 권한을 요청하는 작업이다. 사용자는 이 과정에서 클립 지갑 비밀번호를 입력하여 권한을 승인한다.
Request는 컨트를 실행하는 과정,
Result는 결과를 확인하는 과정이다.
이 내용들도 그렇고 더 자세한 내용들도 doc에 있으니 필요하면 doc을 참고하자!
Klip 자체가 테스트넷을 지원하지 않기 때문에, Klip API도 메인넷에서만 사용이 가능하다.
따라서 컨트랙트 배포와 실행에 실제 클레이가 필요하기 때문에,
컨트랙트를 배포하고 사용할 클레이 지갑을 새로 만들어, 코인을 전송해주도록 하자.
testnet, mainnet, caver-js, klip_api 점점 코드가 복잡해지니까
필요한 파일과 폴더를 생성해 정리해준다.
abi와 api 그리고 constants폴더로 나눴는데
constants에는 cypress와 baobab에서 필요한 컨트랙트 주소들과 키들을 넣어주었다.
<react 문법 조금>
useState는 balance의 기본값을 세팅해준다.
const [balance, setBalance] = useState('0');
그리고 함수에 함수를 인자로 넣어줄 수 있다.
예시코드
function onPressButton2(_balance, _setBalance) {
_setBalance(_balance);
}
function App() {
const [balance, setBalance] = useState('0');
// readCo unt();
// getBalance('0x7b9133bdd4e31765917843532fe76ddc8c0f0249');
return (
<div className="App">
<header className="App-header">
<img src={logo} className="App-logo" alt="logo" />
<button title={'카운트 변경'} onClick={()=>{setCount(100)}} />
<button onClick={()=>{onPressButton2('15', setBalance)}}>HI</button>
<p>
{balance}
</p>
<a
className="App-link"
href="https://reactjs.org"
target="_blank"
rel="noopener noreferrer"
>
Learn React
</a>
</header>
</div>
);
}
필요한 내용을 대충 살펴봤고, 이제 Klip API를 이용해서 지갑의 주소를 가져오는 것을 할 것이다.
<button onClick={()=>{onClickgetAddress();}}>주소 가져오기</button>
버튼 하나 일단 만들어 놓고
UseKlip.js파일에
import axios from "axios";
export const getAddress = () => {
axios.post(
"https://a2a-api.klipwallet.com/v2/a2a/prepare",
bapp: {
name : "KLAY_MARKET"
},
type : "auth"
).then((response) => {
const { request_key } = response.data;
let id = setInterval(( => {
axios.get(`https://a2a-api.klipwallet.com/v2/a2a/result?request_key=$(request_key)`).then((res) => {
if (res.data.result) {
console.log(`[Result] ${res.data.result}`);
clearInterval(timerId);
}
});
}, 1000))
});
}
모바일에서는 바로 연동이 되도록 할 수도 있지만,
PC환경을 생각해 QR코드를 이용해보자
//다운 받았던 QR코드 import 해주고
import QRCode from "qrcode.react";
// UseKlip 파일에 만든 getAddress 함수를 가져온다.
import * as KlipAPI from "./api/UseKlip";
//DEFAULT값 정해주고 적용해준다.
const DEFAULT_QR_CODE = "DEFAULT";
const [qrvalue, setQrvalue] = useState(DEFAULT_QR_CODE);
//위에서 가져온 KlipAPI.getAddress 함수를 실행한다
const onClickgetAddress = () => {
KlipAPI.getAddress(setQrvalue);
}
//태크 만들어주면 위치에 QR이 생긴다
<QRCode value={qrvalue}></QRCode>
UseKlip 파일 완성본
import axios from "axios";
export const getAddress = (setQrvalue) => {
axios.post(
"https://a2a-api.klipwallet.com/v2/a2a/prepare", {
bapp : {
name : "KLAY_MARKET",
},
type : "auth",
})
.then((response) => {
const { request_key } = response.data;
const qrcode = `https://klipwallet.com/?target=/a2a?request_key=${request_key}`;
setQrvalue(qrcode);
let timerId = setInterval(()=> {
axios
.get(
`https://a2a-api.klipwallet.com/v2/a2a/result?request_key=${request_key}`
)
.then((res) => {
if (res.data.result) {
console.log(`[Result] ${JSON.stringify(res.data.result)}`);
clearInterval(timerId);
}
});
}, 1000);
});
}
여기까지하면
이렇게 qr코드를 이용해 klip 지갑의 주소를 가져올 수 있다.
<Klip API 사용하기 (2)>
지난 시간엔 지갑 주소를 불러왔다면, 이번 시간엔 Klip API를 통해 스마트 컨트랙트를 실행해 볼 것이다.
getAddress와 같지만 다른 점은 type부분이다.
const A2P_API_PREPARE_URI = "https://a2a-api.klipwallet.com/v2/a2a/prepare";
const APP_NAME = "KLAY_MARKET";
export const setCount = (count, setQrvalue) => {
axios.post(
A2P_API_PREPARE_URI, {
bapp : {
name : APP_NAME,
},
type : "execute_contract",
transaction : {
"transaction": {
to : COUNT_CONTRACT_ADDRESS, // contract address
value : "0", // 단위는 peb. 1 KLAY
abi : '{"constant": false,"inputs": [{ "name": "_count", "type": "uint256" }], "name": "setCount","outputs": [],"payable": false,"stateMutability": "nonpayable", "type": "function"}',
params : `[\"${count}\"]`;
}
})
.then((response) => {
const { request_key } = response.data;
const qrcode = `https://klipwallet.com/?target=/a2a?request_key=${request_key}`;
setQrvalue(qrcode);
let timerId = setInterval(()=> {
axios
.get(
`https://a2a-api.klipwallet.com/v2/a2a/result?request_key=${request_key}`
)
.then((res) => {
if (res.data.result) {
console.log(`[Result] ${JSON.stringify(res.data.result)}`);
clearInterval(timerId);
}
});
}, 1000);
});
}
수수료는 클레이튼에서 2021년 7월까지는 대납해준다
<Clean up>
트랜잭션 확인 부분을, 성공적으로 블록에 올라갔을 때 값이 프린트되도록 수정하겠다.
일단 코드에서 clearInterval(timerId);을 제거하고 실행해보면 pending이라는 상황과 success 상황이 같이 나온다.
이 때 success일때 멈추도록 if문을 하나 추가해주면 끝!
if (res.data.result) {
console.log(`[Result] ${JSON.stringify(res.data.result)}`);
if (res.data.result.status === "success") {
clearInterval(timerId);
}
이렇게 하면 트랜잭션이 성공적으로 블록에 올라갔을 때 유저들에게 알려줄 수 있다.
'강좌 & 책 > 멋사NFT' 카테고리의 다른 글
[4주차] NFT 블록체인 마켓 앱 만들기 with 그라운드X (3) | 2021.06.30 |
---|---|
[2주차] NFT 블록체인 마켓 앱 만들기 with 그라운드X (1) | 2021.05.26 |
[1주차] NFT 블록체인 마켓 앱 만들기 with 그라운드X (2) | 2021.05.14 |