Hedera (HBAR)
Sign Hedera transactions and messages. The device signs a CryptoTransfer built from balanced transfers[] (native HBAR) or tokenTransfers[] (HTS tokens). A Hedera accountId (0.0.x) is assigned at account creation and is not derivable from the key.
| Field | Value |
|---|---|
| coinType | 3030 |
| keyPath | m/44'/3030'/<accountIdx>' |
| amount unit | tinybar (1 HBAR = 1e8) |
| chainId | hedera:mainnet/slip44:3030 |
Supported Methods
| Method | Supported |
|---|---|
| getAddress | ✓ |
| signTransaction | ✓ |
| signMessage | ✗ |
| signTypedData | ✗ |
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.19.3 or higher | 1.0.0 or higher |
signTransaction | 2.19.3 or higher | 1.0.0 or higher |
getAddress — account address
Retrieves the account public key for a chainId + keyPath. Because a Hedera accountId (0.0.x) is assigned at account creation and is not derivable from the key, the device returns the derived public key in parameter — resolve the matching accountId from a mirror node before building a CryptoTransfer. Hedera has no address variants, so addressFormat does not apply.
| Field | Type | Required | Source | Description |
|---|---|---|---|---|
chainId | string | Required | App | CAIP-19 — hedera:mainnet/slip44:3030 |
keyPath | string | Required | App | BIP-44, default m/44'/3030'/0' |
Request
await dcent.getAddress({
chainId: 'hedera:mainnet/slip44:3030',
keyPath: "m/44'/3030'/0'"
})
Response
| Field | Type | Description |
|---|---|---|
address | string | The account public key derived from keyPath. Hedera does not return a 0.0.x accountId from the device — use this public key to look up the accountId on a mirror node. |
{
"header": {
"version": "1.0",
"status": "success"
},
"body": {
"command": "getAddress",
"parameter": {
"address": "302a30…"
}
}
}
The returned value is the account's public key, not a 0.0.x address. Resolve the accountId from a mirror node before populating transfers[].accountId / tokenTransfers[].accountId.
signTransaction <CryptoTransfer>
Variant — native HBAR transfer
| Field | Type | Required | Source | Description |
|---|---|---|---|---|
type | string | Required | App | Always "CryptoTransfer" |
transfers[].accountId | string | Required-Real | Wallet | Real Hedera account 0.0.x — sender side must be the device account (not key-derivable) |
transfers[].amount | number | Required | App | tinybar; negative = debit (sender), positive = credit (recipient); all entries sum to 0 |
memo | string | Optional | App | Transaction memo (empty string allowed) |
maxTransactionFee | number | Required | App | Fee cap in tinybar |
transactionValidDuration | number | Optional | App | Validity window in seconds (e.g. 120) |
Request:
await dcent.sign({
method: 'signTransaction',
chainId: 'hedera:mainnet/slip44:3030',
payload: {
keyPath: "m/44'/3030'/0'",
transaction: {
"type": "CryptoTransfer",
"transfers": [
{
"accountId": "0.0.587690",
"amount": -100000000
},
{
"accountId": "0.0.587690",
"amount": 100000000
}
],
"memo": "",
"maxTransactionFee": 100000000,
"transactionValidDuration": 120
}
}
})
Response
| Field | Type | Description |
|---|---|---|
header.status | string | "success" on completion; an error envelope otherwise |
body.command | string | "signTransaction" |
body.parameter.signature | string | The broadcast-ready serialized signed Hedera transaction (signed CryptoTransfer protobuf bytes), not a bare signature — submit it directly to a Hedera node |
{
"header": {
"status": "success"
},
"body": {
"command": "signTransaction",
"parameter": {
"signature": "<signed tx bytes>"
}
}
}
Variant — HTS token transfer
Token model: form-D descriptor (same pattern as TRON / Havah / Stacks). Always send a compact token descriptor instead of raw tokenTransfers[] (see the variant below) — the bridge builds the CryptoTransfer envelope itself, no node timestamp or registry lookup required. See Core Concepts → Token descriptors.
HTS (Hedera Token Service) transfers carry tokenTransfers[] instead of transfers[]. There is no native HBAR movement (no transfers field). Amounts are in the token's own base units — for SAUCE (6 decimals) 1.5 SAUCE = 1500000, not tinybar.
| Field | Type | Required | Source | Description |
|---|---|---|---|---|
type | string | Required | App | Always "CryptoTransfer" |
tokenTransfers[].tokenId | string | Required | App | HTS token id 0.0.x — a registered mainnet token resolves display metadata automatically; an unregistered token still signs, but use the token descriptor variant below instead of raw tokenTransfers[] |
tokenTransfers[].accountId | string | Required-Real | Wallet | Real Hedera account 0.0.x; recipient must have associated the token first |
tokenTransfers[].amount | number | Required | App | Token base units; negative/positive pair summing to 0 |
memo | string | Optional | App | Transaction memo |
maxTransactionFee | number | Required | App | Fee cap in tinybar |
Request:
await dcent.sign({
method: 'signTransaction',
chainId: 'hedera:mainnet/slip44:3030',
payload: {
keyPath: "m/44'/3030'/0'",
transaction: {
"type": "CryptoTransfer",
"tokenTransfers": [
{
"tokenId": "0.0.731861",
"accountId": "0.0.587690",
"amount": -1500000
},
{
"tokenId": "0.0.731861",
"accountId": "0.0.587690",
"amount": 1500000
}
],
"memo": "",
"maxTransactionFee": 100000000
}
}
})
Response
| Field | Type | Description |
|---|---|---|
header.status | string | "success" on completion; an error envelope otherwise |
body.command | string | "signTransaction" |
body.parameter.signature | string | The broadcast-ready serialized signed Hedera transaction (signed CryptoTransfer protobuf bytes carrying the HTS transfer), not a bare signature — submit it directly to a Hedera node |
{
"header": {
"status": "success"
},
"body": {
"command": "signTransaction",
"parameter": {
"signature": "<signed tx bytes>"
}
}
}
Variant — HTS form-D descriptor
Send a compact token descriptor instead of raw tokenTransfers[]. The bridge builds the balanced two-leg CryptoTransfer (negative sender / positive recipient) from { contract, to, amount } and signs — no HTS registry lookup, no node timestamp. from (or sender / owner_address) is read-only and identifies the debit leg — omitting it fails with -32602. amount is a base-unit number (not rescaled by decimals).
payload.transaction:
{
"token": {
"contract": "0.0.731861",
"to": "0.0.587690",
"amount": 1500000,
"decimals": 6,
"symbol": "SAUCE"
},
"from": "0.0.587690",
"maxTransactionFee": 100000000
}
| Field | Type | Required | Description |
|---|---|---|---|
token.contract | string | Required | HTS token id 0.0.x; unregistered tokens allowed |
token.to | string | Required | Recipient 0.0.x (must have associated the token) |
token.amount | number | Required | Base-unit number; not rescaled by decimals |
token.decimals | number | Optional | Display metadata only |
token.symbol | string | Optional | Display metadata only |
from | string | Required-Real | Device Hedera account that signs (read-only; sender / owner_address also accepted). Omitting it fails with -32602 |
Variant — unsignedTx (blind-sign passthrough)
Send a pre-built, frozen Hedera Transaction serialized to bytes (hex) under unsignedTx. The device blind-signs the bytes as-is — this is the only fully self-contained signTransaction form for Hedera; it covers any operation (TokenCreate / Mint / Burn / TransferTransaction / …), not just transfers. Missing or malformed bytes return -32602.
| Field | Type | Required | Source | Description |
|---|---|---|---|---|
unsignedTx | hex string | Required-Real | App | Serialized, frozen Hedera Transaction bytes (hex) built outside the device |
Request
await dcent.sign({
method: 'signTransaction',
chainId: 'hedera:mainnet/slip44:3030',
payload: {
keyPath: "m/44'/3030'/0'",
transaction: {
"unsignedTx": "0a9d012a9a010a95010a160a080880e2cfaa06100012080800100018aaef2318..."
}
}
})
signMessage Not supported — returns -32601
| Field | Type | Required | Source | Description |
|---|---|---|---|---|
keyPath | string | Required | Wallet | Signing path, default m/44'/3030'/0' |
message | string | Required | App | Message bytes to sign |
Request:
await dcent.sign({
method: 'signMessage',
chainId: 'hedera:mainnet/slip44:3030',
payload: {
keyPath: "m/44'/3030'/0'",
message: '<bytes>'
}
})
Response
| Field | Type | Description |
|---|---|---|
header.status | string | "success" on completion; an error envelope otherwise |
body.command | string | "signMessage" |
body.parameter.signature | string | The raw signature over the supplied message bytes (not a serialized transaction) |
{
"header": {
"status": "success"
},
"body": {
"command": "signMessage",
"parameter": {
"signature": "<signature>"
}
}
}
Supported transaction types
Hedera has two signing paths:
- Structured operation — pass a typed object and the bridge builds + signs the bytes:
TransferTransaction(HBAR + HTS transfers),TokenAssociate/TokenDissociate. - Blind-sign passthrough — build and freeze the transaction yourself (Hedera SDK) and pass the serialized
unsignedTxbytes; the device blind-signs any operation (TokenCreate / Mint / Burn / etc.). Matches v1getHederaSignedTransaction.
The structured ops the bridge builds are limited to transfer + associate / dissociate. Everything else (TokenCreate / Mint / Burn / Freeze / …, AccountCreate / Update) goes through blind-sign passthrough — the device shows raw bytes rather than a parsed summary.
Common Mistakes
Using a key-derived address as accountId — a Hedera accountId (0.0.x) is assigned at account creation, not derived from the key. Replace transfers[].accountId / tokenTransfers[].accountId with your real device Hedera account. Unbalanced transfers — the negative (sender) and positive (recipient) amounts must sum to exactly 0. Wrong amount unit for tokens — HTS tokenTransfers[].amount is in token base units (SAUCE = 6 decimals), not tinybar. Recipient has not associated the token — an HTS transfer fails unless the recipient ran TokenAssociate for that token first.