前言
在加密货币世界中,去中心化应用(decentralized application, DApp)的发展越来越迅速。
DApp 可以让用户更加自由地管理他们的资产,同时也让开发者更加容易地构建去中心化应用。然而,DApp 在与区块链交互时,往往需要使用钱包进行支付、签名等操作,这就需要 DApp 和钱包之间进行通信。
WalletConnect 是一种标准化的协议,可以帮助 DApp 和钱包之间进行安全的通信。
什么是WalletConnect?
WalletConnect是一种加密货币钱包和DApp之间进行安全通信的协议。其主要目的是让用户在使用DApp时,能够使用自己喜欢的加密货币钱包进行交易,而无需将私钥上传到DApp中。
WalletConnect分为v1和v2版本,版本主要差异在于通信协议的改变。v1版本使用基于Websocket的通信协议,而v2版本则使用了更加高效的基于扩展传输层安全协议(DTLS)的通信协议。
其中,WalletConnect v1是WalletConnect协议的第一个版本,本文将深入介绍其原理和实现细节。
原理介绍
钱包和 DApp 之间的通信原理
钱包和 DApp 之间的通信需要通过一种安全的协议进行。在过去,这种通信往往需要用户手动输入私钥进行签名,存在较大的安全风险。WalletConnect 利用了类似于 OAuth2 的授权流程,将钱包和 DApp 的通信过程进行了抽象,使得用户在授权后无需再手动输入私钥。
WebSocket协议
WebSocket 是一种双向通信协议,可以使客户端和服务器之间实现实时通信。在 WebSocket 中,客户端和服务器可以通过建立长连接进行数据交互。
WalletConnect 利用 WebSocket协议来实现钱包和 DApp 之间的通信。
在通信过程中,首先需要建立 WebSocket 连接,然后通过自定义通信来发送授权请求。一旦钱包用户授权,钱包就可以在双方之间进行加密通信,完成支付、签名等操作。
实现细节
WalletConnect v1的工作原理可以分为两个主要过程:建立连接和通信过程。
- WalletConnect 的核心代码结构
WalletConnect 的核心代码结构包括客户端和服务器两部分。客户端代码可以在 DApp 中实现,服务器代码则需要部署在独立的服务器上。
- 客户端和服务器之间的消息格式
WalletConnect协议中消息的格式采用了JSON格式,包括了请求和响应两种类型。每个消息都包含了一个ID字段,用于标识消息的唯一性。请求消息包含一个Method字段,用于指示请求的方法,而响应消息则包含了一个Result字段,用于指示响应的结果。
- WalletConnect 的通信流程
WalletConnect 的通信流程可以概括为以下几个步骤:
- DApp 向 WalletConnect 服务器发起连接请求,服务器返回连接信息;
- DApp 向钱包发送连接请求,钱包弹出授权窗口,用户选择是否授权;
- 如果用户授权,钱包会向 DApp 返回连接信息,双方开始加密通信;
- 在通信过程中,DApp 可以向钱包发送请求,钱包可以向 DApp 返回响应。
- 客户端调用
- Dapp端
import WalletConnect from "@walletconnect/client";
public connect = async () => {
const bridge = "https://bridge.walletconnect.org";
const connector = new WalletConnect({ bridge, qrcodeModal: QRCodeModal });
if (!connector.connected) {
await connector.createSession();
}
await this.subscribeToEvents();
}
- Wallet端
import LegacysignClientV1 from '@walletconnect/client';
signClientV1 = new LegacysignClientV1({
uri,
clientMeta: METADATA,
});
- WebSocket 的实现方式
- WalletConnect 类
// packages\clients\client\src\index.ts
class WalletConnect extends Connector {
constructor(connectorOpts: IWalletConnectOptions, pushServerOpts?: IPushServerOptions) {
super({
cryptoLib,
connectorOpts,
pushServerOpts,
});
}
}
- Connector 类
// packages\clients\core\src\index.ts
constructor(opts: IConnectorOpts) {
if (!opts.connectorOpts.bridge && !opts.connectorOpts.uri && !opts.connectorOpts.session) {
throw new Error(ERROR_MISSING_REQUIRED);
}
// Dapp端入参处理
if (opts.connectorOpts.bridge) {
this.bridge = getBridgeUrl(opts.connectorOpts.bridge);
}
// Wallet端入参处理
if (opts.connectorOpts.uri) {
this.uri = opts.connectorOpts.uri;
}
// WebSocket建立
this._transport =
opts.transport ||
new SocketTransport({
protocol: this.protocol,
version: this.version,
url: this.bridge,
subscriptions: [this.clientId],
});
// 钩子监听
this._subscribeToInternalEvents();
// WebSocket事件监听
this._initTransport();
// Wallet端建立通信
if (opts.connectorOpts.uri) {
this._subscribeToSessionRequest();
}
}
- 访问器属性
get uri() {
const _uri = this._formatUri();
return _uri;
}
set uri(value) {
if (!value) {
return;
}
const { handshakeTopic, bridge, key } = this._parseUri(value);
this.handshakeTopic = handshakeTopic;
this.bridge = bridge;
this.key = key;
}
- Websocket建立
// packages\helpers\socket-transport\src\index.ts
constructor(private opts: ISocketTransportOptions) {
this._netMonitor = opts.netMonitor || new NetworkMonitor();
this._netMonitor.on("online", () => this._socketCreate());
}
private _socketCreate() {
const url = getWebSocketUrl(this._url, this._protocol, this._version);
this._nextSocket = new WS(url);
if (!this._nextSocket) {
throw new Error("Failed to create socket");
}
this._nextSocket.onmessage = (event: MessageEvent) => this._socketReceive(event);
this._nextSocket.onopen = () => this._socketOpen();
this._nextSocket.onerror = (event: Event) => this._socketError(event);
}
- 可访问的关键方法
// Dapp调用
public async connect(opts?: ICreateSessionOptions): Promise {
// Dapp调用
public async createSession(opts?: ICreateSessionOptions): Promise {
}
// Wallet调用
public approveSession(sessionStatus: ISessionStatus) {
}
// Wallet调用
public rejectSession(sessionError?: ISessionError) {
}
// Wallet调用
public updateSession(sessionStatus: ISessionstatus) {
}
// Wallet调用
public async killSession(sessionError?: ISessionError){
}
// Dapp调用
public async sendTransaction(tx: ITxData) {
}
// Dapp调用
public async signTransaction(tx: ITxData) {
}
// Dapp调用
public async signMessage(params: any[]) {
}
// Dapp调用
public async signPersonalMessage(params: any[]) {
}
// Dapp调用
public async signTypedData(params;any[]){
}
Wallet扫码授权Dapp
v1
初始化签名对象实例&监听通信事件
import LegacysignClientV1 from '@walletconnect/client';
async function createV1SignClient({ uri } = { uri: '' }) {
if (uri) {
clearAllSessionsForV1();
signClientV1 = new LegacysignClientV1({
uri,
clientMeta: METADATA,
});
if (!signClientV1.connected) {
await signClientV1.createSession();
}
} else {
return;
}
/**
* 连接申请 —— wallet扫Dapp二维码后,建立通信时触发
*/
signClientV1.on('session_request', (error, payload) => {
if (error) {
throw new Error(`legacysignClientV1 > session_request failed: ${error}`);
}
const { params = [] } = payload;
const [peer = {}] = params;
});
signClientV1.on('connect', () => {
// 成功与Dapp建立通信的事件通知
console.log('legacysignClientV1 > connect');
});
signClientV1.on('error', (error) => {
throw new Error(`legacysignClientV1 > on error: ${error}`);
});
signClientV1.on('call_request', (error, payload) => {
// 签名事件调用通知
if (error) {
throw new Error(`legacysignClientV1 > call_request failed: ${error}`);
}
});
signClientV1.on('disconnect', async () => {
// 断开的事件监听
clearAllSessionsForV1();
});
}
通信授权
同意授权
const approveSessionV1 = async () => { try { await signClientV1?.approveSession({ accounts: [consumerAddress], chainId: consumer.chainId, // required }); } catch (e) { disconnectSessionForV1(); } };
拒绝授权
const rejectSessionV1 = async () => { try { await signClientV1?.rejectSession({ message: 'USER_REJECTED_METHODS', }); } catch (e) { disconnectSessionForV1(); } };
签名请求处理
签名授权
const onCallRequestV1 = async (payload: { id: number; method: string; params: any[]; }) => { const { id, params, method } = payload; let response = { id, } as any; if (method === EIP155_SIGNING_METHODS.WALLET_SWITCHETHEREUMCHAIN) { // 网络切换 signClientV1?.updateSession({ accounts: [consumerAddress], chainId: params[0].chainId, // required }); return; } switch (method) { case EIP155_SIGNING_METHODS.ETH_SIGN: case EIP155_SIGNING_METHODS.PERSONAL_SIGN: response.result = await walletConnectHelper.signEip192Message({ address, params, }); break; case EIP155_SIGNING_METHODS.ETH_SIGN_TYPED_DATA: case EIP155_SIGNING_METHODS.ETH_SIGN_TYPED_DATA_V3: case EIP155_SIGNING_METHODS.ETH_SIGN_TYPED_DATA_V4: response.result = await walletConnectHelper.signEip712Message({ address, params, }); break; case EIP155_SIGNING_METHODS.ETH_SEND_TRANSACTION: try { response.result = await walletConnectHelper.signAndSendEip155Tx({ params, address, }); } catch (e: any) { const errorText = e.message || 'Error occurs'; Toast.error({ msg: errorText, }); response.error = { code: 400, message: errorText, }; signClientV1?.rejectRequest(response); return; } break; case EIP155_SIGNING_METHODS.ETH_SIGN_TRANSACTION: response.result = await walletConnectHelper.signEip155Tx({ params, address, }); break; default: alert(`${payload.method} is not supported for WalletConnect v1`); } signClientV1?.approveRequest(response); };
拒绝签名
const rejectRequestV1 = async () => { try { await signClientV1?.rejectRequest({ id: proposal.id, error: { message: 'USER_REJECTED_METHODS', }, }); } catch (e) { disconnectSessionForV1(); } };
主动断开
const disconnectSessionForV1 = () => { signClientV1?.killSession(); clearAllSessionsForV1(); };
v2
初始化签名对象实例&监听通信事件
import { Core } from '@walletconnect/core';
import { Web3Wallet, IWeb3Wallet } from '@walletconnect/web3wallet';
async function createV2SignClient() {
const core = new Core({
projectId: PROJECT_ID,
});
signClientV2 = await Web3Wallet.init({
core,
metadata: METADATA,
});
/**
* 连接申请 —— 扫码后触发
*/
signClientV2.on('session_proposal', connectApproveHandlerV2);
/**
* 成功与Dapp建立通信后,签名事件调用通知
*/
signClientV2.on('session_request', (event) => {
});
/**
* 通信断开的通知
*/
signClientV2.on('session_delete', disconnectSessionForV2);
}
配对
async function pairPeerForV2({ uri } = { uri: '' }) {
try {
signClientV2.core.pairing.pair({
uri,
activatePairing: true,
});
} catch (e) {
console.warn('---------------logger: walletConnect v2 connect error', e);
}
}
通信授权
同意授权
const approveSessionV2 = async () => { const { id, params: { requiredNamespaces, pairingTopic }, } = proposal; var n = Object.keys(requiredNamespaces).reduce((acc, cur) => { const { methods, events } = requiredNamespaces[cur]; acc[cur] = { methods, events, accounts: requiredNamespaces[cur].chains.map( (v: string) => v + `:${consumerAddress}` ), }; return acc; }, {} as any); try { await signClientV2?.approveSession({ id, namespaces: n, }); } catch (e) { disconnectSessionForV2(); } };
拒绝授权
const rejectSessionV2 = async () => { const { id } = proposal; try { await signClientV2?.rejectSession({ id, reason: getSdkError('USER_REJECTED_METHODS'), }); } catch (e) { disconnectSessionForV2(); } };
签名请求处理
签名授权
const onCallRequestV2 = async (event: any) => { const { id, params: { request }, topic, } = event; const { method, params } = request; let signature = '' as any; switch (method) { case EIP155_SIGNING_METHODS.ETH_SIGN: case EIP155_SIGNING_METHODS.PERSONAL_SIGN: signature = await walletConnectHelper.signEip192Message({ address, params, }); break; case EIP155_SIGNING_METHODS.ETH_SIGN_TYPED_DATA: case EIP155_SIGNING_METHODS.ETH_SIGN_TYPED_DATA_V3: case EIP155_SIGNING_METHODS.ETH_SIGN_TYPED_DATA_V4: signature = await walletConnectHelper.signEip712Message({ address, params, }); break; case EIP155_SIGNING_METHODS.ETH_SIGN_TRANSACTION: signature = await walletConnectHelper.signEip155Tx({ params, address, }); break; case EIP155_SIGNING_METHODS.ETH_SEND_TRANSACTION: try { signature = await walletConnectHelper.signAndSendEip155Tx({ params, address, }); } catch (e: any) { const errorText = e.message || 'Error occurs'; Toast.error({ msg: errorText, }); signClientV2?.respondSessionRequest({ topic, response: formatJsonRpcError(id, errorText), }); return; } break; case SOLANA_SIGNING_METHODS.SOLANA_SIGN_MESSAGE: signature = await walletConnectHelper.signSolanaMessage({ params, address, }); break; case SOLANA_SIGNING_METHODS.SOLANA_SIGN_TRANSACTION: signature = await walletConnectHelper.signSolanaTx({ params, address, }); break; default: alert(`${method} is not supported for WalletConnect v2`); } const response = formatJsonRpcResult(id, signature); await signClientV2.respondSessionRequest({ topic, response, }); };
拒绝签名
const rejectRequestV2 = async () => { const { id } = proposal; try { await signClientV2?.rejectSession({ id, reason: getSdkError('USER_REJECTED_METHODS'), }); } catch (e) { disconnectSessionForV2(); } };
主动断开
const disconnectSessionForV2 = () => { const activeSessions = signClientV2?.getActiveSessions(); activeSessions && Object.keys(activeSessions).forEach((t) => { signClientV2.disconnectSession({ topic: t, reason: walletConnectHelper.getSdkError('USER_DISCONNECTED'), }); }); };