PromleeBlog
sitemap
aboutMe

posting thumbnail
WalletKit 세션 요청에 응답하기 (React Native, Reown)
Responding to WalletKit Session Requests (React Native, Reown)

📅

🚀

들어가기 전에 🔗

WalletKit 세션 연결하기 (React Native, Reown) 포스팅에서 이어지는 내용입니다.
이전 포스팅에서는 Reown을 사용하여 WalletKit 세션을 연결하는 방법을 알아보았습니다. 이번에는 WalletKit 세션 요청에 응답하는 방법을 알아보겠습니다. WalletKit 세션 요청 이벤트는 트랜잭션 서명과 같은 특정 작업을 수행하기 위해 지갑이 필요할 때 디앱에서 트리거됩니다.
플로우는 다음과 같습니다.
  1. 세션이 연결된 디앱에서 WalletKit 세션 요청을 받습니다.
  2. Reown을 사용하여 WalletKit 세션 요청을 서명합니다.
  3. 서명된 요청을 WalletKit 세션에 응답합니다.
  4. 세션 요청에 대한 응답을 디앱으로 전달합니다.
  5. 디앱에서 세션 요청에 대한 응답을 처리합니다.

🚀

필요 라이브러리 설치 🔗

hexToUtf8메소드를 사용하여 메시지 디코딩을 위해 web3 라이브러리를 설치합니다.
npm i web3
yarn add web3

🚀

session_request 이벤트 핸들링 🔗

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 함수 생성 및 등록 🔗

onSessionRequest 함수 생성 🔗

세션 요청을 처리하는 함수를 생성합니다. 이 함수는 세션 요청이나 트랜젝션을 구분하여 자동을 서명하고, 응답합니다.
// 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 });
	}
}

onSessionRequest 등록 🔗

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',
			},
		});
	}
}

결과 확인 🔗

이제 세션 연결 후 임의로 세션 요청을 보내면, 지갑이 서명을 요청하고, 서명된 메시지를 디앱으로 전달합니다.
image

🚀

세션 요청에 대한 거절/승인 기능 추가 🔗

요청 거절/승인 Alert 컴포넌트 생성 🔗

세션 요청에 대한 거절/승인을 Alert로 보여주는 컴포넌트를 생성합니다. 필요한 인수를 받아서 Alert를 띄우고, 서명 버튼을 누르면 서명을 요청하고, 취소 버튼을 누르면 거절합니다. 서명 버튼의 동작은 요청 종류에 따라 다르기 때문에 외부에서 받아오는 인수로 받아옵니다.
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,
	]);
};
이제 세션 요청에 대한 거절/승인을 Alert로 보여주는 컴포넌트를 생성했습니다. 이제 세션 요청에 대한 응답을 처리하는 함수를 생성합니다.

세션 연결 및 요청 거절/승인 🔗

이제 세션 연결 후 임의로 세션 요청을 보내면, 지갑이 서명을 요청하고, 서명된 메시지를 디앱으로 전달하도록 하기 위해 onSessionRequest 함수를 수정하겠습니다.
트랜젝션 발생 시에는 트랜젝션 서명을 요청하고, 메시지 발생 시에는 메시지 서명을 요청합니다.
/**
	* 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,
		});
	}
};

결과 확인 🔗

이제 세션 연결 후 임의로 세션 요청을 보내면, 서명 요청 Alert가 뜨고, 서명을 요청하면 세션 요청에 대한 응답이 디앱으로 전달됩니다. 반면, 서명을 거절하면 거절 메시지가 디앱으로 전달됩니다.
image

🚀

문제 발생 및 해결 🔗

hexToUtf8가 적용되지 않는 오류 🔗

web3 라이브러리를 설치해서 import 후 Web3.utils.hexToUtf8() 메소드를 사용했지만, hexToUtf8 메소드가 적용되지 않는 오류가 발생했습니다.
문제는 import한 클래스 이름이 Web3가 아닌 web3라는 것이었습니다. 따라서 Web3.utils.hexToUtf8() 메소드를 web3.utils.hexToUtf8()로 수정하니 정상적으로 작동했습니다.
import web3 from 'web3';

🚀

결론 🔗

전체 코드 🔗

전체 코드 보기
/app/index.tsx
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;

참고 🔗