Skip to main content

Signing over QR

After pairing, every signature is one round trip: your wallet shows a request QR code, BitTool Signer shows a response QR code.

ChainRequest (you → signer)Response (signer → you)Page
Bitcoinur:crypto-psbt (also ur:psbt, BBQr, Base64/hex text)The same type, with signatures addedBitcoin
EVMur:eth-sign-requestur:eth-signatureEVM
TRONur:tron-sign-requestur:tron-signatureTRON
Solanaur:sol-sign-requestur:sol-signatureSolana

These are the public Keystone UR registry types. The wallet-level pairing format is only for importing accounts; there is no BitTool-specific signing envelope, and none should be invented.

Request building blocks​

The EVM, TRON and Solana requests are CBOR maps with integer keys, carried directly as the UR body (one CBOR layer, unlike the wallet-level pairing). They share three building blocks.

Request ID: 37(bytes)​

A random identifier, CBOR tag 37 around a byte string. BitTool Signer copies it into the response unchanged, so you can match the response to your pending request.

  • Generate 16 random bytes for every request (a UUID's bytes, not its text).
  • sol-sign-request and tron-sign-request require exactly 16 bytes. eth-sign-request also accepts 1–64 bytes for compatibility with wallets that use longer opaque IDs.
  • Compare IDs as bytes. A matching ID does not make a signature valid; you still verify it.

Derivation path: 304(crypto-keypath)​

The full path of the key that must sign, plus the master fingerprint:

304({
1: [44, true, 60, true, 0, true, 0, false, 0, false], ; m/44'/60'/0'/0/0: index, hardened, index, hardened …
2: 1942346250 ; master fingerprint 0x73c5da0a as a big-endian uint32
})
  • Components alternate index, hardened?. Do not also add 0x80000000 to hardened indexes.
  • Use the concrete leaf path (…/0/5), never an account path or a * wildcard.
  • Always include the fingerprint (key 2). Without it, matching is weaker and some requests cannot be matched at all.

A helper that builds it with cborg:

examples/keypath.mjs
import { Tagged } from 'cborg';

/** crypto-keypath (tag 304): "m/44'/195'/0'/0/0" + "73c5da0a" →
* 304({1: [44, true, 195, true, 0, true, 0, false, 0, false], 2: 1942346250}) */
export function keypath(path, masterFingerprint) {
const components = path.replace(/^m\//, '').split('/')
.flatMap((part) => [Number.parseInt(part, 10), part.endsWith("'")]);
return new Tagged(304, new Map([[1, components], [2, Number.parseInt(masterFingerprint, 16)]]));
}

Origin: text​

A short label shown to the user as the request's source, for example "BitTool Wallet". At most 256 characters. It is displayed, not authenticated: BitTool Signer never treats it as proof of anything.

No extra fields​

Only the keys defined for each type are accepted: 1–7 for eth-sign-request, 1–6 for sol- and tron-sign-request. An unknown key makes the whole request invalid, so do not add your own metadata.

Animated QR codes​

Requests are often larger than one QR code holds. Encode them as a multi-part UR and loop the frames:

  • Upper-case the UR text: UR:ETH-SIGN-REQUEST/… fits QR alphanumeric mode, which needs about 30% fewer bits than byte mode.
  • 200–400 bytes per fragment and 3–5 frames per second scan reliably on phones.
  • Keep calling the encoder's nextPart() instead of replaying the first pass. After the first pass the bc-ur encoder produces mixed "fountain" parts, which let the scanner finish even after missing frames.

BitTool Signer encodes its own responses with 180-byte fragments, so short signatures are a single static QR code and signed PSBTs may be animated. Your scanner must handle both.

The response​

  • Check the UR type first: a response to eth-sign-request must be eth-signature, and so on. Bitcoin answers with the same type you sent.
  • Check the request ID (Bitcoin: check that the unsigned transaction inside the PSBT is unchanged).
  • Verify the signature cryptographically against the expected key and the exact bytes you asked to sign, then assemble and broadcast.

There is no rejection response. If the user cancels in BitTool Signer, nothing is shown. Your UI should let the user go back, show the QR again, or start over, and should never wait forever for a reply.

Keep a pending record​

Store each request until it is answered or abandoned:

pendingId, walletId, chain/network, signer path + key/address,
requestId, request type, exact bytes to sign, digest,
unsigned transaction, created at, expiry (nonce / blockhash / TRON expiration),
status: awaitingSignature → signatureVerified → broadcast → confirmed | failed | cancelled

If anything in the transaction changes (fee, nonce, blockhash, amount), create a new request with a new ID. Never reuse a signature for different bytes, and reject a response that arrives for a cancelled or expired request.

How BitTool Signer finds the key​

You do not tell BitTool Signer which wallet to use; it works it out from the path, fingerprint and address or public key in your request, compared with the public keys of every wallet on the phone. Exactly one wallet must match. See What the signer checks for the full rules. In practice: send the full path, the fingerprint and the address (or public key) on every request, and BitTool Signer finds the key directly.