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 |
| amount unit | Planck (1 DOT = 1e10) |
| chainId | polkadot:91b171bb158e2d3848fa23a9f1c25182/slip44:354 |
Supported Methods
| Method | Supported |
|---|---|
| getAddress | ✓ |
| signTransaction | ✓ |
| signMessage | ✗ relay · ✓ parachains |
| signTypedData | ✗ |
| 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 below 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).
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.
| 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"
},
"body": {
"command": "getAddress",
"parameter": {
"address": "111111111111111111111111111111111HC1"
}
}
}
signTransaction <extrinsic>
The transaction is a Substrate extrinsic: method (pallet 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 | Pallet call, e.g. balances.transferAllowDeath |
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 — the playground preset's placeholder (all-zero / 0) is rejected by the network if sent as-is |
nonce | number | Required-Real | App | Real value from a live node — the playground preset's placeholder (all-zero / 0) is rejected by the network if sent as-is |
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 playground preset's placeholder (all-zero / 0) is rejected by the network if sent as-is |
transactionVersion | number | Required-Real | App | Real value from a live node — the playground preset's placeholder (all-zero / 0) is rejected by the network if sent as-is |
blockHash | hex | Required-Real | App | Real value from a live node — the playground preset's placeholder (all-zero / 0) is rejected by the network if sent as-is |
genesisHash | hex | Required-Real | App | Real value from a live node — the playground preset's placeholder (all-zero / 0) is rejected by the network if sent as-is |
fee | string | Required-Real | App | The fee your app estimated from a live node — this is what the device displays. Must be present and a non-negative, finite number; omitting it or sending a negative / non-numeric value fails with -32602. 0 passes the gate but makes the device show Fee 0 |
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": 9430,
"transactionVersion": 24,
"blockHash": "0x91b1…",
"genesisHash": "0x91b1…",
"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": {
"status": "success"
},
"body": {
"command": "signTransaction",
"parameter": {
"signature": "0x…"
}
}
}
Only method, args[0] and args[1] are consumed when building the extrinsic. era / nonce / specVersion / transactionVersion / blockHash / genesisHash must be real, currently-valid values fetched from a live node — the playground preset's all-zero placeholder is rejected by the network if sent as-is. Omitting any of them does not make the wallet self-derive a value — the request fails with -32602 before anything is signed.
Polkadot is the one exception to the no-RPC rule: 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 unsigned extrinsic)
Send the pre-encoded SCALE unsigned call/extrinsic body 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 method/args alongside extra.scaleHex is rejected with -32602 — "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 — measured (2026-07-22), only the recipient inside the blob was changed while args[0] was left as is, so the displayed field and the signed bytes pointed 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 unsigned call/extrinsic body, hex-prefixed 0x… |
| 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": 9430,
"transactionVersion": 24,
"blockHash": "0x91b1…",
"genesisHash": "0x91b1…",
"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) is not supported as a friendly, self-contained path. balances.transferAllowDeath/transferKeepAlive above move the native asset (DOT / ASTR / etc.) only. An assets.transfer(assetId, target, amount) extrinsic is only reachable by encoding it yourself into extra.scaleHex above and blind-signing it — the device shows no asset symbol or decimals, only the raw call bytes were verified. There is no descriptor form (no transaction.token) for Polkadot.
signMessage <raw bytes>
Parachains only. signMessage on the relay chain (polkadot:91b171bb…/slip44:354) returns -32601 — the device supports it for parachain currencies only (Astar, Shibuya, Creditcoin). Measured 2026-07-21.
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' |
Response
| Field | Type | Description |
|---|---|---|
body.parameter.signature | hex | Raw signature over the supplied bytes — a bare signature only, not a broadcast-ready transaction |
header.status | string | "success" on completion |
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 — only the chainId and 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 era / nonce / specVersion / etc. — 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] must be a Polkadot SS58 address (prefix 0), not a Kusama or generic-Substrate encoding.