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.
| Field | Value |
|---|---|
| Family | Polkadot (Substrate) |
| coinType | 354 |
| keyPath | m/44'/354'/<accountIdx>'/0/0 (Ledger account: m/44'/354'/<accountIdx>'/0'/0') |
| amount unit | Planck (1 DOT = 1e10) |
| chainId | polkadot:91b171bb158e2d3848fa23a9f1c25182/slip44:354 |
Supported Methods
| Method | Supported |
|---|---|
| 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.
| Method | DCENT Biometric Wallet | DCENT X |
|---|---|---|
getAddress | 2.19.1 or higher | 1.0.0 or higher |
signTransaction | 2.19.1 or higher | 1.0.0 or higher |
signMessage (parachains only) | 2.30.2 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. 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).
| Field | Type | Required | Source | Description |
|---|---|---|---|---|
chainId | string | Required | App | CAIP-19 — polkadot:91b171bb158e2d3848fa23a9f1c25182/slip44:354 |
keyPath | string | Required | App | BIP-44 path that derives the account |
Request
await dcent.getAddress({
chainId: 'polkadot:91b171bb158e2d3848fa23a9f1c25182/slip44:354',
keyPath: "m/44'/354'/0'/0/0"
})
Response
| Field | Type | Description |
|---|---|---|
body.parameter.address | string | SS58-encoded Polkadot address (network prefix 0), e.g. 111111111111111111111111111111111HC1 |
header.status | string | "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.
| Field | Type | Required | Source | Description |
|---|---|---|---|---|
method | string | Required | App | One 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 |
sender | string | Optional | Wallet | SS58 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] | string | Required-Real | App | Recipient SS58 address (Polkadot prefix 0) |
args[1] | string | Required | App | Amount in Planck (1 DOT = 1e10) |
era | string | Required-Real | App | Real value from a live node. 0x00 is a valid immortal era, but then blockHash must equal genesisHash — otherwise the request fails with -32602 |
nonce | number | Required-Real | App | The 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 |
tip | number | Required | App | Priority tip in Planck — send 0 for none. Omitting the field fails with -32602 |
specVersion | number | Required-Real | App | Real 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 |
transactionVersion | number | Required-Real | App | Real 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 |
blockHash | hex | Required-Real | App | Real 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 |
genesisHash | hex | Required-Real | App | Real 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 |
fee | string | Required-Real | App | The 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
| Field | Type | Description |
|---|---|---|
body.parameter.signature | hex | Broadcast-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.command | string | "signTransaction" |
header.status | string | "success" on completion |
{
"header": {
"version": "1.0",
"status": "success",
"response_from": "signTransaction"
},
"body": {
"command": "signTransaction",
"parameter": {
"signature": "0x…"
}
}
}
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.
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.
| Field | Type | Required | Source | Description |
|---|---|---|---|---|
extra.scaleHex | hex string | Required-Real | App | SCALE-encoded call (pallet index + call index + args), hex-prefixed 0x… — not a full extrinsic |
| all eight top-level fields | — | Required | App | era / 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"
}
}
}
})
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.
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.
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>
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
| Field | Type | Required | Source | Description |
|---|---|---|---|---|
keyPath | string | Required-Real | Wallet | Path that signs the message |
message | hex string | Required | App | Raw bytes to sign (hex recommended) |
meta.kind | enum | Optional | App | '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
| Field | Type | Description |
|---|---|---|
body.parameter.signature | hex | Signature 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.status | string | "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.
| keyPath | Account |
|---|---|
m/44'/354'/<accountIdx>'/0/0 | DCENT 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
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.