Skip to main content

Polkadot (DOT)

Sign Polkadot extrinsics and raw messages. You describe the extrinsic with a method and args; the app supplies the live runtime context (nonce, fee, era, spec version) — the bridge never fetches them.

FieldValue
FamilyPolkadot (Substrate)
coinType354
keyPathm/44'/354'/<accountIdx>'/0/0 (Ledger account: m/44'/354'/<accountIdx>'/0'/0')
amount unitPlanck (1 DOT = 1e10)
chainIdpolkadot:91b171bb158e2d3848fa23a9f1c25182/slip44:354

Supported Methods​

MethodSupported
getAddress✓
signTransaction✓
signMessage✗ DOT / Paseo · ✓ parachains
signTypedData✗
signData✗
signAuthEntry✗

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.1 or higher1.0.0 or higher
signTransaction2.19.1 or higher1.0.0 or higher
signMessage (parachains only)2.30.2 or higher1.0.0 or higher
info

Firmware versions above are for the DCENT Biometric Wallet line — DCENT X supports them from 1.0.0. Astar and other Substrate parachains are a separate device family (Parachain), added in firmware 2.30.0 — distinct from Polkadot (DOT). The Ledger accounts need DCENT Biometric Wallet firmware 2.36.2 or higher and DCENT X firmware 1.0.1 or higher.

getAddress — account address​

Retrieves the Polkadot account address for a chainId + keyPath. The returned address is an SS58-encoded string using the Polkadot network prefix (0). Call this before building an extrinsic so you can confirm the sender. Polkadot has a single address format, so no addressFormat is needed — the keyPath picks the account (see Ledger accounts).

FieldTypeRequiredSourceDescription
chainIdstringRequiredAppCAIP-19 — polkadot:91b171bb158e2d3848fa23a9f1c25182/slip44:354
keyPathstringRequiredAppBIP-44 path that derives the account

Request

await dcent.getAddress({
chainId: 'polkadot:91b171bb158e2d3848fa23a9f1c25182/slip44:354',
keyPath: "m/44'/354'/0'/0/0"
})

Response

FieldTypeDescription
body.parameter.addressstringSS58-encoded Polkadot address (network prefix 0), e.g. 111111111111111111111111111111111HC1
header.statusstring"success" on completion
{
"header": {
"version": "1.0",
"status": "success",
"response_from": "getAddress"
},
"body": {
"command": "getAddress",
"parameter": {
"address": "111111111111111111111111111111111HC1"
}
}
}

signTransaction <extrinsic>​

The transaction is a Substrate extrinsic: method (a balances transfer call, e.g. balances.transferAllowDeath) plus args. For balances.transferAllowDeath, args[0] is the recipient SS58 address and args[1] is the amount in Planck. The sender is derived from keyPath on the device — the bridge does not look the account up on a node.

FieldTypeRequiredSourceDescription
methodstringRequiredAppOne of balances.transfer, balances.transferKeepAlive, balances.transferAllowDeath. Any other value fails with -32602 — for other calls use the extra.scaleHex variant below (for DOT, DCENT X reads the recipient and amount at a balances-transfer's fixed offsets, so its screen does not describe any other call — treat it as blind-signing). balances.transfer is signed as transferAllowDeath
senderstringOptionalWalletSS58 address of the signing account. If it is a string, it must decode to the same public key as the keyPath account (any SS58 prefix), otherwise -32602
args[0]stringRequired-RealAppRecipient SS58 address (Polkadot prefix 0)
args[1]stringRequiredAppAmount in Planck (1 DOT = 1e10)
erastringRequired-RealAppReal value from a live node. 0x00 is a valid immortal era, but then blockHash must equal genesisHash — otherwise the request fails with -32602
noncenumberRequired-RealAppThe account's current nonce from a live node (0 only for an account that has never sent a transaction). The bridge checks only its format, not its value against the chain
tipnumberRequiredAppPriority tip in Planck — send 0 for none. Omitting the field fails with -32602
specVersionnumberRequired-RealAppReal value from a live node. The bridge checks only its format, not its value against the chain — do not send the playground preset's all-zero placeholder
transactionVersionnumberRequired-RealAppReal value from a live node. The bridge checks only its format, not its value against the chain — do not send the playground preset's all-zero placeholder
blockHashhexRequired-RealAppReal value from a live node. The bridge checks only its format, not its value against the chain — do not send the playground preset's all-zero placeholder
genesisHashhexRequired-RealAppReal value from a live node. The bridge checks only its format, not its value against the chain — do not send the playground preset's all-zero placeholder
feestringRequired-RealAppThe fee your app estimated from a live node — sent to the device as the fee to display; it is not part of the signed payload. Must be present and a non-negative, finite number; omitting it, or a value whose string form (String(fee)) is negative, non-finite or not a number, fails with -32602. 0 passes the gate and is sent as the displayed fee

Request

await dcent.sign({
method: 'signTransaction',
chainId: 'polkadot:91b171bb158e2d3848fa23a9f1c25182/slip44:354',
payload: {
keyPath: "m/44'/354'/0'/0/0",
transaction: {
"method": "balances.transferAllowDeath",
"args": [ "111111111111111111111111111111111HC1", "1000000000000" ],
"era": "0x00",
"nonce": 0,
"tip": 0,
"specVersion": "<from the node>",
"transactionVersion": "<from the node>",
"blockHash": "<from the node>",
"genesisHash": "<from the node>",
"fee": "158000000"
}
}
})

Response

FieldTypeDescription
body.parameter.signaturehexBroadcast-ready signed extrinsic for Polkadot (SCALE-encoded), ready to submit via author_submitExtrinsic — this is the full serialized signed transaction, not a bare signature
body.commandstring"signTransaction"
header.statusstring"success" on completion
{
"header": {
"version": "1.0",
"status": "success",
"response_from": "signTransaction"
},
"body": {
"command": "signTransaction",
"parameter": {
"signature": "0x…"
}
}
}
info

The call is built from method, args[0] and args[1] (sender, if sent as a string, must match the keyPath account). On Astar and Shibuya, a transaction.token whose contract is a registered XC20 asset id turns a balances.* request into assets.transferKeepAlive (for balances.transferKeepAlive) or assets.transfer (otherwise) of that asset, using args[0] / args[1]; on DOT, Paseo, Creditcoin and Creditcoin Testnet a transaction.token with a contract fails with -32602. Leave token out for a native transfer, and when sending an extra.scaleHex blob (the blob is signed as sent). era / nonce / specVersion / transactionVersion / blockHash / genesisHash must be real, currently-valid values fetched from a live node (tip and fee are also required; tip may be 0) — do not send the playground preset's all-zero placeholders as-is (era 0x00 is the exception: it is a valid immortal era when blockHash equals genesisHash). Omitting any of them does not make the wallet self-derive a value — the request fails with -32602 before anything is signed.

Polkadot makes an RPC call on the signing path: the bridge reads runtime metadata for display safety, because a Polkadot payload hash cannot be constructed without it. It does not fetch block headers, and it never re-derives the consensus fields — era, nonce, blockHash and the rest still come from your app.

Variant — extra.scaleHex (SCALE-encoded call)​

Send the pre-encoded SCALE call (pallet index + call index + args — not a full extrinsic) under extra.scaleHex instead of method/args. extra.scaleHex is the byte-authoritative field the device signs; the top-level payload fields listed under signTransaction above are still required.

danger

Form A0 and Form B are mutually exclusive. Sending both method and args alongside extra.scaleHex is rejected with -32602 (a lone method name or a lone args next to the blob is ignored and the blob is signed; a method holding SCALE hex (0x…) is rejected with -32602 — put the blob in extra.scaleHex) — "Polkadot accepts either 'extra.scaleHex' (Form A0) or 'method'+'args' (Form B), not both — remove one". The reason is drift: when the same recipient/amount lives in two places the two copies diverge — if only one copy is updated, the displayed field and the signed bytes point at different recipients. The wallet refuses rather than picking a winner. If you send the blob, treat it as blind-signing.

FieldTypeRequiredSourceDescription
extra.scaleHexhex stringRequired-RealAppSCALE-encoded call (pallet index + call index + args), hex-prefixed 0x… — not a full extrinsic
all eight top-level fields—RequiredAppera / nonce / tip / fee / specVersion / transactionVersion / blockHash / genesisHash — the blob encodes only the call, so every one of them is still sent as a top-level payload field (see the signTransaction table above)

Request

await dcent.sign({
method: 'signTransaction',
chainId: 'polkadot:91b171bb158e2d3848fa23a9f1c25182/slip44:354',
payload: {
keyPath: "m/44'/354'/0'/0/0",
transaction: {
"era": "0x00",
"nonce": 0,
"tip": 0,
"fee": "158000000",
"specVersion": "<from the node>",
"transactionVersion": "<from the node>",
"blockHash": "<from the node>",
"genesisHash": "<from the node>",
"extra": {
"scaleHex": "0x0a00000000000000000000000000000000000000000000000000000000000000000000070010a5d4e8"
}
}
}
})
warning

The blob is chain-specific — not a portable representative encoding. A SCALE call begins with (pallet index, call index), and the pallet index differs per runtime. Encode extra.scaleHex against the chain the bridge actually signs on: for DOT that is Polkadot Asset Hub (Balances = pallet 10 → 0x0a00…), not the relay chain (pallet 5 → 0x0500…). The same bytes decode to a different call on the other chain — a relay-encoded balances.transferAllowDeath reads as preimage.notePreimage on Asset Hub, and it signs without error.

info

Missing or malformed extra.scaleHex returns -32602. This is the byte-authoritative field the device signs, but it only encodes the call (pallet + args) — era/nonce/tip/fee/specVersion/transactionVersion/blockHash/genesisHash are not inside the blob and must still be sent as top-level payload fields with real values, exactly as in the decoded form above.

warning

Non-native token transfer (Assets pallet). The balances.* methods above move the native asset (DOT / ASTR / etc.) unless a registered XC20 transaction.token is sent on Astar / Shibuya (see above). To send an Assets-pallet transfer, you can encode assets.transfer(assetId, target, amount) yourself into extra.scaleHex above — for DOT, DCENT X reads the recipient and amount at a balances-transfer's fixed offsets, so its screen does not describe any other call — treat it as blind-signing. Polkadot has no form-D descriptor builder.

signMessage <raw bytes>​

warning

Parachains only. signMessage on polkadot:91b171bb…/slip44:354 (DOT) and on Paseo returns -32601 — the wallet allows it for the parachain currencies only (Astar, Shibuya, Creditcoin and Creditcoin Testnet).

await dcent.sign({
method: 'signMessage',
chainId: 'polkadot:9eb76c5184c4ab8679d2d5d819fdf90b/slip44:810',
payload: {
keyPath: "m/44'/810'/0'/0/0",
message: '0x48656c6c6f',
meta: {
kind: 'raw'
}
}
})
// → body.parameter.signature
FieldTypeRequiredSourceDescription
keyPathstringRequired-RealWalletPath that signs the message
messagehex stringRequiredAppRaw bytes to sign (hex recommended)
meta.kindenumOptionalApp'raw' (default) or 'personal' — both sign the bytes as-is. 'eip712' fails with -32602 without a valid meta.version (V1 / V3 / V4), or -32601 with one; any other non-null value fails with -32602

Response

FieldTypeDescription
body.parameter.signaturehexSignature over the supplied bytes, prefixed with a 0x00 type byte (Ed25519 in Substrate's MultiSignature encoding) — strip that byte before verifying the raw signature. Not a broadcast-ready transaction
header.statusstring"success" on completion

Ledger accounts​

Polkadot, Astar, Shibuya, Creditcoin and Creditcoin Testnet each have a second, Ledger account derived with Ledger's BIP32-Ed25519 scheme. Its keyPath hardens the last two levels — m/44'/354'/<accountIdx>'/0'/0' instead of m/44'/354'/<accountIdx>'/0/0 (810' in place of 354' on Astar and Shibuya) — and that keyPath alone selects it, so no addressFormat is needed. Paseo (polkadot:d6eec26135305a8ad257a20d00335728/slip44:354, Polkadot Ledger testnet) has only the Ledger account — use m/44'/354'/<accountIdx>'/0'/0'; …/0/0 is rejected with -32602.

keyPathAccount
m/44'/354'/<accountIdx>'/0/0DCENT default account
m/44'/354'/<accountIdx>'/0'/0'Ledger account (BIP32-Ed25519)

getAddress, signTransaction and, on the parachains, signMessage all follow the keyPath. You may add addressFormat: 'ledger' to a Ledger-account keyPath — it names the same account; on the default …/0/0 keyPath it is rejected with -32602 by getAddress / signTransaction. signMessage does not read addressFormat — it follows the keyPath only. Ledger accounts need DCENT Biometric Wallet firmware 2.36.2 or higher and DCENT X firmware 1.0.1 or higher.

Substrate networks — Astar, Shibuya, Creditcoin​

Astar, Shibuya (Astar's testnet) and Creditcoin (plus its testnet) sign through the same signTransaction / balances.transferAllowDeath path shown above — the chainId, the keyPath coinType (810' for Astar / Shibuya, 354' for Creditcoin) and the SS58 self-address encoding change. Unlike DOT (1 DOT = 1e10 Planck) these sidechains use 18 decimals — args[1] for 1 token is 1000000000000000000. See Supported Substrate Networks for the full chainId / keyPath / SS58-prefix table (including Creditcoin Testnet).

Common Mistakes​

danger

Amount not in Planck — 1 DOT = 1e10 Planck (not 1e12). args[1] is a Planck string. Sender not reachable / not on-chain — the bridge does not derive nonce / fee / era from the keyPath account; every one of those values comes from the app. Sending a placeholder (0/all-zero) or stale value for nonce / specVersion / etc. (era 0x00 is valid only as an immortal era with blockHash = genesisHash) — the node rejects it. Omitting a field entirely does not make the wallet fill it in — the request fails with -32602 and nothing is signed. Fetch fresh real values from a live node before every sign. Wrong address format — args[0] is read as an SS58 address and its network prefix is not checked (any prefix of the same key signs the same bytes), so make sure it encodes the right recipient key.