rsksmart / rwallet

The RSK Wallet is based on our rWallet open-source code-base, which will allow developers and organizations to use it to build their own apps.
https://developers.rsk.co/wallet/
GNU General Public License v3.0
43 stars 22 forks source link

WalletConnect Integration Instructions & Code Snippet #455

Open AmazingBeerBelly opened 4 years ago

AmazingBeerBelly commented 4 years ago

Flow path:

  1. The DAPP side displays the QR code, and the wallet side scans the QR code and then selects a wallet to log in. The wallet side calls "approveSession" method to allow login, "rejectSession" method to deny login, and "killSession" method to disconnect.
  initWalletConnect = async () => {
    const { navigation } = this.props;
    const { uri } = navigation.state.params;

    this.setState({ loading: true });

    try {
      // uri is the DAPP QR code value. Example: 'wc:xxxxxxxxxxxxxxxxxx'
      const connector = new WalletConnect({ uri });

      if (!connector.connected) {
        await connector.createSession();
      }

      console.log('connector: ', connector);

      await this.setState({
        loading: false,
        connector,
      });

      // subscribe the events from DAPP
      this.subscribeToEvents();
    } catch (error) {
      this.setState({ loading: false });

      throw error;
    }
  }
  1. After DAPP and the wallet are connected, the wallet side needs to set the listening events (subscribeToEvents method)
  subscribeToEvents = () => {
    console.log('ACTION', 'subscribeToEvents');
    const { connector } = this.state;
    console.log('connector: ', connector);

    if (connector) {
      // Subscribe to session requests
      connector.on('session_request', (error, payload) => {
        console.log('EVENT', 'session_request');

        if (error) {
          throw error;
        }

        /* payload:
          {
            id: 1,
            jsonrpc: '2.0'.
            method: 'session_request',
            params: [{
              peerId: '15d8b6a3-15bd-493e-9358-111e3a4e6ee4',
              peerMeta: {
                name: "WalletConnect Example",
                description: "Try out WalletConnect v1.x.x",
                icons: ["https://example.walletconnect.org/favicon.ico"],
                url: "https://example.walletconnect.org"
              }
            }]
          }
        */

        const { peerMeta } = payload.params[0];
        console.log('payload: ', payload);
        this.setState({ peerMeta });
      });

      // Subscribe to session update
      connector.on('session_update', (error) => {
        console.log('EVENT', 'session_update');

        if (error) {
          throw error;
        }
      });

      // Subscribe to call requests
      connector.on('call_request', async (error, payload) => {
        const { method, params } = payload;
        // tslint:disable-next-line
        console.log('EVENT', 'call_request', 'method', method);
        console.log('EVENT', 'call_request', 'params', params);

        // method type and params format are described in this document (https://docs.walletconnect.org/json-rpc-api-methods/ethereum#parameters-5)

        if (error) {
          throw error;
        }

        this.setState({ payload });
      });

      // Subscribe to connect
      connector.on('connect', (error, payload) => {
        console.log('EVENT', 'connect');

        if (error) {
          throw error;
        }

        this.setState({ connected: true });
      });

      // Subscribe to disconnect
      connector.on('disconnect', (error, payload) => {
        console.log('EVENT', 'disconnect');

        if (error) {
          throw error;
        }
      });

      if (connector.connected) {
        this.setState({
          connected: true,
        });
      }

      this.setState({ connector });
    }
  };
  1. DAPP sends some operations (personal sign, eth SendTransaction, etc.) after the Bridge Server is heard, the wallet side will listen to these operations and do the corresponding processing (that is to process method of payload separately, in handleCallRequest method), then call approveRequest method to return the result to Dapp. Calling rejectRequest means rejecting these operations.
  import { ethers } from 'ethers';
  import Rsk3 from '@rsksmart/rsk3';

  // Init ethers provider and set chainId
  initNetwork = async (network) => {
    this.rskEndpoint = network === 'Mainnet' ? MAINNET.RSK_END_POINT : TESTNET.RSK_END_POINT;
    this.provider = new ethers.providers.JsonRpcProvider(this.rskEndpoint);
    const chainId = network === 'Mainnet' ? MAINNET.NETWORK_VERSION : TESTNET.NETWORK_VERSION;
    this.setState({ chainId });
  }

  /*
  * Change wallet and chainId
  *
  * connector.updateSession({
  *   chainId: newChainId,
  *   accounts: [address],
  * });
  */

  // Sign a single message
  signMessage = async (signWallet) => {
    const { payload: { params } } = this.state;
    const message = Rsk3.utils.hexToAscii(params[0]);
    const signature = await signWallet.signMessage(message);
    return signature;
  }

  // Sign a transaction
  signTransaction = async (signWallet) => {
    const { selectedWallet: { address }, payload: { params } } = this.state;
    const nonce = await this.provider.getTransactionCount(Rsk3.utils.toChecksumAddress(address), 'pending');
    // Get network gasPrice
    const gasPrice = await this.provider.getGasPrice();
    const txData = {
      nonce,
      data: params[0].data,
      gas: params[0].gas || 600000,
      gasPrice: params[0].gasPrice || gasPrice,
      to: Rsk3.utils.toChecksumAddress(params[0].to),
      value: (params[0].value && ethers.utils.bigNumberify(params[0].value)) || '0x0',
    };

    // Sign transaction
    const rawTransaction = await signWallet.sign(txData);

    return rawTransaction;
  }

  // Send the signed transaction to block chain
  sendRawTransaction = async (rawTransaction) => {
    const res = await this.provider.sendTransaction(rawTransaction);
    return res.hash;
  }

  handleCallRequest = async () => {
    const { selectedWallet: { privateKey }, payload } = this.state;
    const { id, method, params } = payload;

    let result = null;
    const signWallet = new ethers.Wallet(privateKey, this.provider);
    switch (method) {
      case 'personal_sign': {
        result = await this.signMessage(signWallet);
        break;
      }

      case 'eth_sign': {
        result = await this.signMessage(signWallet);
        break;
      }

      // I don't know how to deal with it yet
      case 'eth_signTypedData': {
        break;
      }

      case 'eth_sendTransaction': {
        const rawTransaction = await this.signTransaction(signWallet);
        result = await this.sendRawTransaction(rawTransaction);
        break;
      }

      case 'eth_signTransaction': {
        result = await this.signTransaction(signWallet);
        break;
      }

      case 'eth_sendRawTransaction': {
        const rawTransaction = params[0];
        result = await this.sendRawTransaction(rawTransaction);
        break;
      }

      default:
        break;
    }

    // Every operation has a unique id
    return { id, result };
  }

  // Approve call request
  approveRequest = async () => {
    const { connector, payload } = this.state;
    const { id } = payload;

    try {
      const result = await this.handleCallRequest();
      // The dapp will get the result when the wallet call the connector.approveRequest method
      connector.approveRequest(result);
    } catch (error) {
      console.error(error);
      if (connector) {
        // The dapp will get the reject response when the wallet call the connector.rejectRequest method
        connector.rejectRequest({
          id,
          error: { message: 'Failed or Rejected Request' },
        });
      }
    }

    // Close request when the request is finished
    this.closeRequest();
    await this.setState({ connector });
  }
chrisli30 commented 4 years ago

Nice

ilanolkies commented 4 years ago

LRGTM! One question one advise:

AmazingBeerBelly commented 4 years ago

Yes, the user will be asked before signing a transaction/message. Usually, the wallet will pop a popup window to ask the user to approve or reject it. => question: is the user asked before signing a transaction/mesaage? A pop up or a notification?

Ok, thanks. I will spend some time researching eth_signTypedData => advise: eth_signTypedData specs here https://eips.ethereum.org/EIPS/eip-712

grinry commented 4 years ago

Is this already added to the app? Tried QR scanner in the last TestFlight build - from the app side it allows to connect, it even shows that the dapp is connected and allows to disconnect. However dapp itself does not receive any event about connection and just hangs with QR code opened, waiting for the connection.

chrisli30 commented 4 years ago

Hi @grinry what dapp did you test against with? Is it on Testnet or Mainnet?

grinry commented 4 years ago

Hey @chrisli30, I tried both networks, dapp address is https://trade.sovryn.app/ Tested with ios (testflight) and android (build from source) with no luck