UR and CBOR primer
Every QR code BitTool Signer reads or writes, except plain addresses and PSBT text, is a Uniform Resource (BCR-2020-005). You do not need to implement UR yourself (use a library), but knowing the layers makes debugging much easier.
From CBOR to QR text
CBOR bytes
→ append CRC-32 (4 bytes)
→ Bytewords "minimal": each byte becomes 2 letters (first + last letter of a 4-letter word)
→ "ur:" + type + "/" + bytewords single part
For example, a short EVM signature response:
UR:ETH-SIGNATURE/OEADTPDAGDHECKNDDWKIFTGLJLLEBDCEDPFMGWHTADAOHDFPJNBYWYMUYLLTIMKSWDCFWKYTSPVWGYVEGMDNJNWMLSGAOLHGDPATJEGLIOTOQZFPJNQDYNMHQDESRSIHLEHHLGDRTDYLHHUREYGMWTLBMWBKLFCTIHRKJZRHCKMSLFOYADCWJNLUAY
The text is case-insensitive. Upper-case it before making the QR code: the characters then fit QR alphanumeric mode, which is much denser than byte mode.
Multi-part (animated) URs
When a payload is too large for one QR code, the encoder splits it into fragments of equal length and produces parts:
ur:<type>/<seq>-<count>/<bytewords of CBOR [seq, count, message-length, checksum, fragment]>
- Parts
1…countcarry the fragments in order ("pure" parts). - Parts after
countare fountain parts: the XOR of a pseudo-random selection of fragments, chosen from a seed derived fromseqand the checksum. A decoder can rebuild missing fragments from them. count,message-lengthandchecksum(CRC-32 of the whole message) are the same in every part of one session. BitTool Signer rejects a scan that mixes two sessions.
Practical consequences:
- Scan parts in any order; ignore repeats.
- Display: keep generating parts with
nextPart()in a loop rather than replaying the firstcountparts. - Fragment size is a trade-off between QR density and frame count. BitTool Signer uses 400 bytes for the wallet-level pairing and 180 bytes for its signing responses.
Frame 1 of the wallet-level pairing QR code:
UR:BYTES/1-4/LPADAACFAMCECYJYHYKIWKHKADLTHKAMCFOLIEJNJLIEIHIEIYKPJZJZIOJTIHJYKTJLJPJEIOJNHSINJTJTIHJYIOKOIHJPJKINJLJTADISHSIAIAJLKPJTJYJKLTONIEKSJOKPIDKSJLKSJOKPIDENFXHSJYHGIEINHTINJLIEJNGOIHGHFYJOETGSGHECJLJPETJTJNIDGRGLIAKPKKKOKNEMHGKKJEJKHFFGJEGRFWEEGMFDKTFXFYEOHDKKKPKOGDFEIDKOJSFPGYHKEOJPFPGDJKISHGIAGTGSJLGDEYIYGTFGGTGRFDGDGEEEHTIHHTHDHKHFGOISGSKOEHHFGTJPIMGDFXEMGDHGENHFIHIAISHSINJTIAIDJYIAIYIEIHJPINKOIHOEIYIAISHSJTIOIHIAEHDLDRIOJPIHIAIHINKOIHIADYDLDRIYJKIAJPINJOJYIYJOEYKTJOJEISJEHSIAIAJLKPJTJYGDHSJYISJEJNDLETEEDIDLDYDIDLDYDIONIEKSJOKPIDKSJLKSJOKPIDENFWIOFWIOJKIHJKJOHGKOFEGMFGEOGSFDGYKPENFXJTJSIEKOIYFEKOJYGTIAGYIMHKJPIAGMKNKSECEOGYGEIMGUKSHSJPIMEYHSIYHKHGIAGSJYIHJLFLHFJEKKEMFYEOGOGRFYGDESGYKKJPGSJOJPGYEOHFFXFEFXJLHKEEESKKIYIEFYFEFDFLFXJYGTGTIMESEYJOGMIHGOJKGYIHIAISHSINJTIAIDJYIAIYIEIHJPINKOIHOEIYIAISHSJTIOIHUYSWLYTA
CBOR in these payloads
Only a small subset of CBOR appears:
| Major type | Used for |
|---|---|
| 0 unsigned int | Map keys, data types, chain IDs, fingerprints, path indexes |
| 2 byte string | Signing data, keys, chain codes, signatures, PSBTs |
| 3 text string | Origin, names, the pairing map's keys and values |
| 4 array | Path components, descriptor lists, pairing accounts |
| 5 map | Every request, response and key structure |
| 6 tag | Typed items (below) |
| 7 simple | true / false (is-private, hardened flags) |
Requests must use definite lengths, no duplicate map keys and no trailing bytes. Floats, negative integers and indefinite-length items have no place in any of these structures.
Tags
| Tag | Name | Wraps |
|---|---|---|
| 37 | uuid | Request ID bytes |
| 303 | crypto-hdkey | HD key map |
| 304 | crypto-keypath | Path map {1: components, 2: source-fingerprint, 3: depth} |
| 305 | crypto-coin-info | {1: coin type, 2: network} |
| 400 | script-hash | sh(…) |
| 401 | witness-script-hash | wsh(…) |
| 403 | public-key-hash | pkh(…) |
| 404 | witness-public-key-hash | wpkh(…) |
| 409 | taproot | tr(…) |
Byte string vs. text matters: a request ID is bytes inside tag 37, not the UUID as text; an EVM address in key 6 is 20 raw bytes, not "0x…".
Reading a request by hand
The CBOR of the EIP-1559 example, annotated:
a7 map, 7 entries
01 d8 25 50 5f1e9b2c…5a01 1: tag 37 (uuid), 16 bytes
02 58 31 02ef010c…80c0 2: 49 bytes: 0x02 ‖ rlp(unsigned EIP-1559 tx)
03 04 3: data-type 4
04 01 4: chain-id 1
05 d9 0130 a2 5: tag 304 (crypto-keypath), map 2
01 8a 18 2c f5 18 3c f5 00 f5 00 f4 00 f4 components [44,true,60,true,0,true,0,false,0,false]
02 1a 73c5da0a source-fingerprint 0x73c5da0a
06 54 9858effd…eda94 6: 20-byte address
07 6e "BitTool Wallet" 7: origin
a701d825505f1e9b2c7d3a4e6f8a0b1c2d3e4f5a0102583102ef010c843b9aca008506fc23ac0082520894000000000000000000000000000000000000dead872386f26fc1000080c00304040105d90130a2018a182cf5183cf500f500f400f4021a73c5da0a06549858effd232b4033e47d90003d41ec34ecaeda94076e426974546f6f6c2057616c6c6574
Libraries
| Language | UR | CBOR |
|---|---|---|
| JavaScript / TypeScript | @ngraveio/bc-ur; Keystone @keystonehq/bc-ur-registry* for typed classes | cborg |
| Swift | URKit | URKit's DCBOR, SwiftCBOR |
| Kotlin / Java | Hummingbird | Hummingbird, com.upokecenter:cbor |
| Rust | ur | minicbor, ciborium |