1

前言

在加密货币世界中,去中心化应用(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/client": "^1.8.0",
    "@walletconnect/core": "^2.4.4",
    "@walletconnect/legacy-types": "^2.0.0",
    "@walletconnect/utils": "^2.4.4",
    "@walletconnect/web3wallet": "^1.2.0",

WalletConnect v1的工作原理可以分为两个主要过程:建立连接和通信过程。

  1. WalletConnect 的核心代码结构

WalletConnect 的核心代码结构包括客户端和服务器两部分。客户端代码可以在 DApp 中实现,服务器代码则需要部署在独立的服务器上。

  1. 客户端和服务器之间的消息格式

WalletConnect协议中消息的格式采用了JSON格式,包括了请求和响应两种类型。每个消息都包含了一个ID字段,用于标识消息的唯一性。请求消息包含一个Method字段,用于指示请求的方法,而响应消息则包含了一个Result字段,用于指示响应的结果。

  1. WalletConnect 的通信流程

WalletConnect 的通信流程可以概括为以下几个步骤:

  • DApp 向 WalletConnect 服务器发起连接请求,服务器返回连接信息;
  • DApp 向钱包发送连接请求,钱包弹出授权窗口,用户选择是否授权;
  • 如果用户授权,钱包会向 DApp 返回连接信息,双方开始加密通信;
  • 在通信过程中,DApp 可以向钱包发送请求,钱包可以向 DApp 返回响应。

image.png

  1. 客户端调用
  • 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,
  });
  1. 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'),
        });
      });
    };

米花儿团儿
1.3k 声望75 粉丝