前言
在加密货币世界中,去中心化应用(decentralized application, DApp)的发展越来越迅速。
DApp 可以让用户更加自由地管理他们的资产,同时也让开发者更加容易地构建去中心化应用。然而,DApp 在与区块链交互时,往往需要使用钱包进行支付、签名等操作,这就需要 DApp 和钱包之间进行通信。
WalletConnect 是一种标准化的协议,可以帮助 DApp 和钱包之间进行安全的通信。
image.png什么是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<ISessionStatus> {
// Dapp调用
public async createSession(opts?: ICreateSessionOptions): Promise<void> {
}
// 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[]){
}
image.png
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'),
});
});
};
网友评论