Solana (SOL)
Sign Solana transactions and messages. The most reliable input is a base58-serialized transaction; plain-JSON and several data encodings are also accepted.
| Field | Value |
|---|---|
| Family | Solana |
| coinType | 501 |
| keyPath | m/44'/501'/<accountIdx>' (also accepted on mainnet: m/44'/501'/<accountIdx>'/0' — a separate account) |
| chainId | solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp/slip44:501 |
| chainId · Devnet | solana:EtWTRABZaYq6iMfeYKouRu166VU2xqa1/slip44:501 |
Supported Methods
| Method | Supported |
|---|---|
| getAddress | ✓ |
| signTransaction | ✓ |
| signMessage | ✓ |
| signTypedData | ✗ |
| signData | ✗ |
Requirements
Minimum firmware for each method. The two models are on different version lines, so DCENT Biometric Wallet and DCENT X are listed separately (the numbers are not comparable across models). Below this the request returns the firmware-update error (5005) and the bridge prompts to update.
| Method | DCENT Biometric Wallet | DCENT X |
|---|---|---|
getAddress | 2.16.5 or higher | 1.0.0 or higher |
signTransaction | 2.16.5 or higher | 1.0.0 or higher |
signMessage | 2.31.0 or higher | 1.0.0 or higher |
Firmware versions above are for the DCENT Biometric Wallet line — DCENT X supports them from 1.0.0. SPL token support was added in firmware 2.17.1.
getAddress — account address
Retrieves the Solana account address for a chainId + keyPath. A Solana address is the base58-encoded ed25519 public key (32 bytes); there are no address variants — omit addressFormat (on mainnet only legacy is accepted and every other value fails with -32602; on Devnet ledger fails while the other listed values are ignored; any non-null value outside the addressFormat set fails with -32602). Besides the default m/44'/501'/<accountIdx>', on mainnet the 4-level path m/44'/501'/<accountIdx>'/0' is also accepted — it derives a different account, selected by the keyPath alone.
Parameters
| Field | Type | Required | Source | Notes |
|---|---|---|---|---|
chainId | string | Required | App | Full CAIP-19 — solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp/slip44:501 |
keyPath | string | Required | App | BIP-44, default m/44'/501'/0' |
Request
await dcent.getAddress({
chainId: 'solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp/slip44:501',
keyPath: "m/44'/501'/0'"
})
Response
| Field | Type | Notes |
|---|---|---|
body.parameter.address | string | Base58 ed25519 account address — this is the Solana address |
header.status | string | "success" on success |
{
"header": {
"version": "1.0",
"status": "success",
"response_from": "getAddress"
},
"body": {
"command": "getAddress",
"parameter": {
"address": "7Eq…"
}
}
}
signTransaction
Variant — base58 serialized (recommended)
payload.transaction:
"4uQeVj5tqViQh7yWWGStvkEG1Zmhx6u…" // VersionedTransaction: bs58.encode(tx.serialize())
// legacy Transaction: bs58.encode(tx.serialize({ requireAllSignatures: false }))
A base64-encoded serialized VersionedTransaction string is also accepted.
Variant — plain JSON
payload.transaction:
{
"version": 0,
"feePayer": "<pubkey>",
"instructions": [
{
"programId": "11111111111111111111111111111111",
"keys": [
{
"pubkey": "<from>",
"isSigner": true,
"isWritable": true
},
{
"pubkey": "<to>",
"isSigner": false,
"isWritable": true
}
],
"data": {
"instruction": 2,
"lamports": 1000000
}
}
],
"recentBlockhash": "<blockhash>"
}
| Field | Type | Required | Source | Notes |
|---|---|---|---|---|
version | number | Optional | App | 0 builds a v0 message; omitted or any other value builds a legacy message |
feePayer | pubkey | Required-Real | Wallet | Usually the signing wallet |
instructions[].programId | string | Required | App | base58 program id |
instructions[].keys | meta[] | Optional | App | Real account metas / ATAs (omitted → an empty list). The wallet must be one of the transaction's required signers (feePayer or a key marked isSigner: true) |
instructions[].data | bytes | string | object | Optional | App | number[] (0–255) or Uint8Array, a 0x-hex string, or a base58 string (use the 0x prefix for hex — a string without it is tried as base58 first, then as hex). A malformed string is not rejected: hex decoding keeps only the bytes it could parse before the first unparseable byte pair, so e.g. "0xzz" or "hello world!" is signed with empty data — check your encoding. Omitted → empty. An object is accepted only for a SystemProgram transfer — { instruction: 2, lamports } |
recentBlockhash | string | Required-Real | App | Real, current blockhash — the app must supply it (form-E); not auto-filled |
An object missing feePayer, instructions or recentBlockhash fails with param_error (-32602). Other problems — a malformed feePayer, a bad programId / pubkey, a data object or array that is not one of the accepted forms, or a wallet that is not among the required signers (checked after the device signs) — fail with internal_error (-32603).
Variant — structured native SOL transfer
payload.transaction:
{
"type": "transfer",
"sender": "Gv8u221N315GoRx2C6yBNF2jNq379FdvaAp2bL8ywSDk",
"recipient": "11111111111111111111111111111112",
"amount": "1000000",
"recentBlockhash": "2zxvZMg8YScxKqW2QQqQHpNzkLgwvVLj8Q1ZQZoFcTZ6",
"preparedFee": { "fee": 5000 }
}
| Field | Type | Required | Source | Notes |
|---|---|---|---|---|
type | string | Required | App | "transfer" |
sender | string | Required | Wallet | base58 32-byte address |
recipient | string | Required | App | base58 32-byte address |
amount | string | Required | App | Positive integer, in lamports (at most 2^53−1) |
recentBlockhash | string | Required-Real | App | base58 32-byte, current |
preparedFee.fee | number | Required | App | Fee in lamports |
Any missing field fails with -32602. Unlike form-E, this shape is signed as a native SOL transfer rather than as an opaque message.
form-E — completed serialized message. A serialized or plain-JSON transaction is sent to the device as an opaque message to sign — the bridge does not identify any token in it. Use the form-D descriptor below for a token transfer. SPL token transfer: programId = Tokenkeg…, data = 0x0c + amount (u64 LE) + decimals (u8), keys = [source ATA, mint, dest ATA, owner] — all four are Required-Real, plus a real feePayer + recentBlockhash (form-E).
form-D — structured SPL token transfer. Instead of instructions, send transaction.token = { contract (mint), to (the recipient's owner wallet, not an ATA), amount (base units), decimals, symbol }. decimals is required for a mint that is not on DCENT's supported list (without it -32602); a registered mint uses its registered values. symbol is optional (defaults to the first 10 characters of the mint). This is the only shape in which the device can show an SPL token's symbol and amount — a form-E serialized message is signed as opaque bytes. Because the bridge signs with no network access, a Solana form-D transfer must also carry a sender (the fee-paying wallet; from or feePayer is also read — without any of them the call fails with param_error) and these completed fields: recentBlockhash (base58 32-byte, no surrounding whitespace), preparedFee.fee (positive integer lamports — this is what the device displays as the fee), and extra.isAssociated (boolean — whether the recipient's associated token account already exists; false makes the bridge add an ATA-create instruction). Missing any of the three gives -32602. Do not send the recipient ATA — the bridge derives it offline from owner + mint, so what the device shows cannot be redirected by the app.
Response
| Field | Type | Notes |
|---|---|---|
body.parameter.signature | string (hex) | The whole serialized Solana transaction with the wallet's signature added, hex-encoded (no 0x) — not a bare 64-byte signature. Only the wallet's signature is filled in, so it is ready to broadcast only when the wallet is the sole required signer; decode it with Buffer.from(signature, 'hex') before sendRawTransaction. |
header.status | string | "success" on success |
{
"header": {
"version": "1.0",
"status": "success",
"response_from": "signTransaction"
},
"body": {
"command": "signTransaction",
"parameter": {
"signature": "<signed tx hex>"
}
}
} // hex serialized tx (wallet signature applied)
signMessage
await dcent.sign({
method: 'signMessage',
chainId: 'solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp/slip44:501',
payload: {
keyPath: "m/44'/501'/0'",
message: 'Hello Solana!',
meta: {
kind: 'raw'
}
}
})
message is a string. An even-length hex string (with or without 0x) is signed as those bytes; any other non-empty string is signed as its UTF-8 bytes. meta.kind accepts 'raw' or 'personal' — 'eip712' is rejected.
Response
| Field | Type | Notes |
|---|---|---|
body.parameter.signature | string | Base58-encoded ed25519 signature (64 bytes) over the message bytes |
header.status | string | "success" on success |
{
"header": {
"version": "1.0",
"status": "success",
"response_from": "signMessage"
},
"body": {
"command": "signMessage",
"parameter": {
"signature": "…"
}
}
} // ed25519 signature over the message
Common Mistakes
Expired blockhash — fetch recentBlockhash immediately before signing. Unsigned transaction — VersionedTransaction.serialize() works without signatures; for a legacy Transaction use serialize({ requireAllSignatures: false }). Placeholder ATAs — source/dest ATAs must be the real on-chain accounts.