QRL Connect: Post-Quantum dApp-to-Wallet Connection, and How to Add It to Your dApp

Connecting a decentralized application to a user’s wallet sounds simple and is not. The dApp runs in a browser, the wallet lives on a phone, and the two need to exchange signed transactions securely without trusting the network in between. For QRL 2.0 we built QRL Connect, an open-source SDK that solves this with a self-hosted, end-to-end encrypted, post-quantum channel. This post explains how it works and how you can drop it into your own dApp.

Why not WalletConnect?

The obvious question is why not just use WalletConnect. Two reasons. First, WalletConnect v2 depends on third-party cloud infrastructure, and we wanted a transport we fully control and can self-host. Second, generic wallets do not understand QRL 2.0’s Q-prefixed addresses or its post-quantum signature scheme. Since we build both the dApp SDK and the wallet, a purpose-built protocol let us put post-quantum cryptography directly into the transport layer, instead of the classical key exchange WalletConnect relies on.

The cryptography

Everything between the dApp and the wallet is end-to-end encrypted, and the relay server in the middle only ever sees ciphertext. The building blocks are all NIST-standardized post-quantum or well-established primitives:

  • ML-KEM-768 (FIPS 203, NIST Level 3): the key encapsulation mechanism that establishes a shared secret between the dApp and the wallet. This is the post-quantum stand-in for classical Diffie-Hellman.
  • HKDF-SHA-256: derives the actual encryption keys from that shared secret, bound to the full handshake transcript so both sides are provably talking about the same session.
  • AES-256-GCM: authenticated encryption applied to every JSON-RPC message. Any tampering is caught at the authentication tag.

The session key is tied to the entire handshake transcript (a label, the channel id, the public key, and the KEM ciphertext). That binding closes a class of attacks where a malicious peer tries to make two sessions agree on a key while disagreeing about who is who.

The handshake

Establishing the channel takes three steps, modeled loosely on a TCP handshake:

  • SYN: the dApp sends its ML-KEM-768 public key to the wallet through the relay. In practice this public key travels inside the QR code itself, so the relay never even sees an uncommitted key.
  • SYNACK: the wallet encapsulates a shared secret to that public key and returns the KEM ciphertext.
  • ACK: the dApp decapsulates the secret, both sides derive their AES-256-GCM keys from the transcript, and the encrypted channel is open.

From that point on, every request and response is an AES-256-GCM ciphertext passing through a relay that cannot read it.

Post-quantum signing, not Ethereum signing

QRL Connect speaks an EIP-1193 style provider interface, so it feels familiar to anyone who has used a browser wallet, but the signing methods are post-quantum native. The methods qrl_signMessage and qrl_signTypedData use SHAKE256 hashing and QRL 2.0’s ML-DSA-87 (Dilithium) signatures. They return a rich result that includes the signature, the public key, and the signer, and you can verify it locally with the verifyMessage and verifyTypedData helpers the package exports. The old Ethereum-flavored signing methods were removed in version 3.0.0.

Adding it to your dApp

The SDK is published on npm as @qrlwallet/connect. Install it:

npm install @qrlwallet/connect

Then create a connection, show the URI as a QR code (or open it as a deep link on mobile), and use it like any EIP-1193 provider:

import { QRLConnect } from '@qrlwallet/connect';

const qrl = new QRLConnect({
  dappMetadata: { name: 'My QRL dApp', url: 'https://mydapp.com' },
});

// Get the pairing URI
const uri = await qrl.getConnectionURI();

if (qrl.isMobile()) {
  window.location.href = uri;   // open the wallet app via deep link
} else {
  // render `uri` as a QR code using any QR library
}

qrl.on('connect', ({ chainId }) => {
  console.log('Wallet connected on chain', chainId);
});

// Use it like any EIP-1193 provider
const accounts = await qrl.request({ method: 'qrl_requestAccounts' });

const txHash = await qrl.request({
  method: 'qrl_sendTransaction',
  params: [{ from: accounts[0], to: '0x...', value: '0x2386F26FC10000' }], // 0.01 QRL
});

That is the whole flow. Read-only calls such as qrl_getBalance or qrl_blockNumber are proxied automatically, while anything that needs approval, like sending a transaction or signing a message, prompts the user inside the wallet.

Sessions and reconnection

Sessions persist in the browser’s local storage for seven days. When a user returns to your dApp, the SDK can reconnect to the existing channel without a fresh QR scan. Use hasStoredSession() on page load to decide whether to show a reconnect state, keep one QRLConnect instance for the page lifetime, and call newConnection() only when the user wants to pair a different wallet. Listen to the statusChanged event for UI transitions rather than reaching into internals.

Self-hosting the relay

The relay is a lightweight message router that only forwards ciphertext, and it is part of our open-source backend. If you would rather run your own, point the SDK at it with a single config option:

const qrl = new QRLConnect({
  dappMetadata: { name: 'My dApp', url: 'https://mydapp.com' },
  relayUrl: 'https://my-relay-server.com',
});

Try it and read the code

The fastest way to see it work is the live example at zondscan.com/dapp-example, which pairs with the production relay and the MyQRLWallet app out of the box. The SDK is open source and MIT licensed: install it from npm as @qrlwallet/connect or read the source on GitHub. Contributions and questions are welcome.

Scroll naar boven