Enables mobile-to-desktop wallet pairing via QR codes and encrypted WebSocket tunnels. A web app (desktop browser) shows a QR code; user scans with their mobile BSV wallet; all wallet operations (signing, key retrieval, etc.) are proxied over HTTPS+WSS relay servers to the mobile without exposing keys or trust chains to the desktop. Provides both the relay server infrastructure (Node.js) and React frontend components for web apps to add "Connect Mobile Wallet" functionality.
npm install @bsv/wallet-relayimport express from 'express'
import { createServer } from 'http'
import cors from 'cors'
import { WalletRelayService } from '@bsv/wallet-relay'
import { ProtoWallet, PrivateKey } from '@bsv/sdk'
const app = express()
app.use(
cors({
origin: process.env.ORIGIN,
allowedHeaders: ['Content-Type', 'Authorization', 'X-Desktop-Token']
})
)
app.use(express.json())
const server = createServer(app)
const wallet = new ProtoWallet(PrivateKey.fromHex(process.env.WALLET_PRIVATE_KEY!))
new WalletRelayService({
app,
server,
wallet,
relayUrl: process.env.RELAY_URL,
origin: process.env.ORIGIN
})
server.listen(3000)import { useWalletRelayClient } from '@bsv/wallet-relay/react'
import { useEffect, useState } from 'react'
function WalletConnection() {
const { createSession } = useWalletRelayClient({
apiUrl: 'https://relay.example.com',
autoCreate: false
})
const [qrData, setQrData] = useState<string | null>(null)
const [sessionId, setSessionId] = useState<string | null>(null)
useEffect(() => {
const setup = async () => {
const session = await createSession()
setSessionId(session.sessionId)
setQrData(session.qrDataUrl) // Base64 PNG
}
setup()
}, [])
return (
<div>
{qrData && <img src={qrData} alt="Scan to pair wallet" />}
<p>Scan with your BSV wallet app</p>
</div>
)
}
GET /api/session, POST /api/request/:id) and WebSocket endpointencryptEnvelope(), decryptEnvelope() for AES-256-GCM authenticated encryptionbuildPairingUri(), parsePairingUri() for QR encodingverifyPairingSignature() for ECDSA signature validationbytesToBase64url(), base64urlToBytes() for URL-safe binaryimport { WalletConnectionModal } from '@bsv/wallet-relay/react'
import { useState } from 'react'
function App() {
const [showQR, setShowQR] = useState(false)
return (
<>
<WalletConnectionModal
onLocalWallet={wallet => {
console.log('Local wallet connected')
// Use WalletClient directly.
}}
onMobileQR={() => setShowQR(true)}
installUrl="https://desktop.bsvb.tech"
/>
</>
)
}
import { WalletRelayClient } from '@bsv/wallet-relay/client'
import { P2PKH } from '@bsv/sdk'
async function sendPayment(client: WalletRelayClient) {
const lockingScript = new P2PKH().lock('1EvmsbpAY7nESLkN4ajLTMbvsaQ1HpJPGX').toHex()
const response = await client.sendRequest('createAction', {
description: 'Send payment',
outputs: [
{
satoshis: 5000,
lockingScript,
outputDescription: 'payment output'
}
]
})
if (response.error) {
console.error('Mobile rejected:', response.error)
} else {
console.log('Action created:', response.result)
}
}import { WalletPairingSession, parsePairingUri } from '@bsv/wallet-relay/client'
// Scan desktop QR code to get the pairing URI.
const { params, error } = parsePairingUri(scannedQR)
if (!params) throw new Error(error ?? 'Invalid pairing URI')
const session = new WalletPairingSession(myWalletInstance, params, {
autoApproveMethods: new Set(['getPublicKey'])
})
session.onRequest(async (method, params) => {
// Forward approved requests to the local mobile wallet implementation.
return (myWalletInstance as any)[method](params)
})
await session.resolveRelay()
await session.connect()GET /api/session and required on POST /api/request/:id. Ensures only the frontend that created the session can use it.Backend key stability —
PrivateKeymust be the same across server restarts. Store in env var or secure vault, never generate new key each start.
Missing X-Desktop-Token header —
POST /api/request/:idrequiresX-Desktop-Tokenheader. Browser CORS preflight must allow this header inallowedHeaders.
CORS misconfiguration — If frontend and backend are different origins, CORS headers must be set. Missing
Access-Control-Allow-CredentialsorAccess-Control-Allow-Headerswill cause browser to block requests.
WebSocket TLS mismatch — If frontend is HTTPS but relay is ws:// (not wss://), browser blocks upgrade. Always use wss:// in production.
Relay URL in QR — The relay URL in QR is public. If relay is on internal network, mobile can't reach it. Use publicly routable URL or tunnel.