Wallet Provider로 React와 XPLA Vault 지갑 연결하기
XPLA Wallet Provider란 React에서 XPLA Vault 지갑을 이용할 수 있도록 도와주는 도구입니다. 이를 통해 유저들이 React 웹에서 자신의 Vault 지갑을 더 쉽게 이용할 수 있습니다. 직접 React 웹에서 Wallet Provider를 사용해봅시다.
XPLA Wallet Provider에 관한 정보는 XPLA Docs에서도 확인할 수 있습니다.
Edge 브라우저에서는 Wallet Provider를 이용하여 Vault Chrome Extension과 연결할 수 없습니다. 대신 모바일 애플리케이션 Vault를 설치하여, Wallet Connect 방식으로 연결할 수 있습니다.
Preview
React에서 Wallet Provider를 이용하여 Vault 지갑을 연결하는 코드입니다. 아래 Connect Button을 누르고, 테스트넷 XPLA 전송까지 진행해보세요.
Wallet Provider로 유저의 Vault 지갑 정보를 받아, 테스트넷 XPLA 전송까지 진행할 수 있었습니다. Preview Code를 이해하셨다면 다음 단계로 넘어가셔도 좋습니다.
React Code Along
아래 과정을 따라하면서 Preview Code를 이해해봅시다.
먼저 React를 CRA로 설치합니다.
npx create-react-app xpla-app
cd xpla-appWallet Provider를 이용하기 위해 필요한 모듈을 설치합니다. Wallet Provider는 Polyfill을 이용하기 때문에,
react-scripts@4.0.3
를 함께 설치해줍니다.npm install react-scripts@4.0.3
npm install @xpla/wallet-provider @xpla/xpla.jsCRA로 설치한 React 프로젝트에서, src/index.js 파일을 예제와 같이 수정합니다.
src/index.jsimport React from "react";
import { createRoot } from "react-dom/client";
import "./index.css";
import {
getChainOptions,
WalletProvider,
} from "@xpla/wallet-provider";
import App from "./App";
const root = createRoot(document.getElementById('root'));
getChainOptions().then((chainOptions) => {
root.render(
<WalletProvider {...chainOptions}>
<App />
</WalletProvider>
);
});src/App.js 파일을 수정합니다.
src/App.jsimport {
useConnectedWallet,
UserDenied,
useWallet,
WalletStatus,
} from "@xpla/wallet-provider";
import React, { useState, useEffect } from "react";
import { LCDClient, MsgSend } from "@xpla/xpla.js";
export default function App() {
const lcd = new LCDClient({
chainID: 'cube_47-5',
URL: 'https://cube-lcd.xpla.dev'
});
const {
status,
network,
wallets,
connect,
disconnect
} = useWallet();
const connectedWallet = useConnectedWallet();
const [amount, setAmount] = useState("");
const [recipient, setRecipient] = useState("");
const [loading, setLoading] = useState(1);
const [txResult, setTxResult] = useState(null);
const [txError, setTxError] = useState(null);
const handleSend = async () => {
try {
const transactionMsg = {
msgs: [
new MsgSend(connectedWallet.walletAddress, recipient, {
axpla: amount
}),
],
};
const tx = await connectedWallet.post(transactionMsg);
if (tx.success) {
setLoading(true);
setTxResult(tx);
}
else setTxError("Please Retry Send!");
} catch (error) {
if (error instanceof UserDenied) {
setTxError("User Denied");
} else {
setTxError("Unknown Error: " + error instanceof Error ? error.message : String(error));
}
}
};
useEffect(() => {
if (txResult && loading !== 0) {
const timer = setTimeout(async () => {
try {
const txInfo = await lcd.tx.txInfo(txResult.result.txhash);
if (txInfo.txhash) setLoading(0);
} catch (err) {
setLoading(loading + 1);
}
}, 1000);
return () => clearTimeout(timer);
}
}, [loading]);
return <div className="example-container">
{status === WalletStatus.WALLET_NOT_CONNECTED ? (
<>
<button
className="button-css width-full"
type="button"
onClick={() => connect()}
>
Connect Wallet
</button>
<p className="warning">If there is no change even after clicking the button, please press the refresh button in the bottom right corner of the screen.</p>
</>
) : (
<>
<div className="info-container">
<div className="info-title">Connected Address</div>
<div className="info-content">
{wallets.length === 0 ? "Loading..." : wallets[0].xplaAddress}
</div>
</div>
<div className="info-container">
<label className="info-title" for="recipient">
Recipient
</label>
<input
className="info-content"
id="recipient"
autoComplete="off"
type="text"
placeholder="xpla1cwduqw0z8y66mnfpev2mvrzzzu98tuexepmwrk"
onChange={(e) => setRecipient(e.target.value)}
/>
</div>
<div className="info-container">
<label className="info-title" for="amount">
Amount (aXPLA)
</label>
<input
className="info-content"
autoComplete="off"
id="Amount"
placeholder="1"
onChange={(e) => setAmount(e.target.value)}
/>
</div>
<div className="bottom-button-container">
<button className="button-css" type="button" onClick={handleSend}>
Send Tx
</button>
<button className="button-css" type="button" onClick={disconnect}>
Disconnect
</button>
</div>
{txResult && (
<div style={{ marginTop: 20 }}>
<div className="info-title">Send Transaction Hash</div>
<div className="info-content">
{
loading !== 0 ?
<span>Loading...</span>
:
<a
className="link"
href={"https://explorer.xpla.io/" + network.name + "/tx/" + txResult.result.txhash}
target="_blank"
rel="noreferrer"
>
{txResult.result.txhash}
</a>
}
</div>
</div>
)}
{txError && (
<div style={{ marginTop: 20 }}>
<div className="info-title">Tx Error</div>
<div className="info-content">
<span>
{txError}
</span>
</div>
</div>
)}
</>
)}
</div>
}src/index.css 파일을 수정합니다.
src/index.css.example-container {
box-shadow: 0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1);
border-radius: 4px;
padding: 24px 32px 24px 32px;
margin-bottom: 32px;
}
.width-full {
width: 100%;
}
.button-css {
background-color: #00B1FF;
color: white;
font-weight: 600;
padding: 8px 16px 8px 16px;
font-size: 16px;
border: 0px;
border-radius: 8px;
}
.button-css:hover {
cursor: pointer;
opacity: 0.9;
}
.info-container {
margin-bottom: 32px;
}
.info-title {
display: block;
color: #16161A;
font-size: 16px;
font-weight: 600;
margin-bottom: 8px;
}
.info-content {
box-shadow: 0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1);
border-radius: 4px;
width: calc(100% - 24px);
padding: 8px 12px 8px 12px;
font-size: 14px;
}
.bottom-button-container {
display: flex;
align-items: center;
justify-content: space-between;
}
.link {
color: #00B1FF;
word-break: break-all;
}
.link:hover {
color: #00B1FF;
}
.warning {
color: #FF335F;
}로컬에서 React를 실행합니다.
npm start
이후 브라우저에서 localhost:3000
로 접속하여 Preview 화면처럼 Vault 지갑을 연결해볼 수 있습니다. 직접 테스트넷 XPLA를 전송해보세요.
App.js 코드 분석
Wallet Provider를 직접적으로 이용하는 App.js 파일의 코드를 분석해봅시다.
Line 16~24
@xpla/wallet-provider
모듈의 useWallet과 useConnectedWallet 함수를 사용했습니다. status 변수는 현재 유저가 Vault Chrome Extension과 연결했는지 상태를 나타내는 변수값입니다. 유저의 지갑이 연결되지 않았다면, connectedWallet 변수에는 undefined
값이 담겨 있습니다.
const {
status,
network,
wallets,
connect,
disconnect
} = useWallet();
const connectedWallet = useConnectedWallet();
Line 72~83
유저가 아직 Vault 지갑을 연결하지 않았을 경우(status === WalletStatus.WALLET_NOT_CONNECTED), Connect Wallet
버튼을 보여주도록 했습니다. 유저가 버튼을 클릭하면 connect 함수를 실행합니다. connect 함수는 @xpla/wallet-provider
모듈의 내장 함수로, Vault에 연결할 수 있는 Modal 창을 띄워줍니다.
{status === WalletStatus.WALLET_NOT_CONNECTED ? (
<>
<button
className="button-css width-full"
type="button"
onClick={() => connect()}
>
Connect Wallet
</button>
<p className="warning">If there is no change even after clicking the button, please press the refresh button in the bottom right corner of the screen.</p>
</>
) : (
유저가 지갑을 연결하면, 현재 유저가 Vault에서 사용하고 있는 주소 정보가 connectedWallet 변수에 담깁니다. 유저는 Vault에서 Switch wallet
버튼으로 사용하는 지갑 주소를 바꿀 수 있습니다.
Line 91~123
예제 React 화면에서 Recipient 값과 Amount 값을 input으로 입력 받았습니다. 이후 Send Tx
버튼을 클릭할 경우 handleSend 함수를 실행합니다.
<div className="info-container">
<label className="info-title" for="recipient">
Recipient
</label>
<input
className="info-content"
id="recipient"
autoComplete="off"
type="text"
placeholder="xpla1cwduqw0z8y66mnfpev2mvrzzzu98tuexepmwrk"
onChange={(e) => setRecipient(e.target.value)}
/>
</div>
<div className="info-container">
<label className="info-title" for="amount">
Amount (aXPLA)
</label>
<input
className="info-content"
autoComplete="off"
id="Amount"
placeholder="1"
onChange={(e) => setAmount(e.target.value)}
/>
</div>
<div className="bottom-button-container">
<button className="button-css" type="button" onClick={handleSend}>
Send Tx
</button>
<button className="button-css" type="button" onClick={disconnect}>
Disconnect
</button>
</div>
Line 33~55
handleSend 함수는 유저가 연결한 지갑으로부터, Recipient 지갑으로 입력한 금액(aXPLA)만큼 테스트넷 XPLA를 전송합니다. line 35에서 트랜잭션 메시지를 만들고, line 42에서 블록체인 네트워크에 트랜잭션을 전파합니다.
const handleSend = async () => {
try {
const transactionMsg = {
msgs: [
new MsgSend(connectedWallet.walletAddress, recipient, {
axpla: amount
}),
],
};
const tx = await connectedWallet.post(transactionMsg);
if (tx.success) {
setLoading(true);
setTxResult(tx);
}
else setTxError("Please Retry Send!");
} catch (error) {
if (error instanceof UserDenied) {
setTxError("User Denied");
} else {
setTxError("Unknown Error: " + error instanceof Error ? error.message : String(error));
}
}
};
Line 57~69
XPLA 블록체인에서 블록 생성은 약 6초마다 이루어집니다. 따라서 트랜잭션을 전파한 후, 트랜잭션이 블록체인에 기록되기까지는 시간이 조금 걸립니다. 블록체인에 트랜잭션이 잘 기록되었는지 확인하기 위해, 예제에서는 LCDClient
를 이용하여 해시값에 해당하는 트랜잭션이 있는지 조사하였습니다.
handleSend 함수에서 트랜잭션이 전파되었다면, React State 중 loading 변수값이 true로 바뀌게 됩니다(line 44). 이때 useEffect 함수의 dependency array 항목에 loading 변수가 포함되어 있기 때문에 useEffect의 callback 함수가 실행됩니다. callback 함수는 LCDClient에게 트랜잭션 데이터를 받을 때까지, setTimeout 함수로 1초에 한번씩 계속 쿼리를 보냅니다.
useEffect(() => {
if (txResult && loading !== 0) {
const timer = setTimeout(async () => {
try {
const txInfo = await lcd.tx.txInfo(txResult.result.txhash);
if (txInfo.txhash) setLoading(0);
} catch (err) {
setLoading(loading + 1);
}
}, 1000);
return () => clearTimeout(timer);
}
}, [loading]);
LCDClient를 이용하여 트랜잭션 데이터를 가져오는 방법은 이전 단계에서 학습한 내용을 응용한 것입니다.
Line 124~143
이후 트랜잭션 데이터를 받아 loading 변수값이 0이 된다면, React 화면에서 유저에게 트랜잭션 해시값을 보여줍니다. 예제에서는 해시값을 클릭하면 XPLA Explorer로 이동하도록 연결해주었습니다.
{txResult && (
<div style={{ marginTop: 20 }}>
<div className="info-title">Send Transaction Hash</div>
<div className="info-content">
{
loading !== 0 ?
<span>Loading...</span>
:
<a
className="link"
href={"https://explorer.xpla.io/" + network.name + "/tx/" + txResult.result.txhash}
target="_blank"
rel="noreferrer"
>
{txResult.result.txhash}
</a>
}
</div>
</div>
)}
지금까지 React에서 @xpla/wallet-provider
모듈을 이용해봤습니다. 유저의 Vault 지갑 정보를 React에 연결하고, 테스트넷 XPLA까지 전송할 수 있었습니다. 예제 코드를 여러분의 게임에 맞게 수정하여, 유저들이 쉽게 블록체인을 이용할 수 있도록 만들어봅시다.