QRDX Connect
Reach QRDX Wallet on a phone or another browser by QR code, end-to-end encrypted.
QRDX Connect links a site to QRDX Wallet running somewhere else (the iPhone app, or the web wallet in another browser) by scanning a QR code. The site gets the same EIP-1193 provider as the extension's; the wallet runs the same request router, so permissions, approvals and signing are identical. Only the transport differs.
site (browser) relay QRDX Wallet (phone)
provider ──encrypted──▶ /api/relay/v1/{topic} ──encrypted──▶ request router, approvals
◀──encrypted── mailbox per side, WebSocket ◀──encrypted── signs with the phone's keys
+ long-poll, attests the site's OriginThe relay only forwards ciphertext. It cannot read, forge or alter requests; at most it can drop or delay them. The public relay is https://trade.qrdx.org/api/relay and serves any site (QRDX Explorer uses it too).
Pairing
-
The site makes a random 32-byte key; the topic is
hex(sha256(key)). It connects to the relay on the topic asdapp(the relay records the browser'sOrigin), and shows a QR code of the pairing link:https://wallet.qrdx.org/wallet?connect=<url-encoded pairing URI> qrdx-connect:v1?k=<base64url key>&r=<relay URL>&n=<site name>&u=<site URL> -
The site sends
qrdx_requestAccounts. It waits in the relay until the phone joins. -
The wallet reads the code, derives the topic, and asks the relay for the site's attested origin. It refuses unless that equals the origin in
u. Then it joins aswalletand shows its usual connect approval.
Anyone who sees the QR code can join the session, as with WalletConnect, which is why the wallet asks before sharing anything.
Messages
Each message is AES-256-GCM with the pairing key, a fresh 12-byte IV, and additional data qrdx-connect:v1:<topic>:<sender side>, so it cannot be replayed to its sender or into another session. On the wire: base64url(iv ‖ ciphertext).
| From | Plaintext |
|---|---|
| site | { "t": "hello", "dapp": { "name", "url" } } |
| site | { "t": "req", "id", "method", "params" } |
| wallet | { "t": "hello", "wallet": { "name", "platform" } } |
| wallet | { "t": "res", "id", "result" } or { "t": "res", "id", "error": { "code", "message" } } |
| wallet | { "t": "event", "event": "accountsChanged" | "chainChanged" | "disconnect", "data" } |
| either | { "t": "bye" } |
Request ids are random; the wallet ignores an id it has answered, so a replayed request cannot make it sign twice.
Relay API
{topic} is 64 hex characters; side is dapp or wallet.
| Request | |
|---|---|
GET /v1/{topic}/ws?side= | WebSocket: receives { type: "message", seq, payload } and { type: "peer", online }; sends { type: "publish", payload } and { type: "ack", seq } |
POST /v1/{topic}/messages?side= | send { payload } without a socket |
GET /v1/{topic}/messages?side=&after=&wait= | long-poll for messages after seq |
GET /v1/{topic} | the session's attested site origin |
DELETE /v1/{topic} | end the session |
Messages wait in the relay until the other side acknowledges them (up to 24 hours), so a phone that was asleep still gets the request when it wakes. Sessions expire after 7 days idle.
Using it
The reference implementation is a few hundred lines with no dependencies beyond WebCrypto: qrdx-trade lib/connect (protocol) and lib/wallet/remote.ts (the provider). Copy them, point them at the public relay, and treat the result like the extension's provider.