WalletKit 세션 연결하기 (React Native, Reown) 포스팅에서 이어지는 내용입니다.
hexToUtf8
메소드를 사용하여 메시지 디코딩을 위해 web3
라이브러리를 설치합니다.npm i web3
yarn add web3
session_request
이벤트는 WalletKit 세션 요청을 처리하는 데 사용됩니다. 이벤트 핸들러는 세션 요청을 수신하고 트랜잭션 서명 또는 메시지 서명을 위해 지갑을 사용합니다. 이벤트 핸들러는 on
메소드를 사용하여 등록하고, off
메소드를 사용하여 해제합니다.
일단 세션 요청을 수신하면, 요청 메시지를 디코딩하고 서명합니다. 서명된 메시지를 사용하여 세션 요청에 응답합니다.
이제부터는 서명이 필요하기 때문에 온전한 지갑이 필요합니다. 저는 useGetwallet
커스텀 훅을 사용하여 지갑을 가져왔습니다. 지갑을 가져오는 방법은 React Native 환경에서 EVM 블록체인 지갑 생성하기을 참고하세요.// 이 이벤트는 세션 연결과 동시에 선언될 예정이니 참고만 해주세요.
walletKit.on('session_request', async event => {
const { topic, params, id } = event
const { request } = params
const requestParamsMessage = request.params[0]
const message = web3.utils.hexToUtf8(requestParamsMessage) // 메시지 디코딩
const signedMessage = await wallet.signMessage(message) // 메시지를 서명합니다.
const response = { id, result: signedMessage, jsonrpc: '2.0' }
await walletKit.respondSessionRequest({ topic, response }) // 세션 요청에 응답합니다.
})
eth_sendTransaction
메소드를 사용합니다. 이때는 트랜잭션 객체를 서명하고, 서명된 트랜잭션을 사용하여 세션 요청에 응답합니다.// 이 이벤트는 세션 연결과 동시에 선언될 예정이니 참고만 해주세요.
if (params.request.method === 'eth_sendTransaction') {
const { from, to, value, gasLimit, gasPrice } = requestParamsMessage as TransactionLike
const signedMessage = await wallet.signTransaction(requestParamsMessage) // 트랜잭션 서명
const response = { id, result: signedMessage, jsonrpc: '2.0' }
await walletKit.respondSessionRequest({ topic, response }) // 세션 요청에 응답합니다.
}
// onSessionRequest 함수
async function onSessionRequest(event: WalletKitTypes.SessionRequest) {
if (!walletKit || !wallet) return;
const { topic, params, id } = event;
const { request } = params;
const requestParamsMessage = request.params[0];
if (params.request.method === 'eth_sendTransaction') {
const signedMessage = await wallet.signTransaction(requestParamsMessage);
const response = { id, result: signedMessage, jsonrpc: '2.0' };
await walletKit.respondSessionRequest({ topic, response });
} else if (params.request.method === 'personal_sign') {
const message = web3.utils.hexToUtf8(requestParamsMessage);
const signedMessage = await wallet.signMessage(message);
const response = { id, result: signedMessage, jsonrpc: '2.0' };
await walletKit.respondSessionRequest({ topic, response });
}
}
onSessionProposal
함수에 onSessionRequest
함수를 등록합니다. 이 함수는 세션 제안을 받아서 승인하는 함수입니다.const methodList = ['eth_accounts', 'eth_sendTransaction', 'personal_sign'];
const eventList = ['chainChanged', 'accountsChanged'];
// onSessionProposal 함수
async function onSessionProposal({ id, params }: WalletKitTypes.SessionProposal) {
try {
const approvedNamespaces = buildApprovedNamespaces({
proposal: params,
supportedNamespaces: {
eip155: {
chains: ['eip155:1', 'eip155:137'],
methods: methodList,
events: eventList,
accounts: [
'eip155:1:' + wallet?.address,
'eip155:137:' + wallet?.address,
],
},
},
});
const session = await walletKit?.approveSession({
id,
namespaces: approvedNamespaces,
});
Alert.alert('Session Approved');
if (session) {
walletKit?.on('session_request', onSessionRequest);
}
} catch (error) {
console.error(error);
await walletKit?.respondSessionRequest({
topic: params.pairingTopic,
response: {
id,
error: getSdkError('SESSION_SETTLEMENT_FAILED'),
jsonrpc: '2.0',
},
});
}
}
interface AlertParams {
title: string;
message: string;
signButton: AlertButton;
cancelButton?: AlertButton;
topic: string;
id: number;
}
const SignAlert = ({
title,
message,
signButton,
cancelButton,
topic,
id,
}: AlertParams) => {
Alert.alert(title, message, [
cancelButton ?? {
text: 'Cancel',
onPress: async () => {
await walletKit?.respondSessionRequest({
topic,
response: {
id,
error: getSdkError('USER_REJECTED'),
jsonrpc: '2.0',
},
});
},
},
signButton,
]);
};
/**
* onSessionRequest 함수 - 세션 요청을 받아서 서명하는 함수
* @param event 세션 요청 이벤트
* @returns
*/
const onSessionRequest = async (event: WalletKitTypes.SessionRequest) => {
if (!wallet || !walletKit) {
return;
}
const { topic, params, id } = event;
const { request } = params;
console.log('session_request', request);
const requestParamsMessage = request.params[0];
if (params.request.method === 'eth_sendTransaction') {
const { from, to, value, gasLimit, gasPrice } =
requestParamsMessage as TransactionLike;
SignAlert({
title: 'Message',
message: `from: ${from}\n
to: ${to}\n
value: ${value}\n
gasLimit: ${gasLimit}\n
gasPrice: ${gasPrice}`,
signButton: {
text: 'Sign',
onPress: async () => {
const signedMessage =
await wallet.signTransaction(requestParamsMessage);
const response = {
id,
result: signedMessage,
jsonrpc: '2.0',
};
await walletKit.respondSessionRequest({ topic, response });
},
},
topic,
id,
});
} else if (params.request.method === 'personal_sign') {
const message = web3.utils.hexToUtf8(requestParamsMessage);
SignAlert({
title: 'Message',
message,
signButton: {
text: 'Sign',
onPress: async () => {
const signedMessage = await wallet.signMessage(message);
const response = {
id,
result: signedMessage,
jsonrpc: '2.0',
};
await walletKit.respondSessionRequest({ topic, response });
},
},
topic,
id,
});
}
};
import web3 from 'web3';
import '@walletconnect/react-native-compat';
import { WalletKit, WalletKitTypes } from '@reown/walletkit';
import { WalletKit as WalletKitType } from '@reown/walletkit/dist/types/client';
import { Core } from '@walletconnect/core';
import { buildApprovedNamespaces, getSdkError } from '@walletconnect/utils';
import { TransactionLike } from 'ethers';
import { useEffect, useState } from 'react';
import {
ActivityIndicator,
Alert,
AlertButton,
Button,
Text,
TextInput,
View,
} from 'react-native';
import web3 from 'web3';
import useGetwallet from '@/hooks/useGetwallet';
const methodList = ['eth_accounts', 'eth_signTransaction', 'personal_sign'];
const eventList = ['chainChanged', 'accountsChanged'];
const core = new Core({
projectId: 'your_project_id',
});
interface AlertParams {
title: string;
message: string;
signButton: AlertButton;
cancelButton?: AlertButton;
topic: string;
id: number;
}
const App = () => {
const wallet = useGetwallet();
const [walletKit, setWalletKit] = useState<WalletKitType>();
const [uri, setUri] = useState('');
const [sessionKeys, setSessionKeys] = useState<string[]>([]);
/**
* SignAlert 함수 - Alert를 띄워서 메시지를 보여주고, 서명을 요청하는 함수
* @param title Alert 제목
* @param message Alert 메시지
* @param signButton 서명 버튼
* @param cancelButton 취소 버튼
* @param topic 세션 토픽
* @param id 세션 ID
*/
const SignAlert = ({
title,
message,
signButton,
cancelButton,
topic,
id,
}: AlertParams) => {
Alert.alert(title, message, [
cancelButton ?? {
text: 'Cancel',
onPress: async () => {
await walletKit?.respondSessionRequest({
topic,
response: {
id,
error: getSdkError('USER_REJECTED'),
jsonrpc: '2.0',
},
});
},
},
signButton,
]);
};
/**
* onSessionProposal 함수 - 세션 제안을 받아서 승인하는 함수
* @param id 세션 ID
* @param params 세션 제안 파라미터
* @returns
* @throws
*/
async function onSessionProposal({
id,
params,
}: WalletKitTypes.SessionProposal) {
try {
const approvedNamespaces = buildApprovedNamespaces({
proposal: params,
supportedNamespaces: {
eip155: {
chains: ['eip155:1', 'eip155:137'],
methods: methodList,
events: eventList,
accounts: [
'eip155:1:' + wallet?.address,
'eip155:137:' + wallet?.address,
],
},
},
});
const session = await walletKit?.approveSession({
id,
namespaces: approvedNamespaces,
});
Alert.alert('Session Approved');
const sessions = await walletKit?.getActiveSessions();
setSessionKeys(sessions ? Object.keys(sessions) : []);
if (session) {
walletKit?.on('session_request', async (event) => {
const { topic, params, id } = event;
const { request } = params;
console.log('session_request', request);
const requestParamsMessage = request.params[0];
if (params.request.method === 'eth_sendTransaction') {
const { from, to, value, gasLimit, gasPrice } =
requestParamsMessage as TransactionLike;
SignAlert({
title: 'Message',
message: `from: ${from}\n
to: ${to}\n
value: ${value}\n
gasLimit: ${gasLimit}\n
gasPrice: ${gasPrice}`,
signButton: {
text: 'Sign',
onPress: async () => {
const signedMessage =
await wallet?.signTransaction(requestParamsMessage);
const response = {
id,
result: signedMessage,
jsonrpc: '2.0',
};
await walletKit.respondSessionRequest({ topic, response });
},
},
topic,
id,
});
} else if (params.request.method === 'personal_sign') {
const message = web3.utils.hexToUtf8(requestParamsMessage);
SignAlert({
title: 'Message',
message,
signButton: {
text: 'Sign',
onPress: async () => {
const signedMessage = await wallet?.signMessage(message);
const response = {
id,
result: signedMessage,
jsonrpc: '2.0',
};
await walletKit.respondSessionRequest({ topic, response });
},
},
topic,
id,
});
}
});
}
} catch (error) {
console.error(error);
await walletKit?.respondSessionRequest({
topic: params.pairingTopic,
response: {
id,
error: getSdkError('SESSION_SETTLEMENT_FAILED'),
jsonrpc: '2.0',
},
});
}
}
/**
* useEffect - WalletKit 초기화
* @returns
* @throws
*/
useEffect(() => {
WalletKit.init({
core,
metadata: {
// 지갑 메타데이터 (본인 상황에 맞게 수정)
name: 'Demo React Native Wallet Promlee',
description: 'Demo RN Wallet to interface with Dapps',
url: 'www.promleeblog.com',
icons: ['https://www.promleeblog.com/icons/android-chrome-512x512.png'],
redirect: {
native: 'promleewallet://',
},
},
}).then((kit) => {
setWalletKit(kit);
});
}, []);
/**
* useEffect - 세션 제안 이벤트 핸들러 등록
* @returns
* @throws
*/
useEffect(() => {
try {
walletKit?.on('session_proposal', onSessionProposal);
} catch (error) {
console.error(error);
}
return () => {
walletKit?.off('session_proposal', onSessionProposal);
};
}, [walletKit]);
return wallet ? (
<View style={{ padding: 20, gap: 10 }}>
<Text style={{ fontSize: 20 }}>Address</Text>
<Text>{wallet?.address}</Text>
{sessionKeys.length !== 0 && (
<Text style={{ fontSize: 20 }}>Sessions</Text>
)}
{sessionKeys.map((sessionKey) => (
<Text key={sessionKey}>{sessionKey}</Text>
))}
<TextInput
placeholder="Enter WalletConnect URI"
value={uri}
onChangeText={setUri}
style={{
width: '100%',
marginVertical: 10,
padding: 10,
fontSize: 16,
borderRadius: 8,
borderWidth: 1,
borderColor: '#ccc',
marginBottom: 10,
}}
/>
<Button
title="Connect"
onPress={() => walletKit?.pair({ uri })}
disabled={!uri}
/>
<Button
title="Disconnect"
onPress={async () => {
try {
const sessions = await walletKit?.getActiveSessions();
const sessionKeys = sessions ? Object.keys(sessions) : [];
if (sessionKeys.length === 0) {
Alert.alert('No active sessions');
return;
} else {
console.log(sessionKeys);
for (const sessionKey of sessionKeys) {
await walletKit?.disconnectSession({
topic: sessionKey,
reason: getSdkError('USER_REJECTED'),
});
}
walletKit?.off('session_request', () => {});
Alert.alert('Disconnected');
setSessionKeys([]);
}
} catch (error) {
console.error(error);
}
}}
disabled={sessionKeys.length === 0}
/>
</View>
) : (
<ActivityIndicator style={{ flex: 1 }} size={'large'} />
);
};
export default App;