If you have used an Algorand wallet such as Pera or Defly to interact with any of the growing number of dApps out there, then you have no doubt used WalletConnect directly or indirectly. This post is going to do a deep-dive into this part of the blockchain and de-fi infrastructure for Algorand and other chains.

QR Code

When using a dApp on Algorand, you’ve clicked one of these:

Connect Wallet

This allows you to authorize a link between the browser you are using and your wallet. But what exactly goes on when one does that?

WalletConnect

In the case of Algorand and many other chains, dApp integration with wallets is brokered through a protocol called WalletConnect.

WalletConnect provides a specification, as well as hosted infrastructure, for encrypted messaging tunnels between dApp frontends and wallets that are relayed across websockets served by WalletConnect servers.

When you connect a wallet to a dApp, your browser connects to a WalletConnect relay server, and your wallet connects to the same relay server. That relay server is used to exchange messages encrypted with a key provided to the wallet out of band in the QR code.

These messages include things like transactions prepared by the dApp and written to the bridge where they are picked up by the listening wallet which will present them to the user to be signed.

Step 1: The QR Code

When you click the Connect Wallet button to initiate a connection to a wallet on a dApp, you’re presented with a QR code that you can scan with a mobile wallet using the camera on your device. Using a small script we can see what is in the QR code (code below):

Decoding a QR code

This QR code encodes a WalletConnect URI generated dynamically in the user browser for a new session by the dApp frontend. The URI resembles the one in the image below:

WalletConnect example URI

This URI provides the Wallet with the information needed to establish a session with the dApp. Breaking it down:

  • The prefix wc, which is meant to indicate that the URI is for the WalletConnect protocol
  • A UUID that is uniquely generated by the dApp frontend to serve as the handshake topic ID
  • The version of the protocol. There are two WalletConnect protocol versions: v1 and v2. This guide covers v1.
  • Various parameters following the URI ? delimeter metacharacter. Parameters include:
    • The URL encoded URI of the relay server that will host the session between the dApp frontend and the wallet
    • A symmetric (AES256-CBC) key generated at the time of session initiation by the dApp frontend, used to encrypt subsequent messages in the session

This is all of the information needed for the wallet to connect to a bridge and continue initialiazing a session with the dApp.

Some Python code to extract the WalletConnect URI from a QR code image:

## https://corewar.org 

import sys
import re
from pyzbar.pyzbar import decode
from PIL import Image

## WC URI regex
wc_regex = "wc:(.*?)@(\d)\?bridge=(.*?)&key=(.*?)(&.*|$)"

try:
    ## Open a QR code image supplied as a command-line argument to the script
    image = Image.open(sys.argv[1]) 
    
    ## Decode the image using pzybar
    decoded = decode(image)
    
    ## Analyze the extracted data
    for code in decoded:
        re = re.compile(wc_regex)
        code_str = code.data.decode("utf-8")
        
        ## Match on the regex pattern
        x = re.match(code_str)
        if len(x.groups()) > 1:
            
            ## Print the WalletConnect URI parts
            print("handshake topic:",x.group(1))
            print("version:",x.group(2))
            print("bridge:",x.group(3))
            print("key: ",x.group(4))

except Exception as e:
    print("Error: ",e)

Step 2: Session Initialization

The dApp also publishes a session initialization message to the handshake topic ID provided in the WalletConnect URI. This message is encrypted using the symmetric key included as a parameter in the WalletConnect URI encoded in the QR code.

Meanwhile, the wallet is expected to connect to the relay server to decrypt and consume that message, which will contain a new topic ID in its payload. This topic identifier is to be used for the session once it is established. The screenshot below depicts such a message sent to a WalletConnect websocket by a dApp frontend in the user’s browser, being sniffed using a web proxy configured to do so.

Initial message published by dApp to bridge

As is visible in the screenshot, the message data is encrypted using the key generated at the time that the wallet connection was established, in the browser of the dApp user. This key is included in the WalletConnect URI and can be used to decrypt messages.

The following Python script decrypts the above WalletConnect session initialization message, which is included in the code as a hardcoded string.

## https://corewar.org

import json
import base64
from Crypto.Cipher import AES

## Key from the WalletConnect URI string
key = bytes.fromhex("d77698232c1c9530155099bee82757ae23395a0177c18788b3cdce300cc7c33e")
 
## Message published to WalletConnect WebSocket bridge by dApp frontend
handshake1_msg = '''
{ "topic":"84c22564-6892-46e7-9a7f-d8b344253a94",\
  "type":"pub",\
  "payload":"{\\"data\\":\\"ae89d7959f6fc3452a66cfc45fb0ef876ebdde236a98a9b5a348684e0f69702368e2ed3548cd8fb0d55e65eae0fbb61fa8bf3b3dbc0980d73b9becac577b75e4e94ea241c9f9f92b600ba7c872dc770bf307b6bf8c205074989b79554e133ec1da0d2edb95dc356edc18403cbabe24d0d74d6322c17173fa34421627cf9033b6aa7bef1c653032fc71b9262f50cc3424c10149dbe74096d03ec34b6b7c68666f48e5793d51b3c42fd04d36585f48e0739c8ce9ff9315de9dc8c628a8bca56991ad72042264681ede99fe1b0dbbf8ee6e1a5c51a00c09fd8ad18a765e8e292609116dea70d3d09c6e5f9852a024eb8bac110e687478d9950638c0dd52650f7f1976f37b8093617324aca3663b6dc569654894e07e8d258dcc629ca9f8e37f98e154635828841c3410e9f698dcde9802bbb57f1075a7dd5bd2f7a1edb46a29a2b5\\",\
              \\"hmac\\":\\"57ed8e81230f62aa7f5f6513972802c79d3cbf52e7b85cbbf58dbfd52962a188\\",\
              \\"iv\\":\\"ec4c50fef72028e843a72d5bce56c105\\"}",\
  "silent":true}\
'''

## Deserialize the message envelope 
handshake1_msg_obj = json.loads(handshake1_msg)

## Deserialize the nested message payload separately
handshake1_payload_obj = json.loads(handshake1_msg_obj["payload"])

## Extract the IV used for the AES256-CBC encryption
iv = bytes.fromhex(handshake1_payload_obj["iv"])

## Extract the ciphertext
data_ciphertext = bytes.fromhex(handshake1_payload_obj["data"])

## Initialize the cipher context
cipher = AES.new(key, AES.MODE_CBC, iv) 

## Decrypt ciphertext and decode to UTF-8 string
data_plaintext = cipher.decrypt(data_ciphertext).decode('utf-8')

print(data_plaintext)

Step 3: Session Communication

The dApp and the wallet can interact with one another across the relay bridge once the session is initialized. If the user wishes to perform a transaction on-chain, such as an interaction with a smart contract, the transactions are prepared by the dApp frontend, serialized and encrypted, and then written to the relay. The wallet reads the message from the relay, decrypts it, unpacks the transaction, and presents it to the wallet user for signing.

WalletConnect serializes transactions using a JSON-RPC 2.0 schema. One example message in this schema is algo_signTxn, used to transmit transactions to be signed to the wallet over the WalletConnect relay. The structure of the algo_signTxn envelope is:

interface AlgoSignTxnRequest {
  id: number;
  jsonrpc: "2.0";
  method: "algo_signTxn";
  params: SignTxnParams;
}

The actual transactions to be signed are embedded as a param with the key txn. Transactions are encoded as printable ASCII bytes using base64 and packed using the msgpack binary serialization scheme. These can be examined with the following Python code:


## Initialize cipher context 
cipher = AES.new(key, AES.MODE_CBC, iv2)

## Decrypt message read from relay
pt = cipher.decrypt(ciphertext).decode('utf-8')

## Deserialize message 
envelope = json.loads(pt)

## Iterate through params
for param in envelope["params"]: 
    for p in param:
    	## Decode and unpack binary transaction
        print(msgpack.unpackb(base64.b64decode(p["txn"])))

That should be enough for this blog post. One thing that is interesting about this is that the dApp runs entirely in browser. The whole thing can be served by a CDN as static content. Thre’s no backend server necessary for the dApp to operate: the work is done by the blockchain, by the relay servers, and by the wallet. Of course, in reality, many dApps do have some application backend surface hosted on their own infrastructure for off-chain work – but it’s not at all necessary.

More on this in later posts.

Comments? Contact at: corewar @ gmx.com or @corewarcrypto on twitter.