@qrlwallet/connect 3.2.0 → 4.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -10,7 +10,10 @@ Connect your dApp to QRL Wallet. Users scan a QR code (desktop) or tap a button
10
10
  4. Your dApp sends JSON-RPC requests, the wallet prompts for approval
11
11
  5. Signed results come back to your dApp
12
12
 
13
- All communication is end-to-end encrypted with ML-KEM-768 (FIPS 203) key encapsulation and AES-256-GCM. The relay server never sees your data.
13
+ All application traffic is end-to-end encrypted with ML-KEM-768 (FIPS 203)
14
+ key encapsulation and AES-256-GCM. PQP3 adds a fresh 32-byte pairing
15
+ capability to every QR/deep link. The relay serves the dApp public key, but it
16
+ never receives that capability or application plaintext.
14
17
 
15
18
  ## Install
16
19
 
@@ -18,6 +21,10 @@ All communication is end-to-end encrypted with ML-KEM-768 (FIPS 203) key encapsu
18
21
  npm install @qrlwallet/connect
19
22
  ```
20
23
 
24
+ Package installation and source builds require Node.js 20.19.0 or newer because
25
+ the direct cryptography dependencies use that engine floor. Browser execution
26
+ requires Web Crypto.
27
+
21
28
  Want a ready-made pairing dialog (QR code, deep link, copy-code fallback) instead of building your own? Add the framework-free [`@qrlwallet/connect-ui`](ui/) web component.
22
29
 
23
30
  ## Quick start
@@ -68,11 +75,13 @@ const accounts = await qrl.request({ method: 'qrl_requestAccounts' });
68
75
 
69
76
  const txHash = await qrl.request({
70
77
  method: 'qrl_sendTransaction',
71
- params: [{
72
- from: accounts[0],
73
- to: '0x...',
74
- value: '0x2386F26FC10000', // 0.01 QRL
75
- }],
78
+ params: [
79
+ {
80
+ from: accounts[0],
81
+ to: 'Q20E7Bde67f00EA38ABb2aC57e1B0DD93f518446c',
82
+ value: '0x2386F26FC10000', // 0.01 QRL
83
+ },
84
+ ],
76
85
  });
77
86
  ```
78
87
 
@@ -85,71 +94,126 @@ const qrl = new QRLConnect({
85
94
  name: 'QuantaPool',
86
95
  url: 'https://quantapool.com',
87
96
  icon: 'https://quantapool.com/icon.png', // optional
97
+ redirectUrl: 'https://quantapool.com/return', // optional
88
98
  },
89
99
 
90
100
  // Optional
91
- relayUrl: 'https://qrlwallet.com', // default relay
92
- chainId: '0x0', // QRL chain ID
93
- autoReconnect: true, // reconnect on page load (default: true)
94
- debug: false, // console logging
101
+ relayUrl: 'https://qrlwallet.com', // default relay
102
+ chainId: '0x0', // QRL chain ID
103
+ autoReconnect: true, // reconnect on page load (default: true)
104
+ walletRedirectOnRequest: true, // mobile: deep-link the wallet app awake
105
+ // for approval requests (default: true)
106
+ debug: false, // console logging
95
107
  });
96
108
  ```
97
109
 
110
+ `dappMetadata.url` and optional `redirectUrl` are canonicalized
111
+ credential-free HTTP(S) URLs. HTTPS is required except on an explicit
112
+ `localhost`, `.localhost`, `127.0.0.1`, or `[::1]` development hostname. A
113
+ custom app scheme is not accepted as a return target.
114
+
98
115
  ## API
99
116
 
100
117
  ### `QRLConnect`
101
118
 
102
119
  The main class. Creates a connection manager and EIP-1193 provider.
103
120
 
104
- | Method | Description |
105
- |--------|-------------|
106
- | `getConnectionURI()` | Returns the `qrlconnect://` URI for QR codes or deep links |
107
- | `request({ method, params })` | Send a JSON-RPC request to the wallet |
108
- | `isMobile()` | Check if the user is on a mobile browser |
109
- | `getAppStoreUrl()` | Get the app store link for QRL Wallet |
110
- | `isConnected()` | Whether the wallet is connected |
111
- | `getAccounts()` | Get connected accounts |
112
- | `getStatus()` | Current connection status |
113
- | `hasStoredSession()` | Check if a reconnectable session exists in local storage |
114
- | `newConnection()` | Reset current pairing and generate a new channel/URI |
115
- | `disconnect()` | End the session |
116
- | `getChannelId()` | Get the current relay channel ID (useful for debugging) |
121
+ | Method | Description |
122
+ | ----------------------------- | ------------------------------------------------------------------- |
123
+ | `getConnectionURI()` | Retires any prior pairing and returns a fresh `qrlconnect://` URI |
124
+ | `request({ method, params })` | Send a JSON-RPC request to the wallet |
125
+ | `isMobile()` | Check if the user is on a mobile browser |
126
+ | `getAppStoreUrl()` | Get the app store link for QRL Wallet |
127
+ | `isConnected()` | Whether the wallet is connected |
128
+ | `isPaired()` | Whether a pairing exists, even while the wallet app is backgrounded |
129
+ | `isWalletPresent()` | Whether the wallet is currently live in the relay channel |
130
+ | `getAccounts()` | Get connected accounts |
131
+ | `getStatus()` | Current connection status |
132
+ | `hasStoredSession()` | Check if a reconnectable session exists in local storage |
133
+ | `newConnection()` | Reset current pairing and generate a new channel/URI |
134
+ | `disconnect()` | End the session |
135
+ | `getChannelId()` | Get the current relay channel ID (useful for debugging) |
117
136
 
118
137
  ### Events
119
138
 
120
- | Event | Payload | Description |
121
- |-------|---------|-------------|
122
- | `connect` | `{ chainId }` | Wallet connected |
123
- | `disconnect` | `{ code, message }` | Wallet disconnected |
124
- | `accountsChanged` | `string[]` | Account list changed |
125
- | `chainChanged` | `string` | Chain switched |
126
- | `statusChanged` | `ConnectionStatus` | Intermediate lifecycle states (see values below) |
127
- | `connection_lost` | — | Emitted after 5 failed reconnection attempts |
139
+ | Event | Payload | Description |
140
+ | ----------------- | ------------------- | ------------------------------------------------ |
141
+ | `connect` | `{ chainId }` | Wallet connected |
142
+ | `disconnect` | `{ code, message }` | Wallet disconnected |
143
+ | `accountsChanged` | `string[]` | Account list changed |
144
+ | `chainChanged` | `string` | Chain switched |
145
+ | `statusChanged` | `ConnectionStatus` | Intermediate lifecycle states (see values below) |
146
+ | `connection_lost` | (none) | Emitted after 5 failed reconnection attempts |
128
147
 
129
148
  #### `ConnectionStatus` values
130
149
 
131
- | Value | Meaning |
132
- |-------|---------|
133
- | `disconnected` | No active connection |
134
- | `connecting` | Connecting to relay server |
135
- | `waiting` | QR displayed, waiting for wallet to scan |
136
- | `key_exchange` | Post-quantum key exchange in progress |
137
- | `connected` | Wallet connected, ready for requests |
138
- | `reconnecting` | Attempting to restore a previous session |
150
+ | Value | Meaning |
151
+ | -------------- | ------------------------------------------------------------------------------------------------ |
152
+ | `disconnected` | No active connection |
153
+ | `connecting` | Connecting to relay server |
154
+ | `waiting` | Waiting for the wallet: a first scan, or (when `isPaired()`) a backgrounded wallet app to return |
155
+ | `key_exchange` | Post-quantum key exchange in progress |
156
+ | `connected` | Wallet connected, ready for requests |
157
+ | `reconnecting` | Attempting to restore a previous session |
139
158
 
140
159
  ### Supported RPC methods
141
160
 
142
161
  **Require user approval:**
143
- `qrl_requestAccounts`, `qrl_sendTransaction`, `qrl_signTransaction`, `qrl_signMessage`, `qrl_signTypedData`, `wallet_addQrlChain`, `wallet_switchQrlChain`
144
-
145
- `qrl_signMessage` and `qrl_signTypedData` (v3.0.0) replace the Ethereum-flavored `personal_sign` / `qrl_sign` / `qrl_signTypedData_v3` / `qrl_signTypedData_v4`. Both use SHAKE256 + native ML-DSA-87 ctx and return a rich `{ signature, publicKey, signer, digest, schemeVersion }` object; verify locally with `verifyMessage` / `verifyTypedData` exported from this package.
162
+ `qrl_requestAccounts`, `qrl_sendTransaction`, `qrl_signTransaction`, `qrl_signMessage`, `qrl_signTypedData`, `wallet_switchQrlChain`
163
+
164
+ `qrl_signMessage` and `qrl_signTypedData` (v3.0.0) replace the Ethereum-flavored `personal_sign` / `qrl_sign` / `qrl_signTypedData_v3` / `qrl_signTypedData_v4`. Both use SHAKE256 + native ML-DSA-87 ctx and return a rich `{ signature, publicKey, descriptor, signer, digest, schemeVersion }` object.
165
+
166
+ `request()` returns `unknown`. First validate the exact response shape with
167
+ `isQrlSignedMessageResult` or `isQrlSignedTypedDataResult`, then require
168
+ `hasSigningDescriptor`. PQP3 authenticates the paired transport session. It
169
+ does not prove that the returned ML-DSA signature is valid or that its public
170
+ key belongs to the claimed signer. Authentication and authorization flows must
171
+ call `verifyMessageForSigner` or `verifyTypedDataForSigner` with the original
172
+ challenge or payload and the expected current Q + 40 hex address.
173
+
174
+ The lower-level `verifyMessageSignature` / `verifyTypedDataSignature` helpers
175
+ verify only the supplied key and signature; they do not prove that the key
176
+ belongs to a claimed signer. The old `verifyMessage` / `verifyTypedData` names
177
+ remain as deprecated aliases. `descriptor` remains optional in the response
178
+ type for older-wallet compatibility. Strict shape guards accept its absence,
179
+ while `hasSigningDescriptor` and both bound verifiers fail closed.
180
+
181
+ Approval-bound calls are serialized. Unknown methods and direct node mutation
182
+ surfaces such as `qrl_sendRawTransaction` and `wallet_addQrlChain` are rejected
183
+ locally. Signing and transaction calls also fail locally until exactly one
184
+ account has been approved by `qrl_requestAccounts`. Their signer or `from`
185
+ field must exactly match that approved address.
146
186
 
147
187
  **Auto-proxied (no approval needed):**
148
- `qrl_getBalance`, `qrl_call`, `qrl_estimateGas`, `qrl_blockNumber`, `qrl_chainId`, `qrl_getTransactionReceipt`, and 30+ more read-only methods.
188
+ `qrl_chainId`, `qrl_blockNumber`, `qrl_getBalance`,
189
+ `qrl_getTransactionCount`, `qrl_getBlockByNumber`,
190
+ `qrl_getTransactionReceipt`, `qrl_call`, `qrl_estimateGas`, `qrl_gasPrice`,
191
+ `qrl_getCode`, `qrl_getLogs`, `net_version`, and `net_listening`.
192
+
193
+ `qrl_accounts` is a wallet-local authorization read. It returns `[]` until
194
+ `qrl_requestAccounts` succeeds and is never forwarded to hosted node RPC.
195
+ An account string is not proof of control. For authentication, request a fresh
196
+ challenge with `qrl_signMessage` and verify it with `verifyMessageForSigner`.
149
197
 
150
198
  ## Sessions
151
199
 
152
- Sessions persist in `localStorage` for 7 days. When a user returns to your dApp, the SDK can automatically reconnect without requiring a new QR scan.
200
+ Established v5 sessions persist in `localStorage` for 7 days. When a user
201
+ returns to your dApp, the SDK can automatically reconnect without requiring a
202
+ new QR scan. Pairing capabilities are never persisted. An exclusive Web Lock
203
+ gives one browser tab ownership of the persisted key and AEAD counters. A
204
+ second tab cannot restore or use that stream. Browsers without Web Locks use a
205
+ memory-only session and require a fresh pairing after reload.
206
+
207
+ Malformed handshake frames and ambiguous relay acknowledgements retire the
208
+ affected pairing. A fresh QR starts a new key, channel, and request generation;
209
+ pending work from the prior wallet is rejected locally, and the old relay
210
+ channel must be tombstoned before the new bearer capability is issued.
211
+
212
+ A paired session also survives the wallet app being backgrounded or closed. On the same device at most one of the two apps is ever foregrounded, so the wallet's socket being absent is the normal steady state of a mobile flow, not an error:
213
+
214
+ - `request()` still works while the wallet is away: the relay buffers the encrypted request for up to 5 minutes and the wallet drains it when it re-joins.
215
+ - On mobile browsers, an approval-needing request also fires a `qrlconnect://` deep link that brings the wallet app to the foreground (disable with `walletRedirectOnRequest: false`). `qrl_requestAccounts` never redirects: dApps call it on page load without a user gesture.
216
+ - Use `isPaired()` to tell "wallet away, session intact" apart from "never paired" when `getStatus()` is not `connected`.
153
217
 
154
218
  Recommended lifecycle:
155
219
 
@@ -157,6 +221,7 @@ Recommended lifecycle:
157
221
  - Keep a single `QRLConnect` instance for the page lifetime
158
222
  - Use `newConnection()` when the user explicitly wants to pair a different wallet
159
223
  - Use `statusChanged` for UI state transitions instead of relying on internals
224
+ - On a `disconnect` event, check `hasStoredSession()`: if a session is still stored the wallet is merely unreachable (revived by any request); only re-pair when it is gone
160
225
 
161
226
  ## How the relay works
162
227
 
@@ -178,6 +243,11 @@ const qrl = new QRLConnect({
178
243
  });
179
244
  ```
180
245
 
246
+ Relay URLs must use HTTPS. Plain HTTP is accepted only for an explicit
247
+ `localhost`, `.localhost`, `127.0.0.1`, or `[::1]` development endpoint. Relay
248
+ URLs identify an origin only: credentials, paths, query strings, and fragments
249
+ are rejected before the URL can be logged, connected, or embedded in a QR.
250
+
181
251
  ## Development
182
252
 
183
253
  ```bash
@@ -204,7 +274,7 @@ node test-e2e.mjs
204
274
 
205
275
  The `example/` directory contains a Vite test dApp with QR code generation, transaction sending, message signing, and read-only RPC calls.
206
276
 
207
- > **Want to try it without any setup?** The same example is hosted live at [zondscan.com/dapp-example](https://zondscan.com/dapp-example) — it pairs with the production relay and MyQRLWallet mobile app out of the box.
277
+ > **Want to try it without any setup?** The same example is hosted live at [zondscan.com/dapp-example](https://zondscan.com/dapp-example): it pairs with the production relay and MyQRLWallet mobile app out of the box.
208
278
 
209
279
  ```bash
210
280
  # 1. Build the SDK first (the example links to it locally)
@@ -223,14 +293,18 @@ The example connects to the production relay at `wss://qrlwallet.com/relay` by d
223
293
 
224
294
  - Private keys and seeds never leave the wallet
225
295
  - All relay traffic is end-to-end encrypted with **AES-256-GCM** bound to a
226
- **transcript hash** derived from the full handshake (`LABEL || cid || pk || ct`)
296
+ **transcript hash** derived from the full handshake
297
+ (`"pq-pair/v3" || cid || pk || ct || capability`)
227
298
  - Session keys are established with **ML-KEM-768** (FIPS 203, NIST Level 3),
228
- carried in the QR code so the relay never sees an uncommitted public key
299
+ with the public key uploaded to the relay and pinned by a full 32-byte
300
+ capability-bound fingerprint in the QR
301
+ - The PQP3 capability is a bearer secret. Do not log, persist, cache, or send
302
+ a pairing URI to analytics. Retire the channel when a pairing UI is cancelled
229
303
  - Ciphertext tampering is detected exclusively at the AES-GCM tag;
230
304
  ML-KEM's implicit rejection is NOT used for authentication
231
305
  - PIN or biometric authentication required for every transaction
232
- - dApp URL is displayed to the user before connecting
233
- - Unknown RPC methods are rejected with `-32601`
306
+ - dApp name and URL are displayed as unverified, dApp-supplied metadata
307
+ - Unknown RPC methods are rejected locally before a relay round trip
234
308
 
235
309
  ## License
236
310