@qrlwallet/connect 4.0.0 → 5.0.1

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
@@ -2,6 +2,18 @@
2
2
 
3
3
  Connect your dApp to QRL Wallet. Users scan a QR code (desktop) or tap a button (mobile) to pair their wallet, then approve transactions right from the app.
4
4
 
5
+ ## Version 5 migration
6
+
7
+ Version 5 requires QIP-55 addresses: `Q` followed by 128 hexadecimal characters.
8
+ Account validation and message-signature verification use the complete 64-byte
9
+ address. Applications using legacy 20-byte addresses must remain on version 4
10
+ until their network and wallet support QIP-55.
11
+
12
+ Wallet typed-data requests are disabled pending a versioned 64-byte address
13
+ encoding. The local verifier remains available for address-free typed data.
14
+ The private v3 network is a test network; its availability does not establish
15
+ compatibility with legacy deployments or contracts.
16
+
5
17
  ## How it works
6
18
 
7
19
  1. Your dApp generates a connection URI and shows it as a QR code
@@ -78,7 +90,7 @@ const txHash = await qrl.request({
78
90
  params: [
79
91
  {
80
92
  from: accounts[0],
81
- to: 'Q20E7Bde67f00EA38ABb2aC57e1B0DD93f518446c',
93
+ to: 'Q22222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222',
82
94
  value: '0x2386F26FC10000', // 0.01 QRL
83
95
  },
84
96
  ],
@@ -161,15 +173,16 @@ The main class. Creates a connection manager and EIP-1193 provider.
161
173
  **Require user approval:**
162
174
  `qrl_requestAccounts`, `qrl_sendTransaction`, `qrl_signTransaction`, `qrl_signMessage`, `qrl_signTypedData`, `wallet_switchQrlChain`
163
175
 
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.
176
+ `qrl_signMessage` uses SHAKE256 plus native ML-DSA-87 ctx and returns a rich `{ signature, publicKey, descriptor, signer, digest, schemeVersion }` object. During the QIP-55 port, `qrl_signTypedData` stays in the approval policy and fails locally before relay use. Its v1 encoding fixes addresses to a 32-byte slot, so a new 64-byte encoding needs a new scheme version and regenerated wallet parity vectors.
165
177
 
166
178
  `request()` returns `unknown`. First validate the exact response shape with
167
179
  `isQrlSignedMessageResult` or `isQrlSignedTypedDataResult`, then require
168
180
  `hasSigningDescriptor`. PQP3 authenticates the paired transport session. It
169
181
  does not prove that the returned ML-DSA signature is valid or that its public
170
182
  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.
183
+ call `verifyMessageForSigner` or the historical v1 `verifyTypedDataForSigner`
184
+ with the original challenge or payload and the expected QIP-55 Q + 128 hex
185
+ address.
173
186
 
174
187
  The lower-level `verifyMessageSignature` / `verifyTypedDataSignature` helpers
175
188
  verify only the supplied key and signature; they do not prove that the key
@@ -195,6 +208,18 @@ field must exactly match that approved address.
195
208
  An account string is not proof of control. For authentication, request a fresh
196
209
  challenge with `qrl_signMessage` and verify it with `verifyMessageForSigner`.
197
210
 
211
+ ## Address display
212
+
213
+ Use `formatQrlAddressFingerprint(address)` for compact QRL account labels. It
214
+ preserves checksum case and shows the first, exact middle, and final 8 hex
215
+ characters. Keep the complete raw address in links, clipboard values, QR codes,
216
+ RPC requests, and signing payloads.
217
+
218
+ ```typescript
219
+ const label = formatQrlAddressFingerprint(account);
220
+ // Q11111111...33333333...55555555
221
+ ```
222
+
198
223
  ## Sessions
199
224
 
200
225
  Established v5 sessions persist in `localStorage` for 7 days. When a user
package/dist/index.d.mts CHANGED
@@ -269,10 +269,10 @@ interface EIP6963ProviderInfoOverride {
269
269
  rdns?: string;
270
270
  }
271
271
  /**
272
- * Rich response object returned by both `qrl_signMessage` and
273
- * `qrl_signTypedData`. The signature alone is not enough to verify since
274
- * ML-DSA-87 public keys cannot be recovered from a signature, so the
275
- * wallet always returns the public key explicitly. Stateless verifiers need
272
+ * Rich response object returned by `qrl_signMessage` and retained for locked
273
+ * `qrl_signTypedData` v1 fixtures. The signature alone is not enough to
274
+ * verify since ML-DSA-87 public keys cannot be recovered from a signature,
275
+ * so the wallet returns the public key explicitly. Stateless verifiers need
276
276
  * the original payload plus the signature fields below.
277
277
  */
278
278
  interface QrlSignedResult {
@@ -286,7 +286,7 @@ interface QrlSignedResult {
286
286
  * requires it.
287
287
  */
288
288
  descriptor?: string;
289
- /** Current 41-character checksummed Q-address derived from descriptor + key. */
289
+ /** QIP-55 129-character checksummed Q-address derived from descriptor + key. */
290
290
  signer: string;
291
291
  /** 0x-hex of the 64-byte SHAKE256 digest that was signed. */
292
292
  digest: string;
@@ -303,8 +303,8 @@ interface QrlSignedMessageResult extends QrlSignedResult {
303
303
  schemeVersion: 'QRL-SIGN-MSG-v1';
304
304
  }
305
305
  /**
306
- * Response from `qrl_signTypedData`. Echoes `domain` so a stateless
307
- * verifier doesn't need to be told the domain out-of-band.
306
+ * Historical v1 response from `qrl_signTypedData`. It echoes `domain` so a
307
+ * stateless verifier does not need to be told the domain out-of-band.
308
308
  */
309
309
  interface QrlSignedTypedDataResult extends QrlSignedResult {
310
310
  schemeVersion: 'QRL-SIGN-TYPED-v1';
@@ -338,10 +338,10 @@ declare function hasSigningDescriptor(result: unknown): result is QrlSigningResu
338
338
  */
339
339
  type QrlSignMessageParams = [string, string];
340
340
  /**
341
- * `qrl_signTypedData` params: `[signer, payload]`. Payload mirrors EIP-712
342
- * shape: `{ types, primaryType, domain, message }`, but with `QRLDomain`
343
- * in place of `EIP712Domain` and SHAKE256-based hashing. See `signing/`
344
- * for the full encoder.
341
+ * Reserved `qrl_signTypedData` params: `[signer, payload]`. This interface
342
+ * describes the historical v1 payload retained by the verifier exports. The
343
+ * QIP-55 provider rejects requests until a versioned 64-byte encoding is
344
+ * defined.
345
345
  */
346
346
  interface QrlTypedDataPayload {
347
347
  types: Record<string, readonly {
@@ -887,13 +887,17 @@ declare const DIGEST_LEN = 64;
887
887
  declare function computeMessageDigest(messageBytes: Uint8Array): Uint8Array;
888
888
 
889
889
  /**
890
- * EIP-712-shaped typed-data encoding for `qrl_signTypedData` v1.
890
+ * Historical EIP-712-shaped typed-data encoding for `qrl_signTypedData` v1.
891
891
  *
892
892
  * Byte-identical algorithm with the wallet's signing/typedData.ts; this
893
893
  * file is the SDK port (only the import block differs: the SDK routes
894
894
  * SHAKE256 through src/crypto/primitives.ts, its crypto boundary). See
895
895
  * the wallet copy for the spec-level comments; behavior must stay in
896
896
  * lock-step or the cross-repo parity test fails on next CI.
897
+ *
898
+ * The v1 address slot is 32 bytes. QIP-55 address fields fail closed until a
899
+ * 64-byte encoding receives a new scheme tag and lockstep wallet fixtures.
900
+ * Address-free digests retain their existing encoding.
897
901
  */
898
902
  declare const TYPED_DATA_LIMITS: Readonly<{
899
903
  maxTypes: 32;
@@ -965,7 +969,7 @@ interface VerifyTypedDataParams {
965
969
  */
966
970
  declare function verifyTypedDataSignature({ signature, publicKey, payload, }: VerifyTypedDataParams): boolean;
967
971
  interface VerifyMessageForSignerParams extends VerifyMessageParams {
968
- /** Current Q + 40 hex address expected by the dApp. */
972
+ /** QIP-55 Q + 128 hex address expected by the dApp. */
969
973
  expectedSigner: string;
970
974
  /** Exact 3-byte wallet descriptor. Missing legacy values fail closed. */
971
975
  descriptor?: Uint8Array | string | undefined;
@@ -973,7 +977,7 @@ interface VerifyMessageForSignerParams extends VerifyMessageParams {
973
977
  /** Verify both the message signature and public-key-to-signer binding. */
974
978
  declare function verifyMessageForSigner({ expectedSigner, descriptor, signature, publicKey, messageBytes, }: VerifyMessageForSignerParams): boolean;
975
979
  interface VerifyTypedDataForSignerParams extends VerifyTypedDataParams {
976
- /** Current Q + 40 hex address expected by the dApp. */
980
+ /** QIP-55 Q + 128 hex address expected by the dApp. */
977
981
  expectedSigner: string;
978
982
  /** Exact 3-byte wallet descriptor. Missing legacy values fail closed. */
979
983
  descriptor?: Uint8Array | string | undefined;
@@ -1093,6 +1097,19 @@ declare function getAppStoreUrl(): string;
1093
1097
  */
1094
1098
  declare function attemptWalletRedirect(uri: string, timeoutMs?: number): Promise<boolean>;
1095
1099
 
1100
+ declare const QRL_ADDRESS_BYTES = 64;
1101
+ declare const QRL_ADDRESS_HEX_LENGTH: number;
1102
+ /**
1103
+ * Validate the QRL 2.0 text form. The prefix is the network's uppercase Q.
1104
+ * A case-uniform hex body is accepted for compatibility. Mixed-case input
1105
+ * must carry the canonical SHAKE256 QIP-55 checksum.
1106
+ */
1107
+ declare function isCurrentQrlAddress(value: unknown): value is string;
1108
+ /** Stable visual fingerprint for long QRL addresses. */
1109
+ declare function formatQrlAddressFingerprint(address: string): string;
1110
+ /** Format exactly 64 address bytes as the canonical mixed-case QIP-55 form. */
1111
+ declare function qip55AddressFromBytes(address: Uint8Array): string;
1112
+
1096
1113
  declare const DEFAULT_RELAY_URL = "https://qrlwallet.com";
1097
1114
  declare const PROTOCOL_VERSION = 3;
1098
1115
  declare const PAIRING_CAPABILITY_LEN = 32;
@@ -1118,4 +1135,4 @@ declare function isValidJsonRpcMethod(value: unknown): value is string;
1118
1135
  */
1119
1136
  declare function classifyRpcMethod(method: string): RpcMethodPolicy;
1120
1137
 
1121
- export { type AckMessage, BLOB_LEN, CAP_LEN, CID_LEN, ConnectionManager, ConnectionStatus, type DAppMetadata, type DAppSession, DEFAULT_RELAY_URL, DIGEST_LEN, type Domain, type EIP6963ProviderInfoOverride, EXPLICITLY_UNSUPPORTED_METHODS, FP_LEN, type InitiatedKeyExchange, type JsonRpcRequest, type JsonRpcResponse, KeyExchange, KeyExchangeMessageType, MAX_CONNECTION_URI_LENGTH, MAX_JSON_RPC_ID_LENGTH, MAX_JSON_RPC_METHOD_LENGTH, MAX_RELAY_URL_LENGTH, ML_DSA_87_PUBLIC_KEY_BYTES, ML_DSA_87_SIGNATURE_BYTES, ML_DSA_DESCRIPTOR_BYTES, type Message, MessageType, PAIRING_CAPABILITY_LEN, PROTOCOL_VERSION, type ParsedURI, type PendingRequest, type PersistedSession, type ProviderEvents, QRLConnectProvider as QRLConnect, type QRLConnectOptions, QRLConnectProvider, QRL_CONNECT_PROVIDER_INFO, type QrlSignMessageParams, type QrlSignTypedDataParams, type QrlSignedMessageResult, type QrlSignedResult, type QrlSignedResultWithDescriptor, type QrlSignedTypedDataResult, type QrlSigningResult, type QrlSigningSchemeVersion, type QrlTypedDataPayload, RESTRICTED_METHODS, type RelayMessage, type RpcMethodPolicy, SCHEME_TAG_MSG, SCHEME_TAG_TYPED, SCHEME_VERSION_MSG, SCHEME_VERSION_TYPED, type Session, SocketClient, type StructDef, type SynAckMessage, TYPED_DATA_LIMITS, type TypeMap, type TypedDataPayload, type TypedField, UNRESTRICTED_METHODS, type VerifyMessageForSignerParams, type VerifyMessageParams, type VerifyTypedDataForSignerParams, type VerifyTypedDataParams, attemptWalletRedirect, bytesToHex, cidFromString, cidRandom, cidToString, classifyRpcMethod, computeFingerprint, computeMessageDigest, computeTypedDataDigest, concatBytes, encodeField, encodeType, fingerprintEquals, generateConnectionURI, getAppStoreUrl, hasSigningDescriptor, hashStruct, hexToBytes, isMobileBrowser, isQrlSignedMessageResult, isQrlSignedResult, isQrlSignedTypedDataResult, isValidJsonRpcId, isValidJsonRpcMethod, normalizeRelayUrl, parseConnectionURI, typeHash, verifyMessage, verifyMessageForSigner, verifyMessageSignature, verifyTypedData, verifyTypedDataForSigner, verifyTypedDataSignature };
1138
+ export { type AckMessage, BLOB_LEN, CAP_LEN, CID_LEN, ConnectionManager, ConnectionStatus, type DAppMetadata, type DAppSession, DEFAULT_RELAY_URL, DIGEST_LEN, type Domain, type EIP6963ProviderInfoOverride, EXPLICITLY_UNSUPPORTED_METHODS, FP_LEN, type InitiatedKeyExchange, type JsonRpcRequest, type JsonRpcResponse, KeyExchange, KeyExchangeMessageType, MAX_CONNECTION_URI_LENGTH, MAX_JSON_RPC_ID_LENGTH, MAX_JSON_RPC_METHOD_LENGTH, MAX_RELAY_URL_LENGTH, ML_DSA_87_PUBLIC_KEY_BYTES, ML_DSA_87_SIGNATURE_BYTES, ML_DSA_DESCRIPTOR_BYTES, type Message, MessageType, PAIRING_CAPABILITY_LEN, PROTOCOL_VERSION, type ParsedURI, type PendingRequest, type PersistedSession, type ProviderEvents, QRLConnectProvider as QRLConnect, type QRLConnectOptions, QRLConnectProvider, QRL_ADDRESS_BYTES, QRL_ADDRESS_HEX_LENGTH, QRL_CONNECT_PROVIDER_INFO, type QrlSignMessageParams, type QrlSignTypedDataParams, type QrlSignedMessageResult, type QrlSignedResult, type QrlSignedResultWithDescriptor, type QrlSignedTypedDataResult, type QrlSigningResult, type QrlSigningSchemeVersion, type QrlTypedDataPayload, RESTRICTED_METHODS, type RelayMessage, type RpcMethodPolicy, SCHEME_TAG_MSG, SCHEME_TAG_TYPED, SCHEME_VERSION_MSG, SCHEME_VERSION_TYPED, type Session, SocketClient, type StructDef, type SynAckMessage, TYPED_DATA_LIMITS, type TypeMap, type TypedDataPayload, type TypedField, UNRESTRICTED_METHODS, type VerifyMessageForSignerParams, type VerifyMessageParams, type VerifyTypedDataForSignerParams, type VerifyTypedDataParams, attemptWalletRedirect, bytesToHex, cidFromString, cidRandom, cidToString, classifyRpcMethod, computeFingerprint, computeMessageDigest, computeTypedDataDigest, concatBytes, encodeField, encodeType, fingerprintEquals, formatQrlAddressFingerprint, generateConnectionURI, getAppStoreUrl, hasSigningDescriptor, hashStruct, hexToBytes, isCurrentQrlAddress, isMobileBrowser, isQrlSignedMessageResult, isQrlSignedResult, isQrlSignedTypedDataResult, isValidJsonRpcId, isValidJsonRpcMethod, normalizeRelayUrl, parseConnectionURI, qip55AddressFromBytes, typeHash, verifyMessage, verifyMessageForSigner, verifyMessageSignature, verifyTypedData, verifyTypedDataForSigner, verifyTypedDataSignature };
package/dist/index.d.ts CHANGED
@@ -269,10 +269,10 @@ interface EIP6963ProviderInfoOverride {
269
269
  rdns?: string;
270
270
  }
271
271
  /**
272
- * Rich response object returned by both `qrl_signMessage` and
273
- * `qrl_signTypedData`. The signature alone is not enough to verify since
274
- * ML-DSA-87 public keys cannot be recovered from a signature, so the
275
- * wallet always returns the public key explicitly. Stateless verifiers need
272
+ * Rich response object returned by `qrl_signMessage` and retained for locked
273
+ * `qrl_signTypedData` v1 fixtures. The signature alone is not enough to
274
+ * verify since ML-DSA-87 public keys cannot be recovered from a signature,
275
+ * so the wallet returns the public key explicitly. Stateless verifiers need
276
276
  * the original payload plus the signature fields below.
277
277
  */
278
278
  interface QrlSignedResult {
@@ -286,7 +286,7 @@ interface QrlSignedResult {
286
286
  * requires it.
287
287
  */
288
288
  descriptor?: string;
289
- /** Current 41-character checksummed Q-address derived from descriptor + key. */
289
+ /** QIP-55 129-character checksummed Q-address derived from descriptor + key. */
290
290
  signer: string;
291
291
  /** 0x-hex of the 64-byte SHAKE256 digest that was signed. */
292
292
  digest: string;
@@ -303,8 +303,8 @@ interface QrlSignedMessageResult extends QrlSignedResult {
303
303
  schemeVersion: 'QRL-SIGN-MSG-v1';
304
304
  }
305
305
  /**
306
- * Response from `qrl_signTypedData`. Echoes `domain` so a stateless
307
- * verifier doesn't need to be told the domain out-of-band.
306
+ * Historical v1 response from `qrl_signTypedData`. It echoes `domain` so a
307
+ * stateless verifier does not need to be told the domain out-of-band.
308
308
  */
309
309
  interface QrlSignedTypedDataResult extends QrlSignedResult {
310
310
  schemeVersion: 'QRL-SIGN-TYPED-v1';
@@ -338,10 +338,10 @@ declare function hasSigningDescriptor(result: unknown): result is QrlSigningResu
338
338
  */
339
339
  type QrlSignMessageParams = [string, string];
340
340
  /**
341
- * `qrl_signTypedData` params: `[signer, payload]`. Payload mirrors EIP-712
342
- * shape: `{ types, primaryType, domain, message }`, but with `QRLDomain`
343
- * in place of `EIP712Domain` and SHAKE256-based hashing. See `signing/`
344
- * for the full encoder.
341
+ * Reserved `qrl_signTypedData` params: `[signer, payload]`. This interface
342
+ * describes the historical v1 payload retained by the verifier exports. The
343
+ * QIP-55 provider rejects requests until a versioned 64-byte encoding is
344
+ * defined.
345
345
  */
346
346
  interface QrlTypedDataPayload {
347
347
  types: Record<string, readonly {
@@ -887,13 +887,17 @@ declare const DIGEST_LEN = 64;
887
887
  declare function computeMessageDigest(messageBytes: Uint8Array): Uint8Array;
888
888
 
889
889
  /**
890
- * EIP-712-shaped typed-data encoding for `qrl_signTypedData` v1.
890
+ * Historical EIP-712-shaped typed-data encoding for `qrl_signTypedData` v1.
891
891
  *
892
892
  * Byte-identical algorithm with the wallet's signing/typedData.ts; this
893
893
  * file is the SDK port (only the import block differs: the SDK routes
894
894
  * SHAKE256 through src/crypto/primitives.ts, its crypto boundary). See
895
895
  * the wallet copy for the spec-level comments; behavior must stay in
896
896
  * lock-step or the cross-repo parity test fails on next CI.
897
+ *
898
+ * The v1 address slot is 32 bytes. QIP-55 address fields fail closed until a
899
+ * 64-byte encoding receives a new scheme tag and lockstep wallet fixtures.
900
+ * Address-free digests retain their existing encoding.
897
901
  */
898
902
  declare const TYPED_DATA_LIMITS: Readonly<{
899
903
  maxTypes: 32;
@@ -965,7 +969,7 @@ interface VerifyTypedDataParams {
965
969
  */
966
970
  declare function verifyTypedDataSignature({ signature, publicKey, payload, }: VerifyTypedDataParams): boolean;
967
971
  interface VerifyMessageForSignerParams extends VerifyMessageParams {
968
- /** Current Q + 40 hex address expected by the dApp. */
972
+ /** QIP-55 Q + 128 hex address expected by the dApp. */
969
973
  expectedSigner: string;
970
974
  /** Exact 3-byte wallet descriptor. Missing legacy values fail closed. */
971
975
  descriptor?: Uint8Array | string | undefined;
@@ -973,7 +977,7 @@ interface VerifyMessageForSignerParams extends VerifyMessageParams {
973
977
  /** Verify both the message signature and public-key-to-signer binding. */
974
978
  declare function verifyMessageForSigner({ expectedSigner, descriptor, signature, publicKey, messageBytes, }: VerifyMessageForSignerParams): boolean;
975
979
  interface VerifyTypedDataForSignerParams extends VerifyTypedDataParams {
976
- /** Current Q + 40 hex address expected by the dApp. */
980
+ /** QIP-55 Q + 128 hex address expected by the dApp. */
977
981
  expectedSigner: string;
978
982
  /** Exact 3-byte wallet descriptor. Missing legacy values fail closed. */
979
983
  descriptor?: Uint8Array | string | undefined;
@@ -1093,6 +1097,19 @@ declare function getAppStoreUrl(): string;
1093
1097
  */
1094
1098
  declare function attemptWalletRedirect(uri: string, timeoutMs?: number): Promise<boolean>;
1095
1099
 
1100
+ declare const QRL_ADDRESS_BYTES = 64;
1101
+ declare const QRL_ADDRESS_HEX_LENGTH: number;
1102
+ /**
1103
+ * Validate the QRL 2.0 text form. The prefix is the network's uppercase Q.
1104
+ * A case-uniform hex body is accepted for compatibility. Mixed-case input
1105
+ * must carry the canonical SHAKE256 QIP-55 checksum.
1106
+ */
1107
+ declare function isCurrentQrlAddress(value: unknown): value is string;
1108
+ /** Stable visual fingerprint for long QRL addresses. */
1109
+ declare function formatQrlAddressFingerprint(address: string): string;
1110
+ /** Format exactly 64 address bytes as the canonical mixed-case QIP-55 form. */
1111
+ declare function qip55AddressFromBytes(address: Uint8Array): string;
1112
+
1096
1113
  declare const DEFAULT_RELAY_URL = "https://qrlwallet.com";
1097
1114
  declare const PROTOCOL_VERSION = 3;
1098
1115
  declare const PAIRING_CAPABILITY_LEN = 32;
@@ -1118,4 +1135,4 @@ declare function isValidJsonRpcMethod(value: unknown): value is string;
1118
1135
  */
1119
1136
  declare function classifyRpcMethod(method: string): RpcMethodPolicy;
1120
1137
 
1121
- export { type AckMessage, BLOB_LEN, CAP_LEN, CID_LEN, ConnectionManager, ConnectionStatus, type DAppMetadata, type DAppSession, DEFAULT_RELAY_URL, DIGEST_LEN, type Domain, type EIP6963ProviderInfoOverride, EXPLICITLY_UNSUPPORTED_METHODS, FP_LEN, type InitiatedKeyExchange, type JsonRpcRequest, type JsonRpcResponse, KeyExchange, KeyExchangeMessageType, MAX_CONNECTION_URI_LENGTH, MAX_JSON_RPC_ID_LENGTH, MAX_JSON_RPC_METHOD_LENGTH, MAX_RELAY_URL_LENGTH, ML_DSA_87_PUBLIC_KEY_BYTES, ML_DSA_87_SIGNATURE_BYTES, ML_DSA_DESCRIPTOR_BYTES, type Message, MessageType, PAIRING_CAPABILITY_LEN, PROTOCOL_VERSION, type ParsedURI, type PendingRequest, type PersistedSession, type ProviderEvents, QRLConnectProvider as QRLConnect, type QRLConnectOptions, QRLConnectProvider, QRL_CONNECT_PROVIDER_INFO, type QrlSignMessageParams, type QrlSignTypedDataParams, type QrlSignedMessageResult, type QrlSignedResult, type QrlSignedResultWithDescriptor, type QrlSignedTypedDataResult, type QrlSigningResult, type QrlSigningSchemeVersion, type QrlTypedDataPayload, RESTRICTED_METHODS, type RelayMessage, type RpcMethodPolicy, SCHEME_TAG_MSG, SCHEME_TAG_TYPED, SCHEME_VERSION_MSG, SCHEME_VERSION_TYPED, type Session, SocketClient, type StructDef, type SynAckMessage, TYPED_DATA_LIMITS, type TypeMap, type TypedDataPayload, type TypedField, UNRESTRICTED_METHODS, type VerifyMessageForSignerParams, type VerifyMessageParams, type VerifyTypedDataForSignerParams, type VerifyTypedDataParams, attemptWalletRedirect, bytesToHex, cidFromString, cidRandom, cidToString, classifyRpcMethod, computeFingerprint, computeMessageDigest, computeTypedDataDigest, concatBytes, encodeField, encodeType, fingerprintEquals, generateConnectionURI, getAppStoreUrl, hasSigningDescriptor, hashStruct, hexToBytes, isMobileBrowser, isQrlSignedMessageResult, isQrlSignedResult, isQrlSignedTypedDataResult, isValidJsonRpcId, isValidJsonRpcMethod, normalizeRelayUrl, parseConnectionURI, typeHash, verifyMessage, verifyMessageForSigner, verifyMessageSignature, verifyTypedData, verifyTypedDataForSigner, verifyTypedDataSignature };
1138
+ export { type AckMessage, BLOB_LEN, CAP_LEN, CID_LEN, ConnectionManager, ConnectionStatus, type DAppMetadata, type DAppSession, DEFAULT_RELAY_URL, DIGEST_LEN, type Domain, type EIP6963ProviderInfoOverride, EXPLICITLY_UNSUPPORTED_METHODS, FP_LEN, type InitiatedKeyExchange, type JsonRpcRequest, type JsonRpcResponse, KeyExchange, KeyExchangeMessageType, MAX_CONNECTION_URI_LENGTH, MAX_JSON_RPC_ID_LENGTH, MAX_JSON_RPC_METHOD_LENGTH, MAX_RELAY_URL_LENGTH, ML_DSA_87_PUBLIC_KEY_BYTES, ML_DSA_87_SIGNATURE_BYTES, ML_DSA_DESCRIPTOR_BYTES, type Message, MessageType, PAIRING_CAPABILITY_LEN, PROTOCOL_VERSION, type ParsedURI, type PendingRequest, type PersistedSession, type ProviderEvents, QRLConnectProvider as QRLConnect, type QRLConnectOptions, QRLConnectProvider, QRL_ADDRESS_BYTES, QRL_ADDRESS_HEX_LENGTH, QRL_CONNECT_PROVIDER_INFO, type QrlSignMessageParams, type QrlSignTypedDataParams, type QrlSignedMessageResult, type QrlSignedResult, type QrlSignedResultWithDescriptor, type QrlSignedTypedDataResult, type QrlSigningResult, type QrlSigningSchemeVersion, type QrlTypedDataPayload, RESTRICTED_METHODS, type RelayMessage, type RpcMethodPolicy, SCHEME_TAG_MSG, SCHEME_TAG_TYPED, SCHEME_VERSION_MSG, SCHEME_VERSION_TYPED, type Session, SocketClient, type StructDef, type SynAckMessage, TYPED_DATA_LIMITS, type TypeMap, type TypedDataPayload, type TypedField, UNRESTRICTED_METHODS, type VerifyMessageForSignerParams, type VerifyMessageParams, type VerifyTypedDataForSignerParams, type VerifyTypedDataParams, attemptWalletRedirect, bytesToHex, cidFromString, cidRandom, cidToString, classifyRpcMethod, computeFingerprint, computeMessageDigest, computeTypedDataDigest, concatBytes, encodeField, encodeType, fingerprintEquals, formatQrlAddressFingerprint, generateConnectionURI, getAppStoreUrl, hasSigningDescriptor, hashStruct, hexToBytes, isCurrentQrlAddress, isMobileBrowser, isQrlSignedMessageResult, isQrlSignedResult, isQrlSignedTypedDataResult, isValidJsonRpcId, isValidJsonRpcMethod, normalizeRelayUrl, parseConnectionURI, qip55AddressFromBytes, typeHash, verifyMessage, verifyMessageForSigner, verifyMessageSignature, verifyTypedData, verifyTypedDataForSigner, verifyTypedDataSignature };
package/dist/index.js CHANGED
@@ -53,6 +53,8 @@ __export(index_exports, {
53
53
  PROTOCOL_VERSION: () => PROTOCOL_VERSION,
54
54
  QRLConnect: () => QRLConnectProvider,
55
55
  QRLConnectProvider: () => QRLConnectProvider,
56
+ QRL_ADDRESS_BYTES: () => QRL_ADDRESS_BYTES,
57
+ QRL_ADDRESS_HEX_LENGTH: () => QRL_ADDRESS_HEX_LENGTH,
56
58
  QRL_CONNECT_PROVIDER_INFO: () => QRL_CONNECT_PROVIDER_INFO,
57
59
  RESTRICTED_METHODS: () => RESTRICTED_METHODS,
58
60
  SCHEME_TAG_MSG: () => SCHEME_TAG_MSG,
@@ -75,11 +77,13 @@ __export(index_exports, {
75
77
  encodeField: () => encodeField,
76
78
  encodeType: () => encodeType,
77
79
  fingerprintEquals: () => fingerprintEquals,
80
+ formatQrlAddressFingerprint: () => formatQrlAddressFingerprint,
78
81
  generateConnectionURI: () => generateConnectionURI,
79
82
  getAppStoreUrl: () => getAppStoreUrl,
80
83
  hasSigningDescriptor: () => hasSigningDescriptor,
81
84
  hashStruct: () => hashStruct,
82
85
  hexToBytes: () => hexToBytes,
86
+ isCurrentQrlAddress: () => isCurrentQrlAddress,
83
87
  isMobileBrowser: () => isMobileBrowser,
84
88
  isQrlSignedMessageResult: () => isQrlSignedMessageResult,
85
89
  isQrlSignedResult: () => isQrlSignedResult,
@@ -88,6 +92,7 @@ __export(index_exports, {
88
92
  isValidJsonRpcMethod: () => isValidJsonRpcMethod,
89
93
  normalizeRelayUrl: () => normalizeRelayUrl,
90
94
  parseConnectionURI: () => parseConnectionURI,
95
+ qip55AddressFromBytes: () => qip55AddressFromBytes,
91
96
  typeHash: () => typeHash,
92
97
  verifyMessage: () => verifyMessage,
93
98
  verifyMessageForSigner: () => verifyMessageForSigner,
@@ -209,6 +214,54 @@ function constantTimeEquals(a, b) {
209
214
  return d === 0;
210
215
  }
211
216
 
217
+ // src/utils/qrlAddress.ts
218
+ var QRL_ADDRESS_BYTES = 64;
219
+ var QRL_ADDRESS_HEX_LENGTH = QRL_ADDRESS_BYTES * 2;
220
+ var QRL_ADDRESS_RE = new RegExp(`^Q[0-9a-fA-F]{${QRL_ADDRESS_HEX_LENGTH}}$`);
221
+ var textEncoder = new TextEncoder();
222
+ function checksummedHex(lowerHex) {
223
+ const hash = shake256Digest(textEncoder.encode(lowerHex), QRL_ADDRESS_BYTES);
224
+ let result = "";
225
+ for (let index = 0; index < lowerHex.length; index++) {
226
+ const char = lowerHex[index] ?? "";
227
+ if (char >= "a" && char <= "f") {
228
+ const byte = hash[index >> 1] ?? 0;
229
+ const nibble = (index & 1) === 0 ? byte >> 4 : byte & 15;
230
+ result += nibble >= 8 ? char.toUpperCase() : char;
231
+ } else {
232
+ result += char;
233
+ }
234
+ }
235
+ return result;
236
+ }
237
+ function isCurrentQrlAddress(value) {
238
+ if (typeof value !== "string" || !QRL_ADDRESS_RE.test(value)) return false;
239
+ const body = value.slice(1);
240
+ const lower = body.toLowerCase();
241
+ if (body === lower || body === body.toUpperCase()) return true;
242
+ return body === checksummedHex(lower);
243
+ }
244
+ function formatQrlAddressFingerprint(address) {
245
+ if (!/^Q(?:[0-9a-fA-F]{40}|[0-9a-fA-F]{128})$/.test(address)) return address;
246
+ const body = address.slice(1);
247
+ const segmentLength = 8;
248
+ if (body.length < segmentLength * 3) return address;
249
+ const middleStart = Math.floor((body.length - segmentLength) / 2);
250
+ return [
251
+ `Q${body.slice(0, segmentLength)}`,
252
+ body.slice(middleStart, middleStart + segmentLength),
253
+ body.slice(-segmentLength)
254
+ ].join("...");
255
+ }
256
+ function qip55AddressFromBytes(address) {
257
+ if (!(address instanceof Uint8Array) || address.length !== QRL_ADDRESS_BYTES) {
258
+ throw new Error(`address must be exactly ${QRL_ADDRESS_BYTES} bytes`);
259
+ }
260
+ let lower = "";
261
+ for (const byte of address) lower += byte.toString(16).padStart(2, "0");
262
+ return `Q${checksummedHex(lower)}`;
263
+ }
264
+
212
265
  // src/config.ts
213
266
  var DEFAULT_RELAY_URL = "https://qrlwallet.com";
214
267
  var RELAY_PATH = "/relay";
@@ -222,10 +275,6 @@ var MAX_JSON_RPC_ID_LENGTH = 128;
222
275
  var MAX_JSON_RPC_METHOD_LENGTH = 128;
223
276
  var MAX_RELAY_URL_LENGTH = 2048;
224
277
  var RECONNECT_WALLET_PROBE_MS = 12 * 1e3;
225
- var CURRENT_QRL_ADDRESS_RE = /^Q[0-9a-fA-F]{40}$/;
226
- function isCurrentQrlAddress(value) {
227
- return typeof value === "string" && CURRENT_QRL_ADDRESS_RE.test(value);
228
- }
229
278
  function isExplicitLoopbackHostname(value) {
230
279
  const hostname = value.toLowerCase();
231
280
  return hostname === "localhost" || hostname.endsWith(".localhost") || hostname === "127.0.0.1" || hostname === "::1" || hostname === "[::1]";
@@ -301,9 +350,9 @@ function classifyRpcMethod(method) {
301
350
  }
302
351
 
303
352
  // src/PQCrypto.ts
304
- var textEncoder = new TextEncoder();
305
- var LABEL = textEncoder.encode("pq-pair/v3");
306
- var LABEL_AEAD_SUFFIX = textEncoder.encode(" aead");
353
+ var textEncoder2 = new TextEncoder();
354
+ var LABEL = textEncoder2.encode("pq-pair/v3");
355
+ var LABEL_AEAD_SUFFIX = textEncoder2.encode(" aead");
307
356
  var DIR_DAPP_TX = new Uint8Array([0, 0, 0, 1]);
308
357
  var DIR_WALLET_TX = new Uint8Array([0, 0, 0, 2]);
309
358
  var ML_KEM_768_PK_LEN = 1184;
@@ -502,7 +551,7 @@ function isSigningDescriptor(value) {
502
551
  return typeof value === "string" && /^0x01[0-9a-fA-F]{4}$/.test(value);
503
552
  }
504
553
  function hasValidSigningFields(record) {
505
- return isFixedHex(record.signature, SIGNATURE_HEX_BYTES) && isFixedHex(record.publicKey, PUBLIC_KEY_HEX_BYTES) && (!hasOwn(record, "descriptor") || isSigningDescriptor(record.descriptor)) && typeof record.signer === "string" && /^Q[0-9a-fA-F]{40}$/.test(record.signer) && isFixedHex(record.digest, DIGEST_HEX_BYTES);
554
+ return isFixedHex(record.signature, SIGNATURE_HEX_BYTES) && isFixedHex(record.publicKey, PUBLIC_KEY_HEX_BYTES) && (!hasOwn(record, "descriptor") || isSigningDescriptor(record.descriptor)) && typeof record.signer === "string" && isCurrentQrlAddress(record.signer) && isFixedHex(record.digest, DIGEST_HEX_BYTES);
506
555
  }
507
556
  function isQrlSignedMessageResult(value) {
508
557
  try {
@@ -551,10 +600,10 @@ function error(tag, ...args) {
551
600
  }
552
601
 
553
602
  // src/KeyExchange.ts
554
- var textEncoder2 = new TextEncoder();
603
+ var textEncoder3 = new TextEncoder();
555
604
  var textDecoder = new TextDecoder();
556
- var HELLO_WALLET = textEncoder2.encode("hello/wallet/v1");
557
- var HELLO_DAPP = textEncoder2.encode("hello/dapp/v1");
605
+ var HELLO_WALLET = textEncoder3.encode("hello/wallet/v1");
606
+ var HELLO_DAPP = textEncoder3.encode("hello/dapp/v1");
558
607
  var AES_GCM_TAG_LEN = 16;
559
608
  var SYNACK_C0_LEN = HELLO_WALLET.length + AES_GCM_TAG_LEN;
560
609
  var ACK_C1_LEN = HELLO_DAPP.length + AES_GCM_TAG_LEN;
@@ -828,7 +877,7 @@ var KeyExchange = class extends import_eventemitter3.default {
828
877
  throw new Error("KeyExchange: send counter exhausted");
829
878
  }
830
879
  const seq = session.sendSeq++;
831
- const pt = textEncoder2.encode(data);
880
+ const pt = textEncoder3.encode(data);
832
881
  try {
833
882
  const ct = await seal(session.key, session.sendDir, seq, session.htx, pt);
834
883
  if (this.stateGeneration !== generation || this.session !== session) {
@@ -3272,10 +3321,14 @@ function typeHash(primary, types) {
3272
3321
  return shake256Digest(new TextEncoder().encode(encodeType(primary, types)), DIGEST_LEN);
3273
3322
  }
3274
3323
  function parseQAddress(addr) {
3275
- if (typeof addr !== "string" || !/^Q[0-9a-fA-F]{40}$/.test(addr)) {
3276
- throw new Error(`invalid Q-address: ${addr}`);
3324
+ if (!isCurrentQrlAddress(addr)) {
3325
+ throw new Error(`invalid Q-address: ${String(addr)}`);
3326
+ }
3327
+ const bytes = hexToBytes("0x" + addr.slice(1).toLowerCase());
3328
+ if (bytes.length > SLOT) {
3329
+ throw new Error("qrl_signTypedData v1 does not support QIP-55 address fields");
3277
3330
  }
3278
- return hexToBytes("0x" + addr.slice(1).toLowerCase());
3331
+ return bytes;
3279
3332
  }
3280
3333
  function padLeft32(bytes) {
3281
3334
  if (bytes.length > SLOT) throw new Error("cannot pad: bytes > 32");
@@ -3592,7 +3645,7 @@ function requestedSwitchChainId(params) {
3592
3645
  }
3593
3646
  return canonicalChainId2(request.chainId);
3594
3647
  }
3595
- var TRANSACTION_FIELDS = /* @__PURE__ */ new Set(["from", "to", "value", "gas", "data"]);
3648
+ var TRANSACTION_FIELDS = /* @__PURE__ */ new Set(["from", "to", "value", "gas", "data", "chainId"]);
3596
3649
  var QRL_TRANSACTION_MAX_DATA_BYTES = 128 * 1024;
3597
3650
  var QRL_TRANSACTION_MAX_QUANTITY_HEX_DIGITS = 64;
3598
3651
  var RPC_QUANTITY_RE = /^0x(?:0|[1-9a-fA-F][0-9a-fA-F]*)$/;
@@ -3607,8 +3660,11 @@ function validateRpcQuantity(value, field) {
3607
3660
  if (field === "gas" && BigInt(value) > BigInt(Number.MAX_SAFE_INTEGER)) {
3608
3661
  throw new Error("transaction gas exceeds the wallet safe-integer limit");
3609
3662
  }
3663
+ if (field === "chainId" && BigInt(value) === 0n) {
3664
+ throw new Error("transaction chainId must be positive");
3665
+ }
3610
3666
  }
3611
- function validateTransactionRequest(method, params, authorizedAccount) {
3667
+ function validateTransactionRequest(method, params, authorizedAccount, currentChainId) {
3612
3668
  if (params?.length !== 1 || !isRecordObj(params[0])) {
3613
3669
  throw new Error(`${method} requires exactly one transaction object`);
3614
3670
  }
@@ -3626,6 +3682,12 @@ function validateTransactionRequest(method, params, authorizedAccount) {
3626
3682
  }
3627
3683
  if ("value" in tx) validateRpcQuantity(tx.value, "value");
3628
3684
  if ("gas" in tx) validateRpcQuantity(tx.gas, "gas");
3685
+ if ("chainId" in tx) {
3686
+ validateRpcQuantity(tx.chainId, "chainId");
3687
+ if (canonicalChainId2(tx.chainId) !== canonicalChainId2(currentChainId)) {
3688
+ throw new Error("transaction chainId does not match the connected wallet");
3689
+ }
3690
+ }
3629
3691
  if ("data" in tx && (typeof tx.data !== "string" || tx.data.length > QRL_TRANSACTION_MAX_DATA_BYTES * 2 + 2 || !/^0x(?:[0-9a-fA-F]{2})*$/.test(tx.data))) {
3630
3692
  throw new Error("transaction data must be bounded 0x-prefixed bytes");
3631
3693
  }
@@ -3643,7 +3705,7 @@ function snapshotRequestParams(params) {
3643
3705
  throw new Error(`Request params must be JSON-serializable: ${detail}`);
3644
3706
  }
3645
3707
  }
3646
- function validateRestrictedRequest(method, params, authorizedAccounts) {
3708
+ function validateRestrictedRequest(method, params, authorizedAccounts, currentChainId) {
3647
3709
  if (method === "qrl_requestAccounts") {
3648
3710
  if (params !== void 0 && params.length !== 0) {
3649
3711
  throw new Error("qrl_requestAccounts does not accept parameters");
@@ -3658,7 +3720,7 @@ function validateRestrictedRequest(method, params, authorizedAccounts) {
3658
3720
  throw new Error("No authorized account: call qrl_requestAccounts first");
3659
3721
  }
3660
3722
  if (method === "qrl_sendTransaction" || method === "qrl_signTransaction") {
3661
- validateTransactionRequest(method, params, authorizedAccount);
3723
+ validateTransactionRequest(method, params, authorizedAccount, currentChainId);
3662
3724
  return void 0;
3663
3725
  }
3664
3726
  if (method === "qrl_signMessage") {
@@ -3687,8 +3749,9 @@ function validateRestrictedRequest(method, params, authorizedAccounts) {
3687
3749
  if (params[0] !== authorizedAccount) {
3688
3750
  throw new Error("qrl_signTypedData signer is not the authorized account");
3689
3751
  }
3690
- computeTypedDataDigest(params[1]);
3691
- return void 0;
3752
+ throw new Error(
3753
+ "qrl_signTypedData is unavailable for QIP-55 until the 64-byte word encoding and signing scheme version are finalized"
3754
+ );
3692
3755
  }
3693
3756
  function requiresAuthorizedAccount(method) {
3694
3757
  return method === "qrl_sendTransaction" || method === "qrl_signTransaction" || method === "qrl_signMessage" || method === "qrl_signTypedData";
@@ -3896,7 +3959,12 @@ var QRLConnectProvider = class extends import_eventemitter34.default {
3896
3959
  }
3897
3960
  this.pendingRequests.delete(response.id);
3898
3961
  if (response.error) {
3899
- pending.reject(new Error(response.error.message || "Request failed"));
3962
+ pending.reject(
3963
+ Object.assign(new Error(response.error.message || "Request failed"), {
3964
+ code: response.error.code,
3965
+ ...response.error.data === void 0 ? {} : { data: response.error.data }
3966
+ })
3967
+ );
3900
3968
  return;
3901
3969
  }
3902
3970
  try {
@@ -4000,7 +4068,8 @@ var QRLConnectProvider = class extends import_eventemitter34.default {
4000
4068
  const expectedChainId = validateRestrictedRequest(
4001
4069
  method,
4002
4070
  stableParams,
4003
- this.connectionManager.getAccounts()
4071
+ this.connectionManager.getAccounts(),
4072
+ this.connectionManager.getChainId()
4004
4073
  );
4005
4074
  return this.enqueueRestrictedRequest(method, stableParams, expectedChainId, generation);
4006
4075
  }
@@ -4065,6 +4134,14 @@ var QRLConnectProvider = class extends import_eventemitter34.default {
4065
4134
  throw new Error("Not connected to QRL Wallet");
4066
4135
  }
4067
4136
  }
4137
+ if (restricted) {
4138
+ validateRestrictedRequest(
4139
+ method,
4140
+ params,
4141
+ this.connectionManager.getAccounts(),
4142
+ this.connectionManager.getChainId()
4143
+ );
4144
+ }
4068
4145
  if (this.requestCounter >= Number.MAX_SAFE_INTEGER) {
4069
4146
  throw new Error("JSON-RPC request id counter exhausted");
4070
4147
  }
@@ -4143,9 +4220,10 @@ var QRLConnectProvider = class extends import_eventemitter34.default {
4143
4220
  }
4144
4221
  // ── In-flight persistence (survive the return-redirect reload) ──────
4145
4222
  readInflight() {
4146
- if (typeof localStorage === "undefined") return [];
4223
+ const storage = getBrowserStorage();
4224
+ if (!storage) return [];
4147
4225
  try {
4148
- const raw = localStorage.getItem(this.inflightKey);
4226
+ const raw = storage.getItem(this.inflightKey);
4149
4227
  if (!raw) return [];
4150
4228
  const parsed = JSON.parse(raw);
4151
4229
  if (!Array.isArray(parsed)) return [];
@@ -4161,12 +4239,13 @@ var QRLConnectProvider = class extends import_eventemitter34.default {
4161
4239
  }
4162
4240
  }
4163
4241
  writeInflight(records) {
4164
- if (typeof localStorage === "undefined") return;
4242
+ const storage = getBrowserStorage();
4243
+ if (!storage) return;
4165
4244
  try {
4166
4245
  if (records.length === 0) {
4167
- localStorage.removeItem(this.inflightKey);
4246
+ storage.removeItem(this.inflightKey);
4168
4247
  } else {
4169
- localStorage.setItem(this.inflightKey, JSON.stringify(records));
4248
+ storage.setItem(this.inflightKey, JSON.stringify(records));
4170
4249
  }
4171
4250
  } catch {
4172
4251
  }
@@ -4300,7 +4379,6 @@ var ML_DSA_87_DESCRIPTOR_TYPE = 1;
4300
4379
  var ML_DSA_DESCRIPTOR_BYTES = 3;
4301
4380
  var ML_DSA_87_PUBLIC_KEY_BYTES = 2592;
4302
4381
  var ML_DSA_87_SIGNATURE_BYTES = 4627;
4303
- var CURRENT_QRL_ADDRESS_BYTES = 20;
4304
4382
  function bytesOrHex(v) {
4305
4383
  if (v instanceof Uint8Array) return new Uint8Array(v);
4306
4384
  if (typeof v === "string") return hexToBytes(v);
@@ -4357,11 +4435,8 @@ function publicKeyMatchesSigner(expectedSigner, descriptor, publicKey) {
4357
4435
  if (descriptor[0] !== ML_DSA_87_DESCRIPTOR_TYPE) {
4358
4436
  return false;
4359
4437
  }
4360
- const addressBytes = shake256Digest(
4361
- concatBytes(descriptor, publicKey),
4362
- CURRENT_QRL_ADDRESS_BYTES
4363
- );
4364
- const derived = `Q${bytesToHex(addressBytes).slice(2)}`;
4438
+ const addressBytes = shake256Digest(concatBytes(descriptor, publicKey), QRL_ADDRESS_BYTES);
4439
+ const derived = qip55AddressFromBytes(addressBytes);
4365
4440
  return derived.toLowerCase() === expectedSigner.toLowerCase();
4366
4441
  }
4367
4442
  function verifyMessageForSigner({
@@ -4432,6 +4507,8 @@ function verifyTypedData(params) {
4432
4507
  PROTOCOL_VERSION,
4433
4508
  QRLConnect,
4434
4509
  QRLConnectProvider,
4510
+ QRL_ADDRESS_BYTES,
4511
+ QRL_ADDRESS_HEX_LENGTH,
4435
4512
  QRL_CONNECT_PROVIDER_INFO,
4436
4513
  RESTRICTED_METHODS,
4437
4514
  SCHEME_TAG_MSG,
@@ -4454,11 +4531,13 @@ function verifyTypedData(params) {
4454
4531
  encodeField,
4455
4532
  encodeType,
4456
4533
  fingerprintEquals,
4534
+ formatQrlAddressFingerprint,
4457
4535
  generateConnectionURI,
4458
4536
  getAppStoreUrl,
4459
4537
  hasSigningDescriptor,
4460
4538
  hashStruct,
4461
4539
  hexToBytes,
4540
+ isCurrentQrlAddress,
4462
4541
  isMobileBrowser,
4463
4542
  isQrlSignedMessageResult,
4464
4543
  isQrlSignedResult,
@@ -4467,6 +4546,7 @@ function verifyTypedData(params) {
4467
4546
  isValidJsonRpcMethod,
4468
4547
  normalizeRelayUrl,
4469
4548
  parseConnectionURI,
4549
+ qip55AddressFromBytes,
4470
4550
  typeHash,
4471
4551
  verifyMessage,
4472
4552
  verifyMessageForSigner,
package/dist/index.mjs CHANGED
@@ -109,6 +109,54 @@ function constantTimeEquals(a, b) {
109
109
  return d === 0;
110
110
  }
111
111
 
112
+ // src/utils/qrlAddress.ts
113
+ var QRL_ADDRESS_BYTES = 64;
114
+ var QRL_ADDRESS_HEX_LENGTH = QRL_ADDRESS_BYTES * 2;
115
+ var QRL_ADDRESS_RE = new RegExp(`^Q[0-9a-fA-F]{${QRL_ADDRESS_HEX_LENGTH}}$`);
116
+ var textEncoder = new TextEncoder();
117
+ function checksummedHex(lowerHex) {
118
+ const hash = shake256Digest(textEncoder.encode(lowerHex), QRL_ADDRESS_BYTES);
119
+ let result = "";
120
+ for (let index = 0; index < lowerHex.length; index++) {
121
+ const char = lowerHex[index] ?? "";
122
+ if (char >= "a" && char <= "f") {
123
+ const byte = hash[index >> 1] ?? 0;
124
+ const nibble = (index & 1) === 0 ? byte >> 4 : byte & 15;
125
+ result += nibble >= 8 ? char.toUpperCase() : char;
126
+ } else {
127
+ result += char;
128
+ }
129
+ }
130
+ return result;
131
+ }
132
+ function isCurrentQrlAddress(value) {
133
+ if (typeof value !== "string" || !QRL_ADDRESS_RE.test(value)) return false;
134
+ const body = value.slice(1);
135
+ const lower = body.toLowerCase();
136
+ if (body === lower || body === body.toUpperCase()) return true;
137
+ return body === checksummedHex(lower);
138
+ }
139
+ function formatQrlAddressFingerprint(address) {
140
+ if (!/^Q(?:[0-9a-fA-F]{40}|[0-9a-fA-F]{128})$/.test(address)) return address;
141
+ const body = address.slice(1);
142
+ const segmentLength = 8;
143
+ if (body.length < segmentLength * 3) return address;
144
+ const middleStart = Math.floor((body.length - segmentLength) / 2);
145
+ return [
146
+ `Q${body.slice(0, segmentLength)}`,
147
+ body.slice(middleStart, middleStart + segmentLength),
148
+ body.slice(-segmentLength)
149
+ ].join("...");
150
+ }
151
+ function qip55AddressFromBytes(address) {
152
+ if (!(address instanceof Uint8Array) || address.length !== QRL_ADDRESS_BYTES) {
153
+ throw new Error(`address must be exactly ${QRL_ADDRESS_BYTES} bytes`);
154
+ }
155
+ let lower = "";
156
+ for (const byte of address) lower += byte.toString(16).padStart(2, "0");
157
+ return `Q${checksummedHex(lower)}`;
158
+ }
159
+
112
160
  // src/config.ts
113
161
  var DEFAULT_RELAY_URL = "https://qrlwallet.com";
114
162
  var RELAY_PATH = "/relay";
@@ -122,10 +170,6 @@ var MAX_JSON_RPC_ID_LENGTH = 128;
122
170
  var MAX_JSON_RPC_METHOD_LENGTH = 128;
123
171
  var MAX_RELAY_URL_LENGTH = 2048;
124
172
  var RECONNECT_WALLET_PROBE_MS = 12 * 1e3;
125
- var CURRENT_QRL_ADDRESS_RE = /^Q[0-9a-fA-F]{40}$/;
126
- function isCurrentQrlAddress(value) {
127
- return typeof value === "string" && CURRENT_QRL_ADDRESS_RE.test(value);
128
- }
129
173
  function isExplicitLoopbackHostname(value) {
130
174
  const hostname = value.toLowerCase();
131
175
  return hostname === "localhost" || hostname.endsWith(".localhost") || hostname === "127.0.0.1" || hostname === "::1" || hostname === "[::1]";
@@ -201,9 +245,9 @@ function classifyRpcMethod(method) {
201
245
  }
202
246
 
203
247
  // src/PQCrypto.ts
204
- var textEncoder = new TextEncoder();
205
- var LABEL = textEncoder.encode("pq-pair/v3");
206
- var LABEL_AEAD_SUFFIX = textEncoder.encode(" aead");
248
+ var textEncoder2 = new TextEncoder();
249
+ var LABEL = textEncoder2.encode("pq-pair/v3");
250
+ var LABEL_AEAD_SUFFIX = textEncoder2.encode(" aead");
207
251
  var DIR_DAPP_TX = new Uint8Array([0, 0, 0, 1]);
208
252
  var DIR_WALLET_TX = new Uint8Array([0, 0, 0, 2]);
209
253
  var ML_KEM_768_PK_LEN = 1184;
@@ -402,7 +446,7 @@ function isSigningDescriptor(value) {
402
446
  return typeof value === "string" && /^0x01[0-9a-fA-F]{4}$/.test(value);
403
447
  }
404
448
  function hasValidSigningFields(record) {
405
- return isFixedHex(record.signature, SIGNATURE_HEX_BYTES) && isFixedHex(record.publicKey, PUBLIC_KEY_HEX_BYTES) && (!hasOwn(record, "descriptor") || isSigningDescriptor(record.descriptor)) && typeof record.signer === "string" && /^Q[0-9a-fA-F]{40}$/.test(record.signer) && isFixedHex(record.digest, DIGEST_HEX_BYTES);
449
+ return isFixedHex(record.signature, SIGNATURE_HEX_BYTES) && isFixedHex(record.publicKey, PUBLIC_KEY_HEX_BYTES) && (!hasOwn(record, "descriptor") || isSigningDescriptor(record.descriptor)) && typeof record.signer === "string" && isCurrentQrlAddress(record.signer) && isFixedHex(record.digest, DIGEST_HEX_BYTES);
406
450
  }
407
451
  function isQrlSignedMessageResult(value) {
408
452
  try {
@@ -451,10 +495,10 @@ function error(tag, ...args) {
451
495
  }
452
496
 
453
497
  // src/KeyExchange.ts
454
- var textEncoder2 = new TextEncoder();
498
+ var textEncoder3 = new TextEncoder();
455
499
  var textDecoder = new TextDecoder();
456
- var HELLO_WALLET = textEncoder2.encode("hello/wallet/v1");
457
- var HELLO_DAPP = textEncoder2.encode("hello/dapp/v1");
500
+ var HELLO_WALLET = textEncoder3.encode("hello/wallet/v1");
501
+ var HELLO_DAPP = textEncoder3.encode("hello/dapp/v1");
458
502
  var AES_GCM_TAG_LEN = 16;
459
503
  var SYNACK_C0_LEN = HELLO_WALLET.length + AES_GCM_TAG_LEN;
460
504
  var ACK_C1_LEN = HELLO_DAPP.length + AES_GCM_TAG_LEN;
@@ -728,7 +772,7 @@ var KeyExchange = class extends EventEmitter {
728
772
  throw new Error("KeyExchange: send counter exhausted");
729
773
  }
730
774
  const seq = session.sendSeq++;
731
- const pt = textEncoder2.encode(data);
775
+ const pt = textEncoder3.encode(data);
732
776
  try {
733
777
  const ct = await seal(session.key, session.sendDir, seq, session.htx, pt);
734
778
  if (this.stateGeneration !== generation || this.session !== session) {
@@ -3172,10 +3216,14 @@ function typeHash(primary, types) {
3172
3216
  return shake256Digest(new TextEncoder().encode(encodeType(primary, types)), DIGEST_LEN);
3173
3217
  }
3174
3218
  function parseQAddress(addr) {
3175
- if (typeof addr !== "string" || !/^Q[0-9a-fA-F]{40}$/.test(addr)) {
3176
- throw new Error(`invalid Q-address: ${addr}`);
3219
+ if (!isCurrentQrlAddress(addr)) {
3220
+ throw new Error(`invalid Q-address: ${String(addr)}`);
3221
+ }
3222
+ const bytes = hexToBytes("0x" + addr.slice(1).toLowerCase());
3223
+ if (bytes.length > SLOT) {
3224
+ throw new Error("qrl_signTypedData v1 does not support QIP-55 address fields");
3177
3225
  }
3178
- return hexToBytes("0x" + addr.slice(1).toLowerCase());
3226
+ return bytes;
3179
3227
  }
3180
3228
  function padLeft32(bytes) {
3181
3229
  if (bytes.length > SLOT) throw new Error("cannot pad: bytes > 32");
@@ -3492,7 +3540,7 @@ function requestedSwitchChainId(params) {
3492
3540
  }
3493
3541
  return canonicalChainId2(request.chainId);
3494
3542
  }
3495
- var TRANSACTION_FIELDS = /* @__PURE__ */ new Set(["from", "to", "value", "gas", "data"]);
3543
+ var TRANSACTION_FIELDS = /* @__PURE__ */ new Set(["from", "to", "value", "gas", "data", "chainId"]);
3496
3544
  var QRL_TRANSACTION_MAX_DATA_BYTES = 128 * 1024;
3497
3545
  var QRL_TRANSACTION_MAX_QUANTITY_HEX_DIGITS = 64;
3498
3546
  var RPC_QUANTITY_RE = /^0x(?:0|[1-9a-fA-F][0-9a-fA-F]*)$/;
@@ -3507,8 +3555,11 @@ function validateRpcQuantity(value, field) {
3507
3555
  if (field === "gas" && BigInt(value) > BigInt(Number.MAX_SAFE_INTEGER)) {
3508
3556
  throw new Error("transaction gas exceeds the wallet safe-integer limit");
3509
3557
  }
3558
+ if (field === "chainId" && BigInt(value) === 0n) {
3559
+ throw new Error("transaction chainId must be positive");
3560
+ }
3510
3561
  }
3511
- function validateTransactionRequest(method, params, authorizedAccount) {
3562
+ function validateTransactionRequest(method, params, authorizedAccount, currentChainId) {
3512
3563
  if (params?.length !== 1 || !isRecordObj(params[0])) {
3513
3564
  throw new Error(`${method} requires exactly one transaction object`);
3514
3565
  }
@@ -3526,6 +3577,12 @@ function validateTransactionRequest(method, params, authorizedAccount) {
3526
3577
  }
3527
3578
  if ("value" in tx) validateRpcQuantity(tx.value, "value");
3528
3579
  if ("gas" in tx) validateRpcQuantity(tx.gas, "gas");
3580
+ if ("chainId" in tx) {
3581
+ validateRpcQuantity(tx.chainId, "chainId");
3582
+ if (canonicalChainId2(tx.chainId) !== canonicalChainId2(currentChainId)) {
3583
+ throw new Error("transaction chainId does not match the connected wallet");
3584
+ }
3585
+ }
3529
3586
  if ("data" in tx && (typeof tx.data !== "string" || tx.data.length > QRL_TRANSACTION_MAX_DATA_BYTES * 2 + 2 || !/^0x(?:[0-9a-fA-F]{2})*$/.test(tx.data))) {
3530
3587
  throw new Error("transaction data must be bounded 0x-prefixed bytes");
3531
3588
  }
@@ -3543,7 +3600,7 @@ function snapshotRequestParams(params) {
3543
3600
  throw new Error(`Request params must be JSON-serializable: ${detail}`);
3544
3601
  }
3545
3602
  }
3546
- function validateRestrictedRequest(method, params, authorizedAccounts) {
3603
+ function validateRestrictedRequest(method, params, authorizedAccounts, currentChainId) {
3547
3604
  if (method === "qrl_requestAccounts") {
3548
3605
  if (params !== void 0 && params.length !== 0) {
3549
3606
  throw new Error("qrl_requestAccounts does not accept parameters");
@@ -3558,7 +3615,7 @@ function validateRestrictedRequest(method, params, authorizedAccounts) {
3558
3615
  throw new Error("No authorized account: call qrl_requestAccounts first");
3559
3616
  }
3560
3617
  if (method === "qrl_sendTransaction" || method === "qrl_signTransaction") {
3561
- validateTransactionRequest(method, params, authorizedAccount);
3618
+ validateTransactionRequest(method, params, authorizedAccount, currentChainId);
3562
3619
  return void 0;
3563
3620
  }
3564
3621
  if (method === "qrl_signMessage") {
@@ -3587,8 +3644,9 @@ function validateRestrictedRequest(method, params, authorizedAccounts) {
3587
3644
  if (params[0] !== authorizedAccount) {
3588
3645
  throw new Error("qrl_signTypedData signer is not the authorized account");
3589
3646
  }
3590
- computeTypedDataDigest(params[1]);
3591
- return void 0;
3647
+ throw new Error(
3648
+ "qrl_signTypedData is unavailable for QIP-55 until the 64-byte word encoding and signing scheme version are finalized"
3649
+ );
3592
3650
  }
3593
3651
  function requiresAuthorizedAccount(method) {
3594
3652
  return method === "qrl_sendTransaction" || method === "qrl_signTransaction" || method === "qrl_signMessage" || method === "qrl_signTypedData";
@@ -3796,7 +3854,12 @@ var QRLConnectProvider = class extends EventEmitter4 {
3796
3854
  }
3797
3855
  this.pendingRequests.delete(response.id);
3798
3856
  if (response.error) {
3799
- pending.reject(new Error(response.error.message || "Request failed"));
3857
+ pending.reject(
3858
+ Object.assign(new Error(response.error.message || "Request failed"), {
3859
+ code: response.error.code,
3860
+ ...response.error.data === void 0 ? {} : { data: response.error.data }
3861
+ })
3862
+ );
3800
3863
  return;
3801
3864
  }
3802
3865
  try {
@@ -3900,7 +3963,8 @@ var QRLConnectProvider = class extends EventEmitter4 {
3900
3963
  const expectedChainId = validateRestrictedRequest(
3901
3964
  method,
3902
3965
  stableParams,
3903
- this.connectionManager.getAccounts()
3966
+ this.connectionManager.getAccounts(),
3967
+ this.connectionManager.getChainId()
3904
3968
  );
3905
3969
  return this.enqueueRestrictedRequest(method, stableParams, expectedChainId, generation);
3906
3970
  }
@@ -3965,6 +4029,14 @@ var QRLConnectProvider = class extends EventEmitter4 {
3965
4029
  throw new Error("Not connected to QRL Wallet");
3966
4030
  }
3967
4031
  }
4032
+ if (restricted) {
4033
+ validateRestrictedRequest(
4034
+ method,
4035
+ params,
4036
+ this.connectionManager.getAccounts(),
4037
+ this.connectionManager.getChainId()
4038
+ );
4039
+ }
3968
4040
  if (this.requestCounter >= Number.MAX_SAFE_INTEGER) {
3969
4041
  throw new Error("JSON-RPC request id counter exhausted");
3970
4042
  }
@@ -4043,9 +4115,10 @@ var QRLConnectProvider = class extends EventEmitter4 {
4043
4115
  }
4044
4116
  // ── In-flight persistence (survive the return-redirect reload) ──────
4045
4117
  readInflight() {
4046
- if (typeof localStorage === "undefined") return [];
4118
+ const storage = getBrowserStorage();
4119
+ if (!storage) return [];
4047
4120
  try {
4048
- const raw = localStorage.getItem(this.inflightKey);
4121
+ const raw = storage.getItem(this.inflightKey);
4049
4122
  if (!raw) return [];
4050
4123
  const parsed = JSON.parse(raw);
4051
4124
  if (!Array.isArray(parsed)) return [];
@@ -4061,12 +4134,13 @@ var QRLConnectProvider = class extends EventEmitter4 {
4061
4134
  }
4062
4135
  }
4063
4136
  writeInflight(records) {
4064
- if (typeof localStorage === "undefined") return;
4137
+ const storage = getBrowserStorage();
4138
+ if (!storage) return;
4065
4139
  try {
4066
4140
  if (records.length === 0) {
4067
- localStorage.removeItem(this.inflightKey);
4141
+ storage.removeItem(this.inflightKey);
4068
4142
  } else {
4069
- localStorage.setItem(this.inflightKey, JSON.stringify(records));
4143
+ storage.setItem(this.inflightKey, JSON.stringify(records));
4070
4144
  }
4071
4145
  } catch {
4072
4146
  }
@@ -4200,7 +4274,6 @@ var ML_DSA_87_DESCRIPTOR_TYPE = 1;
4200
4274
  var ML_DSA_DESCRIPTOR_BYTES = 3;
4201
4275
  var ML_DSA_87_PUBLIC_KEY_BYTES = 2592;
4202
4276
  var ML_DSA_87_SIGNATURE_BYTES = 4627;
4203
- var CURRENT_QRL_ADDRESS_BYTES = 20;
4204
4277
  function bytesOrHex(v) {
4205
4278
  if (v instanceof Uint8Array) return new Uint8Array(v);
4206
4279
  if (typeof v === "string") return hexToBytes(v);
@@ -4257,11 +4330,8 @@ function publicKeyMatchesSigner(expectedSigner, descriptor, publicKey) {
4257
4330
  if (descriptor[0] !== ML_DSA_87_DESCRIPTOR_TYPE) {
4258
4331
  return false;
4259
4332
  }
4260
- const addressBytes = shake256Digest(
4261
- concatBytes(descriptor, publicKey),
4262
- CURRENT_QRL_ADDRESS_BYTES
4263
- );
4264
- const derived = `Q${bytesToHex(addressBytes).slice(2)}`;
4333
+ const addressBytes = shake256Digest(concatBytes(descriptor, publicKey), QRL_ADDRESS_BYTES);
4334
+ const derived = qip55AddressFromBytes(addressBytes);
4265
4335
  return derived.toLowerCase() === expectedSigner.toLowerCase();
4266
4336
  }
4267
4337
  function verifyMessageForSigner({
@@ -4331,6 +4401,8 @@ export {
4331
4401
  PROTOCOL_VERSION,
4332
4402
  QRLConnectProvider as QRLConnect,
4333
4403
  QRLConnectProvider,
4404
+ QRL_ADDRESS_BYTES,
4405
+ QRL_ADDRESS_HEX_LENGTH,
4334
4406
  QRL_CONNECT_PROVIDER_INFO,
4335
4407
  RESTRICTED_METHODS,
4336
4408
  SCHEME_TAG_MSG,
@@ -4353,11 +4425,13 @@ export {
4353
4425
  encodeField,
4354
4426
  encodeType,
4355
4427
  fingerprintEquals,
4428
+ formatQrlAddressFingerprint,
4356
4429
  generateConnectionURI,
4357
4430
  getAppStoreUrl,
4358
4431
  hasSigningDescriptor,
4359
4432
  hashStruct,
4360
4433
  hexToBytes,
4434
+ isCurrentQrlAddress,
4361
4435
  isMobileBrowser,
4362
4436
  isQrlSignedMessageResult,
4363
4437
  isQrlSignedResult,
@@ -4366,6 +4440,7 @@ export {
4366
4440
  isValidJsonRpcMethod,
4367
4441
  normalizeRelayUrl,
4368
4442
  parseConnectionURI,
4443
+ qip55AddressFromBytes,
4369
4444
  typeHash,
4370
4445
  verifyMessage,
4371
4446
  verifyMessageForSigner,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@qrlwallet/connect",
3
- "version": "4.0.0",
3
+ "version": "5.0.1",
4
4
  "packageManager": "npm@10.9.8",
5
5
  "description": "Connect your dApp to QRL Wallet via QR code or deep link",
6
6
  "main": "dist/index.js",
@@ -44,13 +44,13 @@
44
44
  "dependencies": {
45
45
  "@noble/hashes": "^2.2.0",
46
46
  "@noble/post-quantum": "^0.5.4",
47
- "@theqrl/mldsa87": "^2.1.1",
47
+ "@theqrl/mldsa87": "^2.1.4",
48
48
  "eventemitter3": "^5.0.1",
49
49
  "socket.io-client": "^4.8.1"
50
50
  },
51
51
  "devDependencies": {
52
52
  "@size-limit/file": "^11.0.0",
53
- "@theqrl/wallet.js": "^3.0.1",
53
+ "@theqrl/wallet.js": "^6.2.4",
54
54
  "@types/node": "^25.3.3",
55
55
  "@typescript-eslint/eslint-plugin": "^8.0.0",
56
56
  "@typescript-eslint/parser": "^8.0.0",
@@ -60,10 +60,13 @@
60
60
  "size-limit": "^11.0.0",
61
61
  "tsup": "^8.4.0",
62
62
  "typescript": "^5.7.0",
63
- "vitest": "^3.0.0"
63
+ "vitest": "4.1.11"
64
64
  },
65
65
  "overrides": {
66
- "esbuild": "0.28.1"
66
+ "esbuild": "0.28.1",
67
+ "vitest": {
68
+ "vite": "7.3.5"
69
+ }
67
70
  },
68
71
  "size-limit": [
69
72
  {