Hedera (HBAR)
Sign Hedera transactions. 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 |
| chainId · Testnet | hedera:testnet/slip44:3030 |
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.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 — omit addressFormat (ledger is rejected with -32602; legacy, segwit-wrapped, segwit-native, taproot and standard have no effect).
| 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 |
|---|---|---|
body.parameter.address | string | The account public key derived from keyPath — on DCENT X, the raw 32-byte Ed25519 public key as hex (64 characters, not DER-encoded). 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",
"response_from": "getAddress"
},
"body": {
"command": "getAddress",
"parameter": {
"address": "<64 hex characters>"
}
}
}
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 should be the device account (not key-derivable; the bridge does not check it) |
transfers[].amount | number | Required | App | tinybar; negative = debit (sender), positive = credit (recipient). Exactly two legs — one negative (sender) and one positive (recipient) — summing to 0; anything else → -32602. Sender and recipient should be different accounts — the bridge does not check this (same-account legs are merged into a single 0 entry). An amount sent as a JSON number must be a safe integer (otherwise -32602) |
memo | string | Optional | App | Transaction memo (empty string allowed) |
maxTransactionFee | number | Optional | App | Fee cap in tinybar. Default 3000000 (0.03 HBAR); a negative or non-finite value, or one that does not parse as a number, → -32602 |
extra.hederaValidStart | object | Optional | App | { seconds, nanos } used as the transaction's valid start. If omitted, the bridge uses the local clock minus 8–13 s. The transaction expires 120 s after its valid start |
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.1234567",
"amount": 100000000
}
],
"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 serialized signed Hedera transaction (signed CryptoTransfer protobuf bytes) as hex without 0x, not a bare signature — decode with Transaction.fromBytes(Buffer.from(signature, 'hex')) and submit it to a Hedera node |
{
"header": {
"version": "1.0",
"status": "success",
"response_from": "signTransaction"
},
"body": {
"command": "signTransaction",
"parameter": {
"signature": "<signed tx bytes>"
}
}
}
Variant — HTS token transfer
Token model: form-D descriptor (same pattern as TRON / Havah / Stacks). A registered token can be sent as raw tokenTransfers[] or as a compact token descriptor; an unregistered token must use the descriptor with token.decimals. With the descriptor the bridge builds the CryptoTransfer envelope itself. Neither form needs a node timestamp. 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 | Optional | App | "CryptoTransfer" — not checked on this shape |
tokenTransfers[].tokenId | string | Required | App | HTS token id 0.0.x — a registered mainnet token resolves display metadata automatically; an unregistered token in raw tokenTransfers[] is rejected with -32602 — use the token descriptor variant below with token.decimals |
tokenTransfers[].accountId | string | Required-Real | Wallet | Real Hedera account 0.0.x; the recipient must be associated with the token |
tokenTransfers[].amount | number | Required | App | Token base units. Exactly two legs — one negative (sender) and one positive (recipient) — summing to 0; anything else → -32602. Sender and recipient should be different accounts — the bridge does not check this (same-account legs are merged into a single 0 entry). The amount must not exceed 2^53−1, even as a string (otherwise -32602) |
memo | string | Optional | App | Transaction memo |
maxTransactionFee | number | Optional | App | Fee cap in tinybar. Default 3000000 (0.03 HBAR); a negative or non-finite value, or one that does not parse as a number, → -32602 |
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.1234567",
"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 serialized signed Hedera transaction (signed CryptoTransfer protobuf bytes carrying the HTS transfer) as hex without 0x, not a bare signature — submit it to a Hedera node |
{
"header": {
"version": "1.0",
"status": "success",
"response_from": "signTransaction"
},
"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 node timestamp needed (an unregistered token needs token.decimals). from (or sender / from_address / fromAddress / owner_address / ownerAddress) 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.1234567",
"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. Must not exceed 2^53−1 (otherwise -32602) |
token.decimals | number | Optional (Required for an unregistered token) | Display metadata. An unregistered token without token.decimals is rejected with -32602 |
token.symbol | string | Optional | Display metadata only |
from | string | Required-Real | Device Hedera account that signs (read-only; sender / from_address / fromAddress / owner_address / ownerAddress also accepted). Omitting it fails with -32602 |
Variant — unsignedTx (pre-built transaction passthrough)
Send a pre-built, frozen Hedera Transaction serialized to bytes (hex) under unsignedTx (unsigned_tx and a 0x prefix are also accepted; the whole serialized payload must stay within the connector's 65,536-character limit — above it sign() rejects with a ProviderError (-32602) before the popup opens). The bytes are signed as sent. DCENT X parses CryptoTransfer, CryptoCreate / CryptoUpdate, TokenCreate / TokenUpdate, TokenAssociate / TokenDissociate, TokenMint / TokenBurn, TokenFreeze / TokenUnfreeze, TokenPause / TokenUnpause and TokenWipe; other transaction types fail on the device. 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>'
}
})
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), andTokenAssociate/TokenDissociateas{ type: 'associate' | 'disassociate', tokenId, sender, memo?, maxTransactionFee? }(sender— your0.0.xaccount — is required; without it the request fails with-32602) (maxTransactionFeedefaults to150000000). Associate / dissociate work only for a token on DCENT's supported list — any othertokenId→-32602(useunsignedTx). - Pre-built transaction passthrough — build and freeze the transaction yourself (Hedera SDK) and pass the serialized
unsignedTxbytes (supported types on DCENT X: see theunsignedTxvariant above). Pass the frozen transaction's serialized bytes (tx.toBytes(), hex) asunsignedTx— unlike the v1 guide'sgetHederaSignedTransactionexample, which sent the hex of_signedTransactions[0].bodyBytes; body-only bytes are rejected with-32602.
The structured ops the bridge builds are limited to transfer + associate / dissociate. Everything else (TokenCreate / Mint / Burn / Freeze / …, CryptoCreate / CryptoUpdate) goes through the unsignedTx passthrough.
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 — the recipient account must be associated with the token (the bridge does not check this; use the structured associate operation (see Supported transaction types above) to associate your own account).