Skip to main content

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.

FieldValue
coinType3030
keyPathm/44'/3030'/<accountIdx>'
amount unittinybar (1 HBAR = 1e8)
chainIdhedera:mainnet/slip44:3030
chainId · Testnethedera:testnet/slip44:3030

Supported Methods​

MethodSupported
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.

MethodDCENT Biometric WalletDCENT X
getAddress2.19.3 or higher1.0.0 or higher
signTransaction2.19.3 or higher1.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).

FieldTypeRequiredSourceDescription
chainIdstringRequiredAppCAIP-19 — hedera:mainnet/slip44:3030
keyPathstringRequiredAppBIP-44, default m/44'/3030'/0'

Request

await dcent.getAddress({
chainId: 'hedera:mainnet/slip44:3030',
keyPath: "m/44'/3030'/0'"
})

Response

FieldTypeDescription
body.parameter.addressstringThe 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>"
}
}
}
info

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​

FieldTypeRequiredSourceDescription
typestringRequiredAppAlways "CryptoTransfer"
transfers[].accountIdstringRequired-RealWalletReal Hedera account 0.0.x — sender side should be the device account (not key-derivable; the bridge does not check it)
transfers[].amountnumberRequiredApptinybar; 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)
memostringOptionalAppTransaction memo (empty string allowed)
maxTransactionFeenumberOptionalAppFee 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.hederaValidStartobjectOptionalApp{ 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

FieldTypeDescription
header.statusstring"success" on completion; an error envelope otherwise
body.commandstring"signTransaction"
body.parameter.signaturestringThe 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​

info

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.

FieldTypeRequiredSourceDescription
typestringOptionalApp"CryptoTransfer" — not checked on this shape
tokenTransfers[].tokenIdstringRequiredAppHTS 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[].accountIdstringRequired-RealWalletReal Hedera account 0.0.x; the recipient must be associated with the token
tokenTransfers[].amountnumberRequiredAppToken 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)
memostringOptionalAppTransaction memo
maxTransactionFeenumberOptionalAppFee 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

FieldTypeDescription
header.statusstring"success" on completion; an error envelope otherwise
body.commandstring"signTransaction"
body.parameter.signaturestringThe 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
}
FieldTypeRequiredDescription
token.contractstringRequiredHTS token id 0.0.x; unregistered tokens allowed
token.tostringRequiredRecipient 0.0.x (must have associated the token)
token.amountnumberRequiredBase-unit number; not rescaled by decimals. Must not exceed 2^53−1 (otherwise -32602)
token.decimalsnumberOptional (Required for an unregistered token)Display metadata. An unregistered token without token.decimals is rejected with -32602
token.symbolstringOptionalDisplay metadata only
fromstringRequired-RealDevice 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.

FieldTypeRequiredSourceDescription
unsignedTxhex stringRequired-RealAppSerialized, 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​

FieldTypeRequiredSourceDescription
keyPathstringRequiredWalletSigning path, default m/44'/3030'/0'
messagestringRequiredAppMessage 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), and TokenAssociate / TokenDissociate as { type: 'associate' | 'disassociate', tokenId, sender, memo?, maxTransactionFee? } (sender — your 0.0.x account — is required; without it the request fails with -32602) (maxTransactionFee defaults to 150000000). Associate / dissociate work only for a token on DCENT's supported list — any other tokenId → -32602 (use unsignedTx).
  • Pre-built transaction passthrough — build and freeze the transaction yourself (Hedera SDK) and pass the serialized unsignedTx bytes (supported types on DCENT X: see the unsignedTx variant above). Pass the frozen transaction's serialized bytes (tx.toBytes(), hex) as unsignedTx — unlike the v1 guide's getHederaSignedTransaction example, which sent the hex of _signedTransactions[0].bodyBytes; body-only bytes are rejected with -32602.
info

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​

danger

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).