美文网首页
WalletConnect钱包端实现

WalletConnect钱包端实现

作者: 说叁两事 | 来源:发表于2023-06-02 01:07 被阅读0次

    前言

    在加密货币世界中,去中心化应用(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的工作原理可以分为两个主要过程:建立连接和通信过程。

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

    相关文章

      网友评论

          本文标题:WalletConnect钱包端实现

          本文链接:https://www.haomeiwen.com/subject/lyppedtx.html