Solana (sol-sign-request)
sol-sign-request
| Key | Field | Type | Content |
|---|---|---|---|
| 1 | request-id | 37(bytes) | Exactly 16 random bytes |
| 2 | sign-data | bytes | sign-type 1: the serialized message · sign-type 2: the text message bytes |
| 3 | derivation-path | 304(crypto-keypath) | One of the three preset paths, with master fingerprint |
| 4 | address | bytes (32) | The signer's public key: the Base58-decoded address, not the text |
| 5 | origin | text | Shown to the user |
| 6 | sign-type | uint | 1 transaction · 2 message |
Note the key order differs from eth- and tron-sign-request: the path is key 3 and the type is key 6.
SolSignRequest.constructSOLRequest() in @keystonehq/bc-ur-registry-sol takes the address as a hex string of the 32 key bytes, not Base58. Passing the Base58 address produces a wrong key 4 and the request will not match.
sign-type 1: transactions
sign-data is the message part of the transaction, the bytes that signatures cover: transaction.serializeMessage() in @solana/web3.js v1, or message.serialize() for a VersionedTransaction. Not the full transaction (which starts with the signature count and slots) and not a hash.
| Rule | Detail |
|---|---|
| Message versions | Legacy and v0 |
| Address lookup tables | Not supported. The signer cannot verify table contents offline. Compile v0 messages with static keys only; if the result is too large, do not send it. |
| Signer | The key in key 4 must be one of the message's required signers (any slot, not only the fee payer). |
| Size | 1–64 instructions; the signed transaction must fit the 1,232-byte packet limit. |
| Durable nonce | Allowed, but flagged in the review (such a transaction stays valid indefinitely). |
Decoded programs: System (transfer, create account, nonce, assign, allocate), SPL Token and Token-2022 (transfer, transferChecked, approve, mint, burn, close, set authority, sync native), Associated Token Account (with address checks), Compute Budget, Memo, Stake (delegate, split, withdraw, deactivate) and common NFT programs (Metaplex Token Metadata, Bubblegum, Core). Token-2022 extensions and Squads proposal instructions are partially decoded. Other programs are shown as raw instructions and need explicit consent.
sign-type 2: messages
sign-data is UTF-8 text, 1–1,024 bytes, without control characters. Bytes that could be read as a transaction header are rejected, so a transaction can never be passed off as a "message". BitTool Signer signs the message bytes directly with Ed25519 (no prefix, no hash), which is what signMessage in Solana wallet adapters expects.
sol-signature
sol-signature = {
1: 37(request-id bytes),
2: signature ; 64 bytes, Ed25519
}
It is a detached signature. To finish a transaction:
- Check the request ID and verify the signature over the exact message bytes with the expected public key.
- Find the signer's index among the message's required signers and put the signature in that slot. It is not always slot 0.
- Keep any signatures already in the other slots; serialize once every required slot is filled and broadcast.
If the blockhash expires before broadcasting, build a new message and a new request. A signature cannot be moved to a message with another blockhash.
Code
With @solana/web3.js v1 and Keystone's @keystonehq/bc-ur-registry-sol:
import { randomUUID } from 'node:crypto';
import { PublicKey, Transaction, SystemProgram } from '@solana/web3.js';
import { SolSignRequest, SignType, SolSignature } from '@keystonehq/bc-ur-registry-sol';
import { URDecoder } from '@ngraveio/bc-ur';
import { ed25519 } from '@noble/curves/ed25519.js';
export function buildSolTransfer({ from, to, lamports, recentBlockhash }) {
const tx = new Transaction({ feePayer: new PublicKey(from), recentBlockhash });
tx.add(SystemProgram.transfer({ fromPubkey: new PublicKey(from), toPubkey: new PublicKey(to), lamports }));
return tx;
}
export function buildSolRequest(tx, { path, masterFingerprint, address }) {
const requestId = randomUUID();
const request = SolSignRequest.constructSOLRequest(
tx.serializeMessage(), // message bytes, not the whole transaction
path, // "m/44'/501'/0'/0'"
masterFingerprint, // "73c5da0a"
SignType.Transaction, // 1 (2 = text message)
requestId,
new PublicKey(address).toBuffer().toString('hex'), // this library wants hex, not Base58
'BitTool Wallet',
);
const encoder = request.toUREncoder(400);
const frames = [];
for (let i = 0; i < encoder.fragmentsLength; i++) frames.push(encoder.nextPart().toUpperCase());
return { requestId, frames };
}
export function attachSolSignature(frames, tx, { requestId, address }) {
const decoder = new URDecoder();
for (const frame of frames) decoder.receivePart(frame);
if (!decoder.isSuccess()) throw new Error('Incomplete or invalid QR');
const ur = decoder.resultUR();
if (ur.type !== 'sol-signature') throw new Error(`Expected sol-signature, got ${ur.type}`);
const response = SolSignature.fromCBOR(ur.cbor);
if (Buffer.from(response.getRequestId()).toString('hex') !== requestId.replace(/-/g, '')) throw new Error('Signature is for another request');
const signature = Buffer.from(response.getSignature()); // 64-byte Ed25519 signature
const signer = new PublicKey(address);
if (!ed25519.verify(signature, tx.serializeMessage(), signer.toBytes())) throw new Error('Bad signature');
tx.addSignature(signer, signature); // goes into this key's own slot
return tx.serialize(); // throws if another required signature is missing
}
Test vectors
All from the standard preset m/44'/501'/0'/0' (HAgk14JpMQLgt6rVgv7cBQFJWFto5Dqxi472uT3DKpqk). The blockhash is synthetic.
- SOL transfer
- SPL token (USDC)
- Message
Send 0.25 SOL (250,000,000 lamports) with a legacy message: fee payer and signer, recipient, System Program.
Request (your wallet → BitTool Signer)
{
"1 request-id": {
"uuid (tag 37)": "5f1e9b2c7d3a4e6f8a0b1c2d3e4f5a0b"
},
"2 sign-data": "01000103f036276246a75b9de3349ed42b15e232f6518fc20f5fcd4f1d64e81f9bd258f77e8c088760bfde1dddcf32c17f209b8242ee52aaf131facd88d0ea2c6d0b06f20000000000000000000000000000000000000000000000000000000000000000cc490e928cd2e3873bb343fc95da33179ca60f4dbf46c2c36e91299d55d4e6b901020200010c0200000080b2e60e00000000",
"3 derivation-path": {
"crypto-keypath (tag 304)": {
"1 components": [
44,
true,
501,
true,
0,
true,
0,
true
],
"2 source-fingerprint": 1942346250
}
},
"4 address": "f036276246a75b9de3349ed42b15e232f6518fc20f5fcd4f1d64e81f9bd258f7",
"5 origin": "BitTool Wallet",
"6 sign-type": 1
}
Show the QR text and raw CBOR
UR:SOL-SIGN-REQUEST/1-2/LPADAOCSZOCYLRESBDUEHDKBOLADTPDAGDHECKNDDWKIFTGLJLLEBDCEDPFMGWHTBDAOHDMTADAEADAXWTENDIIDFGOSHPNTVLEENNTYDNBZVOEYYNGYMYSABSHESNGWCAIEVSCTNDTDHDYLKBLKAYLTHNRSUECAUTTKEYSELBCXNDLFFWWYGMPKWNEHZSSNLOTIWDDWJNBDAMWZAEAEAEAEAEAEAEAEAEAEAEAEAEAEAEAEAEAEAEAEAEAEAEAEAEAEAEAEAEAEAEAESFGAWZEETBWS
UR:SOL-SIGN-REQUEST/2-2/LPAOAOCSZOCYLRESBDUEHDKBBAMOLKTDVLLTFRQDFXZTMDTNEOCHNSOLBSGTRSFGSASRJTMEDTNTGOTYVARHADAOAOAEADBNAOAEAEAELAPRVABAAEAEAEAEAXTAADDYOEADLOCSDWYKCFADYKYKAEYKAEYKAOCYJKSKTNBKAAHDCXWTENDIIDFGOSHPNTVLEENNTYDNBZVOEYYNGYMYSABSHESNGWCAIEVSCTNDTDHDYLAHJTFWINJYGHJLJLJZCXHGHSJZJZIHJYAMADAEWYHKLNHH
a601d825505f1e9b2c7d3a4e6f8a0b1c2d3e4f5a0b02589601000103f036276246a75b9de3349ed42b15e232f6518fc20f5fcd4f1d64e81f9bd258f77e8c088760bfde1dddcf32c17f209b8242ee52aaf131facd88d0ea2c6d0b06f20000000000000000000000000000000000000000000000000000000000000000cc490e928cd2e3873bb343fc95da33179ca60f4dbf46c2c36e91299d55d4e6b901020200010c0200000080b2e60e0000000003d90130a20188182cf51901f5f500f500f5021a73c5da0a045820f036276246a75b9de3349ed42b15e232f6518fc20f5fcd4f1d64e81f9bd258f7056e426974546f6f6c2057616c6c65740601
Response (BitTool Signer → your wallet)
{
"request-id": {
"uuid": "5f1e9b2c7d3a4e6f8a0b1c2d3e4f5a0b"
},
"signature": "99b9a42fcb8eead46ae74c5e792098c247c21ab4d791af70a26d8392986d714fd4543af8f121c355ca32f747f321d52fc3856cf8b4ae678045a34648d924940c"
}
Show the QR text and raw CBOR
UR:SOL-SIGNATURE/OEADTPDAGDHECKNDDWKIFTGLJLLEBDCEDPFMGWHTBDAOHDFZNLRHOXDLSBMNWDTYIMVDGSHYKKCXMKSAFLSACYQZTSMEPEJOOEJNLSMOMKJNJSGWTYGHFTYAWNCLSRGOSGEYYLFLWFCLTLDLSRLPJZYAQZPLIOLAFEOTFGFDTADKMWBNDTZCBGGE
a201d825505f1e9b2c7d3a4e6f8a0b1c2d3e4f5a0b02584099b9a42fcb8eead46ae74c5e792098c247c21ab4d791af70a26d8392986d714fd4543af8f121c355ca32f747f321d52fc3856cf8b4ae678045a34648d924940c
Assembled by your wallet
45G7cB2HaLBrWEuZxiWR2dFYu52Cje6d2vv5GnCaZ7YSc6ifhFVdFTGpJSk1PU7tSTPHjg3H4uufoe3XPmTRQiJP
AZm5pC/LjurUaudMXnkgmMJHwhq015GvcKJtg5KYbXFP1FQ6+PEhw1XKMvdH8yHVL8OFbPi0rmeARaNGSNkklAwBAAED8DYnYkanW53jNJ7UKxXiMvZRj8IPX81PHWToH5vSWPd+jAiHYL/eHd3PMsF/IJuCQu5SqvEx+s2I0OosbQsG8gAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAzEkOkozS44c7s0P8ldozF5ymD02/RsLDbpEpnVXU5rkBAgIAAQwCAAAAgLLmDgAAAAA=
Send 12.5 USDC with TransferChecked (6 decimals) from the signer's associated token account 5N3f1tj9v1vc5TUZ8S7mCAnVmjVKrfnzXWhxLaxyZAgt to the recipient's FGETo8T8wMcN2wCjav8VK6eh3dLk63evNDPxzLSJra8B.
Request (your wallet → BitTool Signer)
{
"1 request-id": {
"uuid (tag 37)": "5f1e9b2c7d3a4e6f8a0b1c2d3e4f5a0c"
},
"2 sign-data": "01000205f036276246a75b9de3349ed42b15e232f6518fc20f5fcd4f1d64e81f9bd258f740d2f27c461f29e9feba1ab8acd39496a9c65b0ae62a713f01f941a993bf55edd3ea8cf5acaca8cd05207512175c43cef54a5dd99ede20a16b55253738f397dcc6fa7af3bedbad3a3d65f36aabc97431b1bbe4c2d2f6e0e47ca60203452f5d6106ddf6e1d765a193d9cbe146ceeb79ac1cb485ed5f5b37913a8cf5857eff00a9cc490e928cd2e3873bb343fc95da33179ca60f4dbf46c2c36e91299d55d4e6b9010404010302000a0c20bcbe000000000006",
"3 derivation-path": {
"crypto-keypath (tag 304)": {
"1 components": [
44,
true,
501,
true,
0,
true,
0,
true
],
"2 source-fingerprint": 1942346250
}
},
"4 address": "f036276246a75b9de3349ed42b15e232f6518fc20f5fcd4f1d64e81f9bd258f7",
"5 origin": "BitTool Wallet",
"6 sign-type": 1
}
Show the QR text and raw CBOR
UR:SOL-SIGN-REQUEST/1-2/LPADAOCFADFRCYGEKNLGGDHDNNOLADTPDAGDHECKNDDWKIFTGLJLLEBDCEDPFMGWHTBNAOHDTBADAEAOAHWTENDIIDFGOSHPNTVLEENNTYDNBZVOEYYNGYMYSABSHESNGWCAIEVSCTNDTDHDYLFZTDWZKEFGCTDTWLZERDCYROPSTEMWMTPTSWHPBKVADRJSFHADYTFPPTMURSGOWETEWDLKYKPSPSPDSNAHCXKPBGCHHHFXTOYKGEHLTANNUECXOYJEGODAEMETWFMSUOSWZSKNWFRNUYPMFTFSIHWFIMPYSOJYEHPARKVESATDYNVTVEKEOLAOAXFEDLHLHSAMUTSEPTJPLD
UR:SOL-SIGN-REQUEST/2-2/LPAOAOCFADFRCYGEKNLGGDHDNNYNVYTSIHOYMUTASBVYFGTOWMKKPSCEQZLPWEHEHPEMMEFTLKYKLPKBZMAEPTSFGABAMOLKTDVLLTFRQDFXZTMDTNEOCHNSOLBSGTRSFGSASRJTMEDTNTGOTYVARHADAAAAADAXAOAEBKBNCXRFRNAEAEAEAEAEAMAXTAADDYOEADLOCSDWYKCFADYKYKAEYKAEYKAOCYJKSKTNBKAAHDCXWTENDIIDFGOSHPNTVLEENNTYDNBZVOEYYNGYMYSABSHESNGWCAIEVSCTNDTDHDYLAHJTFWINJYGHJLJLJZCXHGHSJZJZIHJYAMADAEHTNETOQZ
a601d825505f1e9b2c7d3a4e6f8a0b1c2d3e4f5a0c0258d601000205f036276246a75b9de3349ed42b15e232f6518fc20f5fcd4f1d64e81f9bd258f740d2f27c461f29e9feba1ab8acd39496a9c65b0ae62a713f01f941a993bf55edd3ea8cf5acaca8cd05207512175c43cef54a5dd99ede20a16b55253738f397dcc6fa7af3bedbad3a3d65f36aabc97431b1bbe4c2d2f6e0e47ca60203452f5d6106ddf6e1d765a193d9cbe146ceeb79ac1cb485ed5f5b37913a8cf5857eff00a9cc490e928cd2e3873bb343fc95da33179ca60f4dbf46c2c36e91299d55d4e6b9010404010302000a0c20bcbe00000000000603d90130a20188182cf51901f5f500f500f5021a73c5da0a045820f036276246a75b9de3349ed42b15e232f6518fc20f5fcd4f1d64e81f9bd258f7056e426974546f6f6c2057616c6c65740601
Response (BitTool Signer → your wallet)
{
"request-id": {
"uuid": "5f1e9b2c7d3a4e6f8a0b1c2d3e4f5a0c"
},
"signature": "3ad36b0673be3dbb71ec3b8fd346e2a96692f9ffb7b0e55ccdffe2d43f7395a447487e05522b62fed93351603483e1e2278e2193185e39761ae62d901bd1880e"
}
Show the QR text and raw CBOR
UR:SOL-SIGNATURE/OEADTPDAGDHECKNDDWKIFTGLJLLEBDCEDPFMGWHTBNAOHDFZFTTEJEAMJKRNFSRKJSWPFRMYTEFGVOPTIYMOYTZMRLPFVWHHSNZMVOTYFHJKMDOXFLFDKBAHGMDNIDZETAEOGYHNEELSVYVODIMNCLMUCSHYESKOCYVADPMHCWTTLOBABBPLAXRD
a201d825505f1e9b2c7d3a4e6f8a0b1c2d3e4f5a0c0258403ad36b0673be3dbb71ec3b8fd346e2a96692f9ffb7b0e55ccdffe2d43f7395a447487e05522b62fed93351603483e1e2278e2193185e39761ae62d901bd1880e
Assembled by your wallet
2BDTaXgjMPuTPcFZ7v87D5Vdcn9uFky9RixC5HbiStvCDGjGasBZuymph2oEEg2TmftbyB8EgTPdtHGgE1Rjmrvh
ATrTawZzvj27cew7j9NG4qlmkvn/t7DlXM3/4tQ/c5WkR0h+BVIrYv7ZM1FgNIPh4ieOIZMYXjl2GuYtkBvRiA4BAAIF8DYnYkanW53jNJ7UKxXiMvZRj8IPX81PHWToH5vSWPdA0vJ8Rh8p6f66Gris05SWqcZbCuYqcT8B+UGpk79V7dPqjPWsrKjNBSB1EhdcQ871Sl3Znt4goWtVJTc485fcxvp6877brTo9ZfNqq8l0MbG75MLS9uDkfKYCA0UvXWEG3fbh12Whk9nL4UbO63msHLSF7V9bN5E6jPWFfv8AqcxJDpKM0uOHO7ND/JXaMxecpg9Nv0bCw26RKZ1V1Oa5AQQEAQMCAAoMILy+AAAAAAAG
A sign-in message; signatureBase58 is the form a dApp expects from signMessage.
Request (your wallet → BitTool Signer)
{
"1 request-id": {
"uuid (tag 37)": "5f1e9b2c7d3a4e6f8a0b1c2d3e4f5a0d"
},
"2 sign-data": "5369676e20696e20746f204578616d706c6520417070207769746820536f6c616e612e204e6f6e63653a2037516d327850",
"3 derivation-path": {
"crypto-keypath (tag 304)": {
"1 components": [
44,
true,
501,
true,
0,
true,
0,
true
],
"2 source-fingerprint": 1942346250
}
},
"4 address": "f036276246a75b9de3349ed42b15e232f6518fc20f5fcd4f1d64e81f9bd258f7",
"5 origin": "BitTool Wallet",
"6 sign-type": 2
}
Show the QR text and raw CBOR
UR:SOL-SIGN-REQUEST/OLADTPDAGDHECKNDDWKIFTGLJLLEBDCEDPFMGWHTBTAOHDEHGUINIOJTCXINJTCXJYJLCXFEKSHSJNJOJZIHCXFPJOJOCXKTINJYISCXGUJLJZHSJTHSDMCXGLJLJTIAIHFTCXEMGYJNEYKSGDAXTAADDYOEADLOCSDWYKCFADYKYKAEYKAEYKAOCYJKSKTNBKAAHDCXWTENDIIDFGOSHPNTVLEENNTYDNBZVOEYYNGYMYSABSHESNGWCAIEVSCTNDTDHDYLAHJTFWINJYGHJLJLJZCXHGHSJZJZIHJYAMAORFCXVWPK
a601d825505f1e9b2c7d3a4e6f8a0b1c2d3e4f5a0d0258315369676e20696e20746f204578616d706c6520417070207769746820536f6c616e612e204e6f6e63653a2037516d32785003d90130a20188182cf51901f5f500f500f5021a73c5da0a045820f036276246a75b9de3349ed42b15e232f6518fc20f5fcd4f1d64e81f9bd258f7056e426974546f6f6c2057616c6c65740602
Response (BitTool Signer → your wallet)
{
"request-id": {
"uuid": "5f1e9b2c7d3a4e6f8a0b1c2d3e4f5a0d"
},
"signature": "0fad9739ab61e6c9de54b5aa59a00568fd4c12f4e7ac8d6624595c0df3f2f390b7a19b73f9ea08650115a672a81b2e8eb16810a5766519064cbd7f4fc9bb3b01"
}
Show the QR text and raw CBOR
UR:SOL-SIGNATURE/OEADTPDAGDHECKNDDWKIFTGLJLLEBDCEDPFMGWHTBTAOHDFZBSPMMSESPYHSVASOUEGHREPKHKNBAHISZCGSBGWKVDPSLGIYDKHKHHBTWFWZWFMHRLOYNDJKYTWDAYIHADBZOLJPPDCWDMMNPAISBEONKOIHCFAMGSRYLBGWSORKFRADOXRPNYAT
a201d825505f1e9b2c7d3a4e6f8a0b1c2d3e4f5a0d0258400fad9739ab61e6c9de54b5aa59a00568fd4c12f4e7ac8d6624595c0df3f2f390b7a19b73f9ea08650115a672a81b2e8eb16810a5766519064cbd7f4fc9bb3b01
Assembled by your wallet
KBTsdycDs2SLRqv9vaEaq7fKYeSWTRnohspq2ngCgAZhR38CYqB2GqykyQkphhpkYYsCoeNEU5YWiu2hdM2ajwe
Multi-signer transactions
When a transaction needs several signers (an SPL multisig, a Squads proposal approval, a fee payer that is not the sender), send one request per BitTool Signer key, each with that key's path and address. Each response fills one slot. Squads is a program-level multisig: the signer sees the instruction that approves a proposal, not the proposal's contents, and says so in the review.